working through the plan + UI/ UX

This commit is contained in:
itsamejms
2026-07-12 22:13:43 +01:00
parent 5b256242be
commit a24f3615e0
38 changed files with 5295 additions and 328 deletions
+60 -10
View File
@@ -1,6 +1,11 @@
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
import { parseLlmJson, flattenValue } from "../lib/llm-parse";
import { GeneratedImage } from "./GeneratedImage";
import { addToLore } from "../lib/lore";
import { useToast } from "./Toast";
import { addGeneration, extractTitle, type Generation } from "../lib/generations";
import { usePrefillEffect } from "../lib/usePrefill";
interface ItemResponse {
name: string;
@@ -14,7 +19,12 @@ interface ItemResponse {
const RARITIES = ["Common", "Uncommon", "Rare", "Very Rare", "Legendary", "Artifact"];
const TYPES = ["Weapon", "Armor", "Potion", "Scroll", "Wondrous Item", "Ring", "Wand", "Staff"];
export function ItemForge() {
interface Props {
prefill?: Generation | null;
onPrefillConsumed?: () => void;
}
export function ItemForge({ prefill, onPrefillConsumed }: Props = {}) {
const [rarity, setRarity] = useState("Rare");
const [itemType, setItemType] = useState("Wondrous Item");
const [prompt, setPrompt] = useState("");
@@ -22,6 +32,21 @@ export function ItemForge() {
const [rawResponse, setRawResponse] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [artNonce, setArtNonce] = useState(0);
const { addToast } = useToast();
usePrefillEffect(prefill ?? null, "item", () => onPrefillConsumed?.(), (g) => {
const parsed = parseLlmJson<ItemResponse>(g.data);
if (parsed) {
setItem(parsed);
setRarity(parsed.rarity || "Rare");
setItemType(parsed.type || "Wondrous Item");
addToast(`Loaded "${g.title}" from history`, "info");
}
});
// ponytail: FLUX.2 renders readable text, so the item name is painted in-image.
const artPrompt = item ? `fantasy product shot of a ${item.rarity || rarity} ${item.type || itemType} named "${item.name || "magic item"}", ${flattenValue(item.description)}, dramatic side lighting, dark velvet background, 1024x1024` : null;
async function generate() {
setLoading(true);
@@ -49,6 +74,7 @@ The JSON must have exactly these keys:
system: "You are a creative D&D item designer. You MUST respond with ONLY valid JSON. No markdown fences, no code blocks, no explanation. Just the JSON object.",
temperature: 0.85,
max_tokens: 500,
ragQuery: `${rarity} ${itemType} ${prompt}`,
},
});
@@ -57,11 +83,21 @@ The JSON must have exactly these keys:
if (parsed) {
setItem(parsed);
addToast("Item forged", "success");
const title = extractTitle("item", result, `${parsed.rarity ?? rarity} ${parsed.type ?? itemType}`);
const source = `${parsed.rarity || rarity} ${parsed.type || itemType}${prompt ? `${prompt}` : ""}`;
void addGeneration({ kind: "item", title, data: result, source });
void addToLore(
`Item: ${parsed.name || itemType}`,
`${parsed.name || "Magic item"}${parsed.rarity || rarity} ${parsed.type || itemType}. ${flattenValue(parsed.description)}\nMechanics: ${flattenValue(parsed.mechanical)}\nLore: ${flattenValue(parsed.lore)}`,
);
} else {
setError("Could not parse LLM response as JSON. Raw response shown below.");
addToast("LLM returned an unparseable response", "error");
}
} catch (e) {
setError(String(e));
addToast(`Item generation failed: ${e}`, "error");
}
setLoading(false);
}
@@ -132,15 +168,29 @@ The JSON must have exactly these keys:
{item && (
<div className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-glass)] p-4 flex flex-col gap-3">
<div className="flex items-start justify-between">
<div>
<h3 className="font-heading text-lg font-bold" style={{ color: rarityColor[item.rarity] || "var(--color-gold-bright)" }}>
{item.name || "Unknown Item"}
</h3>
<div className="flex gap-2 text-xs text-[var(--color-text-dim)]">
<span>{item.rarity || rarity}</span>
<span></span>
<span>{item.type || itemType}</span>
<div className="flex gap-3">
<div className="shrink-0 w-24">
<GeneratedImage prompt={artPrompt} nonce={artNonce} aspect="aspect-square" />
<button
onClick={() => setArtNonce((n) => n + 1)}
className="mt-1 w-full text-[10px] text-[var(--color-gold-bright)] hover:text-[var(--color-gold-muted)] cursor-pointer transition-colors"
title="Regenerate item art"
>
new art
</button>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div>
<h3 className="font-heading text-lg font-bold" style={{ color: rarityColor[item.rarity] || "var(--color-gold-bright)" }}>
{item.name || "Unknown Item"}
</h3>
<div className="flex gap-2 text-xs text-[var(--color-text-dim)]">
<span>{item.rarity || rarity}</span>
<span></span>
<span>{item.type || itemType}</span>
</div>
</div>
</div>
</div>
</div>