feat: add Leren page for AI-generated learning modules and update Caddyfile configuration for static routing
This commit is contained in:
22
Caddyfile
22
Caddyfile
@@ -1,7 +1,4 @@
|
|||||||
:80 {
|
:80 {
|
||||||
root * /srv
|
|
||||||
file_server
|
|
||||||
|
|
||||||
encode gzip zstd
|
encode gzip zstd
|
||||||
|
|
||||||
header {
|
header {
|
||||||
@@ -13,6 +10,16 @@
|
|||||||
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
Permissions-Policy "geolocation=(), microphone=(), camera=()"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handle /api/anthropic/* {
|
||||||
|
uri strip_prefix /api/anthropic
|
||||||
|
reverse_proxy https://api.anthropic.com {
|
||||||
|
header_up Host api.anthropic.com
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
root * /srv
|
||||||
|
|
||||||
@static {
|
@static {
|
||||||
path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.svg *.woff *.woff2 *.ttf
|
path *.css *.js *.png *.jpg *.jpeg *.gif *.webp *.svg *.woff *.woff2 *.ttf
|
||||||
}
|
}
|
||||||
@@ -23,14 +30,9 @@
|
|||||||
}
|
}
|
||||||
header @html Cache-Control "public, max-age=0, must-revalidate"
|
header @html Cache-Control "public, max-age=0, must-revalidate"
|
||||||
|
|
||||||
handle /api/anthropic/* {
|
|
||||||
uri strip_prefix /api/anthropic
|
|
||||||
reverse_proxy https://api.anthropic.com {
|
|
||||||
header_up Host api.anthropic.com
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try_files {path} /index.html
|
try_files {path} /index.html
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
handle_errors {
|
handle_errors {
|
||||||
rewrite * /index.html
|
rewrite * /index.html
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { BookOpen, CheckCircle, Loader, ArrowRight, Plus, Search, ChevronLeft } from 'lucide-react';
|
import { BookOpen, CheckCircle, Loader, ArrowRight, Plus, Search, ChevronLeft, MessageSquare } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Card from '../components/ui/Card';
|
import Card from '../components/ui/Card';
|
||||||
@@ -32,6 +32,11 @@ const Leren = () => {
|
|||||||
const [weeklyDone, setWeeklyDone] = useState(false);
|
const [weeklyDone, setWeeklyDone] = useState(false);
|
||||||
const [sessionDone, setSessionDone] = useState(false);
|
const [sessionDone, setSessionDone] = useState(false);
|
||||||
|
|
||||||
|
// Feedback
|
||||||
|
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
|
||||||
|
const [feedbackText, setFeedbackText] = useState('');
|
||||||
|
const [feedbackPrompted, setFeedbackPrompted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (state.currentUser) {
|
if (state.currentUser) {
|
||||||
const assigned = getAssignedTopic(state.currentUser.id, state.weekNumber);
|
const assigned = getAssignedTopic(state.currentUser.id, state.weekNumber);
|
||||||
@@ -48,6 +53,8 @@ const Leren = () => {
|
|||||||
setView('detail');
|
setView('detail');
|
||||||
setSessionDone(false);
|
setSessionDone(false);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setFeedbackText('');
|
||||||
|
setFeedbackPrompted(false);
|
||||||
const cached = getCachedContent(topic.id);
|
const cached = getCachedContent(topic.id);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
setContent(cached);
|
setContent(cached);
|
||||||
@@ -90,7 +97,7 @@ const Leren = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = () => {
|
const doComplete = () => {
|
||||||
setSessionDone(true);
|
setSessionDone(true);
|
||||||
if (!weeklyDone) {
|
if (!weeklyDone) {
|
||||||
setWeeklyDone(true);
|
setWeeklyDone(true);
|
||||||
@@ -98,6 +105,33 @@ const Leren = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
if (!feedbackPrompted) {
|
||||||
|
setFeedbackPrompted(true);
|
||||||
|
setShowFeedbackModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitFeedback = () => {
|
||||||
|
if (feedbackText.trim()) {
|
||||||
|
storage.set(`user:${state.currentUser.id}:feedback:${activeTopic.id}`, {
|
||||||
|
text: feedbackText.trim(),
|
||||||
|
topicId: activeTopic.id,
|
||||||
|
week: state.weekNumber,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setShowFeedbackModal(false);
|
||||||
|
doComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkipFeedback = () => {
|
||||||
|
setShowFeedbackModal(false);
|
||||||
|
doComplete();
|
||||||
|
};
|
||||||
|
|
||||||
// ── Detail View ──────────────────────────────────────────
|
// ── Detail View ──────────────────────────────────────────
|
||||||
if (view === 'detail' && activeTopic) {
|
if (view === 'detail' && activeTopic) {
|
||||||
if (sessionDone) {
|
if (sessionDone) {
|
||||||
@@ -123,6 +157,50 @@ const Leren = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-8 max-w-4xl mx-auto pb-24 md:pb-8">
|
<div className="p-4 md:p-8 max-w-4xl mx-auto pb-24 md:pb-8">
|
||||||
|
<AnimatePresence>
|
||||||
|
{showFeedbackModal && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.95, opacity: 0, y: 16 }}
|
||||||
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
|
exit={{ scale: 0.95, opacity: 0, y: 16 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 24 }}
|
||||||
|
className="w-full max-w-lg"
|
||||||
|
>
|
||||||
|
<Card className="border border-bg-warm shadow-xl">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-teal/10 flex items-center justify-center flex-shrink-0">
|
||||||
|
<MessageSquare size={20} className="text-teal" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-xl font-bold">How was this content?</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-fg-muted text-sm mb-5">
|
||||||
|
Your feedback helps improve future learning content. This is optional — you can skip if you prefer.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
value={feedbackText}
|
||||||
|
onChange={e => setFeedbackText(e.target.value)}
|
||||||
|
placeholder="What was clear? What could be improved? Anything missing?"
|
||||||
|
rows={4}
|
||||||
|
className="w-full rounded-[var(--r-sm)] border border-bg-warm bg-bg p-3 text-sm resize-none focus:outline-none focus:border-teal transition-colors"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<Button variant="outline" onClick={handleSkipFeedback}>Skip</Button>
|
||||||
|
<Button onClick={handleSubmitFeedback}>Submit Feedback</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<button onClick={() => setView('overview')} className="flex items-center gap-2 text-fg-muted hover:text-teal mb-6 transition-colors">
|
<button onClick={() => setView('overview')} className="flex items-center gap-2 text-fg-muted hover:text-teal mb-6 transition-colors">
|
||||||
<ChevronLeft size={16} /> Back to overview
|
<ChevronLeft size={16} /> Back to overview
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user