"use client"; import { useState, useRef, useEffect } from "react"; import { useChat } from "ai/react"; import { XMarkIcon } from "@heroicons/react/24/outline"; type MessageWithLoading = { content: string; role: string; isStreaming?: boolean; }; // Define theme CSS variables const themeStyles = { light: { '--bg-primary': '#ffffff', '--bg-secondary': 'rgba(243, 244, 246, 0.7)', '--text-primary': '#111827', '--text-secondary': '#6B7280', '--border-color': 'rgba(229, 231, 235, 0.5)', '--shadow-color': 'rgba(0, 0, 0, 0.1)', '--message-bg': 'rgba(243, 244, 246, 0.7)', '--input-bg': 'rgba(255, 255, 255, 0.9)', }, dark: { '--bg-primary': '#1F2937', '--bg-secondary': 'rgba(31, 41, 55, 0.7)', '--text-primary': '#F9FAFB', '--text-secondary': '#D1D5DB', '--border-color': 'rgba(75, 85, 99, 0.5)', '--shadow-color': 'rgba(0, 0, 0, 0.3)', '--message-bg': 'rgba(55, 65, 81, 0.7)', '--input-bg': 'rgba(31, 41, 55, 0.9)', }, } as const; // Inject required styles into iframe const injectStyles = ` * { margin: 0; padding: 0; box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .playgo-chat { background-color: var(--bg-primary); color: var(--text-primary); } .playgo-chat-header { background-color: var(--bg-secondary); border-bottom: 1px solid var(--border-color); } .playgo-chat-messages { background-color: var(--bg-primary); } .playgo-chat-input { background-color: var(--bg-secondary); border-top: 1px solid var(--border-color); } .playgo-chat-input-field { background-color: var(--input-bg); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 0.75rem; padding: 0.75rem 3rem 0.75rem 0.75rem; width: 100%; transition: all 0.2s; } .playgo-chat-input-field:focus { outline: none; border-color: var(--primary-color); box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.2); } .playgo-chat-input-field::placeholder { color: var(--text-secondary); } .playgo-message-bubble { box-shadow: 0 1px 2px var(--shadow-color); max-width: 80%; padding: 0.75rem; border-radius: 0.75rem; } .playgo-message-assistant { background-color: var(--message-bg); color: var(--text-primary); margin-right: 1rem; } .playgo-message-user { background-color: var(--primary-color); color: white; margin-left: 1rem; } @media (prefers-color-scheme: dark) { .playgo-chat[data-theme="system"] { --bg-primary: #1F2937; --bg-secondary: rgba(31, 41, 55, 0.7); --text-primary: #F9FAFB; --text-secondary: #D1D5DB; --border-color: rgba(75, 85, 99, 0.5); --shadow-color: rgba(0, 0, 0, 0.3); --message-bg: rgba(55, 65, 81, 0.7); --input-bg: rgba(31, 41, 55, 0.9); } } `; interface EmbeddableChatBotConfig { apiUrl?: string; height?: string | number; width?: string | number; theme?: 'light' | 'dark' | 'system'; primaryColor?: string; placeholder?: string; buttonText?: string; } // First, let's fix the CSS type error by declaring the CSS custom properties declare module 'react' { interface CSSProperties { '--primary-color'?: string; '--primary-rgb'?: string; '--bg-primary'?: string; '--bg-secondary'?: string; '--text-primary'?: string; '--text-secondary'?: string; '--border-color'?: string; '--shadow-color'?: string; '--message-bg'?: string; '--input-bg'?: string; } } export default function EmbeddableChatBot({ apiUrl = "/api/landing_page_chat", theme = 'system', primaryColor = "#FF6B6B", placeholder = "請問任何關於學習的問題...", buttonText = "需要協助嗎?", }: EmbeddableChatBotConfig) { const chatContainerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [messages, setMessages] = useState([]); const [currentTheme, setCurrentTheme] = useState(theme); // Convert hex color to RGB for CSS variables const hexToRgb = (hex: string) => { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); return result ? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}` : '255, 107, 107'; // fallback RGB for #FF6B6B }; const { input, handleInputChange, handleSubmit, isLoading, } = useChat({ api: apiUrl, onError: (error) => { console.error("Chat error:", error); }, onFinish: () => { setMessages((prev) => prev.map((msg) => ({ ...msg, isStreaming: false })) ); }, }); // Handle system theme changes useEffect(() => { if (theme === 'system') { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = (e: MediaQueryListEvent) => { setCurrentTheme(e.matches ? 'dark' : 'light'); }; mediaQuery.addEventListener('change', handleChange); setCurrentTheme(mediaQuery.matches ? 'dark' : 'light'); return () => mediaQuery.removeEventListener('change', handleChange); } else { setCurrentTheme(theme); } }, [theme]); useEffect(() => { if (chatContainerRef.current) { chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; } }, [messages]); // Inject styles when component mounts useEffect(() => { if (isOpen && chatContainerRef.current) { const styleSheet = document.createElement('style'); styleSheet.textContent = injectStyles; chatContainerRef.current.appendChild(styleSheet); } }, [isOpen]); const containerStyle = { ...(themeStyles[currentTheme === 'system' ? 'light' : currentTheme]), '--primary-color': primaryColor, '--primary-rgb': hexToRgb(primaryColor), } as React.CSSProperties; // Send message to parent when chat state changes useEffect(() => { if (typeof window !== 'undefined') { window.parent.postMessage({ type: isOpen ? 'chatOpen' : 'chatClose' }, '*'); } }, [isOpen]); if (!isOpen) { return (
); } return (

P l a y G o A I

{messages.map((message, index) => (
{message.content} {message.isStreaming && ( ... )}
))}
); }