dify/web/app/components/develop/hooks/use-doc-toc.ts
FFXN 0e320290e1
feat: evaluation (#35353)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: jyong <718720800@qq.com>
Co-authored-by: Yansong Zhang <916125788@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: hj24 <mambahj24@gmail.com>
Co-authored-by: hj24 <huangjian@dify.ai>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: Stephen Zhou <38493346+hyoban@users.noreply.github.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: 非法操作 <hjlarry@163.com>
Co-authored-by: Ayush Baluni <73417844+aayushbaluni@users.noreply.github.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: jimcody1995 <jjimcody@gmail.com>
Co-authored-by: James <63717587+jamesrayammons@users.noreply.github.com>
Co-authored-by: Yunlu Wen <yunlu.wen@dify.ai>
Co-authored-by: Stephen Zhou <hi@hyoban.cc>
Co-authored-by: Coding On Star <447357187@qq.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: jerryzai <jerryzh8710@protonmail.com>
Co-authored-by: NVIDIAN <speedy.hpc@hotmail.com>
Co-authored-by: ai-hpc <ai-hpc@users.noreply.github.com>
Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
Co-authored-by: Junghwan <70629228+shaun0927@users.noreply.github.com>
Co-authored-by: HeYinKazune <70251095+HeYin-OS@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
Co-authored-by: Jingyi <jingyi.qi@dify.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: sxxtony <166789813+sxxtony@users.noreply.github.com>
2026-04-17 16:37:21 +08:00

116 lines
3.2 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react'
export type TocItem = {
href: string
text: string
}
type UseDocTocOptions = {
appDetail: Record<string, unknown> | null
locale: string
}
const HEADER_OFFSET = 80
const SCROLL_CONTAINER_SELECTOR = '.overflow-auto'
const getTargetId = (href: string) => href.replace('#', '')
/**
* Extract heading anchors from the rendered <article> as TOC items.
*/
const extractTocFromArticle = (): TocItem[] => {
const article = document.querySelector('article')
if (!article)
return []
return Array.from(article.querySelectorAll('h2'))
.map((heading) => {
const anchor = heading.querySelector('a')
if (!anchor)
return null
return {
href: anchor.getAttribute('href') || '',
text: anchor.textContent || '',
}
})
.filter((item): item is TocItem => item !== null)
}
/**
* Custom hook that manages table-of-contents state:
* - Extracts TOC items from rendered headings
* - Tracks the active section on scroll
* - Auto-expands the panel on wide viewports
*/
export const useDocToc = ({ appDetail, locale }: UseDocTocOptions) => {
const [toc, setToc] = useState<TocItem[]>([])
const [isTocExpanded, setIsTocExpanded] = useState(() => {
if (typeof window === 'undefined')
return false
return window.matchMedia('(min-width: 1280px)').matches
})
const [activeSection, setActiveSection] = useState<string>('')
// Re-extract TOC items whenever the doc content changes
useEffect(() => {
const timer = setTimeout(() => {
const tocItems = extractTocFromArticle()
setToc(tocItems)
if (tocItems.length > 0)
setActiveSection(getTargetId(tocItems[0]!.href))
}, 0)
return () => clearTimeout(timer)
}, [appDetail, locale])
// Track active section based on scroll position
useEffect(() => {
const scrollContainer = document.querySelector(SCROLL_CONTAINER_SELECTOR)
if (!scrollContainer || toc.length === 0)
return
const handleScroll = () => {
let currentSection = ''
for (const item of toc) {
const targetId = getTargetId(item.href)
const element = document.getElementById(targetId)
if (element) {
const rect = element.getBoundingClientRect()
if (rect.top <= window.innerHeight / 2)
currentSection = targetId
}
}
if (currentSection && currentSection !== activeSection)
setActiveSection(currentSection)
}
scrollContainer.addEventListener('scroll', handleScroll)
return () => scrollContainer.removeEventListener('scroll', handleScroll)
}, [toc, activeSection])
// Smooth-scroll to a TOC target on click
const handleTocClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>, item: TocItem) => {
e.preventDefault()
const targetId = getTargetId(item.href)
const element = document.getElementById(targetId)
if (!element)
return
const scrollContainer = document.querySelector(SCROLL_CONTAINER_SELECTOR)
if (scrollContainer) {
scrollContainer.scrollTo({
top: element.offsetTop - HEADER_OFFSET,
behavior: 'smooth',
})
}
}, [])
return {
toc,
isTocExpanded,
setIsTocExpanded,
activeSection,
handleTocClick,
}
}