synor/apps/desktop-wallet/src/pages/Compute/ComputeDashboard.tsx
Gulshan Yadav 81347ab15d feat(wallet): add comprehensive desktop wallet features
Add all-in-one desktop wallet with extensive feature set:

Infrastructure:
- Storage: IPFS-based decentralized file storage with upload/download
- Hosting: Domain registration and static site hosting
- Compute: GPU/CPU job marketplace for distributed computing
- Database: Multi-model database services (KV, Document, Vector, etc.)

Financial Features:
- Privacy: Confidential transactions with Pedersen commitments
- Bridge: Cross-chain transfers (Ethereum, Bitcoin, IBC/Cosmos)
- Governance: DAO proposals, voting, and delegation
- ZK-Rollup: L2 scaling with deposits, withdrawals, and transfers

UI/UX Improvements:
- Add ErrorBoundary component for graceful error handling
- Add LoadingStates components (spinners, skeletons, overlays)
- Add Animation components (FadeIn, SlideIn, CountUp, etc.)
- Update navigation with new feature sections

Testing:
- Add Playwright E2E smoke tests
- Test route accessibility and page rendering
- Verify build process and asset loading

Build:
- Fix TypeScript compilation errors
- Update Tauri plugin dependencies
- Successfully build macOS app bundle and DMG
2026-02-02 11:35:21 +05:30

