feat: implement core knowledge graph UI components, extraction pipeline, and initial platform navigation pages

This commit is contained in:
RaymondVerhoef
2026-05-10 21:33:02 +02:00
parent a626042092
commit 31aacd68d5
14 changed files with 1634 additions and 480 deletions

View File

@@ -1,10 +1,37 @@
import { anthropicApi } from './api';
import { storage } from './storage';
const CONTENT_GENERATION_SYSTEM = `Je bent een expert leerinhoud-schrijver voor Respellion, een intern IT-bedrijf.
Je schrijft leermateriaal voor medewerkers op basis van kennisonderwerpen.
Schrijf altijd in het Nederlands, helder en professioneel.
Geef ALTIJD geldige JSON terug, zonder markdown code-blokken.`;
const CONTENT_GENERATION_SYSTEM = `You are an expert learning content writer for Respellion, an internal IT company.
You write training material for employees based on knowledge topics.
Always write in clear, professional English.
ALWAYS return valid JSON only — no markdown code blocks, no extra text.`;
const CONTENT_SCHEMA = `{
"article": {
"title": "Article title",
"intro": "Short intro of 1-2 sentences",
"sections": [
{ "heading": "Section title", "body": "Section text of at least 3 sentences." }
],
"keyTakeaways": ["Takeaway 1", "Takeaway 2", "Takeaway 3"]
},
"slides": [
{ "title": "Slide title", "bullets": ["Point 1", "Point 2", "Point 3"], "speakerNote": "Speaker note for this slide." }
],
"podcastScript": "A natural spoken script of approx. 300 words summarizing the topic as a podcast episode.",
"infographic": {
"headline": "A short, punchy headline summarizing the topic (max 8 words)",
"tagline": "A subtitle of max 15 words",
"stats": [
{ "value": "Number or %", "label": "Short description", "icon": "📊" }
],
"steps": [
{ "number": 1, "title": "Step title", "description": "One-sentence description.", "icon": "🔑" }
],
"quote": "An inspiring or insightful quote about the topic.",
"colorTheme": "teal"
}
}`;
/**
* Get the assigned topic for a user for a given week using round-robin.
@@ -14,66 +41,69 @@ export function getAssignedTopic(userId, weekNumber) {
const topics = storage.get('kb:topics', []);
if (!topics || topics.length === 0) return null;
// Simple deterministic hash
const str = `${userId}:${weekNumber}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0; // Convert to 32-bit integer
hash |= 0;
}
const index = Math.abs(hash) % topics.length;
return topics[index];
}
/**
* Generate a complete learning module for a topic.
* Returns an object with { article, slides, podcastScript, infographic }.
* Caches results in storage.
* Returns the cache key for a topic's content.
*/
export async function generateLearningContent(topic) {
const cacheKey = `kb:content:${topic.id}`;
const cached = storage.get(cacheKey);
if (cached) {
console.log(`[Learn] Cache hit voor topic: ${topic.id}`);
return cached;
export function getContentCacheKey(topicId) {
return `kb:content:${topicId}`;
}
/**
* Returns cached content for a topic, or null if none exists.
*/
export function getCachedContent(topicId) {
return storage.get(getContentCacheKey(topicId), null);
}
/**
* List all topics that have generated content.
*/
export function getAllGeneratedContent() {
const topics = storage.get('kb:topics', []);
return topics
.map(topic => ({
topic,
content: getCachedContent(topic.id),
hasContent: !!getCachedContent(topic.id),
}))
.filter(item => item.hasContent);
}
/**
* Generate a complete learning module for a topic.
* Uses cached version if available (unless force = true).
*/
export async function generateLearningContent(topic, force = false) {
const cacheKey = getContentCacheKey(topic.id);
if (!force) {
const cached = storage.get(cacheKey);
if (cached) {
console.log(`[Learn] Cache hit for topic: ${topic.id}`);
return cached;
}
}
const prompt = `Genereer een compleet leermodule voor het volgende onderwerp:
const prompt = `Generate a complete learning module for the following topic:
Label: ${topic.label}
Type: ${topic.type}
Beschrijving: ${topic.description}
Description: ${topic.description}
Geef ALLEEN een JSON object terug met de volgende structuur:
{
"article": {
"title": "Artikel titel",
"intro": "Korte intro van 1-2 zinnen",
"sections": [
{ "heading": "Sectietitel", "body": "Sectietekst van minimaal 3 zinnen." }
],
"keyTakeaways": ["Lespunt 1", "Lespunt 2", "Lespunt 3"]
},
"slides": [
{ "title": "Diatitel", "bullets": ["Punt 1", "Punt 2", "Punt 3"], "speakerNote": "Toelichting voor de spreker." }
],
"podcastScript": "Een vloeiend gesproken script van ca. 300 woorden dat de inhoud samenvat als een podcast.",
"infographic": {
"headline": "Een korte, krachtige zin die het onderwerp samenvat (max 8 woorden)",
"tagline": "Een subkop van max 15 woorden",
"stats": [
{ "value": "Getal of %", "label": "Korte omschrijving", "icon": "📊" }
],
"steps": [
{ "number": 1, "title": "Staptitel", "description": "Korte beschrijving van 1 zin.", "icon": "🔑" }
],
"quote": "Een inspirerende of kernachtige quote over het onderwerp.",
"colorTheme": "teal"
}
}
Zorg voor minimaal 3 secties in het artikel, 4 slides, 3 statistieken en 3-5 stappen in de infographic.`;
Return ONLY a JSON object with the following structure:
${CONTENT_SCHEMA}
Provide at least 3 article sections, 4 slides, 3 stats, and 3-5 steps in the infographic.`;
const responseText = await anthropicApi.generateContent(CONTENT_GENERATION_SYSTEM, prompt);
@@ -82,10 +112,47 @@ Zorg voor minimaal 3 secties in het artikel, 4 slides, 3 statistieken en 3-5 sta
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
content = JSON.parse(jsonMatch ? jsonMatch[0] : responseText);
} catch (e) {
throw new Error('AI kon geen geldige leerinhoud genereren.');
throw new Error('AI could not generate valid learning content. Please try again.');
}
// Cache the content
storage.set(cacheKey, content);
return content;
}
/**
* Refine existing content for a topic using a natural language instruction.
* Sends current content + refinement prompt to AI, saves new version.
*/
export async function refineLearningContent(topic, refinementInstruction) {
const cacheKey = getContentCacheKey(topic.id);
const existing = storage.get(cacheKey);
const prompt = `You have previously generated the following learning module for the topic "${topic.label}":
${JSON.stringify(existing, null, 2)}
The admin has requested the following refinement:
"${refinementInstruction}"
Apply the refinement and return the complete updated JSON object using the same structure. Return ONLY valid JSON.`;
const responseText = await anthropicApi.generateContent(CONTENT_GENERATION_SYSTEM, prompt);
let content;
try {
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
content = JSON.parse(jsonMatch ? jsonMatch[0] : responseText);
} catch (e) {
throw new Error('AI could not process the refinement. Please try a different instruction.');
}
storage.set(cacheKey, content);
return content;
}
/**
* Delete cached content for a topic, forcing a fresh generation next time.
*/
export function deleteCachedContent(topicId) {
storage.remove(getContentCacheKey(topicId));
}