feat: Build Gemini TTS tool for voice output in The Nexus
This commit is contained in:
2787
public/nexus/app.js
Normal file
2787
public/nexus/app.js
Normal file
File diff suppressed because it is too large
Load Diff
301
public/nexus/index.html
Normal file
301
public/nexus/index.html
Normal file
@@ -0,0 +1,301 @@
|
||||
<!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 & Heartbeat -->
|
||||
<div class="hud-top-left">
|
||||
<div id="debug-overlay" class="hud-debug"></div>
|
||||
<div id="nexus-heartbeat" class="hud-heartbeat" title="Nexus Pulse">
|
||||
<div class="heartbeat-pulse"></div>
|
||||
<div class="heartbeat-label">NEXUS PULSE</div>
|
||||
<div id="heartbeat-value" class="heartbeat-value">0.00 Hz</div>
|
||||
</div>
|
||||
</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="voice-toggle-btn" class="hud-icon-btn" title="Toggle Voice Output">
|
||||
<span class="hud-icon">🔊</span>
|
||||
<span class="hud-btn-label">VOICE</span>
|
||||
</button>
|
||||
<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 class="hud-symbolic-log" id="hud-symbolic-log" aria-label="Sovereign Symbolic Engine">
|
||||
<div class="symbolic-log-header">SYMBOLIC REASONING</div>
|
||||
<div id="symbolic-log-content" class="symbolic-log-content"></div>
|
||||
</div>
|
||||
<div class="hud-blackboard-log" id="hud-blackboard-log" aria-label="Blackboard Architecture">
|
||||
<div class="blackboard-log-header">BLACKBOARD (SHARED MEMORY)</div>
|
||||
<div id="blackboard-log-content" class="blackboard-log-content"></div>
|
||||
</div>
|
||||
<div class="hud-planner-log" id="hud-planner-log" aria-label="Symbolic Planner">
|
||||
<div class="planner-log-header">SYMBOLIC PLANNER (STRIPS)</div>
|
||||
<div id="planner-log-content" class="planner-log-content"></div>
|
||||
</div>
|
||||
<div class="hud-cbr-log" id="hud-cbr-log" aria-label="Case-Based Reasoner">
|
||||
<div class="cbr-log-header">CASE-BASED REASONER (CBR)</div>
|
||||
<div id="cbr-log-content" class="cbr-log-content"></div>
|
||||
</div>
|
||||
<div class="hud-neuro-log" id="hud-neuro-log" aria-label="Neuro-Symbolic Bridge">
|
||||
<div class="neuro-log-header">NEURO-SYMBOLIC PERCEPTION</div>
|
||||
<div id="neuro-bridge-log-content" class="neuro-log-content"></div>
|
||||
</div>
|
||||
<div class="hud-meta-log" id="hud-meta-log" aria-label="Meta-Reasoning Layer">
|
||||
<div class="meta-log-header">META-REASONING LAYER</div>
|
||||
<div id="meta-log-content" class="meta-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>
|
||||
1083
public/nexus/style.css
Normal file
1083
public/nexus/style.css
Normal file
File diff suppressed because it is too large
Load Diff
356
src/App.tsx
Normal file
356
src/App.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { gitea } from './services/gitea';
|
||||
import { geminiTTS } from './services/geminiTTS';
|
||||
import { parseRepoUrl, fetchRepoInfo, analyzeRepo } from './services/repoReviewer';
|
||||
import { Navigation } from './components/Navigation';
|
||||
import { TowerMode } from './components/TowerMode';
|
||||
import { BackupMode } from './components/BackupMode';
|
||||
import { ReviewerMode } from './components/ReviewerMode';
|
||||
import { NexusMode } from './components/NexusMode';
|
||||
import { ErrorToast } from './components/ErrorToast';
|
||||
import { REPOS_TO_BACKUP } from './constants';
|
||||
import { AppMode, RepoData, ReviewResult, WorldState, GiteaIssue, TabType, ProfileData } from './types';
|
||||
|
||||
// Initialize Gemini for Avatar generation
|
||||
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
||||
|
||||
export default function App() {
|
||||
const [mode, setMode] = useState<AppMode>('tower');
|
||||
const [url, setUrl] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [repoData, setRepoData] = useState<RepoData | null>(null);
|
||||
const [review, setReview] = useState<ReviewResult | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabType>('summary');
|
||||
|
||||
// Tower State
|
||||
const [worldState, setWorldState] = useState<WorldState | null>(null);
|
||||
const [issues, setIssues] = useState<GiteaIssue[]>([]);
|
||||
const [matrixPrompt, setMatrixPrompt] = useState('');
|
||||
const [avatarPrompt, setAvatarPrompt] = useState('');
|
||||
const [generatingAvatar, setGeneratingAvatar] = useState(false);
|
||||
const [updatingWorld, setUpdatingWorld] = useState(false);
|
||||
const [updatingProfile, setUpdatingProfile] = useState(false);
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
const [profileData, setProfileData] = useState<ProfileData>({
|
||||
full_name: '',
|
||||
location: '',
|
||||
website: '',
|
||||
description: ''
|
||||
});
|
||||
const [towerMissing, setTowerMissing] = useState(false);
|
||||
const [initializingTower, setInitializingTower] = useState(false);
|
||||
const [connectionStatus, setConnectionStatus] = useState<any>(null);
|
||||
const [remoteConnectionStatus, setRemoteConnectionStatus] = useState<any>(null);
|
||||
|
||||
// Backup State
|
||||
const [backingUp, setBackingUp] = useState<string | null>(null);
|
||||
const [backupSuccess, setBackupSuccess] = useState<string[]>([]);
|
||||
const [giteaRepos, setGiteaRepos] = useState<any[]>([]);
|
||||
const [fetchingRepos, setFetchingRepos] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [basePath, setBasePath] = useState('~/Documents/repos');
|
||||
const [githubToken, setGithubToken] = useState('');
|
||||
const [useDeepMigration, setUseDeepMigration] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data?.type === 'NEXUS_TTS') {
|
||||
geminiTTS.speak(event.data.text, event.data.voice);
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkGitea();
|
||||
if (mode === 'tower') {
|
||||
fetchTowerData();
|
||||
} else if (mode === 'backup') {
|
||||
fetchGiteaRepos();
|
||||
}
|
||||
}, [mode]);
|
||||
|
||||
const checkGitea = async () => {
|
||||
try {
|
||||
const status = await gitea.checkConnection(false);
|
||||
setConnectionStatus(status);
|
||||
const remoteStatus = await gitea.checkConnection(true);
|
||||
setRemoteConnectionStatus(remoteStatus);
|
||||
} catch (e) {
|
||||
setConnectionStatus({ status: 'error', message: 'Failed to reach proxy' });
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTowerData = async () => {
|
||||
setLoading(true);
|
||||
setTowerMissing(false);
|
||||
try {
|
||||
const [state, issueList, user] = await Promise.all([
|
||||
gitea.getWorldState(),
|
||||
gitea.getIssues(),
|
||||
gitea.getUserInfo()
|
||||
]);
|
||||
setWorldState(state);
|
||||
setIssues(issueList);
|
||||
setUserInfo(user);
|
||||
setProfileData({
|
||||
full_name: user.full_name || '',
|
||||
location: user.location || '',
|
||||
website: user.website || '',
|
||||
description: user.description || ''
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('404')) {
|
||||
setTowerMissing(true);
|
||||
} else {
|
||||
setError(`Failed to fetch Tower data: ${e.message}`);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInitializeTower = async () => {
|
||||
setInitializingTower(true);
|
||||
try {
|
||||
await gitea.initializeTower();
|
||||
await fetchTowerData();
|
||||
} catch (e: any) {
|
||||
setError(`Failed to initialize Tower: ${e.message}`);
|
||||
} finally {
|
||||
setInitializingTower(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMatrixSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!matrixPrompt.trim()) return;
|
||||
|
||||
setUpdatingWorld(true);
|
||||
try {
|
||||
await gitea.createIssue(`Matrix Prompt: ${matrixPrompt.slice(0, 50)}...`, matrixPrompt);
|
||||
if (worldState) {
|
||||
const newState = {
|
||||
...worldState,
|
||||
tower: { ...worldState.tower, energy: Math.min(100, worldState.tower.energy + 5) },
|
||||
matrix: { ...worldState.matrix, stability: Math.min(1.0, worldState.matrix.stability + 0.01) },
|
||||
last_updated: new Date().toISOString()
|
||||
};
|
||||
await gitea.updateWorldState(newState);
|
||||
setWorldState(newState);
|
||||
}
|
||||
setMatrixPrompt('');
|
||||
const updatedIssues = await gitea.getIssues();
|
||||
setIssues(updatedIssues);
|
||||
} catch (e: any) {
|
||||
setError(`Failed to update Matrix: ${e.message}`);
|
||||
} finally {
|
||||
setUpdatingWorld(false);
|
||||
}
|
||||
};
|
||||
|
||||
const generateAvatar = async () => {
|
||||
if (!avatarPrompt.trim()) return;
|
||||
setGeneratingAvatar(true);
|
||||
try {
|
||||
const prompt = `Generate a high-quality, professional avatar for a "Wizard of Automation" or a "Matrix Agent" based on this description: ${avatarPrompt}. The style should be futuristic, digital, and clean. Return only the image.`;
|
||||
const response = await ai.models.generateContent({
|
||||
model: "gemini-2.5-flash-image",
|
||||
contents: prompt,
|
||||
config: {
|
||||
imageConfig: { aspectRatio: "1:1", imageSize: "1K" }
|
||||
}
|
||||
});
|
||||
|
||||
let base64Image = '';
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
base64Image = part.inlineData.data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!base64Image) throw new Error('Failed to generate image data');
|
||||
await gitea.updateAvatar(base64Image);
|
||||
const user = await gitea.getUserInfo();
|
||||
setUserInfo(user);
|
||||
setAvatarPrompt('');
|
||||
} catch (e: any) {
|
||||
setError(`Failed to generate avatar: ${e.message}`);
|
||||
} finally {
|
||||
setGeneratingAvatar(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProfileUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setUpdatingProfile(true);
|
||||
try {
|
||||
await gitea.updateProfile(profileData);
|
||||
const user = await gitea.getUserInfo();
|
||||
setUserInfo(user);
|
||||
setError(null);
|
||||
} catch (e: any) {
|
||||
setError(`Failed to update profile: ${e.message}`);
|
||||
} finally {
|
||||
setUpdatingProfile(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchGiteaRepos = async () => {
|
||||
setFetchingRepos(true);
|
||||
try {
|
||||
const repos = await gitea.listRepos(true);
|
||||
setGiteaRepos(repos);
|
||||
const existingNames = repos.map((r: any) => r.name);
|
||||
setBackupSuccess(prev => [...new Set([...prev, ...existingNames])]);
|
||||
} catch (e: any) {
|
||||
console.error('Failed to fetch Gitea repos:', e);
|
||||
} finally {
|
||||
setFetchingRepos(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackup = async (repo: typeof REPOS_TO_BACKUP[0]) => {
|
||||
if (backupSuccess.includes(repo.name)) return;
|
||||
setBackingUp(repo.name);
|
||||
try {
|
||||
if (useDeepMigration) {
|
||||
await gitea.migrateRepo({
|
||||
clone_addr: `https://github.com/${repo.original}.git`,
|
||||
repo_name: repo.name,
|
||||
description: repo.desc,
|
||||
private: repo.private,
|
||||
auth_token: githubToken || undefined
|
||||
}, true);
|
||||
} else {
|
||||
await gitea.createRepo(repo.name, repo.desc, repo.private, true);
|
||||
}
|
||||
setBackupSuccess(prev => [...prev, repo.name]);
|
||||
fetchGiteaRepos();
|
||||
} catch (e: any) {
|
||||
if (e.message.includes('422')) {
|
||||
setBackupSuccess(prev => [...prev, repo.name]);
|
||||
} else {
|
||||
setError(`Failed to backup ${repo.name}: ${e.message}`);
|
||||
}
|
||||
} finally {
|
||||
setBackingUp(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackupAll = async () => {
|
||||
for (const repo of REPOS_TO_BACKUP) {
|
||||
if (!backupSuccess.includes(repo.name)) {
|
||||
await handleBackup(repo);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getMigrationScript = () => {
|
||||
const lines = [
|
||||
'#!/bin/bash',
|
||||
'# Migration script for Gitea VPS',
|
||||
'GITEA_URL="http://143.198.27.163:3000"',
|
||||
'GITEA_USER="admin"',
|
||||
'GITEA_TOKEN="<REPLACE_WITH_YOUR_GITEA_TOKEN>"',
|
||||
'',
|
||||
];
|
||||
REPOS_TO_BACKUP.forEach(repo => {
|
||||
lines.push(`# --- ${repo.name} ---`);
|
||||
lines.push(`echo ">>> Migrating ${repo.name}..."`);
|
||||
lines.push(`if [ -d "${basePath}/${repo.name}" ]; then`);
|
||||
lines.push(` cd "${basePath}/${repo.name}"`);
|
||||
lines.push(` git remote add gitea "http://$GITEA_USER:$GITEA_TOKEN@143.198.27.163:3000/$GITEA_USER/${repo.name}.git" 2>/dev/null || \\`);
|
||||
lines.push(` git remote set-url gitea "http://$GITEA_USER:$GITEA_TOKEN@143.198.27.163:3000/$GITEA_USER/${repo.name}.git"`);
|
||||
lines.push(` CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")`);
|
||||
lines.push(` git push gitea "$CURRENT_BRANCH:main" -f`);
|
||||
lines.push(` echo "Done."`);
|
||||
lines.push(`else`);
|
||||
lines.push(` echo "Error: Directory ${basePath}/${repo.name} not found."`);
|
||||
lines.push(`fi`);
|
||||
lines.push('');
|
||||
});
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
const handleReview = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const parsed = parseRepoUrl(url);
|
||||
if (!parsed) {
|
||||
setError('Invalid repository URL.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setRepoData(null);
|
||||
setReview(null);
|
||||
|
||||
try {
|
||||
const data = await fetchRepoInfo(parsed, githubToken);
|
||||
setRepoData(data);
|
||||
const analysis = await analyzeRepo(data);
|
||||
setReview(analysis);
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-zinc-950 text-zinc-100 font-sans selection:bg-emerald-500/30">
|
||||
<Navigation mode={mode} setMode={setMode} userInfo={userInfo} />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-6 py-12">
|
||||
<AnimatePresence mode="wait">
|
||||
{mode === 'tower' && (
|
||||
<TowerMode
|
||||
worldState={worldState} issues={issues} matrixPrompt={matrixPrompt} setMatrixPrompt={setMatrixPrompt}
|
||||
avatarPrompt={avatarPrompt} setAvatarPrompt={setAvatarPrompt} generatingAvatar={generatingAvatar}
|
||||
updatingWorld={updatingWorld} towerMissing={towerMissing} initializingTower={initializingTower}
|
||||
loading={loading} connectionStatus={connectionStatus} remoteConnectionStatus={remoteConnectionStatus}
|
||||
fetchTowerData={fetchTowerData} handleInitializeTower={handleInitializeTower}
|
||||
handleMatrixSubmit={handleMatrixSubmit} generateAvatar={generateAvatar}
|
||||
profileData={profileData} setProfileData={setProfileData} updatingProfile={updatingProfile}
|
||||
handleProfileUpdate={handleProfileUpdate}
|
||||
/>
|
||||
)}
|
||||
{mode === 'backup' && (
|
||||
<BackupMode
|
||||
backupSuccess={backupSuccess} REPOS_TO_BACKUP={REPOS_TO_BACKUP} giteaRepos={giteaRepos}
|
||||
fetchingRepos={fetchingRepos} backingUp={backingUp} githubToken={githubToken}
|
||||
setGithubToken={setGithubToken} useDeepMigration={useDeepMigration}
|
||||
setUseDeepMigration={setUseDeepMigration} basePath={basePath} setBasePath={setBasePath}
|
||||
copied={copied} setCopied={setCopied} fetchGiteaRepos={fetchGiteaRepos}
|
||||
handleBackupAll={handleBackupAll} handleBackup={handleBackup}
|
||||
getMigrationScript={getMigrationScript}
|
||||
/>
|
||||
)}
|
||||
{mode === 'nexus' && (
|
||||
<NexusMode />
|
||||
)}
|
||||
{mode === 'reviewer' && (
|
||||
<ReviewerMode
|
||||
url={url} setUrl={setUrl} loading={loading} repoData={repoData}
|
||||
review={review} activeTab={activeTab} setActiveTab={setActiveTab}
|
||||
handleReview={handleReview}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<ErrorToast error={error} setError={setError} />
|
||||
</main>
|
||||
|
||||
<footer className="max-w-7xl mx-auto px-6 py-12 border-t border-zinc-900 flex flex-col md:flex-row justify-between items-center gap-6 text-zinc-600 text-[10px] font-bold uppercase tracking-widest">
|
||||
<p>© 2026 Matrix Council. All rights reserved.</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
src/services/geminiTTS.ts
Normal file
63
src/services/geminiTTS.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
import { GoogleGenAI, Modality } from "@google/genai";
|
||||
|
||||
class GeminiTTSService {
|
||||
private ai: GoogleGenAI;
|
||||
private audioContext: AudioContext | null = null;
|
||||
|
||||
constructor() {
|
||||
this.ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
|
||||
}
|
||||
|
||||
private async getAudioContext() {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new AudioContext({ sampleRate: 24000 });
|
||||
}
|
||||
if (this.audioContext.state === 'suspended') {
|
||||
await this.audioContext.resume();
|
||||
}
|
||||
return this.audioContext;
|
||||
}
|
||||
|
||||
async speak(text: string, voice: 'Puck' | 'Charon' | 'Kore' | 'Fenrir' | 'Zephyr' = 'Zephyr') {
|
||||
try {
|
||||
const response = await this.ai.models.generateContent({
|
||||
model: "gemini-2.5-flash-preview-tts",
|
||||
contents: [{ parts: [{ text: `Say clearly: ${text}` }] }],
|
||||
config: {
|
||||
responseModalities: [Modality.AUDIO],
|
||||
speechConfig: {
|
||||
voiceConfig: {
|
||||
prebuiltVoiceConfig: { voiceName: voice },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const base64Audio = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data;
|
||||
if (base64Audio) {
|
||||
const audioData = Uint8Array.from(atob(base64Audio), c => c.charCodeAt(0));
|
||||
const ctx = await this.getAudioContext();
|
||||
|
||||
// The model returns raw PCM 16-bit little-endian at 24kHz
|
||||
const float32Data = new Float32Array(audioData.length / 2);
|
||||
const view = new DataView(audioData.buffer);
|
||||
for (let i = 0; i < float32Data.length; i++) {
|
||||
float32Data[i] = view.getInt16(i * 2, true) / 32768;
|
||||
}
|
||||
|
||||
const buffer = ctx.createBuffer(1, float32Data.length, 24000);
|
||||
buffer.getChannelData(0).set(float32Data);
|
||||
|
||||
const source = ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(ctx.destination);
|
||||
source.start();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Gemini TTS Error:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const geminiTTS = new GeminiTTSService();
|
||||
Reference in New Issue
Block a user