Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from 'react'; | |
| import ReactMarkdown from 'react-markdown'; | |
| import { Play, Square, FileText, CheckCircle2, AlertCircle, Loader2, Sparkles, ChevronDown, ChevronRight } from 'lucide-react'; | |
| import toast from 'react-hot-toast'; | |
| interface CompletedSection { | |
| title: string; | |
| content: string; | |
| index: number; | |
| } | |
| interface AIGeneratorPanelProps { | |
| projectId: string; | |
| onCompleted: () => void; | |
| } | |
| const BACKEND_URL = import.meta.env.VITE_API_URL || 'http://localhost:8001'; | |
| const AIGeneratorPanel: React.FC<AIGeneratorPanelProps> = ({ projectId, onCompleted }) => { | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const [currentStatus, setCurrentStatus] = useState<string>('Gotowy do rozpoczęcia automatycznego generowania wniosku.'); | |
| const [currentSection, setCurrentSection] = useState<string | null>(null); | |
| const [completedSections, setCompletedSections] = useState<CompletedSection[]>([]); | |
| const [expandedSection, setExpandedSection] = useState<number | null>(null); | |
| const [fullDocument, setFullDocument] = useState<string | null>(null); | |
| const [error, setError] = useState<string | null>(null); | |
| const [progress, setProgress] = useState(0); | |
| const [missingDataQuestion, setMissingDataQuestion] = useState<string | null>(null); | |
| const [userResponse, setUserResponse] = useState<string>(''); | |
| const eventSourceRef = useRef<EventSource | null>(null); | |
| const endRef = useRef<HTMLDivElement | null>(null); | |
| useEffect(() => { | |
| if (endRef.current && isGenerating) { | |
| endRef.current.scrollIntoView({ behavior: 'smooth' }); | |
| } | |
| }, [completedSections, isGenerating]); | |
| useEffect(() => { | |
| return () => { eventSourceRef.current?.close(); }; | |
| }, []); | |
| const handleStartGeneration = () => { | |
| if (eventSourceRef.current) eventSourceRef.current.close(); | |
| // Użyj toast zamiast window.confirm dla spójnego UX | |
| toast((t) => ( | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem', minWidth: '280px' }}> | |
| <div style={{ fontWeight: 700, color: '#fff', fontSize: '0.95rem' }}> | |
| 🚀 Uruchomić Autopilota AI? | |
| </div> | |
| <div style={{ color: 'rgba(255,255,255,0.7)', fontSize: '0.85rem', lineHeight: 1.4 }}> | |
| Agent wygeneruje wszystkie sekcje wniosku. Proces zajmie kilka minut — nie zamykaj karty. | |
| </div> | |
| <div style={{ display: 'flex', gap: '0.6rem' }}> | |
| <button | |
| onClick={() => { toast.dismiss(t.id); startGeneration(); }} | |
| style={{ flex: 1, padding: '0.5rem', background: '#8b5cf6', color: '#fff', border: 'none', borderRadius: '6px', fontWeight: 600, cursor: 'pointer', fontSize: '0.85rem' }} | |
| > | |
| Tak, generuj | |
| </button> | |
| <button | |
| onClick={() => toast.dismiss(t.id)} | |
| style={{ flex: 1, padding: '0.5rem', background: 'rgba(255,255,255,0.1)', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '0.85rem' }} | |
| > | |
| Anuluj | |
| </button> | |
| </div> | |
| </div> | |
| ), { duration: Infinity, style: { background: '#1e1b4b', border: '1px solid rgba(139,92,246,0.3)' } }); | |
| }; | |
| const startGeneration = (resume: boolean = false) => { | |
| setIsGenerating(true); | |
| if (!resume) { | |
| setError(null); | |
| setCompletedSections([]); | |
| setFullDocument(null); | |
| setCurrentSection(null); | |
| setProgress(0); | |
| } | |
| setCurrentStatus(resume ? 'Wznawianie połączenia z agentem LangGraph...' : 'Nawiązywanie połączenia z agentem LangGraph...'); | |
| const token = localStorage.getItem('token') || ''; | |
| const url = `${BACKEND_URL}/api/generator/stream?project_id=${projectId}&resume=${resume}&token=${encodeURIComponent(token)}`; | |
| const source = new EventSource(url); | |
| eventSourceRef.current = source; | |
| // Agent zaczął pisać konkretną sekcję | |
| source.addEventListener('section_started', (event) => { | |
| const title = event.data; | |
| setCurrentSection(title); | |
| setCurrentStatus(`Agent pisze: „${title}"...`); | |
| }); | |
| // Sekcja ukończona — dodaj do listy | |
| source.addEventListener('section_completed', (event) => { | |
| try { | |
| const data: CompletedSection = JSON.parse(event.data); | |
| setCompletedSections(prev => { | |
| const updated = [...prev, data]; | |
| // Szacunkowy postęp (SMART ma 16 sekcji) | |
| setProgress(Math.min(95, Math.round((updated.length / 16) * 95))); | |
| return updated; | |
| }); | |
| setCurrentSection(null); | |
| toast.success(`✅ Ukończono: ${data.title}`, { duration: 3000 }); | |
| } catch { | |
| console.error('SSE parse error: section_completed', event.data); | |
| } | |
| }); | |
| // Wszystko gotowe | |
| source.addEventListener('document_done', (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| setFullDocument(data.full_content); | |
| setProgress(100); | |
| setCurrentStatus('Wniosek gotowy! Sekcje zostały zapisane w projekcie.'); | |
| setIsGenerating(false); | |
| setCurrentSection(null); | |
| source.close(); | |
| toast.success('🎉 Autopilot AI zakończył generowanie wniosku!', { duration: 6000 }); | |
| // Odśwież dane projektu (sekcje zapisane przez backend) | |
| setTimeout(() => onCompleted(), 500); | |
| } catch { | |
| console.error('SSE parse error: document_done', event.data); | |
| } | |
| }); | |
| // Utrata połączenia | |
| source.addEventListener('waiting_for_user_input', (event) => { | |
| try { | |
| const data = JSON.parse(event.data); | |
| if (data.status === 'WAITING_FOR_USER_INPUT') { | |
| setMissingDataQuestion(data.missing_data_question); | |
| setCurrentStatus('Wstrzymano — oczekiwanie na Twoją odpowiedź'); | |
| source.close(); | |
| } | |
| } catch { | |
| console.error('SSE parse error: waiting_for_user_input', event.data); | |
| } | |
| }); | |
| // Błąd z backendu | |
| source.addEventListener('error', (event: any) => { | |
| let errMsg = 'Nieoczekiwany błąd agenta.'; | |
| if (event.data) { | |
| try { | |
| const parsed = JSON.parse(event.data); | |
| errMsg = typeof parsed.detail === 'string' ? parsed.detail : JSON.stringify(parsed.detail || parsed); | |
| } catch { | |
| errMsg = typeof event.data === 'string' ? event.data : JSON.stringify(event.data); | |
| } | |
| } | |
| if (typeof errMsg !== 'string') { | |
| errMsg = String(errMsg); | |
| } | |
| setError(errMsg); | |
| toast.error(`Zatrzymano: ${errMsg}`); | |
| setIsGenerating(false); | |
| setCurrentSection(null); | |
| source.close(); | |
| setCurrentStatus('Generowanie zatrzymane z powodu błędu.'); | |
| }); | |
| // Utrata połączenia | |
| source.onerror = () => { | |
| if (isGenerating && !fullDocument) { | |
| // EventSource automatycznie próbuje się ponownie — nie przeszkadzaj | |
| setCurrentStatus('Łączenie ponowne ze strumieniem...'); | |
| } | |
| }; | |
| }; | |
| const handleStop = () => { | |
| eventSourceRef.current?.close(); | |
| setIsGenerating(false); | |
| setCurrentSection(null); | |
| setCurrentStatus('Przerwano przez użytkownika.'); | |
| toast('Zatrzymano Agenta AI.', { icon: '🛑' }); | |
| }; | |
| const submitUserResponse = async () => { | |
| try { | |
| const token = localStorage.getItem('token') || ''; | |
| const res = await fetch(`${BACKEND_URL}/api/generator/resume`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Authorization': `Bearer ${token}` | |
| }, | |
| body: JSON.stringify({ | |
| project_id: projectId, | |
| user_response: userResponse | |
| }) | |
| }); | |
| if (!res.ok) throw new Error('Błąd wysyłania odpowiedzi'); | |
| setMissingDataQuestion(null); | |
| setUserResponse(''); | |
| // Wznów strumień | |
| startGeneration(true); | |
| } catch (err: any) { | |
| toast.error(err.message || 'Wystąpił błąd'); | |
| } | |
| }; | |
| const statusColor = error ? 'var(--accent-red)' : isGenerating ? 'var(--accent-purple)' : fullDocument ? 'var(--accent-green)' : 'var(--text-secondary)'; | |
| return ( | |
| <div style={{ maxWidth: '960px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> | |
| {/* === HEADER CARD === */} | |
| <div className="glass-card" style={{ padding: '2rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.5rem' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}> | |
| <div style={{ width: '48px', height: '48px', background: 'rgba(139,92,246,0.12)', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-purple)', flexShrink: 0 }}> | |
| <Sparkles size={24} /> | |
| </div> | |
| <div> | |
| <h2 style={{ fontSize: '1.35rem', fontWeight: 800, margin: 0, color: 'var(--text-primary)' }}> | |
| Autopilot AI — Generator Wniosku | |
| </h2> | |
| <p style={{ color: 'var(--text-secondary)', margin: '0.2rem 0 0', fontSize: '0.875rem' }}> | |
| Agent LangGraph napisze wszystkie sekcje wniosku jednocześnie, korzystając z bazy wiedzy RAG. | |
| </p> | |
| </div> | |
| </div> | |
| <div style={{ flexShrink: 0 }}> | |
| {!isGenerating ? ( | |
| <button | |
| onClick={handleStartGeneration} | |
| className="btn hover-lift" | |
| style={{ background: 'var(--accent-purple)', color: 'white', fontWeight: 700, display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.65rem 1.4rem' }} | |
| > | |
| <Play size={17} /> Uruchom Agenta | |
| </button> | |
| ) : ( | |
| <button | |
| onClick={handleStop} | |
| className="btn" | |
| style={{ background: 'rgba(239,68,68,0.1)', color: 'var(--accent-red)', border: '1px solid rgba(239,68,68,0.25)', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '0.5rem', padding: '0.65rem 1.4rem' }} | |
| > | |
| <Square size={17} /> Przerwij | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| {/* Status bar */} | |
| <div style={{ background: 'rgba(0,0,0,0.2)', borderRadius: '10px', padding: '0.9rem 1.2rem', border: '1px solid rgba(255,255,255,0.06)', display: 'flex', alignItems: 'center', gap: '0.9rem' }}> | |
| {isGenerating | |
| ? <Loader2 size={19} style={{ color: 'var(--accent-purple)', animation: 'spin 1s linear infinite', flexShrink: 0 }} /> | |
| : error | |
| ? <AlertCircle size={19} color="var(--accent-red)" style={{ flexShrink: 0 }} /> | |
| : <CheckCircle2 size={19} color={fullDocument ? 'var(--accent-green)' : 'var(--text-muted)'} style={{ flexShrink: 0 }} /> | |
| } | |
| <span style={{ color: statusColor, fontSize: '0.92rem', fontWeight: 500 }}>{currentStatus}</span> | |
| </div> | |
| {/* Progress bar */} | |
| {(isGenerating || progress > 0) && ( | |
| <div style={{ marginTop: '1rem' }}> | |
| <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.4rem', fontSize: '0.8rem', color: 'var(--text-muted)' }}> | |
| <span>{currentSection ? `Pisze: ${currentSection}` : completedSections.length > 0 ? `${completedSections.length} sekcji gotowych` : 'Inicjalizacja...'}</span> | |
| <span>{progress}%</span> | |
| </div> | |
| <div style={{ height: '6px', background: 'rgba(255,255,255,0.06)', borderRadius: '3px', overflow: 'hidden' }}> | |
| <div style={{ height: '100%', width: `${progress}%`, background: error ? 'var(--accent-red)' : progress === 100 ? 'var(--accent-green)' : 'var(--accent-purple)', borderRadius: '3px', transition: 'width 0.5s ease' }} /> | |
| </div> | |
| </div> | |
| )} | |
| {/* Error box */} | |
| {error && ( | |
| <div style={{ marginTop: '1rem', padding: '0.9rem 1.1rem', background: 'rgba(239,68,68,0.08)', borderLeft: '3px solid var(--accent-red)', borderRadius: '6px', display: 'flex', alignItems: 'flex-start', gap: '0.75rem', color: '#FECACA', fontSize: '0.875rem' }}> | |
| <AlertCircle size={17} style={{ flexShrink: 0, marginTop: '1px' }} /> | |
| <div><strong>Błąd agenta:</strong> {error}</div> | |
| </div> | |
| )} | |
| </div> | |
| {/* === COMPLETED SECTIONS === */} | |
| {completedSections.length > 0 && ( | |
| <div className="glass-card" style={{ overflow: 'hidden' }}> | |
| <div style={{ padding: '1.2rem 1.5rem', borderBottom: '1px solid rgba(255,255,255,0.05)', display: 'flex', alignItems: 'center', gap: '0.6rem' }}> | |
| <FileText size={17} color="var(--accent-blue)" /> | |
| <h3 style={{ margin: 0, fontSize: '1rem', fontWeight: 700 }}> | |
| Wygenerowane sekcje ({completedSections.length}) | |
| </h3> | |
| {isGenerating && ( | |
| <span style={{ marginLeft: 'auto', fontSize: '0.8rem', color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: '0.4rem' }}> | |
| <Loader2 size={13} style={{ animation: 'spin 1s linear infinite' }} /> | |
| Agent pracuje... | |
| </span> | |
| )} | |
| </div> | |
| <div style={{ maxHeight: '520px', overflowY: 'auto', padding: '0.5rem 0' }}> | |
| {completedSections.map((sec, idx) => ( | |
| <div key={idx} style={{ borderBottom: idx < completedSections.length - 1 ? '1px solid rgba(255,255,255,0.04)' : 'none' }}> | |
| {/* Accordion header */} | |
| <button | |
| onClick={() => setExpandedSection(expandedSection === idx ? null : idx)} | |
| style={{ width: '100%', padding: '1rem 1.5rem', background: 'transparent', border: 'none', display: 'flex', alignItems: 'center', gap: '0.8rem', cursor: 'pointer', textAlign: 'left' }} | |
| > | |
| <div style={{ width: '24px', height: '24px', borderRadius: '50%', background: 'rgba(16,185,129,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> | |
| <CheckCircle2 size={14} color="var(--accent-green)" /> | |
| </div> | |
| <span style={{ flex: 1, fontWeight: 600, fontSize: '0.95rem', color: 'var(--text-primary)' }}>{sec.title}</span> | |
| <span style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginRight: '0.5rem' }}> | |
| {sec.content.length > 0 ? `${Math.round(sec.content.length / 5)} słów` : ''} | |
| </span> | |
| {expandedSection === idx | |
| ? <ChevronDown size={16} color="var(--text-muted)" /> | |
| : <ChevronRight size={16} color="var(--text-muted)" /> | |
| } | |
| </button> | |
| {/* Accordion content */} | |
| {expandedSection === idx && ( | |
| <div style={{ padding: '0 1.5rem 1.5rem 3.5rem' }}> | |
| <div className="markdown-body" style={{ color: 'var(--text-secondary)', fontSize: '0.9rem', lineHeight: 1.75 }}> | |
| <ReactMarkdown>{sec.content}</ReactMarkdown> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ))} | |
| {/* Placeholder sekcji w trakcie pisania */} | |
| {isGenerating && currentSection && ( | |
| <div style={{ padding: '1rem 1.5rem', display: 'flex', alignItems: 'center', gap: '0.8rem' }}> | |
| <div style={{ width: '24px', height: '24px', borderRadius: '50%', background: 'rgba(139,92,246,0.15)', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}> | |
| <Loader2 size={14} color="var(--accent-purple)" style={{ animation: 'spin 1s linear infinite' }} /> | |
| </div> | |
| <span style={{ color: 'var(--text-muted)', fontSize: '0.9rem', fontStyle: 'italic' }}> | |
| Pisze: {currentSection}... | |
| </span> | |
| </div> | |
| )} | |
| <div ref={endRef} /> | |
| </div> | |
| </div> | |
| )} | |
| {/* === SUCCESS BANNER === */} | |
| {fullDocument && ( | |
| <div className="glass-card" style={{ padding: '1.5rem 2rem', border: '1px solid rgba(16,185,129,0.25)', background: 'rgba(16,185,129,0.04)', display: 'flex', alignItems: 'center', gap: '1rem' }}> | |
| <CheckCircle2 size={28} color="var(--accent-green)" style={{ flexShrink: 0 }} /> | |
| <div> | |
| <p style={{ margin: 0, fontWeight: 700, color: 'var(--accent-green)', fontSize: '1.05rem' }}> | |
| Wniosek wygenerowany pomyślnie! | |
| </p> | |
| <p style={{ margin: '0.2rem 0 0', color: 'var(--text-secondary)', fontSize: '0.875rem' }}> | |
| Sekcje zostały zapisane w projekcie. Przejdź do zakładki <strong>Sekcje wniosku</strong>, aby je przejrzeć i edytować, lub do <strong>Wniosku Końcowego</strong>, by skompilować i wyeksportować. | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| {/* === INFO CARD (gdy nic nie uruchomiono) === */} | |
| {!isGenerating && !fullDocument && completedSections.length === 0 && !error && ( | |
| <div className="glass-card" style={{ padding: '2rem', border: '1px solid rgba(255,255,255,0.05)' }}> | |
| <h3 style={{ margin: '0 0 1rem', fontSize: '1rem', fontWeight: 700, color: 'var(--text-primary)' }}> | |
| Jak działa Autopilot AI? | |
| </h3> | |
| <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> | |
| {[ | |
| { n: '1', t: 'Planowanie', d: 'Agent dobiera odpowiednie sekcje dla Twojego programu dotacyjnego.' }, | |
| { n: '2', t: 'Pobieranie kontekstu', d: 'Dla każdej sekcji system przeszukuje bazę wiedzy RAG z regulaminami i wytycznymi.' }, | |
| { n: '3', t: 'Generowanie', d: 'LLM pisze treść każdej sekcji w formacie gotowym do edycji i eksportu.' }, | |
| { n: '4', t: 'Zapis do projektu', d: 'Wygenerowane sekcje są automatycznie zapisywane — możesz je od razu edytować.' }, | |
| ].map(step => ( | |
| <div key={step.n} style={{ display: 'flex', alignItems: 'flex-start', gap: '1rem' }}> | |
| <div style={{ width: '28px', height: '28px', borderRadius: '50%', background: 'rgba(139,92,246,0.12)', border: '1px solid rgba(139,92,246,0.3)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--accent-purple)', fontSize: '0.8rem', fontWeight: 700, flexShrink: 0 }}> | |
| {step.n} | |
| </div> | |
| <div> | |
| <div style={{ fontWeight: 600, fontSize: '0.9rem', color: 'var(--text-primary)', marginBottom: '0.1rem' }}>{step.t}</div> | |
| <div style={{ fontSize: '0.825rem', color: 'var(--text-muted)' }}>{step.d}</div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div style={{ marginTop: '1.5rem', padding: '0.9rem 1.1rem', background: 'rgba(234,179,8,0.06)', borderLeft: '3px solid rgba(234,179,8,0.5)', borderRadius: '6px', fontSize: '0.825rem', color: 'rgba(253,224,71,0.85)' }}> | |
| ⚠️ Treść wygenerowana przez AI na podstawie bazy wiedzy. Zalecana weryfikacja przez doradcę lub prawnika przed wysłaniem wniosku. | |
| </div> | |
| </div> | |
| )} | |
| {/* === MISSING DATA MODAL (HIL) === */} | |
| {missingDataQuestion && ( | |
| <div style={{ | |
| position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, | |
| background: 'rgba(0,0,0,0.7)', zIndex: 1000, | |
| display: 'flex', alignItems: 'center', justifyContent: 'center' | |
| }}> | |
| <div className="glass-card" style={{ width: '550px', maxWidth: '90vw', padding: '2rem', display: 'flex', flexDirection: 'column', gap: '1rem', border: '1px solid var(--accent-purple)' }}> | |
| <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> | |
| <Sparkles size={24} color="var(--accent-purple)" /> | |
| <h3 style={{ margin: 0, color: 'var(--text-primary)', fontSize: '1.2rem', fontWeight: 700 }}> | |
| Agent potrzebuje informacji | |
| </h3> | |
| </div> | |
| <p style={{ color: 'var(--text-secondary)', fontSize: '0.95rem', lineHeight: 1.5, background: 'rgba(0,0,0,0.2)', padding: '1rem', borderRadius: '8px', borderLeft: '3px solid var(--accent-purple)' }}> | |
| {missingDataQuestion} | |
| </p> | |
| <textarea | |
| value={userResponse} | |
| onChange={e => setUserResponse(e.target.value)} | |
| placeholder="Wpisz odpowiedź tutaj, aby agent mógł kontynuować pisanie..." | |
| style={{ | |
| width: '100%', minHeight: '120px', padding: '0.85rem', | |
| background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.15)', | |
| color: 'white', borderRadius: '8px', fontSize: '0.9rem', | |
| resize: 'vertical' | |
| }} | |
| /> | |
| <div style={{ display: 'flex', gap: '1rem', justifyContent: 'flex-end', marginTop: '0.5rem' }}> | |
| <button | |
| onClick={() => { | |
| setMissingDataQuestion(null); | |
| handleStop(); | |
| }} | |
| className="btn" style={{ background: 'transparent', color: 'var(--text-muted)', border: '1px solid rgba(255,255,255,0.1)', padding: '0.6rem 1.2rem' }} | |
| > | |
| Anuluj generowanie | |
| </button> | |
| <button | |
| onClick={submitUserResponse} | |
| disabled={!userResponse.trim()} | |
| className="btn" style={{ background: 'var(--accent-purple)', color: 'white', padding: '0.6rem 1.2rem', opacity: userResponse.trim() ? 1 : 0.5 }} | |
| > | |
| Wyślij i wznów pracę | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| <style>{` | |
| @keyframes spin { 100% { transform: rotate(360deg); } } | |
| .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 { | |
| color: var(--text-primary); margin-top: 1.25em; margin-bottom: 0.4em; font-weight: 700; | |
| } | |
| .markdown-body p { margin-bottom: 0.9em; } | |
| .markdown-body ul { padding-left: 1.4em; margin-bottom: 0.9em; list-style-type: disc; } | |
| .markdown-body li { margin-bottom: 0.2em; } | |
| .markdown-body strong { color: var(--text-primary); font-weight: 600; } | |
| `}</style> | |
| </div> | |
| ); | |
| }; | |
| export default AIGeneratorPanel; | |