fixed teamsbot issues

This commit is contained in:
ValueOn AG 2026-05-11 23:59:27 +02:00
parent 544f36460a
commit 791d575b7d
3 changed files with 75 additions and 142 deletions

View file

@ -413,15 +413,12 @@
flex-direction: column;
gap: 1rem;
padding: 1rem;
height: 100%;
}
/* ----- Session Layout (UDB Sidebar + Main) ------------------------------- */
.sessionLayout {
display: flex;
flex: 1;
min-height: 0;
gap: 1rem;
}
@ -430,7 +427,6 @@
flex-direction: column;
flex: 1;
min-width: 0;
min-height: 0;
gap: 1rem;
}
@ -820,8 +816,6 @@
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
flex: 1;
min-height: 0;
}
/* Transcript Panel */
@ -833,6 +827,8 @@
display: flex;
flex-direction: column;
overflow: hidden;
height: 50vh;
min-height: 250px;
}
.panelTitle {

View file

@ -6,11 +6,9 @@ import type { TeamsbotSession, MeetingModule } from '../../../api/teamsbotApi';
import styles from './Teamsbot.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
const PAST_LIMIT = 10;
/**
* TeamsBot Dashboard IA: KPIs, Modul-Aggregate, Quick-Actions, kompakte Session-Listen.
* Neues Meeting: Assistent (Wizard). Kein paralleler Vollformular-Start hier.
* TeamsBot Dashboard IA: KPIs, Modul-Aggregate, Quick-Actions.
* Neues Meeting: Assistent (Wizard). Sessions sind via Module erreichbar.
*/
export const TeamsbotDashboardView: React.FC = () => {
const { t } = useLanguage();
@ -20,18 +18,14 @@ export const TeamsbotDashboardView: React.FC = () => {
const [sessions, setSessions] = useState<TeamsbotSession[]>([]);
const [modules, setModules] = useState<MeetingModule[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const sessionsRef = useRef<TeamsbotSession[]>([]);
sessionsRef.current = sessions;
const dashboardEsRef = useRef<EventSource | null>(null);
const dashboardReconnectRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const dashboardReconnectRef = useRef<number | null>(null);
const applyDashboardPayload = useCallback((nextSessions: TeamsbotSession[], nextModules: MeetingModule[]) => {
setSessions(nextSessions);
setModules(nextModules);
sessionsRef.current = nextSessions;
}, []);
useEffect(() => {
@ -48,7 +42,6 @@ export const TeamsbotDashboardView: React.FC = () => {
const connect = () => {
if (cancelled) return;
dashboardEsRef.current?.close();
setLoading(true);
const es = teamsbotApi.createDashboardStream(instanceId);
dashboardEsRef.current = es;
@ -62,7 +55,6 @@ export const TeamsbotDashboardView: React.FC = () => {
if (msg.type === 'dashboardState' && Array.isArray(msg.sessions) && Array.isArray(msg.modules)) {
applyDashboardPayload(msg.sessions, msg.modules);
setError(null);
setLoading(false);
}
} catch {
/* ignore malformed SSE */
@ -73,7 +65,6 @@ export const TeamsbotDashboardView: React.FC = () => {
es.close();
dashboardEsRef.current = null;
if (cancelled) return;
setLoading(false);
clearReconnect();
dashboardReconnectRef.current = window.setTimeout(connect, 2500);
};
@ -92,19 +83,6 @@ export const TeamsbotDashboardView: React.FC = () => {
() => sessions.filter((s) => ['pending', 'joining', 'active'].includes(s.status)),
[sessions],
);
const pastSessions = useMemo(
() =>
sessions
.filter((s) => ['ended', 'error', 'leaving'].includes(s.status))
.sort((a, b) => {
const ta = a.startedAt ? new Date(a.startedAt).getTime() : 0;
const tb = b.startedAt ? new Date(b.startedAt).getTime() : 0;
return tb - ta;
}),
[sessions],
);
const pastVisible = pastSessions.slice(0, PAST_LIMIT);
const moduleTitleById = useMemo(() => {
const m = new Map<string, string>();
modules.forEach((mod) => m.set(mod.id, mod.title));
@ -165,10 +143,8 @@ export const TeamsbotDashboardView: React.FC = () => {
teamsbotApi.listSessions(instanceId, true),
teamsbotApi.listModules(instanceId),
]);
const nextSessions = r.sessions || [];
setSessions(nextSessions);
setSessions(r.sessions || []);
setModules(m || []);
sessionsRef.current = nextSessions;
} catch { /* ignore */ }
}, [instanceId]);
@ -181,15 +157,6 @@ export const TeamsbotDashboardView: React.FC = () => {
}
};
const _handleDeleteSession = async (sid: string) => {
try {
await teamsbotApi.deleteSession(instanceId, sid);
await _refreshLists();
} catch (err: any) {
setError(err.message || t('Fehler beim Löschen'));
}
};
return (
<div className={styles.tbDash}>
<header className={styles.tbDashHero}>
@ -306,52 +273,6 @@ export const TeamsbotDashboardView: React.FC = () => {
</section>
)}
<section className={styles.tbDashSection}>
<div className={styles.tbDashSectionHead}>
<h2 className={styles.tbDashSectionTitle}>
{loading ? t('Laden…') : t('Letzte Sitzungen')}
</h2>
{pastSessions.length > PAST_LIMIT && (
<button
type="button"
className={styles.tbDashLinkBtn}
onClick={() => navigate(`/mandates/${mandateId}/${featureCode}/${instanceId}/modules`)}
>
{t('Alle in Modulen')}
</button>
)}
</div>
{pastVisible.length === 0 && !loading ? (
<p className={styles.emptyState}>{t('Noch keine beendeten Sitzungen')}</p>
) : (
<div className={styles.tbDashSessionList}>
{pastVisible.map((session) => (
<div key={session.id} className={styles.tbDashSessionRow}>
<div className={styles.tbDashSessionMain}>
<span className={styles.sessionBotName}>{session.botName}</span>
<span className={`${styles.statusBadge} ${_getStatusBadgeClass(session.status)}`}>
{_getStatusLabel(session.status)}
</span>
</div>
<div className={styles.tbDashSessionMeta}>
{session.startedAt && (
<span>{new Date(session.startedAt).toLocaleString('de-CH')}</span>
)}
<span>{session.transcriptSegmentCount} {t('Segmente')}</span>
</div>
<div className={styles.tbDashSessionActions}>
<button type="button" className={styles.viewButton} onClick={() => navigate(_sessionPath(session.id))}>
{t('Details')}
</button>
<button type="button" className={styles.deleteButton} onClick={() => _handleDeleteSession(session.id)}>
{t('Löschen')}
</button>
</div>
</div>
))}
</div>
)}
</section>
</div>
);
};

View file

@ -51,6 +51,7 @@ export const TeamsbotSessionView: React.FC = () => {
const [screenshots, setScreenshots] = useState<ScreenshotInfo[]>([]);
const [screenshotsLoading, setScreenshotsLoading] = useState(false);
const [screenshotsLoaded, setScreenshotsLoaded] = useState(false);
const [screenshotsExpanded, setScreenshotsExpanded] = useState(false);
const [ttsStatusEvents, setTtsStatusEvents] = useState<Array<{
status: string;
message?: string;
@ -1193,62 +1194,77 @@ export const TeamsbotSessionView: React.FC = () => {
)}
</div>
{/* Debug Screenshots (SysAdmin only) */}
{/* Debug Screenshots (SysAdmin only, collapsible) */}
{_isSysAdmin && (
<div className={styles.summaryCard}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<h4 className={styles.panelTitle} style={{ margin: 0 }}>{t('Debug-Screenshots')}</h4>
<button
className={styles.viewButton}
onClick={async () => {
setScreenshotsLoading(true);
try {
const result = await teamsbotApi.listScreenshots(instanceId, session.id);
setScreenshots(result.screenshots || []);
setScreenshotsLoaded(true);
} catch (err: any) {
setScreenshots([]);
setScreenshotsLoaded(true);
} finally {
setScreenshotsLoading(false);
}
}}
disabled={screenshotsLoading}
>
{screenshotsLoading ? t('Laden…') : screenshotsLoaded ? t('Aktualisieren') : t('Screenshots laden')}
</button>
</div>
{screenshotsLoaded && screenshots.length === 0 && (
<div className={styles.emptyState}>{t('Keine Screenshots für diese Sitzung')}</div>
)}
{screenshots.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '12px' }}>
{screenshots.map((s) => {
const imgUrl = teamsbotApi.getScreenshotUrl(instanceId, s.name);
return (
<a
key={s.name}
href={imgUrl}
target="_blank"
rel="noopener noreferrer"
style={{ display: 'block', textDecoration: 'none', color: 'inherit', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden', background: '#1a1a2e' }}
>
<img
src={imgUrl}
alt={s.step}
style={{ width: '100%', height: '140px', objectFit: 'cover', display: 'block' }}
loading="lazy"
/>
<div style={{ padding: '8px', fontSize: '12px' }}>
<div style={{ fontWeight: 600, marginBottom: '2px' }}>{s.step}</div>
<div style={{ color: '#888' }}>
{new Date(s.timestamp).toLocaleTimeString('de-CH')} {(s.sizeBytes / 1024).toFixed(0)} KB
</div>
</div>
</a>
);
})}
<div
style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' }}
onClick={() => setScreenshotsExpanded((v) => !v)}
>
<h4 className={styles.panelTitle} style={{ margin: 0 }}>
{screenshotsExpanded ? '\u25BC' : '\u25B6'} {t('Debug-Screenshots')}
{screenshotsLoaded && screenshots.length > 0 && ` (${screenshots.length})`}
</h4>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
{screenshotsExpanded && (
<button
className={styles.viewButton}
onClick={async (e) => {
e.stopPropagation();
setScreenshotsLoading(true);
try {
const result = await teamsbotApi.listScreenshots(instanceId, session.id);
setScreenshots(result.screenshots || []);
setScreenshotsLoaded(true);
} catch (err: any) {
setScreenshots([]);
setScreenshotsLoaded(true);
} finally {
setScreenshotsLoading(false);
}
}}
disabled={screenshotsLoading}
>
{screenshotsLoading ? t('Laden…') : screenshotsLoaded ? t('Aktualisieren') : t('Screenshots laden')}
</button>
)}
</div>
</div>
{screenshotsExpanded && (
<>
{screenshotsLoaded && screenshots.length === 0 && (
<div className={styles.emptyState} style={{ marginTop: '12px' }}>{t('Keine Screenshots für diese Sitzung')}</div>
)}
{screenshots.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: '12px', marginTop: '12px' }}>
{screenshots.map((s) => {
const imgUrl = teamsbotApi.getScreenshotUrl(instanceId, s.name);
return (
<a
key={s.name}
href={imgUrl}
target="_blank"
rel="noopener noreferrer"
style={{ display: 'block', textDecoration: 'none', color: 'inherit', border: '1px solid #333', borderRadius: '8px', overflow: 'hidden', background: '#1a1a2e' }}
>
<img
src={imgUrl}
alt={s.step}
style={{ width: '100%', height: '140px', objectFit: 'cover', display: 'block' }}
loading="lazy"
/>
<div style={{ padding: '8px', fontSize: '12px' }}>
<div style={{ fontWeight: 600, marginBottom: '2px' }}>{s.step}</div>
<div style={{ color: '#888' }}>
{new Date(s.timestamp).toLocaleTimeString('de-CH')} {(s.sizeBytes / 1024).toFixed(0)} KB
</div>
</div>
</a>
);
})}
</div>
)}
</>
)}
</div>
)}