add document manager and site heaer

clean

format
This commit is contained in:
ArnoChen
2025-02-15 23:22:37 +08:00
parent 4d58ff8bb4
commit 1fe456666a
28 changed files with 1360 additions and 194 deletions

View File

@@ -1,11 +1,17 @@
import ThemeProvider from '@/components/ThemeProvider'
import MessageAlert from '@/components/MessageAlert'
import StatusIndicator from '@/components/StatusIndicator'
import GraphViewer from '@/GraphViewer'
import { healthCheckInterval } from '@/lib/constants'
import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import { useEffect } from 'react'
import { Toaster } from 'sonner'
import SiteHeader from '@/features/SiteHeader'
import GraphViewer from '@/features/GraphViewer'
import DocumentManager from '@/features/DocumentManager'
import { Tabs, TabsContent } from '@/components/ui/Tabs'
function App() {
const message = useBackendState.use.message()
@@ -26,11 +32,23 @@ function App() {
return (
<ThemeProvider>
<div className="h-screen w-screen">
<GraphViewer />
<div className="flex h-screen w-screen">
<Tabs defaultValue="knowledge-graph" className="flex size-full flex-col">
<SiteHeader />
<TabsContent value="documents" className="flex-1">
<DocumentManager />
</TabsContent>
<TabsContent value="knowledge-graph" className="flex-1">
<GraphViewer />
</TabsContent>
<TabsContent value="settings" className="size-full">
<h1> Settings </h1>
</TabsContent>
</Tabs>
</div>
{enableHealthCheck && <StatusIndicator />}
{message !== null && <MessageAlert />}
<Toaster />
</ThemeProvider>
)
}

View File

@@ -1,3 +1,4 @@
import axios, { AxiosError } from 'axios'
import { backendBaseUrl } from '@/lib/constants'
import { errorMessage } from '@/lib/utils'
import { useSettingsStore } from '@/stores/settings'
@@ -64,81 +65,64 @@ export type QueryResponse = {
response: string
}
export type DocumentActionResponse = {
status: 'success' | 'partial_success' | 'failure'
message: string
document_count: number
}
export const InvalidApiKeyError = 'Invalid API Key'
export const RequireApiKeError = 'API Key required'
// Helper functions
const getResponseContent = async (response: Response) => {
const contentType = response.headers.get('content-type')
if (contentType) {
if (contentType.includes('application/json')) {
const data = await response.json()
return JSON.stringify(data, undefined, 2)
} else if (contentType.startsWith('text/')) {
return await response.text()
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
return await response.text()
} else if (contentType.includes('application/octet-stream')) {
const buffer = await response.arrayBuffer()
const decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true })
return decoder.decode(buffer)
} else {
try {
return await response.text()
} catch (error) {
console.warn('Failed to decode as text, may be binary:', error)
return `[Could not decode response body. Content-Type: ${contentType}]`
}
}
} else {
try {
return await response.text()
} catch (error) {
console.warn('Failed to decode as text, may be binary:', error)
return '[Could not decode response body. No Content-Type header.]'
}
// Axios instance
const axiosInstance = axios.create({
baseURL: backendBaseUrl,
headers: {
'Content-Type': 'application/json'
}
return ''
}
})
const fetchWithAuth = async (url: string, options: RequestInit = {}): Promise<Response> => {
// Interceptoradd api key
axiosInstance.interceptors.request.use((config) => {
const apiKey = useSettingsStore.getState().apiKey
const headers = {
...(options.headers || {}),
...(apiKey ? { 'X-API-Key': apiKey } : {})
if (apiKey) {
config.headers['X-API-Key'] = apiKey
}
return config
})
const response = await fetch(backendBaseUrl + url, {
...options,
headers
})
if (!response.ok) {
throw new Error(
`${response.status} ${response.statusText}\n${await getResponseContent(response)}\n${response.url}`
)
// Interceptorhanle error
axiosInstance.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response) {
throw new Error(
`${error.response.status} ${error.response.statusText}\n${JSON.stringify(
error.response.data
)}\n${error.config?.url}`
)
}
throw error
}
return response
}
)
// API methods
export const queryGraphs = async (label: string): Promise<LightragGraphType> => {
const response = await fetchWithAuth(`/graphs?label=${label}`)
return await response.json()
const response = await axiosInstance.get(`/graphs?label=${label}`)
return response.data
}
export const getGraphLabels = async (): Promise<string[]> => {
const response = await fetchWithAuth('/graph/label/list')
return await response.json()
const response = await axiosInstance.get('/graph/label/list')
return response.data
}
export const checkHealth = async (): Promise<
LightragStatus | { status: 'error'; message: string }
> => {
try {
const response = await fetchWithAuth('/health')
return await response.json()
const response = await axiosInstance.get('/health')
return response.data
} catch (e) {
return {
status: 'error',
@@ -148,63 +132,33 @@ export const checkHealth = async (): Promise<
}
export const getDocuments = async (): Promise<string[]> => {
const response = await fetchWithAuth('/documents')
return await response.json()
const response = await axiosInstance.get('/documents')
return response.data
}
export const scanNewDocuments = async (): Promise<{ status: string }> => {
const response = await axiosInstance.post('/documents/scan')
return response.data
}
export const getDocumentsScanProgress = async (): Promise<LightragDocumentsScanProgress> => {
const response = await fetchWithAuth('/documents/scan-progress')
return await response.json()
}
export const uploadDocument = async (
file: File
): Promise<{
status: string
message: string
total_documents: number
}> => {
const formData = new FormData()
formData.append('file', file)
const response = await fetchWithAuth('/documents/upload', {
method: 'POST',
body: formData
})
return await response.json()
}
export const startDocumentScan = async (): Promise<{ status: string }> => {
const response = await fetchWithAuth('/documents/scan', {
method: 'POST'
})
return await response.json()
const response = await axiosInstance.get('/documents/scan-progress')
return response.data
}
export const queryText = async (request: QueryRequest): Promise<QueryResponse> => {
const response = await fetchWithAuth('/query', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
})
return await response.json()
const response = await axiosInstance.post('/query', request)
return response.data
}
export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => {
const response = await fetchWithAuth('/query/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
const response = await axiosInstance.post('/query/stream', request, {
responseType: 'stream'
})
const reader = response.body?.getReader()
if (!reader) throw new Error('No response body')
const reader = response.data.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
@@ -226,53 +180,50 @@ export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: st
}
}
// Text insertion API
export const insertText = async (
text: string,
description?: string
): Promise<{
status: string
message: string
document_count: number
}> => {
const response = await fetchWithAuth('/documents/text', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text, description })
})
return await response.json()
): Promise<DocumentActionResponse> => {
const response = await axiosInstance.post('/documents/text', { text, description })
return response.data
}
// Batch file upload API
export const uploadBatchDocuments = async (
files: File[]
): Promise<{
status: string
message: string
document_count: number
}> => {
export const uploadDocument = async (
file: File,
onUploadProgress?: (percentCompleted: number) => void
): Promise<DocumentActionResponse> => {
const formData = new FormData()
files.forEach((file) => {
formData.append('files', file)
})
formData.append('file', file)
const response = await fetchWithAuth('/documents/batch', {
method: 'POST',
body: formData
const response = await axiosInstance.post('/documents/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress:
onUploadProgress !== undefined
? (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total!)
onUploadProgress(percentCompleted)
}
: undefined
})
return await response.json()
return response.data
}
// Clear all documents API
export const clearDocuments = async (): Promise<{
status: string
message: string
document_count: number
}> => {
const response = await fetchWithAuth('/documents', {
method: 'DELETE'
})
return await response.json()
export const batchUploadDocuments = async (
files: File[],
onUploadProgress?: (fileName: string, percentCompleted: number) => void
): Promise<DocumentActionResponse[]> => {
return await Promise.all(
files.map(async (file) => {
return await uploadDocument(file, (percentCompleted) => {
onUploadProgress?.(file.name, percentCompleted)
})
})
)
}
export const clearDocuments = async (): Promise<DocumentActionResponse> => {
const response = await axiosInstance.delete('/documents')
return response.data
}

View File

@@ -22,10 +22,11 @@ const MessageAlert = () => {
return (
<Alert
variant={health ? 'default' : 'destructive'}
// variant={health ? 'default' : 'destructive'}
className={cn(
'bg-background/90 absolute top-2 left-1/2 flex w-auto -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0'
'bg-background/90 absolute top-12 left-1/2 flex w-auto max-w-lg -translate-x-1/2 transform items-center gap-4 shadow-md backdrop-blur-lg transition-all duration-500 ease-in-out',
isMounted ? 'translate-y-0 opacity-100' : '-translate-y-20 opacity-0',
!health && 'bg-red-700 text-white'
)}
>
{!health && (
@@ -42,7 +43,7 @@ const MessageAlert = () => {
<Button
size="sm"
variant={controlButtonVariant}
className="text-primary max-h-8 border !p-2 text-xs"
className="border-primary max-h-8 border !p-2 text-xs"
onClick={() => useBackendState.getState().clear()}
>
Close

View File

@@ -200,7 +200,7 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
<label className="text-md pl-1 font-bold tracking-wide text-teal-600">Relationship</label>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={'Id'} value={edge.id} />
<PropertyRow name={'Type'} value={edge.type} />
{edge.type && <PropertyRow name={'Type'} value={edge.type} />}
<PropertyRow
name={'Source'}
value={edge.sourceNode ? edge.sourceNode.labels.join(', ') : edge.source}

View File

@@ -0,0 +1,52 @@
import { useState, useCallback } from 'react'
import Button from '@/components/ui/Button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/Dialog'
import { toast } from 'sonner'
import { errorMessage } from '@/lib/utils'
import { clearDocuments } from '@/api/lightrag'
import { EraserIcon } from 'lucide-react'
export default function ClearDocumentsDialog() {
const [open, setOpen] = useState(false) // 添加状态控制
const handleClear = useCallback(async () => {
try {
const result = await clearDocuments()
if (result.status === 'success') {
toast.success('Documents cleared successfully')
setOpen(false)
} else {
toast.error(`Clear Documents Failed:\n${result.message}`)
}
} catch (err) {
toast.error('Clear Documents Failed:\n' + errorMessage(err))
}
}, [setOpen])
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" tooltip="Clear documents" side="bottom" size="icon">
<EraserIcon />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>Clear documents</DialogTitle>
<DialogDescription>Do you really want to clear all documents?</DialogDescription>
</DialogHeader>
<Button variant="destructive" onClick={handleClear}>
YES
</Button>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,91 @@
import { useState, useCallback } from 'react'
import Button from '@/components/ui/Button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/Dialog'
import FileUploader from '@/components/ui/FileUploader'
import { toast } from 'sonner'
import { errorMessage } from '@/lib/utils'
import { uploadDocument } from '@/api/lightrag'
import { UploadIcon } from 'lucide-react'
export default function UploadDocumentsDialog() {
const [open, setOpen] = useState(false)
const [isUploading, setIsUploading] = useState(false)
const [progresses, setProgresses] = useState<Record<string, number>>({})
const handleDocumentsUpload = useCallback(
async (filesToUpload: File[]) => {
setIsUploading(true)
try {
await Promise.all(
filesToUpload.map(async (file) => {
try {
const result = await uploadDocument(file, (percentCompleted: number) => {
console.debug(`Uploading ${file.name}: ${percentCompleted}%`)
setProgresses((pre) => ({
...pre,
[file.name]: percentCompleted
}))
})
if (result.status === 'success') {
toast.success(`Upload Success:\n${file.name} uploaded successfully`)
} else {
toast.error(`Upload Failed:\n${file.name}\n${result.message}`)
}
} catch (err) {
toast.error(`Upload Failed:\n${file.name}\n${errorMessage(err)}`)
}
})
)
} catch (err) {
toast.error('Upload Failed\n' + errorMessage(err))
} finally {
setIsUploading(false)
setOpen(false)
}
},
[setIsUploading, setProgresses, setOpen]
)
return (
<Dialog
open={open}
onOpenChange={(open) => {
if (isUploading && !open) {
return
}
setOpen(open)
}}
>
<DialogTrigger asChild>
<Button variant="outline" tooltip="Upload documents" side="bottom" size="icon">
<UploadIcon />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle>Upload documents</DialogTitle>
<DialogDescription>
Drag and drop your documents here or click to browse.
</DialogDescription>
</DialogHeader>
<FileUploader
maxFileCount={Infinity}
maxSize={200 * 1024 * 1024}
description="supported types: TXT, MD, DOC, PDF, PPTX"
onUpload={handleDocumentsUpload}
progresses={progresses}
disabled={isUploading}
/>
</DialogContent>
</Dialog>
)
}

View File

@@ -193,7 +193,7 @@ export function AsyncSearch<T>({
</div>
)}
</div>
<CommandList className="max-h-auto" hidden={!open || debouncedSearchTerm.length === 0}>
<CommandList hidden={!open || debouncedSearchTerm.length === 0}>
{error && <div className="text-destructive p-4 text-center">{error}</div>}
{loading && options.length === 0 && (loadingSkeleton || <DefaultLoadingSkeleton />)}
{!loading &&

View File

@@ -0,0 +1,33 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils'
const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
outline: 'text-foreground'
}
},
defaultVariants: {
variant: 'default'
}
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
}
export default Badge

View File

@@ -0,0 +1,55 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('bg-card text-card-foreground rounded-xl border shadow', className)}
{...props}
/>
)
)
Card.displayName = 'Card'
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
)
)
CardHeader.displayName = 'CardHeader'
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('leading-none font-semibold tracking-tight', className)}
{...props}
/>
)
)
CardTitle.displayName = 'CardTitle'
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} />
)
)
CardDescription.displayName = 'CardDescription'
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
)
)
CardContent.displayName = 'CardContent'
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
)
)
CardFooter.displayName = 'CardFooter'
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,64 @@
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/Table'
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export default function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel()
})
return (
<div className="rounded-md border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
)
}

