import type { ChatMessage, ToolCall } from '@/types/os'
import { AgentMessage, UserMessage } from './MessageItem'
import Tooltip from '@/components/ui/tooltip'
import { memo } from 'react'
import {
ToolCallProps,
ReasoningStepProps,
ReasoningProps,
ReferenceData,
Reference
} from '@/types/os'
import React, { type FC } from 'react'
import Icon from '@/components/ui/icon'
import ChatBlankState from './ChatBlankState'
import { useStore } from '@/store'
interface MessageListProps {
messages: ChatMessage[]
}
interface MessageWrapperProps {
message: ChatMessage
isLastMessage: boolean
}
interface ReferenceProps {
references: ReferenceData[]
}
interface ReferenceItemProps {
reference: Reference
}
const ReferenceItem: FC<ReferenceItemProps> = ({ reference }) => (
<div className="relative flex h-[63px] w-[190px] cursor-default flex-col justify-between overflow-hidden rounded-md bg-background-secondary p-3 transition-colors hover:bg-background-secondary/80">
<p className="text-sm font-medium text-primary">{reference.name}</p>
<p className="truncate text-xs text-primary/40">{reference.content}</p>
</div>
)
const References: FC<ReferenceProps> = ({ references }) => (
<div className="flex flex-col gap-4">
{references.map((referenceData, index) => (
<div
key={`${referenceData.query}-${index}`}
className="flex flex-col gap-3"
>
<div className="flex flex-wrap gap-3">
{referenceData.references.map((reference, refIndex) => (
<ReferenceItem
key={`${reference.name}-${reference.meta_data.chunk}-${refIndex}`}
reference={reference}
/>
))}
</div>
</div>
))}
</div>
)
const AgentMessageWrapper = ({ message }: MessageWrapperProps) => {
return (
<div className="flex flex-col gap-y-9">
{message.extra_data?.reasoning_steps &&
message.extra_data.reasoning_steps.length > 0 && (
<div className="flex items-start gap-4">
<Tooltip
delayDuration={0}
content={<p className="text-accent">Reasoning</p>}
side="top"
>
<Icon type="reasoning" size="sm" />
</Tooltip>
<div className="flex flex-col gap-3">
<p className="text-xs uppercase">Reasoning</p>
<Reasonings reasoning={message.extra_data.reasoning_steps} />
</div>
</div>
)}
{message.extra_data?.references &&
message.extra_data.references.length > 0 && (
<div className="flex items-start gap-4">
<Tooltip
delayDuration={0}
content={<p className="text-accent">References</p>}
side="top"
>
<Icon type="references" size="sm" />
</Tooltip>
<div className="flex flex-col gap-3">
<References references={message.extra_data.references} />
</div>
</div>
)}
{message.tool_calls && message.tool_calls.length > 0 && (
<ToolCallsSection toolCalls={message.tool_calls} />
)}
<AgentMessage message={message} />
</div>
)
}
const Reasoning: FC<ReasoningStepProps> = ({ index, stepTitle }) => (
<div className="flex items-center gap-2 text-secondary">
<div className="flex h-[20px] items-center rounded-md bg-background-secondary p-2">
<p className="text-xs">STEP {index + 1}</p>
</div>
<p className="text-xs">{stepTitle}</p>
</div>
)
const Reasonings: FC<ReasoningProps> = ({ reasoning }) => (
<div className="flex flex-col items-start justify-center gap-2">
{reasoning.map((title, index) => (
<Reasoning
key={`${title.title}-${title.action}-${index}`}
stepTitle={title.title}
index={index}
/>
))}
</div>
)
const ToolComponent = memo(({ tools }: ToolCallProps) => (
<div className="cursor-default rounded-full bg-accent px-2 py-1.5 text-xs">
<p className="font-dmmono uppercase text-primary/80">{tools.tool_name}</p>
</div>
))
ToolComponent.displayName = 'ToolComponent'
interface ToolCallsSectionProps {
toolCalls: ToolCall[]
}
const ToolCallsSection: FC<ToolCallsSectionProps> = ({ toolCalls }) => {
const setToolCallPanelOpen = useStore((state) => state.setToolCallPanelOpen)
const setSelectedToolCalls = useStore((state) => state.setSelectedToolCalls)
const setStreamingToolCalls = useStore((state) => state.setStreamingToolCalls)
const isStreaming = useStore((state) => state.isStreaming)
const handleOpenToolPanel = () => {
// Deep copy tool calls to preserve data when panel is reopened
const toolCallsCopy = JSON.parse(JSON.stringify(toolCalls))
setSelectedToolCalls(toolCallsCopy)
// Also sync to streaming tool calls if currently streaming
if (isStreaming) {
setStreamingToolCalls(toolCallsCopy)
}
setToolCallPanelOpen(true)
}
return (
<div className="flex items-start gap-3">
<Tooltip
delayDuration={0}
content={<p className="text-accent">Click to view tool calls</p>}
side="top"
asChild
>
<button
onClick={handleOpenToolPanel}
className="rounded-lg bg-background-secondary p-1 transition-colors hover:bg-background-secondary/80"
aria-label="View tool calls"
>
<Icon type="hammer" size="sm" color="secondary" />
</button>
</Tooltip>
<div className="flex flex-wrap gap-2">
{toolCalls.map((toolCall, index) => (
<button
key={
toolCall.tool_call_id ||
`${toolCall.tool_name}-${toolCall.created_at}-${index}`
}
onClick={handleOpenToolPanel}
className="cursor-pointer rounded-full bg-accent px-2 py-1.5 text-xs transition-colors hover:bg-accent/80"
>
<span className="font-dmmono uppercase text-primary/80">
{toolCall.tool_name}
</span>
</button>
))}
</div>
</div>
)
}
const Messages = ({ messages }: MessageListProps) => {
if (messages.length === 0) {
return <ChatBlankState />
}
return (
<>
{messages.map((message, index) => {
const key = `${message.role}-${message.created_at}-${index}`
const isLastMessage = index === messages.length - 1
if (message.role === 'agent') {
return (
<AgentMessageWrapper
key={key}
message={message}
isLastMessage={isLastMessage}
/>
)
}
return <UserMessage key={key} message={message} />
})}
</>
)
}
export default Messages