teamsbot ux fixes
This commit is contained in:
parent
0d8e6501d3
commit
230055a4fb
3 changed files with 268 additions and 38 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue