Skip to content

Latest commit

 

History

History
348 lines (306 loc) · 11.5 KB

File metadata and controls

348 lines (306 loc) · 11.5 KB

PHASE 6: AI Integration (Google Gemini)

6.1 Get Gemini API Key

1. Go to https://aistudio.google.com
2. Sign in with Google account
3. Click "Get API Key" → "Create API key"
4. Copy the key and add it to server/.env as GEMINI_API_KEY

6.2 AI-Powered Features Implementation

Add AI Routes: server/src/routes/ai.js

// ====================================
// AI Routes
// ====================================
const express = require('express');
const router = express.Router();
const aiService = require('../services/aiService');
const { authenticate } = require('../middleware/auth');
const { supabaseAdmin } = require('../config/database');

// GET /api/ai/suggest-tasks — AI task suggestions
router.get('/suggest-tasks', authenticate, async (req, res) => {
    try {
        const { data: profile } = await supabaseAdmin
            .from('profiles')
            .select('*, wards(*)')
            .eq('id', req.userId)
            .single();

        const suggestions = await aiService.suggestTasks(profile, profile.wards);

        res.json({
            success: true,
            data: suggestions
        });
    } catch (error) {
        console.error('AI suggest tasks error:', error);
        res.status(500).json({
            success: false,
            message: 'Failed to generate task suggestions.'
        });
    }
});

// POST /api/ai/analyze-issue — AI issue analysis
router.post('/analyze-issue', authenticate, async (req, res) => {
    try {
        const { description, category } = req.body;
        const analysis = await aiService.analyzeIssue(description, category);

        res.json({
            success: true,
            data: { analysis }
        });
    } catch (error) {
        console.error('AI analyze error:', error);
        res.status(500).json({
            success: false,
            message: 'Failed to analyze issue.'
        });
    }
});

// GET /api/ai/ward-quiz/:wardId — Generate ward quiz
router.get('/ward-quiz/:wardId', authenticate, async (req, res) => {
    try {
        const { data: ward } = await supabaseAdmin
            .from('wards')
            .select('*')
            .eq('id', req.params.wardId)
            .single();

        if (!ward) {
            return res.status(404).json({
                success: false,
                message: 'Ward not found.'
            });
        }

        const questions = await aiService.generateWardQuiz(ward);

        res.json({
            success: true,
            data: questions
        });
    } catch (error) {
        console.error('AI quiz error:', error);
        res.status(500).json({
            success: false,
            message: 'Failed to generate quiz.'
        });
    }
});

// POST /api/ai/portfolio-summary — Generate portfolio summary
router.post('/portfolio-summary', authenticate, async (req, res) => {
    try {
        const { data: profile } = await supabaseAdmin
            .from('profiles')
            .select('*')
            .eq('id', req.userId)
            .single();

        const summary = await aiService.generatePortfolioSummary(profile);

        res.json({
            success: true,
            data: { summary }
        });
    } catch (error) {
        console.error('AI summary error:', error);
        res.status(500).json({
            success: false,
            message: 'Failed to generate summary.'
        });
    }
});

module.exports = router;

Register AI routes in server/server.js

// Add this with other route imports
const aiRoutes = require('./src/routes/ai');

// Add this with other route mountings
app.use('/api/ai', aiRoutes);

6.3 AI Ward Quiz Component (Frontend)

client/src/components/tasks/WardQuiz.js

'use client';
// ====================================
// Ward Quiz Component
// ====================================
import { useState, useEffect } from 'react';
import { useAuth } from '@/context/AuthContext';
import api from '@/lib/api';
import { motion, AnimatePresence } from 'framer-motion';
import Confetti from 'react-confetti';
import toast from 'react-hot-toast';

