feat: add pipeline status monitoring dialog

- Add pipeline status API and types
- Create PipelineStatusDialog component with position control
- Unify modal overlay style across components
This commit is contained in:
yangdx
2025-03-26 12:05:54 +08:00
parent e1d43ee831
commit d7c0b420b9
5 changed files with 245 additions and 12 deletions

View File

@@ -141,6 +141,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 +438,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

@@ -5,7 +5,8 @@ import {
AlertDialogContent, AlertDialogContent,
AlertDialogDescription, AlertDialogDescription,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle AlertDialogTitle,
AlertDialogOverlay
} from '@/components/ui/AlertDialog' } from '@/components/ui/AlertDialog'
import Button from '@/components/ui/Button' import Button from '@/components/ui/Button'
import Input from '@/components/ui/Input' import Input from '@/components/ui/Input'
@@ -50,6 +51,7 @@ const ApiKeyAlert = ({ open: opened, onOpenChange: setOpened }: ApiKeyAlertProps
return ( return (
<AlertDialog open={opened} onOpenChange={setOpened}> <AlertDialog open={opened} onOpenChange={setOpened}>
<AlertDialogOverlay className="bg-black/30" />
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>{t('apiKeyAlert.title')}</AlertDialogTitle> <AlertDialogTitle>{t('apiKeyAlert.title')}</AlertDialogTitle>

View File

@@ -0,0 +1,196 @@
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { X, AlignLeft, AlignCenter, AlignRight } from 'lucide-react'
import {
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogOverlay
} from '@/components/ui/AlertDialog'
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 (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogOverlay className="bg-black/30" />
<AlertDialogContent
className={cn(
'sm:max-w-[600px] transition-all duration-200',
position === 'left' && '!left-4 !translate-x-0',
position === 'center' && '!left-1/2 !-translate-x-1/2',
position === 'right' && '!right-4 !left-auto !translate-x-0'
)}
>
<AlertDialogHeader className="flex flex-row items-center justify-between">
<AlertDialogTitle>
{t('documentPanel.pipelineStatus.title')}
</AlertDialogTitle>
{/* Position control buttons and close button */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<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>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => onOpenChange(false)}
>
<X className="h-4 w-4" />
</Button>
</div>
</AlertDialogHeader>
{/* 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">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">Request Pending:</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>Job Name: {status?.job_name || '-'}</div>
<div className="flex justify-between">
<span>Start Time: {status?.job_start ? new Date(status.job_start).toLocaleString() : '-'}</span>
<span>Progress: {status ? `${status.cur_batch}/${status.batchs}` : '-'}</span>
</div>
</div>
{/* Latest Message */}
<div className="space-y-2">
<div className="text-sm font-medium">Latest Message:</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">History Messages:</div>
<div
ref={historyRef}
onScroll={handleScroll}
className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3 h-[20em] overflow-y-auto"
>
{status?.history_messages?.map((msg, idx) => (
<div key={idx}>{msg}</div>
)) || '-'}
</div>
</div>
</div>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@@ -30,7 +30,6 @@ const AlertDialogContent = React.forwardRef<
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<AlertDialogPortal> <AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
ref={ref} ref={ref}
className={cn( className={cn(

View File

@@ -20,8 +20,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
@@ -45,6 +46,7 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
}; };
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 [docs, setDocs] = useState<DocsStatusesResponse | null>(null) const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
@@ -113,6 +115,7 @@ export default function DocumentManager() {
<CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle> <CardTitle className="text-lg">{t('documentPanel.documentManager.title')}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex gap-2">
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="outline" variant="outline"
@@ -123,9 +126,23 @@ export default function DocumentManager() {
> >
<RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')} <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
</Button> </Button>
<Button
variant="outline"
onClick={() => setShowPipelineStatus(true)}
side="bottom"
tooltip="View pipeline status"
size="sm"
>
<ActivityIcon /> Pipeline Status
</Button>
</div>
<div className="flex-1" /> <div className="flex-1" />
<ClearDocumentsDialog /> <ClearDocumentsDialog />
<UploadDocumentsDialog /> <UploadDocumentsDialog />
<PipelineStatusDialog
open={showPipelineStatus}
onOpenChange={setShowPipelineStatus}
/>
</div> </div>
<Card> <Card>