Files
website/static/js/modules/chatgpt-assistant.js

546 lines
22 KiB
JavaScript

/**
* 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 '<br>';
return `<p>${line}</p>`;
}).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 = '<i class="fas fa-robot text-xl"></i>';
// 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 = `
<div class="flex items-center">
<i class="fas fa-robot mr-2"></i>
<span>KI-Assistent (4o-mini)</span>
</div>
<button id="assistant-close" class="text-white hover:text-gray-200">
<i class="fas fa-times"></i>
</button>
`;
// 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 = '<i class="fas fa-paper-plane"></i>';
// 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 = `
<span></span>
<span></span>
<span></span>
`;
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;