View File

@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
className
)}
{...props}
@@ -65,7 +65,7 @@ const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivEleme
DialogFooter.displayName = 'DialogFooter'
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
@@ -77,7 +77,7 @@ const DialogTitle = React.forwardRef<
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description

View File

@@ -0,0 +1,38 @@
import { cn } from '@/lib/utils'
import { Card, CardDescription, CardTitle } from '@/components/ui/Card'
import { FilesIcon } from 'lucide-react'
interface EmptyCardProps extends React.ComponentPropsWithoutRef<typeof Card> {
title: string
description?: string
action?: React.ReactNode
icon?: React.ComponentType<{ className?: string }>
}
export default function EmptyCard({
title,
description,
icon: Icon = FilesIcon,
action,
className,
...props
}: EmptyCardProps) {
return (
<Card
className={cn(
'flex w-full flex-col items-center justify-center space-y-6 bg-transparent p-16',
className
)}
{...props}
>
<div className="mr-4 shrink-0 rounded-full border border-dashed p-4">
<Icon className="text-muted-foreground size-8" aria-hidden="true" />
</div>
<div className="flex flex-col items-center gap-1.5 text-center">
<CardTitle>{title}</CardTitle>
{description ? <CardDescription>{description}</CardDescription> : null}
</div>
{action ? action : null}
</Card>
)
}

