File size: 5,134 Bytes
af37c1f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import type { ZoneObs, Action, PyTorchScore } from '../types'

interface Props {
  zones: ZoneObs[]
  action?: Action | null
  falseSOSZones?: string[]
  pytorchScores?: PyTorchScore[]
  compact?: boolean
}

function severityColor(z: ZoneObs, isFalseSOS: boolean, isCompleted: boolean): string {
  if (isFalseSOS) return 'border-zinc-600 bg-zinc-900 opacity-60'
  if (isCompleted) return 'border-green-600 bg-green-950'
  if (z.severity >= 0.8) return 'border-red-500 bg-red-950'
  if (z.severity >= 0.6) return 'border-orange-500 bg-orange-950'
  return 'border-yellow-600 bg-yellow-950'
}

function severityBadgeColor(z: ZoneObs): string {
  if (z.severity >= 0.8) return 'bg-red-500'
  if (z.severity >= 0.6) return 'bg-orange-500'
  return 'bg-yellow-500'
}

export function DisasterMap({ zones, action, falseSOSZones = [], pytorchScores = [], compact = false }: Props) {
  const scoreMap = Object.fromEntries(pytorchScores.map(s => [s.zone_id, s]))
  const actionTarget = action?.to_zone ?? action?.from_zone

  const pad = compact ? 'p-2' : 'p-3'
  const gap = compact ? 'gap-2' : 'gap-3'
  const cols = zones.length <= 1 ? 'grid-cols-1' :
    zones.length <= 3 ? 'grid-cols-3' :
    zones.length <= 5 ? 'grid-cols-3' :
    'grid-cols-5'

  return (
    <div className={`grid ${cols} ${gap}`}>
      {zones.map(z => {
        const isFalseSOS = falseSOSZones.includes(z.zone_id)
        const isCompleted = z.casualties_remaining === 0 && z.supply_gap === 0 && !isFalseSOS
        const isTarget = actionTarget === z.zone_id
        const score = scoreMap[z.zone_id]

        const borderClass = severityColor(z, isFalseSOS, isCompleted)
        const pulseClass = isTarget
          ? (isCompleted ? 'zone-pulse-green' : 'zone-pulse')
          : ''

        return (
          <div
            key={z.zone_id}
            className={`border rounded-lg ${pad} ${borderClass} ${pulseClass} transition-all duration-300 relative`}
          >
            {/* Zone header */}
            <div className="flex items-center justify-between mb-1">
              <span className="font-bold text-white mono text-sm">Zone {z.zone_id}</span>
              <div className="flex gap-1 items-center">
                {z.road_blocked && !isFalseSOS && (
                  <span title="Road blocked" className="text-xs">🚧</span>
                )}
                {z.sos_active && !isFalseSOS && (
                  <span title="SOS active" className="text-xs">πŸ†˜</span>
                )}
                {isFalseSOS && (
                  <span title="False SOS" className="text-xs">⚠️</span>
                )}
                {isCompleted && (
                  <span title="Completed" className="text-xs">βœ…</span>
                )}
                {isTarget && action?.action === 'airlift' && (
                  <span className="text-xs">🚁</span>
                )}
              </div>
            </div>

            {isFalseSOS ? (
              <p className="text-xs text-zinc-500 italic">False SOS β€” no casualties</p>
            ) : (
              <>
                {!compact && (
                  <div className={`h-1 rounded mb-2 ${severityBadgeColor(z)}`}
                    style={{ width: `${Math.round(z.severity * 100)}%` }} />
                )}
                <div className="space-y-0.5">
                  <div className="flex justify-between text-xs">
                    <span className="text-zinc-400">Casualties</span>
                    <span className={`mono font-medium ${z.casualties_remaining > 0 ? 'text-red-400' : 'text-green-400'}`}>
                      {z.casualties_remaining}
                    </span>
                  </div>
                  <div className="flex justify-between text-xs">
                    <span className="text-zinc-400">Supply gap</span>
                    <span className={`mono font-medium ${z.supply_gap > 0 ? 'text-orange-400' : 'text-green-400'}`}>
                      {z.supply_gap}
                    </span>
                  </div>
                  {!compact && (
                    <div className="flex justify-between text-xs">
                      <span className="text-zinc-400">Teams</span>
                      <span className="mono font-medium text-blue-400">{z.teams_present}</span>
                    </div>
                  )}
                  {score && !compact && (
                    <div className="flex justify-between text-xs mt-1 pt-1 border-t border-zinc-700">
                      <span className="text-zinc-500">PyTorch</span>
                      <span className={`mono text-xs ${score.score > 0.6 ? 'text-purple-400' : score.score > 0.3 ? 'text-zinc-300' : 'text-zinc-600'}`}>
                        {score.score.toFixed(3)}
                      </span>
                    </div>
                  )}
                </div>
              </>
            )}

            {/* Action highlight overlay */}
            {isTarget && (
              <div className="absolute inset-0 rounded-lg border-2 border-white/20 pointer-events-none" />
            )}
          </div>
        )
      })}
    </div>
  )
}