feat: enhance ConceptExplainer to handle content extraction and display; add normalization for PocketBase records
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user