welcome to The Grid
This commit is contained in:
parent
3cf6818f66
commit
0bb789ea6b
@ -92,17 +92,16 @@ Between the header and status bars is the Content Area. It uses React Router for
|
|||||||
|
|
||||||
## Unauthenticated Home View
|
## Unauthenticated Home View
|
||||||
|
|
||||||
The logged-out home view displays a retro-styled "GADGET CODE" wordmark in ANSI Art style:
|
The logged-out home view displays a "System Ready" prompt dialog.
|
||||||
|
|
||||||
- VGA color palette: #0000AA, #00AA00, #00AAAA, #AA0000, #AA00AA, #AAAA00, #AAAAAA, #555555
|
|
||||||
- Font: Courier New (monospace)
|
- Font: Courier New (monospace)
|
||||||
- Background: #0a0a0a (pure black)
|
- Background: #0a0a0a (pure black)
|
||||||
- Boxed with 2px border using border-default color
|
- Boxed with 2px border using border-default color
|
||||||
- [Sign In] button styled as `[ Sign In ]` bracket format
|
- Sign In button
|
||||||
|
|
||||||
Users do not "sign up" for Gadget Code - accounts are administered via CLI (`pnpm cli`).
|
Users do not "sign up" for Gadget Code - accounts are administered via CLI (`pnpm cli`).
|
||||||
|
|
||||||
Implementation: `frontend/src/pages/Home.tsx` - AnsiLogo component
|
Implementation: `frontend/src/pages/Home.tsx` - SystemReady component
|
||||||
|
|
||||||
## Authenticated Home View (Dashboard)
|
## Authenticated Home View (Dashboard)
|
||||||
|
|
||||||
|
|||||||
443
gadget-code/frontend/src/components/GadgetGrid.tsx
Normal file
443
gadget-code/frontend/src/components/GadgetGrid.tsx
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
import { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import { Canvas, useFrame, useThree } from '@react-three/fiber';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
const BOARD_SIZE = 200;
|
||||||
|
const GRID_DIVISIONS = 16;
|
||||||
|
const JUNCTION_SPACING = BOARD_SIZE / GRID_DIVISIONS;
|
||||||
|
const HALF_BOARD = BOARD_SIZE / 2;
|
||||||
|
const ORB_COUNT = 20;
|
||||||
|
const PULSE_COUNT = 10;
|
||||||
|
|
||||||
|
interface Junction {
|
||||||
|
x: number;
|
||||||
|
z: number;
|
||||||
|
connections: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Trace {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ParticleData {
|
||||||
|
pos: THREE.Vector3;
|
||||||
|
target: THREE.Vector3;
|
||||||
|
junctionIndex: number;
|
||||||
|
targetJunctionIndex: number;
|
||||||
|
progress: number;
|
||||||
|
speed: number;
|
||||||
|
type: 'orb' | 'pulse';
|
||||||
|
}
|
||||||
|
|
||||||
|
function seededRandom(seed: number): () => number {
|
||||||
|
let s = seed;
|
||||||
|
return () => {
|
||||||
|
s = (s * 9301 + 49297) % 233280;
|
||||||
|
return s / 233280;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createGlowTexture(color: string): THREE.CanvasTexture {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 64;
|
||||||
|
canvas.height = 64;
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
|
||||||
|
const gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
|
||||||
|
gradient.addColorStop(0, color);
|
||||||
|
gradient.addColorStop(0.3, color.replace(')', ', 0.8)').replace('rgb', 'rgba'));
|
||||||
|
gradient.addColorStop(0.6, color.replace(')', ', 0.3)').replace('rgb', 'rgba'));
|
||||||
|
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
||||||
|
|
||||||
|
ctx.fillStyle = gradient;
|
||||||
|
ctx.fillRect(0, 0, 64, 64);
|
||||||
|
|
||||||
|
return new THREE.CanvasTexture(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBoardData() {
|
||||||
|
const rand = seededRandom(42);
|
||||||
|
const junctions: Junction[] = [];
|
||||||
|
const indexMap: Map<string, number> = new Map();
|
||||||
|
|
||||||
|
for (let gx = 0; gx <= GRID_DIVISIONS; gx++) {
|
||||||
|
for (let gz = 0; gz <= GRID_DIVISIONS; gz++) {
|
||||||
|
const x = -HALF_BOARD + gx * JUNCTION_SPACING;
|
||||||
|
const z = -HALF_BOARD + gz * JUNCTION_SPACING;
|
||||||
|
const idx = junctions.length;
|
||||||
|
junctions.push({ x, z, connections: [] });
|
||||||
|
indexMap.set(`${gx},${gz}`, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let gx = 0; gx <= GRID_DIVISIONS; gx++) {
|
||||||
|
for (let gz = 0; gz <= GRID_DIVISIONS; gz++) {
|
||||||
|
const idx = indexMap.get(`${gx},${gz}`);
|
||||||
|
if (idx === undefined) continue;
|
||||||
|
|
||||||
|
if (gx < GRID_DIVISIONS) {
|
||||||
|
const rightIdx = indexMap.get(`${gx + 1},${gz}`);
|
||||||
|
if (rightIdx !== undefined) {
|
||||||
|
junctions[idx].connections.push(rightIdx);
|
||||||
|
junctions[rightIdx].connections.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gz < GRID_DIVISIONS) {
|
||||||
|
const downIdx = indexMap.get(`${gx},${gz + 1}`);
|
||||||
|
if (downIdx !== undefined) {
|
||||||
|
junctions[idx].connections.push(downIdx);
|
||||||
|
junctions[downIdx].connections.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gx < GRID_DIVISIONS && gz < GRID_DIVISIONS && rand() > 0.85) {
|
||||||
|
const diagIdx = indexMap.get(`${gx + 1},${gz + 1}`);
|
||||||
|
if (diagIdx !== undefined) {
|
||||||
|
junctions[idx].connections.push(diagIdx);
|
||||||
|
junctions[diagIdx].connections.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const traceSet = new Set<string>();
|
||||||
|
const traces: Trace[] = [];
|
||||||
|
for (const j of junctions) {
|
||||||
|
const jIdx = junctions.indexOf(j);
|
||||||
|
for (const c of j.connections) {
|
||||||
|
const key = j.x < junctions[c].x || (j.x === junctions[c].x && j.z <= junctions[c].z)
|
||||||
|
? `${jIdx},${c}`
|
||||||
|
: `${c},${jIdx}`;
|
||||||
|
if (!traceSet.has(key)) {
|
||||||
|
traceSet.add(key);
|
||||||
|
traces.push({ start: jIdx, end: c });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chips: Array<{ x: number; z: number; width: number; depth: number }> = [];
|
||||||
|
const usedIndices = new Set<number>();
|
||||||
|
for (let i = 0; i < 22; i++) {
|
||||||
|
let attempts = 0;
|
||||||
|
while (attempts < 50) {
|
||||||
|
const idx = Math.floor(rand() * junctions.length);
|
||||||
|
if (!usedIndices.has(idx) && junctions[idx].connections.length >= 2) {
|
||||||
|
usedIndices.add(idx);
|
||||||
|
chips.push({
|
||||||
|
x: junctions[idx].x,
|
||||||
|
z: junctions[idx].z,
|
||||||
|
width: 5 + rand() * 7,
|
||||||
|
depth: 5 + rand() * 7,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
attempts++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const capacitors: Array<{ x: number; z: number; radius: number; height: number }> = [];
|
||||||
|
for (let i = 0; i < 45; i++) {
|
||||||
|
const idx = Math.floor(rand() * junctions.length);
|
||||||
|
capacitors.push({
|
||||||
|
x: junctions[idx].x + (rand() - 0.5) * 3,
|
||||||
|
z: junctions[idx].z + (rand() - 0.5) * 3,
|
||||||
|
radius: 0.35 + rand() * 0.45,
|
||||||
|
height: 1.2 + rand() * 2.2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ics: Array<{ x: number; z: number; width: number; depth: number }> = [];
|
||||||
|
const icCandidates = junctions.filter(j => j.connections.length > 2);
|
||||||
|
for (let i = 0; i < Math.min(16, icCandidates.length); i++) {
|
||||||
|
const j = icCandidates[i];
|
||||||
|
ics.push({
|
||||||
|
x: j.x,
|
||||||
|
z: j.z,
|
||||||
|
width: 10 + rand() * 6,
|
||||||
|
depth: 6 + rand() * 4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { junctions, traces, chips, capacitors, ics };
|
||||||
|
}
|
||||||
|
|
||||||
|
function TraceLines({ traces, junctions }: { traces: Trace[]; junctions: Junction[] }) {
|
||||||
|
const geometry = useMemo(() => {
|
||||||
|
const positions: number[] = [];
|
||||||
|
for (const t of traces) {
|
||||||
|
const start = junctions[t.start];
|
||||||
|
const end = junctions[t.end];
|
||||||
|
positions.push(start.x, 0.1, start.z);
|
||||||
|
positions.push(end.x, 0.1, end.z);
|
||||||
|
}
|
||||||
|
const geo = new THREE.BufferGeometry();
|
||||||
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||||
|
return geo;
|
||||||
|
}, [traces, junctions]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<lineSegments geometry={geometry}>
|
||||||
|
<lineBasicMaterial color="#3d5a3a" />
|
||||||
|
</lineSegments>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Chips({ chips }: { chips: Array<{ x: number; z: number; width: number; depth: number }> }) {
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{chips.map((chip, i) => (
|
||||||
|
<group key={i} position={[chip.x, 0, chip.z]}>
|
||||||
|
<mesh position={[0, 0.6, 0]} castShadow>
|
||||||
|
<boxGeometry args={[chip.width, 1.2, chip.depth]} />
|
||||||
|
<meshStandardMaterial color="#1c1c1c" roughness={0.4} metalness={0.5} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 1.25, 0]}>
|
||||||
|
<boxGeometry args={[chip.width * 0.4, 0.25, chip.depth * 0.15]} />
|
||||||
|
<meshStandardMaterial color="#3a3a3a" roughness={0.3} metalness={0.6} emissive="#0a1a0a" emissiveIntensity={0.2} />
|
||||||
|
</mesh>
|
||||||
|
{[-1, 1].map((side) =>
|
||||||
|
Array.from({ length: Math.floor(chip.width / 1.2) }).map((_, pi) => (
|
||||||
|
<mesh key={`pin-${side}-${pi}`} position={[side * (chip.width / 2 + 0.25), 0.25, -chip.depth / 2 + 1.0 + pi * 1.2]} castShadow>
|
||||||
|
<boxGeometry args={[0.25, 0.5, 0.25]} />
|
||||||
|
<meshStandardMaterial color="#909090" roughness={0.25} metalness={0.85} />
|
||||||
|
</mesh>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{[-1, 1].map((side) =>
|
||||||
|
Array.from({ length: Math.floor(chip.depth / 1.2) }).map((_, pi) => (
|
||||||
|
<mesh key={`pin-z-${side}-${pi}`} position={[-chip.width / 2 + 1.0 + pi * 1.2, 0.25, side * (chip.depth / 2 + 0.25)]} castShadow>
|
||||||
|
<boxGeometry args={[0.25, 0.5, 0.25]} />
|
||||||
|
<meshStandardMaterial color="#909090" roughness={0.25} metalness={0.85} />
|
||||||
|
</mesh>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Capacitors({ capacitors }: { capacitors: Array<{ x: number; z: number; radius: number; height: number }> }) {
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{capacitors.map((cap, i) => (
|
||||||
|
<mesh key={i} position={[cap.x, cap.height / 2, cap.z]} castShadow>
|
||||||
|
<cylinderGeometry args={[cap.radius, cap.radius, cap.height, 8]} />
|
||||||
|
<meshStandardMaterial color="#2d2d2d" roughness={0.35} metalness={0.5} />
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ICs({ ics }: { ics: Array<{ x: number; z: number; width: number; depth: number }> }) {
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{ics.map((ic, i) => (
|
||||||
|
<group key={i} position={[ic.x, 0, ic.z]}>
|
||||||
|
<mesh position={[0, 0.8, 0]} castShadow>
|
||||||
|
<boxGeometry args={[ic.width, 1.6, ic.depth]} />
|
||||||
|
<meshStandardMaterial color="#181818" roughness={0.5} metalness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, 1.65, 0]}>
|
||||||
|
<boxGeometry args={[ic.width * 0.2, 0.15, ic.depth * 0.1]} />
|
||||||
|
<meshStandardMaterial color="#2a2a2a" roughness={0.4} metalness={0.5} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ParticleSystem({ junctions }: { junctions: Junction[] }) {
|
||||||
|
const orbRefs = useRef<(THREE.Mesh | null)[]>([]);
|
||||||
|
const pulseRefs = useRef<(THREE.Mesh | null)[]>([]);
|
||||||
|
const particles = useRef<ParticleData[]>([]);
|
||||||
|
const initialized = useRef(false);
|
||||||
|
const glowTexture = useMemo(() => createGlowTexture('rgb(0, 255, 68)'), []);
|
||||||
|
const pulseGlowTexture = useMemo(() => createGlowTexture('rgb(0, 255, 255)'), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialized.current) return;
|
||||||
|
initialized.current = true;
|
||||||
|
|
||||||
|
const rand = seededRandom(Date.now());
|
||||||
|
const initial: ParticleData[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < ORB_COUNT; i++) {
|
||||||
|
const idx = Math.floor(rand() * junctions.length);
|
||||||
|
const conn = junctions[idx].connections;
|
||||||
|
const target = conn.length > 0 ? conn[Math.floor(rand() * conn.length)] : idx;
|
||||||
|
initial.push({
|
||||||
|
pos: new THREE.Vector3(junctions[idx].x, 0.4, junctions[idx].z),
|
||||||
|
target: new THREE.Vector3(junctions[target].x, 0.4, junctions[target].z),
|
||||||
|
junctionIndex: idx,
|
||||||
|
targetJunctionIndex: target,
|
||||||
|
progress: rand(),
|
||||||
|
speed: 0.002 + rand() * 0.003,
|
||||||
|
type: 'orb',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < PULSE_COUNT; i++) {
|
||||||
|
const idx = Math.floor(rand() * junctions.length);
|
||||||
|
const conn = junctions[idx].connections;
|
||||||
|
const target = conn.length > 0 ? conn[Math.floor(rand() * conn.length)] : idx;
|
||||||
|
initial.push({
|
||||||
|
pos: new THREE.Vector3(junctions[idx].x, 0.4, junctions[idx].z),
|
||||||
|
target: new THREE.Vector3(junctions[target].x, 0.4, junctions[target].z),
|
||||||
|
junctionIndex: idx,
|
||||||
|
targetJunctionIndex: target,
|
||||||
|
progress: rand(),
|
||||||
|
speed: 0.01 + rand() * 0.015,
|
||||||
|
type: 'pulse',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
particles.current = initial;
|
||||||
|
}, [junctions]);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
const p = particles.current;
|
||||||
|
if (!p.length) return;
|
||||||
|
|
||||||
|
for (let i = 0; i < p.length; i++) {
|
||||||
|
const particle = p[i];
|
||||||
|
particle.progress += particle.speed;
|
||||||
|
|
||||||
|
if (particle.progress >= 1) {
|
||||||
|
const conn = junctions[particle.junctionIndex].connections;
|
||||||
|
if (conn.length > 0) {
|
||||||
|
particle.junctionIndex = particle.targetJunctionIndex;
|
||||||
|
const nextConn = junctions[particle.targetJunctionIndex].connections.filter(c => c !== particle.junctionIndex);
|
||||||
|
particle.targetJunctionIndex = nextConn.length > 0
|
||||||
|
? nextConn[Math.floor(Math.random() * nextConn.length)]
|
||||||
|
: conn[Math.floor(Math.random() * conn.length)];
|
||||||
|
particle.progress = 0;
|
||||||
|
particle.target.set(
|
||||||
|
junctions[particle.targetJunctionIndex].x,
|
||||||
|
0.4,
|
||||||
|
junctions[particle.targetJunctionIndex].z
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
particle.progress = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startJ = junctions[particle.junctionIndex];
|
||||||
|
particle.pos.x = startJ.x + (particle.target.x - startJ.x) * particle.progress;
|
||||||
|
particle.pos.z = startJ.z + (particle.target.z - startJ.z) * particle.progress;
|
||||||
|
particle.pos.y = 0.4 + Math.sin(particle.progress * Math.PI) * 0.6;
|
||||||
|
|
||||||
|
if (particle.type === 'orb') {
|
||||||
|
const mesh = orbRefs.current[i];
|
||||||
|
if (mesh) {
|
||||||
|
mesh.position.copy(particle.pos);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const mesh = pulseRefs.current[i - ORB_COUNT];
|
||||||
|
if (mesh) {
|
||||||
|
mesh.position.copy(particle.pos);
|
||||||
|
const scale = 1 - Math.abs(particle.progress - 0.5) * 2;
|
||||||
|
mesh.scale.setScalar(0.4 + scale * 0.6);
|
||||||
|
(mesh.material as THREE.MeshStandardMaterial).emissiveIntensity = 2 + scale * 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{Array.from({ length: ORB_COUNT }).map((_, i) => (
|
||||||
|
<mesh
|
||||||
|
key={`orb-${i}`}
|
||||||
|
ref={(el) => { orbRefs.current[i] = el; }}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[1.2, 1.2]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={glowTexture}
|
||||||
|
transparent
|
||||||
|
opacity={0.95}
|
||||||
|
depthWrite={false}
|
||||||
|
blending={THREE.AdditiveBlending}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
{Array.from({ length: PULSE_COUNT }).map((_, i) => (
|
||||||
|
<mesh
|
||||||
|
key={`pulse-${i}`}
|
||||||
|
ref={(el) => { pulseRefs.current[i] = el; }}
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
>
|
||||||
|
<planeGeometry args={[0.8, 0.8]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
map={pulseGlowTexture}
|
||||||
|
transparent
|
||||||
|
opacity={0.95}
|
||||||
|
depthWrite={false}
|
||||||
|
blending={THREE.AdditiveBlending}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
))}
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CameraDrift() {
|
||||||
|
const { camera } = useThree();
|
||||||
|
const timeRef = useRef(0);
|
||||||
|
|
||||||
|
useFrame((_, delta) => {
|
||||||
|
timeRef.current += delta;
|
||||||
|
const t = timeRef.current;
|
||||||
|
|
||||||
|
camera.position.x = Math.sin(t * 0.05) * 3;
|
||||||
|
camera.position.y = 80 + Math.sin(t * 0.07) * 2.5;
|
||||||
|
camera.position.z = Math.cos(t * 0.04) * 5;
|
||||||
|
|
||||||
|
camera.lookAt(0, 0, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Scene() {
|
||||||
|
const boardData = useMemo(() => generateBoardData(), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ambientLight intensity={0.3} />
|
||||||
|
<directionalLight position={[60, 120, 60]} intensity={0.7} castShadow />
|
||||||
|
<directionalLight position={[-40, 80, -40]} intensity={0.4} />
|
||||||
|
<pointLight position={[0, 40, 0]} intensity={0.6} color="#00ff44" distance={120} />
|
||||||
|
|
||||||
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0.5, 0]} receiveShadow>
|
||||||
|
<planeGeometry args={[BOARD_SIZE, BOARD_SIZE]} />
|
||||||
|
<meshStandardMaterial color="#080808" roughness={0.85} metalness={0.15} />
|
||||||
|
</mesh>
|
||||||
|
|
||||||
|
<TraceLines traces={boardData.traces} junctions={boardData.junctions} />
|
||||||
|
<Chips chips={boardData.chips} />
|
||||||
|
<Capacitors capacitors={boardData.capacitors} />
|
||||||
|
<ICs ics={boardData.ics} />
|
||||||
|
<ParticleSystem junctions={boardData.junctions} />
|
||||||
|
<CameraDrift />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GadgetGrid() {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<Canvas
|
||||||
|
camera={{ position: [0, 80, 0], fov: 45, near: 0.1, far: 500 }}
|
||||||
|
gl={{ antialias: true, alpha: true }}
|
||||||
|
dpr={[1, 1.5]}
|
||||||
|
>
|
||||||
|
<Scene />
|
||||||
|
</Canvas>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,39 +1,17 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import type { User, Project, DroneRegistration } from '../lib/api';
|
import type { User, Project, DroneRegistration } from "../lib/api";
|
||||||
import { projectApi, droneApi } from '../lib/api';
|
import { projectApi, droneApi } from "../lib/api";
|
||||||
import Clock from '../components/Clock';
|
import Clock from "../components/Clock";
|
||||||
|
import GadgetGrid from "../components/GadgetGrid";
|
||||||
|
|
||||||
const ansiColors = [
|
function SystemReady() {
|
||||||
'#0000AA', '#00AA00', '#00AAAA', '#AA0000',
|
|
||||||
'#AA00AA', '#AAAA00', '#AAAAAA', '#555555',
|
|
||||||
];
|
|
||||||
|
|
||||||
function AnsiLogo() {
|
|
||||||
const word = 'GADGET CODE';
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div
|
|
||||||
className="inline-flex font-mono text-5xl tracking-wider"
|
|
||||||
style={{ textShadow: '2px 2px 4px rgba(0,0,0,0.8)' }}
|
|
||||||
>
|
|
||||||
{word.split('').map((char, i) => (
|
|
||||||
<span
|
|
||||||
key={i}
|
|
||||||
className="inline-block"
|
|
||||||
style={{ color: ansiColors[i % ansiColors.length] }}
|
|
||||||
>
|
|
||||||
{char}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 text-text-muted font-mono text-sm">
|
|
||||||
Agentic Engineering IDE v1.0.0
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 border-2 border-border-default p-6 rounded bg-bg-secondary">
|
<div className="mt-8 border-2 border-border-default p-6 rounded bg-bg-secondary">
|
||||||
<div className="font-mono text-text-secondary text-sm">
|
<div className="font-mono text-text-secondary text-sm">
|
||||||
<div className="mb-2 text-text-muted">// SYSTEM READY</div>
|
<div className="mb-1 text-text-muted">// SYSTEM READY</div>
|
||||||
<div className="mb-4">Please sign in to continue.</div>
|
<div className="mb-3">Please sign in to continue.</div>
|
||||||
<div className="text-text-muted mb-4">
|
<div className="text-text-muted mb-4">
|
||||||
Accounts are administered. Contact your administrator for access.
|
Accounts are administered. Contact your administrator for access.
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +33,13 @@ interface DashboardSidebarProps {
|
|||||||
onSelectDrone: (drone: DroneRegistration | null) => void;
|
onSelectDrone: (drone: DroneRegistration | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DroneInspector({ drone, onClose }: { drone: DroneRegistration; onClose: () => void }) {
|
function DroneInspector({
|
||||||
|
drone,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
drone: DroneRegistration;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center p-8">
|
<div className="flex-1 flex items-center justify-center p-8">
|
||||||
<div className="max-w-lg w-full">
|
<div className="max-w-lg w-full">
|
||||||
@ -75,21 +59,25 @@ function DroneInspector({ drone, onClose }: { drone: DroneRegistration; onClose:
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-text-muted">Workspace</div>
|
<div className="text-sm text-text-muted">Workspace</div>
|
||||||
<div className="font-mono text-text-primary text-sm truncate">{drone.workspaceDir}</div>
|
<div className="font-mono text-text-primary text-sm truncate">
|
||||||
|
{drone.workspaceDir}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-text-muted">Status</div>
|
<div className="text-sm text-text-muted">Status</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`w-2 h-2 rounded-full ${
|
className={`w-2 h-2 rounded-full ${
|
||||||
drone.status === 'available'
|
drone.status === "available"
|
||||||
? 'bg-green-500'
|
? "bg-green-500"
|
||||||
: drone.status === 'busy'
|
: drone.status === "busy"
|
||||||
? 'bg-yellow-500'
|
? "bg-yellow-500"
|
||||||
: 'bg-gray-600'
|
: "bg-gray-600"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-text-primary capitalize">{drone.status}</span>
|
<span className="text-text-primary capitalize">
|
||||||
|
{drone.status}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -104,7 +92,11 @@ function DroneInspector({ drone, onClose }: { drone: DroneRegistration; onClose:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: DashboardSidebarProps) {
|
function DashboardSidebar({
|
||||||
|
onNavigate,
|
||||||
|
selectedDrone,
|
||||||
|
onSelectDrone,
|
||||||
|
}: DashboardSidebarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [projects, setProjects] = useState<Project[]>([]);
|
const [projects, setProjects] = useState<Project[]>([]);
|
||||||
const [drones, setDrones] = useState<DroneRegistration[]>([]);
|
const [drones, setDrones] = useState<DroneRegistration[]>([]);
|
||||||
@ -123,7 +115,7 @@ function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: Dashboar
|
|||||||
setProjects(projectsData);
|
setProjects(projectsData);
|
||||||
setDrones(dronesData);
|
setDrones(dronesData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load dashboard data', err);
|
console.error("Failed to load dashboard data", err);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -170,9 +162,24 @@ function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: Dashboar
|
|||||||
className="text-text-secondary hover:text-text-primary transition-colors"
|
className="text-text-secondary hover:text-text-primary transition-colors"
|
||||||
title="Drone Manager"
|
title="Drone Manager"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
className="w-4 h-4"
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@ -183,40 +190,42 @@ function DashboardSidebar({ onNavigate, selectedDrone, onSelectDrone }: Dashboar
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{drones.map((drone) => (
|
{drones.map((drone) => (
|
||||||
<button
|
<button
|
||||||
key={drone._id}
|
key={drone._id}
|
||||||
onClick={() => onSelectDrone(drone)}
|
onClick={() => onSelectDrone(drone)}
|
||||||
className={`w-full text-left px-2 py-1 rounded transition-colors ${
|
className={`w-full text-left px-2 py-1 rounded transition-colors ${
|
||||||
selectedDrone?._id === drone._id
|
selectedDrone?._id === drone._id
|
||||||
? 'bg-brand text-white'
|
? "bg-brand text-white"
|
||||||
: 'text-text-secondary hover:bg-bg-tertiary hover:text-text-primary'
|
: "text-text-secondary hover:bg-bg-tertiary hover:text-text-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`w-2 h-2 rounded-full shrink-0 ${
|
className={`w-2 h-2 rounded-full shrink-0 ${
|
||||||
drone.status === 'available'
|
drone.status === "available"
|
||||||
? 'bg-green-500'
|
? "bg-green-500"
|
||||||
: 'bg-yellow-500'
|
: "bg-yellow-500"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
<div className="text-sm truncate">{drone.hostname}</div>
|
<div className="text-sm truncate">{drone.hostname}</div>
|
||||||
{drone.workspaceDir && (
|
{drone.workspaceDir && (
|
||||||
<div className={`text-xs truncate ${
|
<div
|
||||||
|
className={`text-xs truncate ${
|
||||||
selectedDrone?._id === drone._id
|
selectedDrone?._id === drone._id
|
||||||
? 'text-white/70'
|
? "text-white/70"
|
||||||
: 'text-text-muted'
|
: "text-text-muted"
|
||||||
}`}>
|
}`}
|
||||||
{drone.workspaceDir.length > 24
|
>
|
||||||
? '...' + drone.workspaceDir.slice(-21)
|
{drone.workspaceDir.length > 24
|
||||||
: drone.workspaceDir}
|
? "..." + drone.workspaceDir.slice(-21)
|
||||||
</div>
|
: drone.workspaceDir}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</div>
|
||||||
))}
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -239,21 +248,42 @@ interface HomeProps {
|
|||||||
|
|
||||||
export default function Home({ user }: HomeProps) {
|
export default function Home({ user }: HomeProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(null);
|
const [selectedDrone, setSelectedDrone] = useState<DroneRegistration | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center bg-bg-primary p-8">
|
<div className="relative flex-1 flex bg-bg-primary overflow-hidden">
|
||||||
<AnsiLogo />
|
<div className="absolute inset-0">
|
||||||
|
<GadgetGrid />
|
||||||
|
</div>
|
||||||
|
<div className="relative z-10 flex items-center justify-center p-8">
|
||||||
|
<SystemReady />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const mainContent = selectedDrone ? (
|
||||||
<div className="flex-1 flex bg-bg-primary">
|
<div className="relative z-10 flex-1 flex">
|
||||||
{selectedDrone ? (
|
<DroneInspector
|
||||||
<DroneInspector drone={selectedDrone} onClose={() => setSelectedDrone(null)} />
|
drone={selectedDrone}
|
||||||
) : (
|
onClose={() => setSelectedDrone(null)}
|
||||||
|
/>
|
||||||
|
<DashboardSidebar
|
||||||
|
onNavigate={(view) => {
|
||||||
|
if (view === "project" && typeof navigate === "function") {
|
||||||
|
navigate("/projects");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
selectedDrone={selectedDrone}
|
||||||
|
onSelectDrone={setSelectedDrone}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="relative z-10 flex-1 flex flex-col">
|
||||||
|
<div className="flex-1 flex">
|
||||||
<div className="flex-1 flex items-center justify-center p-8">
|
<div className="flex-1 flex items-center justify-center p-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="text-2xl font-semibold mb-4">
|
<h1 className="text-2xl font-semibold mb-4">
|
||||||
@ -275,17 +305,25 @@ export default function Home({ user }: HomeProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<DashboardSidebar
|
||||||
|
onNavigate={(view) => {
|
||||||
<DashboardSidebar
|
if (view === "project" && typeof navigate === "function") {
|
||||||
onNavigate={(view) => {
|
navigate("/projects");
|
||||||
if (view === 'project' && typeof navigate === 'function') {
|
}
|
||||||
navigate('/projects');
|
}}
|
||||||
}
|
selectedDrone={selectedDrone}
|
||||||
}}
|
onSelectDrone={setSelectedDrone}
|
||||||
selectedDrone={selectedDrone}
|
/>
|
||||||
onSelectDrone={setSelectedDrone}
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex-1 flex bg-bg-primary overflow-hidden">
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<GadgetGrid />
|
||||||
|
</div>
|
||||||
|
{mainContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -29,6 +29,8 @@
|
|||||||
"@gadget/ai": "workspace:*",
|
"@gadget/ai": "workspace:*",
|
||||||
"@gadget/api": "workspace:*",
|
"@gadget/api": "workspace:*",
|
||||||
"@gadget/config": "workspace:*",
|
"@gadget/config": "workspace:*",
|
||||||
|
"@react-three/drei": "^10.7.7",
|
||||||
|
"@react-three/fiber": "^9.6.1",
|
||||||
"ansicolor": "^2.0.3",
|
"ansicolor": "^2.0.3",
|
||||||
"bull": "^4.16.5",
|
"bull": "^4.16.5",
|
||||||
"chart.js": "^4.5.0",
|
"chart.js": "^4.5.0",
|
||||||
@ -63,6 +65,7 @@
|
|||||||
"slug": "^11.0.1",
|
"slug": "^11.0.1",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"socket.io-client": "^4.8.3",
|
"socket.io-client": "^4.8.3",
|
||||||
|
"three": "^0.184.0",
|
||||||
"uikit": "^3.23.11",
|
"uikit": "^3.23.11",
|
||||||
"undici": "^8.1.0",
|
"undici": "^8.1.0",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0"
|
||||||
|
|||||||
532
pnpm-lock.yaml
532
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user