diff --git a/nginx.conf b/nginx.conf
index 9ec3a38..89d75bc 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -23,4 +23,10 @@ server {
access_log off;
add_header Cache-Control "public";
}
+ # Proxy Anthropic API to bypass CORS
+ location /api/anthropic/ {
+ proxy_pass https://api.anthropic.com/;
+ proxy_set_header Host api.anthropic.com;
+ proxy_ssl_server_name on;
+ }
}
diff --git a/src/App.jsx b/src/App.jsx
index ace264f..80d12a2 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -7,10 +7,10 @@ import Login from './pages/Login'
import Dashboard from './pages/Dashboard'
import Admin from './pages/Admin'
+import Leren from './pages/Leren'
// Placeholder components for routing structure
const Testen = () =>
Weektest
Start your weekly test here.
-const Leren = () =>
Leren
Start your weekly learning session.
const Leaderboard = () =>
Leaderboard
See who is on top!
// Protected Route Wrapper
diff --git a/src/lib/api.js b/src/lib/api.js
index cfea2fa..5aeefd8 100644
--- a/src/lib/api.js
+++ b/src/lib/api.js
@@ -2,47 +2,52 @@ import { storage } from './storage';
/**
* Anthropic API Service
- * Handles communication with the /v1/messages endpoint.
+ * Handles communication with the /v1/messages endpoint via Nginx proxy.
*/
-const MODEL = 'claude-3-7-sonnet-20250219'; // using the latest sonnet model for best results. Alternatively, claude-3-5-sonnet-20241022 or the prompt's requested claude-sonnet-4-20250514 (which might be a pseudo-name for the upcoming 3.7 or future version).
+const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
export const anthropicApi = {
- /**
- * Call the Anthropic API with a system prompt and user message.
- * Includes a basic retry mechanism.
- */
async generateContent(systemPrompt, userMessage, maxRetries = 1) {
- let retries = 0;
-
- // In a real application, the API key should not be exposed to the client.
- // For this prototype, we'll assume it's passed or available in the environment,
- // or proxied via a secure endpoint if deployed.
- // Prioritize key from local storage (set via Admin Settings)
- const apiKey = storage.get('admin:anthropic_key') || import.meta.env.VITE_ANTHROPIC_API_KEY || 'no-key-provided';
+ // Check if simulation mode is on
+ const useSimulation = storage.get('admin:use_simulation') === true;
+ if (useSimulation) {
+ console.log('[API] Simulatie mode actief. Mock data wordt geretourneerd.');
+ return await simulateResponse();
+ }
+ const apiKey = storage.get('admin:anthropic_key') || import.meta.env.VITE_ANTHROPIC_API_KEY;
+ if (!apiKey) {
+ throw new Error('Geen Anthropic API key gevonden. Ga naar Admin -> Settings.');
+ }
+
+ // Model is configurable from Admin > Settings, defaults to the original spec model
+ const model = storage.get('admin:model') || DEFAULT_MODEL;
+ console.log(`[API] Aanroep met model: ${model}`);
+
+ let retries = 0;
while (retries <= maxRetries) {
try {
- const response = await fetch('https://api.anthropic.com/v1/messages', {
+ const response = await fetch('/api/anthropic/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': apiKey,
'anthropic-version': '2023-06-01',
- 'anthropic-dangerous-direct-browser-access': 'true' // Required for client-side fetch
+ 'anthropic-dangerous-direct-browser-access': 'true'
},
body: JSON.stringify({
- model: MODEL,
+ model: model,
max_tokens: 4000,
system: systemPrompt,
- messages: [
- { role: 'user', content: userMessage }
- ]
+ messages: [{ role: 'user', content: userMessage }]
})
});
if (!response.ok) {
- throw new Error(`API Error: ${response.status} ${response.statusText}`);
+ const errData = await response.json().catch(() => ({}));
+ console.error('[API] Error response:', errData);
+ throw new Error(`API Error: ${response.status} ${response.statusText} - ${JSON.stringify(errData)}`);
}
const data = await response.json();
@@ -50,12 +55,24 @@ export const anthropicApi = {
} catch (error) {
console.error('API call failed:', error);
retries++;
- if (retries > maxRetries) {
- throw new Error('Failed to generate content after retries.');
- }
- // Small delay before retry
- await new Promise(resolve => setTimeout(resolve, 1000));
+ if (retries > maxRetries) throw error;
+ await new Promise(r => setTimeout(r, 1000));
}
}
}
};
+
+async function simulateResponse() {
+ await new Promise(r => setTimeout(r, 2000));
+ return JSON.stringify({
+ topics: [
+ { id: "radicale-transparantie", label: "Radicale Transparantie", type: "concept", description: "De kernwaarde van Respellion waarbij alle informatie publiek toegankelijk is." },
+ { id: "kennisbeheer", label: "Kennisbeheer", type: "process", description: "Het proces van het vastleggen en ontsluiten van organisatiekennis." },
+ { id: "wekelijkse-sessie", label: "Wekelijkse Leersessie", type: "process", description: "Elke week leren medewerkers via AI-gegenereerde vragen en quizzen." }
+ ],
+ relations: [
+ { source: "kennisbeheer", target: "radicale-transparantie", type: "depends_on" },
+ { source: "wekelijkse-sessie", target: "kennisbeheer", type: "part_of" }
+ ]
+ });
+}
diff --git a/src/lib/learningService.js b/src/lib/learningService.js
new file mode 100644
index 0000000..cf79fed
--- /dev/null
+++ b/src/lib/learningService.js
@@ -0,0 +1,91 @@
+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.`;
+
+/**
+ * Get the assigned topic for a user for a given week using round-robin.
+ * hash(userId + weekNumber) % topicCount
+ */
+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
+ }
+ 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.
+ */
+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;
+ }
+
+ const prompt = `Genereer een compleet leermodule voor het volgende onderwerp:
+
+Label: ${topic.label}
+Type: ${topic.type}
+Beschrijving: ${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.`;
+
+
+ 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 kon geen geldige leerinhoud genereren.');
+ }
+
+ // Cache the content
+ storage.set(cacheKey, content);
+ return content;
+}
diff --git a/src/pages/Admin/index.jsx b/src/pages/Admin/index.jsx
index c4d9439..0225993 100644
--- a/src/pages/Admin/index.jsx
+++ b/src/pages/Admin/index.jsx
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
-import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save } from 'lucide-react';
+import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info } from 'lucide-react';
import Card from '../../components/ui/Card';
import Tag from '../../components/ui/Tag';
import Button from '../../components/ui/Button';
@@ -12,6 +12,8 @@ const Admin = () => {
const [activeTab, setActiveTab] = useState('bronnen');
const [sources, setSources] = useState([]);
const [apiKey, setApiKey] = useState('');
+ const [model, setModel] = useState('');
+ const [useSimulation, setUseSimulation] = useState(false);
const [saveStatus, setSaveStatus] = useState(null);
const loadSources = () => {
@@ -24,12 +26,16 @@ const Admin = () => {
}
if (activeTab === 'instellingen') {
setApiKey(storage.get('admin:anthropic_key', ''));
+ setModel(storage.get('admin:model', 'claude-sonnet-4-20250514'));
+ setUseSimulation(storage.get('admin:use_simulation', false));
}
}, [activeTab]);
const saveSettings = (e) => {
e.preventDefault();
- storage.set('admin:anthropic_key', apiKey);
+ storage.set('admin:anthropic_key', apiKey.trim());
+ storage.set('admin:model', model.trim());
+ storage.set('admin:use_simulation', useSimulation);
setSaveStatus('Opgeslagen!');
setTimeout(() => setSaveStatus(null), 3000);
};
@@ -153,12 +159,45 @@ const Admin = () => {
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
/>
+
+ setModel(e.target.value)}
+ />
+
Laat leeg voor de standaard. Controleer je Anthropic Console voor beschikbare modellen.
+
- Deze sleutel wordt lokaal in je browser opgeslagen (`localStorage`) en wordt gebruikt voor alle AI-interacties.
+ Deze sleutel wordt lokaal in je browser opgeslagen (`localStorage`).
+
+
+
+
+
Simulatie Mode
+
Gebruik gesimuleerde AI-antwoorden als de API niet werkt.
+
+
+
+ {useSimulation && (
+
+
+
Wanneer ingeschakeld, zal het platform doen alsof de AI teksten verwerkt zonder een echte API-aanroep te doen. Ideaal voor UI testen.