"use client"; import { useState, useEffect, useRef } from "react"; import { useTranslations } from "next-intl"; import { ChatBubble } from "@/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ChatBubble"; import { buildRequestTurns, buildResponseTurns } from "@/mitm/inspector/conversationNormalizer"; import type { InterceptedRequest, NormalizedTurn } from "@/mitm/inspector/types"; // ─── Payload Code Block ───────────────────────────────────────────────────── export function PayloadSection({ title, json, onCopy, collapsible = true, defaultOpen = true }) { const t = useTranslations("requestLogger.detail"); const [copied, setCopied] = useState(false); const [open, setOpen] = useState(defaultOpen); const handleCopy = async () => { const success = await onCopy(); if (success !== false) { setCopied(true); setTimeout(() => setCopied(false), 2000); } }; return (

{title}

{collapsible && ( )}
{open && (
          {json}
        
)}
); } // ─── Conversation context section ─────────────────────────────────────────── // Renders THIS request's own context (its request body's messages/input, plus // its response) — a plain single-request normalization, same shape as the // traffic-inspector's ConversationTab, no cross-request reconstruction. While // the request is still generating (detail.active === true) the response side // shows the partial text captured so far, refreshed on a short poll scoped to // just this section. const CONVERSATION_ACTIVE_POLL_INTERVAL_MS = 1200; function asInterceptedResponseBody(responseBody: unknown): InterceptedRequest { return { id: "", source: "custom-host", timestamp: "", method: "POST", host: "", path: "", requestHeaders: {}, requestBody: null, requestSize: 0, responseHeaders: {}, responseBody: responseBody != null ? JSON.stringify(responseBody) : null, responseSize: 0, status: 0, detectedKind: "llm", }; } export function ConversationContextSection({ log, detail }) { const [open, setOpen] = useState(true); const [liveDetail, setLiveDetail] = useState(detail); const [liveRefresh, setLiveRefresh] = useState(() => { try { const v = localStorage.getItem("pref:conversationContext:liveRefresh"); return v == null ? true : v === "1"; } catch { return true; } }); const turnsBoxRef = useRef(null); useEffect(() => { setLiveDetail(detail); }, [detail]); // Same live-poll pattern as the SSE Events section (StreamSection below), // but gated on liveRefresh too: an active request keeps generating either // way, this toggle only controls whether THIS panel keeps fetching/ // redrawing while the user reads it. useEffect(() => { if (!liveDetail?.active || !liveRefresh) return; let cancelled = false; let timeoutId: ReturnType | undefined; const tick = () => { if (cancelled) return; if (document.visibilityState !== "visible") { timeoutId = setTimeout(tick, CONVERSATION_ACTIVE_POLL_INTERVAL_MS); return; } fetch(`/api/logs/${log.id}`, { cache: "no-store" }) .then((res) => (res.ok ? res.json() : null)) .then((data) => { if (cancelled || !data) return; setLiveDetail(data); if (data.active) timeoutId = setTimeout(tick, CONVERSATION_ACTIVE_POLL_INTERVAL_MS); }) .catch(() => { timeoutId = setTimeout(tick, CONVERSATION_ACTIVE_POLL_INTERVAL_MS); }); }; timeoutId = setTimeout(tick, CONVERSATION_ACTIVE_POLL_INTERVAL_MS); return () => { cancelled = true; if (timeoutId) clearTimeout(timeoutId); }; }, [liveDetail?.active, liveRefresh, log.id]); const toggleLiveRefresh = () => { const next = !liveRefresh; setLiveRefresh(next); try { localStorage.setItem("pref:conversationContext:liveRefresh", next ? "1" : "0"); } catch {} }; const scrollToBottom = () => { const el = turnsBoxRef.current; if (!el) return; requestAnimationFrame(() => { try { el.scrollTop = el.scrollHeight; } catch {} }); }; const requestBody = liveDetail?.requestBody ?? liveDetail?.pipelinePayloads?.clientRequest ?? null; const requestTurns = buildRequestTurns(requestBody) ?? []; const responseBody = liveDetail?.responseBody ?? null; const responseTurns: NormalizedTurn[] = responseBody != null ? buildResponseTurns(asInterceptedResponseBody(responseBody)) : liveDetail?.partialAssistantText ? [ { role: "assistant", blocks: [{ type: "text", text: liveDetail.partialAssistantText }], }, ] : []; const allTurns: NormalizedTurn[] = [...requestTurns, ...responseTurns]; // Follow new content as it streams in — same idea as StreamSection's // autoscroll effect, tied to the same liveRefresh toggle. useEffect(() => { if (!liveRefresh || !open) return; scrollToBottom(); }, [allTurns.length, liveDetail?.partialAssistantText, liveRefresh, open]); if (allTurns.length === 0) return null; return (

Conversation Context

{open && (
{liveDetail?.active && ( )}
)}
{open && (
{allTurns.map((turn, i) => ( ))}
)}
); }