import { ActionBarPrimitive, AuiIf, MessagePrimitive } from '@assistant-ui/react'; import type { AssistantMessage as StoredMessage } from '@trek/shared'; import { Check, Copy, FileText, LoaderCircle, Share2 } from 'lucide-react'; import { useMemo } from 'react'; import { useTranslation } from '../../i18n'; import { chatOf, useAssistantStore } from '../../store/assistantStore'; import { Tooltip } from '../shared/Tooltip'; import AssistantChangeCard from './AssistantChangeCard'; import AssistantFollowUps from './AssistantFollowUps'; import AssistantMarkdown from './AssistantMarkdown'; import AssistantMentionText from './AssistantMentionText'; import AssistantPlaceStrip from './AssistantPlaceStrip'; import AssistantShareDialog from './AssistantShareDialog'; import AssistantToolSteps from './AssistantToolSteps'; import AssistantWeatherStrip from './AssistantWeatherStrip'; import { messageBlocks } from './assistantParts'; import { useAssistantView } from './assistantViewContext'; import AssistantCardBlock from './cards/AssistantCardBlock'; import { useAssistantShare } from './useAssistantShare'; /** * One message of the chat. The user's question sits on the end side in a * quiet accent bubble; the answer runs as plain text across the column, with * its looked-up steps, the places and weather they found, the cards its * tools brought and the proposed changes in between, and a copy and a share * button that show on hover (always on the latest answer, and always on the * phone, which has no hover). */ const BAR_BUTTON = 'rounded-md p-1 text-content-faint hover:bg-surface-hover hover:text-content-secondary disabled:cursor-default disabled:opacity-50 disabled:hover:bg-transparent'; /** Posts the answer to the trip chat after a preview; a tick once it went. */ function ShareButton({ share }: { share: ReturnType }) { const { t } = useTranslation(); if (share.shared) { return ( ); } const label = share.offline ? t('assistant.error.offline') : t('assistant.share.button'); return ( {/* The wrapper carries the tooltip while the button is off. */} ); } /** A file the user handed over, as a chip with what it is and how big. */ function AttachmentChip({ name, size }: { name: string; size: number }) { const kb = Math.max(1, Math.round(size / 1024)); return ( {name} {kb >= 1024 ? `${(kb / 1024).toFixed(1)} MB` : `${kb} KB`} ); } function Question({ message }: { message: StoredMessage }) { const text = message.parts.map((p) => (p.type === 'text' ? p.text : '')).join(''); const files = message.parts.flatMap((p) => (p.type === 'attachment' ? [p] : [])); const mentions = message.parts.flatMap((p) => (p.type === 'data-trek-mentions' ? p.data.items : [])); return ( {files.map((f, i) => ( ))} {text && (

{mentions.length > 0 ? : text}

)}
); } function Answer({ message }: { message: StoredMessage }) { const { t } = useTranslation(); const { tripId, variant } = useAssistantView(); const writing = useAssistantStore((s) => { const chat = chatOf(s, tripId); return chat.isRunning && chat.messages[chat.messages.length - 1]?.id === message.id; }); const blocks = useMemo(() => messageBlocks(message, writing), [message, writing]); const share = useAssistantShare(tripId, message, writing); const last = blocks[blocks.length - 1]; const hasText = blocks.some((b) => b.kind === 'text'); return ( {blocks.map((block) => { if (block.kind === 'text') { return ; } if (block.kind === 'steps') return ; if (block.kind === 'places') { return ; } if (block.kind === 'weather') return ; if (block.kind === 'cards') return ; return ; })} {/* Nothing below says the answer goes on: no words being written, no step running. */} {writing && last?.kind !== 'text' && last?.kind !== 'steps' && (

{t('assistant.step.working')}

)} {hasText && !writing && ( // The bar mounts on hover for older answers; the row keeps its height so nothing jumps.
s.message.isCopied}> !s.message.isCopied}> {share.canShare && }
)} {share.canShare && }
); } export default function AssistantMessage({ message }: { message: StoredMessage }) { return message.role === 'user' ? : ; }