working through the plan + UI/ UX
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user