Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Header } from './components/dashboard/Header';
import { Sidebar } from './components/dashboard/Sidebar';
import { NetworkStats } from './components/network/NetworkStats';
import { NetworkVisualization } from './components/network/NetworkVisualization';
import { SpecializedNetworks } from './components/network/SpecializedNetworks';
import { CDNPanel } from './components/cdn/CDNPanel';
import { WASMModules } from './components/wasm/WASMModules';
import { MCPTools } from './components/mcp/MCPTools';
import { CreditsPanel } from './components/dashboard/CreditsPanel';
import { ConsolePanel } from './components/dashboard/ConsolePanel';
import { IdentityPanel } from './components/identity/IdentityPanel';
import { DocumentationPanel } from './components/docs/DocumentationPanel';
import { CrystalLoader } from './components/common/CrystalLoader';
import { ConsentWidget } from './components/common/ConsentWidget';
import { useNetworkStore } from './stores/networkStore';
function App() {
const [activeTab, setActiveTab] = useState('overview');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const { initializeEdgeNet, updateRealStats, isWASMReady } = useNetworkStore();
// Check for mobile viewport
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
// Initialize real EdgeNet WASM module
useEffect(() => {
const init = async () => {
try {
await initializeEdgeNet();
console.log('[App] EdgeNet initialized, WASM ready:', isWASMReady);
} catch (error) {
console.error('[App] EdgeNet initialization failed:', error);
} finally {
setIsLoading(false);
}
};
init();
}, [initializeEdgeNet, isWASMReady]);
// Update real stats from EdgeNet node
useEffect(() => {
const interval = setInterval(updateRealStats, 1000);
return () => clearInterval(interval);
}, [updateRealStats]);
// Render active tab content
const renderContent = () => {
const content = {
overview: (
<div className="space-y-6">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h1 className="text-2xl md:text-3xl font-bold mb-2">
<span className="bg-gradient-to-r from-sky-400 via-violet-400 to-cyan-400 bg-clip-text text-transparent">
Network Overview
</span>
</h1>
<p className="text-zinc-400">
Monitor your distributed compute network in real-time
</p>
</motion.div>
<NetworkStats />
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<NetworkVisualization />
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
<h3 className="text-sm font-medium text-zinc-400 mb-3">Quick Actions</h3>
<div className="grid grid-cols-2 gap-3">
<button
className="p-4 rounded-lg bg-sky-500/10 border border-sky-500/30 hover:bg-sky-500/20 transition-colors text-left"
onClick={() => setActiveTab('wasm')}
>
<p className="font-medium text-white">Load WASM</p>
<p className="text-xs text-zinc-400 mt-1">Initialize modules</p>
</button>
<button
className="p-4 rounded-lg bg-violet-500/10 border border-violet-500/30 hover:bg-violet-500/20 transition-colors text-left"
onClick={() => setActiveTab('mcp')}
>
<p className="font-medium text-white">MCP Tools</p>
<p className="text-xs text-zinc-400 mt-1">Execute tools</p>
</button>
<button
className="p-4 rounded-lg bg-emerald-500/10 border border-emerald-500/30 hover:bg-emerald-500/20 transition-colors text-left"
onClick={() => setActiveTab('cdn')}
>
<p className="font-medium text-white">CDN Scripts</p>
<p className="text-xs text-zinc-400 mt-1">Load libraries</p>
</button>
<button
className="p-4 rounded-lg bg-amber-500/10 border border-amber-500/30 hover:bg-amber-500/20 transition-colors text-left"
onClick={() => setActiveTab('identity')}
>
<p className="font-medium text-white">Identity</p>
<p className="text-xs text-zinc-400 mt-1">Crypto ID & Networks</p>
</button>
</div>
</motion.div>
</div>
</div>
),
network: (
<div className="space-y-6">
<h1 className="text-2xl font-bold">
<span className="bg-gradient-to-r from-sky-400 to-cyan-400 bg-clip-text text-transparent">
Network & Communities
</span>
</h1>
<p className="text-zinc-400">Join specialized networks to earn credits by contributing compute</p>
<NetworkStats />
<SpecializedNetworks />
<div className="mt-8">
<h2 className="text-lg font-semibold text-zinc-300 mb-4">Network Topology</h2>
<NetworkVisualization />
</div>
</div>
),
wasm: (
<div className="space-y-6">
<h1 className="text-2xl font-bold">WASM Modules</h1>
<WASMModules />
</div>
),
cdn: (
<div className="space-y-6">
<h1 className="text-2xl font-bold">CDN Script Manager</h1>
<CDNPanel />
</div>
),
mcp: <MCPTools />,
credits: (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Credit Economy</h1>
<CreditsPanel />
</div>
),
identity: (
<div className="space-y-6">
<h1 className="text-2xl font-bold">
<span className="bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent">
Identity & Networks
</span>
</h1>
<p className="text-zinc-400">Manage your cryptographic identity and network participation</p>
<IdentityPanel />
</div>
),
console: <ConsolePanel />,
activity: (
<div className="crystal-card p-8 text-center">
<p className="text-zinc-400">Activity log coming soon...</p>
</div>
),
settings: (
<div className="crystal-card p-8 text-center">
<p className="text-zinc-400">Settings panel coming soon...</p>
</div>
),
docs: (
<div className="space-y-6">
<h1 className="text-2xl font-bold">
<span className="bg-gradient-to-r from-sky-400 to-cyan-400 bg-clip-text text-transparent">
Documentation
</span>
</h1>
<p className="text-zinc-400">Learn how to use Edge-Net and integrate it into your projects</p>
<DocumentationPanel />
</div>
),
};
return content[activeTab as keyof typeof content] || content.overview;
};
// Loading screen
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<CrystalLoader size="lg" text="Initializing Edge-Net..." />
</div>
);
}
return (
<div className="min-h-screen flex flex-col">
<Header
onMenuToggle={() => setIsSidebarOpen(true)}
isMobile={isMobile}
/>
<div className="flex flex-1 overflow-hidden">
<Sidebar
activeTab={activeTab}
onTabChange={setActiveTab}
isOpen={isSidebarOpen}
onClose={() => setIsSidebarOpen(false)}
isMobile={isMobile}
/>
<main className="flex-1 overflow-auto p-4 md:p-6 quantum-grid">
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.2 }}
className="max-w-7xl mx-auto"
>
{renderContent()}
</motion.div>
</AnimatePresence>
</main>
</div>
{/* Floating consent widget for CPU/GPU contribution */}
<ConsentWidget />
</div>
);
}
export default App;

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,746 @@
import { useState } from 'react';
import { Button, Card, CardBody, Switch, Progress } from '@heroui/react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Download,
Check,
Package,
Cpu,
Shield,
Network,
Wrench,
Copy,
ExternalLink,
Code,
Terminal,
X,
FileCode,
Clipboard,
} from 'lucide-react';
import { useCDNStore } from '../../stores/cdnStore';
import type { CDNScript } from '../../types';
const categoryIcons = {
wasm: <Cpu size={16} />,
ai: <Package size={16} />,
crypto: <Shield size={16} />,
network: <Network size={16} />,
utility: <Wrench size={16} />,
};
const categoryColors = {
wasm: 'bg-sky-500/20 text-sky-400 border-sky-500/30',
ai: 'bg-violet-500/20 text-violet-400 border-violet-500/30',
crypto: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
network: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
utility: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
};
// Generate code snippets for a script
function getCodeSnippets(script: CDNScript) {
const isWasm = script.category === 'wasm';
const packageName = script.name.startsWith('@') ? script.name : script.id;
return {
scriptTag: `<script src="${script.url}"></script>`,
esModule: isWasm
? `import init, { /* exports */ } from '${script.url.replace('_bg.wasm', '.js')}';\n\nawait init();`
: `import '${script.url}';`,
npmInstall: `npm install ${packageName}`,
cdnFetch: `const response = await fetch('${script.url}');\nconst ${isWasm ? 'wasmModule = await WebAssembly.instantiate(await response.arrayBuffer())' : 'script = await response.text()'};`,
dynamicImport: `const module = await import('${script.url.replace('_bg.wasm', '.js')}');`,
};
}
// Usage examples for different categories
function getUsageExample(script: CDNScript): string {
switch (script.id) {
case 'edge-net-wasm':
return `import init, {
TimeCrystal,
CreditEconomy,
SwarmCoordinator
} from '@ruvector/edge-net';
// Initialize WASM module
await init();
// Create Time Crystal coordinator
const crystal = new TimeCrystal();
crystal.set_frequency(1.618); // Golden ratio
// Initialize credit economy
const economy = new CreditEconomy();
const balance = economy.get_balance();
// Start swarm coordination
const swarm = new SwarmCoordinator();
swarm.join_network('wss://edge-net.ruvector.dev');`;
case 'attention-wasm':
return `import init, { DAGAttention } from '@ruvector/attention-unified-wasm';
await init();
const attention = new DAGAttention();
attention.add_node('task-1', ['dep-a', 'dep-b']);
attention.add_node('task-2', ['task-1']);
const order = attention.topological_sort();
const critical = attention.critical_path();`;
case 'tensorflow':
return `// TensorFlow.js is loaded globally as 'tf'
const model = tf.sequential();
model.add(tf.layers.dense({ units: 32, inputShape: [10] }));
model.add(tf.layers.dense({ units: 1 }));
model.compile({ optimizer: 'sgd', loss: 'meanSquaredError' });
const xs = tf.randomNormal([100, 10]);
const ys = tf.randomNormal([100, 1]);
await model.fit(xs, ys, { epochs: 10 });`;
case 'onnx-runtime':
return `// ONNX Runtime is loaded globally as 'ort'
const session = await ort.InferenceSession.create('model.onnx');
const inputTensor = new ort.Tensor('float32', inputData, [1, 3, 224, 224]);
const feeds = { input: inputTensor };
const results = await session.run(feeds);
const output = results.output.data;`;
case 'noble-curves':
return `import { ed25519 } from '@noble/curves/ed25519';
import { secp256k1 } from '@noble/curves/secp256k1';
// Ed25519 signing
const privateKey = ed25519.utils.randomPrivateKey();
const publicKey = ed25519.getPublicKey(privateKey);
const message = new TextEncoder().encode('Hello Edge-Net');
const signature = ed25519.sign(message, privateKey);
const isValid = ed25519.verify(signature, message, publicKey);`;
case 'libp2p':
return `import { createLibp2p } from 'libp2p';
import { webRTC } from '@libp2p/webrtc';
import { noise } from '@chainsafe/libp2p-noise';
const node = await createLibp2p({
transports: [webRTC()],
connectionEncryption: [noise()],
});
await node.start();
console.log('Node started:', node.peerId.toString());`;
case 'comlink':
return `import * as Comlink from 'comlink';
// In worker.js
const api = {
compute: (data) => heavyComputation(data),
};
Comlink.expose(api);
// In main thread
const worker = new Worker('worker.js');
const api = Comlink.wrap(worker);
const result = await api.compute(data);`;
default:
return `// Load ${script.name}
// See documentation for usage examples`;
}
}
function CodeBlock({
code,
onCopy,
copied,
}: {
code: string;
onCopy: () => void;
copied: boolean;
}) {
return (
<div className="relative group">
<pre className="bg-zinc-950 border border-white/10 rounded-lg p-4 overflow-x-auto text-sm">
<code className="text-zinc-300 font-mono whitespace-pre">{code}</code>
</pre>
<button
onClick={onCopy}
className={`
absolute top-2 right-2 p-2 rounded-md transition-all
${copied
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-zinc-800 text-zinc-400 hover:text-white hover:bg-zinc-700'
}
`}
>
{copied ? <Check size={16} /> : <Copy size={16} />}
</button>
</div>
);
}
function ScriptCard({
script,
onToggle,
onLoad,
onUnload,
isLoading,
onShowCode,
}: {
script: CDNScript;
onToggle: () => void;
onLoad: () => void;
onUnload: () => void;
isLoading: boolean;
onShowCode: () => void;
}) {
const [copied, setCopied] = useState<string | null>(null);
const copyToClipboard = async (text: string, type: string) => {
await navigator.clipboard.writeText(text);
setCopied(type);
setTimeout(() => setCopied(null), 2000);
};
const snippets = getCodeSnippets(script);
return (
<Card
className={`bg-zinc-900/50 border ${
script.loaded
? 'border-emerald-500/30'
: script.enabled
? 'border-sky-500/30'
: 'border-white/10'
}`}
>
<CardBody className="p-4">
{/* Header */}
<div className="flex items-start justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className={`p-1 rounded ${categoryColors[script.category]}`}>
{categoryIcons[script.category]}
</span>
<h4 className="font-medium text-white truncate">{script.name}</h4>
{script.loaded && (
<span className="flex items-center gap-1 text-xs text-emerald-400">
<Check size={12} /> Loaded
</span>
)}
</div>
<p className="text-xs text-zinc-500 mt-1 line-clamp-2">
{script.description}
</p>
</div>
<Switch
isSelected={script.enabled}
onValueChange={onToggle}
size="sm"
classNames={{
wrapper: 'bg-zinc-700 group-data-[selected=true]:bg-sky-500',
}}
/>
</div>
{/* Quick Copy Buttons */}
<div className="flex flex-wrap gap-2 mb-3">
<button
onClick={() => copyToClipboard(snippets.scriptTag, 'script')}
className={`
flex items-center gap-1.5 px-2 py-1 rounded text-xs
transition-all border
${copied === 'script'
? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400'
: 'bg-zinc-800 border-white/10 text-zinc-400 hover:text-white hover:border-white/20'
}
`}
>
{copied === 'script' ? <Check size={12} /> : <Code size={12} />}
Script Tag
</button>
<button
onClick={() => copyToClipboard(script.url, 'url')}
className={`
flex items-center gap-1.5 px-2 py-1 rounded text-xs
transition-all border
${copied === 'url'
? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400'
: 'bg-zinc-800 border-white/10 text-zinc-400 hover:text-white hover:border-white/20'
}
`}
>
{copied === 'url' ? <Check size={12} /> : <ExternalLink size={12} />}
CDN URL
</button>
<button
onClick={() => copyToClipboard(snippets.npmInstall, 'npm')}
className={`
flex items-center gap-1.5 px-2 py-1 rounded text-xs
transition-all border
${copied === 'npm'
? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400'
: 'bg-zinc-800 border-white/10 text-zinc-400 hover:text-white hover:border-white/20'
}
`}
>
{copied === 'npm' ? <Check size={12} /> : <Terminal size={12} />}
npm
</button>
<button
onClick={onShowCode}
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs bg-violet-500/20 border border-violet-500/30 text-violet-400 hover:bg-violet-500/30 transition-all"
>
<FileCode size={12} />
Usage
</button>
</div>
{/* Actions */}
<div className="flex items-center justify-between">
<span className="text-xs text-zinc-500">{script.size}</span>
<div className="flex gap-2">
<Button
size="sm"
variant="flat"
isDisabled={!script.enabled || isLoading}
isLoading={isLoading}
className={
script.loaded
? 'bg-red-500/20 text-red-400'
: 'bg-sky-500/20 text-sky-400'
}
onPress={script.loaded ? onUnload : onLoad}
>
{script.loaded ? 'Unload' : 'Load'}
</Button>
</div>
</div>
</CardBody>
</Card>
);
}
function CodeModal({
script,
isOpen,
onClose,
}: {
script: CDNScript | null;
isOpen: boolean;
onClose: () => void;
}) {
const [copied, setCopied] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('usage');
if (!script) return null;
const snippets = getCodeSnippets(script);
const usage = getUsageExample(script);
const copyToClipboard = async (text: string, type: string) => {
await navigator.clipboard.writeText(text);
setCopied(type);
setTimeout(() => setCopied(null), 2000);
};
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50"
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="fixed inset-4 md:inset-auto md:left-1/2 md:top-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:w-[800px] md:max-h-[80vh] bg-zinc-900 border border-white/10 rounded-xl z-50 flex flex-col overflow-hidden"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-3">
<span className={`p-2 rounded-lg ${categoryColors[script.category]}`}>
{categoryIcons[script.category]}
</span>
<div>
<h2 className="font-semibold text-white">{script.name}</h2>
<p className="text-xs text-zinc-500">{script.description}</p>
</div>
</div>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-white/5 text-zinc-400 hover:text-white transition-colors"
>
<X size={20} />
</button>
</div>
{/* Tabs */}
<div className="border-b border-white/10">
<div className="flex gap-1 p-2">
{['usage', 'import', 'cdn', 'npm'].map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={`
px-4 py-2 rounded-lg text-sm font-medium transition-all
${activeTab === tab
? 'bg-sky-500/20 text-sky-400'
: 'text-zinc-400 hover:text-white hover:bg-white/5'
}
`}
>
{tab.charAt(0).toUpperCase() + tab.slice(1)}
</button>
))}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-auto p-4">
{activeTab === 'usage' && (
<div className="space-y-4">
<h3 className="text-sm font-medium text-zinc-300">Usage Example</h3>
<CodeBlock
code={usage}
onCopy={() => copyToClipboard(usage, 'usage')}
copied={copied === 'usage'}
/>
</div>
)}
{activeTab === 'import' && (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">ES Module Import</h3>
<CodeBlock
code={snippets.esModule}
onCopy={() => copyToClipboard(snippets.esModule, 'esModule')}
copied={copied === 'esModule'}
/>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">Dynamic Import</h3>
<CodeBlock
code={snippets.dynamicImport}
onCopy={() => copyToClipboard(snippets.dynamicImport, 'dynamicImport')}
copied={copied === 'dynamicImport'}
/>
</div>
</div>
)}
{activeTab === 'cdn' && (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">Script Tag</h3>
<CodeBlock
code={snippets.scriptTag}
onCopy={() => copyToClipboard(snippets.scriptTag, 'scriptTag')}
copied={copied === 'scriptTag'}
/>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">Fetch & Instantiate</h3>
<CodeBlock
code={snippets.cdnFetch}
onCopy={() => copyToClipboard(snippets.cdnFetch, 'cdnFetch')}
copied={copied === 'cdnFetch'}
/>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">CDN URL</h3>
<div className="flex items-center gap-2">
<input
type="text"
readOnly
value={script.url}
className="flex-1 bg-zinc-950 border border-white/10 rounded-lg px-3 py-2 text-sm text-zinc-300 font-mono"
/>
<button
onClick={() => copyToClipboard(script.url, 'cdnUrl')}
className={`
p-2 rounded-lg transition-all
${copied === 'cdnUrl'
? 'bg-emerald-500/20 text-emerald-400'
: 'bg-zinc-800 text-zinc-400 hover:text-white'
}
`}
>
{copied === 'cdnUrl' ? <Check size={16} /> : <Copy size={16} />}
</button>
</div>
</div>
</div>
)}
{activeTab === 'npm' && (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">Install via npm</h3>
<CodeBlock
code={snippets.npmInstall}
onCopy={() => copyToClipboard(snippets.npmInstall, 'npmInstall')}
copied={copied === 'npmInstall'}
/>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-300 mb-2">Package Info</h3>
<div className="bg-zinc-950 border border-white/10 rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-zinc-500 text-sm">Package</span>
<span className="text-zinc-300 font-mono text-sm">{script.name}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-zinc-500 text-sm">Size</span>
<span className="text-zinc-300 text-sm">{script.size}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-zinc-500 text-sm">Category</span>
<span className={`px-2 py-0.5 rounded text-xs ${categoryColors[script.category]}`}>
{script.category}
</span>
</div>
</div>
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between p-4 border-t border-white/10">
<a
href={script.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-sky-400 hover:text-sky-300"
>
<ExternalLink size={14} />
Open in new tab
</a>
<Button
color="primary"
onPress={onClose}
>
Done
</Button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
export function CDNPanel() {
const {
scripts,
autoLoad,
cacheEnabled,
isLoading,
loadScript,
unloadScript,
toggleScript,
setAutoLoad,
setCacheEnabled,
} = useCDNStore();
const [selectedScript, setSelectedScript] = useState<CDNScript | null>(null);
const [showCodeModal, setShowCodeModal] = useState(false);
const groupedScripts = scripts.reduce((acc, script) => {
if (!acc[script.category]) acc[script.category] = [];
acc[script.category].push(script);
return acc;
}, {} as Record<string, CDNScript[]>);
const loadedCount = scripts.filter((s) => s.loaded).length;
const enabledCount = scripts.filter((s) => s.enabled).length;
return (
<div className="space-y-6">
{/* Header Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-zinc-400">Loaded</p>
<Download className="text-sky-400" size={20} />
</div>
<p className="text-2xl font-bold text-sky-400">
{loadedCount}<span className="text-lg text-zinc-500">/{scripts.length}</span>
</p>
<Progress
value={(loadedCount / scripts.length) * 100}
className="mt-2"
classNames={{
indicator: 'bg-gradient-to-r from-sky-500 to-cyan-500',
track: 'bg-zinc-800',
}}
/>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-zinc-400">Enabled</p>
<Check className="text-emerald-400" size={20} />
</div>
<p className="text-2xl font-bold text-emerald-400">
{enabledCount}<span className="text-lg text-zinc-500">/{scripts.length}</span>
</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-zinc-400">Auto-Load</span>
<Switch
isSelected={autoLoad}
onValueChange={setAutoLoad}
size="sm"
classNames={{
wrapper: 'bg-zinc-700 group-data-[selected=true]:bg-sky-500',
}}
/>
</div>
<p className="text-xs text-zinc-500">Load enabled scripts on startup</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-zinc-400">Cache</span>
<Switch
isSelected={cacheEnabled}
onValueChange={setCacheEnabled}
size="sm"
classNames={{
wrapper: 'bg-zinc-700 group-data-[selected=true]:bg-emerald-500',
}}
/>
</div>
<p className="text-xs text-zinc-500">Cache in browser storage</p>
</motion.div>
</div>
{/* Quick Copy Section */}
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<h3 className="text-sm font-medium text-zinc-300 mb-3 flex items-center gap-2">
<Clipboard size={16} />
Quick Start - Copy to your project
</h3>
<div className="bg-zinc-950 border border-white/10 rounded-lg p-3 font-mono text-sm overflow-x-auto">
<code className="text-zinc-300">
{'<script src="https://unpkg.com/@ruvector/edge-net@0.1.1"></script>'}
</code>
</div>
<div className="flex gap-2 mt-3">
<Button
size="sm"
variant="flat"
className="bg-sky-500/20 text-sky-400"
onPress={() => {
navigator.clipboard.writeText('<script src="https://unpkg.com/@ruvector/edge-net@0.1.1"></script>');
}}
>
<Copy size={14} />
Copy Script Tag
</Button>
<Button
size="sm"
variant="flat"
className="bg-violet-500/20 text-violet-400"
onPress={() => {
navigator.clipboard.writeText('npm install @ruvector/edge-net');
}}
>
<Terminal size={14} />
Copy npm Install
</Button>
</div>
</motion.div>
{/* Scripts by Category */}
{Object.entries(groupedScripts).map(([category, categoryScripts], idx) => (
<motion.div
key={category}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * (idx + 3) }}
>
<div className="flex items-center gap-2 mb-3">
<div className={`p-1.5 rounded ${categoryColors[category as keyof typeof categoryColors]}`}>
{categoryIcons[category as keyof typeof categoryIcons]}
</div>
<h3 className="text-lg font-semibold capitalize">{category}</h3>
<span className="px-2 py-0.5 rounded bg-zinc-800 text-zinc-400 text-xs">
{categoryScripts.length}
</span>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{categoryScripts.map((script) => (
<ScriptCard
key={script.id}
script={script}
onToggle={() => toggleScript(script.id)}
onLoad={() => loadScript(script.id)}
onUnload={() => unloadScript(script.id)}
isLoading={isLoading}
onShowCode={() => {
setSelectedScript(script);
setShowCodeModal(true);
}}
/>
))}
</div>
</motion.div>
))}
{/* Code Modal */}
<CodeModal
script={selectedScript}
isOpen={showCodeModal}
onClose={() => setShowCodeModal(false)}
/>
</div>
);
}

View File

@@ -0,0 +1,455 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import {
Button,
Slider,
Switch,
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
} from '@heroui/react';
import {
Cpu,
Zap,
Battery,
Clock,
ChevronUp,
ChevronDown,
Shield,
X,
Settings,
Play,
Pause,
} from 'lucide-react';
import { useNetworkStore } from '../../stores/networkStore';
export function ConsentWidget() {
const [isExpanded, setIsExpanded] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const {
contributionSettings,
setContributionSettings,
giveConsent,
revokeConsent,
startContributing,
stopContributing,
stats,
credits,
} = useNetworkStore();
const { consentGiven, enabled, cpuLimit, gpuEnabled, respectBattery, onlyWhenIdle } =
contributionSettings;
// Show initial consent dialog if not given
const [showInitialConsent, setShowInitialConsent] = useState(false);
useEffect(() => {
// Only show after a delay to not be intrusive
const timer = setTimeout(() => {
if (!consentGiven) {
setShowInitialConsent(true);
}
}, 3000);
return () => clearTimeout(timer);
}, [consentGiven]);
const handleGiveConsent = () => {
giveConsent();
setShowInitialConsent(false);
startContributing();
};
const handleToggleContribution = () => {
if (enabled) {
stopContributing();
} else {
startContributing();
}
};
// Minimized floating button - always visible
if (!isExpanded) {
return (
<>
<motion.div
className="fixed bottom-4 right-4 z-50"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 260, damping: 20 }}
>
<button
onClick={() => consentGiven ? setIsExpanded(true) : setShowInitialConsent(true)}
className={`
flex items-center gap-2 px-4 py-2 rounded-full shadow-lg
border backdrop-blur-xl transition-all
${
enabled
? 'bg-emerald-500/20 border-emerald-500/50 text-emerald-400 hover:bg-emerald-500/30'
: consentGiven
? 'bg-zinc-800/80 border-zinc-700 text-zinc-400 hover:bg-zinc-700/80'
: 'bg-violet-500/20 border-violet-500/50 text-violet-400 hover:bg-violet-500/30'
}
`}
aria-label="Open Edge-Net contribution panel"
>
{enabled ? (
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
>
<Cpu size={18} />
</motion.div>
) : (
<Zap size={18} />
)}
<span className="text-sm font-medium">
{enabled ? `${credits.earned.toFixed(2)} rUv` : consentGiven ? 'Paused' : 'Join Edge-Net'}
</span>
<ChevronUp size={14} />
</button>
</motion.div>
{/* Initial consent modal */}
<Modal
isOpen={showInitialConsent}
onClose={() => setShowInitialConsent(false)}
size="md"
placement="center"
backdrop="blur"
classNames={{
base: 'bg-zinc-900/95 backdrop-blur-xl border border-zinc-700/50 shadow-2xl mx-4',
wrapper: 'items-center justify-center',
header: 'border-b-0 pb-0',
body: 'px-8 py-6',
footer: 'border-t border-zinc-800/50 pt-6 px-8 pb-6',
}}
>
<ModalContent>
<ModalHeader className="flex flex-col items-center text-center pt-8 px-8">
{/* Logo */}
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-sky-500 via-violet-500 to-cyan-500 flex items-center justify-center mb-4 shadow-lg shadow-violet-500/30">
<Zap size={32} className="text-white" />
</div>
<h3 className="text-2xl font-bold text-white">
Join Edge-Net
</h3>
<p className="text-sm text-zinc-400 mt-2">
The Collective AI Computing Network
</p>
</ModalHeader>
<ModalBody>
<div className="space-y-6">
{/* Introduction - improved text */}
<div className="text-center space-y-3">
<p className="text-zinc-200 text-base leading-relaxed">
Transform your idle browser into a powerful AI compute node.
</p>
<p className="text-zinc-400 text-sm leading-relaxed">
When you're not using your browser, Edge-Net harnesses unused CPU cycles
to power distributed AI computations. In return, you earn{' '}
<span className="text-emerald-400 font-semibold">rUv credits</span> that
can be used for AI services across the network.
</p>
</div>
{/* Features - compact grid */}
<div className="grid grid-cols-2 gap-3">
<div className="flex items-center gap-3 p-3 bg-zinc-800/40 rounded-xl border border-zinc-700/30">
<Cpu size={18} className="text-sky-400 flex-shrink-0" />
<div>
<div className="text-sm text-zinc-200 font-medium">Idle Only</div>
<div className="text-xs text-zinc-500">Uses spare CPU cycles</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-zinc-800/40 rounded-xl border border-zinc-700/30">
<Battery size={18} className="text-emerald-400 flex-shrink-0" />
<div>
<div className="text-sm text-zinc-200 font-medium">Battery Aware</div>
<div className="text-xs text-zinc-500">Pauses on low power</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-zinc-800/40 rounded-xl border border-zinc-700/30">
<Shield size={18} className="text-violet-400 flex-shrink-0" />
<div>
<div className="text-sm text-zinc-200 font-medium">Privacy First</div>
<div className="text-xs text-zinc-500">WASM sandboxed</div>
</div>
</div>
<div className="flex items-center gap-3 p-3 bg-zinc-800/40 rounded-xl border border-zinc-700/30">
<Clock size={18} className="text-amber-400 flex-shrink-0" />
<div>
<div className="text-sm text-zinc-200 font-medium">Full Control</div>
<div className="text-xs text-zinc-500">Pause anytime</div>
</div>
</div>
</div>
{/* Trust badge */}
<div className="text-center pt-2">
<p className="text-xs text-zinc-500">
Secured by WASM sandbox isolation & PiKey cryptography
</p>
</div>
</div>
</ModalBody>
<ModalFooter className="flex-col gap-3">
<Button
fullWidth
color="primary"
size="lg"
onPress={handleGiveConsent}
className="bg-gradient-to-r from-sky-500 to-violet-500 font-semibold text-base h-12"
startContent={<Play size={18} />}
>
Start Contributing
</Button>
<Button
fullWidth
variant="light"
size="sm"
onPress={() => setShowInitialConsent(false)}
className="text-zinc-500 hover:text-zinc-300"
>
Maybe Later
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}
// Expanded panel with settings modal
return (
<>
<motion.div
className="fixed bottom-4 right-4 z-50 w-80"
initial={{ y: 100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 100, opacity: 0 }}
>
<div className="crystal-card p-4 shadow-xl">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
enabled ? 'bg-emerald-400 animate-pulse' : 'bg-zinc-500'
}`}
/>
<span className="text-sm font-medium text-white">
{enabled ? 'Contributing' : 'Paused'}
</span>
</div>
<div className="flex items-center gap-1">
<Button
isIconOnly
size="sm"
variant="light"
onPress={() => setShowSettings(true)}
aria-label="Open settings"
>
<Settings size={16} />
</Button>
<Button
isIconOnly
size="sm"
variant="light"
onPress={() => setIsExpanded(false)}
aria-label="Minimize panel"
>
<ChevronDown size={16} />
</Button>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-zinc-800/50 rounded-lg p-3">
<div className="text-xs text-zinc-500 mb-1">rUv Earned</div>
<div className="text-lg font-bold text-emerald-400">
{credits.earned.toFixed(2)}
</div>
</div>
<div className="bg-zinc-800/50 rounded-lg p-3">
<div className="text-xs text-zinc-500 mb-1">Tasks</div>
<div className="text-lg font-bold text-sky-400">
{stats.tasksCompleted}
</div>
</div>
</div>
{/* CPU Slider */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-zinc-400">CPU Limit</span>
<span className="text-xs text-white font-medium">{cpuLimit}%</span>
</div>
<Slider
size="sm"
step={10}
minValue={10}
maxValue={80}
value={cpuLimit}
onChange={(value) =>
setContributionSettings({ cpuLimit: value as number })
}
classNames={{
track: 'bg-zinc-700',
filler: 'bg-gradient-to-r from-sky-500 to-violet-500',
}}
aria-label="CPU usage limit"
/>
</div>
{/* Quick toggles */}
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 text-xs text-zinc-400">
<Battery size={14} />
<span>Respect Battery</span>
</div>
<Switch
size="sm"
isSelected={respectBattery}
onValueChange={(value) =>
setContributionSettings({ respectBattery: value })
}
aria-label="Respect battery power"
/>
</div>
{/* Control button */}
<Button
fullWidth
color={enabled ? 'warning' : 'success'}
variant="flat"
onPress={handleToggleContribution}
startContent={enabled ? <Pause size={16} /> : <Play size={16} />}
>
{enabled ? 'Pause Contribution' : 'Start Contributing'}
</Button>
</div>
</motion.div>
{/* Settings Modal */}
<Modal
isOpen={showSettings}
onClose={() => setShowSettings(false)}
size="sm"
placement="center"
classNames={{
base: 'bg-zinc-900/95 backdrop-blur-xl border border-zinc-700/50 mx-4',
header: 'border-b border-zinc-800 py-3 px-5',
body: 'py-5 px-5',
footer: 'border-t border-zinc-800 py-3 px-5',
closeButton: 'top-3 right-3 hover:bg-zinc-700/50',
}}
>
<ModalContent>
<ModalHeader className="flex justify-between items-center">
<h3 className="text-base font-semibold text-white">
Contribution Settings
</h3>
</ModalHeader>
<ModalBody>
<div className="space-y-4">
{/* CPU Settings */}
<div>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<Cpu size={16} className="text-sky-400" />
<span className="text-white text-sm">CPU Limit</span>
</div>
<span className="text-sky-400 text-sm font-bold">{cpuLimit}%</span>
</div>
<Slider
size="sm"
step={5}
minValue={10}
maxValue={80}
value={cpuLimit}
onChange={(value) =>
setContributionSettings({ cpuLimit: value as number })
}
classNames={{
track: 'bg-zinc-700',
filler: 'bg-gradient-to-r from-sky-500 to-cyan-500',
}}
aria-label="CPU usage limit slider"
/>
</div>
{/* GPU Settings */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Zap size={16} className="text-violet-400" />
<span className="text-white text-sm">GPU Acceleration</span>
</div>
<Switch
size="sm"
isSelected={gpuEnabled}
onValueChange={(value) =>
setContributionSettings({ gpuEnabled: value })
}
aria-label="Enable GPU acceleration"
/>
</div>
{/* Other settings */}
<div className="space-y-3 pt-2 border-t border-zinc-800">
<div className="flex items-center justify-between">
<span className="text-zinc-300 text-sm">Respect Battery</span>
<Switch
size="sm"
isSelected={respectBattery}
onValueChange={(value) =>
setContributionSettings({ respectBattery: value })
}
aria-label="Respect battery power"
/>
</div>
<div className="flex items-center justify-between">
<span className="text-zinc-300 text-sm">Only When Idle</span>
<Switch
size="sm"
isSelected={onlyWhenIdle}
onValueChange={(value) =>
setContributionSettings({ onlyWhenIdle: value })
}
aria-label="Only contribute when idle"
/>
</div>
</div>
{/* Revoke consent */}
<div className="pt-3 border-t border-zinc-800">
<Button
size="sm"
variant="flat"
color="danger"
onPress={() => {
revokeConsent();
setShowSettings(false);
setIsExpanded(false);
}}
startContent={<X size={14} />}
>
Revoke Consent
</Button>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button onPress={() => setShowSettings(false)}>Done</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
}

View File

@@ -0,0 +1,82 @@
import { motion } from 'framer-motion';
interface CrystalLoaderProps {
size?: 'sm' | 'md' | 'lg';
text?: string;
}
const sizes = {
sm: { container: 'w-8 h-8', crystal: 'w-4 h-4' },
md: { container: 'w-16 h-16', crystal: 'w-8 h-8' },
lg: { container: 'w-24 h-24', crystal: 'w-12 h-12' },
};
export function CrystalLoader({ size = 'md', text }: CrystalLoaderProps) {
const { container, crystal } = sizes[size];
return (
<div className="flex flex-col items-center gap-4">
<div className={`${container} relative`}>
{/* Outer rotating ring */}
<motion.div
className="absolute inset-0 rounded-full border-2 border-sky-500/30"
animate={{ rotate: 360 }}
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
/>
{/* Middle rotating ring (opposite direction) */}
<motion.div
className="absolute inset-2 rounded-full border-2 border-violet-500/30"
animate={{ rotate: -360 }}
transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
/>
{/* Inner pulsing crystal */}
<motion.div
className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ${crystal}`}
style={{
background: 'linear-gradient(135deg, #0ea5e9, #7c3aed, #06b6d4)',
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
}}
animate={{
scale: [1, 1.2, 1],
opacity: [0.8, 1, 0.8],
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
{/* Glow effect */}
<motion.div
className="absolute inset-0 rounded-full"
style={{
background:
'radial-gradient(circle, rgba(14,165,233,0.3) 0%, transparent 70%)',
}}
animate={{
opacity: [0.3, 0.6, 0.3],
scale: [1, 1.1, 1],
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
</div>
{text && (
<motion.p
className="text-sm text-zinc-400"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
>
{text}
</motion.p>
)}
</div>
);
}

View File

@@ -0,0 +1,68 @@
import { Chip } from '@heroui/react';
import { motion } from 'framer-motion';
import type { ReactNode } from 'react';
interface GlowingBadgeProps {
children: ReactNode;
color?: 'crystal' | 'temporal' | 'quantum' | 'success' | 'warning' | 'danger';
variant?: 'solid' | 'bordered' | 'flat';
size?: 'sm' | 'md' | 'lg';
startContent?: ReactNode;
endContent?: ReactNode;
animate?: boolean;
}
const glowColors = {
crystal: 'shadow-sky-500/50',
temporal: 'shadow-violet-500/50',
quantum: 'shadow-cyan-500/50',
success: 'shadow-emerald-500/50',
warning: 'shadow-amber-500/50',
danger: 'shadow-red-500/50',
};
const bgColors = {
crystal: 'bg-sky-500/20 border-sky-500/50 text-sky-300',
temporal: 'bg-violet-500/20 border-violet-500/50 text-violet-300',
quantum: 'bg-cyan-500/20 border-cyan-500/50 text-cyan-300',
success: 'bg-emerald-500/20 border-emerald-500/50 text-emerald-300',
warning: 'bg-amber-500/20 border-amber-500/50 text-amber-300',
danger: 'bg-red-500/20 border-red-500/50 text-red-300',
};
export function GlowingBadge({
children,
color = 'crystal',
variant = 'flat',
size = 'md',
startContent,
endContent,
animate = false,
}: GlowingBadgeProps) {
const Component = animate ? motion.div : 'div';
return (
<Component
{...(animate
? {
animate: { boxShadow: ['0 0 10px', '0 0 20px', '0 0 10px'] },
transition: { duration: 2, repeat: Infinity },
}
: {})}
className={`inline-block ${animate ? glowColors[color] : ''}`}
>
<Chip
variant={variant}
size={size}
startContent={startContent}
endContent={endContent}
classNames={{
base: `${bgColors[color]} border`,
content: 'font-medium',
}}
>
{children}
</Chip>
</Component>
);
}

View File

@@ -0,0 +1,96 @@
import { Card, CardBody } from '@heroui/react';
import { motion } from 'framer-motion';
import type { ReactNode } from 'react';
interface StatCardProps {
title: string;
value: string | number;
change?: number;
icon?: ReactNode;
color?: 'crystal' | 'temporal' | 'quantum' | 'success' | 'warning' | 'danger';
size?: 'sm' | 'md' | 'lg';
animated?: boolean;
}
const colorClasses = {
crystal: 'from-sky-500/20 to-sky-600/10 border-sky-500/30',
temporal: 'from-violet-500/20 to-violet-600/10 border-violet-500/30',
quantum: 'from-cyan-500/20 to-cyan-600/10 border-cyan-500/30',
success: 'from-emerald-500/20 to-emerald-600/10 border-emerald-500/30',
warning: 'from-amber-500/20 to-amber-600/10 border-amber-500/30',
danger: 'from-red-500/20 to-red-600/10 border-red-500/30',
};
const iconColorClasses = {
crystal: 'text-sky-400',
temporal: 'text-violet-400',
quantum: 'text-cyan-400',
success: 'text-emerald-400',
warning: 'text-amber-400',
danger: 'text-red-400',
};
export function StatCard({
title,
value,
change,
icon,
color = 'crystal',
size = 'md',
animated = true,
}: StatCardProps) {
const sizeClasses = {
sm: 'p-3',
md: 'p-4',
lg: 'p-6',
};
const valueSizeClasses = {
sm: 'text-xl',
md: 'text-2xl',
lg: 'text-4xl',
};
return (
<motion.div
initial={animated ? { opacity: 0, y: 20 } : false}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Card
className={`crystal-card bg-gradient-to-br ${colorClasses[color]} border`}
>
<CardBody className={sizeClasses[size]}>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-sm text-zinc-400 mb-1">{title}</p>
<motion.p
className={`${valueSizeClasses[size]} font-bold stat-value text-white`}
key={String(value)}
initial={animated ? { scale: 1.1, opacity: 0.5 } : false}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.2 }}
>
{typeof value === 'number' ? value.toLocaleString() : value}
</motion.p>
{change !== undefined && (
<p
className={`text-sm mt-1 ${
change >= 0 ? 'text-emerald-400' : 'text-red-400'
}`}
>
{change >= 0 ? '↑' : '↓'} {Math.abs(change).toFixed(1)}%
</p>
)}
</div>
{icon && (
<div className={`${iconColorClasses[color]} opacity-80`}>
{icon}
</div>
)}
</div>
</CardBody>
</Card>
</motion.div>
);
}

