/** * 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' ? 'user-message rounded-lg py-2 px-3 max-w-[85%]' : 'assistant-message rounded-lg py-2 px-3 max-w-[85%]'; // Nachrichtentext einfügen, falls Markdown-Parser verfügbar, nutzen if (this.markdownParser) { bubble.innerHTML = this.markdownParser.parse(text); } else { bubble.textContent = text; } // Links in der Nachricht klickbar machen const links = bubble.querySelectorAll('a'); links.forEach(link => { link.target = '_blank'; link.rel = 'noopener noreferrer'; link.className = 'text-primary-600 dark:text-primary-400 underline'; }); // Code-Blöcke stylen const codeBlocks = bubble.querySelectorAll('pre'); codeBlocks.forEach(block => { block.className = 'bg-gray-100 dark:bg-dark-900 p-2 rounded my-2 overflow-x-auto'; }); const inlineCode = bubble.querySelectorAll('code:not(pre code)'); inlineCode.forEach(code => { code.className = 'bg-gray-100 dark:bg-dark-900 px-1 rounded font-mono text-sm'; }); messageEl.appendChild(bubble); this.chatHistory.appendChild(messageEl); // Scrolle zum Ende des Chat-Verlaufs this.chatHistory.scrollTop = this.chatHistory.scrollHeight; } /** * Zeigt Vorschläge für mögliche Fragen an * @param {Array} suggestions - Array von Vorschlägen */ showSuggestions(suggestions) { if (!this.suggestionArea || !suggestions || !suggestions.length) return; // Vorherige Vorschläge entfernen this.suggestionArea.innerHTML = ''; // Neue Vorschläge hinzufügen suggestions.forEach((text, index) => { const pill = document.createElement('button'); pill.className = 'suggestion-pill text-sm px-3 py-1.5 rounded-full bg-primary-100 dark:bg-primary-900 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition-all duration-200'; pill.style.animationDelay = `${index * 0.1}s`; pill.textContent = text; this.suggestionArea.appendChild(pill); }); // Vorschlagsbereich anzeigen this.suggestionArea.classList.remove('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 timeout: 60000 // 60 Sekunden Timeout }); // Ladeindikator entfernen this.removeLoadingIndicator(); if (!response.ok) { const errorText = await response.text(); let errorMessage; try { // Versuche, die Fehlermeldung zu parsen const errorData = JSON.parse(errorText); errorMessage = errorData.error || `Serverfehler: ${response.status} ${response.statusText}`; } catch { // Bei Parsing-Fehler verwende Standardfehlermeldung errorMessage = `Serverfehler: ${response.status} ${response.statusText}`; } throw new Error(errorMessage); } 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(); // Spezielle Fehlermeldungen für bestimmte Fehlertypen const errorMessage = error.message || ''; let userFriendlyMessage = 'Es gab ein Problem mit der Anfrage.'; if (errorMessage.includes('timeout') || errorMessage.includes('Zeitüberschreitung')) { userFriendlyMessage = 'Die Antwort hat zu lange gedauert. Der Server ist möglicherweise überlastet.'; } else if (errorMessage.includes('500') || errorMessage.includes('Internal Server Error')) { userFriendlyMessage = 'Ein Serverfehler ist aufgetreten. Wir arbeiten an einer Lösung.'; } else if (errorMessage.includes('429') || errorMessage.includes('rate limit')) { userFriendlyMessage = 'Die API-Anfragelimits wurden erreicht. Bitte warte einen Moment.'; } // Fehlermeldung anzeigen oder Wiederholungsversuch starten if (this.retryCount < this.maxRetries) { this.retryCount++; this.addMessage('assistant', `${userFriendlyMessage} Ich versuche es erneut... (Versuch ${this.retryCount}/${this.maxRetries})`); // Letzte Benutzernachricht speichern für den Wiederholungsversuch const lastUserMessageIndex = this.messages.findLastIndex(msg => msg.role === 'user'); if (lastUserMessageIndex >= 0) { const lastUserMessage = this.messages[lastUserMessageIndex].content; // Kurze Verzögerung vor dem erneuten Versuch mit exponentieller Backoff-Strategie const retryDelay = 1500 * Math.pow(2, this.retryCount - 1); // 1.5s, 3s, 6s, ... setTimeout(() => { // Entferne Fehlermeldung aus dem Messages-Array, behalte aber die Benutzernachricht this.messages = this.messages.filter(msg => !(msg.role === 'assistant' && msg.content.includes('versuche es erneut')) ); // Erneuter Versand mit gleicher Nachricht this.inputField.value = lastUserMessage; this.sendMessage(); }, retryDelay); } } 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 oder kontaktiere den Support, falls das Problem weiterhin besteht.'); 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 eine Ladeanimation an */ showLoadingIndicator() { if (!this.chatHistory) return; // Prüfen, ob bereits ein Ladeindikator angezeigt wird if (document.getElementById('assistant-loading-indicator')) return; const loadingEl = document.createElement('div'); loadingEl.className = 'flex justify-start'; loadingEl.id = 'assistant-loading-indicator'; const bubble = document.createElement('div'); bubble.className = 'assistant-message rounded-lg py-3 px-4 max-w-[85%] flex items-center'; const typingIndicator = document.createElement('div'); typingIndicator.className = 'typing-indicator'; typingIndicator.innerHTML = ` `; bubble.appendChild(typingIndicator); loadingEl.appendChild(bubble); this.chatHistory.appendChild(loadingEl); this.chatHistory.scrollTop = this.chatHistory.scrollHeight; } /** * Entfernt den Ladeindikator aus dem Chat */ removeLoadingIndicator() { const loadingIndicator = document.getElementById('assistant-loading-indicator'); 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;