Files
hapi/web/src/components/AssistantChat/messages/UserMessage.tsx
T

87 lines
4.2 KiB
TypeScript

import { MessagePrimitive, useAssistantState } from '@assistant-ui/react'
import { useHappyChatContext } from '@/components/AssistantChat/context'
import type { HappyChatMessageMetadata } from '@/lib/assistant-runtime'
import { MessageStatusIndicator } from '@/components/AssistantChat/messages/MessageStatusIndicator'
import { MessageAttachments } from '@/components/AssistantChat/messages/MessageAttachments'
import { UserBubbleContent, getUserBubbleClassName, shouldShowMessageStatus } from '@/components/AssistantChat/messages/user-bubble'
import { CliOutputBlock } from '@/components/CliOutputBlock'
import { getConversationMessageAnchorId } from '@/chat/outline'
import { MessageActions } from '@/components/AssistantChat/messages/MessageActions'
export function HappyUserMessage() {
const ctx = useHappyChatContext()
const role = useAssistantState(({ message }) => message.role)
const messageId = useAssistantState(({ message }) => message.id)
const text = useAssistantState(({ message }) => {
if (message.role !== 'user') return ''
return message.content.find((part) => part.type === 'text')?.text ?? ''
})
const status = useAssistantState(({ message }) => {
if (message.role !== 'user') return undefined
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
return custom?.status
})
const localId = useAssistantState(({ message }) => {
if (message.role !== 'user') return null
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
return custom?.localId ?? null
})
const attachments = useAssistantState(({ message }) => {
if (message.role !== 'user') return undefined
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
return custom?.attachments
})
const isCliOutput = useAssistantState(({ message }) => {
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
return custom?.kind === 'cli-output'
})
const cliText = useAssistantState(({ message }) => {
const custom = message.metadata.custom as Partial<HappyChatMessageMetadata> | undefined
if (custom?.kind !== 'cli-output') return ''
return message.content.find((part) => part.type === 'text')?.text ?? ''
})
if (role !== 'user') return null
const canRetry = status === 'failed' && typeof localId === 'string' && Boolean(ctx.onRetryMessage)
const onRetry = canRetry ? () => ctx.onRetryMessage!(localId) : undefined
const showStatus = shouldShowMessageStatus(status)
if (isCliOutput) {
return (
<MessagePrimitive.Root
id={getConversationMessageAnchorId(messageId)}
className="happy-message scroll-mt-4 px-1 min-w-0 max-w-full overflow-x-hidden"
>
<div className="ml-auto w-full max-w-[92%]">
<CliOutputBlock text={cliText} />
<MessageActions align="end" copyText={cliText} />
</div>
</MessagePrimitive.Root>
)
}
const hasText = text.length > 0
const hasAttachments = attachments && attachments.length > 0
return (
<MessagePrimitive.Root
id={getConversationMessageAnchorId(messageId)}
className="happy-message flex flex-col items-end scroll-mt-4"
>
<div className={getUserBubbleClassName(status)}>
<div className="flex items-start gap-2">
<div className="min-w-0 flex-1">
{hasText ? <UserBubbleContent text={text} /> : null}
{hasAttachments ? <MessageAttachments attachments={attachments} /> : null}
</div>
{showStatus && (
<div className="happy-message-actions-first-line flex shrink-0 items-center gap-1">
{showStatus ? <MessageStatusIndicator status={status} onRetry={onRetry} /> : null}
</div>
)}
</div>
</div>
<MessageActions align="end" copyText={hasText ? text : undefined} />
</MessagePrimitive.Root>
)
}