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