chore(ui): remove dead UI components unreferenced by any page (#30340)

These components are not reachable from any page: WebRTCTester, the agents agent_card/agent_card_grid pair (the agents route renders a Table directly), and view_logs ErrorViewer/RequestResponsePanel (logs uses inline equivalents in columns.tsx). Each was imported only by its own test, so the tests go too. Also drops the now-stale agent_card_grid vi.mock in agents.test.tsx and the WebRTCTester eslint-suppressions entry.
This commit is contained in:
ryan-crabbe-berri 2026-06-13 10:57:24 -07:00 • committed by GitHub
parent ec9353cb69
commit 1fab5c4ca6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 0 additions and 1670 deletions

View file

@ -678,14 +678,6 @@
"count": 1
}
},
"src/components/WebRTCTester.jsx": {
"no-restricted-syntax": {
"count": 2
},
"react/no-unescaped-entities": {
"count": 2
}
},
"src/components/activity_metrics.tsx": {
"no-restricted-imports": {
"count": 1

View file

@ -1,648 +0,0 @@
import { useState, useRef, useEffect, useCallback } from "react";
const STYLES = `
.wrt-wrap {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
background: #0d0d14;
border: 1px solid #1e1e2e;
border-radius: 10px;
overflow: hidden;
margin: 24px 0;
}
.wrt-toggle {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
cursor: pointer;
user-select: none;
background: #0d0d14;
transition: background 0.15s;
}
.wrt-toggle:hover { background: #111120; }
.wrt-toggle-left { display: flex; align-items: center; gap: 10px; }
.wrt-live-dot {
width: 8px; height: 8px; border-radius: 50%;
background: #00ff88;
box-shadow: 0 0 8px #00ff88;
animation: wrt-blink 2s infinite;
}
@keyframes wrt-blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
.wrt-toggle-title { font-size: 12px; font-weight: 600; color: #e2e8f0; letter-spacing: 0.06em; }
.wrt-toggle-sub { font-size: 10px; color: #4a5568; margin-top: 1px; }
.wrt-chevron { font-size: 11px; color: #4a5568; transition: transform 0.2s; }
.wrt-chevron.open { transform: rotate(180deg); }
.wrt-body {
border-top: 1px solid #1e1e2e;
display: grid;
grid-template-columns: 280px 1fr;
height: 460px;
}
.wrt-sidebar {
border-right: 1px solid #1e1e2e;
padding: 14px;
display: flex;
flex-direction: column;
gap: 12px;
overflow-y: auto;
}
.wrt-label {
font-size: 9px;
letter-spacing: 0.15em;
color: #4a5568;
text-transform: uppercase;
margin-bottom: 5px;
}
.wrt-field { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
.wrt-field label { font-size: 10px; color: #4a5568; }
.wrt-field input {
background: #0a0a0f;
border: 1px solid #1e1e2e;
border-radius: 5px;
color: #e2e8f0;
font-family: inherit;
font-size: 11px;
padding: 7px 9px;
outline: none;
width: 100%;
transition: border-color 0.2s;
}
.wrt-field input:focus { border-color: #7c3aed; }
.wrt-divider { height: 1px; background: #1e1e2e; }
.wrt-btn {
display: flex; align-items: center; justify-content: center;
border: none; border-radius: 5px; cursor: pointer;
font-family: inherit; font-size: 11px; font-weight: 600;
padding: 8px; width: 100%;
transition: all 0.15s; letter-spacing: 0.04em;
}
.wrt-btn + .wrt-btn { margin-top: 5px; }
.wrt-btn-primary { background: #00ff88; color: #000; }
.wrt-btn-primary:hover:not(:disabled) { filter: brightness(1.1); }
.wrt-btn-primary:disabled { opacity: 0.35; cursor: not-allowed; }
.wrt-btn-danger { background: transparent; color: #ff4466; border: 1px solid #ff4466; }
.wrt-btn-danger:hover:not(:disabled) { background: rgba(255,68,102,0.08); }
.wrt-btn-danger:disabled { opacity: 0.3; cursor: not-allowed; }
.wrt-btn-ghost { background: #111118; color: #e2e8f0; border: 1px solid #1e1e2e; }
.wrt-btn-ghost:hover { border-color: #7c3aed; }
.wrt-flow { display: flex; align-items: center; padding: 4px 0; gap: 0; }
.wrt-flow-box {
padding: 4px 7px; border-radius: 4px; font-size: 9px;
border: 1px solid #1e1e2e; color: #4a5568;
transition: all 0.3s; white-space: nowrap;
}
.wrt-flow-box.active { border-color: #00ff88; color: #00ff88; box-shadow: 0 0 8px rgba(0,255,136,0.15); }
.wrt-flow-arrow { font-size: 10px; color: #4a5568; padding: 0 4px; transition: color 0.3s; }
.wrt-flow-arrow.active { color: #00ff88; }
.wrt-meta { display: flex; flex-direction: column; gap: 4px; }
.wrt-meta-row { display: flex; justify-content: space-between; font-size: 10px; }
.wrt-meta-row span:first-child { color: #4a5568; }
.wrt-meta-row span:last-child { color: #e2e8f0; }
.wrt-status-pill {
display: flex; align-items: center; gap: 6px;
font-size: 10px; color: #4a5568;
background: #111118; border: 1px solid #1e1e2e;
border-radius: 100px; padding: 3px 10px;
}
.wrt-status-dot {
width: 6px; height: 6px; border-radius: 50%;
background: #4a5568; transition: all 0.3s;
}
.wrt-status-dot.connected { background: #00ff88; box-shadow: 0 0 6px #00ff88; }
.wrt-status-dot.connecting { background: #ffaa00; animation: wrt-blink 1s infinite; }
.wrt-status-dot.error { background: #ff4466; }
.wrt-main { display: flex; flex-direction: column; overflow: hidden; }
.wrt-header {
display: flex; align-items: center; justify-content: space-between;
padding: 8px 14px; border-bottom: 1px solid #1e1e2e; background: #111118;
}
.wrt-header-title { font-size: 10px; color: #4a5568; letter-spacing: 0.08em; }
.wrt-tabs { display: flex; padding: 0 14px; border-bottom: 1px solid #1e1e2e; }
.wrt-tab {
font-size: 9px; letter-spacing: 0.08em; padding: 10px 12px; cursor: pointer;
color: #4a5568; border-bottom: 2px solid transparent; transition: all 0.15s;
user-select: none;
}
.wrt-tab.active { color: #00ff88; border-bottom-color: #00ff88; }
.wrt-tab:hover:not(.active) { color: #e2e8f0; }
.wrt-tab-content { flex: 1; overflow: hidden; display: none; flex-direction: column; }
.wrt-tab-content.active { display: flex; }
.wrt-log {
flex: 1; overflow-y: auto; padding: 8px 12px;
display: flex; flex-direction: column; gap: 2px;
}
.wrt-log::-webkit-scrollbar { width: 3px; }
.wrt-log::-webkit-scrollbar-thumb { background: #1e1e2e; border-radius: 2px; }
.wrt-entry {
display: grid; grid-template-columns: 58px 56px 1fr; gap: 8px;
padding: 3px 7px; border-radius: 3px;
border-left: 2px solid transparent;
font-size: 10px; line-height: 1.5;
animation: wrt-fadein 0.15s ease;
}
@keyframes wrt-fadein { from { opacity:0; transform:translateY(2px); } to { opacity:1; transform:none; } }
.wrt-entry.info { border-left-color: #7c3aed; }
.wrt-entry.info .we-tag { color: #7c3aed; }
.wrt-entry.success { border-left-color: #00ff88; }
.wrt-entry.success .we-tag { color: #00ff88; }
.wrt-entry.error { border-left-color: #ff4466; }
.wrt-entry.error .we-tag { color: #ff4466; }
.wrt-entry.warn { border-left-color: #ffaa00; }
.wrt-entry.warn .we-tag { color: #ffaa00; }
.wrt-entry.step { border-left-color: #60a5fa; }
.wrt-entry.step .we-tag { color: #60a5fa; }
.we-time { color: #4a5568; font-size: 9px; padding-top: 1px; }
.we-tag { font-size: 9px; font-weight: 700; padding-top: 1px; }
.we-msg { color: #e2e8f0; word-break: break-all; white-space: pre-wrap; }
.wrt-empty {
display: flex; flex-direction: column; align-items: center; justify-content: center;
flex: 1; gap: 6px; color: #4a5568; font-size: 11px;
}
.wrt-sdp-pane { flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden; }
.wrt-sdp-box { display: flex; flex-direction: column; border-right: 1px solid #1e1e2e; overflow: hidden; }
.wrt-sdp-box:last-child { border-right: none; }
.wrt-sdp-hdr {
padding: 7px 12px; border-bottom: 1px solid #1e1e2e;
font-size: 9px; color: #4a5568; letter-spacing: 0.08em;
display: flex; align-items: center; gap: 6px;
}
.wrt-sdp-dot { width: 5px; height: 5px; border-radius: 50%; background: #1e1e2e; }
.wrt-sdp-dot.active { background: #00ff88; }
.wrt-sdp-pane textarea {
flex: 1; background: transparent; border: none; color: #e2e8f0;
font-family: inherit; font-size: 10px; padding: 10px 12px;
resize: none; outline: none; line-height: 1.5;
}
.wrt-audio-pane {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 14px;
}
.wrt-viz { display: flex; align-items: center; gap: 2px; height: 44px; }
.wrt-bar { width: 3px; border-radius: 2px; min-height: 2px; background: #00ff88; transition: height 0.05s; }
.wrt-mic-btn {
width: 52px; height: 52px; border-radius: 50%;
background: #111118; border: 1.5px solid #1e1e2e;
font-size: 18px; cursor: pointer;
display: flex; align-items: center; justify-content: center; transition: all 0.2s;
}
.wrt-mic-btn.active { border-color: #00ff88; box-shadow: 0 0 16px rgba(0,255,136,0.2); }
.wrt-audio-status { font-size: 10px; color: #4a5568; text-align: center; }
`;
function useLog() {
const [entries, setEntries] = useState([]);
const add = useCallback((level, tag, msg) => {
const time = new Date().toTimeString().slice(0, 8);
setEntries((prev) => [...prev, { level, tag, msg, time, id: Date.now() + Math.random() }]);
}, []);
const clear = useCallback(() => setEntries([]), []);
return { entries, add, clear };
}
export default function WebRTCTester() {
const [open, setOpen] = useState(false);
const [activeTab, setActiveTab] = useState("logs");
const [proxyUrl, setProxyUrl] = useState("http://localhost:4000");
const [apiKey, setApiKey] = useState("sk-1234");
const [model, setModel] = useState("gpt-4o-realtime-preview");
const [status, setStatus] = useState("idle");
const [flowStep, setFlowStep] = useState(0);
const [tokenPreview, setTokenPreview] = useState("—");
const [iceState, setIceState] = useState("—");
const [connState, setConnState] = useState("—");
const [dcState, setDcState] = useState("—");
const [sdpOffer, setSdpOffer] = useState("");
const [sdpAnswer, setSdpAnswer] = useState("");
const [offerActive, setOfferActive] = useState(false);
const [answerActive, setAnswerActive] = useState(false);
const [audioStatus, setAudioStatus] = useState("Start a session first");
const [micActive, setMicActive] = useState(false);
const [bars, setBars] = useState(Array(28).fill(2));
const [connected, setConnected] = useState(false);
const { entries, add: log, clear: clearLogs } = useLog();
const logRef = useRef(null);
const pcRef = useRef(null);
const dcRef = useRef(null);
const streamRef = useRef(null);
const audioCtxRef = useRef(null);
const analyserRef = useRef(null);
const animRef = useRef(null);
const tokenRef = useRef(null);
const micRef = useRef(false);
const remoteAudioRef = useRef(null);
useEffect(() => {
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
}, [entries]);
function drawBars() {
animRef.current = requestAnimationFrame(drawBars);
if (!analyserRef.current) return;
const data = new Uint8Array(analyserRef.current.frequencyBinCount);
analyserRef.current.getByteFrequencyData(data);
setBars(Array.from({ length: 28 }, (_, i) => Math.max(2, ((data[i] || 0) / 255) * 42)));
}
function setupAnalyser(stream) {
audioCtxRef.current = new AudioContext();
const src = audioCtxRef.current.createMediaStreamSource(stream);
analyserRef.current = audioCtxRef.current.createAnalyser();
analyserRef.current.fftSize = 64;
src.connect(analyserRef.current);
drawBars();
}
async function startSession() {
const url = proxyUrl.trim().replace(/\/$/, "");
const key = apiKey.trim();
const mdl = model.trim();
setConnected(true);
setStatus("connecting");
setFlowStep(1);
// Step 1: ephemeral token
log("step", "STEP 1", `POST ${url}/v1/realtime/client_secrets`);
let tokenResp;
try {
const r = await fetch(`${url}/v1/realtime/client_secrets`, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
body: JSON.stringify({ model: mdl }),
});
log("info", "HTTP", `${r.status} ${r.statusText}`);
const raw = await r.text();
if (!r.ok) {
log("error", "ERR", raw);
stopSession();
return;
}
tokenResp = JSON.parse(raw);
log("success", "TOKEN", "Received encrypted ephemeral token");
} catch (e) {
log("error", "ERR", `client_secrets failed: ${e.message}`);
stopSession();
return;
}
const token = tokenResp?.client_secret?.value ?? tokenResp?.value;
if (!token) {
log("error", "ERR", `Cannot extract token: ${JSON.stringify(tokenResp)}`);
stopSession();
return;
}
tokenRef.current = token;
setTokenPreview(token.slice(0, 10) + "…");
log("info", "TOKEN", `Preview: ${token.slice(0, 10)}…`);
// Step 2: PeerConnection
log("step", "STEP 2", "Creating RTCPeerConnection");
const pc = new RTCPeerConnection();
pcRef.current = pc;
pc.oniceconnectionstatechange = () => {
setIceState(pc.iceConnectionState);
log("info", "ICE", pc.iceConnectionState);
if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") {
setStatus("connected");
setFlowStep(3);
}
if (pc.iceConnectionState === "failed" || pc.iceConnectionState === "disconnected") {
setStatus("error");
}
};
pc.onconnectionstatechange = () => {
setConnState(pc.connectionState);
log("info", "CONN", pc.connectionState);
};
pc.ontrack = (e) => {
log("success", "AUDIO", "Remote audio track received from OpenAI");
if (remoteAudioRef.current) remoteAudioRef.current.srcObject = e.streams[0];
setupAnalyser(e.streams[0]);
setAudioStatus("Receiving audio from OpenAI ✓");
};
const dc = pc.createDataChannel("oai-events");
dcRef.current = dc;
dc.onopen = () => {
setDcState("open");
log("success", "DC", "Data channel open — ready!");
setStatus("connected");
};
dc.onclose = () => {
setDcState("closed");
log("warn", "DC", "Closed");
};
dc.onmessage = (e) => {
try {
log("info", "EVENT", JSON.parse(e.data).type ?? "unknown");
} catch {
log("info", "EVENT", e.data.slice(0, 100));
}
};
// Mic
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
streamRef.current = stream;
stream.getTracks().forEach((t) => pc.addTrack(t, stream));
log("success", "MIC", "Microphone access granted");
setAudioStatus("Mic active — waiting for remote audio");
micRef.current = true;
setMicActive(true);
} catch (e) {
log("warn", "MIC", `Mic denied: ${e.message}`);
const ctx = new AudioContext();
const dest = ctx.createMediaStreamDestination();
dest.stream.getTracks().forEach((t) => pc.addTrack(t, dest.stream));
}
// Step 3: SDP offer
log("step", "STEP 3", "Creating SDP offer");
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
setSdpOffer(offer.sdp);
setOfferActive(true);
log("info", "SDP", `Offer created (${offer.sdp.split("\n").length} lines)`);
// Step 4: SDP exchange
setFlowStep(2);
log("step", "STEP 4", `POST ${url}/v1/realtime/calls`);
try {
const r = await fetch(`${url}/v1/realtime/calls`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/sdp" },
body: offer.sdp,
});
log("info", "HTTP", `${r.status} ${r.statusText}`);
if (!r.ok) {
log("error", "ERR", await r.text());
stopSession();
return;
}
const ans = await r.text();
log("success", "SDP", `Answer received (${ans.split("\n").length} lines)`);
// Step 5: remote description
log("step", "STEP 5", "Setting remote description");
await pc.setRemoteDescription({ type: "answer", sdp: ans });
setSdpAnswer(ans);
setAnswerActive(true);
log("success", "CONN", "✓ Session established — Browser ↔ LiteLLM ↔ OpenAI");
} catch (e) {
log("error", "ERR", `calls failed: ${e.message}`);
stopSession();
}
}
function stopSession() {
if (pcRef.current) {
pcRef.current.close();
pcRef.current = null;
}
if (streamRef.current) {
streamRef.current.getTracks().forEach((t) => t.stop());
streamRef.current = null;
}
if (animRef.current) {
cancelAnimationFrame(animRef.current);
animRef.current = null;
}
tokenRef.current = null;
micRef.current = false;
setConnected(false);
setStatus("idle");
setFlowStep(0);
setTokenPreview("—");
setIceState("—");
setConnState("—");
setDcState("—");
setMicActive(false);
setOfferActive(false);
setAnswerActive(false);
setBars(Array(28).fill(2));
setAudioStatus("Start a session first");
log("warn", "SESSION", "Session stopped");
}
function toggleMic() {
if (!streamRef.current) {
log("warn", "MIC", "No active session");
return;
}
const next = !micRef.current;
micRef.current = next;
streamRef.current.getAudioTracks().forEach((t) => {
t.enabled = next;
});
setMicActive(next);
log("info", "MIC", next ? "Unmuted" : "Muted");
}
const f = (n) => flowStep >= n;
return (
<>
<style>{STYLES}</style>
<div className="wrt-wrap">
{/* Toggle header */}
<div className={`wrt-toggle${open ? "" : " closed"}`} onClick={() => setOpen((o) => !o)}>
<div className="wrt-toggle-left">
<div className="wrt-live-dot" />
<div>
<div className="wrt-toggle-title">INTERACTIVE TESTER</div>
<div className="wrt-toggle-sub">Browser → LiteLLM → OpenAI · WebRTC</div>
</div>
</div>
<span className={`wrt-chevron${open ? " open" : ""}`}>▼</span>
</div>
{open && (
<div className="wrt-body">
{/* Sidebar */}
<div className="wrt-sidebar">
<div>
<div className="wrt-label">Proxy Config</div>
<div className="wrt-field">
<label>Proxy URL</label>
<input
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
placeholder="http://localhost:4000"
/>
</div>
<div className="wrt-field">
<label>API Key</label>
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-1234"
/>
</div>
<div className="wrt-field">
<label>Model</label>
<input value={model} onChange={(e) => setModel(e.target.value)} />
</div>
</div>
<div className="wrt-divider" />
<div>
<div className="wrt-label">Flow</div>
<div className="wrt-flow">
<div className={`wrt-flow-box${f(1) ? " active" : ""}`}>Browser</div>
<div className={`wrt-flow-arrow${f(1) ? " active" : ""}`}>→</div>
<div className={`wrt-flow-box${f(1) ? " active" : ""}`}>LiteLLM</div>
<div className={`wrt-flow-arrow${f(2) ? " active" : ""}`}>→</div>
<div className={`wrt-flow-box${f(2) ? " active" : ""}`}>OpenAI</div>
</div>
</div>
<div className="wrt-divider" />
<div>
<div className="wrt-label">Controls</div>
<button className="wrt-btn wrt-btn-primary" onClick={startSession} disabled={connected}>
▶ Start Session
</button>
<button className="wrt-btn wrt-btn-danger" onClick={stopSession} disabled={!connected}>
■ Stop
</button>
<button className="wrt-btn wrt-btn-ghost" onClick={clearLogs} style={{ marginTop: 5 }}>
✕ Clear Logs
</button>
</div>
<div className="wrt-divider" />
<div>
<div className="wrt-label">Session Info</div>
<div className="wrt-meta">
{[
["token", tokenPreview],
["ice", iceState],
["conn", connState],
["data ch.", dcState],
].map(([k, v]) => (
<div className="wrt-meta-row" key={k}>
<span>{k}</span>
<span>{v}</span>
</div>
))}
</div>
</div>
</div>
{/* Right panel */}
<div className="wrt-main">
<div className="wrt-header">
<span className="wrt-header-title">WEBRTC REALTIME TESTER</span>
<div className="wrt-status-pill">
<div className={`wrt-status-dot${status !== "idle" ? ` ${status}` : ""}`} />
<span style={{ fontSize: 10, color: "#4a5568" }}>{status}</span>
</div>
</div>
<div className="wrt-tabs">
{["logs", "sdp", "audio"].map((t) => (
<div key={t} className={`wrt-tab${activeTab === t ? " active" : ""}`} onClick={() => setActiveTab(t)}>
{t.toUpperCase()}
</div>
))}
</div>
{/* Logs */}
<div className={`wrt-tab-content${activeTab === "logs" ? " active" : ""}`}>
<div className="wrt-log" ref={logRef}>
{entries.length === 0 ? (
<div className="wrt-empty">
<div style={{ fontSize: 22, opacity: 0.3 }}>📡</div>
<div>Hit "Start Session" to begin</div>
</div>
) : (
entries.map((e) => (
<div key={e.id} className={`wrt-entry ${e.level}`}>
<span className="we-time">{e.time}</span>
<span className="we-tag">[{e.tag}]</span>
<span className="we-msg">{e.msg}</span>
</div>
))
)}
</div>
</div>
{/* SDP */}
<div className={`wrt-tab-content${activeTab === "sdp" ? " active" : ""}`}>
<div className="wrt-sdp-pane">
<div className="wrt-sdp-box">
<div className="wrt-sdp-hdr">
<div className={`wrt-sdp-dot${offerActive ? " active" : ""}`} />
SDP OFFER
</div>
<textarea readOnly value={sdpOffer} placeholder="SDP offer appears here..." />
</div>
<div className="wrt-sdp-box">
<div className="wrt-sdp-hdr">
<div className={`wrt-sdp-dot${answerActive ? " active" : ""}`} />
SDP ANSWER
</div>
<textarea readOnly value={sdpAnswer} placeholder="SDP answer appears here..." />
</div>
</div>
</div>
{/* Audio */}
<div className={`wrt-tab-content${activeTab === "audio" ? " active" : ""}`}>
<div className="wrt-audio-pane">
<div className="wrt-viz">
{bars.map((h, i) => (
<div
key={i}
className="wrt-bar"
style={{ height: h + "px", background: `hsl(${150 - (h / 42) * 30},100%,55%)` }}
/>
))}
</div>
<button className={`wrt-mic-btn${micActive ? " active" : ""}`} onClick={toggleMic}>
🎙️
</button>
<div className="wrt-audio-status">{audioStatus}</div>
<audio ref={remoteAudioRef} autoPlay style={{ display: "none" }} />
</div>
</div>
</div>
</div>
)}
</div>
</>
);
}

