'use client'; import { useState, useRef, useEffect, KeyboardEvent } from 'react'; import { ArrowUp, X, Sparkles, ChevronRight, RotateCcw, Copy, Check } from 'lucide-react'; import Link from 'next/link'; import { useAnalytics } from '../analytics/useAnalytics'; import { AnalyticsEvents } from '../analytics/analytics-events'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import dynamic from 'next/dynamic'; const AIOrb = dynamic(() => import('./AIOrb'), { ssr: false }); const LOADING_TEXTS = [ 'Durchsuche das Kabelhandbuch... πŸ“–', 'Frage den Senior-Ingenieur... πŸ‘΄πŸ”§', 'Frage ChatGPTs Cousin 2. Grades... πŸ€–', ]; interface ProductMatch { id: string; title: string; sku: string; slug: string; } interface Message { role: 'user' | 'assistant'; content: string; products?: ProductMatch[]; timestamp: number; } interface ComponentProps { isOpen: boolean; onClose: () => void; initialQuery?: string; triggerSearch?: boolean; } export function AISearchResults({ isOpen, onClose, initialQuery = '', triggerSearch = false, }: ComponentProps) { const { trackEvent } = useAnalytics(); const [query, setQuery] = useState(''); const [messages, setMessages] = useState([]); const [honeypot, setHoneypot] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [copiedIndex, setCopiedIndex] = useState(null); const [copiedAll, setCopiedAll] = useState(false); const [loadingText, setLoadingText] = useState(LOADING_TEXTS[0]); const inputRef = useRef(null); const modalRef = useRef(null); const messagesEndRef = useRef(null); const loadingIntervalRef = useRef | null>(null); const hasTriggeredRef = useRef(false); // Dedicated focus effect β€” polls until the input actually has focus useEffect(() => { if (!isOpen) return; let attempts = 0; const focusTimer = setInterval(() => { const el = inputRef.current; if (el && document.activeElement !== el) { el.focus({ preventScroll: true }); } attempts++; if (attempts >= 15 || document.activeElement === el) { clearInterval(focusTimer); } }, 100); return () => clearInterval(focusTimer); }, [isOpen]); useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; // Trigger initial search only once if (triggerSearch && initialQuery && !hasTriggeredRef.current) { hasTriggeredRef.current = true; handleSearch(initialQuery); } } else { document.body.style.overflow = 'unset'; setQuery(''); setMessages([]); setError(null); setIsLoading(false); hasTriggeredRef.current = false; } return () => { document.body.style.overflow = 'unset'; }; }, [isOpen, triggerSearch]); useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, isLoading]); // Global ESC handler useEffect(() => { if (!isOpen) return; const handleEsc = (e: globalThis.KeyboardEvent) => { if (e.key === 'Escape') { const activeElement = document.activeElement; const isInputFocused = activeElement === inputRef.current; if (query.trim()) { // If there's text, clear it but keep chat open setQuery(''); inputRef.current?.focus(); } else if (!isInputFocused) { // If no text and input is not focused, focus it inputRef.current?.focus(); } else { // If no text and input IS focused, close the chat onClose(); } } }; document.addEventListener('keydown', handleEsc); return () => document.removeEventListener('keydown', handleEsc); }, [isOpen, onClose, query]); const handleSearch = async (searchQuery: string = query) => { if (!searchQuery.trim() || isLoading) return; const newUserMessage: Message = { role: 'user', content: searchQuery, timestamp: Date.now() }; const newMessagesContext = [...messages, newUserMessage]; setMessages(newMessagesContext); setQuery(''); // Always clear input after send setError(null); // Give the user message animation 400ms to arrive before showing "thinking" setTimeout(() => { setIsLoading(true); // Start rotating loading texts let textIdx = Math.floor(Math.random() * LOADING_TEXTS.length); setLoadingText(LOADING_TEXTS[textIdx]); loadingIntervalRef.current = setInterval(() => { textIdx = (textIdx + 1) % LOADING_TEXTS.length; setLoadingText(LOADING_TEXTS[textIdx]); }, 2500); }, 400); trackEvent(AnalyticsEvents.FORM_SUBMIT, { type: 'ai_chat', query: searchQuery, }); try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 60000); const res = await fetch('/api/ai-search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, body: JSON.stringify({ messages: newMessagesContext, _honeypot: honeypot, }), }); clearTimeout(timeout); const data = await res.json().catch(() => null); if (!res.ok || !data) { throw new Error(data?.error || `Server antwortete mit Status ${res.status}`); } setMessages((prev) => [ ...prev, { role: 'assistant', content: data.answerText, products: data.products, timestamp: Date.now(), }, ]); setTimeout(() => inputRef.current?.focus(), 100); } catch (err: any) { console.error(err); const msg = err.name === 'AbortError' ? 'Anfrage hat zu lange gedauert. Bitte versuche es erneut.' : err.message || 'Ein Fehler ist aufgetreten.'; // Show error as a system message in the chat instead of a separate error banner setMessages((prev) => [ ...prev, { role: 'assistant', content: `⚠️ ${msg}`, timestamp: Date.now(), }, ]); trackEvent(AnalyticsEvents.ERROR, { location: 'ai_chat', message: err.message, query: searchQuery, }); } finally { setIsLoading(false); if (loadingIntervalRef.current) { clearInterval(loadingIntervalRef.current); loadingIntervalRef.current = null; } // Always re-focus the input setTimeout(() => inputRef.current?.focus(), 50); } }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { e.preventDefault(); handleSearch(); } if (e.key === 'ArrowUp' && !query) { // Find the last user message and put it into the input const lastUserNav = [...messages].reverse().find((m) => m.role === 'user'); if (lastUserNav) { e.preventDefault(); setQuery(lastUserNav.content); } } }; const handleCopy = (content: string, index?: number) => { navigator.clipboard.writeText(content); if (index !== undefined) { setCopiedIndex(index); setTimeout(() => setCopiedIndex(null), 2000); } else { setCopiedAll(true); setTimeout(() => setCopiedAll(false), 2000); } }; const handleCopyChat = () => { const fullChat = messages .map((m) => `${m.role === 'user' ? 'Du' : 'Ohm'}:\n${m.content}`) .join('\n\n'); handleCopy(fullChat); }; if (!isOpen) return null; const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; return (
{/* Animated backdrop */}
{/* ── Glassmorphism container ── */}
{/* ── Header ── */}

Ohm

{isLoading ? 'Denkt nach...' : error ? 'Fehler aufgetreten' : 'Online'}

{messages.length > 0 && ( )}
{/* ── Chat Area ── */}
{/* Empty state */} {messages.length === 0 && !isLoading && !error && (

Wie kann ich helfen?

Beschreibe dein Projekt, frag nach bestimmten Kabeln, oder nenne mir deine Anforderungen.

{/* Quick prompts */}
{['Windpark 33kV Verkabelung', 'NYCWY 4x185', 'Erdkabel fΓΌr Solarpark'].map( (prompt) => ( ), )}
)} {/* Messages */} {messages.map((msg, index) => (
{/* Copy Button */} {msg.role === 'assistant' && (
Ohm
)}
{msg.role === 'assistant' ? ( {msg.content} ) : (

{msg.content}

)}
{/* Timestamp */} {!msg.products?.length && (

{new Date(msg.timestamp).toLocaleTimeString('de', { hour: '2-digit', minute: '2-digit', })}

)} {/* Product cards */} {msg.role === 'assistant' && msg.products && msg.products.length > 0 && (

Empfohlene Produkte

{msg.products.map((product, idx) => ( { onClose(); trackEvent(AnalyticsEvents.BUTTON_CLICK, { target: product.slug, location: 'ai_chat', }); }} className="group flex items-center justify-between bg-white/[0.04] hover:bg-white/[0.08] border border-white/[0.06] hover:border-accent/30 rounded-xl px-4 py-3 transition-all duration-300" style={{ animation: `chatFadeIn 0.3s ease-out ${idx * 0.1}s both` }} >

{product.sku}

{product.title}
))}
)}
))} {/* Loading indicator */} {isLoading && (

{loadingText}

{[0, 1, 2].map((i) => (
))}
)} {/* Error */} {error && (

Da ist was schiefgelaufen 😬

{error}

)}
{/* ── Input Area ── */}
setQuery(e.target.value)} onKeyDown={onKeyDown} placeholder="Nachricht eingeben..." className="flex-1 bg-transparent border-none text-white text-sm md:text-base px-5 py-4 focus:outline-none placeholder:text-white/20" disabled={isLoading} tabIndex={1} autoFocus /> setHoneypot(e.target.value)} tabIndex={-1} autoComplete="off" aria-hidden="true" />
Enter zum Senden Β· Esc zum Schließen Β· πŸ›‘οΈ DSGVO-konform Β· EU-Server
{/* ── Keyframe animations ── */}
); }