ClareCourseWare / web /src /components /TeacherDashboard.tsx
claudqunwang's picture
添加持续对话功能:支持每个功能模块与AI进行多轮对话和互动
6a91b07
// 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>
);
}