View file

@ -14,12 +14,6 @@ vi.mock("./agents/add_agent_form", () => ({
default: () => <div data-testid="add-agent-form" />,
}));
vi.mock("./agents/agent_card_grid", () => ({
default: ({ isAdmin }: { isAdmin: boolean }) => <div data-testid="agent-card-grid" data-is-admin={String(isAdmin)} />,
}));
// Note: agents.tsx no longer uses AgentCardGrid — it renders a Table directly.
vi.mock("./agents/agent_info", () => ({
default: () => <div data-testid="agent-info" />,
}));

View file

@ -1,95 +0,0 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../../tests/test-utils";
import AgentCard from "./agent_card";
import type { Agent } from "./types";
const baseAgent: Agent = {
agent_id: "agent-123",
agent_name: "Test Agent",
litellm_params: { model: "gpt-4" },
agent_card_params: {
description: "A test agent for unit testing",
url: "https://agent.example.com",
},
};
const defaultProps = {
agent: baseAgent,
onAgentClick: vi.fn(),
accessToken: "token-123",
isAdmin: false,
onAgentUpdated: vi.fn(),
};
describe("AgentCard", () => {
it("should render the agent name and description", () => {
renderWithProviders(<AgentCard {...defaultProps} />);
expect(screen.getByText("Test Agent")).toBeInTheDocument();
expect(screen.getByText("A test agent for unit testing")).toBeInTheDocument();
});
it("should show 'No description' when agent has no description", () => {
const agent = { ...baseAgent, agent_card_params: {} };
renderWithProviders(<AgentCard {...defaultProps} agent={agent} />);
expect(screen.getByText("No description")).toBeInTheDocument();
});
it("should show the agent URL when provided", () => {
renderWithProviders(<AgentCard {...defaultProps} />);
expect(screen.getByText("https://agent.example.com")).toBeInTheDocument();
});
it("should show 'Needs Setup' badge when agent has no key", () => {
renderWithProviders(<AgentCard {...defaultProps} />);
expect(screen.getByText("Needs Setup")).toBeInTheDocument();
expect(screen.getByText("No key assigned")).toBeInTheDocument();
});
it("should show 'Active' badge and key info when agent has a key", () => {
const keyInfo = { has_key: true, key_alias: "my-key" };
renderWithProviders(<AgentCard {...defaultProps} keyInfo={keyInfo} />);
expect(screen.getByText("Active")).toBeInTheDocument();
expect(screen.getByText("my-key")).toBeInTheDocument();
});
it("should call onAgentClick when card is clicked", async () => {
const user = userEvent.setup();
const onAgentClick = vi.fn();
renderWithProviders(<AgentCard {...defaultProps} onAgentClick={onAgentClick} />);
await user.click(screen.getByText("Test Agent"));
expect(onAgentClick).toHaveBeenCalledWith("agent-123");
});
it("should show delete button only for admins", () => {
const onDeleteClick = vi.fn();
const { unmount } = renderWithProviders(
<AgentCard {...defaultProps} isAdmin={false} onDeleteClick={onDeleteClick} />,
);
expect(screen.queryByRole("button", { name: /delete/i })).not.toBeInTheDocument();
unmount();
renderWithProviders(<AgentCard {...defaultProps} isAdmin={true} onDeleteClick={onDeleteClick} />);
expect(screen.getByRole("button", { name: /delete/i })).toBeInTheDocument();
});
it("should call onDeleteClick with agent id and name when delete is clicked", async () => {
const user = userEvent.setup();
const onDeleteClick = vi.fn();
renderWithProviders(<AgentCard {...defaultProps} isAdmin={true} onDeleteClick={onDeleteClick} />);
await user.click(screen.getByRole("button", { name: /delete/i }));
expect(onDeleteClick).toHaveBeenCalledWith("agent-123", "Test Agent");
});
});

