fix: robust LLM JSON parsing across all AI components
- Created shared lib/llm-parse.ts with extractJson(), parseLlmJson(), flattenValue(), and ensureArray() utilities - Strips markdown code fences (\`\`\`json ... \`\`\`) before parsing - Handles nested objects where strings were expected (flattens them) - Handles arrays that come back as strings or objects - Updated all AI prompts to explicitly request raw JSON with no fences - All 5 AI components now use parseLlmJson() instead of raw regex - ItemForge shows raw LLM response in expandable <details> when parsing fails - Added 'IMPORTANT: Return ONLY raw JSON' instruction to all prompts - Full production build passing
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { useState } from "react";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { parseLlmJson, flattenValue } from "../lib/llm-parse";
|
||||
|
||||
interface ItemResponse {
|
||||
name: string;
|
||||
rarity: string;
|
||||
type: string;
|
||||
description: string;
|
||||
mechanical: string;
|
||||
mechanical: string | Record<string, unknown>;
|
||||
lore: string;
|
||||
}
|
||||
|
||||
@@ -18,6 +19,7 @@ export function ItemForge() {
|
||||
const [itemType, setItemType] = useState("Wondrous Item");
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [item, setItem] = useState<ItemResponse | null>(null);
|
||||
const [rawResponse, setRawResponse] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@@ -25,6 +27,7 @@ export function ItemForge() {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
setItem(null);
|
||||
setRawResponse(null);
|
||||
try {
|
||||
const result = await invoke<string>("generate", {
|
||||
req: {
|
||||
@@ -33,23 +36,29 @@ Rarity: ${rarity}
|
||||
Type: ${itemType}
|
||||
${prompt ? `Additional details: ${prompt}` : ""}
|
||||
|
||||
Provide the response as a JSON object with exactly these keys:
|
||||
- "name": the item's evocative name
|
||||
- "rarity": "${rarity}"
|
||||
- "type": "${itemType}"
|
||||
- "description": a vivid 2-3 sentence physical description
|
||||
- "mechanical": the item's game mechanics (what it does, charges, etc.)
|
||||
- "lore": a 1-2 sentence piece of lore or history about the item`,
|
||||
system: "You are a creative D&D item designer. Always respond with valid JSON only.",
|
||||
IMPORTANT: Return ONLY a raw JSON object, with NO markdown fences, NO code blocks, NO extra text.
|
||||
The response must start with { and end with }.
|
||||
|
||||
The JSON must have exactly these keys:
|
||||
- "name": the item's evocative name (string)
|
||||
- "rarity": "${rarity}" (string)
|
||||
- "type": "${itemType}" (string)
|
||||
- "description": a vivid 2-3 sentence physical description (string)
|
||||
- "mechanical": the item's game mechanics as a single descriptive paragraph (string, NOT an object)
|
||||
- "lore": a 1-2 sentence piece of lore or history (string)`,
|
||||
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,
|
||||
},
|
||||
});
|
||||
const jsonMatch = result.match(/\{[\s\S]*\}/);
|
||||
if (jsonMatch) {
|
||||
setItem(JSON.parse(jsonMatch[0]) as ItemResponse);
|
||||
|
||||
setRawResponse(result);
|
||||
const parsed = parseLlmJson<ItemResponse>(result);
|
||||
|
||||
if (parsed) {
|
||||
setItem(parsed);
|
||||
} else {
|
||||
setError("LLM did not return valid JSON.\n" + result);
|
||||
setError("Could not parse LLM response as JSON. Raw response shown below.");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
@@ -66,6 +75,9 @@ Provide the response as a JSON object with exactly these keys:
|
||||
Artifact: "#e6cc80",
|
||||
};
|
||||
|
||||
const mechanicalText = item ? flattenValue(item.mechanical) : "";
|
||||
const hasParseError = !item && rawResponse;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
@@ -123,32 +135,48 @@ Provide the response as a JSON object with exactly these keys:
|
||||
<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}
|
||||
{item.name || "Unknown Item"}
|
||||
</h3>
|
||||
<div className="flex gap-2 text-xs text-[var(--color-text-dim)]">
|
||||
<span>{item.rarity}</span>
|
||||
<span>{item.rarity || rarity}</span>
|
||||
<span>•</span>
|
||||
<span>{item.type}</span>
|
||||
<span>{item.type || itemType}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--color-text-secondary)] text-[10px] font-medium uppercase tracking-wider">Description</span>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mt-0.5 leading-relaxed">{item.description}</p>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mt-0.5 leading-relaxed">
|
||||
{flattenValue(item.description)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-glass)] p-3">
|
||||
<span className="text-[var(--color-gold-bright)] text-[10px] font-medium uppercase tracking-wider">Mechanics</span>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mt-0.5">{item.mechanical}</p>
|
||||
<p className="text-[var(--color-text-primary)] text-sm mt-0.5">{mechanicalText}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--color-text-secondary)] text-[10px] font-medium uppercase tracking-wider">Lore</span>
|
||||
<p className="text-[var(--color-text-secondary)] text-sm mt-0.5 italic">{item.lore}</p>
|
||||
<p className="text-[var(--color-text-secondary)] text-sm mt-0.5 italic">
|
||||
{flattenValue(item.lore)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show raw response if JSON parsing failed */}
|
||||
{hasParseError && (
|
||||
<details className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-subtle)]">
|
||||
<summary className="px-3 py-2 text-xs text-[var(--color-text-dim)] cursor-pointer hover:text-[var(--color-text-secondary)]">
|
||||
Raw LLM response (click to expand)
|
||||
</summary>
|
||||
<pre className="px-3 pb-3 text-xs text-[var(--color-text-secondary)] overflow-x-auto whitespace-pre-wrap">
|
||||
{rawResponse}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user