- ai: update chat-panel, child-summary, error-book-analysis, grading-assist, lesson-content-generator, markdown-renderer, question-variant-generator, study-path, usage-dashboard, use-ai-chat-stream, use-position-persistence - adaptive-practice: update practice-result-view, practice-session-view, practice-starter, data-access-analytics, lib/type-guards - announcements: update announcement-card, detail, form, data-access - attendance: update attendance-record-list, attendance-rules-form, attendance-sheet, data-access - audit: update actions, audit-log-export-button, audit-retention-settings, data-access - auth: update login-form, register-form, data-access
201 lines
7.2 KiB
TypeScript
201 lines
7.2 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { useTranslations } from "next-intl"
|
||
import { Sparkles, CheckCircle, Clock, AlertCircle, Target } from "lucide-react"
|
||
import { notify } from "@/shared/lib/notify"
|
||
|
||
import { Button } from "@/shared/components/ui/button"
|
||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/ui/card"
|
||
import { Badge } from "@/shared/components/ui/badge"
|
||
import { SectionErrorBoundary } from "@/shared/components/section-error-boundary"
|
||
import { SkeletonCard } from "@/shared/components/ui/skeleton"
|
||
import { useAiClient } from "@/modules/ai/context/ai-client-provider"
|
||
import type { StudyPathResult, StudyPathInput } from "@/modules/ai/types"
|
||
|
||
type AiStudyPathProps = {
|
||
studentId: string
|
||
subject?: string
|
||
currentMastery?: StudyPathInput["currentMastery"]
|
||
learningGoal?: string
|
||
/** 教材 ID(传入后自动获取知识图谱注入路径推荐,对标 Squirrel AI) */
|
||
textbookId?: string
|
||
onStartLearning?: (step: StudyPathResult["learningPath"][number]) => void
|
||
}
|
||
|
||
/**
|
||
* 学生学习路径推荐组件
|
||
*
|
||
* 参考 Squirrel AI 纳米级知识图谱和 Century Tech 自适应路径。
|
||
* 为学生生成个性化学习路径:
|
||
* - 当前水平评估
|
||
* - 分步骤学习路径(含状态、建议、预计时间)
|
||
* - 学习总结
|
||
* - 鼓励语
|
||
*
|
||
* V3:支持 textbookId 参数,自动获取知识图谱与掌握度数据,
|
||
* 使 AI 沿前置依赖链推荐学习路径。
|
||
*/
|
||
export function AiStudyPath({
|
||
studentId,
|
||
subject,
|
||
currentMastery,
|
||
learningGoal,
|
||
textbookId,
|
||
onStartLearning,
|
||
}: AiStudyPathProps): React.ReactNode {
|
||
const t = useTranslations("ai")
|
||
const aiClient = useAiClient()
|
||
const [loading, setLoading] = useState(false)
|
||
const [path, setPath] = useState<StudyPathResult | null>(null)
|
||
|
||
const handleGenerate = async (): Promise<void> => {
|
||
if (!aiClient.recommendStudyPath) {
|
||
notify.error(t("studyPath.error"))
|
||
return
|
||
}
|
||
setLoading(true)
|
||
try {
|
||
const result = await aiClient.recommendStudyPath({
|
||
studentId,
|
||
subject,
|
||
currentMastery,
|
||
learningGoal,
|
||
textbookId,
|
||
})
|
||
if (result.success && result.data) {
|
||
setPath(result.data)
|
||
notify.success(t("studyPath.title"))
|
||
} else {
|
||
notify.error(result.message ?? t("studyPath.error"))
|
||
}
|
||
} catch {
|
||
notify.error(t("studyPath.error"))
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const statusConfig = {
|
||
mastered: {
|
||
icon: CheckCircle,
|
||
color: "text-green-500",
|
||
badge: "secondary" as const,
|
||
label: t("studyPath.mastered"),
|
||
},
|
||
in_progress: {
|
||
icon: Clock,
|
||
color: "text-blue-500",
|
||
badge: "default" as const,
|
||
label: t("studyPath.inProgress"),
|
||
},
|
||
needs_work: {
|
||
icon: AlertCircle,
|
||
color: "text-orange-500",
|
||
badge: "destructive" as const,
|
||
label: t("studyPath.needsWork"),
|
||
},
|
||
}
|
||
|
||
return (
|
||
<SectionErrorBoundary namespace="ai">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2">
|
||
<Target className="h-4 w-4 text-primary" />
|
||
{t("studyPath.title")}
|
||
</CardTitle>
|
||
<CardDescription>{t("studyPath.description")}</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{loading ? (
|
||
<SkeletonCard variant="chart" />
|
||
) : path ? (
|
||
<div className="space-y-4">
|
||
{/* 当前水平 */}
|
||
<div className="rounded-md bg-primary/5 border border-primary/20 p-3">
|
||
<p className="text-sm font-medium text-primary">{path.currentLevel}</p>
|
||
</div>
|
||
|
||
{/* 学习路径步骤 */}
|
||
<div className="space-y-3">
|
||
<h4 className="text-sm font-medium">{t("studyPath.nextSteps")}</h4>
|
||
{path.learningPath.map((step, index) => {
|
||
const config = statusConfig[step.status]
|
||
const Icon = config.icon
|
||
return (
|
||
<div
|
||
key={index}
|
||
className="rounded-md border p-3 space-y-2 relative"
|
||
>
|
||
{/* 连接线 */}
|
||
{index < path.learningPath.length - 1 ? (
|
||
<div className="absolute left-5 top-12 bottom-0 w-px bg-border" />
|
||
) : null}
|
||
|
||
<div className="flex items-start gap-3">
|
||
<div className={`mt-0.5 ${config.color}`}>
|
||
<Icon className="h-4 w-4" />
|
||
</div>
|
||
<div className="flex-1 space-y-1">
|
||
<div className="flex items-center justify-between gap-2">
|
||
<span className="text-sm font-medium">
|
||
{step.step}. {step.knowledgePoint}
|
||
</span>
|
||
<Badge variant={config.badge} className="text-xs">
|
||
{config.label}
|
||
</Badge>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">{step.recommendedAction}</p>
|
||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||
<Clock className="h-3 w-3" />
|
||
<span>{step.estimatedTime}</span>
|
||
</div>
|
||
{onStartLearning && step.status !== "mastered" ? (
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
className="mt-1 h-7 text-xs"
|
||
onClick={() => onStartLearning(step)}
|
||
>
|
||
{t("studyPath.startLearning")}
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 学习总结 */}
|
||
<div className="space-y-1">
|
||
<h4 className="text-sm font-medium">{t("studyPath.title")}</h4>
|
||
<p className="text-sm text-muted-foreground">{path.summary}</p>
|
||
</div>
|
||
|
||
{/* 鼓励语 */}
|
||
<div className="rounded-md bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900 p-3">
|
||
<p className="text-sm text-green-700 dark:text-green-400 flex items-start gap-2">
|
||
<Sparkles className="h-4 w-4 mt-0.5 shrink-0" />
|
||
{path.motivation}
|
||
</p>
|
||
</div>
|
||
|
||
<Button type="button" variant="outline" size="sm" onClick={handleGenerate} className="w-full">
|
||
{t("suggestion.regenerate")}
|
||
</Button>
|
||
</div>
|
||
) : (
|
||
<Button type="button" variant="outline" size="sm" onClick={handleGenerate} className="w-full">
|
||
<Sparkles className="mr-1 h-3.5 w-3.5" />
|
||
{t("studyPath.generate")}
|
||
</Button>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</SectionErrorBoundary>
|
||
)
|
||
}
|