View file

@ -1,88 +0,0 @@
import React from "react";
import { Card, Badge, Tooltip, Button } from "antd";
import { CopyOutlined, KeyOutlined, WarningOutlined, DeleteOutlined } from "@ant-design/icons";
import { Agent, AgentKeyInfo } from "./types";
interface AgentCardProps {
agent: Agent;
keyInfo?: AgentKeyInfo;
onAgentClick: (agentId: string) => void;
onDeleteClick?: (agentId: string, agentName: string) => void;
accessToken: string | null;
isAdmin: boolean;
onAgentUpdated: () => void;
}
const AgentCard: React.FC<AgentCardProps> = ({ agent, keyInfo, onAgentClick, onDeleteClick, isAdmin }) => {
const description = agent.agent_card_params?.description || "No description";
const url = agent.agent_card_params?.url;
const hasKey = keyInfo?.has_key ?? false;
const statusBadge = hasKey ? <Badge status="success" text="Active" /> : <Badge status="warning" text="Needs Setup" />;
const copyToClipboard = (e: React.MouseEvent, text: string) => {
e.stopPropagation();
navigator.clipboard.writeText(text);
};
return (
<Card
hoverable
className="h-full flex flex-col"
styles={{
body: { flex: 1, display: "flex", flexDirection: "column" },
}}
onClick={() => onAgentClick(agent.agent_id)}
>
<div className="flex items-start justify-between gap-2 mb-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-gray-900 truncate">{agent.agent_name}</span>
<Tooltip title="Copy Agent ID">
<CopyOutlined
onClick={(e) => copyToClipboard(e, agent.agent_id)}
className="cursor-pointer text-gray-400 hover:text-blue-500 text-xs shrink-0"
/>
</Tooltip>
</div>
<div className="mt-1">{statusBadge}</div>
</div>
{isAdmin && onDeleteClick && (
<Tooltip title="Delete agent">
<Button
type="text"
size="small"
danger
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
onDeleteClick(agent.agent_id, agent.agent_name);
}}
className="shrink-0 -mr-1"
/>
</Tooltip>
)}
</div>
<p className="text-sm text-gray-600 line-clamp-2 flex-1 mb-3">{description}</p>
{url && (
<p className="text-xs text-gray-500 truncate mb-2" title={url}>
{url}
</p>
)}
<div className="mt-auto pt-3 border-t border-gray-100 text-xs">
{hasKey ? (
<div className="flex items-center gap-1.5 text-gray-600">
<KeyOutlined />
<span>{keyInfo?.key_alias || keyInfo?.token_prefix || "Key assigned"}</span>
</div>
) : (
<div className="flex items-center gap-1.5 text-amber-600">
<WarningOutlined />
<span>No key assigned</span>
</div>
)}
</div>
</Card>
);
};
export default AgentCard;

