feat: implement Anthropic API integration with simulation mode and a configurable admin dashboard
This commit is contained in:
@@ -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" }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
91
src/lib/learningService.js
Normal file
91
src/lib/learningService.js
Normal file
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user