yangdx commited on
Commit
e09663e
·
1 Parent(s): b8ab737

feat: add pipeline status monitoring dialog

Browse files

- Add pipeline status API and types
- Create PipelineStatusDialog component with position control
- Unify modal overlay style across components

lightrag_webui/src/api/lightrag.ts CHANGED
@@ -141,6 +141,20 @@ export type AuthStatusResponse = {
141
  api_version?: string
142
  }
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  export type LoginResponse = {
145
  access_token: string
146
  token_type: string
@@ -424,6 +438,11 @@ export const getAuthStatus = async (): Promise<AuthStatusResponse> => {
424
  }
425
  }
426
 
 
 
 
 
 
427
  export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
428
  const formData = new FormData();
429
  formData.append('username', username);
 
141
  api_version?: string
142
  }
143
 
144
+ export type PipelineStatusResponse = {
145
+ autoscanned: boolean
146
+ busy: boolean
147
+ job_name: string
148
+ job_start?: string
149
+ docs: number
150
+ batchs: number
151
+ cur_batch: number
152
+ request_pending: boolean
153
+ latest_message: string
154
+ history_messages?: string[]
155
+ update_status?: Record<string, any>
156
+ }
157
+
158
  export type LoginResponse = {
159
  access_token: string
160
  token_type: string
 
438
  }
439
  }
440
 
441
+ export const getPipelineStatus = async (): Promise<PipelineStatusResponse> => {
442
+ const response = await axiosInstance.get('/documents/pipeline_status')
443
+ return response.data
444
+ }
445
+
446
  export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
447
  const formData = new FormData();
448
  formData.append('username', username);
lightrag_webui/src/components/ApiKeyAlert.tsx CHANGED
@@ -5,7 +5,8 @@ import {
5
  AlertDialogContent,
6
  AlertDialogDescription,
7
  AlertDialogHeader,
8
- AlertDialogTitle
 
9
  } from '@/components/ui/AlertDialog'
10
  import Button from '@/components/ui/Button'
11
  import Input from '@/components/ui/Input'
