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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user