export default function WardQuiz({ wardId, onComplete }) {
    const { profile } = useAuth();
    const [questions, setQuestions] = useState([]);
    const [currentIndex, setCurrentIndex] = useState(0);
    const [selectedAnswer, setSelectedAnswer] = useState(null);
    const [showResult, setShowResult] = useState(false);
    const [score, setScore] = useState(0);
    const [loading, setLoading] = useState(true);
    const [quizComplete, setQuizComplete] = useState(false);

    useEffect(() => {
        fetchQuiz();
    }, [wardId]);

    const fetchQuiz = async () => {
        try {
            const response = await api.get(`/ai/ward-quiz/${wardId || profile?.ward_id}`);
            if (response.success && response.data.length > 0) {
                setQuestions(response.data);
            }
        } catch (error) {
            console.error('Quiz fetch error:', error);
            toast.error('Failed to load quiz');
        } finally {
            setLoading(false);
        }
    };

    const handleAnswer = (optionIndex) => {
        if (showResult) return;
        setSelectedAnswer(optionIndex);
        setShowResult(true);

        const correct = questions[currentIndex].correct === optionIndex;
        if (correct) {
            setScore(prev => prev + 1);
        }
    };

    const nextQuestion = () => {
        if (currentIndex < questions.length - 1) {
            setCurrentIndex(prev => prev + 1);
            setSelectedAnswer(null);
            setShowResult(false);
        } else {
            setQuizComplete(true);
            if (onComplete) {
                onComplete(score + (selectedAnswer === questions[currentIndex].correct ? 1 : 0));
            }
        }
    };

    if (loading) {
        return (
            <div className="card text-center py-10">
                <div className="text-4xl animate-bounce mb-4">🧠</div>
                <p className="text-slate-400">Generating quiz questions...</p>
            </div>
        );
    }

    if (questions.length === 0) {
        return (
            <div className="card text-center py-10">
                <p className="text-slate-400">Could not generate quiz. Try again later.</p>
            </div>
        );
    }

    if (quizComplete) {
        const passed = score >= Math.ceil(questions.length * 0.6);
        return (
            <div className="card text-center py-10">
                {passed && <Confetti recycle={false} numberOfPieces={200} />}
                <div className="text-5xl mb-4">{passed ? '🎉' : '📚'}</div>
                <h2 className="text-xl font-bold mb-2">
                    {passed ? 'Great Job!' : 'Keep Learning!'}
                </h2>
                <p className="text-slate-400 mb-4">
                    You scored <span className="text-white font-bold">{score}/{questions.length}</span>
                </p>
                {passed && (
                    <p className="text-emerald-400 font-semibold">
                        +15 XP earned! ⭐
                    </p>
                )}
                <button
                    onClick={() => {
                        setCurrentIndex(0);
                        setScore(0);
                        setSelectedAnswer(null);
                        setShowResult(false);
                        setQuizComplete(false);
                        fetchQuiz();
                    }}
                    className="btn-secondary mt-4"
                >
                    Try Again
                </button>
            </div>
        );
    }

    const question = questions[currentIndex];

    return (
        <div className="card">
            {/* Progress */}
            <div className="flex items-center justify-between mb-4">
                <span className="text-sm text-slate-400">
                    Question {currentIndex + 1} of {questions.length}
                </span>
                <span className="text-sm text-indigo-400 font-semibold">
                    Score: {score}
                </span>
            </div>
            <div className="w-full h-1.5 bg-slate-700 rounded-full mb-6">
                <div
                    className="h-full bg-indigo-500 rounded-full transition-all"
                    style={{ width: `${((currentIndex + 1) / questions.length) * 100}%` }}
                />
            </div>

            {/* Question */}
            <AnimatePresence mode="wait">
                <motion.div
                    key={currentIndex}
                    initial={{ opacity: 0, x: 20 }}
                    animate={{ opacity: 1, x: 0 }}
                    exit={{ opacity: 0, x: -20 }}
                >
                    <h3 className="text-lg font-semibold mb-6">{question.question}</h3>

                    <div className="space-y-3">
                        {question.options.map((option, i) => {
                            let optionStyle = 'bg-slate-700/50 hover:bg-slate-700 border-slate-600';
                            if (showResult) {
                                if (i === question.correct) {
                                    optionStyle = 'bg-emerald-500/20 border-emerald-500 text-emerald-400';
                                } else if (i === selectedAnswer && i !== question.correct) {
                                    optionStyle = 'bg-red-500/20 border-red-500 text-red-400';
                                } else {
                                    optionStyle = 'bg-slate-700/30 border-slate-700 opacity-50';
                                }
                            }

                            return (
                                <button
                                    key={i}
                                    onClick={() => handleAnswer(i)}
                                    disabled={showResult}
                                    className={`w-full text-left p-4 rounded-xl border transition-all ${optionStyle}`}
                                >
                                    <span className="font-medium mr-2">
                                        {String.fromCharCode(65 + i)}.
                                    </span>
                                    {option}
                                </button>
                            );
                        })}
                    </div>

                    {/* Explanation */}
                    {showResult && question.explanation && (
                        <motion.div
                            initial={{ opacity: 0, y: 10 }}
                            animate={{ opacity: 1, y: 0 }}
                            className="mt-4 p-4 rounded-xl bg-slate-700/30 border border-slate-600/30"
                        >
                            <p className="text-sm text-slate-300">
                                💡 <strong>Explanation:</strong> {question.explanation}
                            </p>
                        </motion.div>
                    )}

                    {/* Next Button */}
                    {showResult && (
                        <motion.button
                            initial={{ opacity: 0 }}
                            animate={{ opacity: 1 }}
                            onClick={nextQuestion}
                            className="btn-primary w-full mt-6"
                        >
                            {currentIndex < questions.length - 1 ? 'Next Question →' : 'See Results 🎯'}
                        </motion.button>
                    )}
                </motion.div>
            </AnimatePresence>
        </div>
    );
}