/** * ChatGPT Assistent Modul * Verwaltet die Interaktion mit der OpenAI API und die Benutzeroberfläche des Assistenten */ class ChatGPTAssistant { constructor() { this.messages = []; this.isOpen = false; this.isLoading = false; this.container = null; this.chatHistory = null; this.inputField = null; this.suggestionArea = null; this.maxRetries = 2; this.retryCount = 0; this.markdownParser = null; this.initializeMarkdownParser(); } /** * Initialisiert den Markdown-Parser */ async initializeMarkdownParser() { // Dynamisch marked.js laden, wenn noch nicht vorhanden if (!window.marked) { try { // Prüfen, ob marked.js bereits im Dokument geladen ist if (!document.querySelector('script[src*="marked"]')) { // Falls nicht, Script-Tag erstellen und einfügen const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; script.async = true; // Promise erstellen, das resolved wird, wenn das Script geladen wurde await new Promise((resolve, reject) => { script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); console.log('Marked.js erfolgreich geladen'); } // Marked konfigurieren this.markdownParser = window.marked; this.markdownParser.setOptions({ gfm: true, breaks: true, sanitize: true, smartLists: true, smartypants: true }); } catch (error) { console.error('Fehler beim Laden von marked.js:', error); // Fallback-Parser, der nur einfache Absätze erkennt this.markdownParser = { parse: (text) => { return text.split('\n').map(line => { if (line.trim() === '') return '
'; return `

${line}

`; }).join(''); } }; } } else { // Marked ist bereits geladen this.markdownParser = window.marked; } } /** * Initialisiert den Assistenten und fügt die UI zum DOM hinzu */ init() { // Assistent-Container erstellen this.createAssistantUI(); // Event-Listener hinzufügen this.setupEventListeners(); // Ersten Willkommensnachricht anzeigen this.addMessage("assistant", "Hallo! Ich bin dein KI-Assistent (4o-mini) und habe Zugriff auf die Wissensdatenbank. Wie kann ich dir helfen?\n\nDu kannst mir Fragen über:\n- **Gedanken** in der Datenbank\n- **Kategorien** und Wissenschaftsbereiche\n- **Mindmaps** und Wissensverknüpfungen\n\nstellen."); // Vorschläge anzeigen this.showSuggestions([ "Zeige mir Gedanken zur künstlichen Intelligenz", "Welche Kategorien gibt es in der Datenbank?", "Suche nach Mindmaps zum Thema Informatik" ]); console.log('KI-Assistent initialisiert!'); } /** * Erstellt die UI-Elemente für den Assistenten */ createAssistantUI() { // Hauptcontainer erstellen this.container = document.createElement('div'); this.container.id = 'chatgpt-assistant'; this.container.className = 'fixed bottom-4 right-4 z-50 flex flex-col'; // Button zum Öffnen/Schließen des Assistenten const toggleButton = document.createElement('button'); toggleButton.id = 'assistant-toggle'; toggleButton.className = 'ml-auto bg-primary-600 hover:bg-primary-700 text-white rounded-full p-3 shadow-lg transition-all duration-300 mb-2'; toggleButton.innerHTML = ''; // Chat-Container const chatContainer = document.createElement('div'); chatContainer.id = 'assistant-chat'; chatContainer.className = 'bg-white dark:bg-dark-800 rounded-lg shadow-xl overflow-hidden transition-all duration-300 w-80 md:w-96 max-h-0 opacity-0'; // Chat-Header const header = document.createElement('div'); header.className = 'bg-primary-600 text-white p-3 flex items-center justify-between'; header.innerHTML = `
KI-Assistent (4o-mini)
`; // Chat-Verlauf this.chatHistory = document.createElement('div'); this.chatHistory.id = 'assistant-history'; this.chatHistory.className = 'p-3 overflow-y-auto max-h-96 space-y-3'; // Vorschlagsbereich this.suggestionArea = document.createElement('div'); this.suggestionArea.id = 'assistant-suggestions'; this.suggestionArea.className = 'px-3 pb-2 flex flex-wrap gap-2 overflow-x-auto hidden'; // Chat-Eingabe const inputContainer = document.createElement('div'); inputContainer.className = 'border-t border-gray-200 dark:border-dark-600 p-3 flex items-center'; this.inputField = document.createElement('input'); this.inputField.type = 'text'; this.inputField.placeholder = 'Stelle eine Frage zur Wissensdatenbank...'; this.inputField.className = 'flex-1 border border-gray-300 dark:border-dark-600 dark:bg-dark-700 dark:text-white rounded-l-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500'; const sendButton = document.createElement('button'); sendButton.id = 'assistant-send'; sendButton.className = 'bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-r-lg'; sendButton.innerHTML = ''; // Elemente zusammenfügen inputContainer.appendChild(this.inputField); inputContainer.appendChild(sendButton); chatContainer.appendChild(header); chatContainer.appendChild(this.chatHistory); chatContainer.appendChild(this.suggestionArea); chatContainer.appendChild(inputContainer); this.container.appendChild(toggleButton); this.container.appendChild(chatContainer); // Zum DOM hinzufügen document.body.appendChild(this.container); } /** * Richtet Event-Listener für die Benutzeroberfläche ein */ setupEventListeners() { // Toggle-Button const toggleButton = document.getElementById('assistant-toggle'); if (toggleButton) { toggleButton.addEventListener('click', () => this.toggleAssistant()); } // Schließen-Button const closeButton = document.getElementById('assistant-close'); if (closeButton) { closeButton.addEventListener('click', () => this.toggleAssistant(false)); } // Senden-Button const sendButton = document.getElementById('assistant-send'); if (sendButton) { sendButton.addEventListener('click', () => this.sendMessage()); } // Enter-Taste im Eingabefeld if (this.inputField) { this.inputField.addEventListener('keyup', (e) => { if (e.key === 'Enter') { this.sendMessage(); } }); } // Vorschläge klickbar machen if (this.suggestionArea) { this.suggestionArea.addEventListener('click', (e) => { if (e.target.classList.contains('suggestion-pill')) { this.inputField.value = e.target.textContent; this.sendMessage(); } }); } } /** * Öffnet oder schließt den Assistenten * @param {boolean} state - Optional: erzwingt einen bestimmten Zustand */ toggleAssistant(state = null) { const chatContainer = document.getElementById('assistant-chat'); if (!chatContainer) return; this.isOpen = state !== null ? state : !this.isOpen; if (this.isOpen) { chatContainer.classList.remove('max-h-0', 'opacity-0'); chatContainer.classList.add('max-h-[32rem]', 'opacity-100'); if (this.inputField) this.inputField.focus(); // Zeige Vorschläge wenn verfügbar if (this.suggestionArea && this.suggestionArea.children.length > 0) { this.suggestionArea.classList.remove('hidden'); } } else { chatContainer.classList.remove('max-h-[32rem]', 'opacity-100'); chatContainer.classList.add('max-h-0', 'opacity-0'); } } /** * Fügt eine Nachricht zum Chat-Verlauf hinzu * @param {string} sender - 'user' oder 'assistant' * @param {string} text - Nachrichtentext */ addMessage(sender, text) { // Nachricht zum Verlauf hinzufügen this.messages.push({ role: sender, content: text }); // DOM-Element erstellen const messageEl = document.createElement('div'); messageEl.className = `flex ${sender === 'user' ? 'justify-end' : 'justify-start'}`; const bubble = document.createElement('div'); bubble.className = sender === 'user' ? 'bg-primary-100 dark:bg-primary-900 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]' : 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3 max-w-[85%]'; // Formatierung des Texts (mit Markdown für Assistent-Nachrichten) let formattedText = ''; if (sender === 'assistant' && this.markdownParser) { // Für Assistentnachrichten Markdown verwenden try { formattedText = this.markdownParser.parse(text); // CSS für Markdown-Formatierung hinzufügen const markdownStyles = ` .markdown-bubble h1, .markdown-bubble h2, .markdown-bubble h3, .markdown-bubble h4, .markdown-bubble h5, .markdown-bubble h6 { font-weight: bold; margin-top: 0.5rem; margin-bottom: 0.5rem; } .markdown-bubble h1 { font-size: 1.4rem; } .markdown-bubble h2 { font-size: 1.3rem; } .markdown-bubble h3 { font-size: 1.2rem; } .markdown-bubble h4 { font-size: 1.1rem; } .markdown-bubble ul, .markdown-bubble ol { padding-left: 1.5rem; margin: 0.5rem 0; } .markdown-bubble ul { list-style-type: disc; } .markdown-bubble ol { list-style-type: decimal; } .markdown-bubble p { margin: 0.5rem 0; } .markdown-bubble code { font-family: monospace; background-color: rgba(0, 0, 0, 0.1); padding: 1px 4px; border-radius: 3px; } .markdown-bubble pre { background-color: rgba(0, 0, 0, 0.1); padding: 0.5rem; border-radius: 4px; overflow-x: auto; margin: 0.5rem 0; } .markdown-bubble pre code { background-color: transparent; padding: 0; } .markdown-bubble blockquote { border-left: 3px solid rgba(0, 0, 0, 0.2); padding-left: 0.8rem; margin: 0.5rem 0; font-style: italic; } .dark .markdown-bubble code { background-color: rgba(255, 255, 255, 0.1); } .dark .markdown-bubble pre { background-color: rgba(255, 255, 255, 0.1); } .dark .markdown-bubble blockquote { border-left-color: rgba(255, 255, 255, 0.2); } `; // Füge die Styles hinzu, wenn sie noch nicht vorhanden sind if (!document.querySelector('#markdown-chat-styles')) { const style = document.createElement('style'); style.id = 'markdown-chat-styles'; style.textContent = markdownStyles; document.head.appendChild(style); } // Klasse für Markdown-Formatierung hinzufügen bubble.classList.add('markdown-bubble'); } catch (error) { console.error('Fehler bei der Markdown-Formatierung:', error); // Fallback zur einfachen Formatierung formattedText = text.split('\n').map(line => { if (line.trim() === '') return '
'; return `

${line}

`; }).join(''); } } else { // Für Benutzernachrichten einfache Formatierung formattedText = text.split('\n').map(line => { if (line.trim() === '') return '
'; return `

${line}

`; }).join(''); } bubble.innerHTML = formattedText; messageEl.appendChild(bubble); if (this.chatHistory) { this.chatHistory.appendChild(messageEl); // Scroll zum Ende des Verlaufs this.chatHistory.scrollTop = this.chatHistory.scrollHeight; } } /** * Zeigt Vorschläge als klickbare Pills an * @param {string[]} suggestions - Liste von Vorschlägen */ showSuggestions(suggestions) { if (!this.suggestionArea) return; // Vorherige Vorschläge entfernen this.suggestionArea.innerHTML = ''; if (suggestions && suggestions.length > 0) { suggestions.forEach(suggestion => { const pill = document.createElement('button'); pill.className = 'suggestion-pill text-sm bg-gray-200 dark:bg-dark-600 hover:bg-gray-300 dark:hover:bg-dark-500 text-gray-800 dark:text-gray-200 rounded-full px-3 py-1 mb-2 transition-colors'; pill.textContent = suggestion; this.suggestionArea.appendChild(pill); }); this.suggestionArea.classList.remove('hidden'); } else { this.suggestionArea.classList.add('hidden'); } } /** * Sendet die Benutzernachricht an den Server und zeigt die Antwort an */ async sendMessage() { if (!this.inputField) return; const userInput = this.inputField.value.trim(); if (!userInput || this.isLoading) return; // Vorschläge ausblenden if (this.suggestionArea) { this.suggestionArea.classList.add('hidden'); } // Benutzernachricht anzeigen this.addMessage('user', userInput); // Eingabefeld zurücksetzen this.inputField.value = ''; // Ladeindikator anzeigen this.isLoading = true; this.showLoadingIndicator(); try { console.log('Sende Anfrage an KI-Assistent API...'); // Anfrage an den Server senden const response = await fetch('/api/assistant', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ messages: this.messages }), cache: 'no-cache', // Kein Cache verwenden credentials: 'same-origin' // Cookies senden }); // Ladeindikator entfernen this.removeLoadingIndicator(); if (!response.ok) { throw new Error(`Serverfehler: ${response.status} ${response.statusText}`); } const data = await response.json(); console.log('Antwort erhalten:', data); // Antwort anzeigen if (data.response) { this.addMessage('assistant', data.response); // Neue Vorschläge basierend auf dem aktuellen Kontext anzeigen this.generateContextualSuggestions(); // Erfolgreiche Anfrage zurücksetzen this.retryCount = 0; } else if (data.error) { this.addMessage('assistant', `Fehler: ${data.error}`); } else { throw new Error('Unerwartetes Antwortformat vom Server'); } } catch (error) { console.error('Fehler bei der Kommunikation mit dem Assistenten:', error); // Ladeindikator entfernen, falls noch vorhanden this.removeLoadingIndicator(); // Fehlermeldung anzeigen oder Wiederholungsversuch starten if (this.retryCount < this.maxRetries) { this.retryCount++; this.addMessage('assistant', 'Es gab ein Problem mit der Anfrage. Ich versuche es erneut...'); // Kurze Verzögerung vor dem erneuten Versuch setTimeout(() => { // Letzte Benutzernachricht aus dem Messages-Array entfernen const lastUserMessage = this.messages[this.messages.length - 2].content; this.messages = this.messages.slice(0, -2); // Entferne Benutzernachricht und Fehlermeldung // Erneuter Versand mit gleicher Nachricht this.inputField.value = lastUserMessage; this.sendMessage(); }, 1500); } else { // Maximale Anzahl an Wiederholungsversuchen erreicht this.addMessage('assistant', 'Es tut mir leid, aber es gab ein Problem bei der Verarbeitung deiner Anfrage. Bitte versuche es später noch einmal.'); this.retryCount = 0; // Zurücksetzen für die nächste Anfrage } } finally { this.isLoading = false; } } /** * Generiert kontextbasierte Vorschläge basierend auf dem aktuellen Chat-Verlauf */ generateContextualSuggestions() { // Basierend auf letzter Antwort des Assistenten, verschiedene Vorschläge generieren const lastAssistantMessage = this.messages.findLast(msg => msg.role === 'assistant')?.content || ''; let suggestions = []; // Intelligente Vorschläge basierend auf Kontext if (lastAssistantMessage.includes('Künstliche Intelligenz') || lastAssistantMessage.includes('KI ') || lastAssistantMessage.includes('AI ')) { suggestions = [ "Wie wird KI in der Wissenschaft eingesetzt?", "Zeige mir Gedanken zum maschinellen Lernen", "Was ist der Unterschied zwischen KI und ML?" ]; } else if (lastAssistantMessage.includes('Kategorie') || lastAssistantMessage.includes('Kategorien')) { suggestions = [ "Zeige mir die Unterkategorien", "Welche Gedanken gehören zu dieser Kategorie?", "Liste alle Wissenschaftskategorien auf" ]; } else if (lastAssistantMessage.includes('Mindmap') || lastAssistantMessage.includes('Visualisierung')) { suggestions = [ "Wie kann ich eine eigene Mindmap erstellen?", "Zeige mir Beispiele für Mindmaps", "Wie funktionieren die Verbindungen in Mindmaps?" ]; } else { // Standardvorschläge suggestions = [ "Erzähle mir mehr dazu", "Gibt es Beispiele dafür?", "Wie kann ich diese Information nutzen?" ]; } this.showSuggestions(suggestions); } /** * Zeigt einen Ladeindikator im Chat an */ showLoadingIndicator() { if (!this.chatHistory) return; // Entferne vorhandenen Ladeindikator (falls vorhanden) this.removeLoadingIndicator(); const loadingEl = document.createElement('div'); loadingEl.id = 'assistant-loading'; loadingEl.className = 'flex justify-start'; const bubble = document.createElement('div'); bubble.className = 'bg-gray-100 dark:bg-dark-700 text-gray-800 dark:text-white rounded-lg py-2 px-3'; bubble.innerHTML = '
'; loadingEl.appendChild(bubble); this.chatHistory.appendChild(loadingEl); // Scroll zum Ende des Verlaufs this.chatHistory.scrollTop = this.chatHistory.scrollHeight; } /** * Entfernt den Ladeindikator aus dem Chat */ removeLoadingIndicator() { const loadingIndicator = document.getElementById('assistant-loading'); if (loadingIndicator) { loadingIndicator.remove(); } } /** * Öffnet den Assistenten und sendet eine vorgegebene Frage * @param {string} question - Die zu stellende Frage */ async sendQuestion(question) { if (!question || this.isLoading) return; // Assistenten öffnen this.toggleAssistant(true); // Kurze Verzögerung, um sicherzustellen, dass der UI vollständig geöffnet ist await new Promise(resolve => setTimeout(resolve, 300)); // Frage in Eingabefeld setzen if (this.inputField) { this.inputField.value = question; // Sende die Frage this.sendMessage(); } } } // Mache die Klasse global verfügbar window.ChatGPTAssistant = ChatGPTAssistant;