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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user