All files / apps/chatbot/src/hooks useConversationView.ts

100% Statements 27/27
100% Branches 25/25
100% Functions 9/9
100% Lines 20/20

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72            1x 6x             1x 14x   10x 10x   9x 9x               1x     15x     15x   15x         15x           15x 15x 5x               15x 15x 15x     15x                
import { useMemo } from "react";
 
import { BOT } from "@/constants/chat";
 
import { ChatMessage, SuggestionChip } from "@/types/chat";
 
const findSuggestions = (message?: ChatMessage): (string | SuggestionChip)[] =>
  message?.blocks?.find((b) => b.type === "suggestions")?.items ?? [];
 
/**
 * A bot turn that resolved into nothing renders as an empty bubble, so it is
 * dropped: an explicit confirmation whose pending action is gone, and a turn
 * that carries neither blocks nor text.
 */
const isRenderable = (message: ChatMessage) => {
  if (message.user._id !== BOT._id) return true;
 
  const hasConfirmationBlock = message.blocks?.some((b) => b.type === "confirmation");
  if (hasConfirmationBlock && !message.pendingAction) return false;
 
  const hasNoContent = (!message.blocks || message.blocks.length === 0) && !message.text?.trim();
  return !(hasNoContent && !message.pendingAction);
};
 
interface UseConversationViewArgs {
  messages: ChatMessage[];
  isInitializing: boolean;
}
 
export const useConversationView = ({ messages, isInitializing }: UseConversationViewArgs) => {
  // Messages are stored newest-first; reverse to oldest-first so the list
  // renders top-to-bottom with the newest message at the bottom.
  const visibleMessages = useMemo(() => [...messages.filter(isRenderable)].reverse(), [messages]);
 
  /** Only the bot's welcome turn is visible — the user has not spoken yet. */
  const isWelcomeOnly = visibleMessages.length === 1 && visibleMessages[0]?.user._id === BOT._id;
 
  const isInitialising = messages.length === 0 && isInitializing;
  // Nothing to render a conversation from means the greeting, whatever the
  // reason — still starting, failed to start, or an agent turn that resolved
  // into no renderable message at all. Any of those left to MessageList would
  // be a blank screen.
  const isShowingGreeting = visibleMessages.length === 0 || isWelcomeOnly;
 
  // The agent owns the welcome sentence — it ships it in the welcome message's
  // text block, phrased for what THIS actor is allowed to do. The client cannot
  // know that before the thread exists, so an empty result renders a skeleton
  // rather than a stand-in the agent might contradict.
  const welcomeDescription = useMemo(() => {
    if (!isWelcomeOnly) return "";
    return visibleMessages[0]?.blocks?.find((b) => b.type === "text")?.text?.trim() ?? "";
  }, [isWelcomeOnly, visibleMessages]);
 
  /**
   * Suggestions from the bot's latest turn, pinned above the composer. The
   * welcome turn is the latest turn like any other, so its chips ride the same
   * strip — the greeting does not get a second, differently-shaped chip list.
   */
  const activeSuggestions = useMemo(() => {
    const lastMessage = visibleMessages[visibleMessages.length - 1];
    return lastMessage?.user._id === BOT._id ? findSuggestions(lastMessage) : [];
  }, [visibleMessages]);
 
  return {
    visibleMessages,
    isInitialising,
    isShowingGreeting,
    welcomeDescription,
    activeSuggestions,
  };
};