"use client"; import { MessageGenerationStage } from "@/components/ui/message-generation-stage"; import { Tooltip, TooltipProvider } from "@/components/ui/suggestions-tooltip"; import { cn } from "@/lib/utils"; import type { Suggestion, TamboThread } from "@tambo-ai/react"; import { useTambo, useTamboSuggestions } from "@tambo-ai/react"; import { Loader2Icon } from "lucide-react"; import * as React from "react"; import { useEffect, useRef } from "react"; /** * @typedef MessageSuggestionsContextValue * @property {Array} suggestions - Array of suggestion objects * @property {string|null} selectedSuggestionId - ID of the currently selected suggestion * @property {function} accept - Function to accept a suggestion * @property {boolean} isGenerating - Whether suggestions are being generated * @property {Error|null} error - Any error from generation * @property {object} thread - The current Tambo thread */ interface MessageSuggestionsContextValue { suggestions: Suggestion[]; selectedSuggestionId: string | null; accept: (options: { suggestion: Suggestion }) => void; isGenerating: boolean; error: Error | null; thread: TamboThread; isMac: boolean; } /** * React Context for sharing suggestion data and functions among sub-components. * @internal */ const MessageSuggestionsContext = React.createContext(null); /** * Hook to access the message suggestions context. * @returns {MessageSuggestionsContextValue} The message suggestions context value. * @throws {Error} If used outside of MessageSuggestions. * @internal */ const useMessageSuggestionsContext = () => { const context = React.useContext(MessageSuggestionsContext); if (!context) { throw new Error( "MessageSuggestions sub-components must be used within a MessageSuggestions", ); } return context; }; /** * Props for the MessageSuggestions component. * Extends standard HTMLDivElement attributes. */ export interface MessageSuggestionsProps extends React.HTMLAttributes { /** Maximum number of suggestions to display (default: 3) */ maxSuggestions?: number; /** The child elements to render within the container. */ children?: React.ReactNode; /** Pre-seeded suggestions to display initially */ initialSuggestions?: Suggestion[]; } /** * The root container for message suggestions. * It establishes the context for its children and handles overall state management. * @component MessageSuggestions * @example * ```tsx * * * * * ``` */ const MessageSuggestions = React.forwardRef< HTMLDivElement, MessageSuggestionsProps >( ( { children, className, maxSuggestions = 3, initialSuggestions = [], ...props }, ref, ) => { const { thread } = useTambo(); const { suggestions: generatedSuggestions, selectedSuggestionId, accept, generateResult: { isPending: isGenerating, error }, } = useTamboSuggestions({ maxSuggestions }); // Combine initial and generated suggestions, but only use initial ones when thread is empty const suggestions = React.useMemo(() => { // Only use pre-seeded suggestions if thread is empty if (!thread?.messages?.length && initialSuggestions.length > 0) { return initialSuggestions.slice(0, maxSuggestions); } // Otherwise use generated suggestions return generatedSuggestions; }, [ thread?.messages?.length, generatedSuggestions, initialSuggestions, maxSuggestions, ]); const isMac = typeof navigator !== "undefined" && navigator.platform.startsWith("Mac"); // Track the last AI message ID to detect new messages const lastAiMessageIdRef = useRef(null); const loadingTimeoutRef = useRef(null); const contextValue = React.useMemo( () => ({ suggestions, selectedSuggestionId, accept, isGenerating, error, thread, isMac, }), [ suggestions, selectedSuggestionId, accept, isGenerating, error, thread, isMac, ], ); // Find the last AI message const lastAiMessage = thread?.messages ? [...thread.messages].reverse().find((msg) => msg.role === "assistant") : null; // When a new AI message appears, update the reference useEffect(() => { if (lastAiMessage && lastAiMessage.id !== lastAiMessageIdRef.current) { lastAiMessageIdRef.current = lastAiMessage.id; if (loadingTimeoutRef.current) { clearTimeout(loadingTimeoutRef.current); } loadingTimeoutRef.current = setTimeout(() => {}, 5000); } return () => { if (loadingTimeoutRef.current) { clearTimeout(loadingTimeoutRef.current); } }; }, [lastAiMessage, suggestions.length]); // Handle keyboard shortcuts for selecting suggestions useEffect(() => { if (!suggestions || suggestions.length === 0) return; const handleKeyDown = (event: KeyboardEvent) => { const modifierPressed = isMac ? event.metaKey && event.altKey : event.ctrlKey && event.altKey; if (modifierPressed) { const keyNum = parseInt(event.key); if (!isNaN(keyNum) && keyNum > 0 && keyNum <= suggestions.length) { event.preventDefault(); const suggestionIndex = keyNum - 1; accept({ suggestion: suggestions[suggestionIndex] as Suggestion }); } } }; document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); }; }, [suggestions, accept, isMac]); // If we have no messages yet and no initial suggestions, render nothing if (!thread?.messages?.length && initialSuggestions.length === 0) { return null; } return (
{children}
); }, ); MessageSuggestions.displayName = "MessageSuggestions"; /** * Props for the MessageSuggestionsStatus component. * Extends standard HTMLDivElement attributes. */ export type MessageSuggestionsStatusProps = React.HTMLAttributes; /** * Displays loading, error, or generation stage information. * Automatically connects to the context to show the appropriate status. * @component MessageSuggestions.Status * @example * ```tsx * * * * * ``` */ const MessageSuggestionsStatus = React.forwardRef< HTMLDivElement, MessageSuggestionsStatusProps >(({ className, ...props }, ref) => { const { error, isGenerating, thread } = useMessageSuggestionsContext(); return (
{/* Error state */} {error && (

{error.message}

)} {/* Always render a container for generation stage to prevent layout shifts */}
{thread?.generationStage && thread.generationStage !== "COMPLETE" ? ( ) : isGenerating ? (

Generating suggestions...

) : null}
); }); MessageSuggestionsStatus.displayName = "MessageSuggestions.Status"; /** * Props for the MessageSuggestionsList component. * Extends standard HTMLDivElement attributes. */ export type MessageSuggestionsListProps = React.HTMLAttributes; /** * Displays the list of suggestion buttons. * Automatically connects to the context to show the suggestions. * @component MessageSuggestions.List * @example * ```tsx * * * * * ``` */ const MessageSuggestionsList = React.forwardRef< HTMLDivElement, MessageSuggestionsListProps >(({ className, ...props }, ref) => { const { suggestions, selectedSuggestionId, accept, isGenerating, isMac } = useMessageSuggestionsContext(); const modKey = isMac ? "⌘" : "Ctrl"; const altKey = isMac ? "⌥" : "Alt"; // Create placeholder suggestions when there are no real suggestions const placeholders = Array(3).fill(null); return (
{suggestions.length > 0 ? suggestions.map((suggestion, index) => ( {modKey}+{altKey}+{index + 1} } side="top" > )) : // Render placeholder buttons when no suggestions are available placeholders.map((_, index) => (
Placeholder
))}
); }); MessageSuggestionsList.displayName = "MessageSuggestions.List"; export { MessageSuggestions, MessageSuggestionsStatus, MessageSuggestionsList };