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
+12 -10
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";
@@ -32,20 +33,21 @@ Alignment: ${alignment}
Provide the response as a JSON object with exactly these keys:
- "bio": a 2-3 sentence backstory
- "personality": an array of 3-5 personality traits
- "goals": an array of 2-3 goals or motivations`;
- "personality": an array of 3-5 personality traits (each trait must be a string, not an object)
- "goals": an array of 2-3 goals or motivations (each goal must be a string, not an object)
IMPORTANT: Return ONLY a raw JSON object. NO markdown fences, NO code blocks, NO extra text. Start with { and end with }.`;
const result = await invoke<string>("generate", {
req: { prompt, system: "You are a creative D&D dungeon master. Always respond with valid JSON only.", temperature: 0.8, max_tokens: 512 },
req: { prompt, system: "You are a creative D&D dungeon master. You MUST respond with ONLY valid JSON. No markdown fences, no code blocks, no explanation. Just the JSON object.", temperature: 0.8, max_tokens: 512 },
});
// Try to parse JSON from the response
const jsonMatch = result.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]) as NpcResponse;
const parsed = parseLlmJson<NpcResponse>(result);
if (parsed) {
setNpc(parsed);
} else {
setError("LLM did not return valid JSON. Raw response:\n" + result);
setError("Could not parse LLM response. Try again or check your LLM connection.");
}
} catch (e) {
setError(String(e));
@@ -120,9 +122,9 @@ Provide the response as a JSON object with exactly these keys:
<div>
<span className="text-[var(--color-text-secondary)] text-xs font-medium">Personality:</span>
<div className="flex flex-wrap gap-1 mt-1">
{npc.personality?.map((p, i) => (
{ensureArray(npc.personality).map((p, i) => (
<span key={i} className="rounded-full bg-[var(--color-gold-glow)] text-[var(--color-gold-bright)] px-2 py-0.5 text-xs">
{p}
{flattenValue(p)}
</span>
))}
</div>
@@ -130,7 +132,7 @@ Provide the response as a JSON object with exactly these keys:
<div>
<span className="text-[var(--color-text-secondary)] text-xs font-medium">Goals:</span>
<ul className="list-disc list-inside text-[var(--color-text-primary)] text-xs mt-1">
{npc.goals?.map((g, i) => <li key={i}>{g}</li>)}
{ensureArray(npc.goals).map((g, i) => <li key={i}>{flattenValue(g)}</li>)}
</ul>
</div>
</div>