feat: enhance textbook reader with anchor text support and improve knowledge point management

This commit is contained in:
SpecialX
2026-01-16 10:22:16 +08:00
parent 9bfc621d3f
commit bb4555f611
44 changed files with 6284 additions and 2090 deletions

View File

@@ -35,9 +35,10 @@ interface SortableChapterItemProps {
textbookId: string
onDelete: (chapter: Chapter) => void
onCreateSub: (parentId: string) => void
canEdit?: boolean
}
function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId, onDelete, onCreateSub }: SortableChapterItemProps) {
function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId, onDelete, onCreateSub, canEdit = true }: SortableChapterItemProps) {
const [isOpen, setIsOpen] = useState(level === 0)
const hasChildren = chapter.children && chapter.children.length > 0
const isSelected = chapter.id === selectedId
@@ -49,7 +50,7 @@ function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId,
transform,
transition,
isDragging,
} = useSortable({ id: chapter.id })
} = useSortable({ id: chapter.id, disabled: !canEdit })
const style = {
transform: CSS.Transform.toString(transform),
@@ -66,9 +67,11 @@ function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId,
isSelected ? "bg-accent text-accent-foreground font-medium" : "hover:bg-muted/50 text-muted-foreground hover:text-foreground",
isDragging && "opacity-50"
)}>
<div {...attributes} {...listeners} className="mr-1 cursor-grab opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-muted rounded">
<GripVertical className="h-3.5 w-3.5 text-muted-foreground/50" />
</div>
{canEdit && (
<div {...attributes} {...listeners} className="mr-1 cursor-grab opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-muted rounded">
<GripVertical className="h-3.5 w-3.5 text-muted-foreground/50" />
</div>
)}
{hasChildren ? (
<CollapsibleTrigger asChild>
@@ -103,7 +106,7 @@ function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId,
<span className="truncate text-sm">{chapter.title}</span>
</div>
<div className="flex items-center opacity-0 group-hover:opacity-100 transition-opacity ml-2">
<div className={cn("flex items-center opacity-0 group-hover:opacity-100 transition-opacity ml-2", !canEdit && "hidden")}>
<Button
variant="ghost"
size="icon"
@@ -142,6 +145,7 @@ function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId,
textbookId={textbookId}
onDelete={onDelete}
onCreateSub={onCreateSub}
canEdit={canEdit}
/>
)}
</div>
@@ -151,14 +155,15 @@ function SortableChapterItem({ chapter, level, selectedId, onSelect, textbookId,
)
}
function RecursiveSortableList({ items, level, selectedId, onSelect, textbookId, onDelete, onCreateSub }: {
function RecursiveSortableList({ items, level, selectedId, onSelect, textbookId, onDelete, onCreateSub, canEdit = true }: {
items: Chapter[],
level: number,
selectedId?: string,
onSelect: (c: Chapter) => void,
textbookId: string,
onDelete: (c: Chapter) => void,
onCreateSub: (pid: string) => void
onCreateSub: (pid: string) => void,
canEdit?: boolean
}) {
return (
<SortableContext items={items.map(i => i.id)} strategy={verticalListSortingStrategy}>
@@ -172,6 +177,7 @@ function RecursiveSortableList({ items, level, selectedId, onSelect, textbookId,
textbookId={textbookId}
onDelete={onDelete}
onCreateSub={onCreateSub}
canEdit={canEdit}
/>
))}
</SortableContext>
@@ -183,9 +189,10 @@ interface ChapterSidebarListProps {
selectedChapterId?: string
onSelectChapter: (chapter: Chapter) => void
textbookId: string
canEdit?: boolean
}
export function ChapterSidebarList({ chapters, selectedChapterId, onSelectChapter, textbookId }: ChapterSidebarListProps) {
export function ChapterSidebarList({ chapters, selectedChapterId, onSelectChapter, textbookId, canEdit = true }: ChapterSidebarListProps) {
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [createParentId, setCreateParentId] = useState<string | undefined>(undefined)
@@ -300,8 +307,9 @@ export function ChapterSidebarList({ chapters, selectedChapterId, onSelectChapte
setShowCreateDialog(true)
}
return (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
// If not editable, we can skip dnd logic
if (!canEdit) {
return (
<RecursiveSortableList
items={chapters}
level={0}
@@ -310,6 +318,22 @@ export function ChapterSidebarList({ chapters, selectedChapterId, onSelectChapte
textbookId={textbookId}
onDelete={handleDeleteRequest}
onCreateSub={handleCreateSubRequest}
canEdit={false}
/>
)
}
return (
<DndContext id="chapter-sidebar-dnd" sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<RecursiveSortableList
items={chapters}
level={0}
selectedId={selectedChapterId}
onSelect={onSelectChapter}
textbookId={textbookId}
onDelete={handleDeleteRequest}
onCreateSub={handleCreateSubRequest}
canEdit={true}
/>
<CreateChapterDialog