All files / src/components/Chatbot CopilotSuggestions.tsx

96.42% Statements 81/84
95.83% Branches 23/24
100% Functions 4/4
96.42% Lines 81/84

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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 1181x 1x 1x     1x     1x     1x     1x     1x         1x 8x 8x   8x 8x 8x 8x   8x   8x   8x         3x 1x 3x   3x 8x   8x 1x 1x 1x 1x 1x 1x 1x   8x 3x 3x   8x 8x 8x 8x 8x 8x 8x 8x 8x   8x 8x     8x 8x 8x 4x 4x 4x 4x 9x 9x   9x 9x 9x 9x 9x 9x 9x 9x 9x 3x 6x 9x   9x 3x 3x 3x 3x   9x 9x   9x 4x 4x 4x 4x   4x   8x   8x  
import { useCopilotChat } from '@copilotkit/react-core';
import { Role, TextMessage } from '@copilotkit/runtime-client-gql';
import { useEffect, useState } from 'react';
 
// Constants
import { CHATBOT_SUGGESTIONS, SUGGESTION_CONFIG } from '@/constants';
 
// Utils
import { combineClasses } from '@shared/utils';
 
// Components
import { Button } from '@/components/common/Button';
 
// Icons
import { LoadingIcon } from '../icons/reacts';
 
// Stores
import { useSetSuggestionsLoaded, useShouldLoadSuggestions } from '@/stores';
 
// Providers
import { ChatMode, useChat } from '@/providers/chatbot';
 
export const CopilotSuggestions = () => {
  const { appendMessage } = useCopilotChat();
  const shouldLoadSuggestions = useShouldLoadSuggestions();
 
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(
    shouldLoadSuggestions ? CHATBOT_SUGGESTIONS.length : 0,
  );
 
  useEffect(() => {
    // Skip animation if suggestions already loaded
    if (shouldLoadSuggestions || !isOpen) return;
 
    if (activeIndex >= CHATBOT_SUGGESTIONS.length) {
      useSetSuggestionsLoaded(true);
      return;
    }
 
    const timer = setTimeout(() => {
      setActiveIndex((prev) => prev + 1);
    }, SUGGESTION_CONFIG.LOADING_DELAY);
 
    return () => clearTimeout(timer);
  }, [activeIndex, shouldLoadSuggestions, isOpen]);
 
  const handleSuggestionClick = (prompt: string) => () => {
    appendMessage(
      new TextMessage({
        role: Role.User,
        content: prompt,
      }),
    );
  };
 
  const handleToggleOpen = () => {
    setIsOpen((prev) => !prev);
  };
 
  const renderToggleButton = (label: string, className?: string) => (
    <Button
      variant="suggestion"
      onClick={handleToggleOpen}
      className={combineClasses(
        'cursor-pointer w-full gap-1.5 leading-none',
        'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-700/50',
        className,
      )}
    >
      {label}
    </Button>
  );
 
  return (
    <div className={combineClasses('px-5 pt-2')}>
      {isOpen ? (
        <>
          <div className="flex w-full flex-wrap gap-2">
            {CHATBOT_SUGGESTIONS.slice(0, activeIndex + 1).map(
              ({ id, label, prompt }, index) => {
                const isLoading =
                  !shouldLoadSuggestions && index === activeIndex;
 
                return (
                  <Button
                    key={id}
                    variant="suggestion"
                    disabled={isLoading}
                    onClick={handleSuggestionClick(prompt)}
                    className={combineClasses(
                      'max-w-full',
                      isLoading
                        ? 'text-primary-700/60 cursor-default'
                        : 'hover:opacity-90',
                    )}
                  >
                    {isLoading && (
                      <LoadingIcon
                        className="h-3.5 w-3.5 mr-2"
                        data-testid="loading-icon"
                      />
                    )}
                    <span>{label}</span>
                  </Button>
                );
              },
            )}
          </div>
          {renderToggleButton(SUGGESTION_CONFIG.TRIGGER_LABEL_OPEN, 'mt-2')}
        </>
      ) : (
        renderToggleButton(SUGGESTION_CONFIG.TRIGGER_LABEL)
      )}
    </div>
  );
};