feat: RAG inline citations, source highlighting, and admin panel
- Increase RAG retrieval from 5 to 50 docs with relevance threshold (0.15) - LLM inline citations with [来源N] format and reference list - Clickable citation links scroll to source cards with highlight animation - Source previews with full chunk text and answer-matched highlighting - Message-scoped source IDs to fix cross-round citation targeting - Admin panel pages (knowledge, users, forum, course, settings) - Add rehype-raw dependency for HTML-in-markdown rendering Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { BookOpen } from "lucide-react";
|
||||
import { courseContentAPI } from "@/lib/api";
|
||||
|
||||
interface Subsection {
|
||||
id: number;
|
||||
subsection_number: number;
|
||||
title: string;
|
||||
display_order: number;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
id: number;
|
||||
section_number: number;
|
||||
title: string;
|
||||
display_order: number;
|
||||
subsections: Subsection[];
|
||||
}
|
||||
|
||||
interface Chapter {
|
||||
id: number;
|
||||
chapter_number: number;
|
||||
title: string;
|
||||
display_order: number;
|
||||
sections: Section[];
|
||||
}
|
||||
|
||||
export default function CourseAdminPage() {
|
||||
const [chapters, setChapters] = useState<Chapter[]>([]);
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
courseContentAPI.getCourseContent()
|
||||
.then((data) => setChapters(data.chapters || []))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const toggle = (key: string) => {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-muted-foreground text-sm py-12 text-center">加载中...</div>;
|
||||
}
|
||||
|
||||
const totalSections = chapters.reduce((a, c) => a + c.sections.length, 0);
|
||||
const totalSubsections = chapters.reduce(
|
||||
(a, c) => a + c.sections.reduce((b, s) => b + s.subsections.length, 0),
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<BookOpen className="w-5 h-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">课程内容管理</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="rounded-xl border p-4 text-center">
|
||||
<div className="text-2xl font-semibold font-mono tabular-nums">{chapters.length}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">章</div>
|
||||
</div>
|
||||
<div className="rounded-xl border p-4 text-center">
|
||||
<div className="text-2xl font-semibold font-mono tabular-nums">{totalSections}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">节</div>
|
||||
</div>
|
||||
<div className="rounded-xl border p-4 text-center">
|
||||
<div className="text-2xl font-semibold font-mono tabular-nums">{totalSubsections}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">知识点</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{chapters.length === 0 ? (
|
||||
<div className="rounded-xl border py-12 text-center text-muted-foreground text-sm">
|
||||
暂无课程内容数据
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{chapters.map((chapter) => {
|
||||
const chKey = `ch-${chapter.id}`;
|
||||
return (
|
||||
<div key={chapter.id} className="rounded-xl border overflow-hidden">
|
||||
<button
|
||||
onClick={() => toggle(chKey)}
|
||||
className="w-full flex items-center justify-between px-4 py-3 hover:bg-muted/30 transition-colors text-left"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-mono text-muted-foreground w-8">
|
||||
{chapter.chapter_number}
|
||||
</span>
|
||||
<span className="font-medium text-sm">{chapter.title}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{chapter.sections.length} 节
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-4 h-4 text-muted-foreground transition-transform ${expanded.has(chKey) ? "rotate-90" : ""}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{expanded.has(chKey) && (
|
||||
<div className="border-t">
|
||||
{chapter.sections.map((section) => {
|
||||
const secKey = `sec-${section.id}`;
|
||||
return (
|
||||
<div key={section.id} className="border-b last:border-0">
|
||||
<button
|
||||
onClick={() => toggle(secKey)}
|
||||
className="w-full flex items-center justify-between px-4 py-2.5 pl-10 hover:bg-muted/20 transition-colors text-left"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs font-mono text-muted-foreground w-8">
|
||||
{section.section_number}
|
||||
</span>
|
||||
<span className="text-sm">{section.title}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{section.subsections.length} 知识点
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-3.5 h-3.5 text-muted-foreground transition-transform ${expanded.has(secKey) ? "rotate-90" : ""}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{expanded.has(secKey) && section.subsections.length > 0 && (
|
||||
<div className="border-t bg-muted/10">
|
||||
{section.subsections.map((sub) => (
|
||||
<div
|
||||
key={sub.id}
|
||||
className="flex items-center gap-3 px-4 py-2 pl-16 text-sm text-muted-foreground"
|
||||
>
|
||||
<span className="text-xs font-mono w-8">{sub.subsection_number}</span>
|
||||
<span>{sub.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user