Add rulebook RAG pipeline and LLM-driven game setup wizard

RAG: switch Postgres to pgvector, chunk and embed the three D&D
rulebooks locally via sentence-transformers, and retrieve relevant
excerpts per DM turn (query = latest player message) to ground the
system prompt. Retrieval runs off the event loop and is capped by a
relevance threshold and a max character budget so it can't blow up
context size or cost.

Game setup wizard: creating a game now opens a short chat where the
DM asks about genre, length, and the player's experience level, then
proposes a name and description via a tool call. The player can edit
both before creating the game. Stateless endpoint — the frontend
carries the conversation, no DB needed since the game doesn't exist
yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Thorsten
2026-08-31 20:03:05 +02:00
parent f37dc9fa76
commit 419f5e3a89
26 changed files with 82471 additions and 51 deletions
+4 -43
View File
@@ -1,8 +1,9 @@
import { FormEvent, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { createGame, Game, listGames } from "../api/games";
import { Game, listGames } from "../api/games";
import GameCard from "../components/GameCard";
import GameSetupChat from "../components/GameSetupChat";
export default function GamesList() {
const navigate = useNavigate();
@@ -10,10 +11,6 @@ export default function GamesList() {
const [loading, setLoading] = useState(true);
const [showCreate, setShowCreate] = useState(false);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [error, setError] = useState<string | null>(null);
async function load() {
setLoading(true);
try {
@@ -27,17 +24,6 @@ export default function GamesList() {
load();
}, []);
async function handleCreate(e: FormEvent) {
e.preventDefault();
setError(null);
try {
const game = await createGame(name, description);
navigate(`/games/${game.id}`);
} catch {
setError("Spiel konnte nicht erstellt werden.");
}
}
return (
<div className="mx-auto max-w-3xl px-4 py-8">
<div className="mb-6 flex items-center justify-between">
@@ -52,32 +38,7 @@ export default function GamesList() {
</div>
</div>
{showCreate && (
<form onSubmit={handleCreate} className="mb-6 flex flex-col gap-3 rounded-lg border border-slate-800 bg-slate-900 p-4">
<input
required
placeholder="Name des Spiels"
value={name}
onChange={(e) => setName(e.target.value)}
className="rounded bg-slate-800 px-3 py-2 outline-none focus:ring-2 focus:ring-amber-400"
/>
<textarea
placeholder="Kurzbeschreibung (Genre, Welt, ...)"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="rounded bg-slate-800 px-3 py-2 outline-none focus:ring-2 focus:ring-amber-400"
rows={3}
/>
<button
type="submit"
className="self-start rounded bg-amber-500 px-3 py-2 text-sm font-semibold text-slate-950 hover:bg-amber-400"
>
Erstellen
</button>
</form>
)}
{error && <p className="mb-4 text-sm text-red-400">{error}</p>}
{showCreate && <GameSetupChat onCreated={(game) => navigate(`/games/${game.id}`)} />}
{loading ? (
<p className="text-slate-400">Lädt…</p>