diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ChatBubble.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ChatBubble.tsx new file mode 100644 index 0000000000..c9b9042005 --- /dev/null +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ChatBubble.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useState } from "react"; +import type { NormalizedTurn } from "@/mitm/inspector/types"; +import { cn } from "@/shared/utils/cn"; +import { MessageContent } from "./MessageContent"; + +interface ChatBubbleProps { + turn: NormalizedTurn; +} + +const ROLE_STYLES: Record = { + system: "border border-red-500/40 bg-red-900/20 text-red-200", + user: "ml-auto bg-blue-600/30 border border-blue-500/30 text-blue-100", + assistant: "bg-purple-900/30 border border-purple-500/30 text-purple-100", + tool: "bg-gray-800 border border-gray-600/30 text-gray-200", +}; + +const ROLE_LABEL: Record = { + system: "System", + user: "User", + assistant: "Assistant", + tool: "Tool", +}; + +export function ChatBubble({ turn }: ChatBubbleProps) { + const [collapsed, setCollapsed] = useState(turn.role === "system"); + + const isSystem = turn.role === "system"; + const isUser = turn.role === "user"; + + return ( +
+
+ {ROLE_LABEL[turn.role]} + {isSystem && ( + + )} +
+ {!collapsed && } + {collapsed && isSystem && ( +

System prompt hidden — click to expand

+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/MessageContent.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/MessageContent.tsx new file mode 100644 index 0000000000..b6dc106057 --- /dev/null +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/MessageContent.tsx @@ -0,0 +1,40 @@ +"use client"; + +import type { NormalizedBlock } from "@/mitm/inspector/types"; +import { ToolCallBlock } from "./ToolCallBlock"; +import { ToolResultBlock } from "./ToolResultBlock"; + +interface MessageContentProps { + blocks: NormalizedBlock[]; +} + +export function MessageContent({ blocks }: MessageContentProps) { + return ( +
+ {blocks.map((block, i) => { + if (block.type === "text") { + return ( +

+ {block.text} +

+ ); + } + if (block.type === "tool_use") { + return ( + + ); + } + if (block.type === "tool_result") { + return ( + + ); + } + return null; + })} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx new file mode 100644 index 0000000000..6d01759e8f --- /dev/null +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolCallBlock.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useState } from "react"; +import { JsonViewer } from "../shared/JsonViewer"; + +interface ToolCallBlockProps { + id: string; + name: string; + input: unknown; +} + +export function ToolCallBlock({ id, name, input }: ToolCallBlockProps) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + {expanded && ( +
+ +
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolResultBlock.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolResultBlock.tsx new file mode 100644 index 0000000000..1066d3590c --- /dev/null +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/chat/ToolResultBlock.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useState } from "react"; +import { JsonViewer } from "../shared/JsonViewer"; + +interface ToolResultBlockProps { + toolUseId: string; + content: unknown; +} + +export function ToolResultBlock({ toolUseId, content }: ToolResultBlockProps) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + {expanded && ( +
+ +
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionPicker.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionPicker.tsx new file mode 100644 index 0000000000..2a52d4091b --- /dev/null +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionPicker.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; +import type { SessionInfo } from "../../hooks/useSessionRecorder"; + +interface SessionPickerProps { + sessions: SessionInfo[]; + selectedId?: string; + onSelect: (id: string | undefined) => void; + onDelete: (id: string) => void; +} + +export function SessionPicker({ sessions, selectedId, onSelect, onDelete }: SessionPickerProps) { + const [open, setOpen] = useState(false); + + const selected = sessions.find((s) => s.id === selectedId); + + return ( +
+ + + {open && ( +
+ + {sessions.length === 0 && ( +

No sessions yet

+ )} + {sessions.map((s) => ( +
+ + +
+ ))} +
+ )} +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionRecorderBar.tsx b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionRecorderBar.tsx new file mode 100644 index 0000000000..d35714894c --- /dev/null +++ b/src/app/(dashboard)/dashboard/tools/traffic-inspector/components/session/SessionRecorderBar.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { cn } from "@/shared/utils/cn"; +import type { SessionInfo } from "../../hooks/useSessionRecorder"; + +interface SessionRecorderBarProps { + recording: boolean; + session: SessionInfo | null; + elapsed: number; + onStart: (name?: string) => void; + onStop: () => void; +} + +function formatElapsed(s: number): string { + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`; + return `${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`; +} + +export function SessionRecorderBar({ + recording, + session, + elapsed, + onStart, + onStop, +}: SessionRecorderBarProps) { + return ( +
+ {recording ? ( + <> + + {formatElapsed(elapsed)} + {session?.name && ( + {session.name} + )} + + + ) : ( + <> + + Not recording + + + )} +
+ ); +}