import { useState, type FormEvent } from 'react'; import { fmtDate } from '@/lib/format'; import { Link } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ArrowLeft, Upload, Trash2, Eye, EyeOff, RefreshCw, UserPlus, Copy, Check, Shield, ShieldOff, Lock, Mail, Send } from 'lucide-react'; import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { api } from '@/lib/api'; import { Button } from '@/components/ui/button'; import { Spinner } from '@/components/ui/spinner'; import { uqo } from '@/theme/uqo'; interface Dashboard { days: number; messages_per_day: { day: string; count: number }[]; active_students: number; total_users: number; conversations: number; peak_hours: { hour: number; count: number }[]; top_topics: { topic: string; course: string; count: number }[]; question_samples: { question: string; topic: string; course: string; at: string }[]; feedback: Record; tool_usage: { tool: string; count: number }[]; budget: { budget_usd: number; spent_usd: number; ratio: number }; costs_by_course: { course: string; cost_usd: number }[]; } interface CourseSettings { code: string; title: string; extra_system_prompt: string; announcement: string; deadlines: { label: string; date: string }[]; suggestions: string[]; settings: { tools_enabled?: string[] | null; model_primary?: string | null; budget_usd?: number } } interface Settings { courses: CourseSettings[]; tools: string[]; models: Record; budget_usd: number } interface Content { documents: { id: string; course: string; filename: string; title: string; visibility: string; n_chunks: number; ingested_at: string }[]; index_size: number; jobs: Record } const TABS = ['Activité', 'Étudiants', 'Contenu', 'Réglages'] as const; export function ProfessorPage() { const [tab, setTab] = useState<(typeof TABS)[number]>('Activité'); return (

Tableau de bord professeur

IMM1003 · IMM1033 — données anonymisées

{tab === 'Activité' && } {tab === 'Étudiants' && } {tab === 'Contenu' && } {tab === 'Réglages' && }
); } function Card({ title, children, className = '' }: { title: string; children: React.ReactNode; className?: string }) { return

{title}

{children}
; } function Stat({ label, value }: { label: string; value: string | number }) { return
{label}
{value}
; } function Activity() { const { data, isLoading } = useQuery({ queryKey: ['dashboard'], queryFn: () => api('/professor/dashboard?days=30') }); if (isLoading || !data) return
; return (
d.slice(5)} fontSize={11} />
a.hour - b.hour)}> `${h}h`} />
{data.top_topics.length === 0 &&

Pas encore de données.

}
    {data.top_topics.map((t, i) => (
  1. {i + 1}.{t.topic}{t.course}{t.count}
  2. ))}
👍 {data.feedback.up || 0}👎 {data.feedback.down || 0}
    {data.tool_usage.map((t) =>
  • {t.tool}{t.count}
  • )}
    {data.question_samples.map((q, i) =>
  • {q.course} · {q.topic || '—'}{q.question}
  • )} {data.question_samples.length === 0 &&
  • Aucune question pour l'instant.
  • }
); } function ContentTab() { const qc = useQueryClient(); const { data, refetch } = useQuery({ queryKey: ['content'], queryFn: () => api('/professor/content'), refetchInterval: 5000 }); const [course, setCourse] = useState('IMM1033'); const [visibility, setVisibility] = useState('students'); const [title, setTitle] = useState(''); const [file, setFile] = useState(null); const upload = useMutation({ mutationFn: async () => { if (!file) return; const fd = new FormData(); fd.append('file', file); fd.append('course', course); fd.append('visibility', visibility); fd.append('title', title); await api('/professor/content', { method: 'POST', body: fd }); }, onSuccess: () => { setFile(null); setTitle(''); qc.invalidateQueries({ queryKey: ['content'] }); }, }); const submit = (e: FormEvent) => { e.preventDefault(); upload.mutate(); }; return (
setTitle(e.target.value)} placeholder="Titre / module (optionnel)" className="h-11 rounded-xl border border-neutral-line px-3 text-sm sm:col-span-4" />
{upload.isError &&

{(upload.error as Error).message}

} {data && Object.keys(data.jobs).length > 0 && (
    {Object.entries(data.jobs).map(([id, j]) =>
  • {j.filename} — {j.status}{j.chunks ? ` (${j.chunks} passages)` : ''}{j.error ? ` : ${j.error}` : ''}
  • )}
)}
{data?.documents.map((d) => ( ))}
CoursDocumentPassagesVisibilité
{d.course}
{d.title || d.filename}
{d.filename}
{d.n_chunks}
); } function SettingsTab() { const qc = useQueryClient(); const { data } = useQuery({ queryKey: ['prof-settings'], queryFn: () => api('/professor/settings') }); const [promote, setPromote] = useState(''); if (!data) return
; return (
{data.courses.map((c) => qc.invalidateQueries({ queryKey: ['prof-settings'] })} defaultModel={data.models.primary} />)}
{Object.entries(data.models).map(([k, v]) => <>
{k}
{v}
)}

Budget mensuel global : {data.budget_usd} $ US. Au-delà de 100 %, le tuteur bascule automatiquement sur le modèle économique.

{ e.preventDefault(); api('/professor/promote', { method: 'POST', body: JSON.stringify({ email: promote }) }).then(() => { alert('Compte promu.'); setPromote(''); }).catch((er) => alert(er.message)); }} className="flex gap-2"> setPromote(e.target.value)} placeholder="collegue@uqo.ca" className="h-11 flex-1 rounded-xl border border-neutral-line px-3 text-sm" />
); } function CourseForm({ c, tools, onSaved, defaultModel }: { c: CourseSettings; tools: string[]; onSaved: () => void; defaultModel: string }) { const [prompt, setPrompt] = useState(c.extra_system_prompt || ''); const [announcement, setAnnouncement] = useState(c.announcement || ''); const [deadlines, setDeadlines] = useState((c.deadlines || []).map((d) => `${d.label} | ${d.date}`).join('\n')); const [suggestions, setSuggestions] = useState((c.suggestions || []).join('\n')); const [enabled, setEnabled] = useState(c.settings?.tools_enabled || tools); const [model, setModel] = useState(c.settings?.model_primary || ''); const [budget, setBudget] = useState(String(c.settings?.budget_usd ?? '')); const save = useMutation({ mutationFn: () => api(`/professor/settings/${c.code}`, { method: 'PATCH', body: JSON.stringify({ extra_system_prompt: prompt, announcement, deadlines: deadlines.split('\n').map((l) => l.trim()).filter(Boolean).map((l) => { const [label, date] = l.split('|').map((s) => s.trim()); return { label, date: date || '' }; }), suggestions: suggestions.split('\n').map((s) => s.trim()).filter(Boolean), settings: { tools_enabled: enabled.length === tools.length ? null : enabled, model_primary: model || null, budget_usd: budget ? Number(budget) : undefined }, }), }), onSuccess: onSaved, }); return (