Merge pull request #1192 from danielaskdd/pipeline-status

Feat: Add Pipeline status to WebUI
This commit is contained in:
Daniel.y
2025-03-26 19:08:27 +08:00
committed by GitHub
20 changed files with 676 additions and 267 deletions

View File

@@ -1 +1 @@
__api_version__ = "1.2.6" __api_version__ = "1.2.7"

View File

@@ -421,37 +421,44 @@ def create_app(args):
@app.get("/health", dependencies=[Depends(combined_auth)]) @app.get("/health", dependencies=[Depends(combined_auth)])
async def get_status(): async def get_status():
"""Get current system status""" """Get current system status"""
username = os.getenv("AUTH_USERNAME") try:
password = os.getenv("AUTH_PASSWORD") pipeline_status = await get_namespace_data("pipeline_status")
if not (username and password):
auth_mode = "disabled"
else:
auth_mode = "enabled"
return { username = os.getenv("AUTH_USERNAME")
"status": "healthy", password = os.getenv("AUTH_PASSWORD")
"working_directory": str(args.working_dir), if not (username and password):
"input_directory": str(args.input_dir), auth_mode = "disabled"
"configuration": { else:
# LLM configuration binding/host address (if applicable)/model (if applicable) auth_mode = "enabled"
"llm_binding": args.llm_binding,
"llm_binding_host": args.llm_binding_host, return {
"llm_model": args.llm_model, "status": "healthy",
# embedding model configuration binding/host address (if applicable)/model (if applicable) "working_directory": str(args.working_dir),
"embedding_binding": args.embedding_binding, "input_directory": str(args.input_dir),
"embedding_binding_host": args.embedding_binding_host, "configuration": {
"embedding_model": args.embedding_model, # LLM configuration binding/host address (if applicable)/model (if applicable)
"max_tokens": args.max_tokens, "llm_binding": args.llm_binding,
"kv_storage": args.kv_storage, "llm_binding_host": args.llm_binding_host,
"doc_status_storage": args.doc_status_storage, "llm_model": args.llm_model,
"graph_storage": args.graph_storage, # embedding model configuration binding/host address (if applicable)/model (if applicable)
"vector_storage": args.vector_storage, "embedding_binding": args.embedding_binding,
"enable_llm_cache_for_extract": args.enable_llm_cache_for_extract, "embedding_binding_host": args.embedding_binding_host,
}, "embedding_model": args.embedding_model,
"core_version": core_version, "max_tokens": args.max_tokens,
"api_version": __api_version__, "kv_storage": args.kv_storage,
"auth_mode": auth_mode, "doc_status_storage": args.doc_status_storage,
} "graph_storage": args.graph_storage,
"vector_storage": args.vector_storage,
"enable_llm_cache_for_extract": args.enable_llm_cache_for_extract,
},
"core_version": core_version,
"api_version": __api_version__,
"auth_mode": auth_mode,
"pipeline_busy": pipeline_status.get("busy", False),
}
except Exception as e:
logger.error(f"Error getting health status: {str(e)}")
raise HTTPException(status_code=500, detail=str(e))
# Custom StaticFiles class to prevent caching of HTML files # Custom StaticFiles class to prevent caching of HTML files
class NoCacheStaticFiles(StaticFiles): class NoCacheStaticFiles(StaticFiles):

View File

