Files
learning-platform/src/pages/Admin/index.jsx
RaymondVerhoef e2de7f0729
All checks were successful
On Push to Main / test (push) Successful in 29s
On Push to Main / publish (push) Successful in 58s
On Push to Main / deploy-dev (push) Successful in 1m32s
feat: add GitHub repository synchronization functionality with document extraction pipeline and update security policy to permit GitHub API access.
2026-05-17 15:15:51 +02:00

224 lines
11 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import { Database, FileText, Settings, Users, Network, Clock, CheckCircle2, AlertCircle, Save, Info, Layers, CheckSquare } from 'lucide-react';
import Card from '../../components/ui/Card';
import Tag from '../../components/ui/Tag';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import { storage } from '../../lib/storage';
import * as db from '../../lib/db';
import UploadZone from '../../components/admin/UploadZone';
import KnowledgeGraph from '../../components/admin/KnowledgeGraph';
import ContentManager from '../../components/admin/ContentManager';
import TestManager from '../../components/admin/TestManager';
import TeamManager from '../../components/admin/TeamManager';
import { Trash2 } from 'lucide-react';
const Admin = () => {
const [activeTab, setActiveTab] = useState('sources');
const [sources, setSources] = useState([]);
const [model, setModel] = useState('');
const [useSimulation, setUseSimulation] = useState(false);
const [saveStatus, setSaveStatus] = useState(null);
const loadSources = async () => {
const data = await db.getSources();
setSources(data);
};
useEffect(() => {
if (activeTab === 'sources') {
loadSources();
}
if (activeTab === 'settings') {
setModel(storage.get('admin:model', 'claude-sonnet-4-20250514'));
setUseSimulation(storage.get('admin:use_simulation', false));
}
}, [activeTab]);
const saveSettings = async (e) => {
e.preventDefault();
storage.set('admin:model', model.trim());
storage.set('admin:use_simulation', useSimulation);
setSaveStatus('Saved!');
setTimeout(() => setSaveStatus(null), 3000);
};
const handleDeleteSource = async (id) => {
if (confirm('Are you sure you want to delete this source? This will not delete topics already extracted.')) {
await db.deleteSource(id);
await loadSources();
}
};
const navItems = [
{ key: 'sources', icon: Database, label: 'Sources' },
{ key: 'content', icon: Layers, label: 'Content' },
{ key: 'tests', icon: CheckSquare, label: 'Quizzes' },
{ key: 'graph', icon: Network, label: 'Graph' },
{ key: 'team', icon: Users, label: 'Team' },
{ key: 'settings', icon: Settings, label: 'Settings', bottom: true },
];
return (
<div className="flex flex-col md:flex-row h-[calc(100vh-64px)] overflow-hidden">
<div className="w-full md:w-64 bg-paper border-r border-bg-warm flex-shrink-0 flex flex-row md:flex-col p-4 gap-2 overflow-x-auto md:overflow-y-auto">
<h2 className="hidden md:block text-teal font-bold text-lg mb-4 px-2">Knowledge Mgmt</h2>
{navItems.map(({ key, icon: Icon, label, bottom }) => (
<button
key={key}
onClick={() => setActiveTab(key)}
className={`flex flex-col md:flex-row items-center gap-3 p-3 rounded-[var(--r-sm)] transition-colors min-w-[80px] md:min-w-0 ${bottom ? 'mt-0 md:mt-auto' : ''} ${activeTab === key ? 'bg-bg-warm text-teal font-medium' : 'text-fg-muted hover:bg-bg-warm/50 hover:text-fg'}`}
>
<Icon size={20} />
<span className="text-sm mt-1 md:mt-0">{label}</span>
</button>
))}
</div>
<div className="flex-1 overflow-y-auto p-4 md:p-8 bg-bg pb-24 md:pb-8">
{activeTab === 'sources' && (
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Source Material</h1>
<p className="text-fg-muted mb-8">Upload files to feed the AI knowledge extraction pipeline.</p>
<UploadZone onUploadComplete={loadSources} />
<h3 className="text-xl mb-4 mt-12">Recent Sources</h3>
<Card className="p-0 border border-bg-warm overflow-hidden">
<div className="divide-y divide-bg-warm">
{sources.length === 0 ? (
<div className="p-8 text-center text-fg-muted">No sources uploaded yet.</div>
) : (
sources.map((source) => (
<div key={source.id} className="p-4 flex items-center justify-between hover:bg-bg-warm/30 transition-colors">
<div className="flex items-center gap-4">
<div className={`p-2 rounded-full ${
source.status === 'completed' ? 'bg-teal-50 text-teal-600' :
source.status === 'processing' ? 'bg-purple-50 text-purple-600 animate-pulse' :
'bg-red-50 text-red-600'
}`}>
<FileText size={20} />
</div>
<div>
<p className="font-medium">{source.name}</p>
<p className="text-xs text-fg-muted flex items-center gap-1 mt-1">
<Clock size={12} /> {source.created ? new Date(source.created).toLocaleString() : '—'}
</p>
</div>
</div>
<div className="flex items-center gap-3">
{source.status === 'completed' && <Tag variant="success" className="flex items-center gap-1"><CheckCircle2 size={12}/> Completed</Tag>}
{source.status === 'processing' && <Tag variant="accent" className="flex items-center gap-1"><Clock size={12}/> Processing</Tag>}
{source.status === 'failed' && <Tag variant="dark" className="bg-red-100 text-red-800 flex items-center gap-1"><AlertCircle size={12}/> Failed</Tag>}
<button onClick={() => handleDeleteSource(source.id)} className="p-1.5 text-fg-muted hover:text-red-500 hover:bg-red-50 rounded-[var(--r-sm)] transition-colors" title="Delete Source">
<Trash2 size={16} />
</button>
</div>
</div>
))
)}
</div>
</Card>
</div>
)}
{activeTab === 'content' && (
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Learning Content</h1>
<p className="text-fg-muted mb-8">Manage, refine, or regenerate AI-generated learning modules for each knowledge topic.</p>
<ContentManager />
</div>
)}
{activeTab === 'tests' && (
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Question Banks</h1>
<p className="text-fg-muted mb-8">Pre-generate and review quiz questions for each topic to eliminate loading times for users.</p>
<TestManager />
</div>
)}
{activeTab === 'graph' && (
<div className="animate-in fade-in duration-300 h-full flex flex-col">
<h1 className="text-3xl text-teal mb-2">Knowledge Graph</h1>
<p className="text-fg-muted mb-4">Visual map of all extracted Respellion knowledge.</p>
<Card className="flex-1 p-0 border border-bg-warm overflow-hidden bg-paper min-h-[400px]">
<KnowledgeGraph />
</Card>
</div>
)}
{activeTab === 'team' && (
<div className="animate-in fade-in duration-300 max-w-4xl mx-auto">
<h1 className="text-3xl text-teal mb-2">Team Management</h1>
<p className="text-fg-muted mb-8">Manage team members, roles, and login PINs.</p>
<TeamManager />
</div>
)}
{activeTab === 'settings' && (
<div className="animate-in fade-in duration-300 max-w-2xl">
<h1 className="text-3xl text-teal mb-2">Settings</h1>
<p className="text-fg-muted mb-8">Manage API keys and application configuration.</p>
<Card className="border border-bg-warm">
<form onSubmit={saveSettings} className="space-y-6">
<div>
<h3 className="text-lg font-medium mb-4">AI Configuration</h3>
<div className="mt-4">
<Input
label="Model ID"
placeholder="claude-sonnet-4-20250514"
value={model}
onChange={(e) => setModel(e.target.value)}
/>
<p className="text-xs text-fg-muted mt-1">Leave blank for the default. Check your Anthropic Console for available models.</p>
</div>
<p className="text-sm text-fg-muted mt-4">
Note: Your Anthropic API key is securely managed on the server side via environment variables.
</p>
</div>
<div className="pt-4 border-t border-bg-warm">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-medium">Simulation Mode</h3>
<p className="text-sm text-fg-muted">Use simulated AI responses when the API is unavailable.</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={useSimulation}
onChange={(e) => setUseSimulation(e.target.checked)}
/>
<div className="w-11 h-6 bg-bg-warm peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-teal"></div>
</label>
</div>
{useSimulation && (
<div className="p-3 bg-teal/5 border border-teal/20 rounded-[var(--r-sm)] flex gap-3 text-sm text-teal-800">
<Info size={18} className="flex-shrink-0" />
<p>When enabled, the platform will simulate AI processing without making real API calls. Ideal for UI testing.</p>
</div>
)}
</div>
<div className="flex items-center gap-4 pt-4">
<Button type="submit">
<Save size={18} className="mr-2" /> Save Settings
</Button>
{saveStatus && <span className="text-teal font-medium">{saveStatus}</span>}
</div>
</form>
</Card>
</div>
)}
</div>
</div>
);
};
export default Admin;