'use client';
/**
* START HERE — the `useWebChat` example.
*
* Minimal app shape (drop the built-in sidebar if you have your own layout):
*
* const { messages, sendMessage, respondToAction, isRunning, isLoading, error } =
* useWebChat({ agentId });
*
* return (
* <>
* {error ?
{error.message}
: null}
*
* void sendMessage(text)} />
* >
* );
*
* Below: same pattern with assistant-ui Thread and a conversations sidebar.
* Swap `ChatPanel` / `WebChatThreadList` if the host app already has a chat kit.
* Keep this hook wiring.
*/
import { useWebChat } from '@novu/react';
import { useCallback, useMemo, useState } from 'react';
import { WebChatThreadList } from './assistant-ui/thread-list';
import { WebChatRuntimeProvider } from './assistant-ui/web-chat-runtime';
import { ChatPanel } from './chat-panel';
import { useConversations } from './lib/conversations';
import { mapConversationsToThreadData, NEW_CONVERSATION_THREAD_ID } from './lib/thread-list-mapper';
import { TooltipProvider } from './ui/tooltip';
type WebChatProps = {
/** Standalone scaffold passes `config.agentId`; merge mode reads env. */
agentId?: string;
};
export function WebChat({ agentId: agentIdProp }: WebChatProps = {}) {
const agentId = agentIdProp ?? process.env.NEXT_PUBLIC_NOVU_AGENT_ID ?? '';
const [conversationId, setConversationId] = useState();
const conversations = useConversations();
const reloadConversations = useCallback(() => {
void conversations.reload();
}, [conversations.reload]);
const {
messages,
pendingActions,
sendMessage,
respondToAction,
sendAction,
retryMessage,
conversationId: activeConversationId,
error,
isRunning,
isLoading,
pagination,
isRecovering,
catchUpError,
refetch,
typing,
startNewConversation,
} = useWebChat({
agentId,
conversationId,
onMessage: (message) => {
if (message.role === 'assistant') {
reloadConversations();
}
},
});
// Platform ack sets `typing` before `run-start`; treat both as "agent working" for UI.
const showAgentActivity = isRunning || Boolean(typing?.status?.trim());
const composerBusy =
messages.some((message) => message.status === 'sending') || showAgentActivity || isLoading;
const bannerDetail =
catchUpError?.message ?? (messages.some((message) => message.status === 'failed') ? undefined : error?.message);
const activeThreadId = activeConversationId ?? conversationId ?? NEW_CONVERSATION_THREAD_ID;
const ui = useMemo(
() => ({
sendAction,
retryMessage,
pagination,
typingLabel: typing?.status,
pendingActionCount: pendingActions.length,
banner: bannerDetail
? {
title: catchUpError ? "Couldn't sync missed messages" : 'Something went wrong',
detail: bannerDetail,
...(activeConversationId != null ? { onRetry: () => void refetch() } : null),
}
: undefined,
}),
[
sendAction,
retryMessage,
pagination,
typing?.status,
pendingActions.length,
bannerDetail,
catchUpError,
activeConversationId,
refetch,
]
);
const runtimeThreadList = useMemo(
() => ({
threadId: activeThreadId,
threads: mapConversationsToThreadData(conversations.items),
isLoading: conversations.isLoading,
onSwitchToThread: (identifier: string) => {
setConversationId(identifier);
},
onSwitchToNewThread: () => {
startNewConversation();
setConversationId(undefined);
},
}),
[activeThreadId, conversations.items, conversations.isLoading, startNewConversation]
);
return (
);
}