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 |
-
<
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|