572 lines
23 KiB
JavaScript
572 lines
23 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'
|
|
? '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 '<br>';
|
|
return `<p>${line}</p>`;
|
|
}).join('');
|
|
}
|
|
} else {
|
|
// Für Benutzernachrichten einfache Formatierung
|
|
formattedText = text.split('\n').map(line => {
|
|
if (line.trim() === '') return '<br>';
|
|
return `<p>${line}</p>`;
|
|
}).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 = '<div class="typing-indicator"><span></span><span></span><span></span></div>';
|
|
|
|
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;
|