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
+23 -20
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";
@@ -28,20 +29,22 @@ export function WorldBuilder() {
Provide the response as a JSON object with exactly these keys:
- "name": the world's name
- "description": a 2-3 sentence overview
- "regions": an array of 3-4 region names with brief descriptions
- "landmarks": an array of 2-3 notable landmarks
- "conflicts": an array of 2-3 major conflicts or tensions
- "cultures": an array of 2-3 distinct cultures or peoples`,
system: "You are a creative world-building DM. Always respond with valid JSON only.",
- "regions": an array of 3-4 region names with brief descriptions (each must be a string)
- "landmarks": an array of 2-3 notable landmarks (each must be a string)
- "conflicts": an array of 2-3 major conflicts or tensions (each must be a string)
- "cultures": an array of 2-3 distinct cultures or peoples (each must be a string)
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 creative world-building DM. You MUST respond with ONLY valid JSON. No markdown fences, no code blocks, no explanation. Just the JSON object.",
temperature: 0.9,
max_tokens: 600,
},
});
const jsonMatch = result.match(/\{[\s\S]*\}/);
if (jsonMatch) {
setWorld(JSON.parse(jsonMatch[0]) as WorldResponse);
const parsed = parseLlmJson<WorldResponse>(result);
if (parsed) {
setWorld(parsed);
} else {
setError("LLM did not return valid JSON.\n" + result);
setError("Could not parse LLM response. Try again or check your LLM connection.");
}
} catch (e) {
setError(String(e));
@@ -96,9 +99,9 @@ Provide the response as a JSON object with exactly these keys:
🏔 Regions
</h4>
<div className="flex flex-col gap-1.5">
{world.regions.map((r, i) => (
{ensureArray(world.regions).map((r, i) => (
<div key={i} className="rounded-lg bg-[var(--color-bg-surface)] border border-[var(--color-border-subtle)] px-3 py-2 text-sm text-[var(--color-text-primary)]">
{r}
{flattenValue(r)}
</div>
))}
</div>
@@ -112,14 +115,14 @@ Provide the response as a JSON object with exactly these keys:
🏛 Landmarks
</h4>
<div className="flex flex-wrap gap-1.5">
{world.landmarks.map((l, i) => (
{ensureArray(world.landmarks).map((l, i) => (
<span key={i} className="rounded-full bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)] px-3 py-1 text-xs">
{l}
{flattenValue(l)}
</span>
))}
</div>
</div>
)}
)})
{/* Conflicts */}
{world.conflicts?.length > 0 && (
@@ -128,14 +131,14 @@ Provide the response as a JSON object with exactly these keys:
Conflicts
</h4>
<div className="flex flex-col gap-1.5">
{world.conflicts.map((c, i) => (
{ensureArray(world.conflicts).map((c, i) => (
<div key={i} className="rounded-lg bg-[var(--color-bg-surface)] border-l-2 border-[var(--color-danger)] px-3 py-2 text-sm text-[var(--color-text-primary)]">
{c}
{flattenValue(c)}
</div>
))}
</div>
</div>
)}
)})
{/* Cultures */}
{world.cultures?.length > 0 && (
@@ -144,14 +147,14 @@ Provide the response as a JSON object with exactly these keys:
👥 Cultures
</h4>
<div className="flex flex-wrap gap-1.5">
{world.cultures.map((c, i) => (
{ensureArray(world.cultures).map((c, i) => (
<span key={i} className="rounded-full bg-[var(--color-bg-surface)] border border-[var(--color-info)]/30 text-[var(--color-info)] px-3 py-1 text-xs">
{c}
{flattenValue(c)}
</span>
))}
</div>
</div>
)}
)})
</div>
)}
</div>