View File

@@ -0,0 +1,322 @@
/**
* @see https://github.com/sadmann7/file-uploader
*/
import * as React from 'react'
import { FileText, Upload, X } from 'lucide-react'
import Dropzone, { type DropzoneProps, type FileRejection } from 'react-dropzone'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { useControllableState } from '@radix-ui/react-use-controllable-state'
import Button from '@/components/ui/Button'
import Progress from '@/components/ui/Progress'
import { ScrollArea } from '@/components/ui/ScrollArea'
import { supportedFileTypes } from '@/lib/constants'
interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Value of the uploader.
* @type File[]
* @default undefined
* @example value={files}
*/
value?: File[]
/**
* Function to be called when the value changes.
* @type (files: File[]) => void
* @default undefined
* @example onValueChange={(files) => setFiles(files)}
*/
onValueChange?: (files: File[]) => void
/**
* Function to be called when files are uploaded.
* @type (files: File[]) => Promise<void>
* @default undefined
* @example onUpload={(files) => uploadFiles(files)}
*/
onUpload?: (files: File[]) => Promise<void>
/**
* Progress of the uploaded files.
* @type Record<string, number> | undefined
* @default undefined
* @example progresses={{ "file1.png": 50 }}
*/
progresses?: Record<string, number>
/**
* Accepted file types for the uploader.
* @type { [key: string]: string[]}
* @default
* ```ts
* { "text/*": [] }
* ```
* @example accept={["text/plain", "application/pdf"]}
*/
accept?: DropzoneProps['accept']
/**
* Maximum file size for the uploader.
* @type number | undefined
* @default 1024 * 1024 * 200 // 200MB
* @example maxSize={1024 * 1024 * 2} // 2MB
*/
maxSize?: DropzoneProps['maxSize']
/**
* Maximum number of files for the uploader.
* @type number | undefined
* @default 1
* @example maxFileCount={4}
*/
maxFileCount?: DropzoneProps['maxFiles']
/**
* Whether the uploader should accept multiple files.
* @type boolean
* @default false
* @example multiple
*/
multiple?: boolean
/**
* Whether the uploader is disabled.
* @type boolean
* @default false
* @example disabled
*/
disabled?: boolean
description?: string
}
function formatBytes(
bytes: number,
opts: {
decimals?: number
sizeType?: 'accurate' | 'normal'
} = {}
) {
const { decimals = 0, sizeType = 'normal' } = opts
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB']
if (bytes === 0) return '0 Byte'
const i = Math.floor(Math.log(bytes) / Math.log(1024))
return `${(bytes / Math.pow(1024, i)).toFixed(decimals)} ${
sizeType === 'accurate' ? (accurateSizes[i] ?? 'Bytes') : (sizes[i] ?? 'Bytes')
}`
}
function FileUploader(props: FileUploaderProps) {
const {
value: valueProp,
onValueChange,
onUpload,
progresses,
accept = supportedFileTypes,
maxSize = 1024 * 1024 * 200,
maxFileCount = 1,
multiple = false,
disabled = false,
description,
className,
...dropzoneProps
} = props
const [files, setFiles] = useControllableState({
prop: valueProp,
onChange: onValueChange
})
const onDrop = React.useCallback(
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
toast.error('Cannot upload more than 1 file at a time')
return
}
if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) {
toast.error(`Cannot upload more than ${maxFileCount} files`)
return
}
const newFiles = acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file)
})
)
const updatedFiles = files ? [...files, ...newFiles] : newFiles
setFiles(updatedFiles)
if (rejectedFiles.length > 0) {
rejectedFiles.forEach(({ file }) => {
toast.error(`File ${file.name} was rejected`)
})
}
if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) {
const target = updatedFiles.length > 0 ? `${updatedFiles.length} files` : 'file'
toast.promise(onUpload(updatedFiles), {
loading: `Uploading ${target}...`,
success: () => {
setFiles([])
return `${target} uploaded`
},
error: `Failed to upload ${target}`
})
}
},
[files, maxFileCount, multiple, onUpload, setFiles]
)
function onRemove(index: number) {
if (!files) return
const newFiles = files.filter((_, i) => i !== index)
setFiles(newFiles)
onValueChange?.(newFiles)
}
// Revoke preview url when component unmounts
React.useEffect(() => {
return () => {
if (!files) return
files.forEach((file) => {
if (isFileWithPreview(file)) {
URL.revokeObjectURL(file.preview)
}
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const isDisabled = disabled || (files?.length ?? 0) >= maxFileCount
return (
<div className="relative flex flex-col gap-6 overflow-hidden">
<Dropzone
onDrop={onDrop}
accept={accept}
maxSize={maxSize}
maxFiles={maxFileCount}
multiple={maxFileCount > 1 || multiple}
disabled={isDisabled}
>
{({ getRootProps, getInputProps, isDragActive }) => (
<div
{...getRootProps()}
className={cn(
'group border-muted-foreground/25 hover:bg-muted/25 relative grid h-52 w-full cursor-pointer place-items-center rounded-lg border-2 border-dashed px-5 py-2.5 text-center transition',
'ring-offset-background focus-visible:ring-ring focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
isDragActive && 'border-muted-foreground/50',
isDisabled && 'pointer-events-none opacity-60',
className
)}
{...dropzoneProps}
>
<input {...getInputProps()} />
{isDragActive ? (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload className="text-muted-foreground size-7" aria-hidden="true" />
</div>
<p className="text-muted-foreground font-medium">Drop the files here</p>
</div>
) : (
<div className="flex flex-col items-center justify-center gap-4 sm:px-5">
<div className="rounded-full border border-dashed p-3">
<Upload className="text-muted-foreground size-7" aria-hidden="true" />
</div>
<div className="flex flex-col gap-px">
<p className="text-muted-foreground font-medium">
Drag and drop files here, or click to select files
</p>
{description ? (
<p className="text-muted-foreground/70 text-sm">{description}</p>
) : (
<p className="text-muted-foreground/70 text-sm">
You can upload
{maxFileCount > 1
? ` ${maxFileCount === Infinity ? 'multiple' : maxFileCount}
files (up to ${formatBytes(maxSize)} each)`
: ` a file with ${formatBytes(maxSize)}`}
Supported formats: TXT, MD, DOC, PDF, PPTX
</p>
)}
</div>
</div>
)}
</div>
)}
</Dropzone>
{files?.length ? (
<ScrollArea className="h-fit w-full px-3">
<div className="flex max-h-48 flex-col gap-4">
{files?.map((file, index) => (
<FileCard
key={index}
file={file}
onRemove={() => onRemove(index)}
progress={progresses?.[file.name]}
/>
))}
</div>
</ScrollArea>
) : null}
</div>
)
}
interface FileCardProps {
file: File
onRemove: () => void
progress?: number
}
function FileCard({ file, progress, onRemove }: FileCardProps) {
return (
<div className="relative flex items-center gap-2.5">
<div className="flex flex-1 gap-2.5">
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
<div className="flex w-full flex-col gap-2">
<div className="flex flex-col gap-px">
<p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
<p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
</div>
{progress ? <Progress value={progress} /> : null}
</div>
</div>
<div className="flex items-center gap-2">
<Button type="button" variant="outline" size="icon" className="size-7" onClick={onRemove}>
<X className="size-4" aria-hidden="true" />
<span className="sr-only">Remove file</span>
</Button>
</div>
</div>
)
}
function isFileWithPreview(file: File): file is File & { preview: string } {
return 'preview' in file && typeof file.preview === 'string'
}
interface FilePreviewProps {
file: File & { preview: string }
}
function FilePreview({ file }: FilePreviewProps) {
if (file.type.startsWith('image/')) {
return <div className="aspect-square shrink-0 rounded-md object-cover" />
}
return <FileText className="text-muted-foreground size-10" aria-hidden="true" />
}
export default FileUploader

View File

@@ -0,0 +1,23 @@
import * as React from 'react'
import * as ProgressPrimitive from '@radix-ui/react-progress'
import { cn } from '@/lib/utils'
const Progress = React.forwardRef<
React.ComponentRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn('bg-secondary relative h-4 w-full overflow-hidden rounded-full', className)}
{...props}
>
<ProgressPrimitive.Indicator
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export default Progress

View File

@@ -0,0 +1,44 @@
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { cn } from '@/lib/utils'
const ScrollArea = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ComponentRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none transition-colors select-none',
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@@ -0,0 +1,94 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
)
)
Table.displayName = 'Table'
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
))
TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className
)}
{...props}
/>
)
)
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('text-muted-foreground mt-4 text-sm', className)} {...props} />
))
TableCaption.displayName = 'TableCaption'
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }

