History: AI-driven alternate-history simulator
Pax Historia-style sim where the LLM is the world engine.
- React 19 + Vite 6 + Tailwind v4 + shadcn/ui frontend
- Express single-port backend (/api + /sse + built client + SPA fallback)
- SQLite (better-sqlite3) persistence, one row per game
- MapLibre choropleth map (OpenFreeMap dark + ISO_A3 regions)
- Defensive turn loop: strip fences, balanced-{} extract, parse, clamp 0-100,
retry-once nudge, generic 'unclear' fallback; never crashes on bad output
- 5 seed presets (wwii-1939, coldwar-1955, modern-2025, no-wwii-1940, aegis-2400)
- LLM client over native fetch w/ injectable transport (mesh-bot pattern);
LLM_STUB=1 deterministic transport for tests/e2e
- Unit (vitest, 19) + deterministic Playwright e2e
- Multi-stage node:22-slim Docker + Traefik (history.dev.kkhost.pl:4321)
Co-Authored-By: OpenCode <opencode@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { Badge } from './ui/badge';
|
||||
import { Button } from './ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
|
||||
import type { PresetMeta } from '@shared/types';
|
||||
|
||||
interface Props {
|
||||
presets: PresetMeta[];
|
||||
loading?: boolean;
|
||||
onPlay: (presetId: string) => void;
|
||||
}
|
||||
|
||||
/** Home screen: a grid of scenario cards, each with a Play button. */
|
||||
export default function PresetPicker({ presets, loading, onPlay }: Props) {
|
||||
return (
|
||||
<div className="mx-auto max-w-6xl px-6 py-10">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold tracking-tight">History</h1>
|
||||
<p className="mt-2 max-w-2xl text-muted-foreground">
|
||||
An AI-driven alternate-history simulator. Pick a scenario and a nation, then shape the
|
||||
world turn by turn — the LLM is the world engine.
|
||||
</p>
|
||||
</header>
|
||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{presets.map((p) => (
|
||||
<Card
|
||||
key={p.id}
|
||||
className="flex flex-col transition hover:ring-1 hover:ring-primary/40"
|
||||
data-testid="preset-card"
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<CardTitle className="text-lg">{p.title}</CardTitle>
|
||||
<Badge variant="secondary" className="shrink-0 text-xs">
|
||||
{p.startDate}
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription className="text-sm leading-relaxed">{p.summary}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="mt-auto">
|
||||
<Button className="w-full" onClick={() => onPlay(p.id)} disabled={loading}>
|
||||
Play
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user