import { useEffect, useState } from "react"; import { Box, Flex, Heading, Text, Spinner, SimpleGrid, Badge, Wrap, Tag, Divider, Table, Thead, Tbody, Tr, Th, Td, Link, Avatar, Icon, HStack, } from "@chakra-ui/react"; import { CheckIcon, CloseIcon, ExternalLinkIcon } from '@chakra-ui/icons'; import { ResponsiveContainer, BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts"; import { MapContainer, TileLayer, Marker, Popup, useMap, } from "react-leaflet"; import L from "leaflet"; import "leaflet/dist/leaflet.css"; import type { ProjectDetailsProps, OrganizationLocation } from "../hooks/types"; import markerIconPng from "leaflet/dist/images/marker-icon.png"; import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png"; import markerShadow from "leaflet/dist/images/marker-shadow.png"; const customIcon = new L.Icon({ iconUrl: markerIconPng, iconRetinaUrl: markerIcon2x, shadowUrl: markerShadow, iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], }); function ResizeMap({ count }: { count: number }) { const map = useMap(); useEffect(() => { map?.invalidateSize(); }, [count, map]); return null; } export default function ProjectDetails({ project,}: ProjectDetailsProps) { // fetch organization locations const [orgLocations, setOrgLocations] = useState([]); const [loadingOrgs, setLoadingOrgs] = useState(true); const [loadingPlot, setLoadingPlot] = useState(true); useEffect(() => { if (!project) return; setLoadingOrgs(true); fetch(`/api/project/${project.id}/organizations`) .then((r) => r.json()) .then((data) => Array.isArray(data) ? setOrgLocations(data) : console.error(data)) .catch(console.error) .finally(() => setLoadingOrgs(false)); }, [project]); if (!project) { return ( No project selected. ); } const shapData = project.explanations; const predicted = project.predicted_label; const probability = project.predicted_prob; // Map center fallback const validOrgs = orgLocations.filter( (o) => typeof o.latitude === "number" && !Number.isNaN(o.latitude) && typeof o.longitude === "number" && !Number.isNaN(o.longitude) ); // pick a default center (or fallback to [0,0]) const center: [number, number] = validOrgs.length ? [validOrgs[0].latitude, validOrgs[0].longitude] : [51.505, -0.09]; // format numbers with two decimals const fmtNum = (num: number | null | undefined): string => num != null ? num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '-'; return ( {/* Left: Details */} {project.title} {project.status} ID {project.id} Acronym {project.acronym} Start Date{project.startDate} End Date{project.endDate} Funding (EC max)€{fmtNum(project.ecMaxContribution)} Total Cost€{fmtNum(project.totalCost)} Funding Scheme{project.fundingScheme} Legal Basis {project.legalBasis} Framework Programme {project.frameworkProgramme} Objective {project.objective} {(project.list_euroSciVocTitle ?? []).length > 0 && ( EuroSciVoc Titles {(project.list_euroSciVocTitle ?? []).map((t) => ( {t} ))} )} {(project.list_euroSciVocPath ?? []).length > 0 && ( EuroSciVoc Paths {(project.list_euroSciVocPath ?? []).map((p) => ( {p} ))} )} {project.publications && Object.keys(project.publications).length > 0 && ( Publications {Object.entries(project.publications).map(([type, count]) => ( ))}
Type Count
{type} {count}
)} {orgLocations.length > 0 && ( <> Participating Organizations {loadingOrgs ? ( ) : ( {orgLocations.map((o, i) => ( ))}
Name Location SME Role Contribution Activity Type
{o.orgURL ? ( {o.name} ) : ( {o.name} )} {`${o.city || '-'}, ${o.country}`} {o.sme ? : } {o.role} €{fmtNum(o.contribution)} {o.activityType}
)} {!loadingOrgs && validOrgs.length > 0 &&( {validOrgs.map((org, i) => ( {org.name} {org.country} ))} )} )}
{/* Right: Model Explanation */} Model Prediction & Explanation {shapData?.length ? ( <> Predicted Label: {predicted === 1 ? 'Terminated' : 'Closed'} Probability: {predicted === 1 ? (probability * 100).toFixed(2) : ((1-probability) * 100).toFixed(2) }% {shapData.map((entry, index) => ( = 0 ? "#003399" : "#FFCC00"} /> ))} Each bar shows how much that feature pushed the model's prediction. Positive bars increase the chance of termination; Negative bars decrease it. ) : ( )}
); }