teamsbot ux fixes

This commit is contained in:
ValueOn AG 2026-05-12 22:39:45 +02:00
parent 0d8e6501d3
commit 230055a4fb
3 changed files with 268 additions and 38 deletions

View file

@ -921,6 +921,68 @@
color: var(--text-primary, #333);
}
.responseText h1,
.responseText h2,
.responseText h3 {
margin: 0.6em 0 0.3em;
font-size: 1em;
font-weight: 600;
}
.responseText p {
margin: 0.3em 0;
}
.responseText ul,
.responseText ol {
margin: 0.3em 0;
padding-left: 1.4em;
}
.responseText code {
background: var(--bg-tertiary, #f0f0f0);
padding: 0.1em 0.3em;
border-radius: 3px;
font-size: 0.85em;
}
.responseText pre {
background: var(--bg-tertiary, #f0f0f0);
padding: 0.6em;
border-radius: 4px;
overflow-x: auto;
margin: 0.4em 0;
}
.responseText pre code {
background: none;
padding: 0;
}
.responseText table {
border-collapse: collapse;
margin: 0.4em 0;
font-size: 0.85em;
}
.responseText th,
.responseText td {
border: 1px solid var(--border-color, #ddd);
padding: 0.3em 0.6em;
}
.responseText th {
background: var(--bg-secondary, #f5f5f5);
font-weight: 600;
}
.responseText blockquote {
border-left: 3px solid var(--border-color, #ddd);
margin: 0.4em 0;
padding: 0.2em 0.8em;
color: var(--text-secondary, #666);
}
.responseReasoning {
margin-top: 0.5rem;
font-size: 0.8rem;
@ -948,9 +1010,18 @@
font-size: 0.9rem;
line-height: 1.6;
color: var(--text-primary, #333);
white-space: pre-wrap;
}
.summaryText p { margin: 0.3em 0; }
.summaryText ul, .summaryText ol { margin: 0.3em 0; padding-left: 1.4em; }
.summaryText h1, .summaryText h2, .summaryText h3 { margin: 0.6em 0 0.3em; font-size: 1em; font-weight: 600; }
.summaryText code { background: var(--bg-tertiary, #f0f0f0); padding: 0.1em 0.3em; border-radius: 3px; font-size: 0.85em; }
.summaryText pre { background: var(--bg-tertiary, #f0f0f0); padding: 0.6em; border-radius: 4px; overflow-x: auto; }
.summaryText pre code { background: none; padding: 0; }
.summaryText table { border-collapse: collapse; margin: 0.4em 0; font-size: 0.85em; }
.summaryText th, .summaryText td { border: 1px solid var(--border-color, #ddd); padding: 0.3em 0.6em; }
.summaryText th { background: var(--bg-secondary, #f5f5f5); font-weight: 600; }
/* ============================================================================
Settings View
============================================================================ */
@ -1354,6 +1425,36 @@
animation: agentPulse 1s ease-in-out infinite;
}
.agentProgressLog {
padding: 0.5rem 0;
max-height: 200px;
overflow-y: auto;
font-size: 0.8rem;
line-height: 1.4;
}
.agentProgressEntry {
display: flex;
gap: 0.5rem;
padding: 0.15rem 0;
border-bottom: 1px solid var(--border-color, #eee);
}
.agentProgressEntry:last-child {
border-bottom: none;
}
.agentProgressTime {
color: var(--text-tertiary, #999);
flex-shrink: 0;
font-size: 0.75rem;
}
.agentProgressText {
color: var(--text-secondary, #666);
word-break: break-word;
}
.statsCards {
display: flex;
gap: 1rem;
@ -1543,16 +1644,49 @@
border-top: 1px solid var(--border-color, #e0e0e0);
}
.sessionRow {
display: flex;
gap: 1rem;
padding: 0.4rem 0;
cursor: pointer;
font-size: 0.9rem;
.sessionTable {
border-collapse: collapse;
font-size: 0.85rem;
}
.sessionRow:hover {
color: var(--primary-color, #4A90D9);
.sessionTable th {
text-align: left;
padding: 0.35rem 0.5rem;
font-weight: 600;
font-size: 0.75rem;
color: var(--text-secondary, #666);
border-bottom: 1px solid var(--border-color, #ddd);
}
.sessionTableRow {
cursor: pointer;
}
.sessionTableRow td {
padding: 0.35rem 0.5rem;
border-bottom: 1px solid var(--border-color, #eee);
}
.sessionTableRow:hover td {
background: rgba(74, 144, 217, 0.05);
}
.sessionDeleteBtn {
background: none;
border: none;
color: #b91c1c;
cursor: pointer;
font-size: 0.85rem;
font-weight: 600;
padding: 0.1rem 0.35rem;
border-radius: 3px;
line-height: 1;
opacity: 0.5;
}
.sessionDeleteBtn:hover {
opacity: 1;
background: rgba(185, 28, 28, 0.1);
}
.sessionStatus {

View file

@ -188,16 +188,34 @@ export const TeamsbotModulesView: React.FC = () => {
}
};
const _formatSessionDate = (startedAt?: string | number) => {
if (startedAt == null) return '-';
if (typeof startedAt === 'number') {
return new Date(startedAt * 1000).toLocaleDateString('de-CH');
}
const ms = Date.parse(String(startedAt));
if (!Number.isNaN(ms)) return new Date(ms).toLocaleDateString('de-CH');
return '-';
const _formatSessionDateTime = (ts?: string | number): string => {
if (ts == null) return '-';
const ms = typeof ts === 'number' ? ts * 1000 : Date.parse(String(ts));
if (Number.isNaN(ms)) return '-';
const d = new Date(ms);
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
const _calcDurationMin = (startedAt?: string | number, endedAt?: string | number): string => {
if (!startedAt) return '-';
const startMs = typeof startedAt === 'number' ? startedAt * 1000 : Date.parse(String(startedAt));
if (Number.isNaN(startMs)) return '-';
const endMs = endedAt
? (typeof endedAt === 'number' ? endedAt * 1000 : Date.parse(String(endedAt)))
: Date.now();
if (Number.isNaN(endMs)) return '-';
const mins = Math.round((endMs - startMs) / 60000);
return `${mins} min`;
};
const _sortedSessions = (sessions: TeamsbotSession[]): TeamsbotSession[] =>
[...sessions].sort((a, b) => {
const ta = a.startedAt ? (typeof a.startedAt === 'number' ? a.startedAt * 1000 : Date.parse(String(a.startedAt))) : 0;
const tb = b.startedAt ? (typeof b.startedAt === 'number' ? b.startedAt * 1000 : Date.parse(String(b.startedAt))) : 0;
return tb - ta;
});
return (
<div className={styles.modulesContainer}>
<div className={styles.modulesHeader}>
@ -248,17 +266,50 @@ export const TeamsbotModulesView: React.FC = () => {
{(moduleSessions[mod.id] || []).length === 0 ? (
<p className={styles.noSessions}>{t('Keine Sitzungen')}</p>
) : (
(moduleSessions[mod.id] || []).map((sess) => (
<div
key={sess.id}
className={styles.sessionRow}
onClick={() => navigate(`/mandates/${mandateId}/teamsbot/${instanceId}/sessions?sessionId=${sess.id}`)}
>
<span>{sess.botName || 'Bot'}</span>
<span className={styles.sessionStatus}>{sess.status}</span>
<span>{_formatSessionDate(sess.startedAt)}</span>
</div>
))
<table className={styles.sessionTable}>
<thead>
<tr>
<th style={{ width: 32 }}></th>
<th>{t('Datum')}</th>
<th>{t('Dauer')}</th>
<th>{t('Status')}</th>
</tr>
</thead>
<tbody>
{_sortedSessions(moduleSessions[mod.id] || []).map((sess) => (
<tr
key={sess.id}
className={styles.sessionTableRow}
onClick={() => navigate(`/mandates/${mandateId}/teamsbot/${instanceId}/sessions?sessionId=${sess.id}`)}
>
<td>
<button
type="button"
className={styles.sessionDeleteBtn}
title={t('Sitzung loeschen')}
onClick={async (e) => {
e.stopPropagation();
try {
await teamsbotApi.deleteSession(instanceId, sess.id);
setModuleSessions((prev) => ({
...prev,
[mod.id]: (prev[mod.id] || []).filter((s) => s.id !== sess.id),
}));
} catch (err) {
console.error('Delete session failed:', err);
}
}}
>
x
</button>
</td>
<td>{_formatSessionDateTime(sess.startedAt)}</td>
<td>{_calcDurationMin(sess.startedAt, sess.endedAt)}</td>
<td><span className={styles.sessionStatus}>{sess.status}</span></td>
</tr>
))}
</tbody>
</table>
)}
</div>
)}

View file

@ -23,6 +23,8 @@ import { useFileContext } from '../../../contexts/FileContext';
import styles from './Teamsbot.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
/**
* TeamsbotSessionView - Live session view with real-time transcript and bot responses.
@ -61,6 +63,8 @@ export const TeamsbotSessionView: React.FC = () => {
const [agentStatus, setAgentStatus] = useState<{ toolName?: string; status?: string; reason?: string } | null>(null);
const agentStatusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [agentProgressLog, setAgentProgressLog] = useState<Array<{ id: number; text: string; ts: string }>>([]);
const agentProgressIdRef = useRef(0);
const [sessionStats, setSessionStats] = useState<any>(null);
const [reconnectTick, setReconnectTick] = useState(0);
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@ -311,6 +315,22 @@ export const TeamsbotSessionView: React.FC = () => {
setAgentStatus({ toolName: data.toolName, status: data.status, reason: data.reason });
if (agentStatusTimerRef.current) clearTimeout(agentStatusTimerRef.current);
agentStatusTimerRef.current = setTimeout(() => setAgentStatus(null), 15000);
} else if (data.status === 'toolCall') {
const label = data.displayLabel || data.toolName || '';
setAgentStatus({ toolName: label, status: 'running' });
if (agentStatusTimerRef.current) clearTimeout(agentStatusTimerRef.current);
agentStatusTimerRef.current = setTimeout(() => setAgentStatus(null), 15000);
agentProgressIdRef.current += 1;
const entry = { id: agentProgressIdRef.current, text: `${label}`, ts: new Date().toLocaleTimeString() };
setAgentProgressLog((prev) => [...prev.slice(-19), entry]);
} else if (data.status === 'toolResult') {
const summary = data.summary ? `: ${data.summary.substring(0, 120)}` : '';
agentProgressIdRef.current += 1;
const entry = { id: agentProgressIdRef.current, text: `${data.toolName || ''}${summary}`, ts: new Date().toLocaleTimeString() };
setAgentProgressLog((prev) => [...prev.slice(-19), entry]);
} else if (data.status === 'completed') {
if (agentStatusTimerRef.current) clearTimeout(agentStatusTimerRef.current);
setAgentStatus(null);
} else {
if (agentStatusTimerRef.current) clearTimeout(agentStatusTimerRef.current);
agentStatusTimerRef.current = setTimeout(() => setAgentStatus(null), 2000);
@ -807,14 +827,13 @@ export const TeamsbotSessionView: React.FC = () => {
</div>
)}
{/* Agent Status Bubble (F-fix-2) */}
{/* Agent Status Bubble + Progress Log */}
{agentStatus && (
<div className={styles.agentStatusBubble}>
<span className={styles.agentStatusDot} />
<span>{t('Agent denkt nach')}{agentStatus.toolName ? `: ${agentStatus.toolName}` : '...'}</span>
</div>
)}
{/* Stats Cards (F-fix-3) */}
{sessionStats && (
<div className={styles.statsCards}>
@ -1093,7 +1112,8 @@ export const TeamsbotSessionView: React.FC = () => {
<div className={styles.directorHistoryText}>{p.text}</div>
{p.responseText && (
<div className={styles.directorHistoryText} style={{ opacity: 0.85 }}>
<em>{t('Antwort')}:</em> {p.responseText}
<em>{t('Antwort')}:</em>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{p.responseText}</ReactMarkdown>
</div>
)}
{p.statusMessage && p.status === 'failed' && (
@ -1145,7 +1165,9 @@ export const TeamsbotSessionView: React.FC = () => {
<span className={styles.responseIntent}>{r.detectedIntent}</span>
<span className={styles.responseTime}>{_formatTime(r.timestamp || '')}</span>
</div>
<div className={styles.responseText}>{r.responseText}</div>
<div className={styles.responseText}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{r.responseText || ''}</ReactMarkdown>
</div>
{r.reasoning && (
<div className={styles.responseReasoning}>
<em>{t('Begründung: {text}', { text: r.reasoning })}</em>
@ -1171,17 +1193,40 @@ export const TeamsbotSessionView: React.FC = () => {
{session.summary && (
<div className={styles.summaryCard}>
<h4 className={styles.panelTitle}>{t('Meeting-Zusammenfassung')}</h4>
<div className={styles.summaryText}>{session.summary}</div>
<div className={styles.summaryText}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{session.summary || ''}</ReactMarkdown>
</div>
</div>
)}
{/* TTS Delivery Debug */}
<div className={styles.summaryCard}>
<h4 className={styles.panelTitle}>{t('TTS-Lieferstatus')}</h4>
{/* Agent Progress Log (collapsed by default) */}
<details className={styles.summaryCard}>
<summary className={styles.panelTitle} style={{ cursor: 'pointer' }}>
{t('Agent-Fortschritt')} ({agentProgressLog.length})
</summary>
{agentProgressLog.length === 0 ? (
<div className={styles.emptyState}>{t('Noch keine Agent-Aktivitaet')}</div>
) : (
<div className={styles.agentProgressLog}>
{agentProgressLog.map((entry) => (
<div key={entry.id} className={styles.agentProgressEntry}>
<span className={styles.agentProgressTime}>{entry.ts}</span>
<span className={styles.agentProgressText}>{entry.text}</span>
</div>
))}
</div>
)}
</details>
{/* TTS Delivery Debug (collapsed by default) */}
<details className={styles.summaryCard}>
<summary className={styles.panelTitle} style={{ cursor: 'pointer' }}>
{t('TTS-Lieferstatus')} ({ttsStatusEvents.length})
</summary>
{ttsStatusEvents.length === 0 ? (
<div className={styles.emptyState}>{t('Noch keine TTS-Events')}</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', padding: '0.5rem 0' }}>
{ttsStatusEvents.slice(-10).reverse().map((ev, idx) => (
<div key={`${ev.timestamp}-${idx}`} className={styles.responseMeta}>
<span>{_formatTime(ev.timestamp)}</span>
@ -1192,7 +1237,7 @@ export const TeamsbotSessionView: React.FC = () => {
))}
</div>
)}
</div>
</details>
{/* Debug Log (SSE/Transcript/Chat) */}
<div style={{ position: 'fixed', bottom: 0, right: 0, zIndex: 9999 }}>