@@ -164,61 +164,6 @@ def get_combined_auth_dependency(api_key: Optional[str] = None):
return combined_dependency return combined_dependency
def get_api_key_dependency(api_key: Optional[str]):
"""
Create an API key dependency for route protection.
Args:
api_key (Optional[str]): The API key to validate against.
If None, no authentication is required.
Returns:
Callable: A dependency function that validates the API key.
"""
# Use global whitelist_patterns and auth_configured variables
# whitelist_patterns and auth_configured are already initialized at module level
# Only calculate api_key_configured as it depends on the function parameter
api_key_configured = bool(api_key)
if not api_key_configured:
# If no API key is configured, return a dummy dependency that always succeeds
async def no_auth(request: Request = None, **kwargs):
return None
return no_auth
# If API key is configured, use proper authentication with Security for Swagger UI
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
async def api_key_auth(
request: Request,
api_key_header_value: Optional[str] = Security(
api_key_header, description="API Key for authentication"
),
):
# Check if request path is in whitelist
path = request.url.path
for pattern, is_prefix in whitelist_patterns:
if (is_prefix and path.startswith(pattern)) or (
not is_prefix and path == pattern
):
return # Whitelist path, allow access
# Non-whitelist path, validate API key
if not api_key_header_value:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="API Key required"
)
if api_key_header_value != api_key:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Invalid API Key"
)
return api_key_header_value
return api_key_auth
class DefaultRAGStorageConfig: class DefaultRAGStorageConfig:
KV_STORAGE = "JsonKVStorage" KV_STORAGE = "JsonKVStorage"
VECTOR_STORAGE = "NanoVectorDBStorage" VECTOR_STORAGE = "NanoVectorDBStorage"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<link rel="icon" type="image/svg+xml" href="logo.png" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-CSLDmoSD.js"></script> <script type="module" crossorigin src="/webui/assets/index-BX3dHkLt.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-CR7GLPiB.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-CbzkrOyx.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -377,7 +377,7 @@ async def initialize_pipeline_status():
{ {
"autoscanned": False, # Auto-scan started "autoscanned": False, # Auto-scan started
"busy": False, # Control concurrent processes "busy": False, # Control concurrent processes
"job_name": "Default Job", # Current job name (indexing files/indexing texts) "job_name": "-", # Current job name (indexing files/indexing texts)
"job_start": None, # Job start time "job_start": None, # Job start time
"docs": 0, # Total number of documents to be indexed "docs": 0, # Total number of documents to be indexed
"batchs": 0, # Number of batches for processing documents "batchs": 0, # Number of batches for processing documents

View File

@@ -845,7 +845,7 @@ class LightRAG:
pipeline_status.update( pipeline_status.update(
{ {
"busy": True, "busy": True,
"job_name": "indexing files", "job_name": "Default Job",
"job_start": datetime.now().isoformat(), "job_start": datetime.now().isoformat(),
"docs": 0, "docs": 0,
"batchs": 0, "batchs": 0,
@@ -884,11 +884,21 @@ class LightRAG:
logger.info(log_message) logger.info(log_message)
# Update pipeline status with current batch information # Update pipeline status with current batch information
pipeline_status["docs"] += len(to_process_docs) pipeline_status["docs"] = len(to_process_docs)
pipeline_status["batchs"] += len(docs_batches) pipeline_status["batchs"] = len(docs_batches)
pipeline_status["latest_message"] = log_message pipeline_status["latest_message"] = log_message
pipeline_status["history_messages"].append(log_message) pipeline_status["history_messages"].append(log_message)
# Get first document's file path and total count for job name
first_doc_id, first_doc = next(iter(to_process_docs.items()))
first_doc_path = first_doc.file_path
path_prefix = first_doc_path[:20] + (
"..." if len(first_doc_path) > 20 else ""
)
total_files = len(to_process_docs)
job_name = f"{path_prefix}[{total_files} files]"
pipeline_status["job_name"] = job_name
async def process_document( async def process_document(
doc_id: str, doc_id: str,
status_doc: DocProcessingStatus, status_doc: DocProcessingStatus,

View File

@@ -45,6 +45,7 @@ export type LightragStatus = {
core_version?: string core_version?: string
api_version?: string api_version?: string
auth_mode?: 'enabled' | 'disabled' auth_mode?: 'enabled' | 'disabled'
pipeline_busy: boolean
} }
export type LightragDocumentsScanProgress = { export type LightragDocumentsScanProgress = {
@@ -141,6 +142,20 @@ export type AuthStatusResponse = {
api_version?: string api_version?: string
} }
export type PipelineStatusResponse = {
autoscanned: boolean
busy: boolean
job_name: string
job_start?: string
docs: number
batchs: number
cur_batch: number
request_pending: boolean
latest_message: string
history_messages?: string[]
update_status?: Record<string, any>
}
export type LoginResponse = { export type LoginResponse = {
access_token: string access_token: string
token_type: string token_type: string
@@ -424,6 +439,11 @@ export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
} }
} }
export const getPipelineStatus = async (): Promise<PipelineStatusResponse> => {
const response = await axiosInstance.get('/documents/pipeline_status')
return response.data
}
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => { export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
const formData = new FormData(); const formData = new FormData();
formData.append('username', username); formData.append('username', username);

View File

@@ -31,7 +31,7 @@ export default function ClearDocumentsDialog() {
} catch (err) { } catch (err) {
toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) })) toast.error(t('documentPanel.clearDocuments.error', { error: errorMessage(err) }))
} }
}, [setOpen]) }, [setOpen, t])
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
@@ -46,7 +46,7 @@ export default function ClearDocumentsDialog() {
<DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription> <DialogDescription>{t('documentPanel.clearDocuments.confirm')}</DialogDescription>
</DialogHeader> </DialogHeader>
<Button variant="destructive" onClick={handleClear}> <Button variant="destructive" onClick={handleClear}>
{t('documentPanel.clearDocuments.confirmButton')} {t('documentPanel.clearDocuments.confirmButton')}
</Button> </Button>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@@ -0,0 +1,193 @@
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { AlignLeft, AlignCenter, AlignRight } from 'lucide-react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription
} from '@/components/ui/Dialog'
import Button from '@/components/ui/Button'
import { getPipelineStatus, PipelineStatusResponse } from '@/api/lightrag'
import { errorMessage } from '@/lib/utils'
import { cn } from '@/lib/utils'
type DialogPosition = 'left' | 'center' | 'right'
interface PipelineStatusDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export default function PipelineStatusDialog({
open,
onOpenChange
}: PipelineStatusDialogProps) {
const { t } = useTranslation()
const [status, setStatus] = useState<PipelineStatusResponse | null>(null)
const [position, setPosition] = useState<DialogPosition>('center')
const [isUserScrolled, setIsUserScrolled] = useState(false)
const historyRef = useRef<HTMLDivElement>(null)
// Reset position when dialog opens
useEffect(() => {
if (open) {
setPosition('center')
setIsUserScrolled(false)
}
}, [open])
// Handle scroll position
useEffect(() => {
const container = historyRef.current
if (!container || isUserScrolled) return
container.scrollTop = container.scrollHeight
}, [status?.history_messages, isUserScrolled])
const handleScroll = () => {
const container = historyRef.current
if (!container) return
const isAtBottom = Math.abs(
(container.scrollHeight - container.scrollTop) - container.clientHeight
) < 1
if (isAtBottom) {
setIsUserScrolled(false)
} else {
setIsUserScrolled(true)
}
}
// Refresh status every 2 seconds
useEffect(() => {
if (!open) return
const fetchStatus = async () => {
try {
const data = await getPipelineStatus()
setStatus(data)
} catch (err) {
toast.error(t('documentPanel.pipelineStatus.errors.fetchFailed', { error: errorMessage(err) }))
}
}
fetchStatus()
const interval = setInterval(fetchStatus, 2000)
return () => clearInterval(interval)
}, [open, t])
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
className={cn(
'sm:max-w-[600px] transition-all duration-200 fixed',
position === 'left' && '!left-[25%] !translate-x-[-50%] !mx-4',
position === 'center' && '!left-1/2 !-translate-x-1/2',
position === 'right' && '!left-[75%] !translate-x-[-50%] !mx-4'
)}
>
<DialogDescription className="sr-only">
{status?.job_name
? `${t('documentPanel.pipelineStatus.jobName')}: ${status.job_name}, ${t('documentPanel.pipelineStatus.progress')}: ${status.cur_batch}/${status.batchs}`
: t('documentPanel.pipelineStatus.noActiveJob')
}
</DialogDescription>
<DialogHeader className="flex flex-row items-center">
<DialogTitle className="flex-1">
{t('documentPanel.pipelineStatus.title')}
</DialogTitle>
{/* Position control buttons */}
<div className="flex items-center gap-2 mr-8">
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6',
position === 'left' && 'bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600'
)}
onClick={() => setPosition('left')}
>
<AlignLeft className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6',
position === 'center' && 'bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600'
)}
onClick={() => setPosition('center')}
>
<AlignCenter className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className={cn(
'h-6 w-6',
position === 'right' && 'bg-zinc-200 text-zinc-800 hover:bg-zinc-300 dark:bg-zinc-700 dark:text-zinc-200 dark:hover:bg-zinc-600'
)}
onClick={() => setPosition('right')}
>
<AlignRight className="h-4 w-4" />
</Button>
</div>
</DialogHeader>
{/* Status Content */}
<div className="space-y-4 pt-4">
{/* Pipeline Status */}
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{t('documentPanel.pipelineStatus.busy')}:</div>
<div className={`h-2 w-2 rounded-full ${status?.busy ? 'bg-green-500' : 'bg-gray-300'}`} />
</div>
<div className="flex items-center gap-2">
<div className="text-sm font-medium">{t('documentPanel.pipelineStatus.requestPending')}:</div>
<div className={`h-2 w-2 rounded-full ${status?.request_pending ? 'bg-green-500' : 'bg-gray-300'}`} />
</div>
</div>
{/* Job Information */}
<div className="rounded-md border p-3 space-y-2">
<div>{t('documentPanel.pipelineStatus.jobName')}: {status?.job_name || '-'}</div>
<div className="flex justify-between">
<span>{t('documentPanel.pipelineStatus.startTime')}: {status?.job_start ? new Date(status.job_start).toLocaleString() : '-'}</span>
<span>{t('documentPanel.pipelineStatus.progress')}: {status ? `${status.cur_batch}/${status.batchs} ${t('documentPanel.pipelineStatus.unit')}` : '-'}</span>
</div>
</div>
{/* Latest Message */}
<div className="space-y-2">
<div className="text-sm font-medium">{t('documentPanel.pipelineStatus.latestMessage')}:</div>
<div className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3">
{status?.latest_message || '-'}
</div>
</div>
{/* History Messages */}
<div className="space-y-2">
<div className="text-sm font-medium">{t('documentPanel.pipelineStatus.historyMessages')}:</div>
<div
ref={historyRef}
onScroll={handleScroll}
className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3 overflow-y-auto min-h-[7.5em] max-h-[40vh]"
>
{status?.history_messages?.length ? (
status.history_messages.map((msg, idx) => (
<div key={idx}>{msg}</div>
))
) : '-'}
</div>
</div>
</div>
</DialogContent>
</Dialog>
)
}

