Polish map & UI into a command-center war-room; rewire live LLM endpoint
Map (WorldMap): - Keep the near-black OpenFreeMap `dark` basemap (verified HTTP 200; the suggested dark-matter / dark-outdoors / liberty-dark variants 404 on that host) so the choropleth nation colors pop - Stable per-nation fills at 0.8 opacity, thin subtle borders, and a brighter thicker amber border for the player nation so "this is me" is always obvious - Hover brightens a region and shows a color-chipped owner + key stats popup; click opens a full dossier popup and focuses the left panel on that nation - Transient, self-cleaning flash (pulsing amber outline) over regions whose ownership changed this turn - Bottom-left legend auto-generated from the region map; per-scenario initial camera (Europe for the WWII scenarios, wider for Cold War / Modern / Aegis) Command-center UI: - New header bar: scenario title, prominent turn + date, the player nation (name + color chip), and a live Idle / "Simulating…" status indicator - 3-column operations-console grid: nation dossier (left) / dominant map (center) / command deck + event feed (right); stacks map-first on mobile - EventFeed: one card per event with a category icon, a colored outcome badge (success/failure/partial/neutral), a nation-colored accent edge, and the turn + date; new events slide in and flash at the top - NationPanel: value-colored animated stat bars (red → green), territory as color-coded region chips, and diplomacy as a list with a colored indicator per relation (ally/rival/war/neutral) - ActionPanel: a "Your move" composer with one prominent Send button and a spinner + "turns can take a few seconds" hint while a turn resolves - sonner error toasts: a 502 / model-down surfaces as "AI temporarily unavailable — try again" Live LLM rewire (server, previously uncommitted): - Default endpoint https://ai.kkhost.pl/v1 (vLLM, no API key); reasoning model qwen3.8-27b; always send a generous max_tokens (4000) so it finishes its chain-of-thought before emitting content; read only `content`, ignore `reasoning`. docker-compose + AGENTS.md updated to match. Tests: the deterministic Playwright e2e gains assertions for the dossier stat bars, the map legend (all nations), and the event-card type icon + outcome badge; it stays fully deterministic under LLM_STUB=1.
This commit is contained in:
@@ -1,7 +1,32 @@
|
||||
import {
|
||||
DollarSign,
|
||||
Flame,
|
||||
FlaskConical,
|
||||
Handshake,
|
||||
Landmark,
|
||||
Palette,
|
||||
Radar,
|
||||
Swords,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from './ui/badge';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { EVENT_COLORS, OUTCOME_COLORS } from '@/lib/colors';
|
||||
import type { EventEntry, Nation } from '@shared/types';
|
||||
import { nationColor } from '@shared/protocol';
|
||||
import type { EventEntry, EventType, Nation } from '@shared/types';
|
||||
|
||||
/** One icon per event category, so a type is readable without reading. */
|
||||
const EVENT_ICONS: Record<EventType, LucideIcon> = {
|
||||
military: Swords,
|
||||
diplomatic: Handshake,
|
||||
economic: DollarSign,
|
||||
political: Landmark,
|
||||
social: Users,
|
||||
disaster: Flame,
|
||||
scientific: FlaskConical,
|
||||
cultural: Palette,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
events: EventEntry[];
|
||||
@@ -9,54 +34,80 @@ interface Props {
|
||||
}
|
||||
|
||||
/**
|
||||
* Chronological event log (newest first). Each entry shows the turn it resolved,
|
||||
* its date, a color-coded type, a headline, the narrative, the nation it concerns,
|
||||
* and the outcome. Scrolls internally; the parent supplies a bounded height.
|
||||
* A scrolling event log, newest first. Each event is a card with a category
|
||||
* icon, a color-coded outcome badge, a nation-colored accent edge, and the turn
|
||||
* + date it resolved on. New events slide in and flash briefly at the top.
|
||||
*/
|
||||
export default function EventFeed({ events, nations }: Props) {
|
||||
const latestFirst = [...events].reverse();
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col rounded-xl border bg-card" data-testid="event-feed-panel">
|
||||
<div className="flex items-center justify-between px-4 pt-4">
|
||||
<h2 className="text-sm font-semibold">Event feed</h2>
|
||||
<div
|
||||
className="flex h-full min-h-0 flex-col rounded-xl border bg-card"
|
||||
data-testid="event-feed-panel"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b px-4 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Radar className="size-4 text-primary" />
|
||||
<h2 className="text-sm font-semibold tracking-wide">Event feed</h2>
|
||||
</div>
|
||||
<Badge variant="secondary" className="text-xs tabular-nums">
|
||||
{events.length}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="mx-4 mb-4 min-h-0 flex-1">
|
||||
<ScrollArea className="mx-3 mb-3 min-h-0 flex-1">
|
||||
<div className="space-y-2 pr-1" data-testid="event-feed">
|
||||
{latestFirst.length === 0 && (
|
||||
<p className="rounded-md border border-dashed p-3 text-sm text-muted-foreground">
|
||||
<div className="mt-3 rounded-md border border-dashed p-4 text-center text-sm text-muted-foreground">
|
||||
No events yet. Take an action, send a message, or jump forward to move the world.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{latestFirst.map((e) => {
|
||||
const nation = nations[e.nation];
|
||||
const color = EVENT_COLORS[e.type] ?? '#94a3b8';
|
||||
const outcome = OUTCOME_COLORS[e.outcome] ?? '#94a3b8';
|
||||
// Accent edge uses the nation's stable choropleth color (falls back to
|
||||
// the type color if the nation is missing — never crashes).
|
||||
const accent = nation ? nationColor(e.nation) : EVENT_COLORS[e.type] ?? '#94a3b8';
|
||||
const typeColor = EVENT_COLORS[e.type] ?? '#94a3b8';
|
||||
const outcomeColor = OUTCOME_COLORS[e.outcome] ?? '#94a3b8';
|
||||
const Icon = EVENT_ICONS[e.type] ?? Landmark;
|
||||
return (
|
||||
<article key={e.id} className="rounded-lg border bg-card/50 p-2.5" data-testid="event-item">
|
||||
<div className="mb-1 flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span className="font-medium tabular-nums">T{e.turn}</span>
|
||||
<article
|
||||
key={e.id}
|
||||
className="event-new relative overflow-hidden rounded-lg border bg-card/50 p-3 pl-4"
|
||||
style={{ borderLeft: `3px solid ${accent}` }}
|
||||
data-testid="event-item"
|
||||
>
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span className="font-semibold tabular-nums text-foreground/70">T{e.turn}</span>
|
||||
<span>·</span>
|
||||
<span className="tabular-nums">{e.date}</span>
|
||||
<span className="ml-auto inline-flex items-center gap-1.5">
|
||||
<span className="inline-block size-2 rounded-full" style={{ background: color }} />
|
||||
{e.type}
|
||||
<span
|
||||
className="ml-auto inline-flex items-center gap-1.5 rounded-full px-1.5 py-0.5"
|
||||
style={{ color: typeColor, background: `${typeColor}1a` }}
|
||||
>
|
||||
<Icon className="size-3" data-testid="event-type-icon" aria-hidden />
|
||||
<span data-testid="event-type" className="capitalize">
|
||||
{e.type}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium leading-snug" style={{ color }}>
|
||||
{e.title}
|
||||
</p>
|
||||
<p className="mt-0.5 text-sm leading-snug text-foreground/80">{e.text}</p>
|
||||
<div className="mt-1.5 flex items-center gap-2 text-[11px] text-muted-foreground">
|
||||
<span>{nation?.name ?? e.nation}</span>
|
||||
<p className="text-sm font-medium leading-snug">{e.title}</p>
|
||||
<p className="mt-1 text-[13px] leading-snug text-foreground/75">{e.text}</p>
|
||||
<div className="mt-2 flex items-center gap-2 text-[11px]">
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<span className="inline-block size-2 shrink-0 rounded-full" style={{ background: accent }} />
|
||||
<span className="truncate text-muted-foreground">{nation?.name ?? e.nation}</span>
|
||||
</span>
|
||||
<span
|
||||
className="ml-auto rounded px-1.5 py-0.5 font-medium capitalize"
|
||||
style={{ background: `${outcome}22`, color: outcome }}
|
||||
className="ml-auto shrink-0 rounded-md px-2 py-0.5 font-semibold capitalize"
|
||||
style={{
|
||||
background: `${outcomeColor}22`,
|
||||
color: outcomeColor,
|
||||
border: `1px solid ${outcomeColor}55`,
|
||||
}}
|
||||
data-testid="event-outcome"
|
||||
>
|
||||
{e.outcome}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user