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,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>
);
}