feat: Bot antowrten im Vollbild
All checks were successful
Deploy Nyla Frontend INT / build-and-deploy (push) Successful in 10m29s
All checks were successful
Deploy Nyla Frontend INT / build-and-deploy (push) Successful in 10m29s
This commit is contained in:
parent
eb35e4b463
commit
c5d7d85dda
2 changed files with 159 additions and 46 deletions
|
|
@ -851,6 +851,52 @@
|
|||
background: var(--surface-alt, #fafafa);
|
||||
}
|
||||
|
||||
.panelTitleBar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color, #e0e0e0);
|
||||
background: var(--surface-alt, #fafafa);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panelTitleBar .panelTitle {
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.panelExpandBtn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border-color, #e0e0e0);
|
||||
border-radius: 4px;
|
||||
background: var(--surface-color, #fff);
|
||||
color: var(--text-secondary, #666);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.panelExpandBtn:hover {
|
||||
background: var(--surface-alt, #f5f5f5);
|
||||
color: var(--primary-color, #4A90D9);
|
||||
border-color: var(--primary-color, #4A90D9);
|
||||
}
|
||||
|
||||
.popupPanelList {
|
||||
max-height: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.transcriptList,
|
||||
.responseList {
|
||||
flex: 1;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import styles from './Teamsbot.module.css';
|
|||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Popup } from '../../../components/UiComponents/Popup';
|
||||
|
||||
/**
|
||||
* TeamsbotSessionView - Live session view with real-time transcript and bot responses.
|
||||
|
|
@ -54,6 +55,8 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
const [screenshotsLoading, setScreenshotsLoading] = useState(false);
|
||||
const [screenshotsLoaded, setScreenshotsLoaded] = useState(false);
|
||||
const [screenshotsExpanded, setScreenshotsExpanded] = useState(false);
|
||||
const [transcriptPopupOpen, setTranscriptPopupOpen] = useState(false);
|
||||
const [botResponsesPopupOpen, setBotResponsesPopupOpen] = useState(false);
|
||||
const [ttsStatusEvents, setTtsStatusEvents] = useState<Array<{
|
||||
status: string;
|
||||
message?: string;
|
||||
|
|
@ -724,6 +727,64 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
return colors[Math.abs(hash) % colors.length];
|
||||
};
|
||||
|
||||
const _renderExpandIcon = () => (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden>
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const _renderTranscriptList = (endRef?: React.RefObject<HTMLDivElement | null>) => (
|
||||
<>
|
||||
{transcripts.map((seg) => (
|
||||
<div key={seg.id} className={styles.transcriptItem}>
|
||||
<span className={styles.transcriptTime}>{_formatTime(seg.timestamp)}</span>
|
||||
<span
|
||||
className={styles.transcriptSpeaker}
|
||||
style={{ color: _getSpeakerColor(seg.speaker || t('Unbekannt')) }}
|
||||
>
|
||||
{seg.speaker || t('Unbekannt')}:
|
||||
</span>
|
||||
<span className={styles.transcriptText}>{seg.text}</span>
|
||||
</div>
|
||||
))}
|
||||
{endRef && <div ref={endRef} />}
|
||||
{transcripts.length === 0 && (
|
||||
<div className={styles.emptyState}>{t('Noch kein Transkript vorhanden')}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const _renderBotResponsesList = () => (
|
||||
<>
|
||||
{botResponses.map((r) => (
|
||||
<div key={r.id} className={styles.responseItem}>
|
||||
<div className={styles.responseHeader}>
|
||||
<span className={styles.responseIntent}>{r.detectedIntent}</span>
|
||||
<span className={styles.responseTime}>{_formatTime(r.timestamp || '')}</span>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
{(r.modelName || r.processingTime != null) && (
|
||||
<div className={styles.responseMeta}>
|
||||
<span>{r.modelName || ''}</span>
|
||||
{r.processingTime != null && <span>{r.processingTime.toFixed(1)}s</span>}
|
||||
{r.priceCHF != null && <span>{r.priceCHF.toFixed(4)} CHF</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{botResponses.length === 0 && (
|
||||
<div className={styles.emptyState}>{t('Noch keine Botantworten')}</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (loading) return <div className={styles.loading}>{t('Sitzung laden')}</div>;
|
||||
if (noSessions) return (
|
||||
<div className={styles.emptyState || styles.loading} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '1rem', padding: '3rem' }}>
|
||||
|
|
@ -1132,63 +1193,69 @@ export const TeamsbotSessionView: React.FC = () => {
|
|||
<div className={styles.sessionContent}>
|
||||
{/* Left: Transcript */}
|
||||
<div className={styles.transcriptPanel}>
|
||||
<h4 className={styles.panelTitle}>
|
||||
{t('Transkript ({count} Segmente)', { count: transcripts.length })}
|
||||
</h4>
|
||||
<div className={styles.panelTitleBar}>
|
||||
<h4 className={styles.panelTitle}>
|
||||
{t('Transkript ({count} Segmente)', { count: transcripts.length })}
|
||||
</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.panelExpandBtn}
|
||||
onClick={() => setTranscriptPopupOpen(true)}
|
||||
title={t('Vollbild')}
|
||||
aria-label={t('Transkript im Vollbild anzeigen')}
|
||||
>
|
||||
{_renderExpandIcon()}
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.transcriptList}>
|
||||
{transcripts.map((seg) => (
|
||||
<div key={seg.id} className={styles.transcriptItem}>
|
||||
<span className={styles.transcriptTime}>{_formatTime(seg.timestamp)}</span>
|
||||
<span
|
||||
className={styles.transcriptSpeaker}
|
||||
style={{ color: _getSpeakerColor(seg.speaker || t('Unbekannt')) }}
|
||||
>
|
||||
{seg.speaker || t('Unbekannt')}:
|
||||
</span>
|
||||
<span className={styles.transcriptText}>{seg.text}</span>
|
||||
</div>
|
||||
))}
|
||||
<div ref={transcriptEndRef} />
|
||||
{transcripts.length === 0 && (
|
||||
<div className={styles.emptyState}>{t('Noch kein Transkript vorhanden')}</div>
|
||||
)}
|
||||
{_renderTranscriptList(transcriptEndRef)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Bot Responses */}
|
||||
<div className={styles.responsesPanel}>
|
||||
<h4 className={styles.panelTitle}>Bot-Antworten ({botResponses.length})</h4>
|
||||
<div className={styles.panelTitleBar}>
|
||||
<h4 className={styles.panelTitle}>Bot-Antworten ({botResponses.length})</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.panelExpandBtn}
|
||||
onClick={() => setBotResponsesPopupOpen(true)}
|
||||
title={t('Vollbild')}
|
||||
aria-label={t('Bot-Antworten im Vollbild anzeigen')}
|
||||
>
|
||||
{_renderExpandIcon()}
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.responseList}>
|
||||
{botResponses.map((r) => (
|
||||
<div key={r.id} className={styles.responseItem}>
|
||||
<div className={styles.responseHeader}>
|
||||
<span className={styles.responseIntent}>{r.detectedIntent}</span>
|
||||
<span className={styles.responseTime}>{_formatTime(r.timestamp || '')}</span>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
{(r.modelName || r.processingTime != null) && (
|
||||
<div className={styles.responseMeta}>
|
||||
<span>{r.modelName || ''}</span>
|
||||
{r.processingTime != null && <span>{r.processingTime.toFixed(1)}s</span>}
|
||||
{r.priceCHF != null && <span>{r.priceCHF.toFixed(4)} CHF</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{botResponses.length === 0 && (
|
||||
<div className={styles.emptyState}>{t('Noch keine Botantworten')}</div>
|
||||
)}
|
||||
{_renderBotResponsesList()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Popup
|
||||
isOpen={transcriptPopupOpen}
|
||||
title={t('Transkript ({count} Segmente)', { count: transcripts.length })}
|
||||
onClose={() => setTranscriptPopupOpen(false)}
|
||||
size="fullscreen"
|
||||
closeOnBackdropClick
|
||||
>
|
||||
<div className={`${styles.transcriptList} ${styles.popupPanelList}`}>
|
||||
{_renderTranscriptList()}
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
<Popup
|
||||
isOpen={botResponsesPopupOpen}
|
||||
title={`Bot-Antworten (${botResponses.length})`}
|
||||
onClose={() => setBotResponsesPopupOpen(false)}
|
||||
size="fullscreen"
|
||||
closeOnBackdropClick
|
||||
>
|
||||
<div className={`${styles.responseList} ${styles.popupPanelList}`}>
|
||||
{_renderBotResponsesList()}
|
||||
</div>
|
||||
</Popup>
|
||||
|
||||
{/* Summary (for ended sessions) */}
|
||||
{session.summary && (
|
||||
<div className={styles.summaryCard}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue