feat: implement core knowledge graph UI components, extraction pipeline, and initial platform navigation pages
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user