465 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import {
Cpu,
Server,
Play,
Pause,
XCircle,
RefreshCw,
AlertCircle,
Clock,
CheckCircle,
Loader,
ChevronDown,
ChevronUp,
} from 'lucide-react';
import { useComputeStore, formatPrice } from '../../store/compute';
export default function ComputeDashboard() {
const {
providers,
jobs,
isLoading,
isSubmitting,
error,
clearError,
fetchProviders,
fetchJobs,
submitJob,
cancelJob,
} = useComputeStore();
const [activeTab, setActiveTab] = useState<'providers' | 'jobs'>('providers');
const [showSubmitForm, setShowSubmitForm] = useState(false);
const [selectedProvider, setSelectedProvider] = useState('');
const [dockerImage, setDockerImage] = useState('');
const [command, setCommand] = useState('');
const [inputCid, setInputCid] = useState('');
const [cpuCores, setCpuCores] = useState(4);
const [memoryGb, setMemoryGb] = useState(8);
const [maxHours, setMaxHours] = useState(1);
const [gpuType, setGpuType] = useState('');
const [expandedJob, setExpandedJob] = useState<string | null>(null);
useEffect(() => {
fetchProviders();
fetchJobs();
}, [fetchProviders, fetchJobs]);
const handleSubmitJob = async () => {
if (!selectedProvider || !dockerImage || !command) return;
try {
await submitJob({
provider: selectedProvider,
inputCid,
dockerImage,
command: command.split(' '),
gpuType: gpuType || undefined,
cpuCores,
memoryGb,
maxHours,
});
setShowSubmitForm(false);
setSelectedProvider('');
setDockerImage('');
setCommand('');
setInputCid('');
fetchJobs();
} catch {
// Error handled by store
}
};
const handleCancelJob = async (jobId: string) => {
await cancelJob(jobId);
fetchJobs();
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
return <Clock size={16} className="text-yellow-400" />;
case 'running':
return <Loader size={16} className="text-blue-400 animate-spin" />;
case 'completed':
return <CheckCircle size={16} className="text-green-400" />;
case 'failed':
return <XCircle size={16} className="text-red-400" />;
case 'cancelled':
return <Pause size={16} className="text-gray-400" />;
default:
return <Clock size={16} className="text-gray-400" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'text-yellow-400';
case 'running':
return 'text-blue-400';
case 'completed':
return 'text-green-400';
case 'failed':
return 'text-red-400';
case 'cancelled':
return 'text-gray-400';
default:
return 'text-gray-400';
}
};
// Get unique GPU types from all providers
const availableGpuTypes = [...new Set(providers.flatMap((p) => p.gpuTypes))];
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Compute Marketplace</h1>
<p className="text-gray-400 mt-1">Decentralized GPU and CPU compute resources</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => {
fetchProviders();
fetchJobs();
}}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-gray-300 transition-colors disabled:opacity-50"
>
<RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} />
Refresh
</button>
<button
onClick={() => setShowSubmitForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors"
>
<Play size={16} />
Submit Job
</button>
</div>
</div>
{/* Error Alert */}
{error && (
<div className="flex items-center gap-3 p-4 bg-red-900/20 border border-red-800 rounded-lg">
<AlertCircle className="text-red-400" size={20} />
<p className="text-red-400 flex-1">{error}</p>
<button onClick={clearError} className="text-red-400 hover:text-red-300">
×
</button>
</div>
)}
{/* Submit Job Form Modal */}
{showSubmitForm && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800 w-full max-w-lg max-h-[90vh] overflow-y-auto">
<h2 className="text-xl font-bold text-white mb-4">Submit Compute Job</h2>
<div className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Provider</label>
<select
value={selectedProvider}
onChange={(e) => setSelectedProvider(e.target.value)}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-synor-500"
>
<option value="">Select a provider</option>
{providers
.filter((p) => p.isAvailable)
.map((provider) => (
<option key={provider.address} value={provider.address}>
{provider.name} - {formatPrice(provider.pricePerHour)}/hr ({provider.gpuTypes.join(', ') || `${provider.cpuCores} cores`})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Docker Image</label>
<input
type="text"
value={dockerImage}
onChange={(e) => setDockerImage(e.target.value)}
placeholder="pytorch/pytorch:latest"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Command</label>
<input
type="text"
value={command}
onChange={(e) => setCommand(e.target.value)}
placeholder="python train.py --epochs 10"
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Input Data CID (optional)</label>
<input
type="text"
value={inputCid}
onChange={(e) => setInputCid(e.target.value)}
placeholder="Qm..."
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white font-mono placeholder-gray-500 focus:outline-none focus:border-synor-500"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-400 mb-1">CPU Cores</label>
<input
type="number"
value={cpuCores}
onChange={(e) => setCpuCores(Number(e.target.value))}
min={1}
max={64}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-synor-500"
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Memory (GB)</label>
<input
type="number"
value={memoryGb}
onChange={(e) => setMemoryGb(Number(e.target.value))}
min={1}
max={512}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-synor-500"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm text-gray-400 mb-1">GPU Type (optional)</label>
<select
value={gpuType}
onChange={(e) => setGpuType(e.target.value)}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-synor-500"
>
<option value="">None</option>
{availableGpuTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Max Hours</label>
<input
type="number"
value={maxHours}
onChange={(e) => setMaxHours(Number(e.target.value))}
min={1}
max={168}
className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg text-white focus:outline-none focus:border-synor-500"
/>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowSubmitForm(false)}
className="flex-1 px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg text-white font-medium transition-colors"
>
Cancel
</button>
<button
onClick={handleSubmitJob}
disabled={!selectedProvider || !dockerImage || !command || isSubmitting}
className="flex-1 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors disabled:opacity-50"
>
{isSubmitting ? 'Submitting...' : 'Submit Job'}
</button>
</div>
</div>
</div>
</div>
)}
{/* Tabs */}
<div className="flex gap-2 border-b border-gray-800">
<button
onClick={() => setActiveTab('providers')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'providers'
? 'text-synor-400 border-b-2 border-synor-400'
: 'text-gray-400 hover:text-white'
}`}
>
<Server size={16} className="inline mr-2" />
Providers ({providers.length})
</button>
<button
onClick={() => setActiveTab('jobs')}
className={`px-4 py-2 font-medium transition-colors ${
activeTab === 'jobs'
? 'text-synor-400 border-b-2 border-synor-400'
: 'text-gray-400 hover:text-white'
}`}
>
<Cpu size={16} className="inline mr-2" />
My Jobs ({jobs.length})
</button>
</div>
{/* Providers Tab */}
{activeTab === 'providers' && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{providers.map((provider) => (
<div
key={provider.address}
className={`bg-gray-900 rounded-xl p-6 border ${
provider.isAvailable ? 'border-gray-800' : 'border-gray-800/50 opacity-60'
}`}
>
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-white">{provider.name}</h3>
<p className="text-sm text-gray-400 font-mono">{provider.address.slice(0, 12)}...</p>
</div>
<span
className={`px-2 py-1 text-xs rounded-full ${
provider.isAvailable
? 'bg-green-900/50 text-green-400'
: 'bg-gray-800 text-gray-500'
}`}
>
{provider.isAvailable ? 'Available' : 'Busy'}
</span>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
{provider.gpuTypes.length > 0 && (
<div>
<p className="text-gray-500">GPUs</p>
<p className="text-white font-medium">{provider.gpuTypes.join(', ')}</p>
</div>
)}
<div>
<p className="text-gray-500">CPU Cores</p>
<p className="text-white font-medium">{provider.cpuCores}</p>
</div>
<div>
<p className="text-gray-500">Memory</p>
<p className="text-white font-medium">{provider.memoryGb} GB</p>
</div>
<div>
<p className="text-gray-500">Price</p>
<p className="text-synor-400 font-medium">{formatPrice(provider.pricePerHour)}/hr</p>
</div>
<div>
<p className="text-gray-500">Reputation</p>
<p className="text-white font-medium">{provider.reputation}%</p>
</div>
</div>
{provider.isAvailable && (
<button
onClick={() => {
setSelectedProvider(provider.address);
setShowSubmitForm(true);
}}
className="w-full mt-4 px-4 py-2 bg-synor-600 hover:bg-synor-700 rounded-lg text-white font-medium transition-colors"
>
Use This Provider
</button>
)}
</div>
))}
{providers.length === 0 && (
<div className="col-span-2 text-center py-12 text-gray-500">
No compute providers available
</div>
)}
</div>
)}
{/* Jobs Tab */}
{activeTab === 'jobs' && (
<div className="space-y-4">
{jobs.map((job) => (
<div
key={job.jobId}
className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden"
>
<div
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-800/50"
onClick={() => setExpandedJob(expandedJob === job.jobId ? null : job.jobId)}
>
<div className="flex items-center gap-4">
{getStatusIcon(job.status)}
<div>
<h3 className="text-white font-medium">Job {job.jobId.slice(0, 8)}</h3>
<p className="text-sm text-gray-400">
{job.gpuType || `${job.cpuCores} cores`} {job.provider.slice(0, 8)}...
</p>
</div>
</div>
<div className="flex items-center gap-4">
<span className={`text-sm font-medium ${getStatusColor(job.status)}`}>
{job.status.charAt(0).toUpperCase() + job.status.slice(1)}
</span>
{expandedJob === job.jobId ? (
<ChevronUp size={20} className="text-gray-400" />
) : (
<ChevronDown size={20} className="text-gray-400" />
)}
</div>
</div>
{expandedJob === job.jobId && (
<div className="p-4 border-t border-gray-800 bg-gray-800/30">
<div className="grid grid-cols-2 gap-4 text-sm mb-4">
{job.startedAt && (
<div>
<p className="text-gray-500">Started</p>
<p className="text-white">{new Date(job.startedAt).toLocaleString()}</p>
</div>
)}
{job.endedAt && (
<div>
<p className="text-gray-500">Ended</p>
<p className="text-white">{new Date(job.endedAt).toLocaleString()}</p>
</div>
)}
<div>
<p className="text-gray-500">Cost</p>
<p className="text-synor-400">{formatPrice(job.totalCost)}</p>
</div>
<div>
<p className="text-gray-500">Memory</p>
<p className="text-white">{job.memoryGb} GB</p>
</div>
</div>
{job.resultCid && (
<div className="mb-4">
<p className="text-gray-500 text-sm mb-1">Result CID</p>
<code className="block p-3 bg-gray-900 rounded-lg text-sm text-gray-300 overflow-x-auto font-mono">
{job.resultCid}
</code>
</div>
)}
{(job.status === 'pending' || job.status === 'running') && (
<button
onClick={(e) => {
e.stopPropagation();
handleCancelJob(job.jobId);
}}
className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg text-white text-sm font-medium transition-colors"
>
Cancel Job
</button>
)}
</div>
)}
</div>
))}
{jobs.length === 0 && (
<div className="text-center py-12 text-gray-500">
No compute jobs. Submit a job to get started.
</div>
)}
</div>
)}
</div>
);
}