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:
itsamejms
2026-06-28 23:03:10 +01:00
parent 9b0483a2ab
commit b23ab61841
6 changed files with 192 additions and 63 deletions
+10 -8
View File
@@ -1,3 +1,4 @@
import { parseLlmJson, ensureArray, flattenValue } from "../lib/llm-parse";
import { useState } from "react";
import { invoke } from "@tauri-apps/api/core";
@@ -26,21 +27,22 @@ export function EncounterBuilder() {
prompt: `Generate a D&D 5e encounter for a party of ${partySize} level-${partyLevel} characters in a ${terrain.toLowerCase()} setting.
Provide the response as a JSON object with exactly these keys:
- "monsters": an array of 2-4 monster descriptions with quantities (e.g. "3x Goblin Scouts")
- "monsters": an array of 2-4 monster descriptions with quantities (e.g. "3x Goblin Scouts") — each must be a string, not an object
- "terrain": a brief terrain description with environmental features
- "difficulty": one of Easy/Medium/Hard/Deadly
- "loot": a brief treasure description`,
system: "You are a D&D encounter designer. Always respond with valid JSON only.",
- "loot": a brief treasure description
IMPORTANT: Return ONLY a raw JSON object. NO markdown fences, NO code blocks, NO extra text. Start with { and end with }.`,
system: "You are a D&D encounter designer. You MUST respond with ONLY valid JSON. No markdown fences, no code blocks, no explanation. Just the JSON object.",
temperature: 0.9,
max_tokens: 400,
},
});
const jsonMatch = result.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]) as Encounter;
const parsed = parseLlmJson<Encounter>(result);
if (parsed) {
setEncounter(parsed);
} else {
setError("LLM did not return valid JSON. Raw:\n" + result);
setError("Could not parse LLM response. Try again or check your LLM connection.");
}
} catch (e) {
setError(String(e));
@@ -116,7 +118,7 @@ Provide the response as a JSON object with exactly these keys:
<div>
<span className="text-[var(--color-text-secondary)] text-[10px] font-medium">Monsters</span>
<ul className="list-disc list-inside text-[var(--color-text-primary)] text-xs mt-0.5">
{encounter.monsters?.map((m, i) => <li key={i}>{m}</li>)}
{ensureArray(encounter.monsters).map((m, i) => <li key={i}>{flattenValue(m)}</li>)}
</ul>
</div>