View File

@@ -0,0 +1,332 @@
/**
* Activity Panel - Real-time activity log from EdgeNet operations
*/
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Activity,
Zap,
CheckCircle2,
AlertCircle,
Clock,
Server,
Cpu,
Network,
Trash2,
Filter,
} from 'lucide-react';
import { Button, Chip } from '@heroui/react';
import { useNetworkStore } from '../../stores/networkStore';
interface ActivityEvent {
id: string;
type: 'task' | 'credit' | 'network' | 'system' | 'error';
action: string;
description: string;
timestamp: Date;
metadata?: Record<string, string | number>;
}
const typeConfig = {
task: { icon: Cpu, color: 'text-sky-400', bgColor: 'bg-sky-500/20' },
credit: { icon: Zap, color: 'text-emerald-400', bgColor: 'bg-emerald-500/20' },
network: { icon: Network, color: 'text-violet-400', bgColor: 'bg-violet-500/20' },
system: { icon: Server, color: 'text-amber-400', bgColor: 'bg-amber-500/20' },
error: { icon: AlertCircle, color: 'text-red-400', bgColor: 'bg-red-500/20' },
};
function ActivityItem({ event, index }: { event: ActivityEvent; index: number }) {
const config = typeConfig[event.type];
const Icon = config.icon;
return (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ delay: index * 0.02 }}
className="flex items-start gap-3 p-3 rounded-lg bg-zinc-900/50 border border-white/5 hover:border-white/10 transition-all"
>
<div className={`w-8 h-8 rounded-lg ${config.bgColor} flex items-center justify-center flex-shrink-0`}>
<Icon size={16} className={config.color} />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="font-medium text-white text-sm">{event.action}</span>
<Chip size="sm" className={`${config.bgColor} ${config.color} text-xs capitalize`}>
{event.type}
</Chip>
</div>
<p className="text-xs text-zinc-400 truncate">{event.description}</p>
{event.metadata && (
<div className="flex flex-wrap gap-2 mt-1">
{Object.entries(event.metadata).map(([key, value]) => (
<span key={key} className="text-xs text-zinc-500">
{key}: <span className="text-zinc-300">{value}</span>
</span>
))}
</div>
)}
</div>
<div className="text-xs text-zinc-500 flex items-center gap-1 flex-shrink-0">
<Clock size={10} />
{formatTime(event.timestamp)}
</div>
</motion.div>
);
}
function formatTime(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
if (diff < 60000) return 'Just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
return date.toLocaleDateString();
}
export function ActivityPanel() {
const [activities, setActivities] = useState<ActivityEvent[]>([]);
const [filter, setFilter] = useState<'all' | ActivityEvent['type']>('all');
// Get real data from store
const credits = useNetworkStore(state => state.credits);
const stats = useNetworkStore(state => state.stats);
const contributionEnabled = useNetworkStore(state => state.contributionSettings.enabled);
const firebasePeers = useNetworkStore(state => state.firebasePeers);
// Generate activities from real events
useEffect(() => {
const newActivities: ActivityEvent[] = [];
const now = new Date();
// Add credit earning events
if (contributionEnabled && credits.earned > 0) {
newActivities.push({
id: `credit-${Date.now()}`,
type: 'credit',
action: 'Credits Earned',
description: `Accumulated ${credits.earned.toFixed(4)} rUv from network contribution`,
timestamp: now,
metadata: { rate: '0.047/s', total: credits.earned.toFixed(2) },
});
}
// Add network events
if (firebasePeers.length > 0) {
newActivities.push({
id: `network-peers-${Date.now()}`,
type: 'network',
action: 'Peers Connected',
description: `${firebasePeers.length} peer(s) active in network`,
timestamp: new Date(now.getTime() - 30000),
metadata: { peers: firebasePeers.length },
});
}
// Add system events
if (stats.uptime > 0) {
newActivities.push({
id: `system-uptime-${Date.now()}`,
type: 'system',
action: 'Node Active',
description: `Local node running for ${formatUptime(stats.uptime)}`,
timestamp: new Date(now.getTime() - 60000),
metadata: { uptime: formatUptime(stats.uptime) },
});
}
// Add contribution status
newActivities.push({
id: `system-contribution-${Date.now()}`,
type: contributionEnabled ? 'task' : 'system',
action: contributionEnabled ? 'Contributing' : 'Idle',
description: contributionEnabled
? 'Actively contributing compute resources to the network'
: 'Contribution paused - click Start Contributing to earn credits',
timestamp: new Date(now.getTime() - 120000),
});
// Add some historical events
newActivities.push(
{
id: 'init-1',
type: 'system',
action: 'WASM Initialized',
description: 'EdgeNet WASM module loaded successfully',
timestamp: new Date(now.getTime() - 180000),
},
{
id: 'init-2',
type: 'network',
action: 'Firebase Connected',
description: 'Real-time peer synchronization active',
timestamp: new Date(now.getTime() - 200000),
},
{
id: 'init-3',
type: 'network',
action: 'Relay Connected',
description: 'WebSocket relay connection established',
timestamp: new Date(now.getTime() - 220000),
}
);
setActivities(newActivities);
}, [credits.earned, contributionEnabled, firebasePeers.length, stats.uptime]);
const filteredActivities = filter === 'all'
? activities
: activities.filter(a => a.type === filter);
const clearActivities = () => {
setActivities(activities.slice(0, 3)); // Keep only system events
};
return (
<div className="space-y-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h1 className="text-2xl md:text-3xl font-bold mb-2">
<span className="bg-gradient-to-r from-sky-400 via-violet-400 to-cyan-400 bg-clip-text text-transparent">
Activity Log
</span>
</h1>
<p className="text-zinc-400">
Track all network operations and events in real-time
</p>
</motion.div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.05 }}
>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-zinc-400">Total Events</p>
<Activity className="text-sky-400" size={18} />
</div>
<p className="text-2xl font-bold text-sky-400">{activities.length}</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-zinc-400">Credits</p>
<Zap className="text-emerald-400" size={18} />
</div>
<p className="text-2xl font-bold text-emerald-400">{credits.earned.toFixed(2)}</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15 }}
>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-zinc-400">Tasks</p>
<CheckCircle2 className="text-violet-400" size={18} />
</div>
<p className="text-2xl font-bold text-violet-400">{stats.tasksCompleted}</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center justify-between mb-2">
<p className="text-sm text-zinc-400">Peers</p>
<Network className="text-amber-400" size={18} />
</div>
<p className="text-2xl font-bold text-amber-400">{firebasePeers.length}</p>
</motion.div>
</div>
{/* Filters */}
<motion.div
className="flex flex-wrap items-center gap-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.25 }}
>
<span className="text-xs text-zinc-500 flex items-center gap-1">
<Filter size={12} /> Filter:
</span>
{(['all', 'task', 'credit', 'network', 'system', 'error'] as const).map((f) => (
<Button
key={f}
size="sm"
variant="flat"
className={
filter === f
? 'bg-sky-500/20 text-sky-400 border border-sky-500/30'
: 'bg-white/5 text-zinc-400 hover:text-white'
}
onPress={() => setFilter(f)}
>
{f.charAt(0).toUpperCase() + f.slice(1)}
</Button>
))}
<div className="ml-auto">
<Button
size="sm"
variant="flat"
className="bg-white/5 text-zinc-400"
startContent={<Trash2 size={14} />}
onPress={clearActivities}
>
Clear
</Button>
</div>
</motion.div>
{/* Activity List */}
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
<div className="space-y-2 max-h-[500px] overflow-y-auto">
<AnimatePresence>
{filteredActivities.map((activity, index) => (
<ActivityItem key={activity.id} event={activity} index={index} />
))}
</AnimatePresence>
{filteredActivities.length === 0 && (
<div className="text-center py-8">
<Activity className="mx-auto text-zinc-600 mb-3" size={40} />
<p className="text-zinc-400">No activities match the current filter</p>
</div>
)}
</div>
</motion.div>
</div>
);
}
function formatUptime(seconds: number): string {
if (seconds < 60) return `${Math.round(seconds)}s`;
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
if (seconds < 86400) return `${Math.round(seconds / 3600)}h ${Math.round((seconds % 3600) / 60)}m`;
return `${Math.floor(seconds / 86400)}d ${Math.round((seconds % 86400) / 3600)}h`;
}
export default ActivityPanel;

View File

