Files
NextEdu/src/modules/ai/components/ai-study-path.tsx
SpecialX b090a815ae feat(modules-edu): update ai, adaptive-practice, announcements, attendance, audit, auth
- 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
2026-07-07 16:18:29 +08:00

201 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
)
}