View file

@ -1,80 +0,0 @@
import { renderWithProviders, screen } from "../../../tests/test-utils";
import { vi } from "vitest";
import AgentCardGrid from "./agent_card_grid";
import type { Agent, AgentKeyInfo } from "./types";
vi.mock("./agent_card", () => ({
default: ({ agent, onAgentClick }: any) => (
<div data-testid={`agent-card-${agent.agent_id}`} onClick={() => onAgentClick(agent.agent_id)}>
{agent.agent_name}
</div>
),
}));
const mockAgents: Agent[] = [
{
agent_id: "agent-1",
agent_name: "Test Agent 1",
litellm_params: { model: "gpt-4" },
agent_card_params: { description: "First agent" },
},
{
agent_id: "agent-2",
agent_name: "Test Agent 2",
litellm_params: { model: "claude-3" },
agent_card_params: { description: "Second agent" },
},
];
const mockKeyInfoMap: Record<string, AgentKeyInfo> = {
"agent-1": { has_key: true, key_alias: "key-1" },
"agent-2": { has_key: false },
};
const defaultProps = {
agentsList: mockAgents,
keyInfoMap: mockKeyInfoMap,
isLoading: false,
onDeleteClick: vi.fn(),
accessToken: "test-token",
onAgentUpdated: vi.fn(),
isAdmin: true,
onAgentClick: vi.fn(),
};
describe("AgentCardGrid", () => {
it("should render", () => {
renderWithProviders(<AgentCardGrid {...defaultProps} />);
expect(screen.getByText("Test Agent 1")).toBeInTheDocument();
});
it("should render all agent cards", () => {
renderWithProviders(<AgentCardGrid {...defaultProps} />);
expect(screen.getByText("Test Agent 1")).toBeInTheDocument();
expect(screen.getByText("Test Agent 2")).toBeInTheDocument();
});
it("should show loading skeletons when isLoading is true", () => {
renderWithProviders(<AgentCardGrid {...defaultProps} isLoading={true} />);
expect(screen.queryByText("Test Agent 1")).not.toBeInTheDocument();
});
it("should show admin empty state message when no agents and isAdmin", () => {
renderWithProviders(<AgentCardGrid {...defaultProps} agentsList={[]} isAdmin={true} />);
expect(screen.getByText("No agents found. Create one to get started.")).toBeInTheDocument();
});
it("should show non-admin empty state message when no agents and not admin", () => {
renderWithProviders(<AgentCardGrid {...defaultProps} agentsList={[]} isAdmin={false} />);
expect(screen.getByText("No agents found. Contact an admin to create agents.")).toBeInTheDocument();
});
it("should call onAgentClick when a card is clicked", async () => {
const onAgentClick = vi.fn();
renderWithProviders(<AgentCardGrid {...defaultProps} onAgentClick={onAgentClick} />);
const { default: userEvent } = await import("@testing-library/user-event");
const user = userEvent.setup();
await user.click(screen.getByTestId("agent-card-agent-1"));
expect(onAgentClick).toHaveBeenCalledWith("agent-1");
});
});

