Files
microdao-daarion/src/features/space-dashboard/components/GalaxyView.tsx
Apple 744c149300
Some checks failed
Build and Deploy Docs / build-and-deploy (push) Has been cancelled
Add automated session logging system
- Created logs/ structure (sessions, operations, incidents)
- Added session-start/log/end scripts
- Installed Git hooks for auto-logging commits/pushes
- Added shell integration for zsh
- Created CHANGELOG.md
- Documented today's session (2026-01-10)
2026-01-10 04:53:17 -08:00

201 lines
5.5 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 { useEffect, useMemo, useRef } from 'react';
import type { SpaceCluster, SpaceScene } from '../types/space';
interface GalaxyViewProps {
scene: SpaceScene;
onSelect?: (cluster: SpaceCluster) => void;
}
const VIEW_WIDTH = 960;
const VIEW_HEIGHT = 520;
interface StarParticle {
x: number;
y: number;
size: number;
opacity: number;
}
export function GalaxyView({ scene, onSelect }: GalaxyViewProps) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const starField = useMemo<StarParticle[]>(
() =>
Array.from({ length: 180 }).map(() => ({
x: Math.random() * VIEW_WIDTH,
y: Math.random() * VIEW_HEIGHT,
size: 0.6 + Math.random() * 1.6,
opacity: 0.35 + Math.random() * 0.45,
})),
[],
);
const summary = useMemo(() => {
const clusters = scene.clusters ?? [];
return clusters.reduce(
(acc, cluster) => {
acc.nodes += cluster.nodes;
acc.microDaos += cluster.microDaos;
acc.agents += cluster.agents;
acc.density = Math.max(acc.density, cluster.density);
return acc;
},
{ nodes: 0, microDaos: 0, agents: 0, density: 0 },
);
}, [scene.clusters]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const dpr = window.devicePixelRatio ?? 1;
canvas.width = VIEW_WIDTH * dpr;
canvas.height = VIEW_HEIGHT * dpr;
ctx.scale(dpr, dpr);
// Background gradient
const bgGradient = ctx.createRadialGradient(
VIEW_WIDTH / 2,
VIEW_HEIGHT / 2,
60,
VIEW_WIDTH / 2,
VIEW_HEIGHT / 2,
520,
);
bgGradient.addColorStop(0, '#0f172a');
bgGradient.addColorStop(0.5, '#111827');
bgGradient.addColorStop(1, '#020617');
ctx.fillStyle = bgGradient;
ctx.fillRect(0, 0, VIEW_WIDTH, VIEW_HEIGHT);
// Starfield
starField.forEach((star) => {
ctx.beginPath();
ctx.globalAlpha = star.opacity;
ctx.fillStyle = '#f8fafc';
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
// Cluster halos
(scene.clusters ?? []).forEach((cluster) => {
const radius = cluster.position.radius ?? 160;
const gradient = ctx.createRadialGradient(
cluster.position.x,
cluster.position.y,
radius * 0.1,
cluster.position.x,
cluster.position.y,
radius,
);
const baseColor =
cluster.status === 'warning'
? '#facc15'
: cluster.status === 'critical'
? '#f87171'
: '#818cf8';
gradient.addColorStop(0, `${baseColor}EE`);
gradient.addColorStop(0.45, `${baseColor}55`);
gradient.addColorStop(1, `${baseColor}00`);
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(cluster.position.x, cluster.position.y, radius, 0, Math.PI * 2);
ctx.fill();
// Constellation lines (simple circle arcs)
ctx.strokeStyle = `${baseColor}AA`;
ctx.lineWidth = 1.2;
ctx.setLineDash([6, 8]);
ctx.beginPath();
ctx.arc(
cluster.position.x,
cluster.position.y,
radius * (0.55 + cluster.density * 0.2),
0,
Math.PI * 2,
);
ctx.stroke();
ctx.setLineDash([]);
});
}, [scene.clusters, starField]);
const metricCards = [
{ label: 'Ноди', value: summary.nodes },
{ label: 'microDAO', value: summary.microDaos },
{ label: 'Агенти', value: summary.agents },
{
label: 'Макс. щільність',
value: `${Math.round(summary.density * 100)}%`,
},
];
const clusterButtonPosition = (cluster: SpaceCluster) => ({
left: `${(cluster.position.x / VIEW_WIDTH) * 100}%`,
top: `${(cluster.position.y / VIEW_HEIGHT) * 100}%`,
});
return (
<div className="rounded-3xl border border-white/10 bg-slate-950/60 p-6 shadow-[0_20px_120px_rgba(59,7,100,0.25)]">
<div className="mb-4 flex flex-wrap items-center justify-between gap-4">
<div>
<p className="text-sm uppercase text-indigo-300/70">Режим</p>
<h3 className="text-2xl font-semibold text-white">Galaxy View</h3>
</div>
<div className="flex gap-3">
{metricCards.map((card) => (
<div
key={card.label}
className="rounded-2xl border border-white/10 bg-white/5 px-4 py-2 text-center text-sm text-white backdrop-blur"
>
<p className="text-xs uppercase tracking-widest text-white/60">
{card.label}
</p>
<p className="text-lg font-semibold text-white">{card.value}</p>
</div>
))}
</div>
</div>
<div className="relative">
<canvas
ref={canvasRef}
width={VIEW_WIDTH}
height={VIEW_HEIGHT}
className="h-[520px] w-full rounded-2xl"
/>
{(scene.clusters ?? []).map((cluster) => (
<button
key={cluster.id}
style={clusterButtonPosition(cluster)}
className="pointer-events-auto absolute -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/20 bg-slate-900/70 px-4 py-1 text-xs font-semibold uppercase tracking-wide text-white shadow-lg backdrop-blur hover:border-indigo-400 hover:text-indigo-200"
onClick={() => onSelect?.(cluster)}
>
{cluster.name}
</button>
))}
</div>
</div>
);
}