View File

@@ -0,0 +1,53 @@
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import { cn } from '@/lib/utils'
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1',
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm',
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ComponentRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -16,7 +16,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 mx-1 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
className
)}
{...props}

View 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>
)
}

View File

@@ -10,7 +10,7 @@ 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 ThemeToggle from '@/components/ThemeToggle'
import ZoomControl from '@/components/ZoomControl'
import FullScreenControl from '@/components/FullScreenControl'
import Settings from '@/components/Settings'
@@ -166,7 +166,7 @@ const GraphViewer = () => {
<ZoomControl />
<LayoutsControl />
<FullScreenControl />
<ThemeToggle />
{/* <ThemeToggle /> */}
</div>
{showPropertyPanel && (

View 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>
)
}

View File

@@ -24,7 +24,7 @@ const validateGraph = (graph: RawGraph) => {
}
for (const edge of graph.edges) {
if (!edge.id || !edge.source || !edge.target || !edge.type || !edge.properties) {
if (!edge.id || !edge.source || !edge.target) {
return false
}
}
@@ -88,6 +88,14 @@ const fetchGraph = async (label: string) => {
if (source !== undefined && source !== undefined) {
const sourceNode = rawData.nodes[source]
const targetNode = rawData.nodes[target]
if (!sourceNode) {
console.error(`Source node ${edge.source} is undefined`)
continue
}
if (!targetNode) {
console.error(`Target node ${edge.target} is undefined`)
continue
}
sourceNode.degree += 1
targetNode.degree += 1
}
@@ -146,7 +154,7 @@ const createSigmaGraph = (rawGraph: RawGraph | null) => {
for (const rawEdge of rawGraph?.edges ?? []) {
rawEdge.dynamicId = graph.addDirectedEdge(rawEdge.source, rawEdge.target, {
label: rawEdge.type
label: rawEdge.type || undefined
})
}

View File

@@ -1,6 +1,7 @@
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
@plugin 'tailwind-scrollbar';
@custom-variant dark (&:is(.dark *));
@@ -142,3 +143,27 @@
@apply bg-background text-foreground;
}
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background-color: hsl(0 0% 80%);
border-radius: 5px;
}
::-webkit-scrollbar-track {
background-color: hsl(0 0% 95%);
}
.dark {
::-webkit-scrollbar-thumb {
background-color: hsl(0 0% 90%);
}
::-webkit-scrollbar-track {
background-color: hsl(0 0% 0%);
}
}

View File

@@ -23,3 +23,18 @@ export const maxNodeSize = 20
export const healthCheckInterval = 15 // seconds
export const defaultQueryLabel = '*'
// reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types
export const supportedFileTypes = {
'text/plain': ['.txt', '.md'],
'application/pdf': ['.pdf'],
'application/msword': ['.doc'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx']
}
export const SiteInfo = {
name: 'LightRAG',
home: '/',
github: 'https://github.com/HKUDS/LightRAG'
}

View File

@@ -19,7 +19,7 @@ export type RawEdgeType = {
id: string
source: string
target: string
type: string
type?: string
properties: Record<string, any>
dynamicId: string