import React, { useState, useRef, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Bot, X, Send, AlertCircle, ChevronDown, Pin, Sparkles, Trash2 } from 'lucide-react'; import { getProjectChatHistory, sendProjectChatMessage, getProjectSections, updateProjectSection, clearProjectChatHistory } from '../../api/client'; import toast from 'react-hot-toast'; interface ProjectChatPanelProps { projectId: string; projectName?: string; activeSectionId?: string; activeSectionTitle?: string; } interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; created_at?: string; isError?: boolean; originalInput?: string; } const ProjectChatPanel: React.FC = ({ projectId, projectName, activeSectionId, activeSectionTitle }) => { const [isOpen, setIsOpen] = useState(false); const [isPinned, setIsPinned] = useState(false); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; useEffect(() => { const fetchHistory = async () => { try { const history = await getProjectChatHistory(projectId); if (history && history.length > 0) { setMessages(history); } } catch (err) { console.error("Failed to load chat history", err); } }; if (isOpen || isPinned) { fetchHistory(); } }, [projectId, isOpen, isPinned]); useEffect(() => { scrollToBottom(); }, [messages, isOpen, isLoading]); useEffect(() => { const handleCmdJ = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'j') { e.preventDefault(); setIsOpen(prev => !prev); } }; window.addEventListener('keydown', handleCmdJ); return () => window.removeEventListener('keydown', handleCmdJ); }, []); const handleClearHistory = async () => { if (!window.confirm("Czy na pewno chcesz trwale usunąć historię rozmów z asystentem dla tego projektu?")) { return; } setIsLoading(true); try { await clearProjectChatHistory(projectId); setMessages([]); toast.success("Historia została pomyślnie wyczyszczona."); } catch (error) { console.error(error); toast.error("Nie udało się usunąć historii czatu."); } finally { setIsLoading(false); } }; const handleSendRef = useRef(null); useEffect(() => { const handleOpenChat = (e: any) => { setIsOpen(true); const { prefillMessage, autoSend } = e.detail || {}; if (prefillMessage) { if (autoSend) { setTimeout(() => { if (handleSendRef.current) handleSendRef.current(prefillMessage); }, 50); } else { setInput(prefillMessage); } } }; window.addEventListener('open-project-chat', handleOpenChat); return () => window.removeEventListener('open-project-chat', handleOpenChat); }, []); const handleApplySuggestion = async (text: string, mode: 'insert' | 'replace', sectionTarget?: string) => { if (mode === 'insert') { if (sectionTarget && sectionTarget !== 'Aktywna sekcja') { window.dispatchEvent(new CustomEvent('navigate-to-section', { detail: { sectionType: sectionTarget } })); } else { window.dispatchEvent(new CustomEvent('switch-to-sections-tab')); } // Wait a bit for the SectionEditor to mount and the tab to switch setTimeout(() => { window.dispatchEvent(new CustomEvent('insert-suggestion', { detail: { text, mode } })); toast.success('Sugestia została przesłana do edytora.'); }, 400); return; } setIsLoading(true); try { let targetId = null; let foundSectionType = null; const sections = await getProjectSections(projectId); const findSection = (query: string) => { if (!query) return null; const q = query.trim().toLowerCase(); if (!q) return null; let bestMatch = null; let highestScore = 0; sections.forEach((s: any) => { const st = s.section_type?.toLowerCase() || ""; const stClean = s.section_type?.replace(/_/g, ' ').toLowerCase() || ""; const title = s.title?.trim().toLowerCase() || ""; let score = 0; if (s.id === query) { score = 200; } else if (st === q || title === q) { score = 100; } else if (stClean === q) { score = 90; } else if ((st && q.includes(st)) || (stClean && q.includes(stClean))) { score = 80; } else if (title && q && title.includes(q)) { score = 70; } else if (q && title && q.includes(title)) { score = title.length >= 4 ? 60 : 0; } else if (stClean && q && stClean.includes(q)) { score = 50; } else if (q && stClean && q.includes(stClean)) { score = stClean.length >= 4 ? 40 : 0; } else if (q && title) { const targetWords = q.split(/\s+/).filter((w: string) => w.length >= 4); for (const w of targetWords) { if (title.includes(w) || (stClean && stClean.includes(w))) { score = 30; break; } } } if (score > highestScore) { highestScore = score; bestMatch = s; } }); return bestMatch; }; let matchingSection: any = null; if (sectionTarget && sectionTarget !== 'Aktywna sekcja' && sectionTarget !== activeSectionTitle) { matchingSection = findSection(sectionTarget); } if (!matchingSection && activeSectionId) { matchingSection = findSection(activeSectionId); } if (matchingSection) { targetId = matchingSection.id; foundSectionType = matchingSection.section_type; } if (targetId) { await updateProjectSection(projectId, targetId, text); window.dispatchEvent(new CustomEvent('external-section-update', { detail: { sectionType: foundSectionType, content: text } })); window.dispatchEvent(new CustomEvent('refresh-sections')); toast.success(`Zawartość sekcji została pomyślnie nadpisana w bazie.`); window.dispatchEvent(new CustomEvent('navigate-to-section', { detail: { sectionType: foundSectionType } })); } else { toast.error("Nie można ustalić sekcji docelowej upewnij się, jaką sekcję edytujesz."); } } catch (e) { console.error(e); toast.error("Wystąpił błąd podczas nadpisywania sekcji w bazie."); } finally { setIsLoading(false); } }; const renderMessageContent = (content: string) => { const parts = content.split(/(]*>[\s\S]*?<\/SUGGESTION>)/); return ( <> {parts.map((part, i) => { const match = part.match(/([\s\S]*?)<\/SUGGESTION>/); if (match) { const sectionName = match[1] || activeSectionTitle || "Aktywna sekcja"; const suggestionText = match[2].trim(); return (
SUGESTIA
Dla sekcji: {sectionName}
{suggestionText}
); } return {part}; })} ); }; const handleSend = async (overrideInput?: string) => { const textToUse = typeof overrideInput === 'string' ? overrideInput : input; if (!textToUse.trim() || isLoading) return; const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content: textToUse }; setMessages(prev => [...prev, userMsg]); if (typeof overrideInput !== 'string') { setInput(''); } setIsLoading(true); try { const agentMsg = await sendProjectChatMessage(projectId, userMsg.content, activeSectionId, activeSectionTitle); setMessages(prev => [...prev, agentMsg]); } catch (error: any) { console.error(error); toast.error("Nie udało się pobrać odpowiedzi po kilku próbach."); const errorMsg: ChatMessage = { id: (Date.now() + 1).toString(), role: 'assistant', content: "Przepraszam, napotkałem problem techniczny (np. przeciążenie serwerów LLM). System podjął próby automatycznej naprawy, ale niestety nie powiodły się one w tej chwili.\n\nProszę, spróbuj ponownie za chwilę.", isError: true, originalInput: textToUse }; setMessages(prev => [...prev, errorMsg]); } finally { setIsLoading(false); } }; useEffect(() => { handleSendRef.current = handleSend; }, [handleSend]); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; return (
{isOpen && ( {/* HEADER */}

Asystent Projektu (Ctrl+J)

{projectName && (
{projectName}
)}
{!isPinned && ( )}
{/* MESSAGES */}
{messages.length === 0 ? (
Cześć! Jestem Twoim Asystentem Projektu.

Mogę pomóc w pisaniu sekcji, wyjaśnianiu regulaminów lub sugerowaniu poprawek.

W czym mogę Ci pomóc?
) : ( messages.map((msg) => (
{msg.role === 'assistant' ? renderMessageContent(msg.content) : msg.content} {msg.role === 'assistant' && !msg.isError && (
)} {msg.isError && msg.originalInput && (
)}
)) )} {isLoading && (
Przetwarzanie zapytania... Prosimy o chwilę cierpliwości.
)}
{/* INPUT */}