ChatWidget.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import * as React from 'react';
  2. import { ChatWidgetSettings, ChatMessage } from '../types';
  3. import { sendChatMessage } from '../services/geminiService';
  4. import { Icon } from './ui/Icon';
  5. interface ChatWidgetProps {
  6. settings: ChatWidgetSettings;
  7. }
  8. const ChatWidget: React.FC<ChatWidgetProps> = ({ settings }) => {
  9. const [isOpen, setIsOpen] = React.useState(false);
  10. const [messages, setMessages] = React.useState<ChatMessage[]>([]);
  11. const [userInput, setUserInput] = React.useState('');
  12. const [isLoading, setIsLoading] = React.useState(false);
  13. const messagesEndRef = React.useRef<HTMLDivElement>(null);
  14. React.useEffect(() => {
  15. if (isOpen && messages.length === 0) {
  16. setMessages([{ id: `msg-ai-${Date.now()}`, sender: 'ai', text: 'Hello! How can I help you today?' }]);
  17. }
  18. }, [isOpen, messages.length]);
  19. React.useEffect(() => {
  20. if (messagesEndRef.current) {
  21. messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
  22. }
  23. }, [messages, isLoading]);
  24. const handleSendMessage = React.useCallback(async () => {
  25. if (!userInput.trim()) return;
  26. const userMessage: ChatMessage = { id: `msg-user-${Date.now()}`, sender: 'user', text: userInput };
  27. setMessages(prev => [...prev, userMessage]);
  28. const messageToSend = userInput;
  29. setUserInput('');
  30. setIsLoading(true);
  31. const aiResponse = await sendChatMessage(messageToSend);
  32. setMessages(prev => [...prev, aiResponse]);
  33. setIsLoading(false);
  34. }, [userInput]);
  35. return (
  36. <>
  37. <div
  38. className={`absolute bottom-24 right-5 w-full max-w-sm h-[70vh] max-h-[600px] bg-white dark:bg-gray-800 rounded-2xl shadow-2xl flex flex-col transition-all duration-300 ease-in-out z-40 ${isOpen ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-10 pointer-events-none'}`}
  39. style={{ backgroundColor: settings.panelBackgroundColor }}
  40. >
  41. <div className="flex-shrink-0 p-4 flex justify-between items-center rounded-t-2xl" style={{ backgroundColor: settings.headerBackgroundColor, color: settings.headerTextColor }}>
  42. <h3 className="font-bold text-lg">AI Assistant</h3>
  43. <button onClick={() => setIsOpen(false)} style={{ color: settings.headerTextColor }}>
  44. <Icon className="h-6 w-6"><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></Icon>
  45. </button>
  46. </div>
  47. <div className="flex-1 p-4 overflow-y-auto space-y-4">
  48. {messages.map(msg => (
  49. <div key={msg.id} className={`flex items-start gap-3 ${msg.sender === 'user' ? 'justify-end' : ''}`}>
  50. {msg.sender === 'ai' && (
  51. <div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0" style={{ backgroundColor: settings.aiMessageBackgroundColor, color: settings.aiMessageTextColor }}>
  52. <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></Icon>
  53. </div>
  54. )}
  55. <div className="max-w-xs p-3 rounded-lg text-sm"
  56. style={{
  57. backgroundColor: msg.sender === 'user' ? settings.userMessageBackgroundColor : settings.aiMessageBackgroundColor,
  58. color: msg.sender === 'user' ? settings.userMessageTextColor : settings.aiMessageTextColor,
  59. borderRadius: msg.sender === 'user' ? '1rem 1rem 0.25rem 1rem' : '1rem 1rem 1rem 0.25rem'
  60. }}
  61. >
  62. <p>{msg.text}</p>
  63. </div>
  64. </div>
  65. ))}
  66. {isLoading && (
  67. <div className="flex items-start gap-3">
  68. <div className="w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0" style={{ backgroundColor: settings.aiMessageBackgroundColor }}>
  69. <Icon className="h-5 w-5" style={{ color: settings.aiMessageTextColor }}><path d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></Icon>
  70. </div>
  71. <div className="max-w-xs p-3 rounded-lg" style={{ backgroundColor: settings.aiMessageBackgroundColor }}>
  72. <div className="flex items-center space-x-1">
  73. <span className="w-2 h-2 rounded-full animate-pulse delay-0" style={{ backgroundColor: settings.aiMessageTextColor }}></span>
  74. <span className="w-2 h-2 rounded-full animate-pulse delay-150" style={{ backgroundColor: settings.aiMessageTextColor }}></span>
  75. <span className="w-2 h-2 rounded-full animate-pulse delay-300" style={{ backgroundColor: settings.aiMessageTextColor }}></span>
  76. </div>
  77. </div>
  78. </div>
  79. )}
  80. <div ref={messagesEndRef} />
  81. </div>
  82. <div className="flex-shrink-0 p-4 border-t border-gray-200 dark:border-gray-700 flex items-center gap-3">
  83. <input
  84. type="text"
  85. value={userInput}
  86. onChange={e => setUserInput(e.target.value)}
  87. onKeyPress={e => e.key === 'Enter' && !isLoading && handleSendMessage()}
  88. placeholder="Ask me anything..."
  89. className="flex-1 bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-white p-2 rounded-md border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-brand-primary focus:outline-none"
  90. disabled={isLoading}
  91. />
  92. <button onClick={handleSendMessage} disabled={isLoading} className="p-2 rounded-full text-white disabled:opacity-50" style={{ backgroundColor: settings.headerBackgroundColor }}>
  93. <Icon className="h-6 w-6"><path strokeLinecap="round" strokeLinejoin="round" d="M5 10l7-7m0 0l7 7m-7-7v18" /></Icon>
  94. </button>
  95. </div>
  96. </div>
  97. <button
  98. onClick={() => setIsOpen(!isOpen)}
  99. className={`absolute bottom-5 right-5 h-16 w-16 rounded-full shadow-lg flex items-center justify-center text-white transition-transform transform hover:scale-110 z-40 ${isOpen ? 'animate-none' : 'animate-breathing'}`}
  100. style={{ backgroundColor: settings.iconColor }}
  101. aria-label="Open AI Assistant"
  102. >
  103. <Icon className="h-8 w-8 transition-transform duration-300" style={{ transform: isOpen ? 'rotate(180deg) scale(0.75)' : 'rotate(0) scale(1)'}}>
  104. {isOpen
  105. ? <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
  106. : <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
  107. }
  108. </Icon>
  109. </button>
  110. </>
  111. );
  112. };
  113. export default ChatWidget;