@@ -0,0 +1,225 @@
import { useEffect, useState } from 'react';
import { Button, Chip, Input, ScrollShadow } from '@heroui/react';
import { motion } from 'framer-motion';
import { Terminal, Trash2, Download, Filter, Info, AlertTriangle, XCircle, Bug } from 'lucide-react';
import { subscribeToLogs, clearLogs } from '../../utils/debug';
import type { DebugLog } from '../../types';
const levelIcons = {
info: <Info size={14} />,
warn: <AlertTriangle size={14} />,
error: <XCircle size={14} />,
debug: <Bug size={14} />,
};
const levelColors = {
info: 'text-sky-400',
warn: 'text-amber-400',
error: 'text-red-400',
debug: 'text-violet-400',
};
const levelBg = {
info: 'bg-sky-500/10 border-sky-500/30',
warn: 'bg-amber-500/10 border-amber-500/30',
error: 'bg-red-500/10 border-red-500/30',
debug: 'bg-violet-500/10 border-violet-500/30',
};
export function ConsolePanel() {
const [logs, setLogs] = useState<DebugLog[]>([]);
const [filter, setFilter] = useState('');
const [levelFilter, setLevelFilter] = useState<string>('all');
useEffect(() => {
const unsubscribe = subscribeToLogs(setLogs);
return unsubscribe;
}, []);
const filteredLogs = logs.filter((log) => {
const matchesText =
filter === '' ||
log.message.toLowerCase().includes(filter.toLowerCase()) ||
log.source.toLowerCase().includes(filter.toLowerCase());
const matchesLevel = levelFilter === 'all' || log.level === levelFilter;
return matchesText && matchesLevel;
});
const handleExport = () => {
const data = JSON.stringify(logs, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `edge-net-logs-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
};
const logCounts = logs.reduce(
(acc, log) => {
acc[log.level] = (acc[log.level] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
return (
<div className="space-y-4">
{/* Header */}
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-zinc-800">
<Terminal className="text-emerald-400" size={20} />
</div>
<div>
<h2 className="text-lg font-semibold text-white">Debug Console</h2>
<p className="text-xs text-zinc-500">{logs.length} entries</p>
</div>
</div>
<div className="flex items-center gap-2">
<Chip
size="sm"
variant="flat"
className="bg-sky-500/20 text-sky-400"
>
{logCounts.info || 0} info
</Chip>
<Chip
size="sm"
variant="flat"
className="bg-amber-500/20 text-amber-400"
>
{logCounts.warn || 0} warn
</Chip>
<Chip
size="sm"
variant="flat"
className="bg-red-500/20 text-red-400"
>
{logCounts.error || 0} error
</Chip>
</div>
</div>
{/* Controls */}
<div className="flex flex-col md:flex-row gap-3">
<Input
placeholder="Filter logs..."
value={filter}
onValueChange={setFilter}
startContent={<Filter size={16} className="text-zinc-400" />}
classNames={{
input: 'bg-transparent',
inputWrapper: 'bg-zinc-900/50 border border-white/10',
}}
className="flex-1"
/>
<div className="flex gap-2">
<Button
size="sm"
variant={levelFilter === 'all' ? 'solid' : 'flat'}
className={levelFilter === 'all' ? 'bg-zinc-700' : 'bg-zinc-900'}
onPress={() => setLevelFilter('all')}
>
All
</Button>
{(['info', 'warn', 'error', 'debug'] as const).map((level) => (
<Button
key={level}
size="sm"
variant={levelFilter === level ? 'solid' : 'flat'}
className={levelFilter === level ? levelBg[level] : 'bg-zinc-900'}
onPress={() => setLevelFilter(level)}
>
{levelIcons[level]}
</Button>
))}
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="flat"
className="bg-zinc-900"
startContent={<Download size={14} />}
onPress={handleExport}
>
Export
</Button>
<Button
size="sm"
variant="flat"
className="bg-red-500/20 text-red-400"
startContent={<Trash2 size={14} />}
onPress={clearLogs}
>
Clear
</Button>
</div>
</div>
{/* Log List */}
<div className="crystal-card overflow-hidden">
<ScrollShadow className="h-[500px]">
<div className="font-mono text-sm">
{filteredLogs.length === 0 ? (
<div className="p-8 text-center text-zinc-500">
<Terminal size={32} className="mx-auto mb-2 opacity-50" />
<p>No logs to display</p>
</div>
) : (
filteredLogs.map((log, idx) => (
<motion.div
key={log.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: idx * 0.01 }}
className={`flex items-start gap-3 p-3 border-b border-white/5 hover:bg-white/5 ${
log.level === 'error' ? 'bg-red-500/5' : ''
}`}
>
<span className={`flex-shrink-0 ${levelColors[log.level]}`}>
{levelIcons[log.level]}
</span>
<span className="text-zinc-500 flex-shrink-0 w-20">
{new Date(log.timestamp).toLocaleTimeString()}
</span>
<span className="text-zinc-600 flex-shrink-0 w-16">
[{log.source}]
</span>
<span className="text-zinc-300 break-all flex-1">
{log.message}
</span>
{log.data !== undefined && (
<button
className="text-xs text-zinc-500 hover:text-zinc-300"
onClick={() => console.log('Log data:', log.data)}
>
[data]
</button>
)}
</motion.div>
))
)}
</div>
</ScrollShadow>
</div>
{/* Instructions */}
<div className="text-xs text-zinc-500 p-3 rounded-lg bg-zinc-900/50 border border-white/5">
<p className="font-medium text-zinc-400 mb-1">Debug Commands:</p>
<code className="text-sky-400">window.edgeNet.logs()</code> - View all logs<br />
<code className="text-sky-400">window.edgeNet.clear()</code> - Clear logs<br />
<code className="text-sky-400">window.edgeNet.stats()</code> - View log statistics<br />
<code className="text-sky-400">window.edgeNet.export()</code> - Export logs as JSON
</div>
</div>
);
}

View File

@@ -0,0 +1,197 @@
import { Card, CardBody, Button, Progress } from '@heroui/react';
import { motion } from 'framer-motion';
import { Coins, ArrowUpRight, ArrowDownRight, Clock, Wallet, TrendingUp } from 'lucide-react';
import { useNetworkStore } from '../../stores/networkStore';
export function CreditsPanel() {
const { credits, stats } = useNetworkStore();
const transactions = [
{ id: '1', type: 'earn' as const, amount: 25.50, description: 'Compute contribution', time: '2 min ago' },
{ id: '2', type: 'earn' as const, amount: 12.75, description: 'Task completion bonus', time: '15 min ago' },
{ id: '3', type: 'spend' as const, amount: -5.00, description: 'API request', time: '1 hour ago' },
{ id: '4', type: 'earn' as const, amount: 45.00, description: 'Neural training reward', time: '2 hours ago' },
{ id: '5', type: 'spend' as const, amount: -15.00, description: 'Premium feature', time: '3 hours ago' },
];
return (
<div className="space-y-6">
{/* Balance Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 border border-emerald-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Wallet className="text-emerald-400" size={24} />
<span className="text-xs text-emerald-400/70">Available</span>
</div>
<p className="text-3xl font-bold text-white">{credits.available.toFixed(2)}</p>
<p className="text-sm text-emerald-400 mt-1">Credits</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<Card className="bg-gradient-to-br from-amber-500/20 to-amber-600/10 border border-amber-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Clock className="text-amber-400" size={24} />
<span className="text-xs text-amber-400/70">Pending</span>
</div>
<p className="text-3xl font-bold text-white">{credits.pending.toFixed(2)}</p>
<p className="text-sm text-amber-400 mt-1">Credits</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-gradient-to-br from-sky-500/20 to-sky-600/10 border border-sky-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<TrendingUp className="text-sky-400" size={24} />
<span className="text-xs text-sky-400/70">Total Earned</span>
</div>
<p className="text-3xl font-bold text-white">{credits.earned.toFixed(2)}</p>
<p className="text-sm text-sky-400 mt-1">Credits</p>
</CardBody>
</Card>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<Card className="bg-gradient-to-br from-violet-500/20 to-violet-600/10 border border-violet-500/30">
<CardBody className="p-5">
<div className="flex items-center justify-between mb-2">
<Coins className="text-violet-400" size={24} />
<span className="text-xs text-violet-400/70">Net Balance</span>
</div>
<p className="text-3xl font-bold text-white">
{(credits.earned - credits.spent).toFixed(2)}
</p>
<p className="text-sm text-violet-400 mt-1">Credits</p>
</CardBody>
</Card>
</motion.div>
</div>
{/* Earning Progress */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.4 }}
>
<h3 className="text-lg font-semibold mb-4">Daily Earning Progress</h3>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-zinc-400">Compute Contribution</span>
<span className="text-emerald-400">45.8 / 100 TFLOPS</span>
</div>
<Progress
value={45.8}
maxValue={100}
classNames={{
indicator: 'bg-gradient-to-r from-emerald-500 to-cyan-500',
track: 'bg-zinc-800',
}}
/>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-zinc-400">Tasks Completed</span>
<span className="text-sky-400">89,432 / 100,000</span>
</div>
<Progress
value={89.432}
maxValue={100}
classNames={{
indicator: 'bg-gradient-to-r from-sky-500 to-violet-500',
track: 'bg-zinc-800',
}}
/>
</div>
<div>
<div className="flex justify-between text-sm mb-2">
<span className="text-zinc-400">Uptime Bonus</span>
<span className="text-violet-400">{stats.uptime.toFixed(1)}%</span>
</div>
<Progress
value={stats.uptime}
maxValue={100}
classNames={{
indicator: 'bg-gradient-to-r from-violet-500 to-pink-500',
track: 'bg-zinc-800',
}}
/>
</div>
</div>
</motion.div>
{/* Recent Transactions */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5 }}
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Recent Transactions</h3>
<Button size="sm" variant="flat" className="bg-white/5 text-zinc-400">
View All
</Button>
</div>
<div className="space-y-3">
{transactions.map((tx) => (
<div
key={tx.id}
className="flex items-center justify-between p-3 rounded-lg bg-zinc-800/50"
>
<div className="flex items-center gap-3">
<div
className={`p-2 rounded-full ${
tx.type === 'earn' ? 'bg-emerald-500/20' : 'bg-red-500/20'
}`}
>
{tx.type === 'earn' ? (
<ArrowUpRight className="text-emerald-400" size={16} />
) : (
<ArrowDownRight className="text-red-400" size={16} />
)}
</div>
<div>
<p className="text-sm font-medium text-white">{tx.description}</p>
<p className="text-xs text-zinc-500">{tx.time}</p>
</div>
</div>
<span
className={`font-semibold ${
tx.type === 'earn' ? 'text-emerald-400' : 'text-red-400'
}`}
>
{tx.type === 'earn' ? '+' : ''}{tx.amount.toFixed(2)}
</span>
</div>
))}
</div>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,133 @@
import { Button } from '@heroui/react';
import { motion } from 'framer-motion';
import { Activity, Wifi, WifiOff, Sun, Menu } from 'lucide-react';
import { useNetworkStore } from '../../stores/networkStore';
interface HeaderProps {
onMenuToggle?: () => void;
isMobile?: boolean;
}
function StatusChip({
icon,
label,
colorClass
}: {
icon: React.ReactNode;
label: string;
colorClass: string;
}) {
return (
<div className={`
inline-flex items-center gap-2 px-3 py-1.5 rounded-full
border text-xs font-medium
${colorClass}
`}>
<span className="flex-shrink-0 flex items-center">{icon}</span>
<span>{label}</span>
</div>
);
}
export function Header({ onMenuToggle, isMobile }: HeaderProps) {
const { isConnected, stats } = useNetworkStore();
// Defensive defaults for stats
const totalCompute = stats?.totalCompute ?? 0;
const activeNodes = stats?.activeNodes ?? 0;
return (
<header className="h-16 bg-zinc-900/50 backdrop-blur-xl border-b border-white/10 px-4 flex items-center">
{/* Left section */}
<div className="flex items-center gap-3">
{isMobile && onMenuToggle && (
<Button
isIconOnly
variant="light"
onPress={onMenuToggle}
className="text-zinc-400 hover:text-white"
>
<Menu size={20} />
</Button>
)}
{/* Crystal Logo */}
<motion.div
className="relative w-10 h-10 flex-shrink-0"
animate={{ rotate: 360 }}
transition={{ duration: 20, repeat: Infinity, ease: 'linear' }}
>
<div
className="absolute inset-0"
style={{
background: 'linear-gradient(135deg, #0ea5e9, #7c3aed, #06b6d4)',
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
}}
/>
<motion.div
className="absolute inset-2"
style={{
background: 'linear-gradient(135deg, #06b6d4, #0ea5e9)',
clipPath: 'polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)',
}}
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 2, repeat: Infinity }}
/>
</motion.div>
<div className="flex flex-col justify-center">
<span className="font-bold text-lg leading-tight bg-gradient-to-r from-sky-400 via-violet-400 to-cyan-400 bg-clip-text text-transparent">
Edge-Net
</span>
<span className="text-[10px] text-zinc-500 leading-tight">Collective AI Computing</span>
</div>
</div>
{/* Center section - Stats */}
<div className="flex-1 flex items-center justify-center gap-3 hidden md:flex">
<StatusChip
icon={<Activity size={14} />}
label={`${totalCompute.toFixed(1)} TFLOPS`}
colorClass="bg-sky-500/10 border-sky-500/30 text-sky-400"
/>
<StatusChip
icon={
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 1, repeat: Infinity }}
className="w-2 h-2 rounded-full bg-emerald-400"
/>
}
label={`${activeNodes.toLocaleString()} nodes`}
colorClass="bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
/>
</div>
{/* Right section */}
<div className="flex items-center gap-2">
<motion.div
animate={isConnected ? { opacity: [0.5, 1, 0.5] } : {}}
transition={{ duration: 2, repeat: Infinity }}
>
<StatusChip
icon={isConnected ? <Wifi size={14} /> : <WifiOff size={14} />}
label={isConnected ? 'Connected' : 'Offline'}
colorClass={isConnected
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
: 'bg-red-500/10 border-red-500/30 text-red-400'
}
/>
</motion.div>
<Button
isIconOnly
variant="light"
className="text-zinc-400 hover:text-white hidden sm:flex"
>
<Sun size={18} />
</Button>
</div>
</header>
);
}

View File

@@ -0,0 +1,404 @@
/**
* Settings Panel - Configuration for EdgeNet dashboard
*/
import { useState } from 'react';
import { motion } from 'framer-motion';
import {
Cpu,
Zap,
Battery,
Clock,
Bell,
Shield,
Database,
Globe,
Save,
Trash2,
Download,
Upload,
AlertTriangle,
Check,
} from 'lucide-react';
import { Button, Switch, Slider, Card, CardBody } from '@heroui/react';
import { useNetworkStore } from '../../stores/networkStore';
interface SettingsSection {
id: string;
title: string;
icon: React.ReactNode;
description: string;
}
const sections: SettingsSection[] = [
{ id: 'contribution', title: 'Contribution', icon: <Cpu size={20} />, description: 'Configure compute resource sharing' },
{ id: 'network', title: 'Network', icon: <Globe size={20} />, description: 'Network and relay settings' },
{ id: 'notifications', title: 'Notifications', icon: <Bell size={20} />, description: 'Alert and notification preferences' },
{ id: 'storage', title: 'Storage', icon: <Database size={20} />, description: 'Local data and cache management' },
{ id: 'security', title: 'Security', icon: <Shield size={20} />, description: 'Privacy and security options' },
];
export function SettingsPanel() {
const [activeSection, setActiveSection] = useState('contribution');
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'saved'>('idle');
// Get settings from store
const {
contributionSettings,
setContributionSettings,
clearLocalData,
} = useNetworkStore();
const handleSave = async () => {
setSaveStatus('saving');
await new Promise(resolve => setTimeout(resolve, 500));
setSaveStatus('saved');
setTimeout(() => setSaveStatus('idle'), 2000);
};
const handleClearData = () => {
if (confirm('Are you sure you want to clear all local data? This cannot be undone.')) {
clearLocalData();
window.location.reload();
}
};
const handleExportSettings = () => {
const settings = {
contribution: contributionSettings,
exportedAt: new Date().toISOString(),
};
const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'edge-net-settings.json';
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="space-y-6">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<h1 className="text-2xl md:text-3xl font-bold mb-2">
<span className="bg-gradient-to-r from-zinc-200 via-zinc-400 to-zinc-200 bg-clip-text text-transparent">
Settings
</span>
</h1>
<p className="text-zinc-400">
Configure your Edge-Net dashboard preferences
</p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sidebar */}
<motion.div
className="lg:col-span-1"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 }}
>
<div className="crystal-card p-2 space-y-1">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setActiveSection(section.id)}
className={`w-full flex items-center gap-3 p-3 rounded-lg transition-all text-left ${
activeSection === section.id
? 'bg-sky-500/20 text-sky-400 border border-sky-500/30'
: 'text-zinc-400 hover:bg-white/5 hover:text-white'
}`}
>
{section.icon}
<div>
<p className="font-medium text-sm">{section.title}</p>
<p className="text-xs text-zinc-500 hidden md:block">{section.description}</p>
</div>
</button>
))}
</div>
</motion.div>
{/* Content */}
<motion.div
className="lg:col-span-3"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<Card className="bg-zinc-900/50 border border-white/10">
<CardBody className="p-6">
{activeSection === 'contribution' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-white mb-1">Contribution Settings</h3>
<p className="text-sm text-zinc-400">Control how your device contributes to the network</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center gap-3">
<Cpu className="text-sky-400" size={20} />
<div>
<p className="font-medium text-white">Enable Contribution</p>
<p className="text-xs text-zinc-400">Share compute resources with the network</p>
</div>
</div>
<Switch
isSelected={contributionSettings.enabled}
onValueChange={(value) => setContributionSettings({ enabled: value })}
/>
</div>
<div className="p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<Cpu className="text-sky-400" size={20} />
<div>
<p className="font-medium text-white">CPU Limit</p>
<p className="text-xs text-zinc-400">Maximum CPU usage for tasks</p>
</div>
</div>
<span className="text-sky-400 font-bold">{contributionSettings.cpuLimit}%</span>
</div>
<Slider
size="sm"
step={5}
minValue={10}
maxValue={80}
value={contributionSettings.cpuLimit}
onChange={(value) => setContributionSettings({ cpuLimit: value as number })}
classNames={{
track: 'bg-zinc-700',
filler: 'bg-gradient-to-r from-sky-500 to-cyan-500',
}}
/>
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center gap-3">
<Zap className="text-violet-400" size={20} />
<div>
<p className="font-medium text-white">GPU Acceleration</p>
<p className="text-xs text-zinc-400">Use GPU for compatible tasks</p>
</div>
</div>
<Switch
isSelected={contributionSettings.gpuEnabled}
onValueChange={(value) => setContributionSettings({ gpuEnabled: value })}
/>
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center gap-3">
<Battery className="text-emerald-400" size={20} />
<div>
<p className="font-medium text-white">Respect Battery</p>
<p className="text-xs text-zinc-400">Pause when on battery power</p>
</div>
</div>
<Switch
isSelected={contributionSettings.respectBattery}
onValueChange={(value) => setContributionSettings({ respectBattery: value })}
/>
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center gap-3">
<Clock className="text-amber-400" size={20} />
<div>
<p className="font-medium text-white">Only When Idle</p>
<p className="text-xs text-zinc-400">Contribute only when browser is idle</p>
</div>
</div>
<Switch
isSelected={contributionSettings.onlyWhenIdle}
onValueChange={(value) => setContributionSettings({ onlyWhenIdle: value })}
/>
</div>
</div>
</div>
)}
{activeSection === 'network' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-white mb-1">Network Settings</h3>
<p className="text-sm text-zinc-400">Configure network connections and relay servers</p>
</div>
<div className="space-y-4">
<div className="p-4 bg-zinc-800/50 rounded-lg">
<p className="font-medium text-white mb-2">Relay Server</p>
<code className="block p-2 bg-zinc-900 rounded text-sm text-zinc-300 font-mono">
wss://edge-net-relay-875130704813.us-central1.run.app
</code>
</div>
<div className="p-4 bg-zinc-800/50 rounded-lg">
<p className="font-medium text-white mb-2">Firebase Project</p>
<code className="block p-2 bg-zinc-900 rounded text-sm text-zinc-300 font-mono">
ruv-edge-net (peer synchronization)
</code>
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">Auto-Reconnect</p>
<p className="text-xs text-zinc-400">Automatically reconnect to relay</p>
</div>
<Switch isSelected={true} isDisabled />
</div>
</div>
</div>
)}
{activeSection === 'notifications' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-white mb-1">Notification Settings</h3>
<p className="text-sm text-zinc-400">Control alerts and notifications</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">Credit Milestones</p>
<p className="text-xs text-zinc-400">Notify on earning milestones</p>
</div>
<Switch isSelected={true} />
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">Network Events</p>
<p className="text-xs text-zinc-400">Peer joins/leaves notifications</p>
</div>
<Switch isSelected={false} />
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">Task Completions</p>
<p className="text-xs text-zinc-400">Notify when tasks complete</p>
</div>
<Switch isSelected={true} />
</div>
</div>
</div>
)}
{activeSection === 'storage' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-white mb-1">Storage Settings</h3>
<p className="text-sm text-zinc-400">Manage local data and cache</p>
</div>
<div className="space-y-4">
<div className="p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center justify-between mb-2">
<p className="font-medium text-white">Local Storage</p>
<span className="text-sm text-zinc-400">IndexedDB</span>
</div>
<p className="text-xs text-zinc-400">Used for node state and credentials</p>
</div>
<div className="flex gap-3">
<Button
variant="flat"
className="bg-sky-500/20 text-sky-400"
startContent={<Download size={16} />}
onPress={handleExportSettings}
>
Export Settings
</Button>
<Button
variant="flat"
className="bg-violet-500/20 text-violet-400"
startContent={<Upload size={16} />}
>
Import Settings
</Button>
</div>
<div className="p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="text-red-400" size={16} />
<p className="font-medium text-red-400">Danger Zone</p>
</div>
<p className="text-xs text-zinc-400 mb-3">
Clear all local data including identity and credits. This cannot be undone.
</p>
<Button
variant="flat"
className="bg-red-500/20 text-red-400"
startContent={<Trash2 size={16} />}
onPress={handleClearData}
>
Clear All Data
</Button>
</div>
</div>
</div>
)}
{activeSection === 'security' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-white mb-1">Security Settings</h3>
<p className="text-sm text-zinc-400">Privacy and security options</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">WASM Sandbox</p>
<p className="text-xs text-zinc-400">Run tasks in isolated sandbox</p>
</div>
<Switch isSelected={true} isDisabled />
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">Verify Task Sources</p>
<p className="text-xs text-zinc-400">Only accept verified tasks</p>
</div>
<Switch isSelected={true} />
</div>
<div className="flex items-center justify-between p-4 bg-zinc-800/50 rounded-lg">
<div>
<p className="font-medium text-white">Anonymous Mode</p>
<p className="text-xs text-zinc-400">Hide identity from other peers</p>
</div>
<Switch isSelected={false} />
</div>
</div>
</div>
)}
{/* Save Button */}
<div className="flex justify-end pt-6 mt-6 border-t border-white/10">
<Button
className="bg-gradient-to-r from-sky-500 to-violet-500 text-white"
startContent={saveStatus === 'saved' ? <Check size={16} /> : <Save size={16} />}
isLoading={saveStatus === 'saving'}
onPress={handleSave}
>
{saveStatus === 'saved' ? 'Saved!' : 'Save Changes'}
</Button>
</div>
</CardBody>
</Card>
</motion.div>
</div>
</div>
);
}
export default SettingsPanel;

View File

@@ -0,0 +1,166 @@
import { Button } from '@heroui/react';
import { motion, AnimatePresence } from 'framer-motion';
import {
LayoutDashboard,
Network,
Cpu,
Package,
Wrench,
Terminal,
Settings,
X,
Coins,
Activity,
KeyRound,
BookOpen,
} from 'lucide-react';
import type { ReactNode } from 'react';
interface SidebarProps {
activeTab: string;
onTabChange: (tab: string) => void;
isOpen: boolean;
onClose: () => void;
isMobile: boolean;
}
interface NavItem {
id: string;
label: string;
icon: ReactNode;
badge?: number;
}
const navItems: NavItem[] = [
{ id: 'overview', label: 'Overview', icon: <LayoutDashboard size={18} /> },
{ id: 'identity', label: 'Identity', icon: <KeyRound size={18} /> },
{ id: 'network', label: 'Network', icon: <Network size={18} /> },
{ id: 'wasm', label: 'WASM Modules', icon: <Cpu size={18} /> },
{ id: 'cdn', label: 'CDN Scripts', icon: <Package size={18} /> },
{ id: 'mcp', label: 'MCP Tools', icon: <Wrench size={18} /> },
{ id: 'credits', label: 'Credits', icon: <Coins size={18} /> },
{ id: 'console', label: 'Console', icon: <Terminal size={18} /> },
{ id: 'docs', label: 'Documentation', icon: <BookOpen size={18} /> },
];
const bottomItems: NavItem[] = [
{ id: 'activity', label: 'Activity', icon: <Activity size={18} /> },
{ id: 'settings', label: 'Settings', icon: <Settings size={18} /> },
];
export function Sidebar({ activeTab, onTabChange, isOpen, onClose, isMobile }: SidebarProps) {
const NavButton = ({ item, activeColor = 'sky' }: { item: NavItem; activeColor?: string }) => {
const isActive = activeTab === item.id;
const colorClasses = activeColor === 'sky'
? 'bg-sky-500/20 text-sky-400 border-sky-500/30'
: 'bg-violet-500/20 text-violet-400 border-violet-500/30';
return (
<button
onClick={() => {
onTabChange(item.id);
if (isMobile) onClose();
}}
className={`
w-full h-10 px-3 rounded-lg
flex items-center gap-3
transition-all duration-200
${isActive
? `${colorClasses} border`
: 'text-zinc-400 hover:text-white hover:bg-white/5 border border-transparent'
}
`}
>
<span className="flex-shrink-0 flex items-center justify-center w-5">
{item.icon}
</span>
<span className="flex-1 text-left text-sm font-medium truncate">
{item.label}
</span>
{item.badge !== undefined && (
<span className="text-xs bg-sky-500/20 text-sky-400 px-2 py-0.5 rounded-full">
{item.badge.toLocaleString()}
</span>
)}
</button>
);
};
const content = (
<div className="flex flex-col h-full py-4">
{/* Close button (mobile) */}
{isMobile && (
<div className="flex justify-end px-4 mb-4">
<Button isIconOnly variant="light" onPress={onClose} className="text-zinc-400">
<X size={20} />
</Button>
</div>
)}
{/* Main Navigation */}
<nav className="flex-1 px-3">
<div className="space-y-1">
{navItems.map((item) => (
<NavButton key={item.id} item={item} activeColor="sky" />
))}
</div>
</nav>
{/* Divider */}
<div className="border-t border-white/10 mx-3 my-4" />
{/* Bottom Navigation */}
<nav className="px-3">
<div className="space-y-1">
{bottomItems.map((item) => (
<NavButton key={item.id} item={item} activeColor="violet" />
))}
</div>
</nav>
{/* Version info */}
<div className="px-4 pt-4 border-t border-white/10 mt-auto">
<p className="text-xs text-zinc-500">Edge-Net v0.1.1</p>
<p className="text-xs text-zinc-600">@ruvector/edge-net</p>
</div>
</div>
);
// Mobile: Slide-in drawer
if (isMobile) {
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
onClick={onClose}
/>
{/* Drawer */}
<motion.aside
initial={{ x: -280 }}
animate={{ x: 0 }}
exit={{ x: -280 }}
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
className="fixed left-0 top-0 bottom-0 w-[280px] bg-zinc-900/95 backdrop-blur-xl border-r border-white/10 z-50"
>
{content}
</motion.aside>
</>
)}
</AnimatePresence>
);
}
// Desktop: Static sidebar
return (
<aside className="w-[240px] bg-zinc-900/50 backdrop-blur-xl border-r border-white/10 flex-shrink-0">
{content}
</aside>
);
}

View File

@@ -0,0 +1,488 @@
import { useState } from 'react';
import { motion } from 'framer-motion';
import { Card, CardBody, Code, Snippet } from '@heroui/react';
import {
BookOpen,
Zap,
Shield,
Cpu,
Code2,
Terminal,
Wallet,
Users,
ChevronRight,
} from 'lucide-react';
interface DocSection {
id: string;
title: string;
icon: React.ReactNode;
content: React.ReactNode;
}
export function DocumentationPanel() {
const [selectedSection, setSelectedSection] = useState('getting-started');
const sections: DocSection[] = [
{
id: 'getting-started',
title: 'Getting Started',
icon: <BookOpen size={18} />,
content: <GettingStartedSection />,
},
{
id: 'how-it-works',
title: 'How It Works',
icon: <Zap size={18} />,
content: <HowItWorksSection />,
},
{
id: 'pi-key',
title: 'PiKey Identity',
icon: <Shield size={18} />,
content: <PiKeySection />,
},
{
id: 'contributing',
title: 'Contributing Compute',
icon: <Cpu size={18} />,
content: <ContributingSection />,
},
{
id: 'credits',
title: 'rUv Credits',
icon: <Wallet size={18} />,
content: <CreditsSection />,
},
{
id: 'api',
title: 'API Reference',
icon: <Code2 size={18} />,
content: <ApiSection />,
},
{
id: 'cli',
title: 'CLI Usage',
icon: <Terminal size={18} />,
content: <CliSection />,
},
];
return (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Navigation */}
<div className="lg:col-span-1">
<div className="crystal-card p-4 sticky top-4">
<h3 className="text-sm font-medium text-zinc-400 mb-4">Documentation</h3>
<nav className="space-y-1">
{sections.map((section) => (
<button
key={section.id}
onClick={() => setSelectedSection(section.id)}
className={`
w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm
transition-all duration-200
${
selectedSection === section.id
? 'bg-sky-500/20 text-sky-400 border border-sky-500/30'
: 'text-zinc-400 hover:text-white hover:bg-white/5'
}
`}
>
{section.icon}
<span>{section.title}</span>
{selectedSection === section.id && (
<ChevronRight size={14} className="ml-auto" />
)}
</button>
))}
</nav>
</div>
</div>
{/* Content */}
<div className="lg:col-span-3">
<motion.div
key={selectedSection}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
{sections.find((s) => s.id === selectedSection)?.content}
</motion.div>
</div>
</div>
);
}
function GettingStartedSection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">Welcome to Edge-Net</h2>
<p className="text-zinc-400">
Edge-Net is a collective AI computing network that allows you to share idle
browser resources and earn rUv credits in return.
</p>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Quick Start</h3>
<div className="space-y-3">
<div className="flex items-start gap-3 p-4 bg-zinc-800/50 rounded-lg">
<div className="w-6 h-6 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center text-sm font-bold">
1
</div>
<div>
<p className="text-white font-medium">Generate Your Identity</p>
<p className="text-sm text-zinc-400">
Go to the Identity tab and create a PiKey cryptographic identity.
This is your unique identifier on the network.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-zinc-800/50 rounded-lg">
<div className="w-6 h-6 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center text-sm font-bold">
2
</div>
<div>
<p className="text-white font-medium">Give Consent</p>
<p className="text-sm text-zinc-400">
Click the floating button in the bottom-right corner and accept
the consent dialog to start contributing.
</p>
</div>
</div>
<div className="flex items-start gap-3 p-4 bg-zinc-800/50 rounded-lg">
<div className="w-6 h-6 rounded-full bg-sky-500/20 text-sky-400 flex items-center justify-center text-sm font-bold">
3
</div>
<div>
<p className="text-white font-medium">Earn rUv Credits</p>
<p className="text-sm text-zinc-400">
Watch your credits grow as you contribute compute. Use them for
AI tasks or transfer to other users.
</p>
</div>
</div>
</div>
</div>
<div className="p-4 bg-emerald-500/10 border border-emerald-500/30 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Users size={18} className="text-emerald-400" />
<span className="font-medium text-emerald-400">Join the Collective</span>
</div>
<p className="text-sm text-zinc-300">
When you contribute, you become part of a decentralized network of
nodes working together to power AI computations.
</p>
</div>
</div>
);
}
function HowItWorksSection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">How Edge-Net Works</h2>
<p className="text-zinc-400">
Edge-Net uses WebAssembly (WASM) to run secure, sandboxed computations
in your browser.
</p>
</div>
<div className="grid gap-4">
<Card className="bg-zinc-800/50 border border-zinc-700">
<CardBody className="gap-3">
<h4 className="font-semibold text-sky-400">WASM Runtime</h4>
<p className="text-sm text-zinc-400">
All computations run in a WebAssembly sandbox, ensuring security
and isolation from your system.
</p>
</CardBody>
</Card>
<Card className="bg-zinc-800/50 border border-zinc-700">
<CardBody className="gap-3">
<h4 className="font-semibold text-violet-400">Time Crystal Sync</h4>
<p className="text-sm text-zinc-400">
Nodes synchronize using a novel time crystal protocol that ensures
coherent distributed computation without a central clock.
</p>
</CardBody>
</Card>
<Card className="bg-zinc-800/50 border border-zinc-700">
<CardBody className="gap-3">
<h4 className="font-semibold text-emerald-400">Adaptive Security</h4>
<p className="text-sm text-zinc-400">
Machine learning-based security system that detects and prevents
malicious activity in real-time.
</p>
</CardBody>
</Card>
</div>
</div>
);
}
function PiKeySection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">PiKey Cryptographic Identity</h2>
<p className="text-zinc-400">
PiKey provides a unique, mathematically-proven identity using Ed25519
cryptography with pi-based derivation.
</p>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Features</h3>
<ul className="space-y-2 text-zinc-300">
<li className="flex items-center gap-2">
<Shield size={16} className="text-sky-400" />
Ed25519 digital signatures
</li>
<li className="flex items-center gap-2">
<Shield size={16} className="text-violet-400" />
Argon2id encrypted backups
</li>
<li className="flex items-center gap-2">
<Shield size={16} className="text-emerald-400" />
Pi-magic verification for authenticity
</li>
<li className="flex items-center gap-2">
<Shield size={16} className="text-amber-400" />
Cross-platform portability
</li>
</ul>
</div>
<div>
<h3 className="text-lg font-semibold text-white mb-3">Backup Your Key</h3>
<p className="text-sm text-zinc-400 mb-4">
Always create an encrypted backup of your PiKey. Without it, you cannot
recover your identity or earned credits.
</p>
<Code className="w-full p-3 bg-zinc-900 text-sm">
{`// Export encrypted backup
const backup = piKey.createEncryptedBackup("your-password");
// Save backup hex string securely`}
</Code>
</div>
</div>
);
}
function ContributingSection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">Contributing Compute</h2>
<p className="text-zinc-400">
Share your idle browser resources to power AI computations and earn credits.
</p>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Resource Settings</h3>
<div className="grid gap-3">
<div className="p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Cpu size={16} className="text-sky-400" />
<span className="font-medium text-white">CPU Limit</span>
</div>
<p className="text-sm text-zinc-400">
Control how much CPU to allocate (10-80%). Higher values earn more
credits but may affect browser performance.
</p>
</div>
<div className="p-4 bg-zinc-800/50 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<Zap size={16} className="text-violet-400" />
<span className="font-medium text-white">GPU Acceleration</span>
</div>
<p className="text-sm text-zinc-400">
Enable WebGL/WebGPU for AI inference. Earns 3x more credits than
CPU-only contributions.
</p>
</div>
</div>
</div>
<div className="p-4 bg-amber-500/10 border border-amber-500/30 rounded-lg">
<p className="text-sm text-amber-300">
<strong>Privacy First:</strong> No personal data is collected. Your
identity is purely cryptographic, and all computations are sandboxed.
</p>
</div>
</div>
);
}
function CreditsSection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">rUv Credits</h2>
<p className="text-zinc-400">
rUv (Resource Utility Vouchers) are the currency of Edge-Net.
</p>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Credit Economy</h3>
<div className="grid gap-3">
<div className="flex justify-between items-center p-3 bg-zinc-800/50 rounded-lg">
<span className="text-zinc-300">CPU contribution (per hour)</span>
<span className="text-emerald-400 font-mono">~0.5 rUv</span>
</div>
<div className="flex justify-between items-center p-3 bg-zinc-800/50 rounded-lg">
<span className="text-zinc-300">GPU contribution (per hour)</span>
<span className="text-emerald-400 font-mono">~1.5 rUv</span>
</div>
<div className="flex justify-between items-center p-3 bg-zinc-800/50 rounded-lg">
<span className="text-zinc-300">AI inference task</span>
<span className="text-amber-400 font-mono">0.01-1.0 rUv</span>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold text-white mb-3">Use Cases</h3>
<ul className="space-y-2 text-zinc-300 text-sm">
<li>- Submit AI inference tasks to the network</li>
<li>- Access premium WASM modules</li>
<li>- Transfer to other network participants</li>
<li>- Reserve compute capacity for projects</li>
</ul>
</div>
</div>
);
}
function ApiSection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">API Reference</h2>
<p className="text-zinc-400">
Integrate Edge-Net into your applications using our JavaScript API.
</p>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Installation</h3>
<Snippet symbol="$" variant="bordered" className="bg-zinc-900">
npm install @ruvector/edge-net
</Snippet>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Basic Usage</h3>
<Code className="w-full p-4 bg-zinc-900 text-sm overflow-x-auto">
{`import init, { EdgeNetConfig, PiKey } from '@ruvector/edge-net';
// Initialize WASM
await init();
// Create identity
const piKey = new PiKey();
console.log('Node ID:', piKey.getShortId());
// Create and start node
const node = new EdgeNetConfig('my-app')
.cpuLimit(0.5)
.respectBattery(true)
.build();
node.start();
// Get stats
const stats = node.getStats();
console.log('Credits earned:', stats.ruv_earned);`}
</Code>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Key Classes</h3>
<div className="space-y-2">
<div className="p-3 bg-zinc-800/50 rounded-lg">
<code className="text-sky-400">EdgeNetNode</code>
<span className="text-zinc-400 text-sm ml-2">- Main node instance</span>
</div>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<code className="text-violet-400">PiKey</code>
<span className="text-zinc-400 text-sm ml-2">- Cryptographic identity</span>
</div>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<code className="text-emerald-400">AdaptiveSecurity</code>
<span className="text-zinc-400 text-sm ml-2">- ML security system</span>
</div>
<div className="p-3 bg-zinc-800/50 rounded-lg">
<code className="text-amber-400">TimeCrystal</code>
<span className="text-zinc-400 text-sm ml-2">- Distributed sync</span>
</div>
</div>
</div>
</div>
);
}
function CliSection() {
return (
<div className="crystal-card p-6 space-y-6">
<div>
<h2 className="text-xl font-bold text-white mb-2">CLI Usage</h2>
<p className="text-zinc-400">
Run Edge-Net from the command line for server-side contributions.
</p>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Install</h3>
<Snippet symbol="$" variant="bordered" className="bg-zinc-900">
npm install -g @ruvector/edge-net
</Snippet>
</div>
<div className="space-y-4">
<h3 className="text-lg font-semibold text-white">Commands</h3>
<div className="space-y-3">
<div className="p-3 bg-zinc-800/50 rounded-lg font-mono text-sm">
<div className="text-emerald-400">edge-net start</div>
<div className="text-zinc-500 mt-1">Start contributing node</div>
</div>
<div className="p-3 bg-zinc-800/50 rounded-lg font-mono text-sm">
<div className="text-emerald-400">edge-net status</div>
<div className="text-zinc-500 mt-1">View node status and stats</div>
</div>
<div className="p-3 bg-zinc-800/50 rounded-lg font-mono text-sm">
<div className="text-emerald-400">edge-net identity generate</div>
<div className="text-zinc-500 mt-1">Create new PiKey identity</div>
</div>
<div className="p-3 bg-zinc-800/50 rounded-lg font-mono text-sm">
<div className="text-emerald-400">edge-net credits balance</div>
<div className="text-zinc-500 mt-1">Check rUv credit balance</div>
</div>
</div>
</div>
<div className="p-4 bg-sky-500/10 border border-sky-500/30 rounded-lg">
<p className="text-sm text-sky-300">
<strong>Node.js Support:</strong> The CLI uses the same WASM module
as the browser, ensuring consistent behavior across platforms.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,624 @@
import { useState } from 'react';
import { Button, Card, CardBody, Input } from '@heroui/react';
import { motion, AnimatePresence } from 'framer-motion';
import {
User,
Key,
Shield,
Copy,
Check,
Download,
Upload,
Trash2,
Network,
Plus,
X,
Zap,
HardDrive,
Cpu,
Globe,
Star,
AlertCircle,
} from 'lucide-react';
import { useIdentityStore, availableNetworks } from '../../stores/identityStore';
const capabilityIcons: Record<string, React.ReactNode> = {
compute: <Cpu size={14} />,
storage: <HardDrive size={14} />,
relay: <Network size={14} />,
validation: <Shield size={14} />,
};
const capabilityDescriptions: Record<string, string> = {
compute: 'Contribute CPU/GPU compute power',
storage: 'Provide distributed storage',
relay: 'Act as a network relay node',
validation: 'Validate transactions and results',
};
function CopyButton({ text, label }: { text: string; label: string }) {
const [copied, setCopied] = useState(false);
const copy = async () => {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<button
onClick={copy}
className={`
flex items-center gap-1.5 px-2 py-1 rounded text-xs
transition-all border
${copied
? 'bg-emerald-500/20 border-emerald-500/30 text-emerald-400'
: 'bg-zinc-800 border-white/10 text-zinc-400 hover:text-white hover:border-white/20'
}
`}
>
{copied ? <Check size={12} /> : <Copy size={12} />}
{label}
</button>
);
}
function GenerateIdentityCard() {
const { generateIdentity, importIdentity, isGenerating, error } = useIdentityStore();
const [displayName, setDisplayName] = useState('');
const [showImport, setShowImport] = useState(false);
const [importKey, setImportKey] = useState('');
const handleGenerate = () => {
if (displayName.trim()) {
generateIdentity(displayName.trim());
}
};
const handleImport = () => {
if (importKey.trim()) {
importIdentity(importKey.trim());
setImportKey('');
setShowImport(false);
}
};
return (
<Card className="bg-zinc-900/50 border border-white/10">
<CardBody className="p-6">
<div className="text-center mb-6">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-sky-500 to-violet-500 flex items-center justify-center">
<Key size={32} className="text-white" />
</div>
<h2 className="text-xl font-semibold text-white">Create Your Identity</h2>
<p className="text-sm text-zinc-400 mt-1">
Generate a cryptographic identity to participate in Edge-Net
</p>
</div>
{error && (
<div className="mb-4 p-3 rounded-lg bg-red-500/10 border border-red-500/30 flex items-center gap-2 text-red-400 text-sm">
<AlertCircle size={16} />
{error}
</div>
)}
{!showImport ? (
<div className="space-y-4">
<div>
<label className="text-sm text-zinc-400 mb-1 block">Display Name</label>
<Input
placeholder="Enter your display name"
value={displayName}
onValueChange={setDisplayName}
classNames={{
input: 'bg-zinc-800 text-white',
inputWrapper: 'bg-zinc-800 border-white/10 hover:border-white/20',
}}
/>
</div>
<Button
className="w-full bg-gradient-to-r from-sky-500 to-violet-500 text-white"
isLoading={isGenerating}
isDisabled={!displayName.trim()}
onPress={handleGenerate}
>
<Key size={16} />
Generate Identity
</Button>
<div className="text-center">
<button
onClick={() => setShowImport(true)}
className="text-sm text-zinc-500 hover:text-zinc-300"
>
Or import existing identity
</button>
</div>
</div>
) : (
<div className="space-y-4">
<div>
<label className="text-sm text-zinc-400 mb-1 block">Private Key</label>
<Input
placeholder="Paste your private key (64 hex chars)"
value={importKey}
onValueChange={setImportKey}
type="password"
classNames={{
input: 'bg-zinc-800 text-white font-mono',
inputWrapper: 'bg-zinc-800 border-white/10 hover:border-white/20',
}}
/>
</div>
<div className="flex gap-2">
<Button
className="flex-1"
variant="flat"
onPress={() => setShowImport(false)}
>
Cancel
</Button>
<Button
className="flex-1 bg-sky-500/20 text-sky-400"
isLoading={isGenerating}
isDisabled={!importKey.trim()}
onPress={handleImport}
>
<Upload size={16} />
Import
</Button>
</div>
</div>
)}
</CardBody>
</Card>
);
}
function IdentityCard() {
const { identity, exportIdentity, clearIdentity } = useIdentityStore();
const [showConfirmClear, setShowConfirmClear] = useState(false);
if (!identity) return null;
const handleExport = async () => {
// For now, export without encryption (password prompt can be added later)
const exported = await exportIdentity('');
if (exported) {
const blob = new Blob([exported], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `edge-net-identity-${identity.id.substring(0, 8)}.json`;
a.click();
URL.revokeObjectURL(url);
}
};
return (
<Card className="bg-zinc-900/50 border border-emerald-500/30">
<CardBody className="p-4">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-emerald-500 to-cyan-500 flex items-center justify-center">
<User size={24} className="text-white" />
</div>
<div>
<h3 className="font-semibold text-white">{identity.displayName}</h3>
<p className="text-xs text-zinc-500">
Created {new Date(identity.createdAt).toLocaleDateString()}
</p>
</div>
</div>
<span className="px-2 py-1 rounded text-xs bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
Active
</span>
</div>
{/* Peer ID */}
<div className="mb-3">
<label className="text-xs text-zinc-500 mb-1 block">Peer ID</label>
<div className="flex items-center gap-2">
<code className="flex-1 bg-zinc-950 border border-white/10 rounded px-2 py-1.5 text-xs text-zinc-300 font-mono truncate">
{identity.id}
</code>
<CopyButton text={identity.id} label="Copy" />
</div>
</div>
{/* Public Key */}
<div className="mb-4">
<label className="text-xs text-zinc-500 mb-1 block">Public Key</label>
<div className="flex items-center gap-2">
<code className="flex-1 bg-zinc-950 border border-white/10 rounded px-2 py-1.5 text-xs text-zinc-300 font-mono truncate">
{identity.publicKey}
</code>
<CopyButton text={identity.publicKey} label="Copy" />
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<Button
size="sm"
variant="flat"
className="flex-1 bg-sky-500/20 text-sky-400"
onPress={handleExport}
>
<Download size={14} />
Export
</Button>
{!showConfirmClear ? (
<Button
size="sm"
variant="flat"
className="bg-red-500/10 text-red-400"
onPress={() => setShowConfirmClear(true)}
>
<Trash2 size={14} />
</Button>
) : (
<div className="flex gap-1">
<Button
size="sm"
variant="flat"
onPress={() => setShowConfirmClear(false)}
>
Cancel
</Button>
<Button
size="sm"
variant="flat"
className="bg-red-500/20 text-red-400"
onPress={() => {
clearIdentity();
setShowConfirmClear(false);
}}
>
Confirm Delete
</Button>
</div>
)}
</div>
</CardBody>
</Card>
);
}
function NetworkRegistrationModal({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) {
const { registrations, registerNetwork, isRegistering } = useIdentityStore();
const [selectedNetwork, setSelectedNetwork] = useState<string | null>(null);
const [selectedCapabilities, setSelectedCapabilities] = useState<string[]>(['compute']);
const unregisteredNetworks = availableNetworks.filter(
n => !registrations.some(r => r.networkId === n.id)
);
const handleRegister = async () => {
if (selectedNetwork) {
await registerNetwork(selectedNetwork, selectedCapabilities);
onClose();
setSelectedNetwork(null);
setSelectedCapabilities(['compute']);
}
};
const toggleCapability = (cap: string) => {
setSelectedCapabilities(prev =>
prev.includes(cap)
? prev.filter(c => c !== cap)
: [...prev, cap]
);
};
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50"
onClick={onClose}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] max-w-[90vw] bg-zinc-900 border border-white/10 rounded-xl z-50 overflow-hidden"
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center gap-3">
<Globe className="text-sky-400" size={20} />
<h2 className="font-semibold text-white">Join Network</h2>
</div>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-white/5 text-zinc-400 hover:text-white"
>
<X size={20} />
</button>
</div>
{/* Content */}
<div className="p-4 space-y-4">
{/* Network Selection */}
<div>
<label className="text-sm text-zinc-400 mb-2 block">Select Network</label>
<div className="space-y-2">
{unregisteredNetworks.map(network => (
<button
key={network.id}
onClick={() => setSelectedNetwork(network.id)}
className={`
w-full p-3 rounded-lg text-left transition-all border
${selectedNetwork === network.id
? 'bg-sky-500/20 border-sky-500/30'
: 'bg-zinc-800 border-white/10 hover:border-white/20'
}
`}
>
<div className="font-medium text-white">{network.name}</div>
<div className="text-xs text-zinc-500 mt-0.5">{network.description}</div>
</button>
))}
{unregisteredNetworks.length === 0 && (
<p className="text-center text-zinc-500 py-4">
Already registered to all available networks
</p>
)}
</div>
</div>
{/* Capabilities */}
{selectedNetwork && (
<div>
<label className="text-sm text-zinc-400 mb-2 block">Capabilities to Offer</label>
<div className="grid grid-cols-2 gap-2">
{Object.entries(capabilityDescriptions).map(([cap, desc]) => (
<button
key={cap}
onClick={() => toggleCapability(cap)}
className={`
p-3 rounded-lg text-left transition-all border
${selectedCapabilities.includes(cap)
? 'bg-emerald-500/20 border-emerald-500/30'
: 'bg-zinc-800 border-white/10 hover:border-white/20'
}
`}
>
<div className="flex items-center gap-2 mb-1">
{capabilityIcons[cap]}
<span className="font-medium text-white capitalize">{cap}</span>
</div>
<div className="text-xs text-zinc-500">{desc}</div>
</button>
))}
</div>
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-2 p-4 border-t border-white/10">
<Button variant="flat" onPress={onClose}>
Cancel
</Button>
<Button
className="bg-sky-500 text-white"
isLoading={isRegistering}
isDisabled={!selectedNetwork || selectedCapabilities.length === 0}
onPress={handleRegister}
>
<Plus size={16} />
Join Network
</Button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}
function NetworkCard({
registration,
}: {
registration: {
networkId: string;
networkName: string;
status: string;
joinedAt: Date;
capabilities: string[];
reputation: number;
creditsEarned: number;
};
}) {
const { leaveNetwork } = useIdentityStore();
const [showConfirmLeave, setShowConfirmLeave] = useState(false);
return (
<Card className="bg-zinc-900/50 border border-white/10">
<CardBody className="p-4">
<div className="flex items-start justify-between mb-3">
<div>
<h4 className="font-medium text-white">{registration.networkName}</h4>
<p className="text-xs text-zinc-500">
Joined {new Date(registration.joinedAt).toLocaleDateString()}
</p>
</div>
<span
className={`px-2 py-1 rounded text-xs ${
registration.status === 'active'
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/30'
: 'bg-amber-500/20 text-amber-400 border border-amber-500/30'
}`}
>
{registration.status}
</span>
</div>
{/* Stats */}
<div className="grid grid-cols-2 gap-3 mb-3">
<div className="bg-zinc-800 rounded p-2">
<div className="flex items-center gap-1 text-xs text-zinc-500 mb-1">
<Star size={12} />
Reputation
</div>
<div className="text-lg font-semibold text-white">{registration.reputation}</div>
</div>
<div className="bg-zinc-800 rounded p-2">
<div className="flex items-center gap-1 text-xs text-zinc-500 mb-1">
<Zap size={12} />
Credits
</div>
<div className="text-lg font-semibold text-emerald-400">
{registration.creditsEarned.toFixed(2)}
</div>
</div>
</div>
{/* Capabilities */}
<div className="mb-3">
<label className="text-xs text-zinc-500 mb-1 block">Capabilities</label>
<div className="flex flex-wrap gap-1">
{registration.capabilities.map(cap => (
<span
key={cap}
className="px-2 py-1 rounded text-xs bg-sky-500/20 text-sky-400 border border-sky-500/30 flex items-center gap-1"
>
{capabilityIcons[cap]}
{cap}
</span>
))}
</div>
</div>
{/* Actions */}
{!showConfirmLeave ? (
<Button
size="sm"
variant="flat"
className="w-full bg-red-500/10 text-red-400"
onPress={() => setShowConfirmLeave(true)}
>
Leave Network
</Button>
) : (
<div className="flex gap-2">
<Button
size="sm"
variant="flat"
className="flex-1"
onPress={() => setShowConfirmLeave(false)}
>
Cancel
</Button>
<Button
size="sm"
variant="flat"
className="flex-1 bg-red-500/20 text-red-400"
onPress={() => {
leaveNetwork(registration.networkId);
setShowConfirmLeave(false);
}}
>
Confirm Leave
</Button>
</div>
)}
</CardBody>
</Card>
);
}
export function IdentityPanel() {
const { identity, registrations } = useIdentityStore();
const [showRegisterModal, setShowRegisterModal] = useState(false);
return (
<div className="space-y-6">
{/* Identity Section */}
<div>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Key size={20} className="text-sky-400" />
Cryptographic Identity
</h2>
{!identity ? (
<GenerateIdentityCard />
) : (
<IdentityCard />
)}
</div>
{/* Network Registrations */}
{identity && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Globe size={20} className="text-violet-400" />
Network Registrations
</h2>
<Button
size="sm"
className="bg-sky-500/20 text-sky-400"
onPress={() => setShowRegisterModal(true)}
>
<Plus size={16} />
Join Network
</Button>
</div>
{registrations.length === 0 ? (
<Card className="bg-zinc-900/50 border border-white/10">
<CardBody className="p-8 text-center">
<Network size={48} className="mx-auto text-zinc-600 mb-4" />
<h3 className="text-lg font-medium text-zinc-400 mb-2">No Networks Joined</h3>
<p className="text-sm text-zinc-500 mb-4">
Join a network to start participating and earning credits
</p>
<Button
className="bg-sky-500 text-white"
onPress={() => setShowRegisterModal(true)}
>
<Plus size={16} />
Join Your First Network
</Button>
</CardBody>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{registrations.map(reg => (
<NetworkCard key={reg.networkId} registration={reg} />
))}
</div>
)}
</motion.div>
)}
{/* Registration Modal */}
<NetworkRegistrationModal
isOpen={showRegisterModal}
onClose={() => setShowRegisterModal(false)}
/>
</div>
);
}

View File

@@ -0,0 +1,215 @@
import { Button, Card, CardBody, Chip, Input, Tabs, Tab, ScrollShadow } from '@heroui/react';
import { motion } from 'framer-motion';
import { Play, Search, Users, Brain, Database, GitBranch, ListTodo, Loader2, Check, X } from 'lucide-react';
import { useState, useMemo } from 'react';
import { useMCPStore } from '../../stores/mcpStore';
import type { MCPTool } from '../../types';
const categoryIcons = {
swarm: <Users size={16} />,
agent: <Brain size={16} />,
memory: <Database size={16} />,
neural: <Brain size={16} />,
task: <ListTodo size={16} />,
github: <GitBranch size={16} />,
};
const categoryColors = {
swarm: 'from-sky-500/20 to-sky-600/10 border-sky-500/30',
agent: 'from-violet-500/20 to-violet-600/10 border-violet-500/30',
memory: 'from-cyan-500/20 to-cyan-600/10 border-cyan-500/30',
neural: 'from-emerald-500/20 to-emerald-600/10 border-emerald-500/30',
task: 'from-amber-500/20 to-amber-600/10 border-amber-500/30',
github: 'from-zinc-500/20 to-zinc-600/10 border-zinc-500/30',
};
const statusColors = {
ready: 'bg-emerald-500/20 text-emerald-400',
running: 'bg-sky-500/20 text-sky-400',
error: 'bg-red-500/20 text-red-400',
disabled: 'bg-zinc-500/20 text-zinc-400',
};
export function MCPTools() {
const { tools, results, activeTools, isConnected, executeTool } = useMCPStore();
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const categories = useMemo(() => {
const cats = [...new Set(tools.map((t) => t.category))];
return ['all', ...cats];
}, [tools]);
const filteredTools = useMemo(() => {
return tools.filter((tool) => {
const matchesSearch =
tool.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
tool.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategory === 'all' || tool.category === selectedCategory;
return matchesSearch && matchesCategory;
});
}, [tools, searchQuery, selectedCategory]);
const handleExecute = async (tool: MCPTool) => {
console.log(`[MCP] Executing tool: ${tool.id}`);
await executeTool(tool.id);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col md:flex-row gap-4 items-start md:items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">MCP Tools</h2>
<p className="text-sm text-zinc-400">
Execute Model Context Protocol tools for swarm coordination
</p>
</div>
<Chip
variant="flat"
className={isConnected ? 'bg-emerald-500/20 text-emerald-400' : 'bg-red-500/20 text-red-400'}
>
{isConnected ? 'Connected' : 'Disconnected'}
</Chip>
</div>
{/* Search and Filters */}
<div className="flex flex-col md:flex-row gap-4">
<Input
placeholder="Search tools..."
value={searchQuery}
onValueChange={setSearchQuery}
startContent={<Search size={18} className="text-zinc-400" />}
classNames={{
input: 'bg-transparent',
inputWrapper: 'bg-zinc-900/50 border border-white/10',
}}
className="flex-1"
/>
<Tabs
selectedKey={selectedCategory}
onSelectionChange={(key) => setSelectedCategory(key as string)}
variant="bordered"
classNames={{
tabList: 'bg-zinc-900/50 border-white/10',
cursor: 'bg-sky-500/20',
tab: 'text-zinc-400 data-[selected=true]:text-sky-400',
}}
>
{categories.map((cat) => (
<Tab
key={cat}
title={
<div className="flex items-center gap-1.5">
{cat !== 'all' && categoryIcons[cat as keyof typeof categoryIcons]}
<span className="capitalize">{cat}</span>
</div>
}
/>
))}
</Tabs>
</div>
{/* Tools Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredTools.map((tool, idx) => {
const isActive = activeTools.includes(tool.id);
return (
<motion.div
key={tool.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: idx * 0.05 }}
>
<Card
className={`bg-gradient-to-br ${categoryColors[tool.category]} border ${
isActive ? 'ring-2 ring-sky-500/50' : ''
}`}
>
<CardBody className="p-4">
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded bg-white/5">
{categoryIcons[tool.category]}
</div>
<div>
<h4 className="font-medium text-white">{tool.name}</h4>
<p className="text-xs text-zinc-500">{tool.id}</p>
</div>
</div>
<Chip size="sm" variant="flat" className={statusColors[tool.status]}>
{isActive ? (
<Loader2 size={12} className="animate-spin" />
) : tool.status === 'ready' ? (
<Check size={12} />
) : tool.status === 'error' ? (
<X size={12} />
) : null}
</Chip>
</div>
<p className="text-sm text-zinc-400 mb-4 line-clamp-2">
{tool.description}
</p>
<div className="flex items-center justify-between">
{tool.lastRun && (
<span className="text-xs text-zinc-500">
Last: {new Date(tool.lastRun).toLocaleTimeString()}
</span>
)}
<Button
size="sm"
variant="flat"
className="bg-white/10 text-white hover:bg-white/20 ml-auto"
isDisabled={isActive || tool.status === 'disabled'}
isLoading={isActive}
startContent={!isActive && <Play size={14} />}
onPress={() => handleExecute(tool)}
>
Execute
</Button>
</div>
</CardBody>
</Card>
</motion.div>
);
})}
</div>
{/* Recent Results */}
{results.length > 0 && (
<div className="crystal-card p-4">
<h3 className="text-lg font-semibold mb-3">Recent Results</h3>
<ScrollShadow className="max-h-[200px]">
<div className="space-y-2">
{results.slice(0, 10).map((result, idx) => (
<div
key={idx}
className={`p-3 rounded-lg border ${
result.success
? 'bg-emerald-500/10 border-emerald-500/30'
: 'bg-red-500/10 border-red-500/30'
}`}
>
<div className="flex items-center justify-between">
<span className="font-medium text-white">{result.toolId}</span>
<span className="text-xs text-zinc-400">
{result.duration.toFixed(0)}ms
</span>
</div>
{result.error && (
<p className="text-xs text-red-400 mt-1">{result.error}</p>
)}
</div>
))}
</div>
</ScrollShadow>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,185 @@
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Activity, Cpu, Users, Zap, Clock, Gauge } from 'lucide-react';
import { useNetworkStore } from '../../stores/networkStore';
import { StatCard } from '../common/StatCard';
// Format uptime seconds to human readable
function formatUptime(seconds: number): string {
if (seconds < 60) return `${Math.floor(seconds)}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return `${hours}h ${mins}m`;
}
// Session start time - only tracks current browser session
const sessionStart = Date.now();
export function NetworkStats() {
const { stats, timeCrystal, isRelayConnected, connectedPeers, contributionSettings } = useNetworkStore();
// Use React state for session-only uptime
const [sessionUptime, setSessionUptime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSessionUptime((Date.now() - sessionStart) / 1000);
}, 1000);
return () => clearInterval(interval);
}, []);
const statItems = [
{
title: 'Active Nodes',
value: stats.activeNodes,
icon: <Users size={24} />,
color: 'crystal' as const,
},
{
title: 'Total Compute',
value: `${stats.totalCompute.toFixed(1)} TFLOPS`,
icon: <Cpu size={24} />,
color: 'temporal' as const,
},
{
title: 'Tasks Completed',
value: stats.tasksCompleted,
icon: <Activity size={24} />,
color: 'quantum' as const,
},
{
title: 'Credits Earned',
value: `${stats.creditsEarned.toLocaleString()}`,
icon: <Zap size={24} />,
color: 'success' as const,
},
{
title: 'Network Latency',
value: `${stats.latency.toFixed(0)}ms`,
icon: <Clock size={24} />,
color: stats.latency < 50 ? 'success' as const : 'warning' as const,
},
{
title: 'This Session',
value: formatUptime(sessionUptime),
icon: <Gauge size={24} />,
color: 'success' as const,
},
];
return (
<div className="space-y-6">
{/* Connection Status Banner */}
{contributionSettings.enabled && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className={`p-3 rounded-lg border flex items-center justify-between ${
isRelayConnected
? 'bg-emerald-500/10 border-emerald-500/30'
: 'bg-amber-500/10 border-amber-500/30'
}`}
>
<div className="flex items-center gap-3">
<div
className={`w-2 h-2 rounded-full ${
isRelayConnected ? 'bg-emerald-400 animate-pulse' : 'bg-amber-400'
}`}
/>
<span className={isRelayConnected ? 'text-emerald-400' : 'text-amber-400'}>
{isRelayConnected
? `Connected to Edge-Net (${connectedPeers.length + 1} nodes)`
: 'Connecting to relay...'}
</span>
</div>
{isRelayConnected && (
<span className="text-xs text-zinc-500">
wss://edge-net-relay-...us-central1.run.app
</span>
)}
</motion.div>
)}
{/* Main Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{statItems.map((stat, index) => (
<motion.div
key={stat.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<StatCard {...stat} />
</motion.div>
))}
</div>
{/* Time Crystal Status */}
<motion.div
className="crystal-card p-6"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
>
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<motion.div
className={`w-3 h-3 rounded-full ${
isRelayConnected
? 'bg-gradient-to-r from-sky-400 to-violet-400'
: 'bg-zinc-500'
}`}
animate={isRelayConnected ? { scale: [1, 1.2, 1] } : {}}
transition={{ duration: 2, repeat: Infinity }}
/>
Time Crystal Synchronization
{!isRelayConnected && contributionSettings.enabled && (
<span className="text-xs text-amber-400 ml-2">(waiting for relay)</span>
)}
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 rounded-lg bg-sky-500/10 border border-sky-500/20">
<p className="text-2xl font-bold text-sky-400">
{(timeCrystal.phase * 100).toFixed(0)}%
</p>
<p className="text-xs text-zinc-400 mt-1">Phase</p>
</div>
<div className="text-center p-4 rounded-lg bg-violet-500/10 border border-violet-500/20">
<p className="text-2xl font-bold text-violet-400">
{timeCrystal.frequency.toFixed(3)}
</p>
<p className="text-xs text-zinc-400 mt-1">Frequency (φ)</p>
</div>
<div className="text-center p-4 rounded-lg bg-cyan-500/10 border border-cyan-500/20">
<p className="text-2xl font-bold text-cyan-400">
{(timeCrystal.coherence * 100).toFixed(1)}%
</p>
<p className="text-xs text-zinc-400 mt-1">Coherence</p>
</div>
<div className="text-center p-4 rounded-lg bg-emerald-500/10 border border-emerald-500/20">
<p className="text-2xl font-bold text-emerald-400">
{timeCrystal.synchronizedNodes}
</p>
<p className="text-xs text-zinc-400 mt-1">Synced Nodes</p>
</div>
</div>
{/* Crystal Animation */}
<div className="mt-6 h-2 bg-zinc-800 rounded-full overflow-hidden">
<motion.div
className="h-full bg-gradient-to-r from-sky-500 via-violet-500 to-cyan-500"
style={{ width: `${timeCrystal.coherence * 100}%` }}
animate={{
opacity: [0.7, 1, 0.7],
}}
transition={{ duration: 2, repeat: Infinity }}
/>
</div>
</motion.div>
</div>
);
}

View File

@@ -0,0 +1,129 @@
import { useEffect, useRef } from 'react';
import { motion } from 'framer-motion';
import { useNetworkStore } from '../../stores/networkStore';
interface Node {
x: number;
y: number;
vx: number;
vy: number;
connections: number[];
}
export function NetworkVisualization() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const nodesRef = useRef<Node[]>([]);
const animationRef = useRef<number | undefined>(undefined);
const { stats } = useNetworkStore();
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const resizeCanvas = () => {
canvas.width = canvas.offsetWidth * window.devicePixelRatio;
canvas.height = canvas.offsetHeight * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
// Initialize nodes
const nodeCount = 30;
nodesRef.current = Array.from({ length: nodeCount }, (_, i) => ({
x: Math.random() * canvas.offsetWidth,
y: Math.random() * canvas.offsetHeight,
vx: (Math.random() - 0.5) * 0.5,
vy: (Math.random() - 0.5) * 0.5,
connections: Array.from(
{ length: Math.floor(Math.random() * 3) + 1 },
() => Math.floor(Math.random() * nodeCount)
).filter((c) => c !== i),
}));
const animate = () => {
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
ctx.clearRect(0, 0, width, height);
// Update and draw nodes
nodesRef.current.forEach((node) => {
// Update position
node.x += node.vx;
node.y += node.vy;
// Bounce off edges
if (node.x < 0 || node.x > width) node.vx *= -1;
if (node.y < 0 || node.y > height) node.vy *= -1;
// Draw connections
node.connections.forEach((targetIdx) => {
const target = nodesRef.current[targetIdx];
if (target) {
const distance = Math.hypot(target.x - node.x, target.y - node.y);
const maxDistance = 150;
if (distance < maxDistance) {
const opacity = 1 - distance / maxDistance;
ctx.beginPath();
ctx.moveTo(node.x, node.y);
ctx.lineTo(target.x, target.y);
ctx.strokeStyle = `rgba(14, 165, 233, ${opacity * 0.3})`;
ctx.lineWidth = 1;
ctx.stroke();
}
}
});
});
// Draw nodes
nodesRef.current.forEach((node, i) => {
const isActive = i < Math.floor(nodeCount * (stats.activeNodes / stats.totalNodes));
// Glow
const gradient = ctx.createRadialGradient(node.x, node.y, 0, node.x, node.y, 15);
gradient.addColorStop(0, isActive ? 'rgba(14, 165, 233, 0.3)' : 'rgba(100, 100, 100, 0.1)');
gradient.addColorStop(1, 'transparent');
ctx.fillStyle = gradient;
ctx.fillRect(node.x - 15, node.y - 15, 30, 30);
// Node
ctx.beginPath();
ctx.arc(node.x, node.y, 4, 0, Math.PI * 2);
ctx.fillStyle = isActive ? '#0ea5e9' : '#52525b';
ctx.fill();
});
animationRef.current = requestAnimationFrame(animate);
};
animate();
return () => {
window.removeEventListener('resize', resizeCanvas);
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [stats.activeNodes, stats.totalNodes]);
return (
<motion.div
className="crystal-card p-4 h-[300px]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<h3 className="text-sm font-medium text-zinc-400 mb-2">Network Topology</h3>
<canvas
ref={canvasRef}
className="w-full h-full rounded-lg"
style={{ background: 'rgba(0, 0, 0, 0.3)' }}
/>
</motion.div>
);
}

View File

@@ -0,0 +1,588 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Microscope,
Radio,
TrendingUp,
Brain,
Gamepad2,
Users,
Server,
Zap,
Clock,
Award,
CheckCircle,
XCircle,
Loader2,
ChevronRight,
X,
Globe,
} from 'lucide-react';
import type { SpecializedNetwork } from '../../types';
import { useNetworkStore } from '../../stores/networkStore';
// Relay endpoint for real stats
const RELAY_URL = 'https://edge-net-relay-875130704813.us-central1.run.app';
// Relay stats interface
interface RelayStats {
nodes: number;
uptime: number;
tasks: number;
connectedNodes: string[];
}
// Fetch real network stats from relay
async function fetchRelayStats(): Promise<RelayStats> {
try {
const response = await fetch(`${RELAY_URL}/stats`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
return {
nodes: data.activeNodes || 0,
uptime: data.uptime || 0,
tasks: data.totalTasks || 0,
connectedNodes: data.connectedNodes || [],
};
} catch {
return { nodes: 0, uptime: 0, tasks: 0, connectedNodes: [] };
}
}
// Real network - Edge-Net Genesis (the only real one)
function createRealNetwork(relayStats: { nodes: number; uptime: number; tasks: number }): SpecializedNetwork {
const uptimePercent = relayStats.uptime > 0 ? Math.min(100, (relayStats.uptime / (24 * 60 * 60 * 1000)) * 100) : 0;
return {
id: 'edge-net-genesis',
name: 'Edge-Net Genesis',
description: 'The founding distributed compute network. Join to contribute idle CPU cycles and earn rUv credits.',
category: 'compute',
icon: 'globe',
color: 'sky',
stats: {
nodes: relayStats.nodes,
compute: relayStats.nodes * 0.5, // Estimate 0.5 TFLOPS per node
tasks: relayStats.tasks,
uptime: Number(uptimePercent.toFixed(1)),
},
requirements: { minCompute: 0.1, minBandwidth: 5, capabilities: ['compute'] },
rewards: { baseRate: 1.0, bonusMultiplier: 1.0 },
status: 'active',
joined: false,
};
}
// Planned networks - clearly marked as "Coming Soon"
const PLANNED_NETWORKS: SpecializedNetwork[] = [
{
id: 'medical-research',
name: 'MedGrid',
description: 'Planned: Distributed medical research computing for drug discovery and genomics analysis.',
category: 'healthcare',
icon: 'microscope',
color: 'rose',
stats: { nodes: 0, compute: 0, tasks: 0, uptime: 0 },
requirements: { minCompute: 0.5, minBandwidth: 10, capabilities: ['compute', 'storage'] },
rewards: { baseRate: 2.5, bonusMultiplier: 1.5 },
status: 'launching',
joined: false,
},
{
id: 'seti-search',
name: 'SETI@Edge',
description: 'Planned: Search for extraterrestrial intelligence by analyzing radio telescope data.',
category: 'science',
icon: 'radio',
color: 'violet',
stats: { nodes: 0, compute: 0, tasks: 0, uptime: 0 },
requirements: { minCompute: 0.2, minBandwidth: 5, capabilities: ['compute'] },
rewards: { baseRate: 1.0, bonusMultiplier: 1.2 },
status: 'launching',
joined: false,
},
{
id: 'ai-training',
name: 'NeuralMesh',
description: 'Planned: Distributed AI model training for open-source machine learning projects.',
category: 'ai',
icon: 'brain',
color: 'amber',
stats: { nodes: 0, compute: 0, tasks: 0, uptime: 0 },
requirements: { minCompute: 2.0, minBandwidth: 50, capabilities: ['compute', 'storage'] },
rewards: { baseRate: 3.5, bonusMultiplier: 1.8 },
status: 'launching',
joined: false,
},
{
id: 'game-rendering',
name: 'CloudPlay',
description: 'Planned: Cloud gaming infrastructure for low-latency game streaming.',
category: 'gaming',
icon: 'gamepad',
color: 'emerald',
stats: { nodes: 0, compute: 0, tasks: 0, uptime: 0 },
requirements: { minCompute: 1.5, minBandwidth: 200, capabilities: ['compute', 'relay'] },
rewards: { baseRate: 4.0, bonusMultiplier: 1.6 },
status: 'launching',
joined: false,
},
];
const iconMap: Record<string, React.ReactNode> = {
microscope: <Microscope size={24} />,
radio: <Radio size={24} />,
trending: <TrendingUp size={24} />,
brain: <Brain size={24} />,
gamepad: <Gamepad2 size={24} />,
users: <Users size={24} />,
globe: <Globe size={24} />,
};
const colorMap: Record<string, { bg: string; border: string; text: string; glow: string }> = {
rose: { bg: 'bg-rose-500/10', border: 'border-rose-500/30', text: 'text-rose-400', glow: 'shadow-rose-500/20' },
violet: { bg: 'bg-violet-500/10', border: 'border-violet-500/30', text: 'text-violet-400', glow: 'shadow-violet-500/20' },
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/30', text: 'text-emerald-400', glow: 'shadow-emerald-500/20' },
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/30', text: 'text-amber-400', glow: 'shadow-amber-500/20' },
sky: { bg: 'bg-sky-500/10', border: 'border-sky-500/30', text: 'text-sky-400', glow: 'shadow-sky-500/20' },
cyan: { bg: 'bg-cyan-500/10', border: 'border-cyan-500/30', text: 'text-cyan-400', glow: 'shadow-cyan-500/20' },
};
interface NetworkCardProps {
network: SpecializedNetwork;
onJoin: (id: string) => void;
onLeave: (id: string) => void;
onViewDetails: (network: SpecializedNetwork) => void;
}
function NetworkCard({ network, onJoin, onLeave, onViewDetails }: NetworkCardProps) {
const [isJoining, setIsJoining] = useState(false);
const colors = colorMap[network.color] || colorMap.sky;
const handleJoinToggle = async () => {
setIsJoining(true);
await new Promise((r) => setTimeout(r, 1000));
if (network.joined) {
onLeave(network.id);
} else {
onJoin(network.id);
}
setIsJoining(false);
};
const statusBadge = {
active: { label: 'Active', color: 'bg-emerald-500/20 text-emerald-400' },
maintenance: { label: 'Maintenance', color: 'bg-amber-500/20 text-amber-400' },
launching: { label: 'Coming Soon', color: 'bg-violet-500/20 text-violet-400' },
closed: { label: 'Closed', color: 'bg-zinc-500/20 text-zinc-400' },
}[network.status];
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className={`crystal-card p-5 ${network.joined ? `shadow-lg ${colors.glow}` : ''}`}
>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-xl ${colors.bg} ${colors.border} border ${colors.text}`}>
{iconMap[network.icon]}
</div>
<div>
<h3 className="font-semibold text-white flex items-center gap-2">
{network.name}
{network.joined && <CheckCircle size={16} className="text-emerald-400" />}
</h3>
<span className={`text-xs px-2 py-0.5 rounded-full ${statusBadge.color}`}>
{statusBadge.label}
</span>
</div>
</div>
</div>
{/* Description */}
<p className="text-sm text-zinc-400 mb-4 line-clamp-2">{network.description}</p>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="flex items-center gap-2 text-sm">
<Server size={14} className="text-zinc-500" />
<span className="text-zinc-400">{network.stats.nodes.toLocaleString()} nodes</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Zap size={14} className="text-zinc-500" />
<span className="text-zinc-400">{network.stats.compute.toFixed(1)} TFLOPS</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Clock size={14} className="text-zinc-500" />
<span className="text-zinc-400">{network.stats.uptime}% uptime</span>
</div>
<div className="flex items-center gap-2 text-sm">
<Award size={14} className={colors.text} />
<span className={colors.text}>{network.rewards.baseRate} cr/hr</span>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<button
onClick={handleJoinToggle}
disabled={isJoining || network.status === 'closed' || network.status === 'launching'}
className={`flex-1 h-9 rounded-lg font-medium text-sm flex items-center justify-center gap-2 transition-all
${network.joined
? 'bg-zinc-700 hover:bg-zinc-600 text-white'
: `${colors.bg} ${colors.border} border ${colors.text} hover:bg-opacity-20`
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{isJoining ? (
<Loader2 size={16} className="animate-spin" />
) : network.joined ? (
<>
<XCircle size={16} /> Leave
</>
) : (
<>
<CheckCircle size={16} /> Join
</>
)}
</button>
<button
onClick={() => onViewDetails(network)}
className="h-9 px-3 rounded-lg bg-white/5 hover:bg-white/10 border border-white/10 transition-colors"
>
<ChevronRight size={16} className="text-zinc-400" />
</button>
</div>
</motion.div>
);
}
interface NetworkDetailsModalProps {
network: SpecializedNetwork | null;
onClose: () => void;
onJoin: (id: string) => void;
onLeave: (id: string) => void;
}
function NetworkDetailsModal({ network, onClose, onJoin, onLeave }: NetworkDetailsModalProps) {
if (!network) return null;
const colors = colorMap[network.color] || colorMap.sky;
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="bg-zinc-900 border border-white/10 rounded-xl max-w-lg w-full max-h-[80vh] overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className={`p-6 ${colors.bg} border-b ${colors.border}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`p-4 rounded-xl bg-black/20 ${colors.text}`}>
{iconMap[network.icon]}
</div>
<div>
<h2 className="text-xl font-bold text-white">{network.name}</h2>
<p className="text-sm text-zinc-400">{network.category.charAt(0).toUpperCase() + network.category.slice(1)} Network</p>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-lg transition-colors">
<X size={20} className="text-zinc-400" />
</button>
</div>
</div>
{/* Content */}
<div className="p-6 space-y-6 overflow-auto max-h-[50vh]">
<div>
<h3 className="text-sm font-medium text-zinc-400 mb-2">About</h3>
<p className="text-white">{network.description}</p>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-400 mb-3">Network Statistics</h3>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-white/5 rounded-lg">
<p className="text-2xl font-bold text-white">{network.stats.nodes.toLocaleString()}</p>
<p className="text-xs text-zinc-400">Active Nodes</p>
</div>
<div className="p-3 bg-white/5 rounded-lg">
<p className="text-2xl font-bold text-white">{network.stats.compute.toFixed(1)}</p>
<p className="text-xs text-zinc-400">Total TFLOPS</p>
</div>
<div className="p-3 bg-white/5 rounded-lg">
<p className="text-2xl font-bold text-white">{network.stats.tasks.toLocaleString()}</p>
<p className="text-xs text-zinc-400">Tasks Completed</p>
</div>
<div className="p-3 bg-white/5 rounded-lg">
<p className="text-2xl font-bold text-white">{network.stats.uptime}%</p>
<p className="text-xs text-zinc-400">Network Uptime</p>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-400 mb-3">Requirements</h3>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-zinc-400">Minimum Compute</span>
<span className="text-white">{network.requirements.minCompute} TFLOPS</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-zinc-400">Minimum Bandwidth</span>
<span className="text-white">{network.requirements.minBandwidth} Mbps</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-zinc-400">Required Capabilities</span>
<span className="text-white">{network.requirements.capabilities.join(', ')}</span>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-zinc-400 mb-3">Rewards</h3>
<div className="p-4 bg-gradient-to-r from-amber-500/10 to-orange-500/10 border border-amber-500/30 rounded-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-amber-400 font-medium">Base Rate</span>
<span className="text-xl font-bold text-white">{network.rewards.baseRate} credits/hour</span>
</div>
<div className="flex items-center justify-between">
<span className="text-amber-400 font-medium">Bonus Multiplier</span>
<span className="text-lg font-semibold text-white">{network.rewards.bonusMultiplier}x</span>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="p-6 border-t border-white/10">
<button
onClick={() => {
network.joined ? onLeave(network.id) : onJoin(network.id);
onClose();
}}
disabled={network.status === 'closed' || network.status === 'launching'}
className={`w-full h-11 rounded-lg font-medium flex items-center justify-center gap-2 transition-all
${network.joined
? 'bg-zinc-700 hover:bg-zinc-600 text-white'
: `bg-gradient-to-r from-sky-500 to-violet-500 text-white hover:opacity-90`
}
disabled:opacity-50 disabled:cursor-not-allowed
`}
>
{network.joined ? (
<>
<XCircle size={18} /> Leave Network
</>
) : (
<>
<CheckCircle size={18} /> Join Network
</>
)}
</button>
</div>
</motion.div>
</motion.div>
);
}
// Persist joined networks to localStorage
const STORAGE_KEY = 'edge-net-joined-networks';
function loadJoinedIds(): Set<string> {
try {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? new Set(JSON.parse(saved)) : new Set();
} catch {
return new Set();
}
}
function saveJoinedIds(ids: Set<string>) {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...ids]));
}
export function SpecializedNetworks() {
const [networks, setNetworks] = useState<SpecializedNetwork[]>([]);
const [selectedNetwork, setSelectedNetwork] = useState<SpecializedNetwork | null>(null);
const [filter, setFilter] = useState<string>('all');
const [isLoading, setIsLoading] = useState(true);
const [joinedIds, setJoinedIds] = useState<Set<string>>(loadJoinedIds);
// Connect to the network store for real contribution
const { contributionSettings, startContributing, stopContributing, giveConsent } = useNetworkStore();
// Sync join status with contribution status
useEffect(() => {
if (contributionSettings.enabled && !joinedIds.has('edge-net-genesis')) {
const newJoinedIds = new Set(joinedIds);
newJoinedIds.add('edge-net-genesis');
setJoinedIds(newJoinedIds);
saveJoinedIds(newJoinedIds);
}
}, [contributionSettings.enabled, joinedIds]);
// Fetch real stats on mount and periodically
useEffect(() => {
const loadRealStats = async () => {
const relayStats = await fetchRelayStats();
const realNetwork = createRealNetwork(relayStats);
const allNetworks = [realNetwork, ...PLANNED_NETWORKS];
// Apply persisted join status, but Edge-Net Genesis follows contribution status
setNetworks(allNetworks.map(n => ({
...n,
joined: n.id === 'edge-net-genesis'
? contributionSettings.enabled || joinedIds.has(n.id)
: joinedIds.has(n.id),
})));
setIsLoading(false);
};
loadRealStats();
const interval = setInterval(loadRealStats, 10000); // Refresh every 10s
return () => clearInterval(interval);
}, [joinedIds, contributionSettings.enabled]);
const handleJoin = (id: string) => {
const newJoinedIds = new Set(joinedIds);
newJoinedIds.add(id);
setJoinedIds(newJoinedIds);
saveJoinedIds(newJoinedIds);
setNetworks((prev) =>
prev.map((n) => (n.id === id ? { ...n, joined: true, joinedAt: new Date() } : n))
);
// For Edge-Net Genesis, actually start contributing to the network
if (id === 'edge-net-genesis') {
if (!contributionSettings.consentGiven) {
giveConsent();
}
startContributing();
console.log('[Networks] Joined Edge-Net Genesis - started contributing');
}
};
const handleLeave = (id: string) => {
const newJoinedIds = new Set(joinedIds);
newJoinedIds.delete(id);
setJoinedIds(newJoinedIds);
saveJoinedIds(newJoinedIds);
setNetworks((prev) =>
prev.map((n) => (n.id === id ? { ...n, joined: false, joinedAt: undefined } : n))
);
// For Edge-Net Genesis, stop contributing
if (id === 'edge-net-genesis') {
stopContributing();
console.log('[Networks] Left Edge-Net Genesis - stopped contributing');
}
};
const categories = ['all', 'compute', 'science', 'healthcare', 'ai', 'gaming'];
const filteredNetworks = filter === 'all'
? networks
: networks.filter((n) => n.category === filter);
const joinedCount = networks.filter((n) => n.joined).length;
const totalEarnings = networks
.filter((n) => n.joined)
.reduce((sum, n) => sum + n.rewards.baseRate, 0);
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-sky-400" />
<span className="ml-3 text-zinc-400">Fetching network data...</span>
</div>
);
}
return (
<div className="space-y-6">
{/* Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="crystal-card p-4"
>
<p className="text-sm text-zinc-400 mb-1">Joined Networks</p>
<p className="text-2xl font-bold text-white">{joinedCount}</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="crystal-card p-4"
>
<p className="text-sm text-zinc-400 mb-1">Available Networks</p>
<p className="text-2xl font-bold text-white">{networks.filter((n) => n.status === 'active').length}</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="crystal-card p-4"
>
<p className="text-sm text-zinc-400 mb-1">Potential Earnings</p>
<p className="text-2xl font-bold text-amber-400">{totalEarnings.toFixed(1)} cr/hr</p>
</motion.div>
</div>
{/* Filter */}
<div className="flex gap-2 overflow-x-auto pb-2">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setFilter(cat)}
className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-all
${filter === cat
? 'bg-sky-500/20 text-sky-400 border border-sky-500/30'
: 'bg-white/5 text-zinc-400 hover:bg-white/10 border border-transparent'
}
`}
>
{cat.charAt(0).toUpperCase() + cat.slice(1)}
</button>
))}
</div>
{/* Network Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredNetworks.map((network) => (
<NetworkCard
key={network.id}
network={network}
onJoin={handleJoin}
onLeave={handleLeave}
onViewDetails={setSelectedNetwork}
/>
))}
</div>
{/* Details Modal */}
<AnimatePresence>
{selectedNetwork && (
<NetworkDetailsModal
network={selectedNetwork}
onClose={() => setSelectedNetwork(null)}
onJoin={handleJoin}
onLeave={handleLeave}
/>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,225 @@
import { Button, Card, CardBody, Chip, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from '@heroui/react';
import { motion } from 'framer-motion';
import { Cpu, BarChart3, Check, AlertCircle, Loader2 } from 'lucide-react';
import { useState } from 'react';
import { useWASMStore } from '../../stores/wasmStore';
import type { WASMModule, WASMBenchmark } from '../../types';
const statusColors = {
loading: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
ready: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
error: 'bg-red-500/20 text-red-400 border-red-500/30',
unloaded: 'bg-zinc-500/20 text-zinc-400 border-zinc-500/30',
};
const statusIcons = {
loading: <Loader2 size={14} className="animate-spin" />,
ready: <Check size={14} />,
error: <AlertCircle size={14} />,
unloaded: <Cpu size={14} />,
};
export function WASMModules() {
const { modules, benchmarks, loadModule, runBenchmark } = useWASMStore();
const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedModule, setSelectedModule] = useState<WASMModule | null>(null);
const [selectedBenchmark, setSelectedBenchmark] = useState<WASMBenchmark | null>(null);
const formatSize = (bytes: number) => {
if (bytes >= 1000000) return `${(bytes / 1000000).toFixed(1)} MB`;
return `${(bytes / 1000).toFixed(0)} KB`;
};
const handleBenchmark = async (module: WASMModule) => {
setSelectedModule(module);
onOpen();
const result = await runBenchmark(module.id);
setSelectedBenchmark(result);
};
const loadedCount = modules.filter((m) => m.loaded).length;
return (
<div className="space-y-6">
{/* Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<p className="text-sm text-zinc-400">Total Modules</p>
<p className="text-3xl font-bold text-white">{modules.length}</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<p className="text-sm text-zinc-400">Loaded</p>
<p className="text-3xl font-bold text-emerald-400">{loadedCount}</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<p className="text-sm text-zinc-400">Total Size</p>
<p className="text-3xl font-bold text-sky-400">
{formatSize(modules.reduce((acc, m) => acc + m.size, 0))}
</p>
</motion.div>
<motion.div
className="crystal-card p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<p className="text-sm text-zinc-400">Benchmarks Run</p>
<p className="text-3xl font-bold text-violet-400">{benchmarks.length}</p>
</motion.div>
</div>
{/* Module List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{modules.map((module, idx) => (
<motion.div
key={module.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * idx }}
>
<Card className="bg-zinc-900/50 border border-white/10 hover:border-sky-500/30 transition-colors">
<CardBody className="p-5">
<div className="flex items-start justify-between mb-3">
<div>
<h4 className="font-semibold text-white text-lg">{module.name}</h4>
<p className="text-xs text-zinc-500">v{module.version}</p>
</div>
<Chip
size="sm"
variant="bordered"
startContent={statusIcons[module.status]}
className={statusColors[module.status]}
>
{module.status}
</Chip>
</div>
<div className="flex flex-wrap gap-1.5 mb-4">
{module.features.map((feature) => (
<Chip
key={feature}
size="sm"
variant="flat"
className="bg-zinc-800 text-zinc-400 text-xs"
>
{feature}
</Chip>
))}
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-zinc-500">{formatSize(module.size)}</span>
<div className="flex gap-2">
<Button
size="sm"
variant="flat"
className="bg-sky-500/20 text-sky-400"
isDisabled={module.loaded || module.status === 'loading'}
isLoading={module.status === 'loading'}
onPress={() => loadModule(module.id)}
>
{module.loaded ? 'Loaded' : 'Load'}
</Button>
<Button
size="sm"
variant="flat"
className="bg-violet-500/20 text-violet-400"
isDisabled={!module.loaded}
startContent={<BarChart3 size={14} />}
onPress={() => handleBenchmark(module)}
>
Benchmark
</Button>
</div>
</div>
{module.error && (
<div className="mt-3 p-2 rounded bg-red-500/10 border border-red-500/30">
<p className="text-xs text-red-400">{module.error}</p>
</div>
)}
</CardBody>
</Card>
</motion.div>
))}
</div>
{/* Benchmark Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="lg" className="dark">
<ModalContent className="bg-zinc-900 border border-white/10">
<ModalHeader className="border-b border-white/10">
<div className="flex items-center gap-2">
<BarChart3 className="text-violet-400" size={20} />
<span>Benchmark Results</span>
</div>
</ModalHeader>
<ModalBody className="py-6">
{selectedModule && (
<div className="space-y-4">
<div>
<p className="text-sm text-zinc-400">Module</p>
<p className="text-lg font-semibold text-white">{selectedModule.name}</p>
</div>
{selectedBenchmark ? (
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-zinc-800/50">
<p className="text-xs text-zinc-400">Iterations</p>
<p className="text-2xl font-bold text-sky-400">
{selectedBenchmark.iterations.toLocaleString()}
</p>
</div>
<div className="p-4 rounded-lg bg-zinc-800/50">
<p className="text-xs text-zinc-400">Avg Time</p>
<p className="text-2xl font-bold text-violet-400">
{selectedBenchmark.avgTime.toFixed(3)}ms
</p>
</div>
<div className="p-4 rounded-lg bg-zinc-800/50">
<p className="text-xs text-zinc-400">Min/Max</p>
<p className="text-lg font-bold text-cyan-400">
{selectedBenchmark.minTime.toFixed(3)} / {selectedBenchmark.maxTime.toFixed(3)}ms
</p>
</div>
<div className="p-4 rounded-lg bg-zinc-800/50">
<p className="text-xs text-zinc-400">Throughput</p>
<p className="text-2xl font-bold text-emerald-400">
{selectedBenchmark.throughput.toFixed(0)}/s
</p>
</div>
</div>
) : (
<div className="flex items-center justify-center py-8">
<Loader2 className="animate-spin text-sky-400" size={32} />
</div>
)}
</div>
)}
</ModalBody>
<ModalFooter className="border-t border-white/10">
<Button variant="flat" onPress={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { useState, useEffect } from 'react';
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
// Set initial value
setMatches(media.matches);
// Create listener
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
// Add listener
media.addEventListener('change', listener);
// Cleanup
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// Convenience hooks for common breakpoints
export function useIsMobile() {
return useMediaQuery('(max-width: 768px)');
}
export function useIsTablet() {
return useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
}
export function useIsDesktop() {
return useMediaQuery('(min-width: 1025px)');
}
export function usePrefersDarkMode() {
return useMediaQuery('(prefers-color-scheme: dark)');
}
export function usePrefersReducedMotion() {
return useMediaQuery('(prefers-reduced-motion: reduce)');
}

View File

@@ -0,0 +1,154 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Time Crystal Theme Base Styles */
:root {
--crystal-glow: rgba(14, 165, 233, 0.5);
--temporal-glow: rgba(124, 58, 237, 0.5);
--quantum-glow: rgba(6, 182, 212, 0.5);
}
/* Dark mode base */
html {
color-scheme: dark;
}
body {
@apply bg-[#0a0a0f] text-zinc-200 antialiased;
background-image:
radial-gradient(ellipse at 20% 30%, rgba(14, 165, 233, 0.05) 0%, transparent 50%),
radial-gradient(ellipse at 80% 70%, rgba(124, 58, 237, 0.05) 0%, transparent 50%),
radial-gradient(ellipse at 50% 50%, rgba(6, 182, 212, 0.03) 0%, transparent 70%);
min-height: 100vh;
margin: 0;
}
/* Time Crystal Card Effects */
.crystal-card {
@apply relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-xl;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
.crystal-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(14, 165, 233, 0.5), transparent);
}
.crystal-card:hover {
border-color: rgba(14, 165, 233, 0.3);
box-shadow:
0 0 0 1px rgba(14, 165, 233, 0.1),
0 4px 20px -2px rgba(14, 165, 233, 0.15),
0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
/* Glowing elements */
.glow-text {
text-shadow: 0 0 10px var(--crystal-glow), 0 0 20px var(--crystal-glow);
}
.glow-border {
box-shadow: 0 0 10px var(--crystal-glow), inset 0 0 10px rgba(14, 165, 233, 0.1);
}
/* Time Crystal Animation */
.crystal-pulse {
animation: crystal-pulse 2s ease-in-out infinite;
}
@keyframes crystal-pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.8;
transform: scale(1.02);
}
}
/* Data stream animation */
.data-stream {
background: linear-gradient(
90deg,
transparent,
rgba(14, 165, 233, 0.3),
rgba(124, 58, 237, 0.3),
transparent
);
background-size: 200% 100%;
animation: data-flow 2s linear infinite;
}
@keyframes data-flow {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Quantum grid background */
.quantum-grid {
background-image:
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
background-size: 50px 50px;
}
/* Network node visualization */
.network-node {
@apply relative;
}
.network-node::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
background: radial-gradient(circle, rgba(14, 165, 233, 0.3) 0%, transparent 70%);
animation: node-pulse 2s ease-in-out infinite;
}
@keyframes node-pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.5); opacity: 0; }
}
/* Stat counter animation */
.stat-value {
font-variant-numeric: tabular-nums;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: rgba(14, 165, 233, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(14, 165, 233, 0.5);
}
/* Mobile optimizations */
@media (max-width: 768px) {
.crystal-card {
@apply rounded-lg;
}
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { HeroUIProvider } from '@heroui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './index.css';
import { initDebugConsole } from './utils/debug';
// Initialize debug console
initDebugConsole();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000,
refetchInterval: 10000,
},
},
});
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<HeroUIProvider>
<main className="dark min-h-screen">
<App />
</main>
</HeroUIProvider>
</QueryClientProvider>
</React.StrictMode>
);

View File

@@ -0,0 +1,546 @@
/**
* EdgeNet Service - Real WASM Integration
*
* Provides real EdgeNetNode and PiKey functionality from the WASM module.
* All operations are secure and use actual cryptographic primitives.
*/
// Types from the WASM module
export interface NodeStats {
ruv_earned: bigint;
ruv_spent: bigint;
tasks_completed: bigint;
tasks_submitted: bigint;
uptime_seconds: bigint;
reputation: number;
multiplier: number;
celebration_boost: number;
}
export interface EdgeNetModule {
default: (input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module) => Promise<void>;
PiKey: new (genesis_seed?: Uint8Array | null) => PiKeyInstance;
EdgeNetNode: new (site_id: string, config?: NodeConfigInstance | null) => EdgeNetNodeInstance;
EdgeNetConfig: new (site_id: string) => EdgeNetConfigInstance;
BrowserFingerprint: { generate(): Promise<string> };
AdaptiveSecurity: new () => AdaptiveSecurityInstance;
TimeCrystal: new (frequency: number) => TimeCrystalInstance;
}
export interface PiKeyInstance {
free(): void;
getIdentity(): Uint8Array;
getIdentityHex(): string;
getShortId(): string;
getPublicKey(): Uint8Array;
sign(data: Uint8Array): Uint8Array;
verify(data: Uint8Array, signature: Uint8Array, public_key: Uint8Array): boolean;
createEncryptedBackup(password: string): Uint8Array;
exportCompact(): Uint8Array;
getStats(): string;
verifyPiMagic(): boolean;
getGenesisFingerprint(): Uint8Array;
}
export interface NodeConfigInstance {
cpu_limit: number;
memory_limit: number;
bandwidth_limit: number;
min_idle_time: number;
respect_battery: boolean;
}
export interface EdgeNetNodeInstance {
free(): void;
nodeId(): string;
start(): void;
pause(): void;
resume(): void;
disconnect(): void;
isIdle(): boolean;
creditBalance(): bigint;
ruvBalance(): bigint;
getStats(): NodeStats;
getThrottle(): number;
getMultiplier(): number;
getTreasury(): bigint;
getProtocolFund(): bigint;
getMerkleRoot(): string;
getNetworkFitness(): number;
getTimeCrystalSync(): number;
getConflictCount(): number;
getQuarantinedCount(): number;
getCoherenceEventCount(): number;
getPatternCount(): number;
getTrajectoryCount(): number;
getFounderCount(): number;
isStreamHealthy(): boolean;
shouldReplicate(): boolean;
submitTask(task_type: string, payload: Uint8Array, max_credits: bigint): Promise<unknown>;
processNextTask(): Promise<boolean>;
processEpoch(): void;
enableTimeCrystal(oscillators: number): boolean;
enableHDC(): boolean;
enableNAO(quorum: number): boolean;
enableWTA(num_neurons: number): boolean;
enableBTSP(input_dim: number): boolean;
enableMicroLoRA(rank: number): boolean;
enableGlobalWorkspace(capacity: number): boolean;
enableMorphogenetic(size: number): boolean;
storePattern(pattern_json: string): number;
lookupPatterns(query_json: string, k: number): string;
prunePatterns(min_usage: number, min_confidence: number): number;
recordLearningTrajectory(trajectory_json: string): boolean;
recordPerformance(success_rate: number, throughput: number): void;
recordTaskRouting(task_type: string, node_id: string, latency_ms: bigint, success: boolean): void;
recordPeerInteraction(peer_id: string, success_rate: number): void;
getOptimalPeers(count: number): string[];
proposeNAO(action: string): string;
voteNAO(proposal_id: string, weight: number): boolean;
canUseClaim(claim_id: string): boolean;
getClaimQuarantineLevel(claim_id: string): number;
runSecurityAudit(): string;
checkEvents(): string;
getThemedStatus(node_count: number): string;
getMotivation(): string;
getCapabilities(): unknown;
getCapabilitiesSummary(): unknown;
getCoherenceStats(): string;
getEconomicHealth(): string;
getLearningStats(): string;
getOptimizationStats(): string;
getRecommendedConfig(): string;
getEnergyEfficiency(seq_len: number, hidden_dim: number): number;
isSelfSustaining(active_nodes: number, daily_tasks: bigint): boolean;
stepCapabilities(dt: number): void;
}
export interface EdgeNetConfigInstance {
cpuLimit(limit: number): EdgeNetConfigInstance;
memoryLimit(bytes: number): EdgeNetConfigInstance;
minIdleTime(ms: number): EdgeNetConfigInstance;
respectBattery(respect: boolean): EdgeNetConfigInstance;
addRelay(url: string): EdgeNetConfigInstance;
build(): EdgeNetNodeInstance;
}
export interface AdaptiveSecurityInstance {
free(): void;
chooseAction(state: string, available_actions: string): string;
detectAttack(features: Float32Array): number;
exportPatterns(): Uint8Array;
importPatterns(data: Uint8Array): void;
getSecurityLevel(): number;
getRateLimitMax(): number;
getMinReputation(): number;
getSpotCheckProbability(): number;
recordAttackPattern(pattern_type: string, features: Float32Array, severity: number): void;
updateNetworkHealth(active_nodes: number, suspicious_nodes: number, attacks_hour: number, false_positives: number, avg_response_ms: number): void;
learn(state: string, action: string, reward: number, next_state: string): void;
getStats(): string;
}
export interface TimeCrystalInstance {
free(): void;
getPhase(): number;
getCoherence(): number;
step(dt: number): void;
synchronize(other_phase: number): void;
getStats(): string;
}
// Singleton service
class EdgeNetService {
private module: EdgeNetModule | null = null;
private node: EdgeNetNodeInstance | null = null;
private piKey: PiKeyInstance | null = null;
private security: AdaptiveSecurityInstance | null = null;
private initialized = false;
private initPromise: Promise<void> | null = null;
private startTime = Date.now();
private siteId = 'edge-net-dashboard';
/**
* Initialize the WASM module
*/
async init(): Promise<void> {
if (this.initialized) return;
if (this.initPromise) return this.initPromise;
this.initPromise = this._doInit();
await this.initPromise;
}
private async _doInit(): Promise<void> {
try {
console.log('[EdgeNet] Loading WASM module...');
// Try loading from the local package first (for development)
let wasmModule: EdgeNetModule;
// Load from CDN - the package is published to npm
try {
const cdnUrl = 'https://unpkg.com/@ruvector/edge-net@0.1.1/ruvector_edge_net.js';
wasmModule = await import(/* @vite-ignore */ cdnUrl) as unknown as EdgeNetModule;
} catch (cdnError) {
console.warn('[EdgeNet] CDN load failed, running in fallback mode:', cdnError);
// Module load failed - will run in fallback mode
return;
}
// Initialize the WASM
await wasmModule.default();
this.module = wasmModule;
console.log('[EdgeNet] WASM module loaded successfully');
this.initialized = true;
} catch (error) {
console.error('[EdgeNet] Failed to load WASM module:', error);
// Set initialized to true but with null module - will use fallback mode
this.initialized = true;
}
}
/**
* Check if WASM is available
*/
isWASMAvailable(): boolean {
return this.module !== null;
}
/**
* Generate a new PiKey identity
*/
async generateIdentity(seed?: Uint8Array): Promise<PiKeyInstance | null> {
await this.init();
if (!this.module) {
console.warn('[EdgeNet] WASM not available, using Web Crypto fallback');
return null;
}
try {
this.piKey = new this.module.PiKey(seed || null);
console.log('[EdgeNet] Generated PiKey:', this.piKey.getShortId());
return this.piKey;
} catch (error) {
console.error('[EdgeNet] Failed to generate PiKey:', error);
return null;
}
}
/**
* Get the current PiKey
*/
getPiKey(): PiKeyInstance | null {
return this.piKey;
}
/**
* Create and start an EdgeNet node
*/
async createNode(siteId?: string): Promise<EdgeNetNodeInstance | null> {
await this.init();
if (!this.module) {
console.warn('[EdgeNet] WASM not available');
return null;
}
try {
const id = siteId || this.siteId;
// Use config builder for customization
const config = new this.module.EdgeNetConfig(id)
.addRelay('wss://edge-net-relay-875130704813.us-central1.run.app') // Genesis relay
.cpuLimit(0.5) // 50% CPU when idle
.memoryLimit(512 * 1024 * 1024) // 512MB
.minIdleTime(5000) // 5 seconds idle before contributing
.respectBattery(true);
this.node = config.build();
console.log('[EdgeNet] Node created:', this.node.nodeId());
return this.node;
} catch (error) {
console.error('[EdgeNet] Failed to create node:', error);
return null;
}
}
/**
* Get the current node
*/
getNode(): EdgeNetNodeInstance | null {
return this.node;
}
/**
* Start the node
*/
startNode(): void {
if (this.node) {
this.node.start();
// Enable all capabilities for maximum earning
this.node.enableTimeCrystal(8);
this.node.enableHDC();
this.node.enableWTA(64);
console.log('[EdgeNet] Node started with full capabilities');
}
}
/**
* Pause the node
*/
pauseNode(): void {
if (this.node) {
this.node.pause();
console.log('[EdgeNet] Node paused');
}
}
/**
* Resume the node
*/
resumeNode(): void {
if (this.node) {
this.node.resume();
console.log('[EdgeNet] Node resumed');
}
}
/**
* Process an epoch - advances time and accumulates rewards
*/
processEpoch(): void {
if (this.node) {
this.node.processEpoch();
}
}
/**
* Step capabilities forward (for real-time updates)
*/
stepCapabilities(dt: number): void {
if (this.node) {
this.node.stepCapabilities(dt);
}
}
/**
* Record performance for learning
*/
recordPerformance(successRate: number, throughput: number): void {
if (this.node) {
this.node.recordPerformance(successRate, throughput);
}
}
/**
* Get real node statistics
*/
getStats(): NodeStats | null {
if (!this.node) return null;
try {
return this.node.getStats();
} catch (error) {
console.error('[EdgeNet] Failed to get stats:', error);
return null;
}
}
/**
* Get credit balance
*/
getCreditBalance(): bigint {
if (!this.node) return BigInt(0);
return this.node.creditBalance();
}
/**
* Get Time Crystal synchronization level
*/
getTimeCrystalSync(): number {
if (!this.node) return 0;
return this.node.getTimeCrystalSync();
}
/**
* Enable Time Crystal
*/
enableTimeCrystal(oscillators = 8): boolean {
if (!this.node) return false;
return this.node.enableTimeCrystal(oscillators);
}
/**
* Get network fitness score
*/
getNetworkFitness(): number {
if (!this.node) return 0;
return this.node.getNetworkFitness();
}
/**
* Initialize adaptive security
*/
async initSecurity(): Promise<AdaptiveSecurityInstance | null> {
await this.init();
if (!this.module) return null;
try {
this.security = new this.module.AdaptiveSecurity();
console.log('[EdgeNet] Adaptive security initialized');
return this.security;
} catch (error) {
console.error('[EdgeNet] Failed to init security:', error);
return null;
}
}
/**
* Get security level
*/
getSecurityLevel(): number {
if (!this.security) return 0;
return this.security.getSecurityLevel();
}
/**
* Run security audit
*/
runSecurityAudit(): string | null {
if (!this.node) return null;
return this.node.runSecurityAudit();
}
/**
* Get browser fingerprint for unique node identification
*/
async getBrowserFingerprint(): Promise<string | null> {
await this.init();
if (!this.module) return null;
try {
return await this.module.BrowserFingerprint.generate();
} catch (error) {
console.error('[EdgeNet] Failed to generate fingerprint:', error);
return null;
}
}
/**
* Get economic health metrics
*/
getEconomicHealth(): string | null {
if (!this.node) return null;
return this.node.getEconomicHealth();
}
/**
* Get learning statistics
*/
getLearningStats(): string | null {
if (!this.node) return null;
return this.node.getLearningStats();
}
/**
* Store a learning pattern
*/
storePattern(pattern: object): number {
if (!this.node) return -1;
return this.node.storePattern(JSON.stringify(pattern));
}
/**
* Lookup similar patterns
*/
lookupPatterns(query: object, k = 5): unknown[] {
if (!this.node) return [];
try {
const result = this.node.lookupPatterns(JSON.stringify(query), k);
return JSON.parse(result);
} catch {
return [];
}
}
/**
* Submit a task to the network
*/
async submitTask(taskType: string, payload: Uint8Array, maxCredits: bigint): Promise<unknown> {
if (!this.node) throw new Error('Node not initialized');
return this.node.submitTask(taskType, payload, maxCredits);
}
/**
* Submit a demo compute task (for earning credits in demo mode)
*/
async submitDemoTask(): Promise<void> {
if (!this.node) return;
try {
// Submit a small compute task
const payload = new TextEncoder().encode(JSON.stringify({
type: 'compute',
data: Math.random().toString(36),
timestamp: Date.now(),
}));
await this.node.submitTask('compute', payload, BigInt(1000000)); // 0.001 rUv max
} catch {
// Task submission can fail if queue is full - that's ok
}
}
/**
* Process the next available task
*/
async processNextTask(): Promise<boolean> {
if (!this.node) return false;
return this.node.processNextTask();
}
/**
* Get capabilities summary
*/
getCapabilities(): unknown {
if (!this.node) return null;
return this.node.getCapabilitiesSummary();
}
/**
* Get uptime in seconds
*/
getUptime(): number {
return (Date.now() - this.startTime) / 1000;
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.node) {
this.node.disconnect();
this.node.free();
this.node = null;
}
if (this.piKey) {
this.piKey.free();
this.piKey = null;
}
if (this.security) {
this.security.free();
this.security = null;
}
console.log('[EdgeNet] Service destroyed');
}
}
// Export singleton instance
export const edgeNetService = new EdgeNetService();
// Export types for external use
export type { EdgeNetService };

View File

@@ -0,0 +1,394 @@
/**
* Edge-Net Relay WebSocket Client
*
* Provides real-time connection to the Edge-Net relay server for:
* - Node registration and presence
* - Task distribution and completion
* - Credit synchronization
* - Time Crystal phase sync
*/
export interface RelayMessage {
type: string;
[key: string]: unknown;
}
export interface NetworkState {
genesisTime: number;
totalNodes: number;
activeNodes: number;
totalTasks: number;
totalRuvDistributed: bigint;
timeCrystalPhase: number;
}
export interface TaskAssignment {
id: string;
submitter: string;
taskType: string;
payload: Uint8Array;
maxCredits: bigint;
submittedAt: number;
}
export interface RelayEventHandlers {
onConnected?: (nodeId: string, networkState: NetworkState, peers: string[]) => void;
onDisconnected?: () => void;
onNodeJoined?: (nodeId: string, totalNodes: number) => void;
onNodeLeft?: (nodeId: string, totalNodes: number) => void;
onTaskAssigned?: (task: TaskAssignment) => void;
onTaskResult?: (taskId: string, result: unknown, processedBy: string) => void;
onCreditEarned?: (amount: bigint, taskId: string) => void;
onTimeCrystalSync?: (phase: number, timestamp: number, activeNodes: number) => void;
onPeerMessage?: (from: string, payload: unknown) => void;
onError?: (error: Error) => void;
}
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000]; // Exponential backoff
class RelayClient {
private ws: WebSocket | null = null;
private nodeId: string | null = null;
private relayUrl: string;
private handlers: RelayEventHandlers = {};
private reconnectAttempt = 0;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private isConnecting = false;
private shouldReconnect = true;
constructor(relayUrl: string = 'wss://edge-net-relay-875130704813.us-central1.run.app') {
this.relayUrl = relayUrl;
}
/**
* Set event handlers
*/
setHandlers(handlers: RelayEventHandlers): void {
this.handlers = { ...this.handlers, ...handlers };
}
/**
* Connect to the relay server
*/
async connect(nodeId: string): Promise<boolean> {
if (this.ws?.readyState === WebSocket.OPEN) {
console.log('[RelayClient] Already connected');
return true;
}
if (this.isConnecting) {
console.log('[RelayClient] Connection already in progress');
return false;
}
this.nodeId = nodeId;
this.shouldReconnect = true;
this.isConnecting = true;
return new Promise((resolve) => {
try {
console.log(`[RelayClient] Connecting to ${this.relayUrl}...`);
this.ws = new WebSocket(this.relayUrl);
this.ws.onopen = () => {
console.log('[RelayClient] WebSocket connected');
this.isConnecting = false;
this.reconnectAttempt = 0;
// Register with relay
this.send({
type: 'register',
nodeId: this.nodeId,
capabilities: ['compute', 'storage'],
version: '0.1.0',
});
// Start heartbeat
this.startHeartbeat();
};
this.ws.onmessage = (event) => {
this.handleMessage(event.data);
};
this.ws.onclose = (event) => {
console.log(`[RelayClient] WebSocket closed: ${event.code} ${event.reason}`);
this.isConnecting = false;
this.stopHeartbeat();
this.handlers.onDisconnected?.();
if (this.shouldReconnect) {
this.scheduleReconnect();
}
};
this.ws.onerror = (error) => {
console.error('[RelayClient] WebSocket error:', error);
this.isConnecting = false;
this.handlers.onError?.(new Error('WebSocket connection failed'));
resolve(false);
};
// Wait for welcome message to confirm connection
const checkConnected = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
clearInterval(checkConnected);
resolve(true);
}
}, 100);
// Timeout after 10 seconds
setTimeout(() => {
clearInterval(checkConnected);
if (this.ws?.readyState !== WebSocket.OPEN) {
this.isConnecting = false;
resolve(false);
}
}, 10000);
} catch (error) {
console.error('[RelayClient] Failed to create WebSocket:', error);
this.isConnecting = false;
resolve(false);
}
});
}
/**
* Disconnect from the relay
*/
disconnect(): void {
this.shouldReconnect = false;
this.stopHeartbeat();
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close(1000, 'Client disconnect');
this.ws = null;
}
console.log('[RelayClient] Disconnected');
}
/**
* Check if connected
*/
isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
/**
* Get current node ID
*/
getNodeId(): string | null {
return this.nodeId;
}
/**
* Submit a task to the network
*/
submitTask(taskType: string, payload: Uint8Array, maxCredits: bigint): void {
this.send({
type: 'task_submit',
task: {
taskType,
payload: Array.from(payload), // Convert to array for JSON
maxCredits: maxCredits.toString(),
},
});
}
/**
* Report task completion
*/
completeTask(taskId: string, submitterId: string, result: unknown, reward: bigint): void {
this.send({
type: 'task_complete',
taskId,
submitterId,
result,
reward: reward.toString(),
});
}
/**
* Send a message to a specific peer
*/
sendToPeer(targetId: string, payload: unknown): void {
this.send({
type: 'peer_message',
targetId,
payload,
});
}
/**
* Broadcast a message to all peers
*/
broadcast(payload: unknown): void {
this.send({
type: 'broadcast',
payload,
});
}
// ============================================================================
// Private Methods
// ============================================================================
private send(message: RelayMessage): void {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
console.warn('[RelayClient] Cannot send - not connected');
}
}
private handleMessage(data: string): void {
try {
const message = JSON.parse(data) as RelayMessage;
switch (message.type) {
case 'welcome':
console.log('[RelayClient] Registered with relay:', message.nodeId);
this.handlers.onConnected?.(
message.nodeId as string,
{
genesisTime: (message.networkState as NetworkState)?.genesisTime || Date.now(),
totalNodes: (message.networkState as NetworkState)?.totalNodes || 0,
activeNodes: (message.networkState as NetworkState)?.activeNodes || 0,
totalTasks: (message.networkState as NetworkState)?.totalTasks || 0,
totalRuvDistributed: BigInt((message.networkState as NetworkState)?.totalRuvDistributed?.toString() || '0'),
timeCrystalPhase: (message.networkState as NetworkState)?.timeCrystalPhase || 0,
},
(message.peers as string[]) || []
);
break;
case 'node_joined':
console.log('[RelayClient] Node joined:', message.nodeId);
this.handlers.onNodeJoined?.(
message.nodeId as string,
message.totalNodes as number
);
break;
case 'node_left':
console.log('[RelayClient] Node left:', message.nodeId);
this.handlers.onNodeLeft?.(
message.nodeId as string,
message.totalNodes as number
);
break;
case 'task_assignment':
console.log('[RelayClient] Task assigned:', (message.task as TaskAssignment)?.id);
const task = message.task as Record<string, unknown>;
this.handlers.onTaskAssigned?.({
id: task.id as string,
submitter: task.submitter as string,
taskType: task.taskType as string,
payload: new Uint8Array(task.payload as number[]),
maxCredits: BigInt(task.maxCredits as string || '0'),
submittedAt: task.submittedAt as number,
});
break;
case 'task_accepted':
console.log('[RelayClient] Task accepted:', message.taskId);
break;
case 'task_result':
console.log('[RelayClient] Task result:', message.taskId);
this.handlers.onTaskResult?.(
message.taskId as string,
message.result,
message.processedBy as string
);
break;
case 'credit_earned':
console.log('[RelayClient] Credit earned:', message.amount);
this.handlers.onCreditEarned?.(
BigInt(message.amount as string || '0'),
message.taskId as string
);
break;
case 'time_crystal_sync':
this.handlers.onTimeCrystalSync?.(
message.phase as number,
message.timestamp as number,
message.activeNodes as number
);
break;
case 'peer_message':
this.handlers.onPeerMessage?.(
message.from as string,
message.payload
);
break;
case 'heartbeat_ack':
// Heartbeat acknowledged
break;
case 'error':
console.error('[RelayClient] Relay error:', message.message);
this.handlers.onError?.(new Error(message.message as string));
break;
case 'relay_shutdown':
console.warn('[RelayClient] Relay is shutting down');
this.shouldReconnect = true; // Will reconnect when relay comes back
break;
default:
console.log('[RelayClient] Unknown message type:', message.type);
}
} catch (error) {
console.error('[RelayClient] Failed to parse message:', error);
}
}
private startHeartbeat(): void {
this.stopHeartbeat();
this.heartbeatTimer = setInterval(() => {
this.send({ type: 'heartbeat' });
}, 15000); // Every 15 seconds
}
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
private scheduleReconnect(): void {
if (this.reconnectTimer) return;
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
console.log(`[RelayClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.reconnectAttempt++;
if (this.nodeId) {
this.connect(this.nodeId);
}
}, delay);
}
}
// Export singleton instance
export const relayClient = new RelayClient();
// Export class for testing
export { RelayClient };

