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);
|
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,
|
.transcriptList,
|
||||||
.responseList {
|
.responseList {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import styles from './Teamsbot.module.css';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import { Popup } from '../../../components/UiComponents/Popup';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TeamsbotSessionView - Live session view with real-time transcript and bot responses.
|
* 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 [screenshotsLoading, setScreenshotsLoading] = useState(false);
|
||||||
const [screenshotsLoaded, setScreenshotsLoaded] = useState(false);
|
const [screenshotsLoaded, setScreenshotsLoaded] = useState(false);
|
||||||
const [screenshotsExpanded, setScreenshotsExpanded] = useState(false);
|
const [screenshotsExpanded, setScreenshotsExpanded] = useState(false);
|
||||||
|
const [transcriptPopupOpen, setTranscriptPopupOpen] = useState(false);
|
||||||
|
const [botResponsesPopupOpen, setBotResponsesPopupOpen] = useState(false);
|
||||||
const [ttsStatusEvents, setTtsStatusEvents] = useState<Array<{
|
const [ttsStatusEvents, setTtsStatusEvents] = useState<Array<{
|
||||||
status: string;
|
status: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
|
@ -724,6 +727,64 @@ export const TeamsbotSessionView: React.FC = () => {
|
||||||
return colors[Math.abs(hash) % colors.length];
|
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 (loading) return <div className={styles.loading}>{t('Sitzung laden')}</div>;
|
||||||
if (noSessions) return (
|
if (noSessions) return (
|
||||||
<div className={styles.emptyState || styles.loading} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '1rem', padding: '3rem' }}>
|
<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}>
|
<div className={styles.sessionContent}>
|
||||||
{/* Left: Transcript */}
|
{/* Left: Transcript */}
|
||||||
<div className={styles.transcriptPanel}>
|
<div className={styles.transcriptPanel}>
|
||||||
<h4 className={styles.panelTitle}>
|
<div className={styles.panelTitleBar}>
|
||||||
{t('Transkript ({count} Segmente)', { count: transcripts.length })}
|
<h4 className={styles.panelTitle}>
|
||||||
</h4>
|
{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}>
|
<div className={styles.transcriptList}>
|
||||||
{transcripts.map((seg) => (
|
{_renderTranscriptList(transcriptEndRef)}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Bot Responses */}
|
{/* Right: Bot Responses */}
|
||||||
<div className={styles.responsesPanel}>
|
<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}>
|
<div className={styles.responseList}>
|
||||||
{botResponses.map((r) => (
|
{_renderBotResponsesList()}
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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) */}
|
{/* Summary (for ended sessions) */}
|
||||||
{session.summary && (
|
{session.summary && (
|
||||||
<div className={styles.summaryCard}>
|
<div className={styles.summaryCard}>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue