Update styling for chat messages in playground and add delta timestamps

This commit is contained in:
akhisud3195 2025-05-07 14:05:13 +05:30
parent 614f7ee62b
commit e1b1b63980
2 changed files with 146 additions and 85 deletions

View file

@ -6,22 +6,21 @@ 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 { apiV1 } from "rowboat-shared";
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, XIcon } from "lucide-react";
import { MessageSquareIcon, EllipsisIcon, CircleCheckIcon, ChevronRightIcon, ChevronDownIcon, ChevronUpIcon, XIcon, PlusIcon } from "lucide-react";
import { TestProfile } from "@/app/lib/types/testing_types";
import { ProfileContextBox } from "./profile-context-box";
function UserMessage({ content }: { content: string }) {
return (
<div className="self-end flex flex-col items-end gap-1">
<div className="self-end flex flex-col items-end gap-1 mt-5 mb-8">
<div className="text-gray-500 dark:text-gray-400 text-xs">
User
</div>
<div className="max-w-[85%] inline-block">
<div className="bg-blue-50 dark:bg-[#1e2023] px-4 py-2.5
<div className="bg-blue-100 dark:bg-blue-900/40 px-4 py-2.5
rounded-2xl rounded-br-lg text-sm leading-relaxed
text-gray-700 dark:text-gray-200
border border-blue-100 dark:border-[#2a2d31]
shadow-sm animate-slideUpAndFade">
text-gray-800 dark:text-blue-100
border-none shadow-sm animate-slideUpAndFade">
<div className="text-left">
<MarkdownContent content={content} />
</div>
@ -31,52 +30,70 @@ function UserMessage({ content }: { content: string }) {
);
}
function InternalAssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
function InternalAssistantMessage({ content, sender, latency, delta }: { content: string, sender: string | null | undefined, latency: number, delta: number }) {
const [expanded, setExpanded] = useState(false);
// Show plus icon and duration
const deltaDisplay = (
<span className="inline-flex items-center gap-1 text-gray-400 dark:text-gray-500">
<PlusIcon size={12} />
{Math.round(delta / 1000)}s
</span>
);
return (
<div className="self-start flex flex-col gap-1">
{!expanded ? (
<button className="flex items-center text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 gap-1 group"
onClick={() => setExpanded(true)}>
<MessageSquareIcon size={16} />
<EllipsisIcon size={16} />
<span className="text-xs">Show debug message</span>
</button>
) : (
<>
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1 flex items-center justify-between">
<span>{sender ?? 'Assistant'}</span>
<button className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
onClick={() => setExpanded(false)}>
<XIcon size={16} />
</button>
</div>
<div className="max-w-[85%] inline-block">
<div className="border border-gray-200 dark:border-gray-700 border-dashed
px-4 py-2.5 rounded-2xl rounded-bl-lg text-sm
text-gray-700 dark:text-gray-200 shadow-sm">
<pre className="whitespace-pre-wrap">{content}</pre>
<div className="self-start flex flex-col gap-1 my-5">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
{sender ?? 'Assistant'}
</div>
<div className="max-w-[85%] inline-block">
<div className="bg-gray-50 dark:bg-zinc-800 px-4 py-2.5
rounded-2xl rounded-bl-lg text-sm leading-relaxed
text-gray-700 dark:text-gray-200
border-none shadow-sm animate-slideUpAndFade flex flex-col items-stretch">
{!expanded ? (
<div className="flex justify-between items-center mt-2">
<button className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300 hover:underline self-start" onClick={() => setExpanded(true)}>
<ChevronDownIcon size={16} />
Show internal message
</button>
<div className="text-right text-xs">
{deltaDisplay}
</div>
</div>
</div>
</>
)}
) : (
<>
<div className="text-left mb-2">
<MarkdownContent content={content} />
</div>
<div className="flex justify-between items-center mt-2">
<button className="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300 hover:underline self-start" onClick={() => setExpanded(false)}>
<ChevronUpIcon size={16} />
Hide internal message
</button>
<div className="text-right text-xs">
{deltaDisplay}
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}
function AssistantMessage({ content, sender, latency }: { content: string, sender: string | null | undefined, latency: number }) {
return (
<div className="self-start flex flex-col gap-1">
<div className="self-start flex flex-col gap-1 my-5">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
{sender ?? 'Assistant'}
</div>
<div className="max-w-[85%] inline-block">
<div className="bg-gray-50 dark:bg-[#1e2023] px-4 py-2.5
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-lg text-sm leading-relaxed
text-gray-700 dark:text-gray-200
border border-gray-200 dark:border-[#2a2d31]
shadow-sm animate-slideUpAndFade">
text-gray-800 dark:text-purple-100
border-none shadow-sm animate-slideUpAndFade">
<div className="flex flex-col gap-1">
<div className="text-left">
<MarkdownContent content={content} />
@ -93,15 +110,11 @@ function AssistantMessage({ content, sender, latency }: { content: string, sende
function AssistantMessageLoading() {
return (
<div className="self-start flex flex-col gap-1">
<div className="text-gray-500 dark:text-gray-400 text-xs pl-1">
Assistant
</div>
<div className="self-start flex flex-col gap-1 my-5">
<div className="max-w-[85%] inline-block">
<div className="bg-gray-50 dark:bg-gray-800 px-4 py-2.5
<div className="bg-purple-50 dark:bg-purple-900/30 px-4 py-2.5
rounded-2xl rounded-bl-lg
border border-gray-200 dark:border-gray-700
shadow-sm dark:shadow-gray-950/20 animate-pulse min-h-[2.5rem] flex items-center">
border-none shadow-sm animate-slideUpAndFade min-h-[2.5rem] flex items-center">
<Spinner size="sm" className="ml-2" />
</div>
</div>
@ -118,6 +131,7 @@ function ToolCalls({
workflow,
testProfile = null,
systemMessage,
delta
}: {
toolCalls: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'];
results: Record<string, z.infer<typeof apiV1.ToolMessage>>;
@ -127,6 +141,7 @@ function ToolCalls({
workflow: z.infer<typeof Workflow>;
testProfile: z.infer<typeof TestProfile> | null;
systemMessage: string | undefined;
delta: number;
}) {
return <div className="flex flex-col gap-4">
{toolCalls.map(toolCall => {
@ -136,6 +151,7 @@ function ToolCalls({
result={results[toolCall.id]}
sender={sender}
workflow={workflow}
delta={delta}
/>
})}
</div>;
@ -146,11 +162,13 @@ function ToolCall({
result,
sender,
workflow,
delta
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
delta: number;
}) {
let matchingWorkflowTool: z.infer<typeof WorkflowTool> | undefined;
for (const tool of workflow.tools) {
@ -163,45 +181,62 @@ function ToolCall({
if (toolCall.function.name.startsWith('transfer_to_')) {
return <TransferToAgentToolCall
result={result}
sender={sender}
sender={sender ?? ''}
delta={delta}
/>;
}
return <ClientToolCall
toolCall={toolCall}
result={result}
sender={sender}
sender={sender ?? ''}
workflow={workflow}
delta={delta}
/>;
}
function TransferToAgentToolCall({
result: availableResult,
sender,
delta
}: {
result: z.infer<typeof apiV1.ToolMessage> | undefined;
sender: string | null | undefined;
delta: number;
}) {
const typedResult = availableResult ? JSON.parse(availableResult.content) as { assistant: string } : undefined;
if (!typedResult) {
return <></>;
}
return <div className="flex gap-1 items-center text-gray-500 text-sm justify-center">
<div>{sender}</div>
<svg className="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1" d="M19 12H5m14 0-4 4m4-4-4-4" />
</svg>
<div>{typedResult.assistant}</div>
</div>;
const deltaDisplay = (
<span className="inline-flex items-center gap-1 text-gray-400 dark:text-gray-500">
<PlusIcon size={12} />
{Math.round(delta / 1000)}s
</span>
);
return (
<div className="flex justify-center">
<div className="flex items-center gap-2 px-4 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/40 shadow-sm text-xs">
<span className="text-gray-700 dark:text-gray-200">{sender}</span>
<ChevronRightIcon size={14} className="text-gray-400 dark:text-gray-300" />
<span className="text-gray-700 dark:text-gray-200">{typedResult.assistant}</span>
<span className="ml-2">{deltaDisplay}</span>
</div>
</div>
);
}
function ClientToolCall({
toolCall,
result: availableResult,
sender,
workflow,
delta
}: {
toolCall: z.infer<typeof apiV1.AssistantMessageWithToolCalls>['tool_calls'][number];
result: z.infer<typeof apiV1.ToolMessage> | undefined;
sender: string | null | undefined;
workflow: z.infer<typeof Workflow>;
delta: number;
}) {
return (
<div className="self-start flex flex-col gap-1">
@ -322,36 +357,57 @@ export function Messages({
const isConsecutive = index > 0 && messages[index - 1].role === message.role;
if (message.role === 'assistant') {
// the assistant message createdAt is an ISO string timestamp
let latency = new Date(message.createdAt).getTime() - lastUserMessageTimestamp;
// if this is the first message, set the latency to 0
if (!userMessageSeen) {
latency = 0;
}
if ('tool_calls' in message) {
return (
<ToolCalls
toolCalls={message.tool_calls}
results={toolCallResults}
projectId={projectId}
messages={messages}
sender={message.agenticSender}
workflow={workflow}
testProfile={testProfile}
systemMessage={systemMessage}
/>
);
// Helper: is this message a transfer pill or internal message?
const isTransferPill = 'tool_calls' in message && message.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_'));
const isInternal = message.agenticResponseType === 'internal';
if (isTransferPill || isInternal) {
// Find previous message that is either a transfer pill or internal message
let delta = latency;
for (let i = index - 1; i >= 0; i--) {
const prev = messages[i];
const prevIsTransferPill = prev.role === 'assistant' && 'tool_calls' in prev && prev.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_'));
const prevIsInternal = prev.role === 'assistant' && prev.agenticResponseType === 'internal';
if (prevIsTransferPill || prevIsInternal) {
delta = new Date(message.createdAt).getTime() - new Date(prev.createdAt).getTime();
break;
}
if (prev.role === 'user') {
break;
}
}
if (isTransferPill) {
return (
<ToolCalls
toolCalls={message.tool_calls}
results={toolCallResults}
projectId={projectId}
messages={messages}
sender={message.agenticSender ?? ''}
workflow={workflow}
testProfile={testProfile}
systemMessage={systemMessage}
delta={delta}
/>
);
} else {
return (
<InternalAssistantMessage
content={message.content ?? ''}
sender={message.agenticSender ?? ''}
latency={latency}
delta={delta}
/>
);
}
}
return message.agenticResponseType === 'internal' ? (
<InternalAssistantMessage
content={message.content}
sender={message.agenticSender}
latency={latency}
/>
) : (
return (
<AssistantMessage
content={message.content}
sender={message.agenticSender}
content={message.content ?? ''}
sender={message.agenticSender ?? ''}
latency={latency}
/>
);
@ -366,6 +422,14 @@ export function Messages({
return null;
};
const isAgentTransition = (message: z.infer<typeof apiV1.ChatMessage>) => {
return message.role === 'assistant' && 'tool_calls' in message && Array.isArray(message.tool_calls) && message.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_'));
};
const isAssistantMessage = (message: z.infer<typeof apiV1.ChatMessage>) => {
return message.role === 'assistant' && (!('tool_calls' in message) || !Array.isArray(message.tool_calls) || !message.tool_calls.some(tc => tc.function.name.startsWith('transfer_to_')));
};
if (showSystemMessage) {
return (
<ProfileContextBox
@ -378,12 +442,9 @@ export function Messages({
return (
<div className="max-w-[768px] mx-auto">
<div className="flex flex-col space-y-2">
<div className="flex flex-col">
{messages.map((message, index) => (
<div
key={index}
className={`${index > 0 && messages[index - 1].role === message.role ? 'mt-1' : 'mt-4'}`}
>
<div key={index}>
{renderMessage(message, index)}
</div>
))}

View file

@ -15,7 +15,7 @@
},
{
"role": "user",
"content": "[2025-05-02, 10:00] Assistant: Good morning. Thank you for taking the time to speak with us today. Could you start by briefly walking us through your leadership journey over the past decade?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:01] User: Absolutely. Over the past 12 years, I've led product and technology teams across high-growth B2B SaaS companies. Most recently, I was the Chief Product Officer at a fintech scaleup, where I built a team of 60+ PMs and oversaw the expansion into three international markets. Prior to that, I was VP of Product at a healthtech startup acquired by a public company.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:03] Assistant: Impressive. What would you say is your core leadership style, and how has it evolved over the years?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:04] User: I'd describe my style as mission-oriented and data-driven. Early in my career, I leaned heavily on execution. But with time, I've grown more focused on setting context and empowering teams. At my last company, I implemented OKRs org-wide and focused on building a culture of trust and transparency.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:06] Assistant: How do you approach scaling an org while preserving agility and innovation?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:07] User: Great question. I believe the key is in modular team design. At the fintech company, we adopted a \\\\\\\\\\\\\\\"startup within a startup\\\\\\\\\\\\\\\" model—each pod owned metrics and had embedded PMs, designers, and engineers. This preserved autonomy and speed while scaling. We also invested heavily in internal tooling and rituals like demo days to keep the culture of innovation alive.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:10] Assistant: Tell me about a time you made a high-stakes decision without perfect data.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:11] User: In early 2023, we had to decide whether to sunset a low-performing but vocal customer-facing feature. We lacked conclusive cohort data but saw consistent qualitative feedback about confusion and churn. I convened a task force, aligned with customer success and execs, and made the call. Within 60 days, churn dropped by 12%, validating the bet.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:14] Assistant: Final question—what excites you about potentially joining our company as a Chief Product Officer?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:15] User: Your mission aligns with what I care about—bringing transparency and efficiency to financial infrastructure. The team's reputation, your engineering culture, and the velocity of product innovation are all huge draws. I believe I can help you scale without losing the edge that got you here.",
"content": "[2025-05-02, 10:00] Assistant: Good morning. Thank you for taking the time to speak with us today. Could you start by briefly walking us through your leadership journey over the past decade?\n\n[2025-05-02, 10:01] User: Absolutely. Over the past 12 years, I've led product and technology teams across high-growth B2B SaaS companies. Most recently, I was the Chief Product Officer at a fintech scaleup, where I built a team of 60+ PMs and oversaw the expansion into three international markets. Prior to that, I was VP of Product at a healthtech startup acquired by a public company.\n\n[2025-05-02, 10:03] Assistant: Impressive. What would you say is your core leadership style, and how has it evolved over the years?\n\n[2025-05-02, 10:04] User: I'd describe my style as mission-oriented and data-driven. Early in my career, I leaned heavily on execution. But with time, I've grown more focused on setting context and empowering teams. At my last company, I implemented OKRs org-wide and focused on building a culture of trust and transparency.\n\n[2025-05-02, 10:06] Assistant: How do you approach scaling an org while preserving agility and innovation?\n\n[2025-05-02, 10:07] User: Great question. I believe the key is in modular team design. At the fintech company, we adopted a \"startup within a startup\" model—each pod owned metrics and had embedded PMs, designers, and engineers. This preserved autonomy and speed while scaling. We also invested heavily in internal tooling and rituals like demo days to keep the culture of innovation alive.\n\n[2025-05-02, 10:10] Assistant: Tell me about a time you made a high-stakes decision without perfect data.\n\n[2025-05-02, 10:11] User: In early 2023, we had to decide whether to sunset a low-performing but vocal customer-facing feature. We lacked conclusive cohort data but saw consistent qualitative feedback about confusion and churn. I convened a task force, aligned with customer success and execs, and made the call. Within 60 days, churn dropped by 12%, validating the bet.\n\n[2025-05-02, 10:14] Assistant: Final question—what excites you about potentially joining our company as a Chief Product Officer?\n\n[2025-05-02, 10:15] User: Your mission aligns with what I care about—bringing transparency and efficiency to financial infrastructure. The team's reputation, your engineering culture, and the velocity of product innovation are all huge draws. I believe I can help you scale without losing the edge that got you here.",
"version": "v1",
"chatId": "",
"createdAt": "2025-05-05T17:29:44.695Z"
@ -42,7 +42,7 @@
"response_type": "external"
},
{
"content": "[2025-05-02, 10:00] Assistant: Good morning. Thank you for taking the time to speak with us today. Could you start by briefly walking us through your leadership journey over the past decade?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:01] User: Absolutely. Over the past 12 years, I've led product and technology teams across high-growth B2B SaaS companies. Most recently, I was the Chief Product Officer at a fintech scaleup, where I built a team of 60+ PMs and oversaw the expansion into three international markets. Prior to that, I was VP of Product at a healthtech startup acquired by a public company.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:03] Assistant: Impressive. What would you say is your core leadership style, and how has it evolved over the years?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:04] User: I'd describe my style as mission-oriented and data-driven. Early in my career, I leaned heavily on execution. But with time, I've grown more focused on setting context and empowering teams. At my last company, I implemented OKRs org-wide and focused on building a culture of trust and transparency.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:06] Assistant: How do you approach scaling an org while preserving agility and innovation?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:07] User: Great question. I believe the key is in modular team design. At the fintech company, we adopted a \\\\\\\\\\\\\\\"startup within a startup\\\\\\\\\\\\\\\" model—each pod owned metrics and had embedded PMs, designers, and engineers. This preserved autonomy and speed while scaling. We also invested heavily in internal tooling and rituals like demo days to keep the culture of innovation alive.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:10] Assistant: Tell me about a time you made a high-stakes decision without perfect data.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:11] User: In early 2023, we had to decide whether to sunset a low-performing but vocal customer-facing feature. We lacked conclusive cohort data but saw consistent qualitative feedback about confusion and churn. I convened a task force, aligned with customer success and execs, and made the call. Within 60 days, churn dropped by 12%, validating the bet.\\\\\\\\n\\\\\\\\n[2025-05-02, 10:14] Assistant: Final question—what excites you about potentially joining our company as a Chief Product Officer?\\\\\\\\n\\\\\\\\n[2025-05-02, 10:15] User: Your mission aligns with what I care about—bringing transparency and efficiency to financial infrastructure. The team's reputation, your engineering culture, and the velocity of product innovation are all huge draws. I believe I can help you scale without losing the edge that got you here.",
"content": "[2025-05-02, 10:00] Assistant: Good morning. Thank you for taking the time to speak with us today. Could you start by briefly walking us through your leadership journey over the past decade?\n\n[2025-05-02, 10:01] User: Absolutely. Over the past 12 years, I've led product and technology teams across high-growth B2B SaaS companies. Most recently, I was the Chief Product Officer at a fintech scaleup, where I built a team of 60+ PMs and oversaw the expansion into three international markets. Prior to that, I was VP of Product at a healthtech startup acquired by a public company.\n\n[2025-05-02, 10:03] Assistant: Impressive. What would you say is your core leadership style, and how has it evolved over the years?\n\n[2025-05-02, 10:04] User: I'd describe my style as mission-oriented and data-driven. Early in my career, I leaned heavily on execution. But with time, I've grown more focused on setting context and empowering teams. At my last company, I implemented OKRs org-wide and focused on building a culture of trust and transparency.\n\n[2025-05-02, 10:06] Assistant: How do you approach scaling an org while preserving agility and innovation?\n\n[2025-05-02, 10:07] User: Great question. I believe the key is in modular team design. At the fintech company, we adopted a \"startup within a startup\" model—each pod owned metrics and had embedded PMs, designers, and engineers. This preserved autonomy and speed while scaling. We also invested heavily in internal tooling and rituals like demo days to keep the culture of innovation alive.\n\n[2025-05-02, 10:10] Assistant: Tell me about a time you made a high-stakes decision without perfect data.\n\n[2025-05-02, 10:11] User: In early 2023, we had to decide whether to sunset a low-performing but vocal customer-facing feature. We lacked conclusive cohort data but saw consistent qualitative feedback about confusion and churn. I convened a task force, aligned with customer success and execs, and made the call. Within 60 days, churn dropped by 12%, validating the bet.\n\n[2025-05-02, 10:14] Assistant: Final question—what excites you about potentially joining our company as a Chief Product Officer?\n\n[2025-05-02, 10:15] User: Your mission aligns with what I care about—bringing transparency and efficiency to financial infrastructure. The team's reputation, your engineering culture, and the velocity of product innovation are all huge draws. I believe I can help you scale without losing the edge that got you here.",
"role": "user",
"sender": null,
"tool_calls": null,