View file

@ -1,67 +0,0 @@
import React from "react";
import { Skeleton } from "antd";
import AgentCard from "./agent_card";
import { Agent, AgentKeyInfo } from "./types";
interface AgentCardGridProps {
agentsList: Agent[];
keyInfoMap: Record<string, AgentKeyInfo>;
isLoading: boolean;
onDeleteClick: (agentId: string, agentName: string) => void;
accessToken: string | null;
onAgentUpdated: () => void;
isAdmin: boolean;
onAgentClick: (agentId: string) => void;
}
const AgentCardGrid: React.FC<AgentCardGridProps> = ({
agentsList,
keyInfoMap,
isLoading,
onDeleteClick,
accessToken,
onAgentUpdated,
isAdmin,
onAgentClick,
}) => {
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3].map((i) => (
<Skeleton key={i} active paragraph={{ rows: 3 }} />
))}
</div>
);
}
if (!agentsList || agentsList.length === 0) {
return (
<div className="rounded-lg border border-gray-200 bg-gray-50/50 py-12 text-center">
<p className="text-gray-500">
{isAdmin
? "No agents found. Create one to get started."
: "No agents found. Contact an admin to create agents."}
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{agentsList.map((agent) => (
<AgentCard
key={agent.agent_id}
agent={agent}
keyInfo={keyInfoMap[agent.agent_id]}
onAgentClick={onAgentClick}
onDeleteClick={isAdmin ? onDeleteClick : undefined}
accessToken={accessToken}
isAdmin={isAdmin}
onAgentUpdated={onAgentUpdated}
/>
))}
</div>
);
};
export default AgentCardGrid;

