106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
import React, { useEffect, useState, useRef, useCallback } from 'react';
|
|
import { useApiRequest } from '../../hooks/useApi';
|
|
import { useLanguage } from '../../providers/language/LanguageContext';
|
|
import styles from './RagRunningBadge.module.css';
|
|
|
|
interface _RagJob {
|
|
jobId: string;
|
|
connectionId: string;
|
|
connectionLabel?: string;
|
|
jobType: string;
|
|
progress: number | null;
|
|
progressMessage: string;
|
|
}
|
|
|
|
const _POLL_INTERVAL_ACTIVE_MS = 5_000;
|
|
const _POLL_INTERVAL_IDLE_MS = 60_000;
|
|
const _DONE_TOAST_MS = 4_000;
|
|
|
|
export const RagRunningBadge: React.FC = () => {
|
|
const { t } = useLanguage();
|
|
const { request } = useApiRequest();
|
|
const [jobs, setJobs] = useState<_RagJob[]>([]);
|
|
const [justFinished, setJustFinished] = useState(false);
|
|
const [expanded, setExpanded] = useState(false);
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
const previousJobCount = useRef(0);
|
|
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const _fetchJobs = useCallback(async () => {
|
|
try {
|
|
const result = await request({ url: '/api/rag/inventory/jobs', method: 'get' });
|
|
const list = Array.isArray(result) ? (result as _RagJob[]) : [];
|
|
// Detect "all running jobs just completed" → flash a brief success toast
|
|
// so the user gets visible confirmation that the work actually finished
|
|
// instead of the spinner just silently disappearing.
|
|
if (previousJobCount.current > 0 && list.length === 0) {
|
|
setJustFinished(true);
|
|
if (toastTimerRef.current) clearTimeout(toastTimerRef.current);
|
|
toastTimerRef.current = setTimeout(() => setJustFinished(false), _DONE_TOAST_MS);
|
|
}
|
|
previousJobCount.current = list.length;
|
|
setJobs(list);
|
|
} catch {
|
|
setJobs([]);
|
|
}
|
|
}, [request]);
|
|
|
|
useEffect(() => {
|
|
_fetchJobs();
|
|
}, [_fetchJobs]);
|
|
|
|
useEffect(() => {
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
const interval = jobs.length > 0 ? _POLL_INTERVAL_ACTIVE_MS : _POLL_INTERVAL_IDLE_MS;
|
|
timerRef.current = setInterval(_fetchJobs, interval);
|
|
return () => { if (timerRef.current) clearInterval(timerRef.current); };
|
|
}, [_fetchJobs, jobs.length]);
|
|
|
|
useEffect(() => {
|
|
return () => { if (toastTimerRef.current) clearTimeout(toastTimerRef.current); };
|
|
}, []);
|
|
|
|
if (jobs.length === 0 && !justFinished) return null;
|
|
|
|
if (jobs.length === 0 && justFinished) {
|
|
return (
|
|
<div className={styles.badgeContainer}>
|
|
<div className={`${styles.badge} ${styles.badgeDone}`} title={t('Sync abgeschlossen')}>
|
|
<span className={styles.doneIcon}>✓</span>
|
|
<span className={styles.badgeText}>{t('Sync abgeschlossen')}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.badgeContainer}>
|
|
<button
|
|
className={styles.badge}
|
|
onClick={() => setExpanded(prev => !prev)}
|
|
title={t('RAG-Indexierung aktiv')}
|
|
>
|
|
<span className={styles.pulseIcon} />
|
|
<span className={styles.badgeText}>
|
|
{jobs.length} {jobs.length === 1 ? t('Job') : t('Jobs')}
|
|
</span>
|
|
</button>
|
|
|
|
{expanded && (
|
|
<div className={styles.dropdown}>
|
|
<div className={styles.dropdownHeader}>
|
|
{t('Aktive RAG-Jobs')}
|
|
</div>
|
|
{jobs.map(job => (
|
|
<div key={job.jobId} className={styles.jobRow}>
|
|
<span className={styles.jobLabel}>{job.connectionLabel || job.jobType}</span>
|
|
<span className={styles.jobProgress}>
|
|
{job.progressMessage || t('läuft...')}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|