Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions lecture-pulse/src/components/AISummaryCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Sparkles, ThumbsUp, AlertTriangle, BookOpen, Loader2, RefreshCw } from "lucide-react";
import { generateAISummary } from "@/utils/aiSummary";

const sentimentColor = {
positive: "text-emerald-600 bg-emerald-50 border-emerald-200",
neutral: "text-blue-600 bg-blue-50 border-blue-200",
negative: "text-red-600 bg-red-50 border-red-200",
};

const AISummaryCard = ({ lecture, analytics, feedback }) => {
const [summary, setSummary] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const handleGenerate = async () => {
setLoading(true);
setError(null);
try {
const result = await generateAISummary(lecture, analytics, feedback);
setSummary(result);
} catch (e) {
setError("Failed to generate summary. Check your API key or try again.");
} finally {
setLoading(false);
}
};

return (
<Card className="border-primary/20">
<CardHeader>
<CardTitle className="text-base flex items-center justify-between">
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
AI Feedback Summary
</div>
{summary && (
<span className={`text-xs font-medium px-2 py-1 rounded-full border ${sentimentColor[summary.sentiment] || sentimentColor.neutral}`}>
{summary.sentiment}
</span>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{!summary && !loading && (
<div className="flex flex-col items-center gap-3 py-6 text-center">
<Sparkles className="w-8 h-8 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
Generate an AI-powered summary of this lecture's feedback
</p>
<Button onClick={handleGenerate} className="bg-primary hover:bg-primary/90 text-primary-foreground">
<Sparkles className="w-4 h-4 mr-2" />
Generate Summary
</Button>
</div>
)}

{loading && (
<div className="flex flex-col items-center gap-3 py-6">
<Loader2 className="w-6 h-6 animate-spin text-primary" />
<p className="text-sm text-muted-foreground">Analyzing feedback with Llama 3.3...</p>
</div>
)}

{error && (
<div className="text-sm text-destructive bg-destructive/10 p-3 rounded-lg">
{error}
</div>
)}

{summary && (
<div className="space-y-4">
<p className="text-sm text-foreground leading-relaxed">{summary.overall}</p>

{summary.positives?.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-2">
<ThumbsUp className="w-4 h-4 text-emerald-600" />
<span className="text-xs font-semibold uppercase tracking-wide text-emerald-600">Positives</span>
</div>
<ul className="space-y-1">
{summary.positives.map((p, i) => (
<li key={i} className="text-sm text-foreground flex items-start gap-2">
<span className="text-emerald-500 mt-0.5">•</span>{p}
</li>
))}
</ul>
</div>
)}

{summary.concerns?.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-yellow-600" />
<span className="text-xs font-semibold uppercase tracking-wide text-yellow-600">Concerns</span>
</div>
<ul className="space-y-1">
{summary.concerns.map((c, i) => (
<li key={i} className="text-sm text-foreground flex items-start gap-2">
<span className="text-yellow-500 mt-0.5">•</span>{c}
</li>
))}
</ul>
</div>
)}

{summary.followUp?.length > 0 && (
<div>
<div className="flex items-center gap-2 mb-2">
<BookOpen className="w-4 h-4 text-blue-600" />
<span className="text-xs font-semibold uppercase tracking-wide text-blue-600">Follow-up</span>
</div>
<ul className="space-y-1">
{summary.followUp.map((f, i) => (
<li key={i} className="text-sm text-foreground flex items-start gap-2">
<span className="text-blue-500 mt-0.5">•</span>{f}
</li>
))}
</ul>
</div>
)}

<Button variant="outline" size="sm" onClick={handleGenerate} className="w-full text-xs">
<RefreshCw className="w-3 h-3 mr-1" />
Regenerate
</Button>
</div>
)}
</CardContent>
</Card>
);
};

export default AISummaryCard;
5 changes: 4 additions & 1 deletion lecture-pulse/src/pages/Analytics.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import UnderstandingChart from '@/components/charts/UnderstandingChart';
import AttentionChart from '@/components/charts/AttentionChart';
import ConfusionChart from '@/components/charts/ConfusionChart';
import FeedbackTimeline from '@/components/charts/FeedbackTimeline';
import AISummaryCard from '@/components/AISummaryCard';
import { generateLecturePDF } from "@/utils/pdfReport";
import { useRef } from "react";
import html2canvas from "html2canvas";
Expand All @@ -34,6 +35,7 @@ const Analytics = () => {
return canvas.toDataURL('image/png');
};

const [feedback, setFeedback] = useState([]);
const loadData = useCallback(async () => {
if (!sessionId) return;

Expand All @@ -60,7 +62,7 @@ const Analytics = () => {
// Assume empty for now. Real app would wait for students.
// Uncomment below to force mock data for specific tests or add a "Generate Mock Data" button
}

setFeedback(feedback);
const calculatedAnalytics = calculateAnalytics(feedback);
setAnalytics(calculatedAnalytics);
}, [sessionId, navigate]);
Expand Down Expand Up @@ -290,6 +292,7 @@ const Analytics = () => {
</CardContent>
</Card>
</div>
<AISummaryCard lecture={lecture} analytics={analytics} feedback={feedback} />
</div>
)}
</main>
Expand Down
60 changes: 60 additions & 0 deletions lecture-pulse/src/utils/aiSummary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export const generateAISummary = async (lecture, analytics, feedback) => {
const apiKey = import.meta.env.VITE_GROQ_API_KEY;
console.log("KEY:", apiKey);

const feedbackSample = feedback.slice(0, 30).map(f => ({
understanding: f.understanding,
attention: f.attention,
confusionTime: f.confusionTime,
comment: f.comment || null,
}));

const prompt = `
You are an educational analytics assistant. Analyze this lecture feedback and provide a concise summary.

Lecture: "${lecture.topic}" (${lecture.subject}, ${lecture.duration} mins)

Metrics:
- Total responses: ${analytics.totalResponses}
- Understanding score: ${analytics.understandingScore}%
- Attention score: ${analytics.attentionScore}%
- Overall effectiveness: ${Math.round((analytics.understandingScore + analytics.attentionScore) / 2)}%

Feedback sample:
${JSON.stringify(feedbackSample, null, 2)}

Provide a JSON response with exactly this structure:
{
"overall": "2-3 sentence overall summary of the lecture session",
"positives": ["up to 3 positive themes from feedback"],
"concerns": ["up to 3 areas of confusion or concern"],
"followUp": ["up to 3 suggested follow-up topics or actions"],
"sentiment": "positive"
}

Return only valid JSON, no markdown, no explanation.
`;

const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: "llama-3.3-70b-versatile",
messages: [{ role: "user", content: prompt }],
temperature: 0.4,
max_tokens: 600,
}),
});

if (!response.ok) {
const err = await response.text();
throw new Error(err);
}

const data = await response.json();
const text = data.choices[0].message.content.trim();
return JSON.parse(text);
};
23 changes: 14 additions & 9 deletions lecture-pulse/vite.config.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { defineConfig } from 'vite'
import { defineConfig, loadEnv } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': '/src',
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '')
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': '/src',
},
},
},
})
define: {
'import.meta.env.VITE_GROQ_API_KEY': JSON.stringify(env.VITE_GROQ_API_KEY),
},
}
})