View file

@ -1,85 +0,0 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { ErrorViewer } from "./ErrorViewer";
const basicError = {
error_class: "NotFoundError",
error_message: "Model gpt-5 not found",
};
const errorWithTraceback = {
error_class: "AuthenticationError",
error_message: "Invalid API key",
traceback: `Traceback (most recent call last):
File "/app/main.py", line 42, in handle_request
result = await client.chat(model="gpt-4")
File "/app/llms/openai.py", line 100, in chat
response = self._make_request(payload)
File "/app/llms/base.py", line 55, in _make_request
raise AuthenticationError("Invalid API key")`,
};
describe("ErrorViewer", () => {
it("should render error type and message", () => {
render(<ErrorViewer errorInfo={basicError} />);
expect(screen.getByText("NotFoundError")).toBeInTheDocument();
expect(screen.getByText("Model gpt-5 not found")).toBeInTheDocument();
});
it("should show 'Unknown Error' when error_class is missing", () => {
render(<ErrorViewer errorInfo={{ error_message: "something broke" }} />);
expect(screen.getByText("Unknown Error")).toBeInTheDocument();
});
it("should show 'Unknown error occurred' when error_message is missing", () => {
render(<ErrorViewer errorInfo={{ error_class: "RuntimeError" }} />);
expect(screen.getByText("Unknown error occurred")).toBeInTheDocument();
});
it("should render traceback frames when traceback is present", () => {
render(<ErrorViewer errorInfo={errorWithTraceback} />);
expect(screen.getByText("Traceback")).toBeInTheDocument();
expect(screen.getByText("main.py")).toBeInTheDocument();
expect(screen.getByText("openai.py")).toBeInTheDocument();
expect(screen.getByText("base.py")).toBeInTheDocument();
});
it("should expand a frame when clicked", async () => {
const user = userEvent.setup();
render(<ErrorViewer errorInfo={errorWithTraceback} />);
await user.click(screen.getByText("main.py"));
expect(screen.getByText('result = await client.chat(model="gpt-4")')).toBeInTheDocument();
});
it("should expand all frames when 'Expand All' is clicked", async () => {
const user = userEvent.setup();
render(<ErrorViewer errorInfo={errorWithTraceback} />);
await user.click(screen.getByText("Expand All"));
expect(screen.getByText("Collapse All")).toBeInTheDocument();
});
it("should copy traceback to clipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const mockWriteText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: { writeText: mockWriteText },
writable: true,
configurable: true,
});
render(<ErrorViewer errorInfo={errorWithTraceback} />);
await user.click(screen.getByTitle("Copy traceback"));
expect(mockWriteText).toHaveBeenCalledWith(errorWithTraceback.traceback);
});
it("should not render traceback section when traceback is absent", () => {
render(<ErrorViewer errorInfo={basicError} />);
expect(screen.queryByText("Traceback")).not.toBeInTheDocument();
});
});

View file

