add document manager and site heaer
clean format
This commit is contained in:
166
lightrag_webui/src/features/DocumentManager.tsx
Normal file
166
lightrag_webui/src/features/DocumentManager.tsx
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import Button from '@/components/ui/Button'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@/components/ui/Table'
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
|
||||
import Progress from '@/components/ui/Progress'
|
||||
import EmptyCard from '@/components/ui/EmptyCard'
|
||||
import UploadDocumentsDialog from '@/components/document/UploadDocumentsDialog'
|
||||
import ClearDocumentsDialog from '@/components/document/ClearDocumentsDialog'
|
||||
|
||||
import {
|
||||
getDocuments,
|
||||
getDocumentsScanProgress,
|
||||
scanNewDocuments,
|
||||
LightragDocumentsScanProgress
|
||||
} from '@/api/lightrag'
|
||||
import { errorMessage } from '@/lib/utils'
|
||||
import { toast } from 'sonner'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
|
||||
import { RefreshCwIcon, TrashIcon } from 'lucide-react'
|
||||
|
||||
// type DocumentStatus = 'indexed' | 'pending' | 'indexing' | 'error'
|
||||
|
||||
export default function DocumentManager() {
|
||||
const health = useBackendState.use.health()
|
||||
const [files, setFiles] = useState<string[]>([])
|
||||
const [indexedFiles, setIndexedFiles] = useState<string[]>([])
|
||||
const [scanProgress, setScanProgress] = useState<LightragDocumentsScanProgress | null>(null)
|
||||
|
||||
const fetchDocuments = useCallback(async () => {
|
||||
try {
|
||||
const docs = await getDocuments()
|
||||
setFiles(docs)
|
||||
} catch (err) {
|
||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||
}
|
||||
}, [setFiles])
|
||||
|
||||
useEffect(() => {
|
||||
fetchDocuments()
|
||||
}, [])
|
||||
|
||||
const scanDocuments = useCallback(async () => {
|
||||
try {
|
||||
const { status } = await scanNewDocuments()
|
||||
toast.message(status)
|
||||
} catch (err) {
|
||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||
}
|
||||
}, [setFiles])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
if (!health) return
|
||||
const progress = await getDocumentsScanProgress()
|
||||
setScanProgress((pre) => {
|
||||
if (pre?.is_scanning === progress.is_scanning && progress.is_scanning === false) {
|
||||
return pre
|
||||
}
|
||||
return progress
|
||||
})
|
||||
console.log(progress)
|
||||
} catch (err) {
|
||||
toast.error('Failed to get scan progress\n' + errorMessage(err))
|
||||
}
|
||||
}, 2000)
|
||||
return () => clearInterval(interval)
|
||||
}, [health])
|
||||
|
||||
const handleDelete = async (fileName: string) => {
|
||||
console.log(`deleting ${fileName}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="!size-full !rounded-none !border-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Document Management</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
tooltip="Scan Documents"
|
||||
onClick={scanDocuments}
|
||||
side="bottom"
|
||||
>
|
||||
<RefreshCwIcon />
|
||||
</Button>
|
||||
<div className="flex-1" />
|
||||
<ClearDocumentsDialog />
|
||||
<UploadDocumentsDialog />
|
||||
</div>
|
||||
|
||||
{scanProgress?.is_scanning && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Indexing {scanProgress.current_file}</span>
|
||||
<span>{scanProgress.progress}%</span>
|
||||
</div>
|
||||
<Progress value={scanProgress.progress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Uploaded documents</CardTitle>
|
||||
<CardDescription>view the uploaded documents here</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
{files.length == 0 && (
|
||||
<EmptyCard
|
||||
title="No documents uploades"
|
||||
description="upload documents to see them here"
|
||||
/>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Filename</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{files.map((file) => (
|
||||
<TableRow key={file}>
|
||||
<TableCell>{file}</TableCell>
|
||||
<TableCell>
|
||||
{indexedFiles.includes(file) ? (
|
||||
<span className="text-green-600">Indexed</span>
|
||||
) : (
|
||||
<span className="text-yellow-600">Pending</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(file)}
|
||||
// disabled={isUploading}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
185
lightrag_webui/src/features/GraphViewer.tsx
Normal file
185
lightrag_webui/src/features/GraphViewer.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useEffect, useState, useCallback, useMemo } from 'react'
|
||||
// import { MiniMap } from '@react-sigma/minimap'
|
||||
import { SigmaContainer, useRegisterEvents, useSigma } from '@react-sigma/core'
|
||||
import { Settings as SigmaSettings } from 'sigma/settings'
|
||||
import { GraphSearchOption, OptionItem } from '@react-sigma/graph-search'
|
||||
import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/rendering'
|
||||
import { NodeBorderProgram } from '@sigma/node-border'
|
||||
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
||||
|
||||
import FocusOnNode from '@/components/FocusOnNode'
|
||||
import LayoutsControl from '@/components/LayoutsControl'
|
||||
import GraphControl from '@/components/GraphControl'
|
||||
// import ThemeToggle from '@/components/ThemeToggle'
|
||||
import ZoomControl from '@/components/ZoomControl'
|
||||
import FullScreenControl from '@/components/FullScreenControl'
|
||||
import Settings from '@/components/Settings'
|
||||
import GraphSearch from '@/components/GraphSearch'
|
||||
import GraphLabels from '@/components/GraphLabels'
|
||||
import PropertiesView from '@/components/PropertiesView'
|
||||
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
|
||||
import '@react-sigma/core/lib/style.css'
|
||||
import '@react-sigma/graph-search/lib/style.css'
|
||||
|
||||
// Sigma settings
|
||||
const defaultSigmaSettings: Partial<SigmaSettings> = {
|
||||
allowInvalidContainer: true,
|
||||
defaultNodeType: 'default',
|
||||
defaultEdgeType: 'curvedArrow',
|
||||
renderEdgeLabels: false,
|
||||
edgeProgramClasses: {
|
||||
arrow: EdgeArrowProgram,
|
||||
curvedArrow: EdgeCurvedArrowProgram,
|
||||
curvedNoArrow: EdgeCurveProgram
|
||||
},
|
||||
nodeProgramClasses: {
|
||||
default: NodeBorderProgram,
|
||||
circel: NodeCircleProgram,
|
||||
point: NodePointProgram
|
||||
},
|
||||
labelGridCellSize: 60,
|
||||
labelRenderedSizeThreshold: 12,
|
||||
enableEdgeEvents: true,
|
||||
labelColor: {
|
||||
color: '#000',
|
||||
attribute: 'labelColor'
|
||||
},
|
||||
edgeLabelColor: {
|
||||
color: '#000',
|
||||
attribute: 'labelColor'
|
||||
},
|
||||
edgeLabelSize: 8,
|
||||
labelSize: 12
|
||||
// minEdgeThickness: 2
|
||||
// labelFont: 'Lato, sans-serif'
|
||||
}
|
||||
|
||||
const GraphEvents = () => {
|
||||
const registerEvents = useRegisterEvents()
|
||||
const sigma = useSigma()
|
||||
const [draggedNode, setDraggedNode] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Register the events
|
||||
registerEvents({
|
||||
downNode: (e) => {
|
||||
setDraggedNode(e.node)
|
||||
sigma.getGraph().setNodeAttribute(e.node, 'highlighted', true)
|
||||
},
|
||||
// On mouse move, if the drag mode is enabled, we change the position of the draggedNode
|
||||
mousemovebody: (e) => {
|
||||
if (!draggedNode) return
|
||||
// Get new position of node
|
||||
const pos = sigma.viewportToGraph(e)
|
||||
sigma.getGraph().setNodeAttribute(draggedNode, 'x', pos.x)
|
||||
sigma.getGraph().setNodeAttribute(draggedNode, 'y', pos.y)
|
||||
|
||||
// Prevent sigma to move camera:
|
||||
e.preventSigmaDefault()
|
||||
e.original.preventDefault()
|
||||
e.original.stopPropagation()
|
||||
},
|
||||
// On mouse up, we reset the autoscale and the dragging mode
|
||||
mouseup: () => {
|
||||
if (draggedNode) {
|
||||
setDraggedNode(null)
|
||||
sigma.getGraph().removeNodeAttribute(draggedNode, 'highlighted')
|
||||
}
|
||||
},
|
||||
// Disable the autoscale at the first down interaction
|
||||
mousedown: () => {
|
||||
if (!sigma.getCustomBBox()) sigma.setCustomBBox(sigma.getBBox())
|
||||
}
|
||||
})
|
||||
}, [registerEvents, sigma, draggedNode])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const GraphViewer = () => {
|
||||
const [sigmaSettings, setSigmaSettings] = useState(defaultSigmaSettings)
|
||||
|
||||
const selectedNode = useGraphStore.use.selectedNode()
|
||||
const focusedNode = useGraphStore.use.focusedNode()
|
||||
const moveToSelectedNode = useGraphStore.use.moveToSelectedNode()
|
||||
|
||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
||||
const renderLabels = useSettingsStore.use.showNodeLabel()
|
||||
|
||||
const enableEdgeEvents = useSettingsStore.use.enableEdgeEvents()
|
||||
const enableNodeDrag = useSettingsStore.use.enableNodeDrag()
|
||||
const renderEdgeLabels = useSettingsStore.use.showEdgeLabel()
|
||||
|
||||
useEffect(() => {
|
||||
setSigmaSettings({
|
||||
...defaultSigmaSettings,
|
||||
enableEdgeEvents,
|
||||
renderEdgeLabels,
|
||||
renderLabels
|
||||
})
|
||||
}, [renderLabels, enableEdgeEvents, renderEdgeLabels])
|
||||
|
||||
const onSearchFocus = useCallback((value: GraphSearchOption | null) => {
|
||||
if (value === null) useGraphStore.getState().setFocusedNode(null)
|
||||
else if (value.type === 'nodes') useGraphStore.getState().setFocusedNode(value.id)
|
||||
}, [])
|
||||
|
||||
const onSearchSelect = useCallback((value: GraphSearchOption | null) => {
|
||||
if (value === null) {
|
||||
useGraphStore.getState().setSelectedNode(null)
|
||||
} else if (value.type === 'nodes') {
|
||||
useGraphStore.getState().setSelectedNode(value.id, true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const autoFocusedNode = useMemo(() => focusedNode ?? selectedNode, [focusedNode, selectedNode])
|
||||
const searchInitSelectedNode = useMemo(
|
||||
(): OptionItem | null => (selectedNode ? { type: 'nodes', id: selectedNode } : null),
|
||||
[selectedNode]
|
||||
)
|
||||
|
||||
return (
|
||||
<SigmaContainer settings={sigmaSettings} className="!bg-background !size-full overflow-hidden">
|
||||
<GraphControl />
|
||||
|
||||
{enableNodeDrag && <GraphEvents />}
|
||||
|
||||
<FocusOnNode node={autoFocusedNode} move={moveToSelectedNode} />
|
||||
|
||||
<div className="absolute top-2 left-2 flex items-start gap-2">
|
||||
<GraphLabels />
|
||||
{showNodeSearchBar && (
|
||||
<GraphSearch
|
||||
value={searchInitSelectedNode}
|
||||
onFocus={onSearchFocus}
|
||||
onChange={onSearchSelect}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-background/60 absolute bottom-2 left-2 flex flex-col rounded-xl border-2 backdrop-blur-lg">
|
||||
<Settings />
|
||||
<ZoomControl />
|
||||
<LayoutsControl />
|
||||
<FullScreenControl />
|
||||
{/* <ThemeToggle /> */}
|
||||
</div>
|
||||
|
||||
{showPropertyPanel && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<PropertiesView />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* <div className="absolute bottom-2 right-2 flex flex-col rounded-xl border-2">
|
||||
<MiniMap width="100px" height="100px" />
|
||||
</div> */}
|
||||
</SigmaContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default GraphViewer
|
51
lightrag_webui/src/features/SiteHeader.tsx
Normal file
51
lightrag_webui/src/features/SiteHeader.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import Button from '@/components/ui/Button'
|
||||
import { SiteInfo } from '@/lib/constants'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||
|
||||
import { ZapIcon, GithubIcon } from 'lucide-react'
|
||||
|
||||
export default function SiteHeader() {
|
||||
return (
|
||||
<header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
|
||||
<a href="/" className="mr-6 flex items-center gap-2">
|
||||
<ZapIcon className="size-4 text-teal-400" aria-hidden="true" />
|
||||
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
|
||||
</a>
|
||||
|
||||
<div className="flex h-10 flex-1 justify-center">
|
||||
<div className="flex h-8 self-center">
|
||||
<TabsList className="h-full gap-2">
|
||||
<TabsTrigger
|
||||
value="documents"
|
||||
className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
|
||||
>
|
||||
Documents
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="knowledge-graph"
|
||||
className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
|
||||
>
|
||||
Knowledge Graph
|
||||
</TabsTrigger>
|
||||
{/* <TabsTrigger
|
||||
value="settings"
|
||||
className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
|
||||
>
|
||||
Settings
|
||||
</TabsTrigger> */}
|
||||
</TabsList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center">
|
||||
<Button variant="ghost" size="icon">
|
||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user