'use client'; import { useState, useRef, useEffect, useMemo } from 'react'; import { Box, Typography, Paper, Avatar, IconButton, Chip, useMediaQuery, Tooltip, Fab, Zoom, } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { motion, AnimatePresence } from 'framer-motion'; import { User, Bot, Sparkles, ChevronDown, RotateCcw, Copy, Check, AlertCircle, } from 'lucide-react'; import Logo from './Logo'; import { useChatStore, type MessageAttachment } from '../services/chat-store'; import { useAuthStore } from '../services/auth-store'; import { useToolsStore } from '../services/tools-store'; import { useReservationStore } from '../services/reservation-store'; import { MOCK_USERS } from '../services/mock-data'; import { fileToDataUrl } from '../services/file-utils'; import AIInput from './AIInput'; import MarkdownMessage from './MarkdownMessage'; interface ChatApiResponse { content: string; mode?: 'live' | 'mock' | 'mock-fallback'; matchedTools?: unknown[]; sources?: string[]; error?: string; } const SUGGESTED_PROMPTS = [ { icon: '🔧', text: 'Wie repariere ich ein tropfendes Fahrrad-Ventil?' }, { icon: '🪛', text: 'Welches Werkzeug brauche ich zum Bohren in Beton?' }, { icon: '🖨️', text: 'Tipps für meinen ersten 3D-Druck mit PLA' }, { icon: '📅', text: 'Wann ist das Repair Café geöffnet?' }, ]; function formatTime(ts: number) { return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', }); } export default function ChatWindow() { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('md')); const { sessions, activeSessionId, addMessage, createSession } = useChatStore(); const [isLoading, setIsLoading] = useState(false); const [lastMode, setLastMode] = useState(null); const [showScrollDown, setShowScrollDown] = useState(false); const [copiedId, setCopiedId] = useState(null); const scrollRef = useRef(null); const bottomRef = useRef(null); const activeSession = useMemo( () => sessions.find((s) => s.id === activeSessionId) ?? null, [sessions, activeSessionId] ); useEffect(() => { if (!activeSession || activeSession.messages.length === 0) return; const last = activeSession.messages[activeSession.messages.length - 1]; if (last.role === 'user' && !isLoading) { void sendToApi(activeSession.id); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [activeSession?.messages.length]); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); }, [activeSession?.messages.length, isLoading]); useEffect(() => { const el = scrollRef.current; if (!el) return; const onScroll = () => { const distance = el.scrollHeight - el.scrollTop - el.clientHeight; setShowScrollDown(distance > 200); }; el.addEventListener('scroll', onScroll); return () => el.removeEventListener('scroll', onScroll); }, [activeSessionId]); const sendToApi = async (sessionId: string) => { const current = useChatStore.getState().sessions.find((s) => s.id === sessionId); if (!current) return; setIsLoading(true); try { const history = current.messages.map((m) => ({ role: m.role, content: m.content, attachments: m.attachments, })); const user = useAuthStore.getState().user; const tools = useToolsStore.getState().tools; const reservations = useReservationStore.getState().reservations; const context = { userRole: user?.role, username: user?.username, tools, reservations, users: MOCK_USERS.map((u) => ({ id: u.id, username: u.username, role: u.role })), }; const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: history, context }), }); const data = (await response.json()) as ChatApiResponse; if (data.error || !data.content) throw new Error(data.error || 'Empty response'); setLastMode(data.mode ?? null); addMessage(sessionId, { role: 'assistant', content: data.content }); } catch (error) { const msg = error instanceof Error ? error.message : 'Unbekannter Fehler'; addMessage(sessionId, { role: 'assistant', content: `⚠️ Es gab ein Problem bei der Verbindung: ${msg}`, }); } finally { setIsLoading(false); } }; const handleSend = async (content: string, files?: File[]) => { const hasFiles = Boolean(files && files.length > 0); if (!content.trim() && !hasFiles) return; let targetId = activeSessionId; if (!targetId) targetId = createSession(content.slice(0, 30) || 'Bild-Analyse'); let attachments: MessageAttachment[] | undefined; if (hasFiles) { const processed = await Promise.all( files!.map(async (file) => { const { dataUrl, type, size } = await fileToDataUrl(file); return { name: file.name, type, dataUrl, size }; }) ); attachments = processed; } addMessage(targetId, { role: 'user', content: content.trim(), attachments, }); }; const handleSuggested = (text: string) => handleSend(text); const handleCopy = async (id: string, text: string) => { try { await navigator.clipboard.writeText(text); setCopiedId(id); setTimeout(() => setCopiedId((cur) => (cur === id ? null : cur)), 1500); } catch { /* ignore */ } }; const handleRetry = () => { if (!activeSession || activeSession.messages.length === 0 || !activeSessionId) return; const lastUser = [...activeSession.messages].reverse().find((m) => m.role === 'user'); if (lastUser) void sendToApi(activeSessionId); }; const scrollToBottom = () => bottomRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }); if (!activeSession) return ; const isEmpty = activeSession.messages.length === 0; return ( {/* Header */} {activeSession.title} {activeSession.messages.length} Nachrichten · PatchPilot-KI {/* Messages */} {isEmpty && ( Worüber sollen wir reden? Wähle einen Vorschlag unten oder schreibe deine Frage. )} {activeSession.messages.map((msg) => { const isUser = msg.role === 'user'; return ( {isUser ? : } {msg.attachments && msg.attachments.length > 0 && ( {msg.attachments.map((att, i) => att.type.startsWith('image/') ? ( ) : ( 📎 {att.name} ) )} )} {msg.content && (isUser ? ( {msg.content} ) : ( ))} {formatTime(msg.timestamp)} {!isUser && ( handleCopy(msg.id, msg.content)} sx={{ p: 0.25, color: 'text.secondary' }} > {copiedId === msg.id ? : } )} ); })} {isLoading && ( )}
{/* Scroll-to-bottom */} {/* Footer */} {isEmpty && ( {SUGGESTED_PROMPTS.map((p) => ( handleSuggested(p.text)} clickable variant="outlined" label={`${p.icon} ${p.text}`} sx={{ borderRadius: 2, height: 32, fontSize: '0.78rem', borderStyle: 'dashed', '&:hover': { borderColor: 'primary.main', bgcolor: 'action.hover', borderStyle: 'solid', }, }} /> ))} )} {!isMobile && ( )} ); } function EmptyState({ onPick }: { onPick: (text: string) => void }) { return ( Womit darf ich helfen? Frag mich nach Werkzeugen, Reparatur-Tipps oder beschreibe dein Problem. {SUGGESTED_PROMPTS.map((p) => ( onPick(p.text)} sx={{ p: 2, cursor: 'pointer', textAlign: 'left', borderRadius: 2.5, transition: 'all 0.2s', borderStyle: 'dashed', '&:hover': { borderColor: 'primary.main', bgcolor: 'action.hover', transform: 'translateY(-2px)', borderStyle: 'solid', }, }} > {p.icon}  {p.text} ))} ); } function ModeBadge({ mode }: { mode: ChatApiResponse['mode'] | null }) { if (!mode) return null; if (mode === 'live') { return ( ); } if (mode === 'mock-fallback') { return ( } label="Fallback" sx={{ bgcolor: 'warning.main', color: 'white', fontWeight: 700, fontSize: '0.65rem', height: 22, }} /> ); } return ( ); }