@ -1,180 +0,0 @@
import React from "react";
interface ErrorViewerProps {
errorInfo: {
error_class?: string;
error_message?: string;
traceback?: string;
llm_provider?: string;
error_code?: string | number;
};
}
export const ErrorViewer: React.FC<ErrorViewerProps> = ({ errorInfo }) => {
const [expandedFrames, setExpandedFrames] = React.useState<{ [key: number]: boolean }>({});
const [allExpanded, setAllExpanded] = React.useState(false);
// Toggle individual frame
const toggleFrame = (index: number) => {
setExpandedFrames((prev) => ({
...prev,
[index]: !prev[index],
}));
};
// Toggle all frames
const toggleAllFrames = () => {
const newState = !allExpanded;
setAllExpanded(newState);
if (tracebackFrames.length > 0) {
const newExpandedState: { [key: number]: boolean } = {};
tracebackFrames.forEach((_, idx) => {
newExpandedState[idx] = newState;
});
setExpandedFrames(newExpandedState);
}
};
// Parse traceback into frames
const parseTraceback = (traceback: string) => {
if (!traceback) return [];
// Extract file paths, line numbers and code from traceback
const fileLineRegex = /File "([^"]+)", line (\d+)/g;
const matches = Array.from(traceback.matchAll(fileLineRegex));
// Create simplified frames
return matches.map((match) => {
const filePath = match[1];
const lineNumber = match[2];
const fileName = filePath.split("/").pop() || filePath;
// Extract the context around this frame
const matchIndex = match.index || 0;
const nextMatchIndex = traceback.indexOf('File "', matchIndex + 1);
const frameContent =
nextMatchIndex > -1
? traceback.substring(matchIndex, nextMatchIndex).trim()
: traceback.substring(matchIndex).trim();
// Try to extract the code line
const lines = frameContent.split("\n");
let code = "";
if (lines.length > 1) {
code = lines[lines.length - 1].trim();
}
return {
filePath,
fileName,
lineNumber,
code,
inFunction: frameContent.includes(" in ") ? frameContent.split(" in ")[1].split("\n")[0] : "",
};
});
};
const tracebackFrames = errorInfo.traceback ? parseTraceback(errorInfo.traceback) : [];
return (
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b">
<h3 className="text-lg font-medium flex items-center text-red-600">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
Error Details
</h3>
</div>
<div className="p-4">
<div className="bg-red-50 rounded-md p-4 mb-4">
<div className="flex">
<span className="text-red-800 font-medium w-20">Type:</span>
<span className="text-red-700">{errorInfo.error_class || "Unknown Error"}</span>
</div>
<div className="flex mt-2">
<span className="text-red-800 font-medium w-20 flex-shrink-0">Message:</span>
<span className="text-red-700 break-words whitespace-pre-wrap">
{errorInfo.error_message || "Unknown error occurred"}
</span>
</div>
</div>
{errorInfo.traceback && (
<div className="mt-4">
<div className="flex justify-between items-center mb-2">
<h4 className="font-medium">Traceback</h4>
<div className="flex items-center space-x-4">
<button
onClick={toggleAllFrames}
className="text-gray-500 hover:text-gray-700 flex items-center text-sm"
>
{allExpanded ? "Collapse All" : "Expand All"}
</button>
<button
onClick={() => navigator.clipboard.writeText(errorInfo.traceback || "")}
className="text-gray-500 hover:text-gray-700 flex items-center"
title="Copy traceback"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<span className="ml-1">Copy</span>
</button>
</div>
</div>
<div className="bg-white rounded-md border border-gray-200 overflow-hidden shadow-sm">
{tracebackFrames.map((frame, index) => (
<div key={index} className="border-b border-gray-200 last:border-b-0">
<div
className="px-4 py-2 flex items-center justify-between cursor-pointer hover:bg-gray-50"
onClick={() => toggleFrame(index)}
>
<div className="flex items-center">
<span className="text-gray-400 mr-2 w-12 text-right">{frame.lineNumber}</span>
<span className="text-gray-600 font-medium">{frame.fileName}</span>
<span className="text-gray-500 mx-1">in</span>
<span className="text-indigo-600 font-medium">{frame.inFunction || frame.fileName}</span>
</div>
<svg
className={`w-5 h-5 text-gray-500 transition-transform ${expandedFrames[index] ? "transform rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
{(expandedFrames[index] || false) && frame.code && (
<div className="px-12 py-2 font-mono text-sm text-gray-800 bg-gray-50 overflow-x-auto border-t border-gray-100">
{frame.code}
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};

View file

@ -1,267 +0,0 @@
import { render, screen, act } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { RequestResponsePanel } from "./RequestResponsePanel";
import type { LogEntry } from "./columns";
import NotificationsManager from "../molecules/notifications_manager";
const mockNotificationsManager = vi.mocked(NotificationsManager);
const baseLogEntry: LogEntry = {
request_id: "chatcmpl-test-id",
api_key: "api-key",
team_id: "team-id",
model: "gpt-4",
model_id: "gpt-4",
call_type: "chat",
spend: 0,
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
startTime: "2025-11-14T00:00:00Z",
endTime: "2025-11-14T00:00:00Z",
cache_hit: "miss",
request_duration_ms: 1000,
messages: [{ role: "user", content: "hello" }],
response: { status: "ok" },
metadata: {
status: "success",
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
request_tags: {},
custom_llm_provider: "openai",
api_base: "https://api.example.com",
};
describe("RequestResponsePanel", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
writable: true,
configurable: true,
});
Object.defineProperty(window, "isSecureContext", {
value: true,
writable: true,
configurable: true,
});
});
it("should render the component with request and response panels", () => {
const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" });
const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" });
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
expect(screen.getByText("Request")).toBeInTheDocument();
expect(screen.getByText("Response")).toBeInTheDocument();
});
it("should copy request to clipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request data" });
const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" });
const mockWriteText = vi.fn().mockResolvedValue(undefined);
if (navigator.clipboard) {
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText);
} else {
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: mockWriteText,
},
writable: true,
configurable: true,
});
}
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
const copyButtons = screen.getAllByRole("button");
const copyRequestButton = copyButtons.find((button) => button.getAttribute("title") === "Copy request");
expect(copyRequestButton).toBeInTheDocument();
await act(async () => {
await user.click(copyRequestButton!);
});
expect(mockGetRawRequest).toHaveBeenCalled();
expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "request data" }, null, 2));
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Request copied to clipboard");
});
it("should copy response to clipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" });
const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response data" });
const mockWriteText = vi.fn().mockResolvedValue(undefined);
if (navigator.clipboard) {
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText);
} else {
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: mockWriteText,
},
writable: true,
configurable: true,
});
}
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
const copyButtons = screen.getAllByRole("button");
const copyResponseButton = copyButtons.find((button) => button.getAttribute("title") === "Copy response");
expect(copyResponseButton).toBeInTheDocument();
expect(copyResponseButton).not.toBeDisabled();
await act(async () => {
await user.click(copyResponseButton!);
});
expect(mockFormattedResponse).toHaveBeenCalled();
expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "response data" }, null, 2));
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Response copied to clipboard");
});
it("should call formattedResponse for the response panel and not getRawRequest", () => {
const mockGetRawRequest = vi.fn().mockReturnValue({ requestData: "this should not appear in response" });
const mockFormattedResponse = vi.fn().mockReturnValue({ responseData: "this should appear in response" });
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
expect(mockFormattedResponse).toHaveBeenCalled();
expect(mockGetRawRequest).toHaveBeenCalled();
const formattedResponseCallCount = mockFormattedResponse.mock.calls.length;
expect(formattedResponseCallCount).toBeGreaterThanOrEqual(1);
const responseData = mockFormattedResponse.mock.results[0].value;
expect(responseData).toEqual({ responseData: "this should appear in response" });
expect(responseData).not.toEqual({ requestData: "this should not appear in response" });
});
it("should show error response data when hasError is true and hasResponse is false", () => {
const failedLogEntry: LogEntry = {
...baseLogEntry,
messages: [],
response: {},
metadata: {
status: "failure",
error_information: {
error_message: "Model not found",
error_class: "NotFoundError",
error_code: 404,
},
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
};
const errorResponse = { error: { message: "Model not found", type: "NotFoundError", code: 404, param: null } };
const mockGetRawRequest = vi.fn().mockReturnValue({ messages: [] });
const mockFormattedResponse = vi.fn().mockReturnValue(errorResponse);
render(
<RequestResponsePanel
row={{ original: failedLogEntry }}
hasMessages={false}
hasResponse={false}
hasError={true}
errorInfo={failedLogEntry.metadata.error_information}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
expect(screen.queryByText("Response data not available")).not.toBeInTheDocument();
expect(mockFormattedResponse).toHaveBeenCalled();
const copyButtons = screen.getAllByRole("button");
const copyResponseButton = copyButtons.find((button) => button.getAttribute("title") === "Copy response");
expect(copyResponseButton).not.toBeDisabled();
});
it("should show Response data not available when hasResponse and hasError are both false", () => {
const mockGetRawRequest = vi.fn().mockReturnValue({ messages: [] });
const mockFormattedResponse = vi.fn().mockReturnValue({});
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={false}
hasResponse={false}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
expect(screen.getByText("Response data not available")).toBeInTheDocument();
});
it("should show error code in response header when hasError is true", () => {
const errorInfo = { error_message: "Rate limit exceeded", error_class: "RateLimitError", error_code: 429 };
const mockGetRawRequest = vi.fn().mockReturnValue({ messages: [] });
const mockFormattedResponse = vi
.fn()
.mockReturnValue({ error: { message: "Rate limit exceeded", type: "RateLimitError", code: 429, param: null } });
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={false}
hasResponse={false}
hasError={true}
errorInfo={errorInfo}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
expect(screen.getByText(/HTTP code 429/)).toBeInTheDocument();
});
});

View file

@ -1,146 +0,0 @@
import { LogEntry } from "./columns";
import NotificationsManager from "../molecules/notifications_manager";
import { JsonView, defaultStyles } from "react-json-view-lite";
import "react-json-view-lite/dist/index.css";
interface RequestResponsePanelProps {
row: {
original: LogEntry;
};
hasMessages: string | boolean;
hasResponse: string | boolean;
hasError: boolean;
errorInfo: any;
getRawRequest: () => any;
formattedResponse: () => any;
}
export function RequestResponsePanel({
row,
hasMessages,
hasResponse,
hasError,
errorInfo,
getRawRequest,
formattedResponse,
}: RequestResponsePanelProps) {
const copyToClipboard = async (text: string) => {
try {
// Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
} else {
// Fallback for non-secure contexts (like 0.0.0.0)
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand("copy");
document.body.removeChild(textArea);
if (!successful) {
throw new Error("execCommand failed");
}
return true;
}
} catch (error) {
console.error("Copy failed:", error);
return false;
}
};
const handleCopyRequest = async () => {
const success = await copyToClipboard(JSON.stringify(getRawRequest(), null, 2));
if (success) {
NotificationsManager.success("Request copied to clipboard");
} else {
NotificationsManager.fromBackend("Failed to copy request");
}
};
const handleCopyResponse = async () => {
const success = await copyToClipboard(JSON.stringify(formattedResponse(), null, 2));
if (success) {
NotificationsManager.success("Response copied to clipboard");
} else {
NotificationsManager.fromBackend("Failed to copy response");
}
};
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full max-w-full overflow-hidden box-border">
{/* Request Side */}
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">Request</h3>
<button onClick={handleCopyRequest} className="p-1 hover:bg-gray-200 rounded" title="Copy request">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
<div className="[&_[role='tree']]:bg-white [&_[role='tree']]:text-slate-900">
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode={true} />
</div>
</div>
</div>
{/* Response Side */}
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">
Response
{hasError && <span className="ml-2 text-sm text-red-600">• HTTP code {errorInfo?.error_code || 400}</span>}
</h3>
<button
onClick={handleCopyResponse}
className="p-1 hover:bg-gray-200 rounded"
title="Copy response"
disabled={!hasResponse && !hasError}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
</div>
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
{hasResponse || hasError ? (
<div className="[&_[role='tree']]:bg-white [&_[role='tree']]:text-slate-900">
<JsonView data={formattedResponse()} style={defaultStyles} clickToExpandNode />
</div>
) : (
<div className="text-gray-500 text-sm italic text-center py-4">Response data not available</div>
)}
</div>
</div>
</div>
);
}