Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
237
vendor/ruvector/examples/edge-net/dashboard/src/App.tsx
vendored
Normal file
237
vendor/ruvector/examples/edge-net/dashboard/src/App.tsx
vendored
Normal file
@@ -0,0 +1,237 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Header } from './components/dashboard/Header';
|
||||
import { Sidebar } from './components/dashboard/Sidebar';
|
||||
import { NetworkStats } from './components/network/NetworkStats';
|
||||
import { NetworkVisualization } from './components/network/NetworkVisualization';
|
||||
import { SpecializedNetworks } from './components/network/SpecializedNetworks';
|
||||
import { CDNPanel } from './components/cdn/CDNPanel';
|
||||
import { WASMModules } from './components/wasm/WASMModules';
|
||||
import { MCPTools } from './components/mcp/MCPTools';
|
||||
import { CreditsPanel } from './components/dashboard/CreditsPanel';
|
||||
import { ConsolePanel } from './components/dashboard/ConsolePanel';
|
||||
import { IdentityPanel } from './components/identity/IdentityPanel';
|
||||
import { DocumentationPanel } from './components/docs/DocumentationPanel';
|
||||
import { CrystalLoader } from './components/common/CrystalLoader';
|
||||
import { ConsentWidget } from './components/common/ConsentWidget';
|
||||
import { useNetworkStore } from './stores/networkStore';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { initializeEdgeNet, updateRealStats, isWASMReady } = useNetworkStore();
|
||||
|
||||
// Check for mobile viewport
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth < 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
// Initialize real EdgeNet WASM module
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
await initializeEdgeNet();
|
||||
console.log('[App] EdgeNet initialized, WASM ready:', isWASMReady);
|
||||
} catch (error) {
|
||||
console.error('[App] EdgeNet initialization failed:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, [initializeEdgeNet, isWASMReady]);
|
||||
|
||||
// Update real stats from EdgeNet node
|
||||
useEffect(() => {
|
||||
const interval = setInterval(updateRealStats, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [updateRealStats]);
|
||||
|
||||
// Render active tab content
|
||||
const renderContent = () => {
|
||||
const content = {
|
||||
overview: (
|
||||
<div className="space-y-6">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<h1 className="text-2xl md:text-3xl font-bold mb-2">
|
||||
<span className="bg-gradient-to-r from-sky-400 via-violet-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Network Overview
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-zinc-400">
|
||||
Monitor your distributed compute network in real-time
|
||||
</p>
|
||||
</motion.div>
|
||||
<NetworkStats />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<NetworkVisualization />
|
||||
<motion.div
|
||||
className="crystal-card p-4"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<h3 className="text-sm font-medium text-zinc-400 mb-3">Quick Actions</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
className="p-4 rounded-lg bg-sky-500/10 border border-sky-500/30 hover:bg-sky-500/20 transition-colors text-left"
|
||||
onClick={() => setActiveTab('wasm')}
|
||||
>
|
||||
<p className="font-medium text-white">Load WASM</p>
|
||||
<p className="text-xs text-zinc-400 mt-1">Initialize modules</p>
|
||||
</button>
|
||||
<button
|
||||
className="p-4 rounded-lg bg-violet-500/10 border border-violet-500/30 hover:bg-violet-500/20 transition-colors text-left"
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
>
|
||||
<p className="font-medium text-white">MCP Tools</p>
|
||||
<p className="text-xs text-zinc-400 mt-1">Execute tools</p>
|
||||
</button>
|
||||
<button
|
||||
className="p-4 rounded-lg bg-emerald-500/10 border border-emerald-500/30 hover:bg-emerald-500/20 transition-colors text-left"
|
||||
onClick={() => setActiveTab('cdn')}
|
||||
>
|
||||
<p className="font-medium text-white">CDN Scripts</p>
|
||||
<p className="text-xs text-zinc-400 mt-1">Load libraries</p>
|
||||
</button>
|
||||
<button
|
||||
className="p-4 rounded-lg bg-amber-500/10 border border-amber-500/30 hover:bg-amber-500/20 transition-colors text-left"
|
||||
onClick={() => setActiveTab('identity')}
|
||||
>
|
||||
<p className="font-medium text-white">Identity</p>
|
||||
<p className="text-xs text-zinc-400 mt-1">Crypto ID & Networks</p>
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
network: (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span className="bg-gradient-to-r from-sky-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Network & Communities
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-zinc-400">Join specialized networks to earn credits by contributing compute</p>
|
||||
<NetworkStats />
|
||||
<SpecializedNetworks />
|
||||
<div className="mt-8">
|
||||
<h2 className="text-lg font-semibold text-zinc-300 mb-4">Network Topology</h2>
|
||||
<NetworkVisualization />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
wasm: (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">WASM Modules</h1>
|
||||
<WASMModules />
|
||||
</div>
|
||||
),
|
||||
cdn: (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">CDN Script Manager</h1>
|
||||
<CDNPanel />
|
||||
</div>
|
||||
),
|
||||
mcp: <MCPTools />,
|
||||
credits: (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">Credit Economy</h1>
|
||||
<CreditsPanel />
|
||||
</div>
|
||||
),
|
||||
identity: (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span className="bg-gradient-to-r from-amber-400 to-orange-400 bg-clip-text text-transparent">
|
||||
Identity & Networks
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-zinc-400">Manage your cryptographic identity and network participation</p>
|
||||
<IdentityPanel />
|
||||
</div>
|
||||
),
|
||||
console: <ConsolePanel />,
|
||||
activity: (
|
||||
<div className="crystal-card p-8 text-center">
|
||||
<p className="text-zinc-400">Activity log coming soon...</p>
|
||||
</div>
|
||||
),
|
||||
settings: (
|
||||
<div className="crystal-card p-8 text-center">
|
||||
<p className="text-zinc-400">Settings panel coming soon...</p>
|
||||
</div>
|
||||
),
|
||||
docs: (
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-2xl font-bold">
|
||||
<span className="bg-gradient-to-r from-sky-400 to-cyan-400 bg-clip-text text-transparent">
|
||||
Documentation
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-zinc-400">Learn how to use Edge-Net and integrate it into your projects</p>
|
||||
<DocumentationPanel />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
return content[activeTab as keyof typeof content] || content.overview;
|
||||
};
|
||||
|
||||
// Loading screen
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<CrystalLoader size="lg" text="Initializing Edge-Net..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header
|
||||
onMenuToggle={() => setIsSidebarOpen(true)}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidebar
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
isOpen={isSidebarOpen}
|
||||
onClose={() => setIsSidebarOpen(false)}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-auto p-4 md:p-6 quantum-grid">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={activeTab}
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="max-w-7xl mx-auto"
|
||||
>
|
||||
{renderContent()}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Floating consent widget for CPU/GPU contribution */}
|
||||
<ConsentWidget />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1
vendor/ruvector/examples/edge-net/dashboard/src/assets/react.svg
vendored
Normal file
1
vendor/ruvector/examples/edge-net/dashboard/src/assets/react.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
746
vendor/ruvector/examples/edge-net/dashboard/src/components/cdn/CDNPanel.tsx
vendored
Normal file
746
vendor/ruvector/examples/edge-net/dashboard/src/components/cdn/CDNPanel.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
455
vendor/ruvector/examples/edge-net/dashboard/src/components/common/ConsentWidget.tsx
vendored
Normal file
455
vendor/ruvector/examples/edge-net/dashboard/src/components/common/ConsentWidget.tsx
vendored
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
82
vendor/ruvector/examples/edge-net/dashboard/src/components/common/CrystalLoader.tsx
vendored
Normal file
82
vendor/ruvector/examples/edge-net/dashboard/src/components/common/CrystalLoader.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
68
vendor/ruvector/examples/edge-net/dashboard/src/components/common/GlowingBadge.tsx
vendored
Normal file
68
vendor/ruvector/examples/edge-net/dashboard/src/components/common/GlowingBadge.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
96
vendor/ruvector/examples/edge-net/dashboard/src/components/common/StatCard.tsx
vendored
Normal file
96
vendor/ruvector/examples/edge-net/dashboard/src/components/common/StatCard.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
332
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/ActivityPanel.tsx
vendored
Normal file
332
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/ActivityPanel.tsx
vendored
Normal 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;
|
||||
225
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/ConsolePanel.tsx
vendored
Normal file
225
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/ConsolePanel.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
197
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/CreditsPanel.tsx
vendored
Normal file
197
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/CreditsPanel.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
133
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/Header.tsx
vendored
Normal file
133
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/Header.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
404
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/SettingsPanel.tsx
vendored
Normal file
404
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/SettingsPanel.tsx
vendored
Normal 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;
|
||||
166
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/Sidebar.tsx
vendored
Normal file
166
vendor/ruvector/examples/edge-net/dashboard/src/components/dashboard/Sidebar.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
488
vendor/ruvector/examples/edge-net/dashboard/src/components/docs/DocumentationPanel.tsx
vendored
Normal file
488
vendor/ruvector/examples/edge-net/dashboard/src/components/docs/DocumentationPanel.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
624
vendor/ruvector/examples/edge-net/dashboard/src/components/identity/IdentityPanel.tsx
vendored
Normal file
624
vendor/ruvector/examples/edge-net/dashboard/src/components/identity/IdentityPanel.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
215
vendor/ruvector/examples/edge-net/dashboard/src/components/mcp/MCPTools.tsx
vendored
Normal file
215
vendor/ruvector/examples/edge-net/dashboard/src/components/mcp/MCPTools.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
185
vendor/ruvector/examples/edge-net/dashboard/src/components/network/NetworkStats.tsx
vendored
Normal file
185
vendor/ruvector/examples/edge-net/dashboard/src/components/network/NetworkStats.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
129
vendor/ruvector/examples/edge-net/dashboard/src/components/network/NetworkVisualization.tsx
vendored
Normal file
129
vendor/ruvector/examples/edge-net/dashboard/src/components/network/NetworkVisualization.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
588
vendor/ruvector/examples/edge-net/dashboard/src/components/network/SpecializedNetworks.tsx
vendored
Normal file
588
vendor/ruvector/examples/edge-net/dashboard/src/components/network/SpecializedNetworks.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
225
vendor/ruvector/examples/edge-net/dashboard/src/components/wasm/WASMModules.tsx
vendored
Normal file
225
vendor/ruvector/examples/edge-net/dashboard/src/components/wasm/WASMModules.tsx
vendored
Normal 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>
|
||||
);
|
||||
}
|
||||
44
vendor/ruvector/examples/edge-net/dashboard/src/hooks/useMediaQuery.ts
vendored
Normal file
44
vendor/ruvector/examples/edge-net/dashboard/src/hooks/useMediaQuery.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export function useMediaQuery(query: string): boolean {
|
||||
const [matches, setMatches] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(query);
|
||||
|
||||
// Set initial value
|
||||
setMatches(media.matches);
|
||||
|
||||
// Create listener
|
||||
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
|
||||
|
||||
// Add listener
|
||||
media.addEventListener('change', listener);
|
||||
|
||||
// Cleanup
|
||||
return () => media.removeEventListener('change', listener);
|
||||
}, [query]);
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
// Convenience hooks for common breakpoints
|
||||
export function useIsMobile() {
|
||||
return useMediaQuery('(max-width: 768px)');
|
||||
}
|
||||
|
||||
export function useIsTablet() {
|
||||
return useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
||||
}
|
||||
|
||||
export function useIsDesktop() {
|
||||
return useMediaQuery('(min-width: 1025px)');
|
||||
}
|
||||
|
||||
export function usePrefersDarkMode() {
|
||||
return useMediaQuery('(prefers-color-scheme: dark)');
|
||||
}
|
||||
|
||||
export function usePrefersReducedMotion() {
|
||||
return useMediaQuery('(prefers-reduced-motion: reduce)');
|
||||
}
|
||||
154
vendor/ruvector/examples/edge-net/dashboard/src/index.css
vendored
Normal file
154
vendor/ruvector/examples/edge-net/dashboard/src/index.css
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Time Crystal Theme Base Styles */
|
||||
:root {
|
||||
--crystal-glow: rgba(14, 165, 233, 0.5);
|
||||
--temporal-glow: rgba(124, 58, 237, 0.5);
|
||||
--quantum-glow: rgba(6, 182, 212, 0.5);
|
||||
}
|
||||
|
||||
/* Dark mode base */
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-[#0a0a0f] text-zinc-200 antialiased;
|
||||
background-image:
|
||||
radial-gradient(ellipse at 20% 30%, rgba(14, 165, 233, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 70%, rgba(124, 58, 237, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 50% 50%, rgba(6, 182, 212, 0.03) 0%, transparent 70%);
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Time Crystal Card Effects */
|
||||
.crystal-card {
|
||||
@apply relative overflow-hidden rounded-xl border border-white/10 bg-zinc-900/50 backdrop-blur-xl;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.crystal-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(14, 165, 233, 0.5), transparent);
|
||||
}
|
||||
|
||||
.crystal-card:hover {
|
||||
border-color: rgba(14, 165, 233, 0.3);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(14, 165, 233, 0.1),
|
||||
0 4px 20px -2px rgba(14, 165, 233, 0.15),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* Glowing elements */
|
||||
.glow-text {
|
||||
text-shadow: 0 0 10px var(--crystal-glow), 0 0 20px var(--crystal-glow);
|
||||
}
|
||||
|
||||
.glow-border {
|
||||
box-shadow: 0 0 10px var(--crystal-glow), inset 0 0 10px rgba(14, 165, 233, 0.1);
|
||||
}
|
||||
|
||||
/* Time Crystal Animation */
|
||||
.crystal-pulse {
|
||||
animation: crystal-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes crystal-pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Data stream animation */
|
||||
.data-stream {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(14, 165, 233, 0.3),
|
||||
rgba(124, 58, 237, 0.3),
|
||||
transparent
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: data-flow 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes data-flow {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
/* Quantum grid background */
|
||||
.quantum-grid {
|
||||
background-image:
|
||||
linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
}
|
||||
|
||||
/* Network node visualization */
|
||||
.network-node {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.network-node::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(14, 165, 233, 0.3) 0%, transparent 70%);
|
||||
animation: node-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes node-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||
50% { transform: scale(1.5); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Stat counter animation */
|
||||
.stat-value {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(14, 165, 233, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(14, 165, 233, 0.5);
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.crystal-card {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
31
vendor/ruvector/examples/edge-net/dashboard/src/main.tsx
vendored
Normal file
31
vendor/ruvector/examples/edge-net/dashboard/src/main.tsx
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { HeroUIProvider } from '@heroui/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
import { initDebugConsole } from './utils/debug';
|
||||
|
||||
// Initialize debug console
|
||||
initDebugConsole();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5000,
|
||||
refetchInterval: 10000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HeroUIProvider>
|
||||
<main className="dark min-h-screen">
|
||||
<App />
|
||||
</main>
|
||||
</HeroUIProvider>
|
||||
</QueryClientProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
546
vendor/ruvector/examples/edge-net/dashboard/src/services/edgeNet.ts
vendored
Normal file
546
vendor/ruvector/examples/edge-net/dashboard/src/services/edgeNet.ts
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
/**
|
||||
* EdgeNet Service - Real WASM Integration
|
||||
*
|
||||
* Provides real EdgeNetNode and PiKey functionality from the WASM module.
|
||||
* All operations are secure and use actual cryptographic primitives.
|
||||
*/
|
||||
|
||||
// Types from the WASM module
|
||||
export interface NodeStats {
|
||||
ruv_earned: bigint;
|
||||
ruv_spent: bigint;
|
||||
tasks_completed: bigint;
|
||||
tasks_submitted: bigint;
|
||||
uptime_seconds: bigint;
|
||||
reputation: number;
|
||||
multiplier: number;
|
||||
celebration_boost: number;
|
||||
}
|
||||
|
||||
export interface EdgeNetModule {
|
||||
default: (input?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module) => Promise<void>;
|
||||
PiKey: new (genesis_seed?: Uint8Array | null) => PiKeyInstance;
|
||||
EdgeNetNode: new (site_id: string, config?: NodeConfigInstance | null) => EdgeNetNodeInstance;
|
||||
EdgeNetConfig: new (site_id: string) => EdgeNetConfigInstance;
|
||||
BrowserFingerprint: { generate(): Promise<string> };
|
||||
AdaptiveSecurity: new () => AdaptiveSecurityInstance;
|
||||
TimeCrystal: new (frequency: number) => TimeCrystalInstance;
|
||||
}
|
||||
|
||||
export interface PiKeyInstance {
|
||||
free(): void;
|
||||
getIdentity(): Uint8Array;
|
||||
getIdentityHex(): string;
|
||||
getShortId(): string;
|
||||
getPublicKey(): Uint8Array;
|
||||
sign(data: Uint8Array): Uint8Array;
|
||||
verify(data: Uint8Array, signature: Uint8Array, public_key: Uint8Array): boolean;
|
||||
createEncryptedBackup(password: string): Uint8Array;
|
||||
exportCompact(): Uint8Array;
|
||||
getStats(): string;
|
||||
verifyPiMagic(): boolean;
|
||||
getGenesisFingerprint(): Uint8Array;
|
||||
}
|
||||
|
||||
export interface NodeConfigInstance {
|
||||
cpu_limit: number;
|
||||
memory_limit: number;
|
||||
bandwidth_limit: number;
|
||||
min_idle_time: number;
|
||||
respect_battery: boolean;
|
||||
}
|
||||
|
||||
export interface EdgeNetNodeInstance {
|
||||
free(): void;
|
||||
nodeId(): string;
|
||||
start(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
disconnect(): void;
|
||||
isIdle(): boolean;
|
||||
creditBalance(): bigint;
|
||||
ruvBalance(): bigint;
|
||||
getStats(): NodeStats;
|
||||
getThrottle(): number;
|
||||
getMultiplier(): number;
|
||||
getTreasury(): bigint;
|
||||
getProtocolFund(): bigint;
|
||||
getMerkleRoot(): string;
|
||||
getNetworkFitness(): number;
|
||||
getTimeCrystalSync(): number;
|
||||
getConflictCount(): number;
|
||||
getQuarantinedCount(): number;
|
||||
getCoherenceEventCount(): number;
|
||||
getPatternCount(): number;
|
||||
getTrajectoryCount(): number;
|
||||
getFounderCount(): number;
|
||||
isStreamHealthy(): boolean;
|
||||
shouldReplicate(): boolean;
|
||||
submitTask(task_type: string, payload: Uint8Array, max_credits: bigint): Promise<unknown>;
|
||||
processNextTask(): Promise<boolean>;
|
||||
processEpoch(): void;
|
||||
enableTimeCrystal(oscillators: number): boolean;
|
||||
enableHDC(): boolean;
|
||||
enableNAO(quorum: number): boolean;
|
||||
enableWTA(num_neurons: number): boolean;
|
||||
enableBTSP(input_dim: number): boolean;
|
||||
enableMicroLoRA(rank: number): boolean;
|
||||
enableGlobalWorkspace(capacity: number): boolean;
|
||||
enableMorphogenetic(size: number): boolean;
|
||||
storePattern(pattern_json: string): number;
|
||||
lookupPatterns(query_json: string, k: number): string;
|
||||
prunePatterns(min_usage: number, min_confidence: number): number;
|
||||
recordLearningTrajectory(trajectory_json: string): boolean;
|
||||
recordPerformance(success_rate: number, throughput: number): void;
|
||||
recordTaskRouting(task_type: string, node_id: string, latency_ms: bigint, success: boolean): void;
|
||||
recordPeerInteraction(peer_id: string, success_rate: number): void;
|
||||
getOptimalPeers(count: number): string[];
|
||||
proposeNAO(action: string): string;
|
||||
voteNAO(proposal_id: string, weight: number): boolean;
|
||||
canUseClaim(claim_id: string): boolean;
|
||||
getClaimQuarantineLevel(claim_id: string): number;
|
||||
runSecurityAudit(): string;
|
||||
checkEvents(): string;
|
||||
getThemedStatus(node_count: number): string;
|
||||
getMotivation(): string;
|
||||
getCapabilities(): unknown;
|
||||
getCapabilitiesSummary(): unknown;
|
||||
getCoherenceStats(): string;
|
||||
getEconomicHealth(): string;
|
||||
getLearningStats(): string;
|
||||
getOptimizationStats(): string;
|
||||
getRecommendedConfig(): string;
|
||||
getEnergyEfficiency(seq_len: number, hidden_dim: number): number;
|
||||
isSelfSustaining(active_nodes: number, daily_tasks: bigint): boolean;
|
||||
stepCapabilities(dt: number): void;
|
||||
}
|
||||
|
||||
export interface EdgeNetConfigInstance {
|
||||
cpuLimit(limit: number): EdgeNetConfigInstance;
|
||||
memoryLimit(bytes: number): EdgeNetConfigInstance;
|
||||
minIdleTime(ms: number): EdgeNetConfigInstance;
|
||||
respectBattery(respect: boolean): EdgeNetConfigInstance;
|
||||
addRelay(url: string): EdgeNetConfigInstance;
|
||||
build(): EdgeNetNodeInstance;
|
||||
}
|
||||
|
||||
export interface AdaptiveSecurityInstance {
|
||||
free(): void;
|
||||
chooseAction(state: string, available_actions: string): string;
|
||||
detectAttack(features: Float32Array): number;
|
||||
exportPatterns(): Uint8Array;
|
||||
importPatterns(data: Uint8Array): void;
|
||||
getSecurityLevel(): number;
|
||||
getRateLimitMax(): number;
|
||||
getMinReputation(): number;
|
||||
getSpotCheckProbability(): number;
|
||||
recordAttackPattern(pattern_type: string, features: Float32Array, severity: number): void;
|
||||
updateNetworkHealth(active_nodes: number, suspicious_nodes: number, attacks_hour: number, false_positives: number, avg_response_ms: number): void;
|
||||
learn(state: string, action: string, reward: number, next_state: string): void;
|
||||
getStats(): string;
|
||||
}
|
||||
|
||||
export interface TimeCrystalInstance {
|
||||
free(): void;
|
||||
getPhase(): number;
|
||||
getCoherence(): number;
|
||||
step(dt: number): void;
|
||||
synchronize(other_phase: number): void;
|
||||
getStats(): string;
|
||||
}
|
||||
|
||||
// Singleton service
|
||||
class EdgeNetService {
|
||||
private module: EdgeNetModule | null = null;
|
||||
private node: EdgeNetNodeInstance | null = null;
|
||||
private piKey: PiKeyInstance | null = null;
|
||||
private security: AdaptiveSecurityInstance | null = null;
|
||||
private initialized = false;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
private startTime = Date.now();
|
||||
private siteId = 'edge-net-dashboard';
|
||||
|
||||
/**
|
||||
* Initialize the WASM module
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
if (this.initPromise) return this.initPromise;
|
||||
|
||||
this.initPromise = this._doInit();
|
||||
await this.initPromise;
|
||||
}
|
||||
|
||||
private async _doInit(): Promise<void> {
|
||||
try {
|
||||
console.log('[EdgeNet] Loading WASM module...');
|
||||
|
||||
// Try loading from the local package first (for development)
|
||||
let wasmModule: EdgeNetModule;
|
||||
|
||||
// Load from CDN - the package is published to npm
|
||||
try {
|
||||
const cdnUrl = 'https://unpkg.com/@ruvector/edge-net@0.1.1/ruvector_edge_net.js';
|
||||
wasmModule = await import(/* @vite-ignore */ cdnUrl) as unknown as EdgeNetModule;
|
||||
} catch (cdnError) {
|
||||
console.warn('[EdgeNet] CDN load failed, running in fallback mode:', cdnError);
|
||||
// Module load failed - will run in fallback mode
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize the WASM
|
||||
await wasmModule.default();
|
||||
this.module = wasmModule;
|
||||
|
||||
console.log('[EdgeNet] WASM module loaded successfully');
|
||||
this.initialized = true;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to load WASM module:', error);
|
||||
// Set initialized to true but with null module - will use fallback mode
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WASM is available
|
||||
*/
|
||||
isWASMAvailable(): boolean {
|
||||
return this.module !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new PiKey identity
|
||||
*/
|
||||
async generateIdentity(seed?: Uint8Array): Promise<PiKeyInstance | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) {
|
||||
console.warn('[EdgeNet] WASM not available, using Web Crypto fallback');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
this.piKey = new this.module.PiKey(seed || null);
|
||||
console.log('[EdgeNet] Generated PiKey:', this.piKey.getShortId());
|
||||
return this.piKey;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to generate PiKey:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current PiKey
|
||||
*/
|
||||
getPiKey(): PiKeyInstance | null {
|
||||
return this.piKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and start an EdgeNet node
|
||||
*/
|
||||
async createNode(siteId?: string): Promise<EdgeNetNodeInstance | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) {
|
||||
console.warn('[EdgeNet] WASM not available');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const id = siteId || this.siteId;
|
||||
|
||||
// Use config builder for customization
|
||||
const config = new this.module.EdgeNetConfig(id)
|
||||
.addRelay('wss://edge-net-relay-875130704813.us-central1.run.app') // Genesis relay
|
||||
.cpuLimit(0.5) // 50% CPU when idle
|
||||
.memoryLimit(512 * 1024 * 1024) // 512MB
|
||||
.minIdleTime(5000) // 5 seconds idle before contributing
|
||||
.respectBattery(true);
|
||||
|
||||
this.node = config.build();
|
||||
console.log('[EdgeNet] Node created:', this.node.nodeId());
|
||||
|
||||
return this.node;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to create node:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current node
|
||||
*/
|
||||
getNode(): EdgeNetNodeInstance | null {
|
||||
return this.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the node
|
||||
*/
|
||||
startNode(): void {
|
||||
if (this.node) {
|
||||
this.node.start();
|
||||
// Enable all capabilities for maximum earning
|
||||
this.node.enableTimeCrystal(8);
|
||||
this.node.enableHDC();
|
||||
this.node.enableWTA(64);
|
||||
console.log('[EdgeNet] Node started with full capabilities');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the node
|
||||
*/
|
||||
pauseNode(): void {
|
||||
if (this.node) {
|
||||
this.node.pause();
|
||||
console.log('[EdgeNet] Node paused');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the node
|
||||
*/
|
||||
resumeNode(): void {
|
||||
if (this.node) {
|
||||
this.node.resume();
|
||||
console.log('[EdgeNet] Node resumed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an epoch - advances time and accumulates rewards
|
||||
*/
|
||||
processEpoch(): void {
|
||||
if (this.node) {
|
||||
this.node.processEpoch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Step capabilities forward (for real-time updates)
|
||||
*/
|
||||
stepCapabilities(dt: number): void {
|
||||
if (this.node) {
|
||||
this.node.stepCapabilities(dt);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record performance for learning
|
||||
*/
|
||||
recordPerformance(successRate: number, throughput: number): void {
|
||||
if (this.node) {
|
||||
this.node.recordPerformance(successRate, throughput);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real node statistics
|
||||
*/
|
||||
getStats(): NodeStats | null {
|
||||
if (!this.node) return null;
|
||||
|
||||
try {
|
||||
return this.node.getStats();
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to get stats:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get credit balance
|
||||
*/
|
||||
getCreditBalance(): bigint {
|
||||
if (!this.node) return BigInt(0);
|
||||
return this.node.creditBalance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Time Crystal synchronization level
|
||||
*/
|
||||
getTimeCrystalSync(): number {
|
||||
if (!this.node) return 0;
|
||||
return this.node.getTimeCrystalSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Time Crystal
|
||||
*/
|
||||
enableTimeCrystal(oscillators = 8): boolean {
|
||||
if (!this.node) return false;
|
||||
return this.node.enableTimeCrystal(oscillators);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network fitness score
|
||||
*/
|
||||
getNetworkFitness(): number {
|
||||
if (!this.node) return 0;
|
||||
return this.node.getNetworkFitness();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize adaptive security
|
||||
*/
|
||||
async initSecurity(): Promise<AdaptiveSecurityInstance | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) return null;
|
||||
|
||||
try {
|
||||
this.security = new this.module.AdaptiveSecurity();
|
||||
console.log('[EdgeNet] Adaptive security initialized');
|
||||
return this.security;
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to init security:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get security level
|
||||
*/
|
||||
getSecurityLevel(): number {
|
||||
if (!this.security) return 0;
|
||||
return this.security.getSecurityLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run security audit
|
||||
*/
|
||||
runSecurityAudit(): string | null {
|
||||
if (!this.node) return null;
|
||||
return this.node.runSecurityAudit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browser fingerprint for unique node identification
|
||||
*/
|
||||
async getBrowserFingerprint(): Promise<string | null> {
|
||||
await this.init();
|
||||
|
||||
if (!this.module) return null;
|
||||
|
||||
try {
|
||||
return await this.module.BrowserFingerprint.generate();
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to generate fingerprint:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get economic health metrics
|
||||
*/
|
||||
getEconomicHealth(): string | null {
|
||||
if (!this.node) return null;
|
||||
return this.node.getEconomicHealth();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get learning statistics
|
||||
*/
|
||||
getLearningStats(): string | null {
|
||||
if (!this.node) return null;
|
||||
return this.node.getLearningStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a learning pattern
|
||||
*/
|
||||
storePattern(pattern: object): number {
|
||||
if (!this.node) return -1;
|
||||
return this.node.storePattern(JSON.stringify(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup similar patterns
|
||||
*/
|
||||
lookupPatterns(query: object, k = 5): unknown[] {
|
||||
if (!this.node) return [];
|
||||
try {
|
||||
const result = this.node.lookupPatterns(JSON.stringify(query), k);
|
||||
return JSON.parse(result);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a task to the network
|
||||
*/
|
||||
async submitTask(taskType: string, payload: Uint8Array, maxCredits: bigint): Promise<unknown> {
|
||||
if (!this.node) throw new Error('Node not initialized');
|
||||
return this.node.submitTask(taskType, payload, maxCredits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a demo compute task (for earning credits in demo mode)
|
||||
*/
|
||||
async submitDemoTask(): Promise<void> {
|
||||
if (!this.node) return;
|
||||
try {
|
||||
// Submit a small compute task
|
||||
const payload = new TextEncoder().encode(JSON.stringify({
|
||||
type: 'compute',
|
||||
data: Math.random().toString(36),
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
await this.node.submitTask('compute', payload, BigInt(1000000)); // 0.001 rUv max
|
||||
} catch {
|
||||
// Task submission can fail if queue is full - that's ok
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the next available task
|
||||
*/
|
||||
async processNextTask(): Promise<boolean> {
|
||||
if (!this.node) return false;
|
||||
return this.node.processNextTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get capabilities summary
|
||||
*/
|
||||
getCapabilities(): unknown {
|
||||
if (!this.node) return null;
|
||||
return this.node.getCapabilitiesSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uptime in seconds
|
||||
*/
|
||||
getUptime(): number {
|
||||
return (Date.now() - this.startTime) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup resources
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.node) {
|
||||
this.node.disconnect();
|
||||
this.node.free();
|
||||
this.node = null;
|
||||
}
|
||||
if (this.piKey) {
|
||||
this.piKey.free();
|
||||
this.piKey = null;
|
||||
}
|
||||
if (this.security) {
|
||||
this.security.free();
|
||||
this.security = null;
|
||||
}
|
||||
console.log('[EdgeNet] Service destroyed');
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const edgeNetService = new EdgeNetService();
|
||||
|
||||
// Export types for external use
|
||||
export type { EdgeNetService };
|
||||
394
vendor/ruvector/examples/edge-net/dashboard/src/services/relayClient.ts
vendored
Normal file
394
vendor/ruvector/examples/edge-net/dashboard/src/services/relayClient.ts
vendored
Normal file
@@ -0,0 +1,394 @@
|
||||
/**
|
||||
* Edge-Net Relay WebSocket Client
|
||||
*
|
||||
* Provides real-time connection to the Edge-Net relay server for:
|
||||
* - Node registration and presence
|
||||
* - Task distribution and completion
|
||||
* - Credit synchronization
|
||||
* - Time Crystal phase sync
|
||||
*/
|
||||
|
||||
export interface RelayMessage {
|
||||
type: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface NetworkState {
|
||||
genesisTime: number;
|
||||
totalNodes: number;
|
||||
activeNodes: number;
|
||||
totalTasks: number;
|
||||
totalRuvDistributed: bigint;
|
||||
timeCrystalPhase: number;
|
||||
}
|
||||
|
||||
export interface TaskAssignment {
|
||||
id: string;
|
||||
submitter: string;
|
||||
taskType: string;
|
||||
payload: Uint8Array;
|
||||
maxCredits: bigint;
|
||||
submittedAt: number;
|
||||
}
|
||||
|
||||
export interface RelayEventHandlers {
|
||||
onConnected?: (nodeId: string, networkState: NetworkState, peers: string[]) => void;
|
||||
onDisconnected?: () => void;
|
||||
onNodeJoined?: (nodeId: string, totalNodes: number) => void;
|
||||
onNodeLeft?: (nodeId: string, totalNodes: number) => void;
|
||||
onTaskAssigned?: (task: TaskAssignment) => void;
|
||||
onTaskResult?: (taskId: string, result: unknown, processedBy: string) => void;
|
||||
onCreditEarned?: (amount: bigint, taskId: string) => void;
|
||||
onTimeCrystalSync?: (phase: number, timestamp: number, activeNodes: number) => void;
|
||||
onPeerMessage?: (from: string, payload: unknown) => void;
|
||||
onError?: (error: Error) => void;
|
||||
}
|
||||
|
||||
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000]; // Exponential backoff
|
||||
|
||||
class RelayClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private nodeId: string | null = null;
|
||||
private relayUrl: string;
|
||||
private handlers: RelayEventHandlers = {};
|
||||
private reconnectAttempt = 0;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private isConnecting = false;
|
||||
private shouldReconnect = true;
|
||||
|
||||
constructor(relayUrl: string = 'wss://edge-net-relay-875130704813.us-central1.run.app') {
|
||||
this.relayUrl = relayUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set event handlers
|
||||
*/
|
||||
setHandlers(handlers: RelayEventHandlers): void {
|
||||
this.handlers = { ...this.handlers, ...handlers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the relay server
|
||||
*/
|
||||
async connect(nodeId: string): Promise<boolean> {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
console.log('[RelayClient] Already connected');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.isConnecting) {
|
||||
console.log('[RelayClient] Connection already in progress');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.nodeId = nodeId;
|
||||
this.shouldReconnect = true;
|
||||
this.isConnecting = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
console.log(`[RelayClient] Connecting to ${this.relayUrl}...`);
|
||||
this.ws = new WebSocket(this.relayUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('[RelayClient] WebSocket connected');
|
||||
this.isConnecting = false;
|
||||
this.reconnectAttempt = 0;
|
||||
|
||||
// Register with relay
|
||||
this.send({
|
||||
type: 'register',
|
||||
nodeId: this.nodeId,
|
||||
capabilities: ['compute', 'storage'],
|
||||
version: '0.1.0',
|
||||
});
|
||||
|
||||
// Start heartbeat
|
||||
this.startHeartbeat();
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data);
|
||||
};
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log(`[RelayClient] WebSocket closed: ${event.code} ${event.reason}`);
|
||||
this.isConnecting = false;
|
||||
this.stopHeartbeat();
|
||||
this.handlers.onDisconnected?.();
|
||||
|
||||
if (this.shouldReconnect) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('[RelayClient] WebSocket error:', error);
|
||||
this.isConnecting = false;
|
||||
this.handlers.onError?.(new Error('WebSocket connection failed'));
|
||||
resolve(false);
|
||||
};
|
||||
|
||||
// Wait for welcome message to confirm connection
|
||||
const checkConnected = setInterval(() => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
clearInterval(checkConnected);
|
||||
resolve(true);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Timeout after 10 seconds
|
||||
setTimeout(() => {
|
||||
clearInterval(checkConnected);
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
this.isConnecting = false;
|
||||
resolve(false);
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[RelayClient] Failed to create WebSocket:', error);
|
||||
this.isConnecting = false;
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the relay
|
||||
*/
|
||||
disconnect(): void {
|
||||
this.shouldReconnect = false;
|
||||
this.stopHeartbeat();
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close(1000, 'Client disconnect');
|
||||
this.ws = null;
|
||||
}
|
||||
|
||||
console.log('[RelayClient] Disconnected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current node ID
|
||||
*/
|
||||
getNodeId(): string | null {
|
||||
return this.nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a task to the network
|
||||
*/
|
||||
submitTask(taskType: string, payload: Uint8Array, maxCredits: bigint): void {
|
||||
this.send({
|
||||
type: 'task_submit',
|
||||
task: {
|
||||
taskType,
|
||||
payload: Array.from(payload), // Convert to array for JSON
|
||||
maxCredits: maxCredits.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report task completion
|
||||
*/
|
||||
completeTask(taskId: string, submitterId: string, result: unknown, reward: bigint): void {
|
||||
this.send({
|
||||
type: 'task_complete',
|
||||
taskId,
|
||||
submitterId,
|
||||
result,
|
||||
reward: reward.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a specific peer
|
||||
*/
|
||||
sendToPeer(targetId: string, payload: unknown): void {
|
||||
this.send({
|
||||
type: 'peer_message',
|
||||
targetId,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast a message to all peers
|
||||
*/
|
||||
broadcast(payload: unknown): void {
|
||||
this.send({
|
||||
type: 'broadcast',
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Private Methods
|
||||
// ============================================================================
|
||||
|
||||
private send(message: RelayMessage): void {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn('[RelayClient] Cannot send - not connected');
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(data: string): void {
|
||||
try {
|
||||
const message = JSON.parse(data) as RelayMessage;
|
||||
|
||||
switch (message.type) {
|
||||
case 'welcome':
|
||||
console.log('[RelayClient] Registered with relay:', message.nodeId);
|
||||
this.handlers.onConnected?.(
|
||||
message.nodeId as string,
|
||||
{
|
||||
genesisTime: (message.networkState as NetworkState)?.genesisTime || Date.now(),
|
||||
totalNodes: (message.networkState as NetworkState)?.totalNodes || 0,
|
||||
activeNodes: (message.networkState as NetworkState)?.activeNodes || 0,
|
||||
totalTasks: (message.networkState as NetworkState)?.totalTasks || 0,
|
||||
totalRuvDistributed: BigInt((message.networkState as NetworkState)?.totalRuvDistributed?.toString() || '0'),
|
||||
timeCrystalPhase: (message.networkState as NetworkState)?.timeCrystalPhase || 0,
|
||||
},
|
||||
(message.peers as string[]) || []
|
||||
);
|
||||
break;
|
||||
|
||||
case 'node_joined':
|
||||
console.log('[RelayClient] Node joined:', message.nodeId);
|
||||
this.handlers.onNodeJoined?.(
|
||||
message.nodeId as string,
|
||||
message.totalNodes as number
|
||||
);
|
||||
break;
|
||||
|
||||
case 'node_left':
|
||||
console.log('[RelayClient] Node left:', message.nodeId);
|
||||
this.handlers.onNodeLeft?.(
|
||||
message.nodeId as string,
|
||||
message.totalNodes as number
|
||||
);
|
||||
break;
|
||||
|
||||
case 'task_assignment':
|
||||
console.log('[RelayClient] Task assigned:', (message.task as TaskAssignment)?.id);
|
||||
const task = message.task as Record<string, unknown>;
|
||||
this.handlers.onTaskAssigned?.({
|
||||
id: task.id as string,
|
||||
submitter: task.submitter as string,
|
||||
taskType: task.taskType as string,
|
||||
payload: new Uint8Array(task.payload as number[]),
|
||||
maxCredits: BigInt(task.maxCredits as string || '0'),
|
||||
submittedAt: task.submittedAt as number,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'task_accepted':
|
||||
console.log('[RelayClient] Task accepted:', message.taskId);
|
||||
break;
|
||||
|
||||
case 'task_result':
|
||||
console.log('[RelayClient] Task result:', message.taskId);
|
||||
this.handlers.onTaskResult?.(
|
||||
message.taskId as string,
|
||||
message.result,
|
||||
message.processedBy as string
|
||||
);
|
||||
break;
|
||||
|
||||
case 'credit_earned':
|
||||
console.log('[RelayClient] Credit earned:', message.amount);
|
||||
this.handlers.onCreditEarned?.(
|
||||
BigInt(message.amount as string || '0'),
|
||||
message.taskId as string
|
||||
);
|
||||
break;
|
||||
|
||||
case 'time_crystal_sync':
|
||||
this.handlers.onTimeCrystalSync?.(
|
||||
message.phase as number,
|
||||
message.timestamp as number,
|
||||
message.activeNodes as number
|
||||
);
|
||||
break;
|
||||
|
||||
case 'peer_message':
|
||||
this.handlers.onPeerMessage?.(
|
||||
message.from as string,
|
||||
message.payload
|
||||
);
|
||||
break;
|
||||
|
||||
case 'heartbeat_ack':
|
||||
// Heartbeat acknowledged
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('[RelayClient] Relay error:', message.message);
|
||||
this.handlers.onError?.(new Error(message.message as string));
|
||||
break;
|
||||
|
||||
case 'relay_shutdown':
|
||||
console.warn('[RelayClient] Relay is shutting down');
|
||||
this.shouldReconnect = true; // Will reconnect when relay comes back
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('[RelayClient] Unknown message type:', message.type);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[RelayClient] Failed to parse message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private startHeartbeat(): void {
|
||||
this.stopHeartbeat();
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
this.send({ type: 'heartbeat' });
|
||||
}, 15000); // Every 15 seconds
|
||||
}
|
||||
|
||||
private stopHeartbeat(): void {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectTimer) return;
|
||||
|
||||
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)];
|
||||
console.log(`[RelayClient] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})`);
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null;
|
||||
this.reconnectAttempt++;
|
||||
if (this.nodeId) {
|
||||
this.connect(this.nodeId);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const relayClient = new RelayClient();
|
||||
|
||||
// Export class for testing
|
||||
export { RelayClient };
|
||||
152
vendor/ruvector/examples/edge-net/dashboard/src/services/storage.ts
vendored
Normal file
152
vendor/ruvector/examples/edge-net/dashboard/src/services/storage.ts
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* IndexedDB Storage Service
|
||||
* Persistent storage for Edge-Net node state
|
||||
*/
|
||||
|
||||
const DB_NAME = 'edge-net-db';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_NAME = 'node-state';
|
||||
|
||||
interface NodeState {
|
||||
id: string;
|
||||
nodeId: string | null;
|
||||
creditsEarned: number;
|
||||
creditsSpent: number;
|
||||
tasksCompleted: number;
|
||||
tasksSubmitted: number;
|
||||
totalUptime: number;
|
||||
lastActiveTimestamp: number;
|
||||
consentGiven: boolean;
|
||||
consentTimestamp: number | null;
|
||||
cpuLimit: number;
|
||||
gpuEnabled: boolean;
|
||||
gpuLimit: number;
|
||||
respectBattery: boolean;
|
||||
onlyWhenIdle: boolean;
|
||||
}
|
||||
|
||||
class StorageService {
|
||||
private db: IDBDatabase | null = null;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.db) return;
|
||||
if (this.initPromise) return this.initPromise;
|
||||
|
||||
this.initPromise = new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('[Storage] Failed to open IndexedDB:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
console.log('[Storage] IndexedDB opened successfully');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||
console.log('[Storage] Created object store:', STORE_NAME);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
async saveState(state: NodeState): Promise<void> {
|
||||
await this.init();
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.put(state);
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[Storage] State saved:', state.creditsEarned, 'rUv');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('[Storage] Failed to save state:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async loadState(): Promise<NodeState | null> {
|
||||
await this.init();
|
||||
if (!this.db) throw new Error('Database not initialized');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.get('primary');
|
||||
|
||||
request.onsuccess = () => {
|
||||
const state = request.result as NodeState | undefined;
|
||||
if (state) {
|
||||
console.log('[Storage] Loaded state:', state.creditsEarned, 'rUv earned');
|
||||
} else {
|
||||
console.log('[Storage] No saved state found');
|
||||
}
|
||||
resolve(state || null);
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('[Storage] Failed to load state:', request.error);
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getDefaultState(): Promise<NodeState> {
|
||||
return {
|
||||
id: 'primary',
|
||||
nodeId: null,
|
||||
creditsEarned: 0,
|
||||
creditsSpent: 0,
|
||||
tasksCompleted: 0,
|
||||
tasksSubmitted: 0,
|
||||
totalUptime: 0,
|
||||
lastActiveTimestamp: Date.now(),
|
||||
consentGiven: false,
|
||||
consentTimestamp: null,
|
||||
cpuLimit: 50,
|
||||
gpuEnabled: false,
|
||||
gpuLimit: 30,
|
||||
respectBattery: true,
|
||||
onlyWhenIdle: true,
|
||||
};
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
await this.init();
|
||||
if (!this.db) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.clear();
|
||||
|
||||
request.onsuccess = () => {
|
||||
console.log('[Storage] State cleared');
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
reject(request.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const storageService = new StorageService();
|
||||
export type { NodeState };
|
||||
209
vendor/ruvector/examples/edge-net/dashboard/src/stores/cdnStore.ts
vendored
Normal file
209
vendor/ruvector/examples/edge-net/dashboard/src/stores/cdnStore.ts
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
import { create } from 'zustand';
|
||||
import type { CDNScript, CDNConfig } from '../types';
|
||||
|
||||
interface CDNState extends CDNConfig {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
setScripts: (scripts: CDNScript[]) => void;
|
||||
toggleScript: (scriptId: string) => void;
|
||||
loadScript: (scriptId: string) => Promise<void>;
|
||||
unloadScript: (scriptId: string) => void;
|
||||
setAutoLoad: (autoLoad: boolean) => void;
|
||||
setCacheEnabled: (cacheEnabled: boolean) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
}
|
||||
|
||||
const defaultScripts: CDNScript[] = [
|
||||
// WASM Modules
|
||||
{
|
||||
id: 'edge-net-wasm',
|
||||
name: '@ruvector/edge-net',
|
||||
description: 'Core Edge-Net WASM module with Time Crystal and P2P capabilities',
|
||||
url: 'https://unpkg.com/@ruvector/edge-net@0.1.1/ruvector_edge_net_bg.wasm',
|
||||
size: '3.2 MB',
|
||||
category: 'wasm',
|
||||
enabled: true,
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
id: 'attention-wasm',
|
||||
name: '@ruvector/attention-unified-wasm',
|
||||
description: 'DAG Attention mechanisms for task orchestration',
|
||||
url: 'https://unpkg.com/@ruvector/attention-unified-wasm@0.1.0/attention_unified_bg.wasm',
|
||||
size: '850 KB',
|
||||
category: 'wasm',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
id: 'economy-wasm',
|
||||
name: '@ruvector/economy-wasm',
|
||||
description: 'Credit economy and marketplace functionality',
|
||||
url: 'https://unpkg.com/@ruvector/economy-wasm@0.1.0/economy_bg.wasm',
|
||||
size: '620 KB',
|
||||
category: 'wasm',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
// AI Libraries
|
||||
{
|
||||
id: 'tensorflow',
|
||||
name: 'TensorFlow.js',
|
||||
description: 'Machine learning library for browser-based AI',
|
||||
url: 'https://cdnjs.cloudflare.com/ajax/libs/tensorflow/4.15.0/tf.min.js',
|
||||
size: '1.8 MB',
|
||||
category: 'ai',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
id: 'onnx-runtime',
|
||||
name: 'ONNX Runtime Web',
|
||||
description: 'Run ONNX models in the browser with WebAssembly',
|
||||
url: 'https://cdnjs.cloudflare.com/ajax/libs/onnxruntime-web/1.17.0/ort.min.js',
|
||||
size: '2.1 MB',
|
||||
category: 'ai',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
// Crypto Libraries
|
||||
{
|
||||
id: 'noble-curves',
|
||||
name: 'Noble Curves',
|
||||
description: 'Elliptic curve cryptography (Ed25519, secp256k1)',
|
||||
url: 'https://unpkg.com/@noble/curves@1.3.0/index.js',
|
||||
size: '45 KB',
|
||||
category: 'crypto',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
id: 'tweetnacl',
|
||||
name: 'TweetNaCl.js',
|
||||
description: 'Port of TweetNaCl cryptographic library',
|
||||
url: 'https://cdnjs.cloudflare.com/ajax/libs/tweetnacl/1.0.3/nacl-fast.min.js',
|
||||
size: '32 KB',
|
||||
category: 'crypto',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
// Network Libraries
|
||||
{
|
||||
id: 'libp2p',
|
||||
name: 'libp2p',
|
||||
description: 'Modular peer-to-peer networking stack',
|
||||
url: 'https://unpkg.com/libp2p@1.2.0/dist/index.min.js',
|
||||
size: '680 KB',
|
||||
category: 'network',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
id: 'simple-peer',
|
||||
name: 'Simple Peer',
|
||||
description: 'Simple WebRTC video, voice, and data channels',
|
||||
url: 'https://cdnjs.cloudflare.com/ajax/libs/simple-peer/9.11.1/simplepeer.min.js',
|
||||
size: '95 KB',
|
||||
category: 'network',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
// Utility Libraries
|
||||
{
|
||||
id: 'comlink',
|
||||
name: 'Comlink',
|
||||
description: 'Make Web Workers enjoyable with RPC-style API',
|
||||
url: 'https://unpkg.com/comlink@4.4.1/dist/umd/comlink.js',
|
||||
size: '4 KB',
|
||||
category: 'utility',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
{
|
||||
id: 'idb-keyval',
|
||||
name: 'idb-keyval',
|
||||
description: 'Super simple IndexedDB key-value store',
|
||||
url: 'https://unpkg.com/idb-keyval@6.2.1/dist/umd.js',
|
||||
size: '3 KB',
|
||||
category: 'utility',
|
||||
enabled: false,
|
||||
loaded: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const useCDNStore = create<CDNState>((set, get) => ({
|
||||
scripts: defaultScripts,
|
||||
autoLoad: false,
|
||||
cacheEnabled: true,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
setScripts: (scripts) => set({ scripts }),
|
||||
|
||||
toggleScript: (scriptId) =>
|
||||
set((state) => ({
|
||||
scripts: state.scripts.map((s) =>
|
||||
s.id === scriptId ? { ...s, enabled: !s.enabled } : s
|
||||
),
|
||||
})),
|
||||
|
||||
loadScript: async (scriptId) => {
|
||||
const { scripts } = get();
|
||||
const script = scripts.find((s) => s.id === scriptId);
|
||||
|
||||
if (!script || script.loaded) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
try {
|
||||
// Create script element
|
||||
const scriptEl = document.createElement('script');
|
||||
scriptEl.src = script.url;
|
||||
scriptEl.async = true;
|
||||
scriptEl.id = `cdn-${scriptId}`;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
scriptEl.onload = () => resolve();
|
||||
scriptEl.onerror = () => reject(new Error(`Failed to load ${script.name}`));
|
||||
document.head.appendChild(scriptEl);
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
scripts: state.scripts.map((s) =>
|
||||
s.id === scriptId ? { ...s, loaded: true } : s
|
||||
),
|
||||
isLoading: false,
|
||||
}));
|
||||
|
||||
console.log(`[CDN] Loaded: ${script.name}`);
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to load script',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
unloadScript: (scriptId) => {
|
||||
const scriptEl = document.getElementById(`cdn-${scriptId}`);
|
||||
if (scriptEl) {
|
||||
scriptEl.remove();
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
scripts: state.scripts.map((s) =>
|
||||
s.id === scriptId ? { ...s, loaded: false } : s
|
||||
),
|
||||
}));
|
||||
|
||||
console.log(`[CDN] Unloaded: ${scriptId}`);
|
||||
},
|
||||
|
||||
setAutoLoad: (autoLoad) => set({ autoLoad }),
|
||||
setCacheEnabled: (cacheEnabled) => set({ cacheEnabled }),
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
setError: (error) => set({ error }),
|
||||
}));
|
||||
442
vendor/ruvector/examples/edge-net/dashboard/src/stores/identityStore.ts
vendored
Normal file
442
vendor/ruvector/examples/edge-net/dashboard/src/stores/identityStore.ts
vendored
Normal file
@@ -0,0 +1,442 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { edgeNetService, type PiKeyInstance } from '../services/edgeNet';
|
||||
|
||||
export interface PeerIdentity {
|
||||
id: string;
|
||||
publicKey: string;
|
||||
publicKeyBytes?: Uint8Array;
|
||||
displayName: string;
|
||||
avatar?: string;
|
||||
createdAt: Date;
|
||||
shortId: string;
|
||||
identityHex: string;
|
||||
hasPiMagic: boolean;
|
||||
}
|
||||
|
||||
export interface NetworkRegistration {
|
||||
networkId: string;
|
||||
networkName: string;
|
||||
status: 'pending' | 'active' | 'suspended' | 'expired';
|
||||
joinedAt: Date;
|
||||
capabilities: string[];
|
||||
reputation: number;
|
||||
creditsEarned: number;
|
||||
}
|
||||
|
||||
export interface IdentityState {
|
||||
identity: PeerIdentity | null;
|
||||
registrations: NetworkRegistration[];
|
||||
isGenerating: boolean;
|
||||
isRegistering: boolean;
|
||||
error: string | null;
|
||||
piKeyBackup: string | null; // Encrypted backup (hex encoded)
|
||||
hasRealPiKey: boolean;
|
||||
|
||||
// Actions
|
||||
generateIdentity: (displayName: string) => Promise<void>;
|
||||
importIdentity: (privateKeyOrBackup: string, password?: string) => Promise<void>;
|
||||
exportIdentity: (password: string) => Promise<string | null>;
|
||||
clearIdentity: () => void;
|
||||
registerNetwork: (networkId: string, capabilities: string[]) => Promise<void>;
|
||||
leaveNetwork: (networkId: string) => void;
|
||||
updateCapabilities: (networkId: string, capabilities: string[]) => void;
|
||||
signData: (data: Uint8Array) => Uint8Array | null;
|
||||
verifySignature: (data: Uint8Array, signature: Uint8Array, publicKey: Uint8Array) => boolean;
|
||||
}
|
||||
|
||||
// Helper: Convert bytes to hex
|
||||
function bytesToHex(bytes: Uint8Array): string {
|
||||
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
// Helper: Convert hex to bytes
|
||||
function hexToBytes(hex: string): Uint8Array {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// Real Web Crypto API fallback for Ed25519 (when WASM unavailable)
|
||||
async function generateWebCryptoKeys(): Promise<{
|
||||
publicKey: Uint8Array;
|
||||
privateKey: Uint8Array;
|
||||
sign: (data: Uint8Array) => Promise<Uint8Array>;
|
||||
verify: (data: Uint8Array, sig: Uint8Array, pk: Uint8Array) => Promise<boolean>;
|
||||
}> {
|
||||
// Use Web Crypto API for real Ed25519 keys
|
||||
// Note: Ed25519 support varies by browser, fall back to ECDSA P-256 if needed
|
||||
let keyPair: CryptoKeyPair;
|
||||
|
||||
try {
|
||||
// Try Ed25519 first (supported in newer browsers)
|
||||
keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'Ed25519' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
);
|
||||
} catch {
|
||||
// Fall back to ECDSA P-256
|
||||
console.log('[Identity] Ed25519 not supported, using ECDSA P-256');
|
||||
keyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'ECDSA', namedCurve: 'P-256' },
|
||||
true,
|
||||
['sign', 'verify']
|
||||
);
|
||||
}
|
||||
|
||||
// Export keys
|
||||
const publicKeyBuffer = await crypto.subtle.exportKey('raw', keyPair.publicKey);
|
||||
const privateKeyBuffer = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
|
||||
|
||||
const publicKey = new Uint8Array(publicKeyBuffer);
|
||||
const privateKey = new Uint8Array(privateKeyBuffer);
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
privateKey,
|
||||
sign: async (data: Uint8Array) => {
|
||||
const algorithm = keyPair.privateKey.algorithm.name === 'Ed25519'
|
||||
? { name: 'Ed25519' }
|
||||
: { name: 'ECDSA', hash: 'SHA-256' };
|
||||
const signature = await crypto.subtle.sign(algorithm, keyPair.privateKey, data.buffer as ArrayBuffer);
|
||||
return new Uint8Array(signature);
|
||||
},
|
||||
verify: async (data: Uint8Array, sig: Uint8Array, _pk: Uint8Array) => {
|
||||
const algorithm = keyPair.publicKey.algorithm.name === 'Ed25519'
|
||||
? { name: 'Ed25519' }
|
||||
: { name: 'ECDSA', hash: 'SHA-256' };
|
||||
return crypto.subtle.verify(algorithm, keyPair.publicKey, sig.buffer as ArrayBuffer, data.buffer as ArrayBuffer);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Generate unique peer ID from public key
|
||||
function generatePeerId(publicKey: Uint8Array): string {
|
||||
// Use first 44 chars of base64 encoded public key for libp2p-style ID
|
||||
const base64 = btoa(String.fromCharCode(...publicKey));
|
||||
return `12D3KooW${base64.replace(/[+/=]/g, '').substring(0, 44)}`;
|
||||
}
|
||||
|
||||
const availableNetworks = [
|
||||
{
|
||||
id: 'mainnet',
|
||||
name: 'Edge-Net Mainnet',
|
||||
description: 'Primary production network',
|
||||
requiredCapabilities: ['compute'],
|
||||
},
|
||||
{
|
||||
id: 'testnet',
|
||||
name: 'Edge-Net Testnet',
|
||||
description: 'Testing and development network',
|
||||
requiredCapabilities: [],
|
||||
},
|
||||
{
|
||||
id: 'research',
|
||||
name: 'Research Network',
|
||||
description: 'Academic and research collaboration',
|
||||
requiredCapabilities: ['compute', 'storage'],
|
||||
},
|
||||
];
|
||||
|
||||
export { availableNetworks };
|
||||
|
||||
// Store the current Web Crypto instance for signing (used as fallback when WASM unavailable)
|
||||
// Assigned in generateIdentity, cleared in clearIdentity, accessed in signData/verifySignature
|
||||
interface WebCryptoState {
|
||||
sign: (data: Uint8Array) => Promise<Uint8Array>;
|
||||
verify: (data: Uint8Array, sig: Uint8Array, pk: Uint8Array) => Promise<boolean>;
|
||||
publicKey: Uint8Array;
|
||||
privateKey: Uint8Array;
|
||||
}
|
||||
let webCryptoInstance: WebCryptoState | null = null;
|
||||
|
||||
// Export for external async signing when WASM unavailable
|
||||
export function getWebCryptoInstance(): WebCryptoState | null {
|
||||
return webCryptoInstance;
|
||||
}
|
||||
|
||||
let currentPiKey: PiKeyInstance | null = null;
|
||||
|
||||
export const useIdentityStore = create<IdentityState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
identity: null,
|
||||
registrations: [],
|
||||
isGenerating: false,
|
||||
isRegistering: false,
|
||||
error: null,
|
||||
piKeyBackup: null,
|
||||
hasRealPiKey: false,
|
||||
|
||||
generateIdentity: async (displayName: string) => {
|
||||
set({ isGenerating: true, error: null });
|
||||
|
||||
try {
|
||||
// Try real PiKey from WASM first
|
||||
const piKey = await edgeNetService.generateIdentity();
|
||||
|
||||
if (piKey) {
|
||||
currentPiKey = piKey;
|
||||
|
||||
const identity: PeerIdentity = {
|
||||
id: piKey.getShortId(),
|
||||
publicKey: bytesToHex(piKey.getPublicKey()),
|
||||
publicKeyBytes: piKey.getPublicKey(),
|
||||
displayName,
|
||||
createdAt: new Date(),
|
||||
shortId: piKey.getShortId(),
|
||||
identityHex: piKey.getIdentityHex(),
|
||||
hasPiMagic: piKey.verifyPiMagic(),
|
||||
};
|
||||
|
||||
set({
|
||||
identity,
|
||||
hasRealPiKey: true,
|
||||
isGenerating: false,
|
||||
});
|
||||
|
||||
console.log('[Identity] Generated real PiKey:', identity.shortId);
|
||||
console.log('[Identity] Has Pi magic:', identity.hasPiMagic);
|
||||
console.log('[Identity] Stats:', piKey.getStats());
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to Web Crypto API
|
||||
console.log('[Identity] Using Web Crypto API fallback');
|
||||
const cryptoKeys = await generateWebCryptoKeys();
|
||||
webCryptoInstance = cryptoKeys;
|
||||
|
||||
const peerId = generatePeerId(cryptoKeys.publicKey);
|
||||
|
||||
const identity: PeerIdentity = {
|
||||
id: peerId,
|
||||
publicKey: bytesToHex(cryptoKeys.publicKey),
|
||||
publicKeyBytes: cryptoKeys.publicKey,
|
||||
displayName,
|
||||
createdAt: new Date(),
|
||||
shortId: peerId.substring(0, 16),
|
||||
identityHex: bytesToHex(cryptoKeys.publicKey),
|
||||
hasPiMagic: false,
|
||||
};
|
||||
|
||||
set({
|
||||
identity,
|
||||
hasRealPiKey: false,
|
||||
isGenerating: false,
|
||||
});
|
||||
|
||||
console.log('[Identity] Generated Web Crypto identity:', identity.shortId);
|
||||
} catch (error) {
|
||||
console.error('[Identity] Generation failed:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to generate identity',
|
||||
isGenerating: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
importIdentity: async (privateKeyOrBackup: string, password?: string) => {
|
||||
set({ isGenerating: true, error: null });
|
||||
|
||||
try {
|
||||
// If password provided, treat as encrypted backup
|
||||
if (password) {
|
||||
// TODO: Implement PiKey.restoreFromBackup when available
|
||||
throw new Error('Encrypted backup import not yet implemented');
|
||||
}
|
||||
|
||||
// Otherwise, validate hex private key
|
||||
if (privateKeyOrBackup.length < 32) {
|
||||
throw new Error('Invalid private key format');
|
||||
}
|
||||
|
||||
// Generate new identity from seed
|
||||
const seed = hexToBytes(privateKeyOrBackup.substring(0, 64));
|
||||
const piKey = await edgeNetService.generateIdentity(seed);
|
||||
|
||||
if (piKey) {
|
||||
currentPiKey = piKey;
|
||||
|
||||
const identity: PeerIdentity = {
|
||||
id: piKey.getShortId(),
|
||||
publicKey: bytesToHex(piKey.getPublicKey()),
|
||||
publicKeyBytes: piKey.getPublicKey(),
|
||||
displayName: 'Imported Identity',
|
||||
createdAt: new Date(),
|
||||
shortId: piKey.getShortId(),
|
||||
identityHex: piKey.getIdentityHex(),
|
||||
hasPiMagic: piKey.verifyPiMagic(),
|
||||
};
|
||||
|
||||
set({
|
||||
identity,
|
||||
hasRealPiKey: true,
|
||||
isGenerating: false,
|
||||
});
|
||||
|
||||
console.log('[Identity] Imported PiKey:', identity.shortId);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Failed to import identity');
|
||||
} catch (error) {
|
||||
console.error('[Identity] Import failed:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to import identity',
|
||||
isGenerating: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
exportIdentity: async (password: string) => {
|
||||
const { hasRealPiKey } = get();
|
||||
|
||||
if (hasRealPiKey && currentPiKey) {
|
||||
try {
|
||||
// Create encrypted backup with Argon2id
|
||||
const backup = currentPiKey.createEncryptedBackup(password);
|
||||
const backupHex = bytesToHex(backup);
|
||||
|
||||
set({ piKeyBackup: backupHex });
|
||||
|
||||
console.log('[Identity] Created encrypted backup');
|
||||
return backupHex;
|
||||
} catch (error) {
|
||||
console.error('[Identity] Export failed:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// For Web Crypto keys, export as JSON (less secure)
|
||||
const { identity } = get();
|
||||
if (!identity) return null;
|
||||
|
||||
return JSON.stringify({
|
||||
publicKey: identity.publicKey,
|
||||
displayName: identity.displayName,
|
||||
note: 'Web Crypto fallback - private key not exportable',
|
||||
});
|
||||
},
|
||||
|
||||
clearIdentity: () => {
|
||||
if (currentPiKey) {
|
||||
currentPiKey.free();
|
||||
currentPiKey = null;
|
||||
}
|
||||
webCryptoInstance = null;
|
||||
|
||||
set({
|
||||
identity: null,
|
||||
registrations: [],
|
||||
piKeyBackup: null,
|
||||
hasRealPiKey: false,
|
||||
});
|
||||
|
||||
console.log('[Identity] Cleared identity');
|
||||
},
|
||||
|
||||
registerNetwork: async (networkId: string, capabilities: string[]) => {
|
||||
const { identity, registrations } = get();
|
||||
|
||||
if (!identity) {
|
||||
set({ error: 'No identity found. Generate or import an identity first.' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (registrations.some(r => r.networkId === networkId)) {
|
||||
set({ error: 'Already registered to this network' });
|
||||
return;
|
||||
}
|
||||
|
||||
set({ isRegistering: true, error: null });
|
||||
|
||||
try {
|
||||
// Create real EdgeNet node for this network
|
||||
const node = await edgeNetService.createNode(`${networkId}-${identity.shortId}`);
|
||||
|
||||
if (node) {
|
||||
// Enable Time Crystal for synchronization
|
||||
edgeNetService.enableTimeCrystal(8);
|
||||
edgeNetService.startNode();
|
||||
|
||||
console.log('[Identity] Connected to real EdgeNet node');
|
||||
}
|
||||
|
||||
const network = availableNetworks.find(n => n.id === networkId);
|
||||
|
||||
const registration: NetworkRegistration = {
|
||||
networkId,
|
||||
networkName: network?.name || networkId,
|
||||
status: 'active',
|
||||
joinedAt: new Date(),
|
||||
capabilities,
|
||||
reputation: 100,
|
||||
creditsEarned: 0,
|
||||
};
|
||||
|
||||
set({
|
||||
registrations: [...registrations, registration],
|
||||
isRegistering: false,
|
||||
});
|
||||
|
||||
console.log('[Identity] Registered to network:', networkId);
|
||||
} catch (error) {
|
||||
console.error('[Identity] Registration failed:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to register',
|
||||
isRegistering: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
leaveNetwork: (networkId: string) => {
|
||||
edgeNetService.pauseNode();
|
||||
|
||||
set((state) => ({
|
||||
registrations: state.registrations.filter(r => r.networkId !== networkId),
|
||||
}));
|
||||
|
||||
console.log('[Identity] Left network:', networkId);
|
||||
},
|
||||
|
||||
updateCapabilities: (networkId: string, capabilities: string[]) => {
|
||||
set((state) => ({
|
||||
registrations: state.registrations.map(r =>
|
||||
r.networkId === networkId ? { ...r, capabilities } : r
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
signData: (data: Uint8Array): Uint8Array | null => {
|
||||
if (currentPiKey) {
|
||||
return currentPiKey.sign(data);
|
||||
}
|
||||
// Web Crypto signing is async, but we need sync here
|
||||
// Return null and use async version externally
|
||||
return null;
|
||||
},
|
||||
|
||||
verifySignature: (data: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean => {
|
||||
if (currentPiKey) {
|
||||
return currentPiKey.verify(data, signature, publicKey);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'edge-net-identity',
|
||||
partialize: (state) => ({
|
||||
identity: state.identity ? {
|
||||
...state.identity,
|
||||
publicKeyBytes: undefined, // Don't persist Uint8Array
|
||||
} : null,
|
||||
registrations: state.registrations,
|
||||
piKeyBackup: state.piKeyBackup,
|
||||
hasRealPiKey: state.hasRealPiKey,
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
209
vendor/ruvector/examples/edge-net/dashboard/src/stores/mcpStore.ts
vendored
Normal file
209
vendor/ruvector/examples/edge-net/dashboard/src/stores/mcpStore.ts
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
import { create } from 'zustand';
|
||||
import type { MCPTool, MCPResult } from '../types';
|
||||
|
||||
interface MCPState {
|
||||
tools: MCPTool[];
|
||||
results: MCPResult[];
|
||||
isConnected: boolean;
|
||||
activeTools: string[];
|
||||
|
||||
// Actions
|
||||
setTools: (tools: MCPTool[]) => void;
|
||||
updateTool: (toolId: string, updates: Partial<MCPTool>) => void;
|
||||
addResult: (result: MCPResult) => void;
|
||||
clearResults: () => void;
|
||||
setConnected: (connected: boolean) => void;
|
||||
executeTool: (toolId: string, params?: Record<string, unknown>) => Promise<MCPResult>;
|
||||
}
|
||||
|
||||
const defaultTools: MCPTool[] = [
|
||||
// Swarm Tools
|
||||
{
|
||||
id: 'swarm_init',
|
||||
name: 'Swarm Initialize',
|
||||
description: 'Initialize a new swarm with specified topology',
|
||||
category: 'swarm',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'swarm_status',
|
||||
name: 'Swarm Status',
|
||||
description: 'Get current swarm status and agent information',
|
||||
category: 'swarm',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'swarm_monitor',
|
||||
name: 'Swarm Monitor',
|
||||
description: 'Monitor swarm activity in real-time',
|
||||
category: 'swarm',
|
||||
status: 'ready',
|
||||
},
|
||||
// Agent Tools
|
||||
{
|
||||
id: 'agent_spawn',
|
||||
name: 'Agent Spawn',
|
||||
description: 'Spawn a new agent in the swarm',
|
||||
category: 'agent',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'agent_list',
|
||||
name: 'Agent List',
|
||||
description: 'List all active agents in the swarm',
|
||||
category: 'agent',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'agent_metrics',
|
||||
name: 'Agent Metrics',
|
||||
description: 'Get performance metrics for agents',
|
||||
category: 'agent',
|
||||
status: 'ready',
|
||||
},
|
||||
// Task Tools
|
||||
{
|
||||
id: 'task_orchestrate',
|
||||
name: 'Task Orchestrate',
|
||||
description: 'Orchestrate a task across the swarm',
|
||||
category: 'task',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'task_status',
|
||||
name: 'Task Status',
|
||||
description: 'Check progress of running tasks',
|
||||
category: 'task',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'task_results',
|
||||
name: 'Task Results',
|
||||
description: 'Retrieve results from completed tasks',
|
||||
category: 'task',
|
||||
status: 'ready',
|
||||
},
|
||||
// Memory Tools
|
||||
{
|
||||
id: 'memory_usage',
|
||||
name: 'Memory Usage',
|
||||
description: 'Get current memory usage statistics',
|
||||
category: 'memory',
|
||||
status: 'ready',
|
||||
},
|
||||
// Neural Tools
|
||||
{
|
||||
id: 'neural_status',
|
||||
name: 'Neural Status',
|
||||
description: 'Get neural agent status and performance metrics',
|
||||
category: 'neural',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'neural_train',
|
||||
name: 'Neural Train',
|
||||
description: 'Train neural agents with sample tasks',
|
||||
category: 'neural',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'neural_patterns',
|
||||
name: 'Neural Patterns',
|
||||
description: 'Get cognitive pattern information',
|
||||
category: 'neural',
|
||||
status: 'ready',
|
||||
},
|
||||
// GitHub Tools
|
||||
{
|
||||
id: 'github_repo_analyze',
|
||||
name: 'Repository Analyze',
|
||||
description: 'Analyze GitHub repository structure and code',
|
||||
category: 'github',
|
||||
status: 'ready',
|
||||
},
|
||||
{
|
||||
id: 'github_pr_manage',
|
||||
name: 'PR Management',
|
||||
description: 'Manage pull requests and reviews',
|
||||
category: 'github',
|
||||
status: 'ready',
|
||||
},
|
||||
];
|
||||
|
||||
export const useMCPStore = create<MCPState>((set, get) => ({
|
||||
tools: defaultTools,
|
||||
results: [],
|
||||
isConnected: true,
|
||||
activeTools: [],
|
||||
|
||||
setTools: (tools) => set({ tools }),
|
||||
|
||||
updateTool: (toolId, updates) =>
|
||||
set((state) => ({
|
||||
tools: state.tools.map((t) =>
|
||||
t.id === toolId ? { ...t, ...updates } : t
|
||||
),
|
||||
})),
|
||||
|
||||
addResult: (result) =>
|
||||
set((state) => ({
|
||||
results: [result, ...state.results].slice(0, 50), // Keep last 50 results
|
||||
})),
|
||||
|
||||
clearResults: () => set({ results: [] }),
|
||||
setConnected: (connected) => set({ isConnected: connected }),
|
||||
|
||||
executeTool: async (toolId, params) => {
|
||||
const { updateTool, addResult } = get();
|
||||
const startTime = performance.now();
|
||||
|
||||
updateTool(toolId, { status: 'running' });
|
||||
set((state) => ({ activeTools: [...state.activeTools, toolId] }));
|
||||
|
||||
try {
|
||||
// Simulate tool execution
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, 500 + Math.random() * 1500)
|
||||
);
|
||||
|
||||
const result: MCPResult = {
|
||||
toolId,
|
||||
success: true,
|
||||
data: {
|
||||
message: `Tool ${toolId} executed successfully`,
|
||||
params,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
timestamp: new Date(),
|
||||
duration: performance.now() - startTime,
|
||||
};
|
||||
|
||||
updateTool(toolId, { status: 'ready', lastRun: new Date() });
|
||||
addResult(result);
|
||||
|
||||
set((state) => ({
|
||||
activeTools: state.activeTools.filter((id) => id !== toolId),
|
||||
}));
|
||||
|
||||
console.log(`[MCP] Tool ${toolId} completed:`, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result: MCPResult = {
|
||||
toolId,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date(),
|
||||
duration: performance.now() - startTime,
|
||||
};
|
||||
|
||||
updateTool(toolId, { status: 'error' });
|
||||
addResult(result);
|
||||
|
||||
set((state) => ({
|
||||
activeTools: state.activeTools.filter((id) => id !== toolId),
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
}));
|
||||
670
vendor/ruvector/examples/edge-net/dashboard/src/stores/networkStore.ts
vendored
Normal file
670
vendor/ruvector/examples/edge-net/dashboard/src/stores/networkStore.ts
vendored
Normal file
@@ -0,0 +1,670 @@
|
||||
import { create } from 'zustand';
|
||||
import type { NetworkStats, NodeInfo, TimeCrystal, CreditBalance } from '../types';
|
||||
import { edgeNetService } from '../services/edgeNet';
|
||||
import { storageService } from '../services/storage';
|
||||
import { relayClient, type TaskAssignment, type NetworkState as RelayNetworkState } from '../services/relayClient';
|
||||
|
||||
interface ContributionSettings {
|
||||
enabled: boolean;
|
||||
cpuLimit: number;
|
||||
gpuEnabled: boolean;
|
||||
gpuLimit: number;
|
||||
memoryLimit: number;
|
||||
bandwidthLimit: number;
|
||||
respectBattery: boolean;
|
||||
onlyWhenIdle: boolean;
|
||||
idleThreshold: number;
|
||||
consentGiven: boolean;
|
||||
consentTimestamp: Date | null;
|
||||
}
|
||||
|
||||
interface NetworkState {
|
||||
stats: NetworkStats;
|
||||
nodes: NodeInfo[];
|
||||
timeCrystal: TimeCrystal;
|
||||
credits: CreditBalance;
|
||||
isConnected: boolean;
|
||||
isRelayConnected: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
startTime: number;
|
||||
contributionSettings: ContributionSettings;
|
||||
isWASMReady: boolean;
|
||||
nodeId: string | null;
|
||||
// Relay network state
|
||||
relayNetworkState: RelayNetworkState | null;
|
||||
connectedPeers: string[];
|
||||
pendingTasks: TaskAssignment[];
|
||||
// Firebase peers (alias for connectedPeers for backward compatibility)
|
||||
firebasePeers: string[];
|
||||
// Persisted cumulative values from IndexedDB
|
||||
persistedCredits: number;
|
||||
persistedTasks: number;
|
||||
persistedUptime: number;
|
||||
|
||||
setStats: (stats: Partial<NetworkStats>) => void;
|
||||
addNode: (node: NodeInfo) => void;
|
||||
removeNode: (nodeId: string) => void;
|
||||
updateNode: (nodeId: string, updates: Partial<NodeInfo>) => void;
|
||||
setTimeCrystal: (crystal: Partial<TimeCrystal>) => void;
|
||||
setCredits: (credits: Partial<CreditBalance>) => void;
|
||||
setConnected: (connected: boolean) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
updateRealStats: () => void;
|
||||
getUptime: () => number;
|
||||
setContributionSettings: (settings: Partial<ContributionSettings>) => void;
|
||||
giveConsent: () => void;
|
||||
revokeConsent: () => void;
|
||||
initializeEdgeNet: () => Promise<void>;
|
||||
startContributing: () => void;
|
||||
stopContributing: () => void;
|
||||
saveToIndexedDB: () => Promise<void>;
|
||||
loadFromIndexedDB: () => Promise<void>;
|
||||
connectToRelay: () => Promise<boolean>;
|
||||
disconnectFromRelay: () => void;
|
||||
processAssignedTask: (task: TaskAssignment) => Promise<void>;
|
||||
clearLocalData: () => Promise<void>;
|
||||
}
|
||||
|
||||
const initialStats: NetworkStats = {
|
||||
totalNodes: 0,
|
||||
activeNodes: 0,
|
||||
totalCompute: 0,
|
||||
creditsEarned: 0,
|
||||
tasksCompleted: 0,
|
||||
uptime: 0,
|
||||
latency: 0,
|
||||
bandwidth: 0,
|
||||
};
|
||||
|
||||
const initialTimeCrystal: TimeCrystal = {
|
||||
phase: 0,
|
||||
frequency: 1.618,
|
||||
coherence: 0,
|
||||
entropy: 1.0,
|
||||
synchronizedNodes: 0,
|
||||
};
|
||||
|
||||
const initialCredits: CreditBalance = {
|
||||
available: 0,
|
||||
pending: 0,
|
||||
earned: 0,
|
||||
spent: 0,
|
||||
};
|
||||
|
||||
const defaultContributionSettings: ContributionSettings = {
|
||||
enabled: false,
|
||||
cpuLimit: 50,
|
||||
gpuEnabled: false,
|
||||
gpuLimit: 30,
|
||||
memoryLimit: 512,
|
||||
bandwidthLimit: 10,
|
||||
respectBattery: true,
|
||||
onlyWhenIdle: true,
|
||||
idleThreshold: 30,
|
||||
consentGiven: false,
|
||||
consentTimestamp: null,
|
||||
};
|
||||
|
||||
export const useNetworkStore = create<NetworkState>()((set, get) => ({
|
||||
stats: initialStats,
|
||||
nodes: [],
|
||||
timeCrystal: initialTimeCrystal,
|
||||
credits: initialCredits,
|
||||
isConnected: false,
|
||||
isRelayConnected: false,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
startTime: Date.now(),
|
||||
contributionSettings: defaultContributionSettings,
|
||||
isWASMReady: false,
|
||||
nodeId: null,
|
||||
relayNetworkState: null,
|
||||
connectedPeers: [],
|
||||
pendingTasks: [],
|
||||
firebasePeers: [], // Kept in sync with connectedPeers for backward compatibility
|
||||
persistedCredits: 0,
|
||||
persistedTasks: 0,
|
||||
persistedUptime: 0,
|
||||
|
||||
setStats: (stats) =>
|
||||
set((state) => ({ stats: { ...state.stats, ...stats } })),
|
||||
|
||||
addNode: (node) =>
|
||||
set((state) => {
|
||||
const newNodes = [...state.nodes, node];
|
||||
return {
|
||||
nodes: newNodes,
|
||||
stats: {
|
||||
...state.stats,
|
||||
totalNodes: newNodes.length,
|
||||
activeNodes: newNodes.filter((n) => n.status === 'active').length,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
removeNode: (nodeId) =>
|
||||
set((state) => {
|
||||
const newNodes = state.nodes.filter((n) => n.id !== nodeId);
|
||||
return {
|
||||
nodes: newNodes,
|
||||
stats: {
|
||||
...state.stats,
|
||||
totalNodes: newNodes.length,
|
||||
activeNodes: newNodes.filter((n) => n.status === 'active').length,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
updateNode: (nodeId, updates) =>
|
||||
set((state) => ({
|
||||
nodes: state.nodes.map((n) =>
|
||||
n.id === nodeId ? { ...n, ...updates } : n
|
||||
),
|
||||
})),
|
||||
|
||||
setTimeCrystal: (crystal) =>
|
||||
set((state) => ({
|
||||
timeCrystal: { ...state.timeCrystal, ...crystal },
|
||||
})),
|
||||
|
||||
setCredits: (credits) =>
|
||||
set((state) => ({
|
||||
credits: { ...state.credits, ...credits },
|
||||
})),
|
||||
|
||||
setConnected: (connected) =>
|
||||
set({ isConnected: connected, isLoading: false }),
|
||||
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
|
||||
setError: (error) => set({ error, isLoading: false }),
|
||||
|
||||
getUptime: () => {
|
||||
const state = get();
|
||||
return (Date.now() - state.startTime) / 1000;
|
||||
},
|
||||
|
||||
setContributionSettings: (settings) =>
|
||||
set((state) => ({
|
||||
contributionSettings: { ...state.contributionSettings, ...settings },
|
||||
})),
|
||||
|
||||
loadFromIndexedDB: async () => {
|
||||
try {
|
||||
const savedState = await storageService.loadState();
|
||||
if (savedState) {
|
||||
set({
|
||||
persistedCredits: savedState.creditsEarned,
|
||||
persistedTasks: savedState.tasksCompleted,
|
||||
persistedUptime: savedState.totalUptime,
|
||||
nodeId: savedState.nodeId,
|
||||
contributionSettings: {
|
||||
...defaultContributionSettings,
|
||||
consentGiven: savedState.consentGiven,
|
||||
consentTimestamp: savedState.consentTimestamp
|
||||
? new Date(savedState.consentTimestamp)
|
||||
: null,
|
||||
cpuLimit: savedState.cpuLimit,
|
||||
gpuEnabled: savedState.gpuEnabled,
|
||||
gpuLimit: savedState.gpuLimit,
|
||||
respectBattery: savedState.respectBattery,
|
||||
onlyWhenIdle: savedState.onlyWhenIdle,
|
||||
},
|
||||
credits: {
|
||||
earned: savedState.creditsEarned,
|
||||
spent: savedState.creditsSpent,
|
||||
available: savedState.creditsEarned - savedState.creditsSpent,
|
||||
pending: 0,
|
||||
},
|
||||
stats: {
|
||||
...initialStats,
|
||||
creditsEarned: savedState.creditsEarned,
|
||||
tasksCompleted: savedState.tasksCompleted,
|
||||
},
|
||||
});
|
||||
console.log('[EdgeNet] Loaded persisted state:', savedState.creditsEarned, 'rUv');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to load from IndexedDB:', error);
|
||||
}
|
||||
},
|
||||
|
||||
saveToIndexedDB: async () => {
|
||||
const state = get();
|
||||
try {
|
||||
await storageService.saveState({
|
||||
id: 'primary',
|
||||
nodeId: state.nodeId,
|
||||
creditsEarned: state.credits.earned,
|
||||
creditsSpent: state.credits.spent,
|
||||
tasksCompleted: state.stats.tasksCompleted,
|
||||
tasksSubmitted: 0,
|
||||
totalUptime: state.stats.uptime + state.persistedUptime,
|
||||
lastActiveTimestamp: Date.now(),
|
||||
consentGiven: state.contributionSettings.consentGiven,
|
||||
consentTimestamp: state.contributionSettings.consentTimestamp?.getTime() || null,
|
||||
cpuLimit: state.contributionSettings.cpuLimit,
|
||||
gpuEnabled: state.contributionSettings.gpuEnabled,
|
||||
gpuLimit: state.contributionSettings.gpuLimit,
|
||||
respectBattery: state.contributionSettings.respectBattery,
|
||||
onlyWhenIdle: state.contributionSettings.onlyWhenIdle,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Failed to save to IndexedDB:', error);
|
||||
}
|
||||
},
|
||||
|
||||
giveConsent: () => {
|
||||
set((state) => ({
|
||||
contributionSettings: {
|
||||
...state.contributionSettings,
|
||||
consentGiven: true,
|
||||
consentTimestamp: new Date(),
|
||||
},
|
||||
}));
|
||||
get().saveToIndexedDB();
|
||||
console.log('[EdgeNet] User consent given for contribution');
|
||||
},
|
||||
|
||||
revokeConsent: async () => {
|
||||
const { stopContributing } = get();
|
||||
stopContributing();
|
||||
set((state) => ({
|
||||
contributionSettings: {
|
||||
...state.contributionSettings,
|
||||
consentGiven: false,
|
||||
consentTimestamp: null,
|
||||
enabled: false,
|
||||
},
|
||||
}));
|
||||
await storageService.clear();
|
||||
console.log('[EdgeNet] User consent revoked, data cleared');
|
||||
},
|
||||
|
||||
initializeEdgeNet: async () => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
console.log('[EdgeNet] Initializing...');
|
||||
|
||||
// Load persisted state from IndexedDB first
|
||||
await get().loadFromIndexedDB();
|
||||
|
||||
// Initialize WASM module
|
||||
await edgeNetService.init();
|
||||
const isWASMReady = edgeNetService.isWASMAvailable();
|
||||
set({ isWASMReady });
|
||||
|
||||
if (isWASMReady) {
|
||||
console.log('[EdgeNet] WASM module ready');
|
||||
const node = await edgeNetService.createNode();
|
||||
if (node) {
|
||||
const nodeId = node.nodeId();
|
||||
set({ nodeId });
|
||||
console.log('[EdgeNet] Node created:', nodeId);
|
||||
edgeNetService.enableTimeCrystal(8);
|
||||
|
||||
// Auto-start if consent was previously given
|
||||
const state = get();
|
||||
if (state.contributionSettings.consentGiven) {
|
||||
edgeNetService.startNode();
|
||||
set((s) => ({
|
||||
contributionSettings: { ...s.contributionSettings, enabled: true },
|
||||
}));
|
||||
console.log('[EdgeNet] Auto-started from previous session');
|
||||
|
||||
// Auto-connect to relay
|
||||
setTimeout(() => {
|
||||
get().connectToRelay();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set({ isConnected: true, isLoading: false });
|
||||
console.log('[EdgeNet] Initialization complete');
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Initialization failed:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to initialize',
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
startContributing: async () => {
|
||||
const { contributionSettings, isWASMReady, nodeId } = get();
|
||||
if (!contributionSettings.consentGiven) {
|
||||
console.warn('[EdgeNet] Cannot start without consent');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start WASM node
|
||||
if (isWASMReady) {
|
||||
edgeNetService.startNode();
|
||||
console.log('[EdgeNet] Started WASM node');
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
contributionSettings: { ...state.contributionSettings, enabled: true },
|
||||
}));
|
||||
|
||||
// Connect to relay for distributed network
|
||||
if (nodeId) {
|
||||
const connected = await get().connectToRelay();
|
||||
if (connected) {
|
||||
console.log('[EdgeNet] Connected to distributed network');
|
||||
}
|
||||
}
|
||||
|
||||
get().saveToIndexedDB();
|
||||
console.log('[EdgeNet] Started contributing');
|
||||
},
|
||||
|
||||
stopContributing: () => {
|
||||
// Pause WASM node
|
||||
edgeNetService.pauseNode();
|
||||
|
||||
// Disconnect from relay
|
||||
get().disconnectFromRelay();
|
||||
|
||||
set((state) => ({
|
||||
contributionSettings: { ...state.contributionSettings, enabled: false },
|
||||
}));
|
||||
get().saveToIndexedDB();
|
||||
console.log('[EdgeNet] Stopped contributing');
|
||||
},
|
||||
|
||||
updateRealStats: () => {
|
||||
const state = get();
|
||||
const sessionUptime = (Date.now() - state.startTime) / 1000;
|
||||
const totalUptime = sessionUptime + state.persistedUptime;
|
||||
const { isWASMReady, contributionSettings } = state;
|
||||
|
||||
// Process epoch if contributing (advances WASM state)
|
||||
if (isWASMReady && contributionSettings.enabled) {
|
||||
edgeNetService.processEpoch();
|
||||
edgeNetService.stepCapabilities(1.0);
|
||||
edgeNetService.recordPerformance(0.95, 100);
|
||||
|
||||
// Submit demo tasks periodically (every ~5 seconds) and process them
|
||||
if (Math.floor(sessionUptime) % 5 === 0) {
|
||||
edgeNetService.submitDemoTask();
|
||||
}
|
||||
// Process any queued tasks to earn credits
|
||||
edgeNetService.processNextTask().catch(() => {
|
||||
// No tasks available is normal
|
||||
});
|
||||
}
|
||||
|
||||
// Get REAL stats from WASM node
|
||||
const realStats = edgeNetService.getStats();
|
||||
const timeCrystalSync = edgeNetService.getTimeCrystalSync();
|
||||
const networkFitness = edgeNetService.getNetworkFitness();
|
||||
|
||||
// Debug: Log raw stats periodically
|
||||
if (realStats && Math.floor(sessionUptime) % 10 === 0) {
|
||||
console.log('[EdgeNet] Raw WASM stats:', {
|
||||
ruv_earned: realStats.ruv_earned?.toString(),
|
||||
tasks_completed: realStats.tasks_completed?.toString(),
|
||||
multiplier: realStats.multiplier,
|
||||
reputation: realStats.reputation,
|
||||
timeCrystalSync,
|
||||
networkFitness,
|
||||
});
|
||||
}
|
||||
|
||||
if (realStats) {
|
||||
// Convert from nanoRuv (1e9) to Ruv
|
||||
const sessionRuvEarned = Number(realStats.ruv_earned) / 1e9;
|
||||
const sessionRuvSpent = Number(realStats.ruv_spent) / 1e9;
|
||||
const sessionTasks = Number(realStats.tasks_completed);
|
||||
|
||||
// Add persisted values for cumulative totals
|
||||
const totalRuvEarned = state.persistedCredits + sessionRuvEarned;
|
||||
const totalTasks = state.persistedTasks + sessionTasks;
|
||||
|
||||
set({
|
||||
stats: {
|
||||
totalNodes: contributionSettings.enabled ? 1 : 0,
|
||||
activeNodes: contributionSettings.enabled ? 1 : 0,
|
||||
totalCompute: Math.round(networkFitness * (contributionSettings.cpuLimit / 100) * 100) / 100,
|
||||
creditsEarned: Math.round(totalRuvEarned * 100) / 100,
|
||||
tasksCompleted: totalTasks,
|
||||
uptime: Math.round(totalUptime * 10) / 10,
|
||||
latency: Math.round((1 - timeCrystalSync) * 100),
|
||||
bandwidth: Math.round(contributionSettings.bandwidthLimit * 10) / 10,
|
||||
},
|
||||
timeCrystal: {
|
||||
...state.timeCrystal,
|
||||
phase: (state.timeCrystal.phase + 0.01) % 1,
|
||||
coherence: Math.round(timeCrystalSync * 1000) / 1000,
|
||||
entropy: Math.round((1 - timeCrystalSync * 0.8) * 1000) / 1000,
|
||||
synchronizedNodes: contributionSettings.enabled ? 1 : 0,
|
||||
},
|
||||
credits: {
|
||||
available: Math.round((totalRuvEarned - sessionRuvSpent - state.credits.spent) * 100) / 100,
|
||||
pending: 0,
|
||||
earned: Math.round(totalRuvEarned * 100) / 100,
|
||||
spent: Math.round((sessionRuvSpent + state.credits.spent) * 100) / 100,
|
||||
},
|
||||
isConnected: isWASMReady || get().isRelayConnected,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
// Save to IndexedDB periodically (every 10 seconds worth of updates)
|
||||
if (Math.floor(sessionUptime) % 10 === 0) {
|
||||
get().saveToIndexedDB();
|
||||
}
|
||||
} else {
|
||||
// WASM not ready - show zeros but keep persisted values
|
||||
set({
|
||||
stats: {
|
||||
...state.stats,
|
||||
totalNodes: 0,
|
||||
activeNodes: 0,
|
||||
totalCompute: 0,
|
||||
uptime: Math.round(totalUptime * 10) / 10,
|
||||
creditsEarned: state.persistedCredits,
|
||||
tasksCompleted: state.persistedTasks,
|
||||
},
|
||||
credits: {
|
||||
...state.credits,
|
||||
earned: state.persistedCredits,
|
||||
},
|
||||
isConnected: false,
|
||||
isLoading: !isWASMReady,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
connectToRelay: async () => {
|
||||
const state = get();
|
||||
if (!state.nodeId) {
|
||||
console.warn('[EdgeNet] Cannot connect to relay without node ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up relay event handlers
|
||||
relayClient.setHandlers({
|
||||
onConnected: (_nodeId, networkState, peers) => {
|
||||
console.log('[EdgeNet] Connected to relay, peers:', peers.length);
|
||||
set({
|
||||
isRelayConnected: true,
|
||||
relayNetworkState: networkState,
|
||||
connectedPeers: peers,
|
||||
firebasePeers: peers,
|
||||
stats: {
|
||||
...get().stats,
|
||||
activeNodes: networkState.activeNodes + 1, // Include ourselves
|
||||
totalNodes: networkState.totalNodes + 1,
|
||||
},
|
||||
timeCrystal: {
|
||||
...get().timeCrystal,
|
||||
phase: networkState.timeCrystalPhase,
|
||||
synchronizedNodes: networkState.activeNodes + 1,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onDisconnected: () => {
|
||||
console.log('[EdgeNet] Disconnected from relay');
|
||||
set({
|
||||
isRelayConnected: false,
|
||||
connectedPeers: [],
|
||||
firebasePeers: [],
|
||||
});
|
||||
},
|
||||
|
||||
onNodeJoined: (nodeId, totalNodes) => {
|
||||
console.log('[EdgeNet] Peer joined:', nodeId);
|
||||
set((s) => ({
|
||||
connectedPeers: [...s.connectedPeers, nodeId],
|
||||
firebasePeers: [...s.firebasePeers, nodeId],
|
||||
stats: { ...s.stats, activeNodes: totalNodes, totalNodes },
|
||||
timeCrystal: { ...s.timeCrystal, synchronizedNodes: totalNodes },
|
||||
}));
|
||||
},
|
||||
|
||||
onNodeLeft: (nodeId, totalNodes) => {
|
||||
console.log('[EdgeNet] Peer left:', nodeId);
|
||||
set((s) => ({
|
||||
connectedPeers: s.connectedPeers.filter((id) => id !== nodeId),
|
||||
firebasePeers: s.firebasePeers.filter((id) => id !== nodeId),
|
||||
stats: { ...s.stats, activeNodes: totalNodes, totalNodes },
|
||||
timeCrystal: { ...s.timeCrystal, synchronizedNodes: totalNodes },
|
||||
}));
|
||||
},
|
||||
|
||||
onTaskAssigned: (task) => {
|
||||
console.log('[EdgeNet] Task assigned:', task.id);
|
||||
set((s) => ({
|
||||
pendingTasks: [...s.pendingTasks, task],
|
||||
}));
|
||||
// Auto-process the task
|
||||
get().processAssignedTask(task);
|
||||
},
|
||||
|
||||
onCreditEarned: (amount, taskId) => {
|
||||
const ruvAmount = Number(amount) / 1e9; // Convert from nanoRuv
|
||||
console.log('[EdgeNet] Credit earned:', ruvAmount, 'rUv for task', taskId);
|
||||
set((s) => ({
|
||||
credits: {
|
||||
...s.credits,
|
||||
earned: s.credits.earned + ruvAmount,
|
||||
available: s.credits.available + ruvAmount,
|
||||
},
|
||||
stats: {
|
||||
...s.stats,
|
||||
creditsEarned: s.stats.creditsEarned + ruvAmount,
|
||||
tasksCompleted: s.stats.tasksCompleted + 1,
|
||||
},
|
||||
}));
|
||||
get().saveToIndexedDB();
|
||||
},
|
||||
|
||||
onTimeCrystalSync: (phase, _timestamp, activeNodes) => {
|
||||
set((s) => ({
|
||||
timeCrystal: {
|
||||
...s.timeCrystal,
|
||||
phase,
|
||||
synchronizedNodes: activeNodes,
|
||||
coherence: Math.min(1, activeNodes / 10), // Coherence increases with more nodes
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
onError: (error) => {
|
||||
console.error('[EdgeNet] Relay error:', error);
|
||||
set({ error: error.message });
|
||||
},
|
||||
});
|
||||
|
||||
// Connect to the relay
|
||||
const connected = await relayClient.connect(state.nodeId);
|
||||
if (connected) {
|
||||
console.log('[EdgeNet] Relay connection established');
|
||||
} else {
|
||||
console.warn('[EdgeNet] Failed to connect to relay');
|
||||
}
|
||||
return connected;
|
||||
},
|
||||
|
||||
disconnectFromRelay: () => {
|
||||
relayClient.disconnect();
|
||||
set({
|
||||
isRelayConnected: false,
|
||||
connectedPeers: [],
|
||||
firebasePeers: [],
|
||||
pendingTasks: [],
|
||||
});
|
||||
},
|
||||
|
||||
processAssignedTask: async (task) => {
|
||||
const state = get();
|
||||
if (!state.isWASMReady) {
|
||||
console.warn('[EdgeNet] Cannot process task - WASM not ready');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('[EdgeNet] Processing task:', task.id, task.taskType);
|
||||
|
||||
// Process the task using WASM
|
||||
const result = await edgeNetService.submitTask(
|
||||
task.taskType,
|
||||
task.payload,
|
||||
task.maxCredits
|
||||
);
|
||||
|
||||
// Process the task in WASM node
|
||||
await edgeNetService.processNextTask();
|
||||
|
||||
// Report completion to relay
|
||||
const reward = task.maxCredits / BigInt(2); // Earn half the max credits
|
||||
relayClient.completeTask(task.id, task.submitter, result, reward);
|
||||
|
||||
// Remove from pending
|
||||
set((s) => ({
|
||||
pendingTasks: s.pendingTasks.filter((t) => t.id !== task.id),
|
||||
}));
|
||||
|
||||
console.log('[EdgeNet] Task completed:', task.id);
|
||||
} catch (error) {
|
||||
console.error('[EdgeNet] Task processing failed:', error);
|
||||
}
|
||||
},
|
||||
|
||||
clearLocalData: async () => {
|
||||
// Disconnect from relay
|
||||
get().disconnectFromRelay();
|
||||
// Stop contributing
|
||||
get().stopContributing();
|
||||
// Clear IndexedDB
|
||||
await storageService.clear();
|
||||
// Reset state to defaults
|
||||
set({
|
||||
stats: initialStats,
|
||||
nodes: [],
|
||||
timeCrystal: initialTimeCrystal,
|
||||
credits: initialCredits,
|
||||
isConnected: false,
|
||||
isRelayConnected: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
startTime: Date.now(),
|
||||
contributionSettings: defaultContributionSettings,
|
||||
isWASMReady: false,
|
||||
nodeId: null,
|
||||
relayNetworkState: null,
|
||||
connectedPeers: [],
|
||||
pendingTasks: [],
|
||||
firebasePeers: [],
|
||||
persistedCredits: 0,
|
||||
persistedTasks: 0,
|
||||
persistedUptime: 0,
|
||||
});
|
||||
console.log('[EdgeNet] Local data cleared');
|
||||
},
|
||||
}));
|
||||
229
vendor/ruvector/examples/edge-net/dashboard/src/stores/wasmStore.ts
vendored
Normal file
229
vendor/ruvector/examples/edge-net/dashboard/src/stores/wasmStore.ts
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
import { create } from 'zustand';
|
||||
import type { WASMModule, WASMBenchmark } from '../types';
|
||||
|
||||
interface WASMState {
|
||||
modules: WASMModule[];
|
||||
benchmarks: WASMBenchmark[];
|
||||
isInitialized: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
wasmInstance: unknown | null;
|
||||
|
||||
// Actions
|
||||
setModules: (modules: WASMModule[]) => void;
|
||||
updateModule: (moduleId: string, updates: Partial<WASMModule>) => void;
|
||||
addBenchmark: (benchmark: WASMBenchmark) => void;
|
||||
clearBenchmarks: () => void;
|
||||
setInitialized: (initialized: boolean) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setError: (error: string | null) => void;
|
||||
loadModule: (moduleId: string) => Promise<void>;
|
||||
runBenchmark: (moduleId: string) => Promise<WASMBenchmark | null>;
|
||||
}
|
||||
|
||||
// Actual WASM modules from the edge-net ecosystem
|
||||
const defaultModules: WASMModule[] = [
|
||||
{
|
||||
id: 'edge-net',
|
||||
name: '@ruvector/edge-net',
|
||||
version: '0.1.1',
|
||||
loaded: false,
|
||||
size: 0, // Will be populated when loaded
|
||||
features: ['Time Crystal', 'DAG Attention', 'P2P Swarm', 'Credit Economy', 'Adaptive Security'],
|
||||
status: 'unloaded',
|
||||
},
|
||||
{
|
||||
id: 'attention-unified',
|
||||
name: '@ruvector/attention-unified-wasm',
|
||||
version: '0.1.0',
|
||||
loaded: false,
|
||||
size: 0,
|
||||
features: ['DAG Attention', 'Critical Path', 'Topological Sort'],
|
||||
status: 'unloaded',
|
||||
},
|
||||
{
|
||||
id: 'economy',
|
||||
name: '@ruvector/economy-wasm',
|
||||
version: '0.1.0',
|
||||
loaded: false,
|
||||
size: 0,
|
||||
features: ['Credit Marketplace', 'Staking', 'Governance'],
|
||||
status: 'unloaded',
|
||||
},
|
||||
{
|
||||
id: 'exotic',
|
||||
name: '@ruvector/exotic-wasm',
|
||||
version: '0.1.0',
|
||||
loaded: false,
|
||||
size: 0,
|
||||
features: ['Exotic AI', 'MinCut Signals', 'RAC Coherence'],
|
||||
status: 'unloaded',
|
||||
},
|
||||
{
|
||||
id: 'learning',
|
||||
name: '@ruvector/learning-wasm',
|
||||
version: '0.1.0',
|
||||
loaded: false,
|
||||
size: 0,
|
||||
features: ['Q-Learning', 'Pattern Recognition', 'Self-Improvement'],
|
||||
status: 'unloaded',
|
||||
},
|
||||
{
|
||||
id: 'nervous-system',
|
||||
name: '@ruvector/nervous-system-wasm',
|
||||
version: '0.1.0',
|
||||
loaded: false,
|
||||
size: 0,
|
||||
features: ['Neural Coordination', 'Homeostasis', 'Reflex Arcs'],
|
||||
status: 'unloaded',
|
||||
},
|
||||
];
|
||||
|
||||
export const useWASMStore = create<WASMState>((set, get) => ({
|
||||
modules: defaultModules,
|
||||
benchmarks: [],
|
||||
isInitialized: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
wasmInstance: null,
|
||||
|
||||
setModules: (modules) => set({ modules }),
|
||||
|
||||
updateModule: (moduleId, updates) =>
|
||||
set((state) => ({
|
||||
modules: state.modules.map((m) =>
|
||||
m.id === moduleId ? { ...m, ...updates } : m
|
||||
),
|
||||
})),
|
||||
|
||||
addBenchmark: (benchmark) =>
|
||||
set((state) => ({
|
||||
benchmarks: [...state.benchmarks, benchmark],
|
||||
})),
|
||||
|
||||
clearBenchmarks: () => set({ benchmarks: [] }),
|
||||
|
||||
setInitialized: (initialized) => set({ isInitialized: initialized }),
|
||||
setLoading: (loading) => set({ isLoading: loading }),
|
||||
setError: (error) => set({ error }),
|
||||
|
||||
loadModule: async (moduleId) => {
|
||||
const { updateModule } = get();
|
||||
|
||||
updateModule(moduleId, { status: 'loading' });
|
||||
|
||||
try {
|
||||
// Attempt to load actual WASM module from CDN
|
||||
const module = get().modules.find(m => m.id === moduleId);
|
||||
if (!module) throw new Error(`Module ${moduleId} not found`);
|
||||
|
||||
const startTime = performance.now();
|
||||
|
||||
// Try loading from unpkg CDN
|
||||
const cdnUrl = `https://unpkg.com/${module.name}@${module.version}/ruvector_edge_net_bg.wasm`;
|
||||
|
||||
console.log(`[WASM] Loading ${module.name} from ${cdnUrl}...`);
|
||||
|
||||
try {
|
||||
const response = await fetch(cdnUrl);
|
||||
if (response.ok) {
|
||||
const wasmBuffer = await response.arrayBuffer();
|
||||
const loadTime = performance.now() - startTime;
|
||||
|
||||
updateModule(moduleId, {
|
||||
status: 'ready',
|
||||
loaded: true,
|
||||
size: wasmBuffer.byteLength,
|
||||
loadTime: Math.round(loadTime),
|
||||
});
|
||||
|
||||
console.log(`[WASM] Module ${moduleId} loaded: ${(wasmBuffer.byteLength / 1024).toFixed(1)}KB in ${loadTime.toFixed(0)}ms`);
|
||||
return;
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.warn(`[WASM] CDN fetch failed for ${moduleId}, using local simulation`);
|
||||
}
|
||||
|
||||
// Fallback: simulate loading if CDN unavailable
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + Math.random() * 500));
|
||||
const loadTime = performance.now() - startTime;
|
||||
|
||||
// Estimate realistic sizes based on actual WASM modules
|
||||
const estimatedSizes: Record<string, number> = {
|
||||
'edge-net': 3_200_000,
|
||||
'attention-unified': 850_000,
|
||||
'economy': 620_000,
|
||||
'exotic': 780_000,
|
||||
'learning': 540_000,
|
||||
'nervous-system': 920_000,
|
||||
};
|
||||
|
||||
updateModule(moduleId, {
|
||||
status: 'ready',
|
||||
loaded: true,
|
||||
size: estimatedSizes[moduleId] || 500_000,
|
||||
loadTime: Math.round(loadTime),
|
||||
});
|
||||
|
||||
console.log(`[WASM] Module ${moduleId} loaded (simulated) in ${loadTime.toFixed(0)}ms`);
|
||||
} catch (error) {
|
||||
updateModule(moduleId, {
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
console.error(`[WASM] Failed to load ${moduleId}:`, error);
|
||||
}
|
||||
},
|
||||
|
||||
runBenchmark: async (moduleId) => {
|
||||
const { modules, addBenchmark } = get();
|
||||
const module = modules.find((m) => m.id === moduleId);
|
||||
|
||||
if (!module || !module.loaded) {
|
||||
console.warn(`[WASM] Cannot benchmark unloaded module: ${moduleId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[WASM] Running benchmark for ${moduleId}...`);
|
||||
|
||||
// Run actual performance benchmark
|
||||
const iterations = 1000;
|
||||
const times: number[] = [];
|
||||
|
||||
// Warm up
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise((r) => requestAnimationFrame(() => r(undefined)));
|
||||
}
|
||||
|
||||
// Benchmark iterations
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = performance.now();
|
||||
// Simulate WASM operation (matrix multiply, vector ops, etc)
|
||||
const arr = new Float32Array(256);
|
||||
for (let j = 0; j < 256; j++) {
|
||||
arr[j] = Math.sin(j) * Math.cos(j);
|
||||
}
|
||||
times.push(performance.now() - start);
|
||||
}
|
||||
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
const minTime = Math.min(...times);
|
||||
const maxTime = Math.max(...times);
|
||||
const totalTime = times.reduce((a, b) => a + b, 0);
|
||||
|
||||
const benchmark: WASMBenchmark = {
|
||||
moduleId,
|
||||
operation: 'vector_ops_256',
|
||||
iterations,
|
||||
avgTime: Math.round(avgTime * 1000) / 1000,
|
||||
minTime: Math.round(minTime * 1000) / 1000,
|
||||
maxTime: Math.round(maxTime * 1000) / 1000,
|
||||
throughput: Math.round(iterations / (totalTime / 1000)),
|
||||
};
|
||||
|
||||
addBenchmark(benchmark);
|
||||
console.log(`[WASM] Benchmark complete for ${moduleId}:`, benchmark);
|
||||
|
||||
return benchmark;
|
||||
},
|
||||
}));
|
||||
99
vendor/ruvector/examples/edge-net/dashboard/src/tests/App.test.tsx
vendored
Normal file
99
vendor/ruvector/examples/edge-net/dashboard/src/tests/App.test.tsx
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { HeroUIProvider } from '@heroui/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import App from '../App';
|
||||
import { useNetworkStore } from '../stores/networkStore';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderApp = () => {
|
||||
return render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<HeroUIProvider>
|
||||
<App />
|
||||
</HeroUIProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('App', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// Reset network store to initial state
|
||||
useNetworkStore.setState({
|
||||
stats: {
|
||||
totalNodes: 0,
|
||||
activeNodes: 0,
|
||||
totalCompute: 0,
|
||||
creditsEarned: 0,
|
||||
tasksCompleted: 0,
|
||||
uptime: 0,
|
||||
latency: 0,
|
||||
bandwidth: 0,
|
||||
},
|
||||
isConnected: false,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders loading state initially', () => {
|
||||
renderApp();
|
||||
expect(screen.getByText(/Initializing Edge-Net/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders main dashboard after loading', async () => {
|
||||
renderApp();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText(/Network Overview/i)).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it('renders header with Edge-Net branding', async () => {
|
||||
renderApp();
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText('Edge-Net')).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it('shows connection status after network connects', async () => {
|
||||
renderApp();
|
||||
|
||||
// Wait for loading to complete and dashboard to render
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(screen.getByText(/Network Overview/i)).toBeInTheDocument();
|
||||
},
|
||||
{ timeout: 3000 }
|
||||
);
|
||||
|
||||
// Update real stats which sets isConnected: true
|
||||
useNetworkStore.getState().updateRealStats();
|
||||
|
||||
// Now check for connection status - could be "Connected" or node count
|
||||
await waitFor(
|
||||
() => {
|
||||
const state = useNetworkStore.getState();
|
||||
// Verify the store state is connected
|
||||
expect(state.isConnected).toBe(true);
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
92
vendor/ruvector/examples/edge-net/dashboard/src/tests/components.test.tsx
vendored
Normal file
92
vendor/ruvector/examples/edge-net/dashboard/src/tests/components.test.tsx
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { HeroUIProvider } from '@heroui/react';
|
||||
import { StatCard } from '../components/common/StatCard';
|
||||
import { GlowingBadge } from '../components/common/GlowingBadge';
|
||||
import { CrystalLoader } from '../components/common/CrystalLoader';
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<HeroUIProvider>{children}</HeroUIProvider>
|
||||
);
|
||||
|
||||
describe('StatCard', () => {
|
||||
it('renders title and value', () => {
|
||||
render(<StatCard title="Test Stat" value={1234} />, { wrapper });
|
||||
|
||||
expect(screen.getByText('Test Stat')).toBeInTheDocument();
|
||||
expect(screen.getByText('1,234')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders string value correctly', () => {
|
||||
render(<StatCard title="String Stat" value="45.8 TFLOPS" />, { wrapper });
|
||||
|
||||
expect(screen.getByText('45.8 TFLOPS')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows positive change indicator', () => {
|
||||
render(<StatCard title="Test" value={100} change={5.5} />, { wrapper });
|
||||
|
||||
expect(screen.getByText(/5.5%/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/↑/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows negative change indicator', () => {
|
||||
render(<StatCard title="Test" value={100} change={-3.2} />, { wrapper });
|
||||
|
||||
expect(screen.getByText(/3.2%/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/↓/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies different color variants', () => {
|
||||
const { rerender } = render(
|
||||
<StatCard title="Test" value={100} color="crystal" />,
|
||||
{ wrapper }
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
<HeroUIProvider>
|
||||
<StatCard title="Test" value={100} color="temporal" />
|
||||
</HeroUIProvider>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GlowingBadge', () => {
|
||||
it('renders children content', () => {
|
||||
render(<GlowingBadge>Test Badge</GlowingBadge>, { wrapper });
|
||||
|
||||
expect(screen.getByText('Test Badge')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies different color variants', () => {
|
||||
render(<GlowingBadge color="success">Success</GlowingBadge>, { wrapper });
|
||||
|
||||
expect(screen.getByText('Success')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CrystalLoader', () => {
|
||||
it('renders without text', () => {
|
||||
const { container } = render(<CrystalLoader />, { wrapper });
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with text', () => {
|
||||
render(<CrystalLoader text="Loading..." />, { wrapper });
|
||||
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('supports different sizes', () => {
|
||||
const { rerender, container } = render(<CrystalLoader size="sm" />, { wrapper });
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
|
||||
rerender(<HeroUIProvider><CrystalLoader size="lg" /></HeroUIProvider>);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
118
vendor/ruvector/examples/edge-net/dashboard/src/tests/debug.test.ts
vendored
Normal file
118
vendor/ruvector/examples/edge-net/dashboard/src/tests/debug.test.ts
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
initDebugConsole,
|
||||
subscribeToLogs,
|
||||
getLogs,
|
||||
clearLogs,
|
||||
debug,
|
||||
timing,
|
||||
} from '../utils/debug';
|
||||
|
||||
describe('Debug Console', () => {
|
||||
beforeEach(() => {
|
||||
clearLogs();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('initDebugConsole', () => {
|
||||
it('initializes without errors', () => {
|
||||
expect(() => initDebugConsole()).not.toThrow();
|
||||
});
|
||||
|
||||
it('overrides console methods', () => {
|
||||
initDebugConsole();
|
||||
|
||||
// Console.log should still work
|
||||
expect(() => console.log('test')).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('debug logging', () => {
|
||||
it('logs info messages', () => {
|
||||
debug.info('Test info message', { data: 'test' });
|
||||
|
||||
const logs = getLogs();
|
||||
expect(logs.some((l) => l.message === 'Test info message')).toBe(true);
|
||||
});
|
||||
|
||||
it('logs warning messages', () => {
|
||||
debug.warn('Test warning', { warning: true });
|
||||
|
||||
const logs = getLogs();
|
||||
expect(logs.some((l) => l.level === 'warn')).toBe(true);
|
||||
});
|
||||
|
||||
it('logs error messages', () => {
|
||||
debug.error('Test error');
|
||||
|
||||
const logs = getLogs();
|
||||
expect(logs.some((l) => l.level === 'error')).toBe(true);
|
||||
});
|
||||
|
||||
it('logs debug messages', () => {
|
||||
debug.debug('Debug message');
|
||||
|
||||
const logs = getLogs();
|
||||
expect(logs.some((l) => l.level === 'debug')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscribeToLogs', () => {
|
||||
it('notifies subscribers on new logs', () => {
|
||||
const listener = vi.fn();
|
||||
subscribeToLogs(listener);
|
||||
|
||||
debug.log('New log');
|
||||
|
||||
expect(listener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns unsubscribe function', () => {
|
||||
const listener = vi.fn();
|
||||
const unsubscribe = subscribeToLogs(listener);
|
||||
|
||||
unsubscribe();
|
||||
listener.mockClear();
|
||||
|
||||
debug.log('After unsubscribe');
|
||||
|
||||
// Listener should not be called after unsubscribe
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearLogs', () => {
|
||||
it('removes all logs', () => {
|
||||
debug.log('Log 1');
|
||||
debug.log('Log 2');
|
||||
|
||||
expect(getLogs().length).toBeGreaterThan(0);
|
||||
|
||||
clearLogs();
|
||||
|
||||
expect(getLogs().length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timing', () => {
|
||||
it('starts and ends timing', () => {
|
||||
timing.start('test-operation');
|
||||
const duration = timing.end('test-operation');
|
||||
|
||||
expect(duration).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('returns 0 for unknown labels', () => {
|
||||
const duration = timing.end('unknown-label');
|
||||
expect(duration).toBe(0);
|
||||
});
|
||||
|
||||
it('measures async operations', async () => {
|
||||
const result = await timing.measure('async-op', async () => {
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
return 'done';
|
||||
});
|
||||
|
||||
expect(result).toBe('done');
|
||||
});
|
||||
});
|
||||
});
|
||||
71
vendor/ruvector/examples/edge-net/dashboard/src/tests/setup.ts
vendored
Normal file
71
vendor/ruvector/examples/edge-net/dashboard/src/tests/setup.ts
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
|
||||
// Cleanup after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// Mock window.matchMedia
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
// Mock ResizeObserver
|
||||
(globalThis as Record<string, unknown>).ResizeObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock IntersectionObserver
|
||||
(globalThis as Record<string, unknown>).IntersectionObserver = vi.fn().mockImplementation(() => ({
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock canvas context
|
||||
HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue({
|
||||
clearRect: vi.fn(),
|
||||
beginPath: vi.fn(),
|
||||
moveTo: vi.fn(),
|
||||
lineTo: vi.fn(),
|
||||
stroke: vi.fn(),
|
||||
arc: vi.fn(),
|
||||
fill: vi.fn(),
|
||||
fillRect: vi.fn(),
|
||||
createRadialGradient: vi.fn().mockReturnValue({
|
||||
addColorStop: vi.fn(),
|
||||
}),
|
||||
scale: vi.fn(),
|
||||
});
|
||||
|
||||
// Mock requestAnimationFrame
|
||||
(globalThis as Record<string, unknown>).requestAnimationFrame = vi.fn((callback: FrameRequestCallback) => {
|
||||
return setTimeout(() => callback(performance.now()), 16) as unknown as number;
|
||||
});
|
||||
|
||||
(globalThis as Record<string, unknown>).cancelAnimationFrame = vi.fn((id: number) => {
|
||||
clearTimeout(id);
|
||||
});
|
||||
|
||||
// Mock performance.now
|
||||
if (!globalThis.performance) {
|
||||
(globalThis as unknown as Record<string, unknown>).performance = {} as Performance;
|
||||
}
|
||||
Object.defineProperty(globalThis.performance, 'now', {
|
||||
value: vi.fn(() => Date.now()),
|
||||
writable: true,
|
||||
});
|
||||
196
vendor/ruvector/examples/edge-net/dashboard/src/tests/stores.test.ts
vendored
Normal file
196
vendor/ruvector/examples/edge-net/dashboard/src/tests/stores.test.ts
vendored
Normal file
@@ -0,0 +1,196 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useNetworkStore } from '../stores/networkStore';
|
||||
import { useWASMStore } from '../stores/wasmStore';
|
||||
import { useMCPStore } from '../stores/mcpStore';
|
||||
import { useCDNStore } from '../stores/cdnStore';
|
||||
|
||||
describe('Network Store', () => {
|
||||
beforeEach(() => {
|
||||
// Reset to initial state (real data starts at 0)
|
||||
useNetworkStore.setState({
|
||||
stats: {
|
||||
totalNodes: 0,
|
||||
activeNodes: 0,
|
||||
totalCompute: 0,
|
||||
creditsEarned: 0,
|
||||
tasksCompleted: 0,
|
||||
uptime: 0,
|
||||
latency: 0,
|
||||
bandwidth: 0,
|
||||
},
|
||||
isConnected: false,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
startTime: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
it('should start with empty network (real data)', () => {
|
||||
const { stats } = useNetworkStore.getState();
|
||||
expect(stats.totalNodes).toBe(0);
|
||||
expect(stats.activeNodes).toBe(0);
|
||||
});
|
||||
|
||||
it('should update stats', () => {
|
||||
const { setStats } = useNetworkStore.getState();
|
||||
setStats({ activeNodes: 5, totalNodes: 10 });
|
||||
|
||||
const { stats } = useNetworkStore.getState();
|
||||
expect(stats.activeNodes).toBe(5);
|
||||
expect(stats.totalNodes).toBe(10);
|
||||
});
|
||||
|
||||
it('should update real stats and track network', () => {
|
||||
// Run multiple ticks to ensure stats update
|
||||
for (let i = 0; i < 50; i++) {
|
||||
useNetworkStore.getState().updateRealStats();
|
||||
}
|
||||
|
||||
const { stats, isConnected } = useNetworkStore.getState();
|
||||
// Network should be connected after updates
|
||||
expect(isConnected).toBe(true);
|
||||
// Some metrics should have updated
|
||||
expect(typeof stats.totalCompute).toBe('number');
|
||||
expect(typeof stats.uptime).toBe('number');
|
||||
});
|
||||
|
||||
it('should track connection status', () => {
|
||||
const { setConnected } = useNetworkStore.getState();
|
||||
|
||||
setConnected(false);
|
||||
expect(useNetworkStore.getState().isConnected).toBe(false);
|
||||
expect(useNetworkStore.getState().isLoading).toBe(false);
|
||||
|
||||
setConnected(true);
|
||||
expect(useNetworkStore.getState().isConnected).toBe(true);
|
||||
});
|
||||
|
||||
it('should calculate uptime', () => {
|
||||
const { getUptime } = useNetworkStore.getState();
|
||||
const uptime = getUptime();
|
||||
expect(typeof uptime).toBe('number');
|
||||
expect(uptime).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WASM Store', () => {
|
||||
it('should have default modules', () => {
|
||||
const { modules } = useWASMStore.getState();
|
||||
expect(modules.length).toBeGreaterThan(0);
|
||||
expect(modules[0].id).toBe('edge-net');
|
||||
expect(modules[0].version).toBe('0.1.1');
|
||||
});
|
||||
|
||||
it('should start with unloaded modules', () => {
|
||||
const { modules } = useWASMStore.getState();
|
||||
const edgeNet = modules.find(m => m.id === 'edge-net');
|
||||
expect(edgeNet?.loaded).toBe(false);
|
||||
expect(edgeNet?.status).toBe('unloaded');
|
||||
expect(edgeNet?.size).toBe(0); // Size unknown until loaded
|
||||
});
|
||||
|
||||
it('should update module status', () => {
|
||||
const { updateModule } = useWASMStore.getState();
|
||||
|
||||
updateModule('edge-net', { status: 'loading' });
|
||||
|
||||
const updatedModules = useWASMStore.getState().modules;
|
||||
const edgeNet = updatedModules.find((m) => m.id === 'edge-net');
|
||||
expect(edgeNet?.status).toBe('loading');
|
||||
});
|
||||
|
||||
it('should track benchmarks', () => {
|
||||
const { addBenchmark, benchmarks } = useWASMStore.getState();
|
||||
const initialCount = benchmarks.length;
|
||||
|
||||
addBenchmark({
|
||||
moduleId: 'edge-net',
|
||||
operation: 'vector_ops_256',
|
||||
iterations: 1000,
|
||||
avgTime: 0.05,
|
||||
minTime: 0.01,
|
||||
maxTime: 0.15,
|
||||
throughput: 20000,
|
||||
});
|
||||
|
||||
expect(useWASMStore.getState().benchmarks.length).toBe(initialCount + 1);
|
||||
});
|
||||
|
||||
it('should clear benchmarks', () => {
|
||||
const { addBenchmark, clearBenchmarks } = useWASMStore.getState();
|
||||
|
||||
addBenchmark({
|
||||
moduleId: 'edge-net',
|
||||
operation: 'test',
|
||||
iterations: 100,
|
||||
avgTime: 1,
|
||||
minTime: 0.5,
|
||||
maxTime: 2,
|
||||
throughput: 100,
|
||||
});
|
||||
|
||||
clearBenchmarks();
|
||||
expect(useWASMStore.getState().benchmarks.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Store', () => {
|
||||
it('should have default tools', () => {
|
||||
const { tools } = useMCPStore.getState();
|
||||
expect(tools.length).toBeGreaterThan(0);
|
||||
expect(tools.some((t) => t.category === 'swarm')).toBe(true);
|
||||
});
|
||||
|
||||
it('should update tool status', () => {
|
||||
const { updateTool } = useMCPStore.getState();
|
||||
|
||||
updateTool('swarm_init', { status: 'running' });
|
||||
|
||||
const updatedTools = useMCPStore.getState().tools;
|
||||
const tool = updatedTools.find((t) => t.id === 'swarm_init');
|
||||
expect(tool?.status).toBe('running');
|
||||
});
|
||||
|
||||
it('should add results', () => {
|
||||
const { addResult } = useMCPStore.getState();
|
||||
|
||||
addResult({
|
||||
toolId: 'swarm_init',
|
||||
success: true,
|
||||
data: { test: true },
|
||||
timestamp: new Date(),
|
||||
duration: 100,
|
||||
});
|
||||
|
||||
const { results } = useMCPStore.getState();
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CDN Store', () => {
|
||||
it('should have default scripts', () => {
|
||||
const { scripts } = useCDNStore.getState();
|
||||
expect(scripts.length).toBeGreaterThan(0);
|
||||
expect(scripts.some((s) => s.category === 'wasm')).toBe(true);
|
||||
});
|
||||
|
||||
it('should toggle script enabled state', () => {
|
||||
const { toggleScript, scripts } = useCDNStore.getState();
|
||||
const initialEnabled = scripts[0].enabled;
|
||||
|
||||
toggleScript(scripts[0].id);
|
||||
|
||||
const updatedScripts = useCDNStore.getState().scripts;
|
||||
expect(updatedScripts[0].enabled).toBe(!initialEnabled);
|
||||
});
|
||||
|
||||
it('should track auto-load setting', () => {
|
||||
const { setAutoLoad } = useCDNStore.getState();
|
||||
|
||||
setAutoLoad(true);
|
||||
expect(useCDNStore.getState().autoLoad).toBe(true);
|
||||
|
||||
setAutoLoad(false);
|
||||
expect(useCDNStore.getState().autoLoad).toBe(false);
|
||||
});
|
||||
});
|
||||
174
vendor/ruvector/examples/edge-net/dashboard/src/types/index.ts
vendored
Normal file
174
vendor/ruvector/examples/edge-net/dashboard/src/types/index.ts
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
// Network Stats Types
|
||||
export interface NetworkStats {
|
||||
totalNodes: number;
|
||||
activeNodes: number;
|
||||
totalCompute: number; // TFLOPS
|
||||
creditsEarned: number;
|
||||
tasksCompleted: number;
|
||||
uptime: number; // percentage
|
||||
latency: number; // ms
|
||||
bandwidth: number; // Mbps
|
||||
}
|
||||
|
||||
export interface NodeInfo {
|
||||
id: string;
|
||||
status: 'online' | 'offline' | 'busy' | 'idle' | 'active';
|
||||
computePower: number;
|
||||
creditsEarned: number;
|
||||
tasksCompleted: number;
|
||||
location?: string;
|
||||
lastSeen: Date;
|
||||
}
|
||||
|
||||
// CDN Configuration
|
||||
export interface CDNScript {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
size: string;
|
||||
category: 'wasm' | 'ai' | 'crypto' | 'network' | 'utility';
|
||||
enabled: boolean;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
export interface CDNConfig {
|
||||
scripts: CDNScript[];
|
||||
autoLoad: boolean;
|
||||
cacheEnabled: boolean;
|
||||
}
|
||||
|
||||
// MCP Tool Types
|
||||
export interface MCPTool {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'swarm' | 'agent' | 'memory' | 'neural' | 'task' | 'github';
|
||||
status: 'ready' | 'running' | 'error' | 'disabled';
|
||||
lastRun?: Date;
|
||||
parameters?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MCPResult {
|
||||
toolId: string;
|
||||
success: boolean;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
timestamp: Date;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
// WASM Module Types
|
||||
export interface WASMModule {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
loaded: boolean;
|
||||
size: number;
|
||||
features: string[];
|
||||
status: 'loading' | 'ready' | 'error' | 'unloaded';
|
||||
error?: string;
|
||||
loadTime?: number; // ms to load
|
||||
}
|
||||
|
||||
export interface WASMBenchmark {
|
||||
moduleId: string;
|
||||
operation: string;
|
||||
iterations: number;
|
||||
avgTime: number;
|
||||
minTime: number;
|
||||
maxTime: number;
|
||||
throughput: number;
|
||||
}
|
||||
|
||||
// Dashboard State
|
||||
export interface DashboardTab {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
export interface ModalConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
|
||||
}
|
||||
|
||||
// Time Crystal Types
|
||||
export interface TimeCrystal {
|
||||
phase: number;
|
||||
frequency: number;
|
||||
coherence: number;
|
||||
entropy: number;
|
||||
synchronizedNodes: number;
|
||||
}
|
||||
|
||||
export interface TemporalMetrics {
|
||||
crystalPhase: number;
|
||||
driftCorrection: number;
|
||||
consensusLatency: number;
|
||||
epochNumber: number;
|
||||
}
|
||||
|
||||
// Specialized Networks
|
||||
export interface SpecializedNetwork {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: 'science' | 'finance' | 'healthcare' | 'ai' | 'gaming' | 'social' | 'compute';
|
||||
icon: string;
|
||||
color: string;
|
||||
stats: {
|
||||
nodes: number;
|
||||
compute: number; // TFLOPS
|
||||
tasks: number;
|
||||
uptime: number; // percentage
|
||||
};
|
||||
requirements: {
|
||||
minCompute: number;
|
||||
minBandwidth: number;
|
||||
capabilities: string[];
|
||||
};
|
||||
rewards: {
|
||||
baseRate: number; // credits per hour
|
||||
bonusMultiplier: number;
|
||||
};
|
||||
status: 'active' | 'maintenance' | 'launching' | 'closed';
|
||||
joined: boolean;
|
||||
joinedAt?: Date;
|
||||
}
|
||||
|
||||
// Credit Economy
|
||||
export interface CreditBalance {
|
||||
available: number;
|
||||
pending: number;
|
||||
earned: number;
|
||||
spent: number;
|
||||
}
|
||||
|
||||
export interface CreditTransaction {
|
||||
id: string;
|
||||
type: 'earn' | 'spend' | 'transfer';
|
||||
amount: number;
|
||||
description: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Debug Console
|
||||
export interface DebugLog {
|
||||
id: string;
|
||||
level: 'info' | 'warn' | 'error' | 'debug';
|
||||
message: string;
|
||||
data?: unknown;
|
||||
timestamp: Date;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface DebugState {
|
||||
logs: DebugLog[];
|
||||
isVisible: boolean;
|
||||
filter: string;
|
||||
level: 'all' | 'info' | 'warn' | 'error' | 'debug';
|
||||
}
|
||||
169
vendor/ruvector/examples/edge-net/dashboard/src/utils/debug.ts
vendored
Normal file
169
vendor/ruvector/examples/edge-net/dashboard/src/utils/debug.ts
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
import type { DebugLog } from '../types';
|
||||
|
||||
// Debug state
|
||||
let debugLogs: DebugLog[] = [];
|
||||
let logListeners: ((logs: DebugLog[]) => void)[] = [];
|
||||
let isConsoleOverridden = false;
|
||||
|
||||
const MAX_LOGS = 500;
|
||||
|
||||
// Generate unique ID
|
||||
const generateId = () => Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// Add log entry
|
||||
const addLog = (level: DebugLog['level'], message: string, data?: unknown, source = 'app') => {
|
||||
const log: DebugLog = {
|
||||
id: generateId(),
|
||||
level,
|
||||
message,
|
||||
data,
|
||||
timestamp: new Date(),
|
||||
source,
|
||||
};
|
||||
|
||||
debugLogs = [log, ...debugLogs].slice(0, MAX_LOGS);
|
||||
logListeners.forEach((listener) => listener(debugLogs));
|
||||
};
|
||||
|
||||
// Initialize debug console
|
||||
export const initDebugConsole = () => {
|
||||
if (isConsoleOverridden) return;
|
||||
isConsoleOverridden = true;
|
||||
|
||||
const originalConsole = {
|
||||
log: console.log.bind(console),
|
||||
warn: console.warn.bind(console),
|
||||
error: console.error.bind(console),
|
||||
info: console.info.bind(console),
|
||||
debug: console.debug.bind(console),
|
||||
};
|
||||
|
||||
// Override console methods
|
||||
console.log = (...args: unknown[]) => {
|
||||
originalConsole.log(...args);
|
||||
addLog('info', formatArgs(args), args.length > 1 ? args : undefined);
|
||||
};
|
||||
|
||||
console.warn = (...args: unknown[]) => {
|
||||
originalConsole.warn(...args);
|
||||
addLog('warn', formatArgs(args), args.length > 1 ? args : undefined);
|
||||
};
|
||||
|
||||
console.error = (...args: unknown[]) => {
|
||||
originalConsole.error(...args);
|
||||
addLog('error', formatArgs(args), args.length > 1 ? args : undefined);
|
||||
};
|
||||
|
||||
console.info = (...args: unknown[]) => {
|
||||
originalConsole.info(...args);
|
||||
addLog('info', formatArgs(args), args.length > 1 ? args : undefined);
|
||||
};
|
||||
|
||||
console.debug = (...args: unknown[]) => {
|
||||
originalConsole.debug(...args);
|
||||
addLog('debug', formatArgs(args), args.length > 1 ? args : undefined);
|
||||
};
|
||||
|
||||
// Add global debug utilities
|
||||
(window as any).edgeNet = {
|
||||
logs: () => debugLogs,
|
||||
clear: () => {
|
||||
debugLogs = [];
|
||||
logListeners.forEach((listener) => listener(debugLogs));
|
||||
},
|
||||
export: () => JSON.stringify(debugLogs, null, 2),
|
||||
stats: () => ({
|
||||
total: debugLogs.length,
|
||||
byLevel: debugLogs.reduce((acc, log) => {
|
||||
acc[log.level] = (acc[log.level] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
bySource: debugLogs.reduce((acc, log) => {
|
||||
acc[log.source] = (acc[log.source] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
}),
|
||||
};
|
||||
|
||||
// Log initialization
|
||||
console.log('[Debug] Console debug utilities initialized');
|
||||
console.log('[Debug] Access debug tools via window.edgeNet');
|
||||
};
|
||||
|
||||
// Format console arguments
|
||||
const formatArgs = (args: unknown[]): string => {
|
||||
return args
|
||||
.map((arg) => {
|
||||
if (typeof arg === 'string') return arg;
|
||||
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
||||
try {
|
||||
return JSON.stringify(arg);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
// Subscribe to log updates
|
||||
export const subscribeToLogs = (listener: (logs: DebugLog[]) => void) => {
|
||||
logListeners.push(listener);
|
||||
listener(debugLogs);
|
||||
|
||||
return () => {
|
||||
logListeners = logListeners.filter((l) => l !== listener);
|
||||
};
|
||||
};
|
||||
|
||||
// Get current logs
|
||||
export const getLogs = () => debugLogs;
|
||||
|
||||
// Clear logs
|
||||
export const clearLogs = () => {
|
||||
debugLogs = [];
|
||||
logListeners.forEach((listener) => listener(debugLogs));
|
||||
};
|
||||
|
||||
// Manual log functions
|
||||
export const debug = {
|
||||
log: (message: string, data?: unknown, source?: string) =>
|
||||
addLog('info', message, data, source),
|
||||
warn: (message: string, data?: unknown, source?: string) =>
|
||||
addLog('warn', message, data, source),
|
||||
error: (message: string, data?: unknown, source?: string) =>
|
||||
addLog('error', message, data, source),
|
||||
debug: (message: string, data?: unknown, source?: string) =>
|
||||
addLog('debug', message, data, source),
|
||||
info: (message: string, data?: unknown, source?: string) =>
|
||||
addLog('info', message, data, source),
|
||||
};
|
||||
|
||||
// Performance timing utilities
|
||||
export const timing = {
|
||||
marks: new Map<string, number>(),
|
||||
|
||||
start: (label: string) => {
|
||||
timing.marks.set(label, performance.now());
|
||||
console.debug(`[Timing] Started: ${label}`);
|
||||
},
|
||||
|
||||
end: (label: string) => {
|
||||
const start = timing.marks.get(label);
|
||||
if (start) {
|
||||
const duration = performance.now() - start;
|
||||
timing.marks.delete(label);
|
||||
console.debug(`[Timing] ${label}: ${duration.toFixed(2)}ms`);
|
||||
return duration;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
measure: async <T>(label: string, fn: () => Promise<T>): Promise<T> => {
|
||||
timing.start(label);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
timing.end(label);
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user