View File

@@ -16,7 +16,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
className={cn( className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className className
)} )}
{...props} {...props}

View File

@@ -19,7 +19,7 @@ const DialogOverlay = React.forwardRef<
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/30',
className className
)} )}
{...props} {...props}

View File

@@ -1,7 +1,8 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from '@/stores/settings'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import { cn } from '@/lib/utils'
import { import {
Table, Table,
TableBody, TableBody,
@@ -20,8 +21,9 @@ import { errorMessage } from '@/lib/utils'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useBackendState } from '@/stores/state' import { useBackendState } from '@/stores/state'
import { RefreshCwIcon } from 'lucide-react' import { RefreshCwIcon, ActivityIcon } from 'lucide-react'
import { DocStatusResponse } from '@/api/lightrag' import { DocStatusResponse } from '@/api/lightrag'
import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => { const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
// Check if file_path exists and is a non-empty string // Check if file_path exists and is a non-empty string
@@ -44,19 +46,102 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
: fileName; : fileName;
}; };
const pulseStyle = `
@keyframes pulse {
0% {
background-color: rgb(255 0 0 / 0.1);
border-color: rgb(255 0 0 / 0.2);
}
50% {
background-color: rgb(255 0 0 / 0.2);
border-color: rgb(255 0 0 / 0.4);
}
100% {
background-color: rgb(255 0 0 / 0.1);
border-color: rgb(255 0 0 / 0.2);
}
}
.dark .pipeline-busy {
animation: dark-pulse 2s infinite;
}
@keyframes dark-pulse {
0% {
background-color: rgb(255 0 0 / 0.2);
border-color: rgb(255 0 0 / 0.4);
}
50% {
background-color: rgb(255 0 0 / 0.3);
border-color: rgb(255 0 0 / 0.6);
}
100% {
background-color: rgb(255 0 0 / 0.2);
border-color: rgb(255 0 0 / 0.4);
}
}
.pipeline-busy {
animation: pulse 2s infinite;
border: 1px solid;
}
`;
export default function DocumentManager() { export default function DocumentManager() {
const [showPipelineStatus, setShowPipelineStatus] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
const health = useBackendState.use.health() const health = useBackendState.use.health()
const pipelineBusy = useBackendState.use.pipelineBusy()
const [docs, setDocs] = useState<DocsStatusesResponse | null>(null) const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
const currentTab = useSettingsStore.use.currentTab() const currentTab = useSettingsStore.use.currentTab()
const showFileName = useSettingsStore.use.showFileName() const showFileName = useSettingsStore.use.showFileName()
const setShowFileName = useSettingsStore.use.setShowFileName() const setShowFileName = useSettingsStore.use.setShowFileName()
// Store previous status counts
const prevStatusCounts = useRef({
processed: 0,
processing: 0,
pending: 0,
failed: 0
})
// Add pulse style to document
useEffect(() => {
const style = document.createElement('style')
style.textContent = pulseStyle
document.head.appendChild(style)
return () => {
document.head.removeChild(style)
}
}, [])
const fetchDocuments = useCallback(async () => { const fetchDocuments = useCallback(async () => {
try { try {
const docs = await getDocuments() const docs = await getDocuments()
// Get new status counts (treat null as all zeros)
const newStatusCounts = {
processed: docs?.statuses?.processed?.length || 0,
processing: docs?.statuses?.processing?.length || 0,
pending: docs?.statuses?.pending?.length || 0,
failed: docs?.statuses?.failed?.length || 0
}
// Check if any status count has changed
const hasStatusCountChange = (Object.keys(newStatusCounts) as Array<keyof typeof newStatusCounts>).some(
status => newStatusCounts[status] !== prevStatusCounts.current[status]
)
// Trigger health check if changes detected
if (hasStatusCountChange) {
useBackendState.getState().check()
}
// Update previous status counts
prevStatusCounts.current = newStatusCounts
// Update docs state
if (docs && docs.statuses) { if (docs && docs.statuses) {
// compose all documents count
const numDocuments = Object.values(docs.statuses).reduce( const numDocuments = Object.values(docs.statuses).reduce(
(acc, status) => acc + status.length, (acc, status) => acc + status.length,
0 0
@@ -114,18 +199,36 @@ export default function DocumentManager() {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex gap-2"> <div className="flex gap-2">
<Button <div className="flex gap-2">
variant="outline" <Button
onClick={scanDocuments} variant="outline"
side="bottom" onClick={scanDocuments}
tooltip={t('documentPanel.documentManager.scanTooltip')} side="bottom"
size="sm" tooltip={t('documentPanel.documentManager.scanTooltip')}
> size="sm"
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')} >
</Button> <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
</Button>
<Button
variant="outline"
onClick={() => setShowPipelineStatus(true)}
side="bottom"
tooltip={t('documentPanel.documentManager.pipelineStatusTooltip')}
size="sm"
className={cn(
pipelineBusy && 'pipeline-busy'
)}
>
<ActivityIcon /> {t('documentPanel.documentManager.pipelineStatusButton')}
</Button>
</div>
<div className="flex-1" /> <div className="flex-1" />
<ClearDocumentsDialog /> <ClearDocumentsDialog />
<UploadDocumentsDialog /> <UploadDocumentsDialog />
<PipelineStatusDialog
open={showPipelineStatus}
onOpenChange={setShowPipelineStatus}
/>
</div> </div>
<Card> <Card>

View File

@@ -59,6 +59,8 @@
"title": "إدارة المستندات", "title": "إدارة المستندات",
"scanButton": "مسح ضوئي", "scanButton": "مسح ضوئي",
"scanTooltip": "مسح المستندات ضوئيًا", "scanTooltip": "مسح المستندات ضوئيًا",
"pipelineStatusButton": "حالة خط المعالجة",
"pipelineStatusTooltip": "عرض حالة خط المعالجة",
"uploadedTitle": "المستندات المرفوعة", "uploadedTitle": "المستندات المرفوعة",
"uploadedDescription": "قائمة المستندات المرفوعة وحالاتها.", "uploadedDescription": "قائمة المستندات المرفوعة وحالاتها.",
"emptyTitle": "لا توجد مستندات", "emptyTitle": "لا توجد مستندات",
@@ -89,6 +91,20 @@
"hideButton": "إخفاء", "hideButton": "إخفاء",
"showFileNameTooltip": "عرض اسم الملف", "showFileNameTooltip": "عرض اسم الملف",
"hideFileNameTooltip": "إخفاء اسم الملف" "hideFileNameTooltip": "إخفاء اسم الملف"
},
"pipelineStatus": {
"title": "حالة خط المعالجة",
"busy": "خط المعالجة مشغول",
"requestPending": "الطلب معلق",
"jobName": "اسم المهمة",
"startTime": "وقت البدء",
"progress": "التقدم",
"unit": "دفعة",
"latestMessage": "آخر رسالة",
"historyMessages": "سجل الرسائل",
"errors": {
"fetchFailed": "فشل في جلب حالة خط المعالجة\n{{error}}"
}
} }
}, },
"graphPanel": { "graphPanel": {

View File

@@ -59,6 +59,8 @@
"title": "Document Management", "title": "Document Management",
"scanButton": "Scan", "scanButton": "Scan",
"scanTooltip": "Scan documents", "scanTooltip": "Scan documents",
"pipelineStatusButton": "Pipeline Status",
"pipelineStatusTooltip": "View pipeline status",
"uploadedTitle": "Uploaded Documents", "uploadedTitle": "Uploaded Documents",
"uploadedDescription": "List of uploaded documents and their statuses.", "uploadedDescription": "List of uploaded documents and their statuses.",
"emptyTitle": "No Documents", "emptyTitle": "No Documents",
@@ -89,6 +91,20 @@
"hideButton": "Hide", "hideButton": "Hide",
"showFileNameTooltip": "Show file name", "showFileNameTooltip": "Show file name",
"hideFileNameTooltip": "Hide file name" "hideFileNameTooltip": "Hide file name"
},
"pipelineStatus": {
"title": "Pipeline Status",
"busy": "Pipeline Busy",
"requestPending": "Reques Pending",
"jobName": "Job Name",
"startTime": "Start Time",
"progress": "Progress",
"unit": "batch",
"latestMessage": "Latest Message",
"historyMessages": "History Message",
"errors": {
"fetchFailed": "Fail to get pipeline status\n{{error}}"
}
} }
}, },
"graphPanel": { "graphPanel": {
@@ -113,7 +129,6 @@
"save": "Save", "save": "Save",
"refreshLayout": "Refresh Layout" "refreshLayout": "Refresh Layout"
}, },
"zoomControl": { "zoomControl": {
"zoomIn": "Zoom In", "zoomIn": "Zoom In",
"zoomOut": "Zoom Out", "zoomOut": "Zoom Out",
@@ -121,7 +136,6 @@
"rotateCamera": "Clockwise Rotate", "rotateCamera": "Clockwise Rotate",
"rotateCameraCounterClockwise": "Counter-Clockwise Rotate" "rotateCameraCounterClockwise": "Counter-Clockwise Rotate"
}, },
"layoutsControl": { "layoutsControl": {
"startAnimation": "Continue layout animation", "startAnimation": "Continue layout animation",
"stopAnimation": "Stop layout animation", "stopAnimation": "Stop layout animation",
@@ -135,7 +149,6 @@
"Force Atlas": "Force Atlas" "Force Atlas": "Force Atlas"
} }
}, },
"fullScreenControl": { "fullScreenControl": {
"fullScreen": "Full Screen", "fullScreen": "Full Screen",
"windowed": "Windowed" "windowed": "Windowed"
@@ -224,7 +237,6 @@
"querySettings": { "querySettings": {
"parametersTitle": "Parameters", "parametersTitle": "Parameters",
"parametersDescription": "Configure your query parameters", "parametersDescription": "Configure your query parameters",
"queryMode": "Query Mode", "queryMode": "Query Mode",
"queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval", "queryModeTooltip": "Select the retrieval strategy:\n• Naive: Basic search without advanced techniques\n• Local: Context-dependent information retrieval\n• Global: Utilizes global knowledge base\n• Hybrid: Combines local and global retrieval\n• Mix: Integrates knowledge graph with vector retrieval",
"queryModeOptions": { "queryModeOptions": {
@@ -234,7 +246,6 @@
"hybrid": "Hybrid", "hybrid": "Hybrid",
"mix": "Mix" "mix": "Mix"
}, },
"responseFormat": "Response Format", "responseFormat": "Response Format",
"responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points", "responseFormatTooltip": "Defines the response format. Examples:\n• Multiple Paragraphs\n• Single Paragraph\n• Bullet Points",
"responseFormatOptions": { "responseFormatOptions": {
@@ -242,37 +253,27 @@
"singleParagraph": "Single Paragraph", "singleParagraph": "Single Paragraph",
"bulletPoints": "Bullet Points" "bulletPoints": "Bullet Points"
}, },
"topK": "Top K Results", "topK": "Top K Results",
"topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode", "topKTooltip": "Number of top items to retrieve. Represents entities in 'local' mode and relationships in 'global' mode",
"topKPlaceholder": "Number of results", "topKPlaceholder": "Number of results",
"maxTokensTextUnit": "Max Tokens for Text Unit", "maxTokensTextUnit": "Max Tokens for Text Unit",
"maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk", "maxTokensTextUnitTooltip": "Maximum number of tokens allowed for each retrieved text chunk",
"maxTokensGlobalContext": "Max Tokens for Global Context", "maxTokensGlobalContext": "Max Tokens for Global Context",
"maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval", "maxTokensGlobalContextTooltip": "Maximum number of tokens allocated for relationship descriptions in global retrieval",
"maxTokensLocalContext": "Max Tokens for Local Context", "maxTokensLocalContext": "Max Tokens for Local Context",
"maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval", "maxTokensLocalContextTooltip": "Maximum number of tokens allocated for entity descriptions in local retrieval",
"historyTurns": "History Turns", "historyTurns": "History Turns",
"historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context", "historyTurnsTooltip": "Number of complete conversation turns (user-assistant pairs) to consider in the response context",
"historyTurnsPlaceholder": "Number of history turns", "historyTurnsPlaceholder": "Number of history turns",
"hlKeywords": "High-Level Keywords", "hlKeywords": "High-Level Keywords",
"hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas", "hlKeywordsTooltip": "List of high-level keywords to prioritize in retrieval. Separate with commas",
"hlkeywordsPlaceHolder": "Enter keywords", "hlkeywordsPlaceHolder": "Enter keywords",
"llKeywords": "Low-Level Keywords", "llKeywords": "Low-Level Keywords",
"llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas", "llKeywordsTooltip": "List of low-level keywords to refine retrieval focus. Separate with commas",
"onlyNeedContext": "Only Need Context", "onlyNeedContext": "Only Need Context",
"onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response", "onlyNeedContextTooltip": "If True, only returns the retrieved context without generating a response",
"onlyNeedPrompt": "Only Need Prompt", "onlyNeedPrompt": "Only Need Prompt",
"onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response", "onlyNeedPromptTooltip": "If True, only returns the generated prompt without producing a response",
"streamResponse": "Stream Response", "streamResponse": "Stream Response",
"streamResponseTooltip": "If True, enables streaming output for real-time responses" "streamResponseTooltip": "If True, enables streaming output for real-time responses"
} }

View File

@@ -59,6 +59,8 @@
"title": "Gestion des documents", "title": "Gestion des documents",
"scanButton": "Scanner", "scanButton": "Scanner",
"scanTooltip": "Scanner les documents", "scanTooltip": "Scanner les documents",
"pipelineStatusButton": "État du Pipeline",
"pipelineStatusTooltip": "Voir l'état du pipeline",
"uploadedTitle": "Documents téléchargés", "uploadedTitle": "Documents téléchargés",
"uploadedDescription": "Liste des documents téléchargés et leurs statuts.", "uploadedDescription": "Liste des documents téléchargés et leurs statuts.",
"emptyTitle": "Aucun document", "emptyTitle": "Aucun document",
@@ -83,6 +85,25 @@
"loadFailed": "Échec du chargement des documents\n{{error}}", "loadFailed": "Échec du chargement des documents\n{{error}}",
"scanFailed": "Échec de la numérisation des documents\n{{error}}", "scanFailed": "Échec de la numérisation des documents\n{{error}}",
"scanProgressFailed": "Échec de l'obtention de la progression de la numérisation\n{{error}}" "scanProgressFailed": "Échec de l'obtention de la progression de la numérisation\n{{error}}"
},
"fileNameLabel": "Nom du fichier",
"showButton": "Afficher",
"hideButton": "Masquer",
"showFileNameTooltip": "Afficher le nom du fichier",
"hideFileNameTooltip": "Masquer le nom du fichier"
},
"pipelineStatus": {
"title": "État du Pipeline",
"busy": "Pipeline occupé",
"requestPending": "Requête en attente",
"jobName": "Nom du travail",
"startTime": "Heure de début",
"progress": "Progression",
"unit": "lot",
"latestMessage": "Dernier message",
"historyMessages": "Historique des messages",
"errors": {
"fetchFailed": "Échec de la récupération de l'état du pipeline\n{{error}}"
} }
} }
}, },

View File

@@ -59,6 +59,8 @@
"title": "文档管理", "title": "文档管理",
"scanButton": "扫描", "scanButton": "扫描",
"scanTooltip": "扫描文档", "scanTooltip": "扫描文档",
"pipelineStatusButton": "流水线状态",
"pipelineStatusTooltip": "查看流水线状态",
"uploadedTitle": "已上传文档", "uploadedTitle": "已上传文档",
"uploadedDescription": "已上传文档列表及其状态", "uploadedDescription": "已上传文档列表及其状态",
"emptyTitle": "无文档", "emptyTitle": "无文档",
@@ -89,6 +91,20 @@
"hideButton": "隐藏", "hideButton": "隐藏",
"showFileNameTooltip": "显示文件名", "showFileNameTooltip": "显示文件名",
"hideFileNameTooltip": "隐藏文件名" "hideFileNameTooltip": "隐藏文件名"
},
"pipelineStatus": {
"title": "流水线状态",
"busy": "流水线忙碌",
"requestPending": "待处理请求",
"jobName": "作业名称",
"startTime": "开始时间",
"progress": "进度",
"unit": "批",
"latestMessage": "最新消息",
"historyMessages": "历史消息",
"errors": {
"fetchFailed": "获取流水线状态失败\n{{error}}"
}
} }
}, },
"graphPanel": { "graphPanel": {

View File

@@ -6,14 +6,14 @@ interface BackendState {
health: boolean health: boolean
message: string | null message: string | null
messageTitle: string | null messageTitle: string | null
status: LightragStatus | null status: LightragStatus | null
lastCheckTime: number lastCheckTime: number
pipelineBusy: boolean
check: () => Promise<boolean> check: () => Promise<boolean>
clear: () => void clear: () => void
setErrorMessage: (message: string, messageTitle: string) => void setErrorMessage: (message: string, messageTitle: string) => void
setPipelineBusy: (busy: boolean) => void
} }
interface AuthState { interface AuthState {
@@ -34,6 +34,7 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
messageTitle: null, messageTitle: null,
lastCheckTime: Date.now(), lastCheckTime: Date.now(),
status: null, status: null,
pipelineBusy: false,
check: async () => { check: async () => {
const health = await checkHealth() const health = await checkHealth()
@@ -51,7 +52,8 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
message: null, message: null,
messageTitle: null, messageTitle: null,
lastCheckTime: Date.now(), lastCheckTime: Date.now(),
status: health status: health,
pipelineBusy: health.pipeline_busy
}) })
return true return true
} }
@@ -71,6 +73,10 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
setErrorMessage: (message: string, messageTitle: string) => { setErrorMessage: (message: string, messageTitle: string) => {
set({ health: false, message, messageTitle }) set({ health: false, message, messageTitle })
},
setPipelineBusy: (busy: boolean) => {
set({ pipelineBusy: busy })
} }
})) }))