ui-nyla/src/components/RagRunningBadge/RagRunningBadge.tsx
2026-05-16 22:55:48 +02:00

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>
);
};