diff --git a/AI_AGENT.md b/AI_AGENT.md new file mode 100644 index 0000000..7ca56be --- /dev/null +++ b/AI_AGENT.md @@ -0,0 +1,58 @@ +# AI Agent Context Guide: Respellion Learning Platform + +Welcome, fellow AI agent! If you are reading this, you are tasked with maintaining or extending the Respellion Learning Platform. This document provides the critical context, architectural patterns, and design standards you need to successfully work on this codebase. + +## 1. Architectural Overview +This is a single-page React application built with **Vite**. It is completely self-contained and currently runs without a dedicated backend database. +* **Frontend:** React, React Router, Tailwind CSS. +* **Animations:** Framer Motion (used extensively for page transitions, podium effects, and gamification feedback). +* **Icons:** Lucide React. +* **Visualizations:** D3.js (used strictly for the Admin Knowledge Graph). + +## 2. State Management & Storage (Critical) +There is **no backend database**. All data is persisted locally in the browser using a custom wrapper around `window.localStorage` located at `src/lib/storage.js`. + +**Namespacing Convention:** +You must strictly adhere to these prefixes when reading/writing to `storage.js`: +* `kb:*` - Knowledge base data (e.g., `kb:topics`, `kb:relations`, `kb:content:{topicId}`). +* `admin:*` - Admin settings (e.g., `admin:anthropic_key`, `admin:sources`, `admin:use_simulation`). +* `user:{id}:*` - Specific user progress (e.g., `user:{id}:week:{n}:learn_done`). +* `quiz:bank:{topicId}` - Cached banks of AI-generated multiple-choice questions. +* `quiz:result:{userId}:week:{n}` - The exact score and payload of a user's weekly test. +* `leaderboard:current` - The active points ledger. Array of objects: `{ userId, name, points, testsCompleted, perfectScores }`. +* `users:registry` - Array of registered users, including admins. + +## 3. The AI Integration (Anthropic) +The application acts as a proxy to the Anthropic API (`claude-sonnet-4`). +* **Location:** `src/lib/api.js`. +* **Proxy:** In Docker, `/api/anthropic` is proxied via Nginx to bypass CORS restrictions. If the user reports CORS errors during local dev, ensure the Vite proxy in `vite.config.js` matches the Nginx spec. +* **JSON Enforcement:** Prompts *must* strictly enforce that Claude returns *only* raw JSON. Do not let the AI wrap the response in markdown blocks (`\`\`\``) unless you build a regex to strip them (which is currently implemented via `.match(/\{[\s\S]*\}/)`). + +## 4. Design System & Aesthetics +Respellion relies on a premium, modern aesthetic. +* **CSS Variables:** Rely heavily on the variables defined in `src/index.css`. + * Colors: `var(--color-bg)`, `var(--color-paper)`, `var(--color-teal)`, `var(--color-accent)`. + * Radii: `var(--r-sm)`, `var(--r-lg)`, `var(--r-org)` (an organic, pill-like shape used for badges and podiums). +* **Tailwind:** Tailwind is mapped to these CSS variables. Avoid hardcoding random hex codes. Use the existing Tailwind classes like `bg-teal`, `text-fg-muted`, and `border-bg-warm`. +* **Components:** Always reuse the UI primitives in `src/components/ui/` (`Card.jsx`, `Button.jsx`, `Tag.jsx`, `Input.jsx`). + +## 5. Gamification Rules +If you are extending the Gamification system (`src/pages/Leaderboard.jsx`): +* Tests grant **+2 points** per correct answer. +* A 100% score grants the **Perfectionist** badge. +* Completing 1 test grants **First Steps**, completing 5 grants **Veteran**. +* Do not reset points dynamically. Read from `leaderboard:current`, mutate, and save back immediately. + +## 6. Docker & Deployment +The app is fully containerized. +* **Build:** `docker build -t respellion-app:latest .` +* **Run:** `docker run -d -p 8080:80 --name respellion respellion-app:latest` +* **Nginx:** Ensure any new frontend routes are caught by the Nginx `try_files $uri $uri/ /index.html;` directive defined in `nginx.conf` to prevent 404s on page refresh. + +## 7. How to Add New Features +1. **Define State:** Decide where the data lives in `localStorage`. +2. **Build UI:** Create components using `Card`, `Button`, and `framer-motion` for smooth entry (`animate-in fade-in slide-in-from-bottom-4`). +3. **Wire Logic:** Connect to `src/store/AppContext.jsx` if it requires global user context, otherwise manage locally and persist to `storage.js`. +4. **Test:** Run the Docker container to ensure no build errors exist. + +Good luck! You are building a platform that empowers continuous learning. Keep it fast, keep it beautiful, and keep the user engaged. diff --git a/src/components/admin/KnowledgeGraph.jsx b/src/components/admin/KnowledgeGraph.jsx index b2f336b..efc74e5 100644 --- a/src/components/admin/KnowledgeGraph.jsx +++ b/src/components/admin/KnowledgeGraph.jsx @@ -1,16 +1,25 @@ import React, { useEffect, useRef, useState } from 'react'; import * as d3 from 'd3'; +import { Trash2, Edit2, Save, X } from 'lucide-react'; import { storage } from '../../lib/storage'; +import Button from '../ui/Button'; const KnowledgeGraph = () => { const svgRef = useRef(null); const wrapperRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 800, height: 600 }); const [selectedNode, setSelectedNode] = useState(null); + const [isEditing, setIsEditing] = useState(false); + const [editData, setEditData] = useState({}); - // Load data - const topics = storage.get('kb:topics', []); - const relations = storage.get('kb:relations', []); + // Load data into state so it can update + const [topics, setTopics] = useState([]); + const [relations, setRelations] = useState([]); + + useEffect(() => { + setTopics(storage.get('kb:topics', [])); + setRelations(storage.get('kb:relations', [])); + }, []); useEffect(() => { if (!wrapperRef.current) return; @@ -31,16 +40,11 @@ const KnowledgeGraph = () => { if (!svgRef.current || topics.length === 0) return; const { width, height } = dimensions; - - // Clear previous render d3.select(svgRef.current).selectAll('*').remove(); - // Setup nodes and links - // Copy objects because d3 modifies them const nodes = topics.map(d => ({ ...d })); const links = relations.map(d => ({ ...d })); - // Create SVG container with zoom support const svg = d3.select(svgRef.current) .attr('viewBox', [0, 0, width, height]) .attr('width', width) @@ -56,19 +60,16 @@ const KnowledgeGraph = () => { svg.call(zoom); - // Color scale for node types const color = d3.scaleOrdinal() .domain(['concept', 'role', 'process', 'fact']) .range(['#1F5560', '#5C1DD8', '#AFC0FF', '#E2E8F0']); - // Setup force simulation const simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(d => d.id).distance(100)) .force('charge', d3.forceManyBody().strength(-300)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collide', d3.forceCollide().radius(40)); - // Draw links const link = g.append('g') .attr('stroke', 'var(--color-bg-warm)') .attr('stroke-opacity', 0.6) @@ -77,14 +78,12 @@ const KnowledgeGraph = () => { .join('line') .attr('stroke-width', 2); - // Draw nodes const node = g.append('g') .selectAll('g') .data(nodes) .join('g') .call(drag(simulation)); - // Add circles to nodes node.append('circle') .attr('r', 20) .attr('fill', d => color(d.type) || '#1F5560') @@ -92,9 +91,9 @@ const KnowledgeGraph = () => { .attr('stroke-width', 2) .on('click', (event, d) => { setSelectedNode(d); + setIsEditing(false); }); - // Add labels to nodes node.append('text') .text(d => d.label) .attr('x', 25) @@ -109,41 +108,64 @@ const KnowledgeGraph = () => { .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); - - node - .attr('transform', d => `translate(${d.x},${d.y})`); + node.attr('transform', d => `translate(${d.x},${d.y})`); }); - // Drag behavior function drag(simulation) { function dragstarted(event) { if (!event.active) simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; } - function dragged(event) { event.subject.fx = event.x; event.subject.fy = event.y; } - function dragended(event) { if (!event.active) simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; } - - return d3.drag() - .on('start', dragstarted) - .on('drag', dragged) - .on('end', dragended); + return d3.drag().on('start', dragstarted).on('drag', dragged).on('end', dragended); } - return () => { - simulation.stop(); - }; + return () => simulation.stop(); }, [dimensions, topics, relations]); + const startEdit = () => { + setEditData({ label: selectedNode.label, type: selectedNode.type, description: selectedNode.description }); + setIsEditing(true); + }; + + const saveEdit = () => { + const updatedTopics = topics.map(t => + t.id === selectedNode.id ? { ...t, ...editData } : t + ); + storage.set('kb:topics', updatedTopics); + setTopics(updatedTopics); + setSelectedNode({ ...selectedNode, ...editData }); + setIsEditing(false); + }; + + const deleteNode = () => { + if (confirm(`Are you sure you want to delete "${selectedNode.label}"? This will also remove any relations connected to it.`)) { + const updatedTopics = topics.filter(t => t.id !== selectedNode.id); + const updatedRelations = relations.filter(r => r.source !== selectedNode.id && r.target !== selectedNode.id); + + storage.set('kb:topics', updatedTopics); + storage.set('kb:relations', updatedRelations); + + setTopics(updatedTopics); + setRelations(updatedRelations); + setSelectedNode(null); + setIsEditing(false); + + // Clear associated content to keep it clean + storage.remove(`kb:content:${selectedNode.id}`); + storage.remove(`quiz:bank:${selectedNode.id}`); + } + }; + return (
@@ -157,31 +179,82 @@ const KnowledgeGraph = () => {
{/* Node Info Panel */} -
-

Node Details

+
+
+

Node Details

+ {selectedNode && !isEditing && ( +
+ + +
+ )} +
+ {selectedNode ? ( -
-
-

Label

-

{selectedNode.label}

+ isEditing ? ( +
+
+ + setEditData({...editData, label: e.target.value})} + /> +
+
+ + +
+
+ +