'use client';
import { Spinner } from "@heroui/react";
import { useMemo, useState } from "react";
import z from "zod";
import { Workflow } from "@/app/lib/types/workflow_types";
import { WorkflowTool } from "@/app/lib/types/workflow_types";
import MarkdownContent from "@/app/lib/components/markdown-content";
import { ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, CodeIcon, CheckCircleIcon, FileTextIcon, EyeIcon, EyeOffIcon, WrapTextIcon, ArrowRightFromLineIcon, BracesIcon, TextIcon } from "lucide-react";
import { TestProfile } from "@/app/lib/types/testing_types";
import { ProfileContextBox } from "./profile-context-box";
import { Message, ToolMessage, AssistantMessageWithToolCalls } from "@/app/lib/types/types";
function UserMessage({ content }: { content: string }) {
return (
);
}
function InternalAssistantMessage({ content, sender, latency, delta, showJsonMode = false }: { content: string, sender: string | null | undefined, latency: number, delta: number, showJsonMode?: boolean }) {
const isJsonContent = useMemo(() => {
try {
JSON.parse(content);
return true;
} catch {
return false;
}
}, [content]);
const hasResponseKey = useMemo(() => {
if (!isJsonContent) return false;
try {
const parsed = JSON.parse(content);
return parsed && typeof parsed === 'object' && 'response' in parsed;
} catch {
return false;
}
}, [content, isJsonContent]);
const [jsonMode, setJsonMode] = useState(false);
const [wrapText, setWrapText] = useState(true);
// Show plus icon and duration
const deltaDisplay = (
+{Math.round(delta / 1000)}s
);
// Extract response content for display
const displayContent = useMemo(() => {
if (!isJsonContent || !hasResponseKey) return content;
try {
const parsed = JSON.parse(content);
return parsed.response || content;
} catch {
return content;
}
}, [content, isJsonContent, hasResponseKey]);
// Format JSON content
const formattedJson = useMemo(() => {
if (!isJsonContent) return content;
try {
return JSON.stringify(JSON.parse(content), null, 2);
} catch {
return content;
}
}, [content, isJsonContent]);
return (
{sender ?? 'Assistant'}
{isJsonContent && hasResponseKey && (
{jsonMode && (
)}
)}
{isJsonContent && hasResponseKey && jsonMode ? (
{formattedJson}
) : (
)}
);
}
function AssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
return (
{sender ?? 'Assistant'}
{latency > 0 &&
{Math.round(latency / 1000)}s
}
);
}
function AssistantMessageLoading() {
return (
);
}
function ToolCalls({
toolCalls,
results,
projectId,
messages,
sender,
workflow,
testProfile = null,
systemMessage,
delta
}: {
toolCalls: z.infer['toolCalls'];
results: Record>;
projectId: string;
messages: z.infer[];
sender: string | null | undefined;
workflow: z.infer;
testProfile: z.infer | null;
systemMessage: string | undefined;
delta: number;
}) {
return
{toolCalls.map(toolCall => {
return
})}
;
}
function ToolCall({
toolCall,
result,
sender,
workflow,
delta
}: {
toolCall: z.infer['toolCalls'][number];
result: z.infer | undefined;
sender: string | null | undefined;
workflow: z.infer;
delta: number;
}) {
let matchingWorkflowTool: z.infer | undefined;
for (const tool of workflow.tools) {
if (tool.name === toolCall.function.name) {
matchingWorkflowTool = tool;
break;
}
}
if (toolCall.function.name.startsWith('transfer_to_')) {
return ;
}
return ;
}
function TransferToAgentToolCall({
result: availableResult,
sender,
delta
}: {
result: z.infer | undefined;
sender: string | null | undefined;
delta: number;
}) {
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
if (!typedResult) {
return <>>;
}
const deltaDisplay = (
+{Math.round(delta / 1000)}s
);
return (
{sender}
{typedResult.assistant}
{deltaDisplay}
);
}
function ClientToolCall({
toolCall,
result: availableResult,
sender,
workflow,
delta
}: {
toolCall: z.infer['toolCalls'][number];
result: z.infer | undefined;
sender: string | null | undefined;
workflow: z.infer;
delta: number;
}) {
const [wrapText, setWrapText] = useState(true);
const [paramsExpanded, setParamsExpanded] = useState(false);
const [resultsExpanded, setResultsExpanded] = useState(false);
const hasExpandedContent = paramsExpanded || resultsExpanded;
const isCompressed = !paramsExpanded && !resultsExpanded;
// Compressed state: stretch header, no wrapping
if (isCompressed) {
return (
{sender && (
{sender}
)}
{!availableResult &&
}
{availableResult &&
}
Invoked Tool:
{toolCall.function.name}
{hasExpandedContent && (
)}
}
wrapText={wrapText}
onExpandedChange={setParamsExpanded}
/>
{availableResult && (
}
wrapText={wrapText}
onExpandedChange={setResultsExpanded}
/>
)}
);
}
// Expanded state: respect 85% max width, prevent overshoot
return (
{sender && (
{sender}
)}
{!availableResult &&
}
{availableResult &&
}
Invoked Tool:
{toolCall.function.name}
{hasExpandedContent && (
)}
}
wrapText={wrapText}
onExpandedChange={setParamsExpanded}
/>
{availableResult && (
}
wrapText={wrapText}
onExpandedChange={setResultsExpanded}
/>
)}
);
}
function ExpandableContent({
label,
content,
expanded = false,
icon,
wrapText = false,
onExpandedChange,
rightButton
}: {
label: string,
content: string | object | undefined,
expanded?: boolean,
icon?: React.ReactNode,
wrapText?: boolean,
onExpandedChange?: (expanded: boolean) => void,
rightButton?: React.ReactNode
}) {
const [isExpanded, setIsExpanded] = useState(expanded);
const formattedContent = useMemo(() => {
if (typeof content === 'string') {
try {
const parsed = JSON.parse(content);
return JSON.stringify(parsed, null, 2);
} catch (e) {
// If it's not JSON, return the string as-is
return content;
}
}
if (typeof content === 'object') {
return JSON.stringify(content, null, 2);
}
return 'undefined';
}, [content]);
function toggleExpanded() {
const newExpanded = !isExpanded;
setIsExpanded(newExpanded);
onExpandedChange?.(newExpanded);
}
const isMarkdown = label === 'Result' && typeof content === 'string' && !content.startsWith('{');
return
{!isExpanded &&
}
{isExpanded &&
}
{icon &&
{icon}}
{label}
{rightButton &&
{rightButton}}
{isExpanded && (
isMarkdown ? (
) : (
{formattedContent}
)
)}
;
}
export function Messages({
projectId,
messages,
toolCallResults,
loadingAssistantResponse,
workflow,
testProfile = null,
systemMessage,
onSystemMessageChange,
showSystemMessage,
showDebugMessages = true,
showJsonMode = false,
}: {
projectId: string;
messages: z.infer[];
toolCallResults: Record>;
loadingAssistantResponse: boolean;
workflow: z.infer;
testProfile: z.infer | null;
systemMessage: string | undefined;
onSystemMessageChange: (message: string) => void;
showSystemMessage: boolean;
showDebugMessages?: boolean;
showJsonMode?: boolean;
}) {
// Remove scroll/auto-scroll state and logic
// const scrollContainerRef = useRef(null);
// const [autoScroll, setAutoScroll] = useState(true);
// const [showUnreadBubble, setShowUnreadBubble] = useState(false);
// Remove handleScroll and useEffect for scroll
const renderMessage = (message: z.infer, index: number) => {
if (message.role === 'assistant') {
// TODO: add latency support
// let latency = new Date(message.createdAt).getTime() - lastUserMessageTimestamp;
// if (!userMessageSeen) {
// latency = 0;
// }
let latency = 0;
// First check for tool calls
if ('toolCalls' in message) {
// Skip tool calls if debug mode is off
if (!showDebugMessages) {
return null;
}
return (
);
}
// Then check for internal messages
if (message.content && message.responseType === 'internal') {
// Skip internal messages if debug mode is off
if (!showDebugMessages) {
return null;
}
return (
);
}
// Finally, regular assistant messages
return (
);
}
if (message.role === 'user') {
// TODO: add latency support
// lastUserMessageTimestamp = new Date(message.createdAt).getTime();
// userMessageSeen = true;
return ;
}
return null;
};
const isAgentTransition = (message: z.infer) => {
return message.role === 'assistant' && 'toolCalls' in message && Array.isArray(message.toolCalls) && message.toolCalls.some(tc => tc.function.name.startsWith('transfer_to_'));
};
const isAssistantMessage = (message: z.infer) => {
return message.role === 'assistant' && (!('toolCalls' in message) || !Array.isArray(message.toolCalls) || !message.toolCalls.some(tc => tc.function.name.startsWith('transfer_to_')));
};
if (showSystemMessage) {
return (
);
}
// Just render the messages, no scroll container or unread bubble
return (
{messages.map((message, index) => {
const renderedMessage = renderMessage(message, index);
if (renderedMessage) {
return (
{renderedMessage}
);
}
return null;
})}
{loadingAssistantResponse &&
}
);
}