Spaces:
Sleeping
Sleeping
| // web/src/components/TeacherDashboard.tsx | |
| // AI 智能建课:课程描述、文档建议、作业题库、评估分析 + Courseware(愿景、活动、助教、QA 优化、教案 PPT) | |
| import React, { useState, useEffect } from "react"; | |
| import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"; | |
| import { Button } from "./ui/button"; | |
| import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; | |
| import { Lightbulb, FileText, ClipboardList, BarChart3, Target, CalendarDays, MessageSquare, TrendingUp, BookOpen, Languages } from "lucide-react"; | |
| import { | |
| apiTeacherStatus, | |
| apiTeacherCourseDescription, | |
| apiTeacherDocSuggestion, | |
| apiTeacherAssignmentQuestions, | |
| apiTeacherAssessmentAnalysis, | |
| apiCoursewareVision, | |
| apiCoursewareActivities, | |
| apiCoursewareCopilot, | |
| apiCoursewareQAOptimize, | |
| apiCoursewareContent, | |
| type TeacherStatus, | |
| } from "../lib/api"; | |
| import { toast } from "sonner"; | |
| import { TeacherChatPage } from "./TeacherChatPage"; | |
| import { i18n, getText, type Language } from "./teacherI18n"; | |
| import type { User } from "../App"; | |
| type FeatureId = | |
| | "course-description" | |
| | "doc-suggestion" | |
| | "assignment-questions" | |
| | "assessment-analysis" | |
| | "vision" | |
| | "activities" | |
| | "copilot" | |
| | "qa-optimize" | |
| | "content"; | |
| // Features will be generated dynamically based on language | |
| function getFeatures(lang: Language): { id: FeatureId; title: string; desc: string; icon: React.ReactNode }[] { | |
| return [ | |
| { | |
| id: "course-description", | |
| title: i18n[lang].features["course-description"].title, | |
| desc: i18n[lang].features["course-description"].desc, | |
| icon: <FileText className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "doc-suggestion", | |
| title: i18n[lang].features["doc-suggestion"].title, | |
| desc: i18n[lang].features["doc-suggestion"].desc, | |
| icon: <Lightbulb className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "assignment-questions", | |
| title: i18n[lang].features["assignment-questions"].title, | |
| desc: i18n[lang].features["assignment-questions"].desc, | |
| icon: <ClipboardList className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "assessment-analysis", | |
| title: i18n[lang].features["assessment-analysis"].title, | |
| desc: i18n[lang].features["assessment-analysis"].desc, | |
| icon: <BarChart3 className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "vision", | |
| title: i18n[lang].features.vision.title, | |
| desc: i18n[lang].features.vision.desc, | |
| icon: <Target className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "activities", | |
| title: i18n[lang].features.activities.title, | |
| desc: i18n[lang].features.activities.desc, | |
| icon: <CalendarDays className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "copilot", | |
| title: i18n[lang].features.copilot.title, | |
| desc: i18n[lang].features.copilot.desc, | |
| icon: <MessageSquare className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "qa-optimize", | |
| title: i18n[lang].features["qa-optimize"].title, | |
| desc: i18n[lang].features["qa-optimize"].desc, | |
| icon: <TrendingUp className="h-6 w-6" />, | |
| }, | |
| { | |
| id: "content", | |
| title: i18n[lang].features.content.title, | |
| desc: i18n[lang].features.content.desc, | |
| icon: <BookOpen className="h-6 w-6" />, | |
| }, | |
| ]; | |
| } | |
| // This will be called dynamically | |
| type Props = { | |
| user: User; | |
| onBack: () => void; | |
| }; | |
| export function TeacherDashboard({ user, onBack }: Props) { | |
| const [currentChatFeature, setCurrentChatFeature] = useState<FeatureId | null>(null); | |
| const [uiLanguage, setUiLanguage] = useState<Language>("en"); | |
| const [status, setStatus] = useState<TeacherStatus | null>(null); | |
| const [replyLanguage, setReplyLanguage] = useState<"zh" | "en" | "auto">("en"); | |
| // UI language follows reply language (except "auto") | |
| useEffect(() => { | |
| if (replyLanguage !== "auto") { | |
| setUiLanguage(replyLanguage); | |
| } | |
| }, [replyLanguage]); | |
| const replyLangParam = replyLanguage === "auto" ? undefined : replyLanguage; | |
| const features = getFeatures(uiLanguage); | |
| const BASE_FEATURES = features.slice(0, 4); | |
| const COURSEWARE_FEATURES = features.slice(4, 9); | |
| useEffect(() => { | |
| apiTeacherStatus() | |
| .then(setStatus) | |
| .catch(() => setStatus({ weaviate_configured: false, features: [] })); | |
| }, []); | |
| // Handler functions for chat page | |
| const getSubmitHandler = (featureId: FeatureId) => { | |
| return async ( | |
| inputs: Record<string, string>, | |
| files: File[], | |
| history?: Array<[string, string]> | |
| ): Promise<string> => { | |
| const historyParam = history && history.length > 0 ? history : null; | |
| switch (featureId) { | |
| case "course-description": { | |
| const res = await apiTeacherCourseDescription({ | |
| topic: inputs.topic || "", | |
| outline_hint: inputs.outline || undefined, | |
| reply_language: replyLangParam, | |
| history: historyParam, | |
| userMessage: inputs.userMessage || undefined, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.description; | |
| } | |
| case "doc-suggestion": { | |
| const res = await apiTeacherDocSuggestion({ | |
| topic: inputs.topic || "", | |
| current_doc_excerpt: inputs.excerpt || undefined, | |
| doc_type: inputs.docType || "Lecture Notes / Slides", | |
| reply_language: replyLangParam, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.suggestion; | |
| } | |
| case "assignment-questions": { | |
| const res = await apiTeacherAssignmentQuestions({ | |
| topic: inputs.topic || "", | |
| week_or_module: inputs.week || undefined, | |
| question_type: inputs.questionType || "Mixed", | |
| reply_language: replyLangParam, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.suggestion; | |
| } | |
| case "assessment-analysis": { | |
| const res = await apiTeacherAssessmentAnalysis({ | |
| assessment_summary: inputs.summary || "", | |
| course_topic_hint: inputs.topic || undefined, | |
| reply_language: replyLangParam, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.analysis; | |
| } | |
| case "vision": { | |
| const res = await apiCoursewareVision({ | |
| course_info: inputs.courseInfo || "", | |
| syllabus: inputs.syllabus || "", | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.content; | |
| } | |
| case "activities": { | |
| const res = await apiCoursewareActivities({ | |
| topic: inputs.topic || "", | |
| learning_objectives: inputs.objectives || undefined, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.content; | |
| } | |
| case "copilot": { | |
| const profiles = inputs.profiles?.trim() | |
| ? inputs.profiles | |
| .split("\n") | |
| .filter((l) => l.trim()) | |
| .map((l) => { | |
| const parts = l.split(/[::]/).map((s) => s.trim()); | |
| return { name: parts[0], progress: parts[1], behavior: parts[2] }; | |
| }) | |
| : undefined; | |
| const res = await apiCoursewareCopilot({ | |
| current_content: inputs.content || "", | |
| student_profiles: profiles, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.content; | |
| } | |
| case "qa-optimize": { | |
| const res = await apiCoursewareQAOptimize({ | |
| quiz_summary: inputs.summary || "", | |
| course_topic: inputs.topic || undefined, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.content; | |
| } | |
| case "content": { | |
| const res = await apiCoursewareContent({ | |
| topic: inputs.topic || "", | |
| duration: inputs.duration || undefined, | |
| outline_points: inputs.outline || undefined, | |
| history: historyParam, | |
| }); | |
| if (res.weaviate_used) toast.success(getText(uiLanguage, "knowledgeBaseUsed")); | |
| return res.content; | |
| } | |
| default: | |
| throw new Error("Unknown feature"); | |
| } | |
| }; | |
| }; | |
| // Show chat page if a feature is selected | |
| if (currentChatFeature) { | |
| const feature = features.find((f) => f.id === currentChatFeature); | |
| if (!feature) { | |
| setCurrentChatFeature(null); | |
| return null; | |
| } | |
| return ( | |
| <TeacherChatPage | |
| featureId={currentChatFeature} | |
| featureTitle={feature.title} | |
| featureDesc={feature.desc} | |
| onBack={() => setCurrentChatFeature(null)} | |
| userId={user.email} | |
| replyLanguage={replyLanguage} | |
| onSubmit={getSubmitHandler(currentChatFeature)} | |
| /> | |
| ); | |
| } | |
| return ( | |
| <div className="h-full flex flex-col bg-background"> | |
| <div className="flex-shrink-0 flex items-center gap-4 px-4 py-3 border-b border-border"> | |
| <div> | |
| <h1 className="text-lg font-semibold">{getText(uiLanguage, "title")}</h1> | |
| <p className="text-sm text-muted-foreground"> | |
| {getText(uiLanguage, "subtitle")} | |
| {status?.weaviate_configured && ( | |
| <span className="ml-2 text-green-600 dark:text-green-400"> | |
| {getText(uiLanguage, "knowledgeBaseConnected")} | |
| </span> | |
| )} | |
| </p> | |
| </div> | |
| <div className="ml-auto flex items-center gap-2"> | |
| <Languages className="h-4 w-4 text-muted-foreground" /> | |
| <Select value={replyLanguage} onValueChange={(v: "zh" | "en" | "auto") => setReplyLanguage(v)}> | |
| <SelectTrigger className="w-[140px]"> | |
| <SelectValue placeholder={getText(uiLanguage, "replyLanguage")} /> | |
| </SelectTrigger> | |
| <SelectContent> | |
| <SelectItem value="zh">中文</SelectItem> | |
| <SelectItem value="en">English</SelectItem> | |
| <SelectItem value="auto">{getText(uiLanguage, "followInput")}</SelectItem> | |
| </SelectContent> | |
| </Select> | |
| <Button variant="ghost" size="sm" onClick={onBack} className="ml-2"> | |
| {getText(uiLanguage, "logout")} | |
| </Button> | |
| </div> | |
| </div> | |
| <div className="flex-1 overflow-auto p-4 md:p-6"> | |
| <div className="max-w-5xl mx-auto space-y-8"> | |
| <section> | |
| <h2 className="text-sm font-medium text-muted-foreground mb-3 px-1"> | |
| {getText(uiLanguage, "basicCourseBuilding")} | |
| </h2> | |
| <div className="grid gap-6 md:grid-cols-2"> | |
| {BASE_FEATURES.map((f) => ( | |
| <Card key={f.id} className="flex flex-col"> | |
| <CardHeader className="pb-2"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-muted-foreground">{f.icon}</span> | |
| <CardTitle className="text-base">{f.title}</CardTitle> | |
| </div> | |
| <CardDescription>{f.desc}</CardDescription> | |
| </CardHeader> | |
| <CardContent className="flex-1 flex flex-col gap-3"> | |
| <Button onClick={() => setCurrentChatFeature(f.id)} className="w-full"> | |
| {getText(uiLanguage, "startUsing")} | |
| </Button> | |
| {history[f.id]?.length > 0 && ( | |
| <div className="mt-2 space-y-2"> | |
| <p className="text-xs font-medium text-muted-foreground"> | |
| {getText(uiLanguage, "history", { count: Math.min(3, history[f.id].length) })} | |
| </p> | |
| {history[f.id].slice(0, 3).map((item, i) => ( | |
| <div key={i} className="rounded border border-border bg-muted/40 p-2 text-xs"> | |
| <p className="truncate text-muted-foreground">{item.prompt}</p> | |
| <p className="mt-1 line-clamp-2 whitespace-pre-wrap">{item.result}</p> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| </section> | |
| <section> | |
| <h2 className="text-sm font-medium text-muted-foreground mb-3 px-1"> | |
| {getText(uiLanguage, "coursewareAdvanced")} | |
| </h2> | |
| <div className="grid gap-6 md:grid-cols-2"> | |
| {COURSEWARE_FEATURES.map((f) => ( | |
| <Card key={f.id} className="flex flex-col"> | |
| <CardHeader className="pb-2"> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-muted-foreground">{f.icon}</span> | |
| <CardTitle className="text-base">{f.title}</CardTitle> | |
| </div> | |
| <CardDescription>{f.desc}</CardDescription> | |
| </CardHeader> | |
| <CardContent className="flex-1 flex flex-col gap-3"> | |
| <Button onClick={() => setCurrentChatFeature(f.id)} className="w-full"> | |
| {getText(uiLanguage, "startUsing")} | |
| </Button> | |
| {history[f.id]?.length > 0 && ( | |
| <div className="mt-2 space-y-2"> | |
| <p className="text-xs font-medium text-muted-foreground"> | |
| {getText(uiLanguage, "history", { count: Math.min(3, history[f.id].length) })} | |
| </p> | |
| {history[f.id].slice(0, 3).map((item, i) => ( | |
| <div key={i} className="rounded border border-border bg-muted/40 p-2 text-xs"> | |
| <p className="truncate text-muted-foreground">{item.prompt}</p> | |
| <p className="mt-1 line-clamp-2 whitespace-pre-wrap">{item.result}</p> | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| </section> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |