'use client' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslations } from 'next-intl' import { MarkdownRenderer } from '@/components/markdown-renderer' interface DocsTreeNode { path: string name: string type: 'file' | 'directory' size?: number modified?: number children?: DocsTreeNode[] } interface DocsTreeResponse { roots: string[] tree: DocsTreeNode[] error?: string } interface DocsContentResponse { path: string content: string size: number modified: number error?: string } interface DocsSearchResult { path: string name: string matches: number } interface DocsSearchResponse { results: DocsSearchResult[] error?: string } function collectFilePaths(nodes: DocsTreeNode[]): string[] { const filePaths: string[] = [] for (const node of nodes) { if (node.type === 'file') { filePaths.push(node.path) continue } if (node.children && node.children.length > 0) { filePaths.push(...collectFilePaths(node.children)) } } return filePaths } function formatBytes(value: number): string { if (value < 1024) return `${value} B` if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB` return `${(value / (1024 * 1024)).toFixed(1)} MB` } function formatTime(value: number): string { return new Date(value).toLocaleString() } export function DocumentsPanel() { const t = useTranslations('documents') const [tree, setTree] = useState([]) const [roots, setRoots] = useState([]) const [loadingTree, setLoadingTree] = useState(true) const [treeError, setTreeError] = useState(null) const [selectedPath, setSelectedPath] = useState(null) const [docContent, setDocContent] = useState('') const [docMeta, setDocMeta] = useState<{ size: number; modified: number } | null>(null) const [loadingDoc, setLoadingDoc] = useState(false) const [docError, setDocError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [searchResults, setSearchResults] = useState([]) const [searching, setSearching] = useState(false) const [searchError, setSearchError] = useState(null) const [expandedDirs, setExpandedDirs] = useState>(new Set()) const loadTree = useCallback(async () => { setLoadingTree(true) setTreeError(null) try { const res = await fetch('/api/docs/tree') const data = (await res.json()) as DocsTreeResponse if (!res.ok) throw new Error(data.error || 'Failed to load documents') setTree(data.tree || []) setRoots(data.roots || []) const defaultExpanded = new Set((data.roots || []).filter(Boolean)) setExpandedDirs(defaultExpanded) } catch (error) { setTree([]) setRoots([]) setTreeError((error as Error).message || 'Failed to load documents') } finally { setLoadingTree(false) } }, []) const loadDoc = useCallback(async (path: string) => { setLoadingDoc(true) setDocError(null) setSelectedPath(path) try { const res = await fetch(`/api/docs/content?path=${encodeURIComponent(path)}`) const data = (await res.json()) as DocsContentResponse if (!res.ok) throw new Error(data.error || 'Failed to load document') setDocContent(data.content || '') setDocMeta({ size: data.size, modified: data.modified }) } catch (error) { setDocContent('') setDocMeta(null) setDocError((error as Error).message || 'Failed to load document') } finally { setLoadingDoc(false) } }, []) useEffect(() => { void loadTree() }, [loadTree]) const filePaths = useMemo(() => collectFilePaths(tree), [tree]) useEffect(() => { if (selectedPath) return if (filePaths.length === 0) return void loadDoc(filePaths[0]) }, [filePaths, loadDoc, selectedPath]) useEffect(() => { const query = searchQuery.trim() if (query.length < 2) { setSearchResults([]) setSearchError(null) setSearching(false) return } const handle = setTimeout(async () => { setSearching(true) setSearchError(null) try { const res = await fetch(`/api/docs/search?q=${encodeURIComponent(query)}&limit=100`) const data = (await res.json()) as DocsSearchResponse if (!res.ok) throw new Error(data.error || 'Failed to search docs') setSearchResults(data.results || []) } catch (error) { setSearchResults([]) setSearchError((error as Error).message || 'Failed to search docs') } finally { setSearching(false) } }, 250) return () => clearTimeout(handle) }, [searchQuery]) const isShowingSearch = searchQuery.trim().length >= 2 const toggleDir = (path: string) => { setExpandedDirs((prev) => { const next = new Set(prev) if (next.has(path)) next.delete(path) else next.add(path) return next }) } const renderNode = (node: DocsTreeNode, depth = 0) => { if (node.type === 'directory') { const isOpen = expandedDirs.has(node.path) return (
{isOpen && node.children && (
{node.children.map((child) => renderNode(child, depth + 1))}
)}
) } const active = selectedPath === node.path return ( ) } return (

{t('viewerTitle')}

{t('viewerDescription')}

{!selectedPath && (
{t('selectFile')}
)} {selectedPath && (
{selectedPath}
{docMeta && (
{formatBytes(docMeta.size)} • {t('updated')} {formatTime(docMeta.modified)}
)}
{loadingDoc &&
{t('loadingDocument')}
} {docError &&
{docError}
} {!loadingDoc && !docError && (
)}
)}
) }