feat: add debug screenshot viewer in session view (SysAdmin only)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
32c071911a
commit
2cf296dee6
2 changed files with 91 additions and 1 deletions
|
|
@ -378,3 +378,25 @@ export function createSessionStream(instanceId: string, sessionId: string): Even
|
||||||
const url = `${baseUrl}/api/teamsbot/${instanceId}/sessions/${sessionId}/stream`;
|
const url = `${baseUrl}/api/teamsbot/${instanceId}/sessions/${sessionId}/stream`;
|
||||||
return new EventSource(url, { withCredentials: true });
|
return new EventSource(url, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Debug Screenshots (SysAdmin only)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
export interface ScreenshotInfo {
|
||||||
|
name: string;
|
||||||
|
step: string;
|
||||||
|
timestamp: number;
|
||||||
|
sizeBytes: number;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listScreenshots(instanceId: string, sessionId: string): Promise<{ screenshots: ScreenshotInfo[] }> {
|
||||||
|
const response = await api.get(`/api/teamsbot/${instanceId}/sessions/${sessionId}/screenshots`);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScreenshotUrl(instanceId: string, filename: string): string {
|
||||||
|
const baseUrl = api.defaults.baseURL || '';
|
||||||
|
return `${baseUrl}/api/teamsbot/${instanceId}/screenshots/${filename}`;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||||
import type { TeamsbotSession, TeamsbotTranscript, TeamsbotBotResponse, TeamsbotSSEEvent } from '../../../api/teamsbotApi';
|
import type { TeamsbotSession, TeamsbotTranscript, TeamsbotBotResponse, TeamsbotSSEEvent, ScreenshotInfo } from '../../../api/teamsbotApi';
|
||||||
|
import { getUserDataCache } from '../../../utils/userCache';
|
||||||
import styles from './Teamsbot.module.css';
|
import styles from './Teamsbot.module.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -14,6 +15,9 @@ export const TeamsbotSessionView: React.FC = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const sessionId = searchParams.get('sessionId') || '';
|
const sessionId = searchParams.get('sessionId') || '';
|
||||||
|
|
||||||
|
const cachedUser = getUserDataCache();
|
||||||
|
const _isSysAdmin = cachedUser?.isSysAdmin === true;
|
||||||
|
|
||||||
const [session, setSession] = useState<TeamsbotSession | null>(null);
|
const [session, setSession] = useState<TeamsbotSession | null>(null);
|
||||||
const [allSessions, setAllSessions] = useState<TeamsbotSession[]>([]);
|
const [allSessions, setAllSessions] = useState<TeamsbotSession[]>([]);
|
||||||
const [transcripts, setTranscripts] = useState<TeamsbotTranscript[]>([]);
|
const [transcripts, setTranscripts] = useState<TeamsbotTranscript[]>([]);
|
||||||
|
|
@ -23,6 +27,10 @@ export const TeamsbotSessionView: React.FC = () => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [isLive, setIsLive] = useState(false);
|
const [isLive, setIsLive] = useState(false);
|
||||||
|
|
||||||
|
const [screenshots, setScreenshots] = useState<ScreenshotInfo[]>([]);
|
||||||
|
const [screenshotsLoading, setScreenshotsLoading] = useState(false);
|
||||||
|
const [screenshotsLoaded, setScreenshotsLoaded] = useState(false);
|
||||||
|
|
||||||
const transcriptEndRef = useRef<HTMLDivElement>(null);
|
const transcriptEndRef = useRef<HTMLDivElement>(null);
|
||||||
const eventSourceRef = useRef<EventSource | null>(null);
|
const eventSourceRef = useRef<EventSource | null>(null);
|
||||||
|
|
||||||
|
|
@ -294,6 +302,66 @@ export const TeamsbotSessionView: React.FC = () => {
|
||||||
<div className={styles.summaryText}>{session.summary}</div>
|
<div className={styles.summaryText}>{session.summary}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Debug Screenshots (SysAdmin only) */}
|
||||||
|
{_isSysAdmin && (
|
||||||
|
<div className={styles.summaryCard}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||||
|
<h4 className={styles.panelTitle} style={{ margin: 0 }}>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 ? 'Laden...' : screenshotsLoaded ? 'Aktualisieren' : 'Screenshots laden'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{screenshotsLoaded && screenshots.length === 0 && (
|
||||||
|
<div className={styles.emptyState}>Keine Screenshots fuer diese Session.</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue