'use client'; import { NovuApprovalCard } from './elements/novu-approval-card'; import { defaultThreadPartGroupBy, Thread, type ThreadComponents, } from './elements/thread.aui'; import { ErrorState } from './elements/error-state'; import { ToolFallback } from './elements/tool-fallback.aui'; import { ThinkingIndicator } from './elements/thinking-indicator'; import { Button } from '../ui/button'; import { cn } from '../lib/utils'; import { MessagePrimitive, useAuiState, type ToolCallMessagePartComponent } from '@assistant-ui/react'; import { RotateCcwIcon } from 'lucide-react'; import { useCallback } from 'react'; import { NovuCardUI, NovuFileUI, NovuMcpUI } from './novu-parts'; import { useWebChatUi } from './web-chat-actions'; function approvalStandaloneGroupBy( part: Parameters[0], context: Parameters[1], ) { if (part.type === 'tool-call' && part.approval != null) { return []; } return defaultThreadPartGroupBy(part, context); } const NovuToolFallback: ToolCallMessagePartComponent = (props) => { const pending = props.approval != null && props.approval.approved === undefined && props.approval.resolution === undefined; if (pending) { return ; } return ; }; const THREAD_COMPONENTS: ThreadComponents = { Banner: WebChatBanner, Indicator: WebChatTypingIndicator, UserMessage: WebChatUserMessage, ToolFallback: NovuToolFallback, groupBy: approvalStandaloneGroupBy, }; function WebChatBanner() { const { banner } = useWebChatUi(); if (!banner) return null; return ( ); } function WebChatTypingIndicator() { const { typingLabel, pendingActionCount } = useWebChatUi(); if (pendingActionCount > 0) return null; const label = (typingLabel?.trim() || 'Thinking').replace(/[.\u2026]+$/, ''); return ( ); } function WebChatUserMessage() { const { retryMessage, composerBusy } = useWebChatUi(); const metadata = useAuiState((state) => state.message.metadata); const custom = metadata?.custom as { novuStatus?: string; novuMessageId?: string } | undefined; const status = custom?.novuStatus; const novuMessageId = custom?.novuMessageId; const failed = status === 'failed'; return (
{failed ? ( ) : null}
); } function LoadOlder() { const { pagination } = useWebChatUi(); const loadOlder = useCallback(async () => { // assistant-ui viewport has no scroll-preservation API; preserve position via DOM delta. const container = document.querySelector('[data-slot="aui_thread-viewport"]') as HTMLElement | null; const heightBefore = container?.scrollHeight ?? 0; await pagination.fetchMore(); requestAnimationFrame(() => { if (!container) return; container.scrollTop += container.scrollHeight - heightBefore; }); }, [pagination]); if (!pagination.hasMore) return null; return (
); } export function WebChatThread() { return (
); }