@@ -50,6 +51,7 @@ const ApiKeyAlert = ({ open: opened, onOpenChange: setOpened }: ApiKeyAlertProps
50
 
51
  return (
52
  <AlertDialog open={opened} onOpenChange={setOpened}>
 
53
  <AlertDialogContent>
54
  <AlertDialogHeader>
55
  <AlertDialogTitle>{t('apiKeyAlert.title')}</AlertDialogTitle>
 
5
  AlertDialogContent,
6
  AlertDialogDescription,
7
  AlertDialogHeader,
8
+ AlertDialogTitle,
9
+ AlertDialogOverlay
10
  } from '@/components/ui/AlertDialog'
11
  import Button from '@/components/ui/Button'
12
  import Input from '@/components/ui/Input'
 
51
 
52
  return (
53
  <AlertDialog open={opened} onOpenChange={setOpened}>
54
+ <AlertDialogOverlay className="bg-black/30" />
55
  <AlertDialogContent>
56
  <AlertDialogHeader>
57
  <AlertDialogTitle>{t('apiKeyAlert.title')}</AlertDialogTitle>
lightrag_webui/src/components/documents/PipelineStatusDialog.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { toast } from 'sonner'
4
+ import { X, AlignLeft, AlignCenter, AlignRight } from 'lucide-react'
5
+
6
+ import {
7
+ AlertDialog,
8
+ AlertDialogContent,
9
+ AlertDialogHeader,
10
+ AlertDialogTitle,
11
+ AlertDialogOverlay
12
+ } from '@/components/ui/AlertDialog'
13
+ import Button from '@/components/ui/Button'
14
+ import { getPipelineStatus, PipelineStatusResponse } from '@/api/lightrag'
15
+ import { errorMessage } from '@/lib/utils'
16
+ import { cn } from '@/lib/utils'
17
+
18
+ type DialogPosition = 'left' | 'center' | 'right'
19
+
20
+ interface PipelineStatusDialogProps {
21
+ open: boolean
22
+ onOpenChange: (open: boolean) => void
23
+ }
24
+
25
+ export default function PipelineStatusDialog({
26
+ open,
27
+ onOpenChange
28
+ }: PipelineStatusDialogProps) {
29
+ const { t } = useTranslation()
30
+ const [status, setStatus] = useState<PipelineStatusResponse | null>(null)
31
+ const [position, setPosition] = useState<DialogPosition>('center')
32
+ const [isUserScrolled, setIsUserScrolled] = useState(false)
33
+ const historyRef = useRef<HTMLDivElement>(null)
34
+
35
+ // Reset position when dialog opens
36
+ useEffect(() => {
37
+ if (open) {
38
+ setPosition('center')
39
+ setIsUserScrolled(false)
40
+ }
41
+ }, [open])
42
+
43
+ // Handle scroll position
44
+ useEffect(() => {
45
+ const container = historyRef.current
46
+ if (!container || isUserScrolled) return
47
+
48
+ container.scrollTop = container.scrollHeight
49
+ }, [status?.history_messages, isUserScrolled])
50
+
51
+ const handleScroll = () => {
52
+ const container = historyRef.current
53
+ if (!container) return
54
+
55
+ const isAtBottom = Math.abs(
56
+ (container.scrollHeight - container.scrollTop) - container.clientHeight
57
+ ) < 1
58
+
59
+ if (isAtBottom) {
60
+ setIsUserScrolled(false)
61
+ } else {
62
+ setIsUserScrolled(true)
63
+ }
64
+ }
65
+
66
+ // Refresh status every 2 seconds
67
+ useEffect(() => {
68
+ if (!open) return
69
+
70
+ const fetchStatus = async () => {
71
+ try {
72
+ const data = await getPipelineStatus()
73
+ setStatus(data)
74
+ } catch (err) {
75
+ toast.error(t('documentPanel.pipelineStatus.errors.fetchFailed', { error: errorMessage(err) }))
76
+ }
77
+ }
78
+
79
+ fetchStatus()
80
+ const interval = setInterval(fetchStatus, 2000)
81
+ return () => clearInterval(interval)
82
+ }, [open, t])
83
+
84
+ return (
85
+ <AlertDialog open={open} onOpenChange={onOpenChange}>
86
+ <AlertDialogOverlay className="bg-black/30" />
87
+ <AlertDialogContent
88
+ className={cn(
89
+ 'sm:max-w-[600px] transition-all duration-200',
90
+ position === 'left' && '!left-4 !translate-x-0',
91
+ position === 'center' && '!left-1/2 !-translate-x-1/2',
92
+ position === 'right' && '!right-4 !left-auto !translate-x-0'
93
+ )}
94
+ >
95
+ <AlertDialogHeader className="flex flex-row items-center justify-between">
96
+ <AlertDialogTitle>
97
+ {t('documentPanel.pipelineStatus.title')}
98
+ </AlertDialogTitle>
99
+
100
+ {/* Position control buttons and close button */}
101
+ <div className="flex items-center gap-2">
102
+ <div className="flex items-center gap-1">
103
+ <Button
104
+ variant="ghost"
105
+ size="icon"
106
+ className={cn(
107
+ 'h-6 w-6',
108
+ 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'
109
+ )}
110
+ onClick={() => setPosition('left')}
111
+ >
112
+ <AlignLeft className="h-4 w-4" />
113
+ </Button>
114
+ <Button
115
+ variant="ghost"
116
+ size="icon"
117
+ className={cn(
118
+ 'h-6 w-6',
119
+ 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'
120
+ )}
121
+ onClick={() => setPosition('center')}
122
+ >
123
+ <AlignCenter className="h-4 w-4" />
124
+ </Button>
125
+ <Button
126
+ variant="ghost"
127
+ size="icon"
128
+ className={cn(
129
+ 'h-6 w-6',
130
+ 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'
131
+ )}
132
+ onClick={() => setPosition('right')}
133
+ >
134
+ <AlignRight className="h-4 w-4" />
135
+ </Button>
136
+ </div>
137
+ <Button
138
+ variant="ghost"
139
+ size="icon"
140
+ className="h-6 w-6"
141
+ onClick={() => onOpenChange(false)}
142
+ >
143
+ <X className="h-4 w-4" />
144
+ </Button>
145
+ </div>
146
+ </AlertDialogHeader>
147
+
148
+ {/* Status Content */}
149
+ <div className="space-y-4 pt-4">
150
+ {/* Pipeline Status */}
151
+ <div className="flex items-center gap-4">
152
+ <div className="flex items-center gap-2">
153
+ <div className="text-sm font-medium">Busy:</div>
154
+ <div className={`h-2 w-2 rounded-full ${status?.busy ? 'bg-green-500' : 'bg-gray-300'}`} />
155
+ </div>
156
+ <div className="flex items-center gap-2">
157
+ <div className="text-sm font-medium">Request Pending:</div>
158
+ <div className={`h-2 w-2 rounded-full ${status?.request_pending ? 'bg-green-500' : 'bg-gray-300'}`} />
159
+ </div>
160
+ </div>
161
+
162
+ {/* Job Information */}
163
+ <div className="rounded-md border p-3 space-y-2">
164
+ <div>Job Name: {status?.job_name || '-'}</div>
165
+ <div className="flex justify-between">
166
+ <span>Start Time: {status?.job_start ? new Date(status.job_start).toLocaleString() : '-'}</span>
167
+ <span>Progress: {status ? `${status.cur_batch}/${status.batchs}` : '-'}</span>
168
+ </div>
169
+ </div>
170
+
171
+ {/* Latest Message */}
172
+ <div className="space-y-2">
173
+ <div className="text-sm font-medium">Latest Message:</div>
174
+ <div className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3">
175
+ {status?.latest_message || '-'}
176
+ </div>
177
+ </div>
178
+
179
+ {/* History Messages */}
180
+ <div className="space-y-2">
181
+ <div className="text-sm font-medium">History Messages:</div>
182
+ <div
183
+ ref={historyRef}
184
+ onScroll={handleScroll}
185
+ className="font-mono text-sm rounded-md bg-zinc-800 text-zinc-100 p-3 h-[20em] overflow-y-auto"
186
+ >
187
+ {status?.history_messages?.map((msg, idx) => (
188
+ <div key={idx}>{msg}</div>
189
+ )) || '-'}
190
+ </div>
191
+ </div>
192
+ </div>
193
+ </AlertDialogContent>
194
+ </AlertDialog>
195
+ )
196
+ }
lightrag_webui/src/components/ui/AlertDialog.tsx CHANGED
@@ -30,7 +30,6 @@ const AlertDialogContent = React.forwardRef<
30
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
  >(({ className, ...props }, ref) => (
32
  <AlertDialogPortal>
33
- <AlertDialogOverlay />
34
  <AlertDialogPrimitive.Content
35
  ref={ref}
36
  className={cn(
 
30
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
  >(({ className, ...props }, ref) => (
32
  <AlertDialogPortal>
 
33
  <AlertDialogPrimitive.Content
34
  ref={ref}
35
  className={cn(
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -20,8 +20,9 @@ import { errorMessage } from '@/lib/utils'
20
  import { toast } from 'sonner'
21
  import { useBackendState } from '@/stores/state'
22
 
23
- import { RefreshCwIcon } from 'lucide-react'
24
  import { DocStatusResponse } from '@/api/lightrag'
 
25
 
26
  const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
27
  // Check if file_path exists and is a non-empty string
@@ -45,6 +46,7 @@ const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): str
45
  };
46
 
47
  export default function DocumentManager() {
 
48
  const { t } = useTranslation()
49
  const health = useBackendState.use.health()
50
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
@@ -114,18 +116,33 @@ export default function DocumentManager() {
114
  </CardHeader>
115
  <CardContent className="space-y-4">
116
  <div className="flex gap-2">
117
- <Button
118
- variant="outline"
119
- onClick={scanDocuments}
120
- side="bottom"
121
- tooltip={t('documentPanel.documentManager.scanTooltip')}
122
- size="sm"
123
- >
124
- <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
125
- </Button>
 
 
 
 
 
 
 
 
 
 
 
126
  <div className="flex-1" />
127
  <ClearDocumentsDialog />
128
  <UploadDocumentsDialog />
 
 
 
 
129
  </div>
130
 
131
  <Card>
 
20
  import { toast } from 'sonner'
21
  import { useBackendState } from '@/stores/state'
22
 
23
+ import { RefreshCwIcon, ActivityIcon } from 'lucide-react'
24
  import { DocStatusResponse } from '@/api/lightrag'
25
+ import PipelineStatusDialog from '@/components/documents/PipelineStatusDialog'
26
 
27
  const getDisplayFileName = (doc: DocStatusResponse, maxLength: number = 20): string => {
28
  // Check if file_path exists and is a non-empty string
 
46
  };
47
 
48
  export default function DocumentManager() {
49
+ const [showPipelineStatus, setShowPipelineStatus] = useState(false)
50
  const { t } = useTranslation()
51
  const health = useBackendState.use.health()
52
  const [docs, setDocs] = useState<DocsStatusesResponse | null>(null)
 
116
  </CardHeader>
117
  <CardContent className="space-y-4">
118
  <div className="flex gap-2">
119
+ <div className="flex gap-2">
120
+ <Button
121
+ variant="outline"
122
+ onClick={scanDocuments}
123
+ side="bottom"
124
+ tooltip={t('documentPanel.documentManager.scanTooltip')}
125
+ size="sm"
126
+ >
127
+ <RefreshCwIcon /> {t('documentPanel.documentManager.scanButton')}
128
+ </Button>
129
+ <Button
130
+ variant="outline"
131
+ onClick={() => setShowPipelineStatus(true)}
132
+ side="bottom"
133
+ tooltip="View pipeline status"
134
+ size="sm"
135
+ >
136
+ <ActivityIcon /> Pipeline Status
137
+ </Button>
138
+ </div>
139
  <div className="flex-1" />
140
  <ClearDocumentsDialog />
141
  <UploadDocumentsDialog />
142
+ <PipelineStatusDialog
143
+ open={showPipelineStatus}
144
+ onOpenChange={setShowPipelineStatus}
145
+ />
146
  </div>
147
 
148
  <Card>