Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user