feat: enhance ConceptExplainer to handle content extraction and display; add normalization for PocketBase records
All checks were successful
On Push to Main / test (push) Successful in 39s
On Push to Main / publish (push) Successful in 1m8s
On Push to Main / deploy-dev (push) Successful in 1m34s

This commit is contained in:
RaymondVerhoef
2026-05-27 21:18:17 +02:00
parent ce276f0296
commit ad5be01b70
2 changed files with 57 additions and 8 deletions

View File

@@ -1,19 +1,42 @@
import Card from '../ui/Card';
import Button from '../ui/Button';
/**
* Safely extract sections from a content object that may be:
* - a properly parsed object: { sections: [...] }
* - a JSON string: '{"sections":[...]}'
* - null / undefined
*
* Returns an array in every case so .map() never throws.
*/
function getSections(content) {
let obj = content;
if (typeof obj === 'string') {
try { obj = JSON.parse(obj); } catch { return []; }
}
const sections = obj?.sections;
return Array.isArray(sections) ? sections : [];
}
export default function ConceptExplainer({ content, onComplete }) {
const sections = getSections(content);
return (
<Card className="w-full h-[60vh] flex flex-col p-0">
<div className="p-6 border-b border-bg-warm">
<h2 className="text-xl font-bold">Concept Explainer</h2>
</div>
<div className="flex-1 overflow-y-auto p-6 prose dark:prose-invert">
{content?.sections?.map((section, i) => (
{sections.length === 0 ? (
<p className="text-fg-muted italic">No content available for this topic yet.</p>
) : (
sections.map((section, i) => (
<div key={i} className="mb-6">
<h3>{section.title}</h3>
<div dangerouslySetInnerHTML={{ __html: section.content }} />
</div>
))}
))
)}
</div>
<div className="p-4 border-t border-bg-warm flex justify-end">
<Button onClick={onComplete}>Finish session</Button>

View File

@@ -94,7 +94,8 @@ export async function getOrGenerateMicroLearning(topicId, type) {
});
console.log(`[MicroLearning] Stored: ${record.id}`);
return record;
// Normalise in case PB returns the JSON field as a string on this PB version.
return normalizeRecord(record);
}
/**
@@ -154,12 +155,37 @@ export async function deleteAllForTopic(topicId) {
// ── Internal helpers ──────────────────────────────────────────────────────────
/**
* Ensure the PocketBase record's `content` field is a parsed JS object.
*
* Older PocketBase versions (or misconfigured instances) may return a JSON
* field as a raw string instead of a parsed object. The PB JS SDK normally
* handles this, but we have seen production deployments where `r.content` is
* the serialised JSON string — which causes `content.sections?.map` to throw
* "is not a function" because strings don't have a `.map` method.
*
* This guard is defensive and idempotent: if `content` is already a plain
* object it is returned as-is; only strings are re-parsed.
*/
function normalizeRecord(record) {
if (!record) return record;
if (typeof record.content === 'string') {
try {
record.content = JSON.parse(record.content);
} catch {
// Malformed JSON — replace with an empty object so the UI can handle it.
record.content = {};
}
}
return record;
}
async function findExisting(topicId, type) {
try {
const records = await pb.collection('micro_learnings').getFullList({
filter: `topic_id = "${topicId}" && type = "${type}" && status = "published"`,
});
return records.length > 0 ? records[0] : null;
return records.length > 0 ? normalizeRecord(records[0]) : null;
} catch {
return null;
}