UserSyncInterface / components /SimulationGraph.tsx
AUXteam's picture
Upload folder using huggingface_hub
e6db76d verified
import React, { useEffect, useRef, useState } from 'react';
import Plotly from 'plotly.js';
import { X, Linkedin, Globe, MapPin, User, Briefcase, Users } from 'lucide-react';
import Button from './ui/Button';
interface SimulationGraphProps {
isBuilding: boolean;
societyType: string;
onStartChat?: () => void;
}
const SimulationGraph: React.FC<SimulationGraphProps> = ({ isBuilding, societyType, onStartChat }) => {
const graphDiv = useRef<HTMLDivElement>(null);
const [selectedProfile, setSelectedProfile] = useState<{ x: number, y: number, data: any } | null>(null);
// Close popup if building starts
useEffect(() => {
if (isBuilding) setSelectedProfile(null);
}, [isBuilding]);
useEffect(() => {
if (!graphDiv.current || isBuilding) return;
// --- Dynamic Data Generation based on Society Type ---
const safeSocietyType = typeof societyType === 'string' ? societyType : '';
const isTech = safeSocietyType.includes('Tech') || safeSocietyType.includes('Founders');
const N = isTech ? 120 : 80;
const radius = isTech ? 0.18 : 0.22;
const nodes = [];
// Create nodes
for (let i = 0; i < N; i++) {
nodes.push({
x: Math.random(),
y: Math.random(),
connections: 0,
// Mock data for the popup
role: isTech ? ['Founder', 'CTO', 'Product Lead', 'VC'][Math.floor(Math.random() * 4)] : ['Journalist', 'Reader', 'Editor', 'Subscriber'][Math.floor(Math.random() * 4)],
location: ['New York, USA', 'London, UK', 'Berlin, DE', 'Paris, FR'][Math.floor(Math.random() * 4)]
});
}
// Create edges
const edgeX: (number | null)[] = [];
const edgeY: (number | null)[] = [];
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
const dx = nodes[i].x - nodes[j].x;
const dy = nodes[i].y - nodes[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < radius) {
nodes[i].connections++;
nodes[j].connections++;
edgeX.push(nodes[i].x, nodes[j].x, null);
edgeY.push(nodes[i].y, nodes[j].y, null);
}
}
}
const nodeX = nodes.map(n => n.x);
const nodeY = nodes.map(n => n.y);
const nodeColor = nodes.map(n => n.connections);
// --- Plotly Config ---
const edgeTrace = {
x: edgeX,
y: edgeY,
mode: 'lines',
line: { width: 0.5, color: '#4b5563' },
hoverinfo: 'none',
type: 'scatter'
};
const nodeTrace = {
x: nodeX,
y: nodeY,
mode: 'markers',
hoverinfo: 'none',
marker: {
showscale: false,
colorscale: isTech ? 'Electric' : 'Viridis',
color: nodeColor,
size: 10,
line: { width: 0 }
},
type: 'scatter'
};
const layout = {
showlegend: false,
hovermode: 'closest',
margin: { b: 0, l: 0, r: 0, t: 0 },
xaxis: { showgrid: false, zeroline: false, showticklabels: false, range: [-0.05, 1.05], fixedrange: true },
yaxis: { showgrid: false, zeroline: false, showticklabels: false, range: [-0.05, 1.05], fixedrange: true },
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
autosize: true,
dragmode: false
};
const config = {
staticPlot: false,
displayModeBar: false,
responsive: true
};
// @ts-ignore
Plotly.newPlot(graphDiv.current, [edgeTrace, nodeTrace], layout, config).then((gd) => {
// @ts-ignore
gd.on('plotly_click', (data) => {
const point = data.points[0];
if (point) {
const nodeIndex = point.pointNumber;
const nodeData = nodes[nodeIndex];
setSelectedProfile({
x: point.x,
y: point.y,
data: nodeData
});
}
});
});
}, [isBuilding, societyType]);
return (
<div className="relative w-full h-full bg-black">
{/* Loading Overlay */}
{isBuilding && (
<div className="absolute inset-0 z-50 flex flex-col items-center justify-center bg-black/80 backdrop-blur-sm transition-opacity duration-300">
<div className="w-16 h-16 border-4 border-teal-900 border-t-teal-500 rounded-full animate-spin mb-4"></div>
<p className="text-teal-400 font-mono animate-pulse">Constructing Focus Group Graph...</p>
</div>
)}
{/* The Graph */}
<div ref={graphDiv} className="w-full h-full" />
{/* Profile Popup */}
{selectedProfile && !isBuilding && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 bg-gray-900/90 backdrop-blur-md border border-gray-700 rounded-2xl shadow-2xl overflow-hidden z-40 animate-in fade-in zoom-in-95 duration-200">
{/* Header */}
<div className="p-4 border-b border-gray-800 flex justify-between items-start">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-teal-400 to-blue-600 flex items-center justify-center text-white font-bold text-lg">
{selectedProfile.data.role[0]}
</div>
<div>
<h3 className="text-white font-semibold text-sm">{selectedProfile.data.role}</h3>
<p className="text-gray-400 text-xs">Head of Product at BrightCore</p>
</div>
</div>
<button
onClick={() => setSelectedProfile(null)}
className="text-gray-500 hover:text-white"
>
<X size={16} />
</button>
</div>
{/* Body */}
<div className="p-4 space-y-4">
<div className="flex items-center gap-2 text-xs text-gray-400">
<span>Built from</span>
<Linkedin size={14} className="text-[#0077b5]" />
<Globe size={14} className="text-gray-300" />
</div>
<div className="flex flex-wrap gap-2">
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
<MapPin size={12} /> {selectedProfile.data.location}
</div>
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
<User size={12} /> Millennial
</div>
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
<Briefcase size={12} /> Mid Level
</div>
<div className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg bg-gray-800 border border-gray-700 text-xs text-gray-300">
<Users size={12} /> Creative & Design
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 pt-0">
<Button
className="w-full text-sm py-2"
onClick={onStartChat}
>
Start Conversation
</Button>
</div>
</div>
)}
</div>
);
};
export default SimulationGraph;