feat(ai): 新增 AI 模块并集成至备课/错题集/试卷/改题四大业务场景
- 新增 src/modules/ai 独立模块,遵循三层架构(actions → services → shared/lib/ai) - 通过 AiClientProvider + useAiClient 实现 React Context 依赖注入,业务组件零直接 import - 6 个 Server Actions 均调用 requirePermission() 权限校验,返回 ActionState<T> - withAiTracking 统一埋点,覆盖 chat/similar_question/grading_assist/lesson_content/question_variant/weakness_analysis - 集成场景:作业批改 AiGradingAssist、错题集 AiErrorBookAnalysis、备课 AiLessonContentGenerator、试卷 AiQuestionVariantGenerator - 全量 i18n(en/zh-CN ai.json),Error Boundary + Skeleton 边界处理 - 同步架构图 004/005,新增审计报告 ai-module-audit-report.md
This commit is contained in:
88
src/modules/ai/components/ai-error-boundary.tsx
Normal file
88
src/modules/ai/components/ai-error-boundary.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import { Component, type ReactNode } from "react"
|
||||
import { AlertCircle, RefreshCw } from "lucide-react"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
||||
|
||||
type AiErrorBoundaryProps = {
|
||||
children: ReactNode
|
||||
/** 自定义 fallback 渲染 */
|
||||
fallback?: (error: Error, reset: () => void) => ReactNode
|
||||
/** 错误回调(用于埋点) */
|
||||
onError?: (error: Error, info: unknown) => void
|
||||
}
|
||||
|
||||
type AiErrorBoundaryState = {
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 专用 Error Boundary
|
||||
*
|
||||
* 包裹所有 AI 数据区块,防止单个 AI 调用失败导致整页崩溃。
|
||||
* 提供重试按钮与友好的错误提示。
|
||||
*/
|
||||
export class AiErrorBoundary extends Component<
|
||||
AiErrorBoundaryProps,
|
||||
AiErrorBoundaryState
|
||||
> {
|
||||
constructor(props: AiErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = { error: null }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): AiErrorBoundaryState {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, info: unknown): void {
|
||||
if (this.props.onError) {
|
||||
this.props.onError(error, info)
|
||||
}
|
||||
}
|
||||
|
||||
private handleReset = (): void => {
|
||||
this.setState({ error: null })
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback(this.state.error, this.handleReset)
|
||||
}
|
||||
return <DefaultAiErrorFallback error={this.state.error} onReset={this.handleReset} />
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultAiErrorFallback({
|
||||
error,
|
||||
onReset,
|
||||
}: {
|
||||
error: Error
|
||||
onReset: () => void
|
||||
}): ReactNode {
|
||||
const t = useTranslations("ai")
|
||||
return (
|
||||
<Card className="border-destructive/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
{t("error.boundaryTitle")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground">{t("error.boundaryDescription")}</p>
|
||||
<p className="text-xs text-muted-foreground/70 font-mono">{error.message}</p>
|
||||
<Button type="button" variant="outline" size="sm" onClick={onReset}>
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
{t("error.retry")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user