View File

@@ -0,0 +1,152 @@
/**
* IndexedDB Storage Service
* Persistent storage for Edge-Net node state
*/
const DB_NAME = 'edge-net-db';
const DB_VERSION = 1;
const STORE_NAME = 'node-state';
interface NodeState {
id: string;
nodeId: string | null;
creditsEarned: number;
creditsSpent: number;
tasksCompleted: number;
tasksSubmitted: number;
totalUptime: number;
lastActiveTimestamp: number;
consentGiven: boolean;
consentTimestamp: number | null;
cpuLimit: number;
gpuEnabled: boolean;
gpuLimit: number;
respectBattery: boolean;
onlyWhenIdle: boolean;
}
class StorageService {
private db: IDBDatabase | null = null;
private initPromise: Promise<void> | null = null;
async init(): Promise<void> {
if (this.db) return;
if (this.initPromise) return this.initPromise;
this.initPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => {
console.error('[Storage] Failed to open IndexedDB:', request.error);
reject(request.error);
};
request.onsuccess = () => {
this.db = request.result;
console.log('[Storage] IndexedDB opened successfully');
resolve();
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
console.log('[Storage] Created object store:', STORE_NAME);
}
};
});
return this.initPromise;
}
async saveState(state: NodeState): Promise<void> {
await this.init();
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.put(state);
request.onsuccess = () => {
console.log('[Storage] State saved:', state.creditsEarned, 'rUv');
resolve();
};
request.onerror = () => {
console.error('[Storage] Failed to save state:', request.error);
reject(request.error);
};
});
}
async loadState(): Promise<NodeState | null> {
await this.init();
if (!this.db) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get('primary');
request.onsuccess = () => {
const state = request.result as NodeState | undefined;
if (state) {
console.log('[Storage] Loaded state:', state.creditsEarned, 'rUv earned');
} else {
console.log('[Storage] No saved state found');
}
resolve(state || null);
};
request.onerror = () => {
console.error('[Storage] Failed to load state:', request.error);
reject(request.error);
};
});
}
async getDefaultState(): Promise<NodeState> {
return {
id: 'primary',
nodeId: null,
creditsEarned: 0,
creditsSpent: 0,
tasksCompleted: 0,
tasksSubmitted: 0,
totalUptime: 0,
lastActiveTimestamp: Date.now(),
consentGiven: false,
consentTimestamp: null,
cpuLimit: 50,
gpuEnabled: false,
gpuLimit: 30,
respectBattery: true,
onlyWhenIdle: true,
};
}
async clear(): Promise<void> {
await this.init();
if (!this.db) return;
return new Promise((resolve, reject) => {
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onsuccess = () => {
console.log('[Storage] State cleared');
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
}
export const storageService = new StorageService();
export type { NodeState };

View File

@@ -0,0 +1,209 @@
import { create } from 'zustand';
import type { CDNScript, CDNConfig } from '../types';
interface CDNState extends CDNConfig {
isLoading: boolean;
error: string | null;
// Actions
setScripts: (scripts: CDNScript[]) => void;
toggleScript: (scriptId: string) => void;
loadScript: (scriptId: string) => Promise<void>;
unloadScript: (scriptId: string) => void;
setAutoLoad: (autoLoad: boolean) => void;
setCacheEnabled: (cacheEnabled: boolean) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
}
const defaultScripts: CDNScript[] = [
// WASM Modules
{
id: 'edge-net-wasm',
name: '@ruvector/edge-net',
description: 'Core Edge-Net WASM module with Time Crystal and P2P capabilities',
url: 'https://unpkg.com/@ruvector/edge-net@0.1.1/ruvector_edge_net_bg.wasm',
size: '3.2 MB',
category: 'wasm',
enabled: true,
loaded: false,
},
{
id: 'attention-wasm',
name: '@ruvector/attention-unified-wasm',
description: 'DAG Attention mechanisms for task orchestration',
url: 'https://unpkg.com/@ruvector/attention-unified-wasm@0.1.0/attention_unified_bg.wasm',
size: '850 KB',
category: 'wasm',
enabled: false,
loaded: false,
},
{
id: 'economy-wasm',
name: '@ruvector/economy-wasm',
description: 'Credit economy and marketplace functionality',
url: 'https://unpkg.com/@ruvector/economy-wasm@0.1.0/economy_bg.wasm',
size: '620 KB',
category: 'wasm',
enabled: false,
loaded: false,
},
// AI Libraries
{
id: 'tensorflow',
name: 'TensorFlow.js',
description: 'Machine learning library for browser-based AI',
url: 'https://cdnjs.cloudflare.com/ajax/libs/tensorflow/4.15.0/tf.min.js',
size: '1.8 MB',
category: 'ai',
enabled: false,
loaded: false,
},
{
id: 'onnx-runtime',
name: 'ONNX Runtime Web',
description: 'Run ONNX models in the browser with WebAssembly',
url: 'https://cdnjs.cloudflare.com/ajax/libs/onnxruntime-web/1.17.0/ort.min.js',
size: '2.1 MB',
category: 'ai',
enabled: false,
loaded: false,
},
// Crypto Libraries
{
id: 'noble-curves',
name: 'Noble Curves',
description: 'Elliptic curve cryptography (Ed25519, secp256k1)',
url: 'https://unpkg.com/@noble/curves@1.3.0/index.js',
size: '45 KB',
category: 'crypto',
enabled: false,
loaded: false,
},
{
id: 'tweetnacl',
name: 'TweetNaCl.js',
description: 'Port of TweetNaCl cryptographic library',
url: 'https://cdnjs.cloudflare.com/ajax/libs/tweetnacl/1.0.3/nacl-fast.min.js',
size: '32 KB',
category: 'crypto',
enabled: false,
loaded: false,
},
// Network Libraries
{
id: 'libp2p',
name: 'libp2p',
description: 'Modular peer-to-peer networking stack',
url: 'https://unpkg.com/libp2p@1.2.0/dist/index.min.js',
size: '680 KB',
category: 'network',
enabled: false,
loaded: false,
},
{
id: 'simple-peer',
name: 'Simple Peer',
description: 'Simple WebRTC video, voice, and data channels',
url: 'https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.11.1/simplepeer.min.js',
size: '95 KB',
category: 'network',
enabled: false,
loaded: false,
},
// Utility Libraries
{
id: 'comlink',
name: 'Comlink',
description: 'Make Web Workers enjoyable with RPC-style API',
url: 'https://unpkg.com/comlink@4.4.1/dist/umd/comlink.js',
size: '4 KB',
category: 'utility',
enabled: false,
loaded: false,
},
{
id: 'idb-keyval',
name: 'idb-keyval',
description: 'Super simple IndexedDB key-value store',
url: 'https://unpkg.com/idb-keyval@6.2.1/dist/umd.js',
size: '3 KB',
category: 'utility',
enabled: false,
loaded: false,
},
];
export const useCDNStore = create<CDNState>((set, get) => ({
scripts: defaultScripts,
autoLoad: false,
cacheEnabled: true,
isLoading: false,
error: null,
setScripts: (scripts) => set({ scripts }),
toggleScript: (scriptId) =>
set((state) => ({
scripts: state.scripts.map((s) =>
s.id === scriptId ? { ...s, enabled: !s.enabled } : s
),
})),
loadScript: async (scriptId) => {
const { scripts } = get();
const script = scripts.find((s) => s.id === scriptId);
if (!script || script.loaded) return;
set({ isLoading: true, error: null });
try {
// Create script element
const scriptEl = document.createElement('script');
scriptEl.src = script.url;
scriptEl.async = true;
scriptEl.id = `cdn-${scriptId}`;
await new Promise<void>((resolve, reject) => {
scriptEl.onload = () => resolve();
scriptEl.onerror = () => reject(new Error(`Failed to load ${script.name}`));
document.head.appendChild(scriptEl);
});
set((state) => ({
scripts: state.scripts.map((s) =>
s.id === scriptId ? { ...s, loaded: true } : s
),
isLoading: false,
}));
console.log(`[CDN] Loaded: ${script.name}`);
} catch (error) {
set({
error: error instanceof Error ? error.message : 'Failed to load script',
isLoading: false,
});
}
},
unloadScript: (scriptId) => {
const scriptEl = document.getElementById(`cdn-${scriptId}`);
if (scriptEl) {
scriptEl.remove();
}
set((state) => ({
scripts: state.scripts.map((s) =>
s.id === scriptId ? { ...s, loaded: false } : s
),
}));
console.log(`[CDN] Unloaded: ${scriptId}`);
},
setAutoLoad: (autoLoad) => set({ autoLoad }),
setCacheEnabled: (cacheEnabled) => set({ cacheEnabled }),
setLoading: (loading) => set({ isLoading: loading }),
setError: (error) => set({ error }),
}));

View File

@@ -0,0 +1,442 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { edgeNetService, type PiKeyInstance } from '../services/edgeNet';
export interface PeerIdentity {
id: string;
publicKey: string;
publicKeyBytes?: Uint8Array;
displayName: string;
avatar?: string;
createdAt: Date;
shortId: string;
identityHex: string;
hasPiMagic: boolean;
}
export interface NetworkRegistration {
networkId: string;
networkName: string;
status: 'pending' | 'active' | 'suspended' | 'expired';
joinedAt: Date;
capabilities: string[];
reputation: number;
creditsEarned: number;
}
export interface IdentityState {
identity: PeerIdentity | null;
registrations: NetworkRegistration[];
isGenerating: boolean;
isRegistering: boolean;
error: string | null;
piKeyBackup: string | null; // Encrypted backup (hex encoded)
hasRealPiKey: boolean;
// Actions
generateIdentity: (displayName: string) => Promise<void>;
importIdentity: (privateKeyOrBackup: string, password?: string) => Promise<void>;
exportIdentity: (password: string) => Promise<string | null>;
clearIdentity: () => void;
registerNetwork: (networkId: string, capabilities: string[]) => Promise<void>;
leaveNetwork: (networkId: string) => void;
updateCapabilities: (networkId: string, capabilities: string[]) => void;
signData: (data: Uint8Array) => Uint8Array | null;
verifySignature: (data: Uint8Array, signature: Uint8Array, publicKey: Uint8Array) => boolean;
}
// Helper: Convert bytes to hex
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
// Helper: Convert hex to bytes
function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
}
return bytes;
}
// Real Web Crypto API fallback for Ed25519 (when WASM unavailable)
async function generateWebCryptoKeys(): Promise<{
publicKey: Uint8Array;
privateKey: Uint8Array;
sign: (data: Uint8Array) => Promise<Uint8Array>;
verify: (data: Uint8Array, sig: Uint8Array, pk: Uint8Array) => Promise<boolean>;
}> {
// Use Web Crypto API for real Ed25519 keys
// Note: Ed25519 support varies by browser, fall back to ECDSA P-256 if needed
let keyPair: CryptoKeyPair;
try {
// Try Ed25519 first (supported in newer browsers)
keyPair = await crypto.subtle.generateKey(
{ name: 'Ed25519' },
true,
['sign', 'verify']
);
} catch {
// Fall back to ECDSA P-256
console.log('[Identity] Ed25519 not supported, using ECDSA P-256');
keyPair = await crypto.subtle.generateKey(
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
['sign', 'verify']
);
}
// Export keys
const publicKeyBuffer = await crypto.subtle.exportKey('raw', keyPair.publicKey);
const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
const publicKey = new Uint8Array(publicKeyBuffer);
const privateKey = new Uint8Array(privateKeyBuffer);
return {
publicKey,
privateKey,
sign: async (data: Uint8Array) => {
const algorithm = keyPair.privateKey.algorithm.name === 'Ed25519'
? { name: 'Ed25519' }
: { name: 'ECDSA', hash: 'SHA-256' };
const signature = await crypto.subtle.sign(algorithm, keyPair.privateKey, data.buffer as ArrayBuffer);
return new Uint8Array(signature);
},
verify: async (data: Uint8Array, sig: Uint8Array, _pk: Uint8Array) => {
const algorithm = keyPair.publicKey.algorithm.name === 'Ed25519'
? { name: 'Ed25519' }
: { name: 'ECDSA', hash: 'SHA-256' };
return crypto.subtle.verify(algorithm, keyPair.publicKey, sig.buffer as ArrayBuffer, data.buffer as ArrayBuffer);
},
};
}
// Generate unique peer ID from public key
function generatePeerId(publicKey: Uint8Array): string {
// Use first 44 chars of base64 encoded public key for libp2p-style ID
const base64 = btoa(String.fromCharCode(...publicKey));
return `12D3KooW${base64.replace(/[+/=]/g, '').substring(0, 44)}`;
}
const availableNetworks = [
{
id: 'mainnet',
name: 'Edge-Net Mainnet',
description: 'Primary production network',
requiredCapabilities: ['compute'],
},
{
id: 'testnet',
name: 'Edge-Net Testnet',
description: 'Testing and development network',
requiredCapabilities: [],
},
{
id: 'research',
name: 'Research Network',
description: 'Academic and research collaboration',
requiredCapabilities: ['compute', 'storage'],
},
];
export { availableNetworks };
// Store the current Web Crypto instance for signing (used as fallback when WASM unavailable)
// Assigned in generateIdentity, cleared in clearIdentity, accessed in signData/verifySignature
interface WebCryptoState {
sign: (data: Uint8Array) => Promise<Uint8Array>;
verify: (data: Uint8Array, sig: Uint8Array, pk: Uint8Array) => Promise<boolean>;
publicKey: Uint8Array;
privateKey: Uint8Array;
}
let webCryptoInstance: WebCryptoState | null = null;
// Export for external async signing when WASM unavailable
export function getWebCryptoInstance(): WebCryptoState | null {
return webCryptoInstance;
}
let currentPiKey: PiKeyInstance | null = null;
export const useIdentityStore = create<IdentityState>()(
persist(
(set, get) => ({
identity: null,
registrations: [],
isGenerating: false,
isRegistering: false,
error: null,
piKeyBackup: null,
hasRealPiKey: false,
generateIdentity: async (displayName: string) => {
set({ isGenerating: true, error: null });
try {
// Try real PiKey from WASM first
const piKey = await edgeNetService.generateIdentity();
if (piKey) {
currentPiKey = piKey;
const identity: PeerIdentity = {
id: piKey.getShortId(),
publicKey: bytesToHex(piKey.getPublicKey()),
publicKeyBytes: piKey.getPublicKey(),
displayName,
createdAt: new Date(),
shortId: piKey.getShortId(),
identityHex: piKey.getIdentityHex(),
hasPiMagic: piKey.verifyPiMagic(),
};
set({
identity,
hasRealPiKey: true,
isGenerating: false,
});
console.log('[Identity] Generated real PiKey:', identity.shortId);
console.log('[Identity] Has Pi magic:', identity.hasPiMagic);
console.log('[Identity] Stats:', piKey.getStats());
return;
}
// Fallback to Web Crypto API
console.log('[Identity] Using Web Crypto API fallback');
const cryptoKeys = await generateWebCryptoKeys();
webCryptoInstance = cryptoKeys;
const peerId = generatePeerId(cryptoKeys.publicKey);
const identity: PeerIdentity = {
id: peerId,
publicKey: bytesToHex(cryptoKeys.publicKey),
publicKeyBytes: cryptoKeys.publicKey,
displayName,
createdAt: new Date(),
shortId: peerId.substring(0, 16),
identityHex: bytesToHex(cryptoKeys.publicKey),
hasPiMagic: false,
};
set({
identity,
hasRealPiKey: false,
isGenerating: false,
});
console.log('[Identity] Generated Web Crypto identity:', identity.shortId);
} catch (error) {
console.error('[Identity] Generation failed:', error);
set({
error: error instanceof Error ? error.message : 'Failed to generate identity',
isGenerating: false,
});
}
},
importIdentity: async (privateKeyOrBackup: string, password?: string) => {
set({ isGenerating: true, error: null });
try {
// If password provided, treat as encrypted backup
if (password) {
// TODO: Implement PiKey.restoreFromBackup when available
throw new Error('Encrypted backup import not yet implemented');
}
// Otherwise, validate hex private key
if (privateKeyOrBackup.length < 32) {
throw new Error('Invalid private key format');
}
// Generate new identity from seed
const seed = hexToBytes(privateKeyOrBackup.substring(0, 64));
const piKey = await edgeNetService.generateIdentity(seed);
if (piKey) {
currentPiKey = piKey;
const identity: PeerIdentity = {
id: piKey.getShortId(),
publicKey: bytesToHex(piKey.getPublicKey()),
publicKeyBytes: piKey.getPublicKey(),
displayName: 'Imported Identity',
createdAt: new Date(),
shortId: piKey.getShortId(),
identityHex: piKey.getIdentityHex(),
hasPiMagic: piKey.verifyPiMagic(),
};
set({
identity,
hasRealPiKey: true,
isGenerating: false,
});
console.log('[Identity] Imported PiKey:', identity.shortId);
return;
}
throw new Error('Failed to import identity');
} catch (error) {
console.error('[Identity] Import failed:', error);
set({
error: error instanceof Error ? error.message : 'Failed to import identity',
isGenerating: false,
});
}
},
exportIdentity: async (password: string) => {
const { hasRealPiKey } = get();
if (hasRealPiKey && currentPiKey) {
try {
// Create encrypted backup with Argon2id
const backup = currentPiKey.createEncryptedBackup(password);
const backupHex = bytesToHex(backup);
set({ piKeyBackup: backupHex });
console.log('[Identity] Created encrypted backup');
return backupHex;
} catch (error) {
console.error('[Identity] Export failed:', error);
return null;
}
}
// For Web Crypto keys, export as JSON (less secure)
const { identity } = get();
if (!identity) return null;
return JSON.stringify({
publicKey: identity.publicKey,
displayName: identity.displayName,
note: 'Web Crypto fallback - private key not exportable',
});
},
clearIdentity: () => {
if (currentPiKey) {
currentPiKey.free();
currentPiKey = null;
}
webCryptoInstance = null;
set({
identity: null,
registrations: [],
piKeyBackup: null,
hasRealPiKey: false,
});
console.log('[Identity] Cleared identity');
},
registerNetwork: async (networkId: string, capabilities: string[]) => {
const { identity, registrations } = get();
if (!identity) {
set({ error: 'No identity found. Generate or import an identity first.' });
return;
}
if (registrations.some(r => r.networkId === networkId)) {
set({ error: 'Already registered to this network' });
return;
}
set({ isRegistering: true, error: null });
try {
// Create real EdgeNet node for this network
const node = await edgeNetService.createNode(`${networkId}-${identity.shortId}`);
if (node) {
// Enable Time Crystal for synchronization
edgeNetService.enableTimeCrystal(8);
edgeNetService.startNode();
console.log('[Identity] Connected to real EdgeNet node');
}
const network = availableNetworks.find(n => n.id === networkId);
const registration: NetworkRegistration = {
networkId,
networkName: network?.name || networkId,
status: 'active',
joinedAt: new Date(),
capabilities,
reputation: 100,
creditsEarned: 0,
};
set({
registrations: [...registrations, registration],
isRegistering: false,
});
console.log('[Identity] Registered to network:', networkId);
} catch (error) {
console.error('[Identity] Registration failed:', error);
set({
error: error instanceof Error ? error.message : 'Failed to register',
isRegistering: false,
});
}
},
leaveNetwork: (networkId: string) => {
edgeNetService.pauseNode();
set((state) => ({
registrations: state.registrations.filter(r => r.networkId !== networkId),
}));
console.log('[Identity] Left network:', networkId);
},
updateCapabilities: (networkId: string, capabilities: string[]) => {
set((state) => ({
registrations: state.registrations.map(r =>
r.networkId === networkId ? { ...r, capabilities } : r
),
}));
},
signData: (data: Uint8Array): Uint8Array | null => {
if (currentPiKey) {
return currentPiKey.sign(data);
}
// Web Crypto signing is async, but we need sync here
// Return null and use async version externally
return null;
},
verifySignature: (data: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean => {
if (currentPiKey) {
return currentPiKey.verify(data, signature, publicKey);
}
return false;
},
}),
{
name: 'edge-net-identity',
partialize: (state) => ({
identity: state.identity ? {
...state.identity,
publicKeyBytes: undefined, // Don't persist Uint8Array
} : null,
registrations: state.registrations,
piKeyBackup: state.piKeyBackup,
hasRealPiKey: state.hasRealPiKey,
}),
}
)
);

View File

@@ -0,0 +1,209 @@
import { create } from 'zustand';
import type { MCPTool, MCPResult } from '../types';
interface MCPState {
tools: MCPTool[];
results: MCPResult[];
isConnected: boolean;
activeTools: string[];
// Actions
setTools: (tools: MCPTool[]) => void;
updateTool: (toolId: string, updates: Partial<MCPTool>) => void;
addResult: (result: MCPResult) => void;
clearResults: () => void;
setConnected: (connected: boolean) => void;
executeTool: (toolId: string, params?: Record<string, unknown>) => Promise<MCPResult>;
}
const defaultTools: MCPTool[] = [
// Swarm Tools
{
id: 'swarm_init',
name: 'Swarm Initialize',
description: 'Initialize a new swarm with specified topology',
category: 'swarm',
status: 'ready',
},
{
id: 'swarm_status',
name: 'Swarm Status',
description: 'Get current swarm status and agent information',
category: 'swarm',
status: 'ready',
},
{
id: 'swarm_monitor',
name: 'Swarm Monitor',
description: 'Monitor swarm activity in real-time',
category: 'swarm',
status: 'ready',
},
// Agent Tools
{
id: 'agent_spawn',
name: 'Agent Spawn',
description: 'Spawn a new agent in the swarm',
category: 'agent',
status: 'ready',
},
{
id: 'agent_list',
name: 'Agent List',
description: 'List all active agents in the swarm',
category: 'agent',
status: 'ready',
},
{
id: 'agent_metrics',
name: 'Agent Metrics',
description: 'Get performance metrics for agents',
category: 'agent',
status: 'ready',
},
// Task Tools
{
id: 'task_orchestrate',
name: 'Task Orchestrate',
description: 'Orchestrate a task across the swarm',
category: 'task',
status: 'ready',
},
{
id: 'task_status',
name: 'Task Status',
description: 'Check progress of running tasks',
category: 'task',
status: 'ready',
},
{
id: 'task_results',
name: 'Task Results',
description: 'Retrieve results from completed tasks',
category: 'task',
status: 'ready',
},
// Memory Tools
{
id: 'memory_usage',
name: 'Memory Usage',
description: 'Get current memory usage statistics',
category: 'memory',
status: 'ready',
},
// Neural Tools
{
id: 'neural_status',
name: 'Neural Status',
description: 'Get neural agent status and performance metrics',
category: 'neural',
status: 'ready',
},
{
id: 'neural_train',
name: 'Neural Train',
description: 'Train neural agents with sample tasks',
category: 'neural',
status: 'ready',
},
{
id: 'neural_patterns',
name: 'Neural Patterns',
description: 'Get cognitive pattern information',
category: 'neural',
status: 'ready',
},
// GitHub Tools
{
id: 'github_repo_analyze',
name: 'Repository Analyze',
description: 'Analyze GitHub repository structure and code',
category: 'github',
status: 'ready',
},
{
id: 'github_pr_manage',
name: 'PR Management',
description: 'Manage pull requests and reviews',
category: 'github',
status: 'ready',
},
];
export const useMCPStore = create<MCPState>((set, get) => ({
tools: defaultTools,
results: [],
isConnected: true,
activeTools: [],
setTools: (tools) => set({ tools }),
updateTool: (toolId, updates) =>
set((state) => ({
tools: state.tools.map((t) =>
t.id === toolId ? { ...t, ...updates } : t
),
})),
addResult: (result) =>
set((state) => ({
results: [result, ...state.results].slice(0, 50), // Keep last 50 results
})),
clearResults: () => set({ results: [] }),
setConnected: (connected) => set({ isConnected: connected }),
executeTool: async (toolId, params) => {
const { updateTool, addResult } = get();
const startTime = performance.now();
updateTool(toolId, { status: 'running' });
set((state) => ({ activeTools: [...state.activeTools, toolId] }));
try {
// Simulate tool execution
await new Promise((resolve) =>
setTimeout(resolve, 500 + Math.random() * 1500)
);
const result: MCPResult = {
toolId,
success: true,
data: {
message: `Tool ${toolId} executed successfully`,
params,
timestamp: new Date().toISOString(),
},
timestamp: new Date(),
duration: performance.now() - startTime,
};
updateTool(toolId, { status: 'ready', lastRun: new Date() });
addResult(result);
set((state) => ({
activeTools: state.activeTools.filter((id) => id !== toolId),
}));
console.log(`[MCP] Tool ${toolId} completed:`, result);
return result;
} catch (error) {
const result: MCPResult = {
toolId,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: new Date(),
duration: performance.now() - startTime,
};
updateTool(toolId, { status: 'error' });
addResult(result);
set((state) => ({
activeTools: state.activeTools.filter((id) => id !== toolId),
}));
return result;
}
},
}));

View File

@@ -0,0 +1,670 @@
import { create } from 'zustand';
import type { NetworkStats, NodeInfo, TimeCrystal, CreditBalance } from '../types';
import { edgeNetService } from '../services/edgeNet';
import { storageService } from '../services/storage';
import { relayClient, type TaskAssignment, type NetworkState as RelayNetworkState } from '../services/relayClient';
interface ContributionSettings {
enabled: boolean;
cpuLimit: number;
gpuEnabled: boolean;
gpuLimit: number;
memoryLimit: number;
bandwidthLimit: number;
respectBattery: boolean;
onlyWhenIdle: boolean;
idleThreshold: number;
consentGiven: boolean;
consentTimestamp: Date | null;
}
interface NetworkState {
stats: NetworkStats;
nodes: NodeInfo[];
timeCrystal: TimeCrystal;
credits: CreditBalance;
isConnected: boolean;
isRelayConnected: boolean;
isLoading: boolean;
error: string | null;
startTime: number;
contributionSettings: ContributionSettings;
isWASMReady: boolean;
nodeId: string | null;
// Relay network state
relayNetworkState: RelayNetworkState | null;
connectedPeers: string[];
pendingTasks: TaskAssignment[];
// Firebase peers (alias for connectedPeers for backward compatibility)
firebasePeers: string[];
// Persisted cumulative values from IndexedDB
persistedCredits: number;
persistedTasks: number;
persistedUptime: number;
setStats: (stats: Partial<NetworkStats>) => void;
addNode: (node: NodeInfo) => void;
removeNode: (nodeId: string) => void;
updateNode: (nodeId: string, updates: Partial<NodeInfo>) => void;
setTimeCrystal: (crystal: Partial<TimeCrystal>) => void;
setCredits: (credits: Partial<CreditBalance>) => void;
setConnected: (connected: boolean) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
updateRealStats: () => void;
getUptime: () => number;
setContributionSettings: (settings: Partial<ContributionSettings>) => void;
giveConsent: () => void;
revokeConsent: () => void;
initializeEdgeNet: () => Promise<void>;
startContributing: () => void;
stopContributing: () => void;
saveToIndexedDB: () => Promise<void>;
loadFromIndexedDB: () => Promise<void>;
connectToRelay: () => Promise<boolean>;
disconnectFromRelay: () => void;
processAssignedTask: (task: TaskAssignment) => Promise<void>;
clearLocalData: () => Promise<void>;
}
const initialStats: NetworkStats = {
totalNodes: 0,
activeNodes: 0,
totalCompute: 0,
creditsEarned: 0,
tasksCompleted: 0,
uptime: 0,
latency: 0,
bandwidth: 0,
};
const initialTimeCrystal: TimeCrystal = {
phase: 0,
frequency: 1.618,
coherence: 0,
entropy: 1.0,
synchronizedNodes: 0,
};
const initialCredits: CreditBalance = {
available: 0,
pending: 0,
earned: 0,
spent: 0,
};
const defaultContributionSettings: ContributionSettings = {
enabled: false,
cpuLimit: 50,
gpuEnabled: false,
gpuLimit: 30,
memoryLimit: 512,
bandwidthLimit: 10,
respectBattery: true,
onlyWhenIdle: true,
idleThreshold: 30,
consentGiven: false,
consentTimestamp: null,
};
export const useNetworkStore = create<NetworkState>()((set, get) => ({
stats: initialStats,
nodes: [],
timeCrystal: initialTimeCrystal,
credits: initialCredits,
isConnected: false,
isRelayConnected: false,
isLoading: true,
error: null,
startTime: Date.now(),
contributionSettings: defaultContributionSettings,
isWASMReady: false,
nodeId: null,
relayNetworkState: null,
connectedPeers: [],
pendingTasks: [],
firebasePeers: [], // Kept in sync with connectedPeers for backward compatibility
persistedCredits: 0,
persistedTasks: 0,
persistedUptime: 0,
setStats: (stats) =>
set((state) => ({ stats: { ...state.stats, ...stats } })),
addNode: (node) =>
set((state) => {
const newNodes = [...state.nodes, node];
return {
nodes: newNodes,
stats: {
...state.stats,
totalNodes: newNodes.length,
activeNodes: newNodes.filter((n) => n.status === 'active').length,
},
};
}),
removeNode: (nodeId) =>
set((state) => {
const newNodes = state.nodes.filter((n) => n.id !== nodeId);
return {
nodes: newNodes,
stats: {
...state.stats,
totalNodes: newNodes.length,
activeNodes: newNodes.filter((n) => n.status === 'active').length,
},
};
}),
updateNode: (nodeId, updates) =>
set((state) => ({
nodes: state.nodes.map((n) =>
n.id === nodeId ? { ...n, ...updates } : n
),
})),
setTimeCrystal: (crystal) =>
set((state) => ({
timeCrystal: { ...state.timeCrystal, ...crystal },
})),
setCredits: (credits) =>
set((state) => ({
credits: { ...state.credits, ...credits },
})),
setConnected: (connected) =>
set({ isConnected: connected, isLoading: false }),
setLoading: (loading) => set({ isLoading: loading }),
setError: (error) => set({ error, isLoading: false }),
getUptime: () => {
const state = get();
return (Date.now() - state.startTime) / 1000;
},
setContributionSettings: (settings) =>
set((state) => ({
contributionSettings: { ...state.contributionSettings, ...settings },
})),
loadFromIndexedDB: async () => {
try {
const savedState = await storageService.loadState();
if (savedState) {
set({
persistedCredits: savedState.creditsEarned,
persistedTasks: savedState.tasksCompleted,
persistedUptime: savedState.totalUptime,
nodeId: savedState.nodeId,
contributionSettings: {
...defaultContributionSettings,
consentGiven: savedState.consentGiven,
consentTimestamp: savedState.consentTimestamp
? new Date(savedState.consentTimestamp)
: null,
cpuLimit: savedState.cpuLimit,
gpuEnabled: savedState.gpuEnabled,
gpuLimit: savedState.gpuLimit,
respectBattery: savedState.respectBattery,
onlyWhenIdle: savedState.onlyWhenIdle,
},
credits: {
earned: savedState.creditsEarned,
spent: savedState.creditsSpent,
available: savedState.creditsEarned - savedState.creditsSpent,
pending: 0,
},
stats: {
...initialStats,
creditsEarned: savedState.creditsEarned,
tasksCompleted: savedState.tasksCompleted,
},
});
console.log('[EdgeNet] Loaded persisted state:', savedState.creditsEarned, 'rUv');
}
} catch (error) {
console.error('[EdgeNet] Failed to load from IndexedDB:', error);
}
},
saveToIndexedDB: async () => {
const state = get();
try {
await storageService.saveState({
id: 'primary',
nodeId: state.nodeId,
creditsEarned: state.credits.earned,
creditsSpent: state.credits.spent,
tasksCompleted: state.stats.tasksCompleted,
tasksSubmitted: 0,
totalUptime: state.stats.uptime + state.persistedUptime,
lastActiveTimestamp: Date.now(),
consentGiven: state.contributionSettings.consentGiven,
consentTimestamp: state.contributionSettings.consentTimestamp?.getTime() || null,
cpuLimit: state.contributionSettings.cpuLimit,
gpuEnabled: state.contributionSettings.gpuEnabled,
gpuLimit: state.contributionSettings.gpuLimit,
respectBattery: state.contributionSettings.respectBattery,
onlyWhenIdle: state.contributionSettings.onlyWhenIdle,
});
} catch (error) {
console.error('[EdgeNet] Failed to save to IndexedDB:', error);
}
},
giveConsent: () => {
set((state) => ({
contributionSettings: {
...state.contributionSettings,
consentGiven: true,
consentTimestamp: new Date(),
},
}));
get().saveToIndexedDB();
console.log('[EdgeNet] User consent given for contribution');
},
revokeConsent: async () => {
const { stopContributing } = get();
stopContributing();
set((state) => ({
contributionSettings: {
...state.contributionSettings,
consentGiven: false,
consentTimestamp: null,
enabled: false,
},
}));
await storageService.clear();
console.log('[EdgeNet] User consent revoked, data cleared');
},
initializeEdgeNet: async () => {
try {
set({ isLoading: true, error: null });
console.log('[EdgeNet] Initializing...');
// Load persisted state from IndexedDB first
await get().loadFromIndexedDB();
// Initialize WASM module
await edgeNetService.init();
const isWASMReady = edgeNetService.isWASMAvailable();
set({ isWASMReady });
if (isWASMReady) {
console.log('[EdgeNet] WASM module ready');
const node = await edgeNetService.createNode();
if (node) {
const nodeId = node.nodeId();
set({ nodeId });
console.log('[EdgeNet] Node created:', nodeId);
edgeNetService.enableTimeCrystal(8);
// Auto-start if consent was previously given
const state = get();
if (state.contributionSettings.consentGiven) {
edgeNetService.startNode();
set((s) => ({
contributionSettings: { ...s.contributionSettings, enabled: true },
}));
console.log('[EdgeNet] Auto-started from previous session');
// Auto-connect to relay
setTimeout(() => {
get().connectToRelay();
}, 1000);
}
}
}
set({ isConnected: true, isLoading: false });
console.log('[EdgeNet] Initialization complete');
} catch (error) {
console.error('[EdgeNet] Initialization failed:', error);
set({
error: error instanceof Error ? error.message : 'Failed to initialize',
isLoading: false,
});
}
},
startContributing: async () => {
const { contributionSettings, isWASMReady, nodeId } = get();
if (!contributionSettings.consentGiven) {
console.warn('[EdgeNet] Cannot start without consent');
return;
}
// Start WASM node
if (isWASMReady) {
edgeNetService.startNode();
console.log('[EdgeNet] Started WASM node');
}
set((state) => ({
contributionSettings: { ...state.contributionSettings, enabled: true },
}));
// Connect to relay for distributed network
if (nodeId) {
const connected = await get().connectToRelay();
if (connected) {
console.log('[EdgeNet] Connected to distributed network');
}
}
get().saveToIndexedDB();
console.log('[EdgeNet] Started contributing');
},
stopContributing: () => {
// Pause WASM node
edgeNetService.pauseNode();
// Disconnect from relay
get().disconnectFromRelay();
set((state) => ({
contributionSettings: { ...state.contributionSettings, enabled: false },
}));
get().saveToIndexedDB();
console.log('[EdgeNet] Stopped contributing');
},
updateRealStats: () => {
const state = get();
const sessionUptime = (Date.now() - state.startTime) / 1000;
const totalUptime = sessionUptime + state.persistedUptime;
const { isWASMReady, contributionSettings } = state;
// Process epoch if contributing (advances WASM state)
if (isWASMReady && contributionSettings.enabled) {
edgeNetService.processEpoch();
edgeNetService.stepCapabilities(1.0);
edgeNetService.recordPerformance(0.95, 100);
// Submit demo tasks periodically (every ~5 seconds) and process them
if (Math.floor(sessionUptime) % 5 === 0) {
edgeNetService.submitDemoTask();
}
// Process any queued tasks to earn credits
edgeNetService.processNextTask().catch(() => {
// No tasks available is normal
});
}
// Get REAL stats from WASM node
const realStats = edgeNetService.getStats();
const timeCrystalSync = edgeNetService.getTimeCrystalSync();
const networkFitness = edgeNetService.getNetworkFitness();
// Debug: Log raw stats periodically
if (realStats && Math.floor(sessionUptime) % 10 === 0) {
console.log('[EdgeNet] Raw WASM stats:', {
ruv_earned: realStats.ruv_earned?.toString(),
tasks_completed: realStats.tasks_completed?.toString(),
multiplier: realStats.multiplier,
reputation: realStats.reputation,
timeCrystalSync,
networkFitness,
});
}
if (realStats) {
// Convert from nanoRuv (1e9) to Ruv
const sessionRuvEarned = Number(realStats.ruv_earned) / 1e9;
const sessionRuvSpent = Number(realStats.ruv_spent) / 1e9;
const sessionTasks = Number(realStats.tasks_completed);
// Add persisted values for cumulative totals
const totalRuvEarned = state.persistedCredits + sessionRuvEarned;
const totalTasks = state.persistedTasks + sessionTasks;
set({
stats: {
totalNodes: contributionSettings.enabled ? 1 : 0,
activeNodes: contributionSettings.enabled ? 1 : 0,
totalCompute: Math.round(networkFitness * (contributionSettings.cpuLimit / 100) * 100) / 100,
creditsEarned: Math.round(totalRuvEarned * 100) / 100,
tasksCompleted: totalTasks,
uptime: Math.round(totalUptime * 10) / 10,
latency: Math.round((1 - timeCrystalSync) * 100),
bandwidth: Math.round(contributionSettings.bandwidthLimit * 10) / 10,
},
timeCrystal: {
...state.timeCrystal,
phase: (state.timeCrystal.phase + 0.01) % 1,
coherence: Math.round(timeCrystalSync * 1000) / 1000,
entropy: Math.round((1 - timeCrystalSync * 0.8) * 1000) / 1000,
synchronizedNodes: contributionSettings.enabled ? 1 : 0,
},
credits: {
available: Math.round((totalRuvEarned - sessionRuvSpent - state.credits.spent) * 100) / 100,
pending: 0,
earned: Math.round(totalRuvEarned * 100) / 100,
spent: Math.round((sessionRuvSpent + state.credits.spent) * 100) / 100,
},
isConnected: isWASMReady || get().isRelayConnected,
isLoading: false,
});
// Save to IndexedDB periodically (every 10 seconds worth of updates)
if (Math.floor(sessionUptime) % 10 === 0) {
get().saveToIndexedDB();
}
} else {
// WASM not ready - show zeros but keep persisted values
set({
stats: {
...state.stats,
totalNodes: 0,
activeNodes: 0,
totalCompute: 0,
uptime: Math.round(totalUptime * 10) / 10,
creditsEarned: state.persistedCredits,
tasksCompleted: state.persistedTasks,
},
credits: {
...state.credits,
earned: state.persistedCredits,
},
isConnected: false,
isLoading: !isWASMReady,
});
}
},
connectToRelay: async () => {
const state = get();
if (!state.nodeId) {
console.warn('[EdgeNet] Cannot connect to relay without node ID');
return false;
}
// Set up relay event handlers
relayClient.setHandlers({
onConnected: (_nodeId, networkState, peers) => {
console.log('[EdgeNet] Connected to relay, peers:', peers.length);
set({
isRelayConnected: true,
relayNetworkState: networkState,
connectedPeers: peers,
firebasePeers: peers,
stats: {
...get().stats,
activeNodes: networkState.activeNodes + 1, // Include ourselves
totalNodes: networkState.totalNodes + 1,
},
timeCrystal: {
...get().timeCrystal,
phase: networkState.timeCrystalPhase,
synchronizedNodes: networkState.activeNodes + 1,
},
});
},
onDisconnected: () => {
console.log('[EdgeNet] Disconnected from relay');
set({
isRelayConnected: false,
connectedPeers: [],
firebasePeers: [],
});
},
onNodeJoined: (nodeId, totalNodes) => {
console.log('[EdgeNet] Peer joined:', nodeId);
set((s) => ({
connectedPeers: [...s.connectedPeers, nodeId],
firebasePeers: [...s.firebasePeers, nodeId],
stats: { ...s.stats, activeNodes: totalNodes, totalNodes },
timeCrystal: { ...s.timeCrystal, synchronizedNodes: totalNodes },
}));
},
onNodeLeft: (nodeId, totalNodes) => {
console.log('[EdgeNet] Peer left:', nodeId);
set((s) => ({
connectedPeers: s.connectedPeers.filter((id) => id !== nodeId),
firebasePeers: s.firebasePeers.filter((id) => id !== nodeId),
stats: { ...s.stats, activeNodes: totalNodes, totalNodes },
timeCrystal: { ...s.timeCrystal, synchronizedNodes: totalNodes },
}));
},
onTaskAssigned: (task) => {
console.log('[EdgeNet] Task assigned:', task.id);
set((s) => ({
pendingTasks: [...s.pendingTasks, task],
}));
// Auto-process the task
get().processAssignedTask(task);
},
onCreditEarned: (amount, taskId) => {
const ruvAmount = Number(amount) / 1e9; // Convert from nanoRuv
console.log('[EdgeNet] Credit earned:', ruvAmount, 'rUv for task', taskId);
set((s) => ({
credits: {
...s.credits,
earned: s.credits.earned + ruvAmount,
available: s.credits.available + ruvAmount,
},
stats: {
...s.stats,
creditsEarned: s.stats.creditsEarned + ruvAmount,
tasksCompleted: s.stats.tasksCompleted + 1,
},
}));
get().saveToIndexedDB();
},
onTimeCrystalSync: (phase, _timestamp, activeNodes) => {
set((s) => ({
timeCrystal: {
...s.timeCrystal,
phase,
synchronizedNodes: activeNodes,
coherence: Math.min(1, activeNodes / 10), // Coherence increases with more nodes
},
}));
},
onError: (error) => {
console.error('[EdgeNet] Relay error:', error);
set({ error: error.message });
},
});
// Connect to the relay
const connected = await relayClient.connect(state.nodeId);
if (connected) {
console.log('[EdgeNet] Relay connection established');
} else {
console.warn('[EdgeNet] Failed to connect to relay');
}
return connected;
},
disconnectFromRelay: () => {
relayClient.disconnect();
set({
isRelayConnected: false,
connectedPeers: [],
firebasePeers: [],
pendingTasks: [],
});
},
processAssignedTask: async (task) => {
const state = get();
if (!state.isWASMReady) {
console.warn('[EdgeNet] Cannot process task - WASM not ready');
return;
}
try {
console.log('[EdgeNet] Processing task:', task.id, task.taskType);
// Process the task using WASM
const result = await edgeNetService.submitTask(
task.taskType,
task.payload,
task.maxCredits
);
// Process the task in WASM node
await edgeNetService.processNextTask();
// Report completion to relay
const reward = task.maxCredits / BigInt(2); // Earn half the max credits
relayClient.completeTask(task.id, task.submitter, result, reward);
// Remove from pending
set((s) => ({
pendingTasks: s.pendingTasks.filter((t) => t.id !== task.id),
}));
console.log('[EdgeNet] Task completed:', task.id);
} catch (error) {
console.error('[EdgeNet] Task processing failed:', error);
}
},
clearLocalData: async () => {
// Disconnect from relay
get().disconnectFromRelay();
// Stop contributing
get().stopContributing();
// Clear IndexedDB
await storageService.clear();
// Reset state to defaults
set({
stats: initialStats,
nodes: [],
timeCrystal: initialTimeCrystal,
credits: initialCredits,
isConnected: false,
isRelayConnected: false,
isLoading: false,
error: null,
startTime: Date.now(),
contributionSettings: defaultContributionSettings,
isWASMReady: false,
nodeId: null,
relayNetworkState: null,
connectedPeers: [],
pendingTasks: [],
firebasePeers: [],
persistedCredits: 0,
persistedTasks: 0,
persistedUptime: 0,
});
console.log('[EdgeNet] Local data cleared');
},
}));

View File

@@ -0,0 +1,229 @@
import { create } from 'zustand';
import type { WASMModule, WASMBenchmark } from '../types';
interface WASMState {
modules: WASMModule[];
benchmarks: WASMBenchmark[];
isInitialized: boolean;
isLoading: boolean;
error: string | null;
wasmInstance: unknown | null;
// Actions
setModules: (modules: WASMModule[]) => void;
updateModule: (moduleId: string, updates: Partial<WASMModule>) => void;
addBenchmark: (benchmark: WASMBenchmark) => void;
clearBenchmarks: () => void;
setInitialized: (initialized: boolean) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
loadModule: (moduleId: string) => Promise<void>;
runBenchmark: (moduleId: string) => Promise<WASMBenchmark | null>;
}
// Actual WASM modules from the edge-net ecosystem
const defaultModules: WASMModule[] = [
{
id: 'edge-net',
name: '@ruvector/edge-net',
version: '0.1.1',
loaded: false,
size: 0, // Will be populated when loaded
features: ['Time Crystal', 'DAG Attention', 'P2P Swarm', 'Credit Economy', 'Adaptive Security'],
status: 'unloaded',
},
{
id: 'attention-unified',
name: '@ruvector/attention-unified-wasm',
version: '0.1.0',
loaded: false,
size: 0,
features: ['DAG Attention', 'Critical Path', 'Topological Sort'],
status: 'unloaded',
},
{
id: 'economy',
name: '@ruvector/economy-wasm',
version: '0.1.0',
loaded: false,
size: 0,
features: ['Credit Marketplace', 'Staking', 'Governance'],
status: 'unloaded',
},
{
id: 'exotic',
name: '@ruvector/exotic-wasm',
version: '0.1.0',
loaded: false,
size: 0,
features: ['Exotic AI', 'MinCut Signals', 'RAC Coherence'],
status: 'unloaded',
},
{
id: 'learning',
name: '@ruvector/learning-wasm',
version: '0.1.0',
loaded: false,
size: 0,
features: ['Q-Learning', 'Pattern Recognition', 'Self-Improvement'],
status: 'unloaded',
},
{
id: 'nervous-system',
name: '@ruvector/nervous-system-wasm',
version: '0.1.0',
loaded: false,
size: 0,
features: ['Neural Coordination', 'Homeostasis', 'Reflex Arcs'],
status: 'unloaded',
},
];
export const useWASMStore = create<WASMState>((set, get) => ({
modules: defaultModules,
benchmarks: [],
isInitialized: false,
isLoading: false,
error: null,
wasmInstance: null,
setModules: (modules) => set({ modules }),
updateModule: (moduleId, updates) =>
set((state) => ({
modules: state.modules.map((m) =>
m.id === moduleId ? { ...m, ...updates } : m
),
})),
addBenchmark: (benchmark) =>
set((state) => ({
benchmarks: [...state.benchmarks, benchmark],
})),
clearBenchmarks: () => set({ benchmarks: [] }),
setInitialized: (initialized) => set({ isInitialized: initialized }),
setLoading: (loading) => set({ isLoading: loading }),
setError: (error) => set({ error }),
loadModule: async (moduleId) => {
const { updateModule } = get();
updateModule(moduleId, { status: 'loading' });
try {
// Attempt to load actual WASM module from CDN
const module = get().modules.find(m => m.id === moduleId);
if (!module) throw new Error(`Module ${moduleId} not found`);
const startTime = performance.now();
// Try loading from unpkg CDN
const cdnUrl = `https://unpkg.com/${module.name}@${module.version}/ruvector_edge_net_bg.wasm`;
console.log(`[WASM] Loading ${module.name} from ${cdnUrl}...`);
try {
const response = await fetch(cdnUrl);
if (response.ok) {
const wasmBuffer = await response.arrayBuffer();
const loadTime = performance.now() - startTime;
updateModule(moduleId, {
status: 'ready',
loaded: true,
size: wasmBuffer.byteLength,
loadTime: Math.round(loadTime),
});
console.log(`[WASM] Module ${moduleId} loaded: ${(wasmBuffer.byteLength / 1024).toFixed(1)}KB in ${loadTime.toFixed(0)}ms`);
return;
}
} catch (fetchError) {
console.warn(`[WASM] CDN fetch failed for ${moduleId}, using local simulation`);
}
// Fallback: simulate loading if CDN unavailable
await new Promise((resolve) => setTimeout(resolve, 500 + Math.random() * 500));
const loadTime = performance.now() - startTime;
// Estimate realistic sizes based on actual WASM modules
const estimatedSizes: Record<string, number> = {
'edge-net': 3_200_000,
'attention-unified': 850_000,
'economy': 620_000,
'exotic': 780_000,
'learning': 540_000,
'nervous-system': 920_000,
};
updateModule(moduleId, {
status: 'ready',
loaded: true,
size: estimatedSizes[moduleId] || 500_000,
loadTime: Math.round(loadTime),
});
console.log(`[WASM] Module ${moduleId} loaded (simulated) in ${loadTime.toFixed(0)}ms`);
} catch (error) {
updateModule(moduleId, {
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error',
});
console.error(`[WASM] Failed to load ${moduleId}:`, error);
}
},
runBenchmark: async (moduleId) => {
const { modules, addBenchmark } = get();
const module = modules.find((m) => m.id === moduleId);
if (!module || !module.loaded) {
console.warn(`[WASM] Cannot benchmark unloaded module: ${moduleId}`);
return null;
}
console.log(`[WASM] Running benchmark for ${moduleId}...`);
// Run actual performance benchmark
const iterations = 1000;
const times: number[] = [];
// Warm up
for (let i = 0; i < 10; i++) {
await new Promise((r) => requestAnimationFrame(() => r(undefined)));
}
// Benchmark iterations
for (let i = 0; i < iterations; i++) {
const start = performance.now();
// Simulate WASM operation (matrix multiply, vector ops, etc)
const arr = new Float32Array(256);
for (let j = 0; j < 256; j++) {
arr[j] = Math.sin(j) * Math.cos(j);
}
times.push(performance.now() - start);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
const totalTime = times.reduce((a, b) => a + b, 0);
const benchmark: WASMBenchmark = {
moduleId,
operation: 'vector_ops_256',
iterations,
avgTime: Math.round(avgTime * 1000) / 1000,
minTime: Math.round(minTime * 1000) / 1000,
maxTime: Math.round(maxTime * 1000) / 1000,
throughput: Math.round(iterations / (totalTime / 1000)),
};
addBenchmark(benchmark);
console.log(`[WASM] Benchmark complete for ${moduleId}:`, benchmark);
return benchmark;
},
}));

View File

@@ -0,0 +1,99 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import { HeroUIProvider } from '@heroui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from '../App';
import { useNetworkStore } from '../stores/networkStore';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const renderApp = () => {
return render(
<QueryClientProvider client={queryClient}>
<HeroUIProvider>
<App />
</HeroUIProvider>
</QueryClientProvider>
);
};
describe('App', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset network store to initial state
useNetworkStore.setState({
stats: {
totalNodes: 0,
activeNodes: 0,
totalCompute: 0,
creditsEarned: 0,
tasksCompleted: 0,
uptime: 0,
latency: 0,
bandwidth: 0,
},
isConnected: false,
isLoading: true,
error: null,
startTime: Date.now(),
});
});
it('renders loading state initially', () => {
renderApp();
expect(screen.getByText(/Initializing Edge-Net/i)).toBeInTheDocument();
});
it('renders main dashboard after loading', async () => {
renderApp();
await waitFor(
() => {
expect(screen.getByText(/Network Overview/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it('renders header with Edge-Net branding', async () => {
renderApp();
await waitFor(
() => {
expect(screen.getByText('Edge-Net')).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it('shows connection status after network connects', async () => {
renderApp();
// Wait for loading to complete and dashboard to render
await waitFor(
() => {
expect(screen.getByText(/Network Overview/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Update real stats which sets isConnected: true
useNetworkStore.getState().updateRealStats();
// Now check for connection status - could be "Connected" or node count
await waitFor(
() => {
const state = useNetworkStore.getState();
// Verify the store state is connected
expect(state.isConnected).toBe(true);
},
{ timeout: 1000 }
);
});
});

View File

@@ -0,0 +1,92 @@
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { HeroUIProvider } from '@heroui/react';
import { StatCard } from '../components/common/StatCard';
import { GlowingBadge } from '../components/common/GlowingBadge';
import { CrystalLoader } from '../components/common/CrystalLoader';
const wrapper = ({ children }: { children: React.ReactNode }) => (
<HeroUIProvider>{children}</HeroUIProvider>
);
describe('StatCard', () => {
it('renders title and value', () => {
render(<StatCard title="Test Stat" value={1234} />, { wrapper });
expect(screen.getByText('Test Stat')).toBeInTheDocument();
expect(screen.getByText('1,234')).toBeInTheDocument();
});
it('renders string value correctly', () => {
render(<StatCard title="String Stat" value="45.8 TFLOPS" />, { wrapper });
expect(screen.getByText('45.8 TFLOPS')).toBeInTheDocument();
});
it('shows positive change indicator', () => {
render(<StatCard title="Test" value={100} change={5.5} />, { wrapper });
expect(screen.getByText(/5.5%/)).toBeInTheDocument();
expect(screen.getByText(/↑/)).toBeInTheDocument();
});
it('shows negative change indicator', () => {
render(<StatCard title="Test" value={100} change={-3.2} />, { wrapper });
expect(screen.getByText(/3.2%/)).toBeInTheDocument();
expect(screen.getByText(/↓/)).toBeInTheDocument();
});
it('applies different color variants', () => {
const { rerender } = render(
<StatCard title="Test" value={100} color="crystal" />,
{ wrapper }
);
expect(screen.getByText('Test')).toBeInTheDocument();
rerender(
<HeroUIProvider>
<StatCard title="Test" value={100} color="temporal" />
</HeroUIProvider>
);
expect(screen.getByText('Test')).toBeInTheDocument();
});
});
describe('GlowingBadge', () => {
it('renders children content', () => {
render(<GlowingBadge>Test Badge</GlowingBadge>, { wrapper });
expect(screen.getByText('Test Badge')).toBeInTheDocument();
});
it('applies different color variants', () => {
render(<GlowingBadge color="success">Success</GlowingBadge>, { wrapper });
expect(screen.getByText('Success')).toBeInTheDocument();
});
});
describe('CrystalLoader', () => {
it('renders without text', () => {
const { container } = render(<CrystalLoader />, { wrapper });
expect(container.firstChild).toBeInTheDocument();
});
it('renders with text', () => {
render(<CrystalLoader text="Loading..." />, { wrapper });
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('supports different sizes', () => {
const { rerender, container } = render(<CrystalLoader size="sm" />, { wrapper });
expect(container.firstChild).toBeInTheDocument();
rerender(<HeroUIProvider><CrystalLoader size="lg" /></HeroUIProvider>);
expect(container.firstChild).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,118 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
initDebugConsole,
subscribeToLogs,
getLogs,
clearLogs,
debug,
timing,
} from '../utils/debug';
describe('Debug Console', () => {
beforeEach(() => {
clearLogs();
vi.clearAllMocks();
});
describe('initDebugConsole', () => {
it('initializes without errors', () => {
expect(() => initDebugConsole()).not.toThrow();
});
it('overrides console methods', () => {
initDebugConsole();
// Console.log should still work
expect(() => console.log('test')).not.toThrow();
});
});
describe('debug logging', () => {
it('logs info messages', () => {
debug.info('Test info message', { data: 'test' });
const logs = getLogs();
expect(logs.some((l) => l.message === 'Test info message')).toBe(true);
});
it('logs warning messages', () => {
debug.warn('Test warning', { warning: true });
const logs = getLogs();
expect(logs.some((l) => l.level === 'warn')).toBe(true);
});
it('logs error messages', () => {
debug.error('Test error');
const logs = getLogs();
expect(logs.some((l) => l.level === 'error')).toBe(true);
});
it('logs debug messages', () => {
debug.debug('Debug message');
const logs = getLogs();
expect(logs.some((l) => l.level === 'debug')).toBe(true);
});
});
describe('subscribeToLogs', () => {
it('notifies subscribers on new logs', () => {
const listener = vi.fn();
subscribeToLogs(listener);
debug.log('New log');
expect(listener).toHaveBeenCalled();
});
it('returns unsubscribe function', () => {
const listener = vi.fn();
const unsubscribe = subscribeToLogs(listener);
unsubscribe();
listener.mockClear();
debug.log('After unsubscribe');
// Listener should not be called after unsubscribe
});
});
describe('clearLogs', () => {
it('removes all logs', () => {
debug.log('Log 1');
debug.log('Log 2');
expect(getLogs().length).toBeGreaterThan(0);
clearLogs();
expect(getLogs().length).toBe(0);
});
});
describe('timing', () => {
it('starts and ends timing', () => {
timing.start('test-operation');
const duration = timing.end('test-operation');
expect(duration).toBeGreaterThanOrEqual(0);
});
it('returns 0 for unknown labels', () => {
const duration = timing.end('unknown-label');
expect(duration).toBe(0);
});
it('measures async operations', async () => {
const result = await timing.measure('async-op', async () => {
await new Promise((r) => setTimeout(r, 10));
return 'done';
});
expect(result).toBe('done');
});
});
});

View File

@@ -0,0 +1,71 @@
import '@testing-library/jest-dom';
import { afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
// Mock ResizeObserver
(globalThis as Record<string, unknown>).ResizeObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Mock IntersectionObserver
(globalThis as Record<string, unknown>).IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
// Mock canvas context
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue({
clearRect: vi.fn(),
beginPath: vi.fn(),
moveTo: vi.fn(),
lineTo: vi.fn(),
stroke: vi.fn(),
arc: vi.fn(),
fill: vi.fn(),
fillRect: vi.fn(),
createRadialGradient: vi.fn().mockReturnValue({
addColorStop: vi.fn(),
}),
scale: vi.fn(),
});
// Mock requestAnimationFrame
(globalThis as Record<string, unknown>).requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
return setTimeout(() => callback(performance.now()), 16) as unknown as number;
});
(globalThis as Record<string, unknown>).cancelAnimationFrame = vi.fn((id: number) => {
clearTimeout(id);
});
// Mock performance.now
if (!globalThis.performance) {
(globalThis as unknown as Record<string, unknown>).performance = {} as Performance;
}
Object.defineProperty(globalThis.performance, 'now', {
value: vi.fn(() => Date.now()),
writable: true,
});

View File

@@ -0,0 +1,196 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { useNetworkStore } from '../stores/networkStore';
import { useWASMStore } from '../stores/wasmStore';
import { useMCPStore } from '../stores/mcpStore';
import { useCDNStore } from '../stores/cdnStore';
describe('Network Store', () => {
beforeEach(() => {
// Reset to initial state (real data starts at 0)
useNetworkStore.setState({
stats: {
totalNodes: 0,
activeNodes: 0,
totalCompute: 0,
creditsEarned: 0,
tasksCompleted: 0,
uptime: 0,
latency: 0,
bandwidth: 0,
},
isConnected: false,
isLoading: true,
error: null,
startTime: Date.now(),
});
});
it('should start with empty network (real data)', () => {
const { stats } = useNetworkStore.getState();
expect(stats.totalNodes).toBe(0);
expect(stats.activeNodes).toBe(0);
});
it('should update stats', () => {
const { setStats } = useNetworkStore.getState();
setStats({ activeNodes: 5, totalNodes: 10 });
const { stats } = useNetworkStore.getState();
expect(stats.activeNodes).toBe(5);
expect(stats.totalNodes).toBe(10);
});
it('should update real stats and track network', () => {
// Run multiple ticks to ensure stats update
for (let i = 0; i < 50; i++) {
useNetworkStore.getState().updateRealStats();
}
const { stats, isConnected } = useNetworkStore.getState();
// Network should be connected after updates
expect(isConnected).toBe(true);
// Some metrics should have updated
expect(typeof stats.totalCompute).toBe('number');
expect(typeof stats.uptime).toBe('number');
});
it('should track connection status', () => {
const { setConnected } = useNetworkStore.getState();
setConnected(false);
expect(useNetworkStore.getState().isConnected).toBe(false);
expect(useNetworkStore.getState().isLoading).toBe(false);
setConnected(true);
expect(useNetworkStore.getState().isConnected).toBe(true);
});
it('should calculate uptime', () => {
const { getUptime } = useNetworkStore.getState();
const uptime = getUptime();
expect(typeof uptime).toBe('number');
expect(uptime).toBeGreaterThanOrEqual(0);
});
});
describe('WASM Store', () => {
it('should have default modules', () => {
const { modules } = useWASMStore.getState();
expect(modules.length).toBeGreaterThan(0);
expect(modules[0].id).toBe('edge-net');
expect(modules[0].version).toBe('0.1.1');
});
it('should start with unloaded modules', () => {
const { modules } = useWASMStore.getState();
const edgeNet = modules.find(m => m.id === 'edge-net');
expect(edgeNet?.loaded).toBe(false);
expect(edgeNet?.status).toBe('unloaded');
expect(edgeNet?.size).toBe(0); // Size unknown until loaded
});
it('should update module status', () => {
const { updateModule } = useWASMStore.getState();
updateModule('edge-net', { status: 'loading' });
const updatedModules = useWASMStore.getState().modules;
const edgeNet = updatedModules.find((m) => m.id === 'edge-net');
expect(edgeNet?.status).toBe('loading');
});
it('should track benchmarks', () => {
const { addBenchmark, benchmarks } = useWASMStore.getState();
const initialCount = benchmarks.length;
addBenchmark({
moduleId: 'edge-net',
operation: 'vector_ops_256',
iterations: 1000,
avgTime: 0.05,
minTime: 0.01,
maxTime: 0.15,
throughput: 20000,
});
expect(useWASMStore.getState().benchmarks.length).toBe(initialCount + 1);
});
it('should clear benchmarks', () => {
const { addBenchmark, clearBenchmarks } = useWASMStore.getState();
addBenchmark({
moduleId: 'edge-net',
operation: 'test',
iterations: 100,
avgTime: 1,
minTime: 0.5,
maxTime: 2,
throughput: 100,
});
clearBenchmarks();
expect(useWASMStore.getState().benchmarks.length).toBe(0);
});
});
describe('MCP Store', () => {
it('should have default tools', () => {
const { tools } = useMCPStore.getState();
expect(tools.length).toBeGreaterThan(0);
expect(tools.some((t) => t.category === 'swarm')).toBe(true);
});
it('should update tool status', () => {
const { updateTool } = useMCPStore.getState();
updateTool('swarm_init', { status: 'running' });
const updatedTools = useMCPStore.getState().tools;
const tool = updatedTools.find((t) => t.id === 'swarm_init');
expect(tool?.status).toBe('running');
});
it('should add results', () => {
const { addResult } = useMCPStore.getState();
addResult({
toolId: 'swarm_init',
success: true,
data: { test: true },
timestamp: new Date(),
duration: 100,
});
const { results } = useMCPStore.getState();
expect(results.length).toBeGreaterThan(0);
});
});
describe('CDN Store', () => {
it('should have default scripts', () => {
const { scripts } = useCDNStore.getState();
expect(scripts.length).toBeGreaterThan(0);
expect(scripts.some((s) => s.category === 'wasm')).toBe(true);
});
it('should toggle script enabled state', () => {
const { toggleScript, scripts } = useCDNStore.getState();
const initialEnabled = scripts[0].enabled;
toggleScript(scripts[0].id);
const updatedScripts = useCDNStore.getState().scripts;
expect(updatedScripts[0].enabled).toBe(!initialEnabled);
});
it('should track auto-load setting', () => {
const { setAutoLoad } = useCDNStore.getState();
setAutoLoad(true);
expect(useCDNStore.getState().autoLoad).toBe(true);
setAutoLoad(false);
expect(useCDNStore.getState().autoLoad).toBe(false);
});
});

View File

@@ -0,0 +1,174 @@
// Network Stats Types
export interface NetworkStats {
totalNodes: number;
activeNodes: number;
totalCompute: number; // TFLOPS
creditsEarned: number;
tasksCompleted: number;
uptime: number; // percentage
latency: number; // ms
bandwidth: number; // Mbps
}
export interface NodeInfo {
id: string;
status: 'online' | 'offline' | 'busy' | 'idle' | 'active';
computePower: number;
creditsEarned: number;
tasksCompleted: number;
location?: string;
lastSeen: Date;
}
// CDN Configuration
export interface CDNScript {
id: string;
name: string;
description: string;
url: string;
size: string;
category: 'wasm' | 'ai' | 'crypto' | 'network' | 'utility';
enabled: boolean;
loaded: boolean;
}
export interface CDNConfig {
scripts: CDNScript[];
autoLoad: boolean;
cacheEnabled: boolean;
}
// MCP Tool Types
export interface MCPTool {
id: string;
name: string;
description: string;
category: 'swarm' | 'agent' | 'memory' | 'neural' | 'task' | 'github';
status: 'ready' | 'running' | 'error' | 'disabled';
lastRun?: Date;
parameters?: Record<string, unknown>;
}
export interface MCPResult {
toolId: string;
success: boolean;
data?: unknown;
error?: string;
timestamp: Date;
duration: number;
}
// WASM Module Types
export interface WASMModule {
id: string;
name: string;
version: string;
loaded: boolean;
size: number;
features: string[];
status: 'loading' | 'ready' | 'error' | 'unloaded';
error?: string;
loadTime?: number; // ms to load
}
export interface WASMBenchmark {
moduleId: string;
operation: string;
iterations: number;
avgTime: number;
minTime: number;
maxTime: number;
throughput: number;
}
// Dashboard State
export interface DashboardTab {
id: string;
label: string;
icon: string;
badge?: number;
}
export interface ModalConfig {
id: string;
title: string;
isOpen: boolean;
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
// Time Crystal Types
export interface TimeCrystal {
phase: number;
frequency: number;
coherence: number;
entropy: number;
synchronizedNodes: number;
}
export interface TemporalMetrics {
crystalPhase: number;
driftCorrection: number;
consensusLatency: number;
epochNumber: number;
}
// Specialized Networks
export interface SpecializedNetwork {
id: string;
name: string;
description: string;
category: 'science' | 'finance' | 'healthcare' | 'ai' | 'gaming' | 'social' | 'compute';
icon: string;
color: string;
stats: {
nodes: number;
compute: number; // TFLOPS
tasks: number;
uptime: number; // percentage
};
requirements: {
minCompute: number;
minBandwidth: number;
capabilities: string[];
};
rewards: {
baseRate: number; // credits per hour
bonusMultiplier: number;
};
status: 'active' | 'maintenance' | 'launching' | 'closed';
joined: boolean;
joinedAt?: Date;
}
// Credit Economy
export interface CreditBalance {
available: number;
pending: number;
earned: number;
spent: number;
}
export interface CreditTransaction {
id: string;
type: 'earn' | 'spend' | 'transfer';
amount: number;
description: string;
timestamp: Date;
}
// Debug Console
export interface DebugLog {
id: string;
level: 'info' | 'warn' | 'error' | 'debug';
message: string;
data?: unknown;
timestamp: Date;
source: string;
}
export interface DebugState {
logs: DebugLog[];
isVisible: boolean;
filter: string;
level: 'all' | 'info' | 'warn' | 'error' | 'debug';
}

View File

@@ -0,0 +1,169 @@
import type { DebugLog } from '../types';
// Debug state
let debugLogs: DebugLog[] = [];
let logListeners: ((logs: DebugLog[]) => void)[] = [];
let isConsoleOverridden = false;
const MAX_LOGS = 500;
// Generate unique ID
const generateId = () => Math.random().toString(36).substr(2, 9);
// Add log entry
const addLog = (level: DebugLog['level'], message: string, data?: unknown, source = 'app') => {
const log: DebugLog = {
id: generateId(),
level,
message,
data,
timestamp: new Date(),
source,
};
debugLogs = [log, ...debugLogs].slice(0, MAX_LOGS);
logListeners.forEach((listener) => listener(debugLogs));
};
// Initialize debug console
export const initDebugConsole = () => {
if (isConsoleOverridden) return;
isConsoleOverridden = true;
const originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
info: console.info.bind(console),
debug: console.debug.bind(console),
};
// Override console methods
console.log = (...args: unknown[]) => {
originalConsole.log(...args);
addLog('info', formatArgs(args), args.length > 1 ? args : undefined);
};
console.warn = (...args: unknown[]) => {
originalConsole.warn(...args);
addLog('warn', formatArgs(args), args.length > 1 ? args : undefined);
};
console.error = (...args: unknown[]) => {
originalConsole.error(...args);
addLog('error', formatArgs(args), args.length > 1 ? args : undefined);
};
console.info = (...args: unknown[]) => {
originalConsole.info(...args);
addLog('info', formatArgs(args), args.length > 1 ? args : undefined);
};
console.debug = (...args: unknown[]) => {
originalConsole.debug(...args);
addLog('debug', formatArgs(args), args.length > 1 ? args : undefined);
};
// Add global debug utilities
(window as any).edgeNet = {
logs: () => debugLogs,
clear: () => {
debugLogs = [];
logListeners.forEach((listener) => listener(debugLogs));
},
export: () => JSON.stringify(debugLogs, null, 2),
stats: () => ({
total: debugLogs.length,
byLevel: debugLogs.reduce((acc, log) => {
acc[log.level] = (acc[log.level] || 0) + 1;
return acc;
}, {} as Record<string, number>),
bySource: debugLogs.reduce((acc, log) => {
acc[log.source] = (acc[log.source] || 0) + 1;
return acc;
}, {} as Record<string, number>),
}),
};
// Log initialization
console.log('[Debug] Console debug utilities initialized');
console.log('[Debug] Access debug tools via window.edgeNet');
};
// Format console arguments
const formatArgs = (args: unknown[]): string => {
return args
.map((arg) => {
if (typeof arg === 'string') return arg;
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
try {
return JSON.stringify(arg);
} catch {
return String(arg);
}
})
.join(' ');
};
// Subscribe to log updates
export const subscribeToLogs = (listener: (logs: DebugLog[]) => void) => {
logListeners.push(listener);
listener(debugLogs);
return () => {
logListeners = logListeners.filter((l) => l !== listener);
};
};
// Get current logs
export const getLogs = () => debugLogs;
// Clear logs
export const clearLogs = () => {
debugLogs = [];
logListeners.forEach((listener) => listener(debugLogs));
};
// Manual log functions
export const debug = {
log: (message: string, data?: unknown, source?: string) =>
addLog('info', message, data, source),
warn: (message: string, data?: unknown, source?: string) =>
addLog('warn', message, data, source),
error: (message: string, data?: unknown, source?: string) =>
addLog('error', message, data, source),
debug: (message: string, data?: unknown, source?: string) =>
addLog('debug', message, data, source),
info: (message: string, data?: unknown, source?: string) =>
addLog('info', message, data, source),
};
// Performance timing utilities
export const timing = {
marks: new Map<string, number>(),
start: (label: string) => {
timing.marks.set(label, performance.now());
console.debug(`[Timing] Started: ${label}`);
},
end: (label: string) => {
const start = timing.marks.get(label);
if (start) {
const duration = performance.now() - start;
timing.marks.delete(label);
console.debug(`[Timing] ${label}: ${duration.toFixed(2)}ms`);
return duration;
}
return 0;
},
measure: async <T>(label: string, fn: () => Promise<T>): Promise<T> => {
timing.start(label);
try {
return await fn();
} finally {
timing.end(label);
}
},
};