refactor + new look

This commit is contained in:
Christian Rute 2025-04-07 22:22:12 +02:00
parent e98dad71e5
commit 67c0e9e8af
10 changed files with 1844 additions and 724 deletions

View File

@ -4,32 +4,64 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin-Bereich</title>
<!-- Google Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
<style>
/* Styles specific to admin user list presentation */
.user-list-item {
display: flex;
flex-wrap: wrap; /* Allow wrapping on smaller screens */
align-items: center;
justify-content: space-between;
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
gap: 10px; /* Space between items */
}
.user-list-item:last-child { border-bottom: none; }
.user-list-item p { margin: 0; flex-basis: 22%; min-width: 150px; /* Grow/shrink basis */ }
.user-list-item .button-delete { flex-shrink: 0; /* Prevent button shrinking */ }
</style>
</head>
<body>
<!-- Header für Admin-Bereich -->
<body id="admin-page">
<!-- Header -->
<header class="header">
<h1 class="header-title">Admin-Bereich</h1>
<h1 class="header-logo">Admin Bereich</h1> <!-- Use logo class for consistency -->
<div class="header-buttons">
<button id="loadUserListBtn" class="header-btn">Benutzerliste laden</button>
<button id="welcomeButton" class="header-btn">Zur Willkommensseite</button>
<button id="themeToggleBtn" class="header-btn theme-toggle" aria-label="Toggle Theme">☀️</button> <!-- Added Theme Toggle -->
<button id="loadUserListBtn" class="header-btn">Liste neu laden</button> <!-- Renamed -->
<button id="welcomeButton" class="header-btn">Zurück zum Chat</button>
<button id="logoutBtn" class="header-btn">Abmelden</button>
</div>
</header>
<!-- Hauptbereich für Benutzerliste -->
<main>
<div id="custom-alert-confirm" style="display:none;" class="alert-box">
<p id="alert-message-confirm"></p>
<button id="close-alert-confirm">Close</button>
<button id="confirm-alert-confirm" style="display:none;">Confirm</button>
<!-- Main Content -->
<main class="admin-container" style="padding: 20px; margin-top: 60px;"> <!-- Add padding -->
<h2>Benutzerverwaltung</h2>
<div id="user-list" style="background-color: var(--bg-primary); border-radius: var(--border-radius-medium); margin-top: 15px; box-shadow: var(--shadow-sm); overflow: hidden;">
<!-- User list items added by JS -->
</div>
<div id="user-list" class="user-list"></div>
</main>
<!-- Modals -->
<div id="custom-alert" class="custom-alert">
<div class="alert-content"> <span id="alert-message"></span> <button id="close-alert" class="close-btn">OK</button> </div>
</div>
<div id="custom-alert-confirm" class="custom-alert-confirm">
<div class="alert-content">
<p id="alert-message-confirm"></p>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button id="confirm-alert-confirm">Bestätigen</button>
<button id="close-alert-confirm">Abbrechen</button>
</div>
</div>
</div>
<!-- Scripts -->
<script type="module" src="js/config.js"></script>
<script type="module" src="js/shared_functions.js"></script>
<script type="module" src="js/main.js"></script>
<script type="module" src="js/main.js"></script> <!-- main.js now handles theme toggle too -->
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login & Registrierung</title>
<!-- Google Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<!-- The modal for the alert -->
<body id="login-page">
<!-- Alert Modal -->
<div id="custom-alert" class="custom-alert">
<div class="alert-content">
<span id="alert-message"></span>
@ -15,27 +19,38 @@
</div>
</div>
<div class="container">
<h1>Willkommen! Bitte melden Sie sich an oder registrieren Sie sich.</h1>
<!-- Main Content -->
<main class="container">
<h1>Willkommen!</h1>
<p style="color: var(--text-secondary); margin-bottom: 25px;">Bitte melden Sie sich an oder registrieren Sie sich.</p>
<div class="form-wrapper">
<form id="registerForm">
<h2>Registrierung</h2>
<input type="text" id="regUsername" placeholder="Benutzername" required>
<input type="email" id="regEmail" placeholder="E-Mail" required>
<input type="password" id="regPassword" placeholder="Passwort" required>
<button type="submit">Registrieren</button>
</form>
<!-- Registration Form -->
<section class="form" aria-labelledby="register-heading">
<h2 id="register-heading">Registrierung</h2>
<form id="registerForm">
<input type="text" id="regUsername" placeholder="Benutzername" required aria-label="Registration Username">
<input type="email" id="regEmail" placeholder="E-Mail" required aria-label="Registration Email">
<input type="password" id="regPassword" placeholder="Passwort" required aria-label="Registration Password">
<button type="submit">Registrieren</button>
</form>
</section>
<form id="loginForm">
<h2>Login</h2>
<input type="email" id="loginEmail" placeholder="E-Mail" required>
<input type="password" id="loginPassword" placeholder="Passwort" required>
<button type="submit">Anmelden</button>
</form>
<!-- Login Form -->
<section class="form" aria-labelledby="login-heading">
<h2 id="login-heading">Login</h2>
<form id="loginForm">
<input type="email" id="loginEmail" placeholder="E-Mail" required aria-label="Login Email">
<input type="password" id="loginPassword" placeholder="Passwort" required aria-label="Login Password">
<button type="submit">Anmelden</button>
</form>
</section>
</div>
</div>
</main>
<!-- Scripts -->
<script type="module" src="js/config.js"></script>
<script type="module" src="js/shared_functions.js"></script>
<script type="module" src="js/auth.js"></script>
</body>
</html>
</html>

168
client/js/api_service.js Normal file
View File

@ -0,0 +1,168 @@
import { BASE_API_URL, API_KEY } from './config.js';
import { showAlert } from './shared_functions.js';
/**
* Generic fetch wrapper for API calls
* @param {string} endpoint API endpoint (e.g., '/api/auth/me')
* @param {object} options Fetch options (method, headers, body)
* @param {boolean} includeAuthHeader Add Authorization header?
* @returns {Promise<any>} Parsed JSON response or throws error
*/
async function fetchApi(endpoint, options = {}, includeAuthHeader = true) {
const url = `${BASE_API_URL}${endpoint}`;
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
if (includeAuthHeader) {
const token = localStorage.getItem('token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
} else {
// Handle cases where token is expected but missing (e.g., redirect to login)
console.warn('Auth token missing for API call:', endpoint);
// Optionally throw an error or redirect here
// window.location.href = 'index.html';
// throw new Error('Authentication token not found.');
}
}
try {
const response = await fetch(url, { ...options, headers });
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (e) {
// If response is not JSON
errorData = { message: response.statusText };
}
console.error(`API Error ${response.status}:`, errorData);
const error = new Error(errorData.message || `HTTP error! status: ${response.status}`);
error.status = response.status;
error.data = errorData;
throw error;
}
// Handle responses with no content (e.g., DELETE)
if (response.status === 204) {
return null;
}
return await response.json();
} catch (error) {
console.error(`Fetch error for ${url}:`, error);
// Re-throw generic network errors or specific API errors
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
throw new Error(`Network Error: Could not connect to API (${url}).`);
}
throw error; // Re-throw API errors or other exceptions
}
}
// --- Specific API Functions ---
export async function getUserData() {
return await fetchApi('/api/auth/me', { method: 'GET' });
}
export async function getAdminData() {
return await fetchApi('/api/auth/admin', { method: 'GET' });
}
export async function getAllUsers() {
return await fetchApi('/api/auth/users', { method: 'GET' });
}
export async function deleteUserById(userId) {
return await fetchApi(`/api/auth/user/${userId}`, { method: 'DELETE' });
}
export async function getAvailableModels() {
// Note: OpenAI endpoints might not require the Bearer token if using a proxy/backend key
const requiresAuth = !API_KEY.startsWith('YOUR_'); // Simple check if a real key seems present
const headers = requiresAuth ? { 'Authorization': `Bearer ${API_KEY}` } : {};
try {
// Using BASE_API_URL assumes your proxy handles the /v1 path
const data = await fetchApi('/v1/models', { method: 'GET', headers }, false); // Auth header likely not needed here if backend handles it
// Return the first model ID, or handle multiple models if needed
return data?.data?.[0]?.id || 'default-model-id'; // Provide a fallback
} catch (error) {
console.error("Error fetching models:", error);
showAlert("Fehler beim Abrufen der Modelle vom Server.", "error");
throw error; // Allow caller to handle
}
}
export async function streamChatCompletion(messages, systemPrompt, model) {
const body = {
model: model,
messages: [{ role: "system", content: systemPrompt }, ...messages],
stream: true,
temperature: 0.5, // Adjust temperature as needed
};
const requiresAuth = !API_KEY.startsWith('YOUR_');
const headers = requiresAuth ? { 'Authorization': `Bearer ${API_KEY}` } : {};
// This needs to return the raw Response object to handle the stream
try {
const response = await fetch(`${BASE_API_URL}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...headers },
body: JSON.stringify(body)
});
if (!response.ok || !response.body) {
let errorData;
try { errorData = await response.json(); } catch (e) { errorData = { message: response.statusText }; }
console.error(`API Error ${response.status}:`, errorData);
const error = new Error(errorData.message || `HTTP error! status: ${response.status}`);
error.status = response.status;
throw error;
}
return response; // Return the raw response for stream handling
} catch (error) {
console.error(`Fetch stream error:`, error);
if (error instanceof TypeError && error.message.includes('Failed to fetch')) {
throw new Error(`Network Error: Could not connect to API (${BASE_API_URL}).`);
}
throw error;
}
}
export async function tokenizePrompt(promptMessages, systemPrompt, model) {
// Format messages array into the specific string format expected by your /tokenize endpoint
const formatArrayToCustomString = (array) => {
return '[' +
array.map(dict => '{' + Object.entries(dict).map(([key, value]) => `'${key}':'${value.replace(/'/g, "\\'")}'`).join(', ') + '}') // Escape single quotes in values
.join(', ') +
']';
};
const messages = [{ role: "system", content: systemPrompt }, ...promptMessages];
const formattedString = `"${formatArrayToCustomString(messages)}"`; // Wrap in double quotes as per your example
const body = {
model: model,
prompt: formattedString,
add_special_tokens: true // Assuming this is correct for your backend
};
const requiresAuth = !API_KEY.startsWith('YOUR_');
const headers = requiresAuth ? { 'Authorization': `Bearer ${API_KEY}` } : {};
try {
// Assuming /tokenize is relative to BASE_API_URL
return await fetchApi('/tokenize', { method: 'POST', body: JSON.stringify(body), headers }, false);
} catch (error) {
console.error("Error tokenizing prompt:", error);
showAlert("Fehler bei der Token-Berechnung.", "error");
// Return a default structure or re-throw
return { count: 0, max_model_len: 4096 }; // Example fallback
}
}

256
client/js/chat_manager.js Normal file
View File

@ -0,0 +1,256 @@
import * as api from './api_service.js';
import * as ui from './chat_ui.js';
import { showAlert, showConfirm } from './shared_functions.js';
let currentChatId = null;
let chatHistories = {}; // In-memory cache of chat histories { chatId: [messages] }
let availableModel = 'default-model-id'; // Fetched model ID
let systemPrompt = "You are a helpful and concise assistant."; // Default system prompt
let maxTokens = 4096; // Default max tokens, will be updated by tokenize
// --- Initialization ---
export async function initializeChat() {
await loadAvailableModel();
loadChatSessionsFromStorage();
if (Object.keys(chatHistories).length > 0) {
// Load the most recent chat (assuming keys are somewhat ordered or track last used)
const lastChatId = Object.keys(chatHistories).sort().pop(); // Simple sort, might need better tracking
await loadChat(lastChatId);
} else {
// Create a new chat if none exist
await createNewChat();
}
// Initial token calculation for the loaded chat
await updateTokenUsage();
}
async function loadAvailableModel() {
try {
availableModel = await api.getAvailableModels();
console.log("Using model:", availableModel);
} catch (error) {
console.error("Failed to load model, using default:", error);
// Keep the default model ID
}
}
// --- Chat Session Management ---
function loadChatSessionsFromStorage() {
chatHistories = {}; // Clear cache first
let hasChats = false;
Object.keys(localStorage).forEach((key) => {
if (key.startsWith('chatHistory-')) {
const chatId = key.substring('chatHistory-'.length);
try {
const history = JSON.parse(localStorage.getItem(key));
if (Array.isArray(history)) {
chatHistories[chatId] = history;
const chatName = generateChatName(history, chatId);
ui.addChatToList({ id: chatId, name: chatName }, false, loadChat, deleteChat); // Add to UI
hasChats = true;
} else {
localStorage.removeItem(key); // Clean up invalid data
}
} catch (e) {
console.error(`Error parsing chat history for ${key}:`, e);
localStorage.removeItem(key); // Clean up corrupted data
}
}
});
console.log("Loaded sessions:", Object.keys(chatHistories).length);
}
export async function loadChat(chatId) {
if (!chatHistories[chatId]) {
console.error(`Chat history for ${chatId} not found.`);
showAlert(`Chat ${chatId} konnte nicht geladen werden.`, 'error');
// Optionally load the first available chat or create new
const firstChatId = Object.keys(chatHistories)[0];
if(firstChatId) await loadChat(firstChatId);
else await createNewChat();
return;
}
if (currentChatId === chatId) return; // Already loaded
currentChatId = chatId;
console.log(`Loading chat: ${chatId}`);
ui.clearChatBox();
chatHistories[currentChatId].forEach(msg => {
ui.displayMessage(msg.content, msg.role, { isMarkdown: msg.role === 'assistant' });
});
ui.setActiveChatInList(chatId);
ui.enableInput();
await updateTokenUsage(); // Update token count for the newly loaded chat
}
export async function createNewChat() {
currentChatId = `chat_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`;
chatHistories[currentChatId] = []; // Initialize empty history
saveChatHistoryToStorage(currentChatId); // Save empty chat immediately
const chatName = generateChatName([], currentChatId);
ui.addChatToList({ id: currentChatId, name: chatName }, true, loadChat, deleteChat); // Add to UI and mark active
ui.clearChatBox();
ui.displayMessage("Neuer Chat gestartet. Frag mich etwas!", "assistant");
ui.setActiveChatInList(currentChatId);
ui.enableInput();
await updateTokenUsage(); // Reset token count
console.log("Created new chat:", currentChatId);
}
export async function deleteChat(chatId) {
const confirmed = await showConfirm(`Möchten Sie den Chat "${generateChatName(chatHistories[chatId], chatId)}" wirklich löschen?`, 'error');
if (confirmed) {
console.log(`Deleting chat: ${chatId}`);
delete chatHistories[chatId]; // Remove from memory
localStorage.removeItem(`chatHistory-${chatId}`); // Remove from storage
ui.removeChatFromList(chatId); // Remove from UI
// If the deleted chat was the current one, load another or create new
if (currentChatId === chatId) {
const remainingChatIds = Object.keys(chatHistories);
if (remainingChatIds.length > 0) {
await loadChat(remainingChatIds[0]); // Load the first remaining
} else {
await createNewChat(); // Create a new one if none left
}
}
showAlert('Chat gelöscht.', 'success');
}
}
// --- Message Handling ---
export async function handleSendMessage(messageText) {
if (!currentChatId || !messageText) return;
ui.disableInput(true); // Disable input and show loading
// 1. Add user message to history and UI
const userMessage = { role: 'user', content: messageText };
chatHistories[currentChatId].push(userMessage);
ui.displayMessage(messageText, 'user');
saveChatHistoryToStorage(currentChatId); // Save after adding user message
updateChatNameIfNeeded(currentChatId); // Update sidebar name if it's the first message
// 2. Update token count *before* sending to AI (includes new user message)
await updateTokenUsage();
// 3. Get AI Response (Streaming)
let fullResponse = '';
try {
const responseStream = await api.streamChatCompletion(
chatHistories[currentChatId], // Send current history
systemPrompt,
availableModel
);
// Process the stream
const reader = responseStream.body.getReader();
const decoder = new TextDecoder('utf-8');
ui.updateStreamingMessage('', true); // Initialize the streaming div
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
// Handle potential multiple data chunks in one value
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.substring(6).trim();
if (jsonStr === '[DONE]') {
break; // Stream finished signal
}
try {
const json = JSON.parse(jsonStr);
const deltaContent = json.choices?.[0]?.delta?.content;
if (deltaContent) {
fullResponse += deltaContent;
ui.updateStreamingMessage(deltaContent, true); // Update UI incrementally
}
} catch (error) {
if (jsonStr) { // Avoid logging empty lines as errors
console.warn('Error parsing stream JSON:', error, 'Received:', jsonStr);
}
}
}
}
}
ui.finalizeStreamingMessage(); // Finalize the UI element
// 4. Add complete AI response to history
if (fullResponse) {
const assistantMessage = { role: 'assistant', content: fullResponse };
chatHistories[currentChatId].push(assistantMessage);
saveChatHistoryToStorage(currentChatId); // Save after getting full response
} else {
// Handle case where stream ended with no content
console.warn("Stream ended without content.");
// Maybe display a generic "No response" message? Not added here.
}
// 5. Update token usage *after* receiving AI response
await updateTokenUsage();
} catch (error) {
console.error('Error during chat completion:', error);
showAlert(`Fehler bei der Kommunikation mit der AI: ${error.message}`, 'error');
ui.finalizeStreamingMessage(); // Ensure UI is cleaned up on error
// Optionally remove the empty assistant message div if created?
} finally {
ui.enableInput(); // Re-enable input regardless of success/error
}
}
// --- History & Storage ---
function saveChatHistoryToStorage(chatId) {
if (chatHistories[chatId]) {
try {
localStorage.setItem(`chatHistory-${chatId}`, JSON.stringify(chatHistories[chatId]));
} catch (e) {
console.error("Error saving chat history to localStorage:", e);
showAlert("Fehler beim Speichern des Chat-Verlaufs. Möglicherweise ist der Speicher voll.", "error");
}
}
}
function generateChatName(history, chatId) {
// Use the first user message as the name, truncated
const firstUserMessage = history?.find(msg => msg.role === 'user')?.content;
if (firstUserMessage) {
return firstUserMessage.substring(0, 25) + (firstUserMessage.length > 25 ? '...' : '');
}
// Fallback name
return `Chat ${chatId.slice(-4)}`;
}
function updateChatNameIfNeeded(chatId) {
// If history only has 1 message (the user one just added), update name
if (chatHistories[chatId] && chatHistories[chatId].length === 1) {
const newName = generateChatName(chatHistories[chatId], chatId);
ui.updateChatNameInList(chatId, newName);
}
}
// --- Tokenization & Progress ---
async function updateTokenUsage() {
if (!currentChatId || !chatHistories[currentChatId]) {
ui.updateProgressBar(0, maxTokens); // Reset if no chat
return;
}
try {
// Call tokenize API with current history
const tokenInfo = await api.tokenizePrompt(chatHistories[currentChatId], systemPrompt, availableModel);
maxTokens = tokenInfo.max_model_len || maxTokens; // Update max tokens if provided
const currentTokens = tokenInfo.count || 0;
ui.updateProgressBar(currentTokens, maxTokens);
} catch (error) {
console.error("Failed to update token usage:", error);
// Keep previous progress bar state or reset? Resetting might be clearer.
ui.updateProgressBar(0, maxTokens);
}
}

193
client/js/chat_ui.js Normal file
View File

@ -0,0 +1,193 @@
const chatBox = document.getElementById('chatBox');
const chatInput = document.getElementById('chatInput');
const sendButton = document.getElementById('sendButton');
const chatList = document.getElementById('chatList');
const progressBar = document.getElementById('progress-bar');
const usageText = document.getElementById('usage-text');
const loadingIndicator = document.getElementById('loadingIndicator');
let currentAssistantMessageDiv = null; // To hold the streaming message div
export function displayMessage(text, sender, options = { isMarkdown: false, isStreaming: false }) {
// Sanitize text before inserting, especially if using innerHTML
// For user messages, simple textContent is safer. For Markdown, use DOMPurify.
const sanitizedText = sender === 'user' ? text : (options.isMarkdown ? DOMPurify.sanitize(marked.parse(text)) : text);
const messageElement = document.createElement('div');
messageElement.classList.add('chat-message', sender);
if (sender === 'assistant' && options.isMarkdown) {
messageElement.innerHTML = sanitizedText; // Use sanitized HTML
} else {
messageElement.textContent = sanitizedText; // Safer for plain text
}
const isScrolledToBottom = chatBox.scrollHeight - chatBox.clientHeight <= chatBox.scrollTop + 5; // Tolerance
// If starting a new streaming message, create the container
if (sender === 'assistant' && options.isStreaming && !currentAssistantMessageDiv) {
currentAssistantMessageDiv = document.createElement('div');
currentAssistantMessageDiv.classList.add('chat-message', 'assistant');
chatBox.appendChild(currentAssistantMessageDiv);
}
// Update or append message
if (currentAssistantMessageDiv && sender === 'assistant' && options.isStreaming) {
currentAssistantMessageDiv.innerHTML = sanitizedText; // Update streaming div
} else {
chatBox.appendChild(messageElement); // Append regular message or finalize stream
currentAssistantMessageDiv = null; // Reset stream div reference
}
// Scroll to bottom only if the user was already near the bottom
if (isScrolledToBottom) {
chatBox.scrollTop = chatBox.scrollHeight;
}
}
// Function to update the streaming message div content incrementally
export function updateStreamingMessage(contentChunk, isMarkdown = true) {
if (!currentAssistantMessageDiv) {
// Create the div if it doesn't exist (e.g., first chunk)
currentAssistantMessageDiv = document.createElement('div');
currentAssistantMessageDiv.classList.add('chat-message', 'assistant');
chatBox.appendChild(currentAssistantMessageDiv);
}
// Append chunk (if plain text) or re-render markdown (simpler for now)
// For true streaming of markdown, need more complex parsing
let currentContent = currentAssistantMessageDiv.dataset.rawContent || '';
currentContent += contentChunk;
currentAssistantMessageDiv.dataset.rawContent = currentContent; // Store raw text
if (isMarkdown && window.marked && window.DOMPurify) {
currentAssistantMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(currentContent));
} else {
currentAssistantMessageDiv.textContent = currentContent;
}
// Keep scrolled to bottom during streaming
chatBox.scrollTop = chatBox.scrollHeight;
}
// Function to finalize the streaming message div
export function finalizeStreamingMessage() {
if (currentAssistantMessageDiv) {
delete currentAssistantMessageDiv.dataset.rawContent; // Clean up temp data
}
currentAssistantMessageDiv = null;
}
export function clearChatBox() {
chatBox.innerHTML = '';
// Optionally add a default message
// displayMessage("Neuer Chat gestartet.", "assistant");
}
export function clearChatInput() {
chatInput.value = '';
autoResizeTextarea(); // Reset height
}
export function disableInput(isLoading = true) {
chatInput.disabled = true;
sendButton.disabled = true;
chatInput.placeholder = isLoading ? 'Warte auf Antwort...' : 'Schreibe eine Nachricht...';
if (isLoading && loadingIndicator) loadingIndicator.style.display = 'inline-block'; // Show spinner
if (isLoading) sendButton.style.display = 'none'; // Hide send button
}
export function enableInput() {
chatInput.disabled = false;
sendButton.disabled = false;
chatInput.placeholder = 'Schreibe eine Nachricht...';
if (loadingIndicator) loadingIndicator.style.display = 'none'; // Hide spinner
sendButton.style.display = 'flex'; // Show send button
chatInput.focus();
}
export function addChatToList(chatData, isActive, loadChatCallback, deleteChatCallback) {
const container = document.createElement('div');
container.classList.add('chat-session-button-container');
container.dataset.chatId = chatData.id; // Add ID to container for easier selection
if (isActive) {
container.classList.add('active');
}
const button = document.createElement('button');
button.classList.add('chat-session-button');
button.textContent = chatData.name || `Chat ${chatData.id.slice(-4)}`; // Use name or part of ID
button.addEventListener('click', () => loadChatCallback(chatData.id));
const deleteBtn = document.createElement('button');
deleteBtn.classList.add('delete-chat-button');
deleteBtn.setAttribute('aria-label', `Delete chat ${chatData.name || chatData.id}`);
deleteBtn.innerHTML = '🗑️'; // Use trash icon
deleteBtn.addEventListener('click', (event) => {
event.stopPropagation(); // Prevent chat loading
deleteChatCallback(chatData.id);
});
container.appendChild(button);
container.appendChild(deleteBtn);
// Prepend to the top of the list for newest first
chatList.insertBefore(container, chatList.firstChild);
}
export function removeChatFromList(chatId) {
const chatElement = chatList.querySelector(`.chat-session-button-container[data-chat-id="${chatId}"]`);
if (chatElement) {
chatElement.remove();
}
}
export function updateChatNameInList(chatId, name) {
const button = chatList.querySelector(`.chat-session-button-container[data-chat-id="${chatId}"] .chat-session-button`);
const deleteButton = chatList.querySelector(`.chat-session-button-container[data-chat-id="${chatId}"] .delete-chat-button`);
if (button) {
button.textContent = name || `Chat ${chatId.slice(-4)}`;
}
if (deleteButton) {
deleteButton.setAttribute('aria-label', `Delete chat ${name || chatId}`);
}
}
export function setActiveChatInList(chatId) {
document.querySelectorAll('.chat-session-button-container').forEach(container => {
container.classList.toggle('active', container.dataset.chatId === chatId);
});
}
export function updateProgressBar(currentUsage, maxUsage) {
// Ensure values are numbers and maxUsage is positive
currentUsage = Number(currentUsage) || 0;
maxUsage = Number(maxUsage) || 1; // Avoid division by zero
maxUsage = Math.max(1, maxUsage); // Ensure maxUsage is at least 1
const percentage = Math.min(100, Math.max(0, (currentUsage / maxUsage) * 100)); // Clamp between 0 and 100
if (progressBar) {
progressBar.style.width = `${percentage}%`;
}
if (usageText) {
// Format maxUsage for readability if large?
usageText.textContent = `Token Usage: ${currentUsage} / ${maxUsage}`;
}
}
// Auto-resize textarea helper
export function autoResizeTextarea() {
chatInput.style.overflowY = 'hidden'; // Prevent scrollbar flash
chatInput.style.height = 'auto';
const scrollHeight = chatInput.scrollHeight;
const maxHeight = parseInt(window.getComputedStyle(chatInput).maxHeight, 10);
if (maxHeight > 0 && scrollHeight > maxHeight) {
chatInput.style.height = `${maxHeight}px`;
chatInput.style.overflowY = 'auto';
} else {
chatInput.style.height = `${scrollHeight}px`;
chatInput.style.overflowY = 'hidden';
}
}

3
client/js/config.js Normal file
View File

@ -0,0 +1,3 @@
// Central configuration
export const BASE_API_URL = 'http://localhost:8015'; // Your backend URL
export const API_KEY = 'YOUR_API_KEY'; // Replace with your actual key or use a more secure method

View File

@ -1,175 +1,381 @@
import {showAlert, showConfirm} from './shared_functions.js';
import { showAlert, showConfirm, sanitizeInput } from './shared_functions.js';
import * as api from './api_service.js';
import { initializeChat, createNewChat, handleSendMessage } from './chat_manager.js'; // Only import needed functions directly used by main
import * as ui from './chat_ui.js'; // Need UI functions
// IP-Adresse
const baseUrl = 'http://localhost:8015';
// --- DOM Elements ---
// Moved some element selections inside setup functions to ensure they exist
const bodyElement = document.body;
console.log("main: ")
console.log(bodyElement)
let themeToggleButton = null; // Initialize as null
let logoutBtn = null; // Initialize as null
// Funktion zum Laden der Benutzerdaten für die normale Webseite
async function loadUserData() {
const token = localStorage.getItem('token');
if (!token) {
showAlert('Nicht autorisiert. Bitte einloggen.');
window.location.href = 'index.html';
return;
// --- Initialization ---
document.addEventListener('DOMContentLoaded', () => {
console.log("DOM Content Loaded. Body ID:", bodyElement.id); // Log body ID
// Common elements present on all/most pages
themeToggleButton = document.getElementById('themeToggleBtn');
logoutBtn = document.getElementById('logoutBtn');
loadThemePreference(); // Load theme early
// Add common listeners if elements exist
if (themeToggleButton) {
console.log("Attaching theme listener");
themeToggleButton.addEventListener('click', toggleTheme);
} else {
console.warn("Theme toggle button not found");
}
if (logoutBtn) {
console.log("Attaching logout listener");
logoutBtn.addEventListener('click', logout);
} else {
console.warn("Logout button not found");
}
try {
const response = await fetch(`${baseUrl}/api/auth/me`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
console.log('Response status:', response.status);
if (!response.ok) {
throw new Error('Fehler beim Abrufen der Benutzerdaten');
}
const data = await response.json();
document.getElementById('username').textContent = data.user;
document.getElementById('isAdmin').textContent = data.isAdmin ? 'Ja' : 'Nein';
// Überprüfen, ob der Benutzer Admin ist
if (data.isAdmin) {
const adminBtn = document.getElementById('adminPermissionsBtn');
adminBtn.style.display = 'inline-block'; // Button anzeigen
adminBtn.addEventListener('click', () => {
window.location.href = 'admin.html'; // Bei Klick weiterleiten
// Page specific initialization with error handling for the setup promises
if (bodyElement.id === 'chat-page') {
console.log("Running Chat Page Setup");
setupChatPage()
.catch(error => {
console.error("Error during Chat Page Setup:", error);
showAlert("Ein kritischer Fehler ist beim Laden der Chat-Seite aufgetreten.", "error");
});
}
} catch (error) {
console.error(error);
showAlert('Sitzung abgelaufen. Bitte erneut einloggen.');
localStorage.removeItem('token');
window.location.href = 'index.html';
} else if (bodyElement.id === 'admin-page') {
console.log("Running Admin Page Setup");
setupAdminPage()
.catch(error => {
// This catch is now primarily for unexpected errors *within* setupAdminPage's try block
// as checkAdminStatus errors should be handled more specifically inside.
console.error("Unhandled error during Admin Page Setup:", error);
if (!error.message?.includes("handled")) { // Avoid double alerts if handled internally
showAlert("Ein unerwarteter Fehler ist beim Laden der Admin-Seite aufgetreten.", "error");
}
});
} else if (bodyElement.id === 'login-page') {
console.log("Login/Register page setup handled by auth.js");
} else {
console.warn("No specific page setup for body ID:", bodyElement.id);
}
}
// Funktion zum Abmelden
document.getElementById('logoutBtn').addEventListener('click', () => {
localStorage.removeItem('token');
window.location.href = 'index.html';
});
function toWelcome() {
location.href = 'welcome.html';
}
async function loadAdminData() {
const token = localStorage.getItem('token');
if (!token) {
showAlert('Nicht autorisiert. Bitte einloggen.');
window.location.href = 'index.html';
// --- Page Setup Functions ---
async function setupChatPage() {
console.log("setupChatPage started");
if (!localStorage.getItem('token')) {
redirectToLogin('Kein Token gefunden. Bitte einloggen.');
return;
}
// Select elements specific to chat page *inside* setup
const adminPermissionsBtn = document.getElementById('adminPermissionsBtn');
const newChatButton = document.getElementById('newChatButton');
const chatInput = document.getElementById('chatInput');
const sendButton = document.getElementById('sendButton');
// Load user data first
await loadUserDataForChat(); // This function now handles UI updates internally
// Setup chat specific event listeners *after* confirming elements exist
if (newChatButton) {
console.log("Attaching new chat listener");
newChatButton.addEventListener('click', createNewChat);
} else {
console.error("New Chat button not found!");
}
if (sendButton) {
console.log("Attaching send listener");
sendButton.addEventListener('click', processUserInput);
} else {
console.error("Send button not found!");
}
if (chatInput) {
console.log("Attaching input listeners");
chatInput.addEventListener('keydown', handleChatInputKeydown);
chatInput.addEventListener('input', ui.autoResizeTextarea);
// Initial resize
ui.autoResizeTextarea();
} else {
console.error("Chat input not found!");
}
if (adminPermissionsBtn) {
console.log("Attaching admin button listener");
adminPermissionsBtn.addEventListener('click', () => { window.location.href = 'admin.html'; });
} else {
// This might be expected if the user is not admin, handled by loadUserDataForChat
// console.warn("Admin permissions button not found (may be expected)");
}
// Initialize chat manager AFTER setting up listeners that might use its functions
try {
const response = await fetch(`${baseUrl}/api/auth/admin`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
showAlert('Zugriff verweigert. Sie sind kein Admin.');
window.location.href = 'welcome.html';
return;
}
const data = await response.json();
console.log(data.message); // "Willkommen, Admin!"
} catch (error) {
console.error(error);
showAlert('Sitzung abgelaufen. Bitte erneut einloggen.');
localStorage.removeItem('token');
window.location.href = 'index.html';
console.log("Initializing Chat Manager...");
await initializeChat();
console.log("Chat Manager Initialized.");
} catch(error) {
console.error("Error initializing chat manager:", error);
showAlert("Fehler beim Initialisieren des Chats.", "error");
}
}
// Funktion zum Löschen eines Benutzers
async function deleteUser(userId, isAdmin) {
// If the user is an admin, show a warning confirmation
if (isAdmin) {
const userConfirmed = await showConfirm('Warnung: Möchten Sie wirklich einen Administrator löschen? Dies kann nicht rückgängig gemacht werden.', 'error');
if (!userConfirmed) {
return; // If the user cancels, do nothing
}
async function setupAdminPage() {
console.log("setupAdminPage started");
if (!localStorage.getItem('token')) {
redirectToLogin('Kein Token gefunden. Bitte einloggen.');
return;
}
const token = localStorage.getItem('token');
try {
const response = await fetch(`${baseUrl}/api/auth/user/${userId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
// Select elements specific to admin page
const loadUserListBtn = document.getElementById('loadUserListBtn');
const welcomeButton = document.getElementById('welcomeButton');
const userListDiv = document.getElementById('user-list'); // Also select user list container
if (response.ok) {
await loadUserList(); // Liste aktualisieren
if (!userListDiv) {
console.error("User list container ('user-list') not found on admin page!");
// Optionally show an error message in the main area if the container is missing
}
try {
// Check admin status first. If this fails, it will throw an error.
// checkAdminStatus handles redirection/alerts for 401/403.
await checkAdminStatus();
console.log("Admin status confirmed. Proceeding with admin setup.");
// ---- If admin check passes, THEN setup listeners and load list ----
// Setup 'Reload List' button listener
if (loadUserListBtn) {
console.log("Attaching load user list listener");
loadUserListBtn.addEventListener('click', () => {
console.log("Reload list button clicked.");
loadUserList(); // Call loadUserList on click
});
} else {
showAlert('Fehler beim Löschen des Benutzers');
console.error("Load user list button ('loadUserListBtn') not found!");
}
// Setup 'Back to Chat' button listener
if (welcomeButton) {
console.log("Attaching welcome button listener");
welcomeButton.addEventListener('click', () => {
console.log("Welcome button clicked, redirecting...");
window.location.href = 'welcome.html';
});
} else {
console.error("Welcome (back to chat) button ('welcomeButton') not found!");
}
// Load list initially ONLY if admin check succeeded
console.log("Initiating initial user list load.");
await loadUserList(); // Load the list data
console.log("Initial user list load attempt finished.");
} catch (error) {
// This catch block now primarily handles errors from checkAdminStatus
// or potentially the initial loadUserList call if it throws unexpectedly.
console.error("SetupAdminPage failed during admin check or initial load:", error);
// Most user-facing errors (like redirection or alerts for auth/network issues)
// should have been handled within checkAdminStatus or loadUserList's call to handleApiError.
// We might not need to show another alert here unless it's a truly unexpected setup problem.
// Mark the error as handled to avoid potential double alerts from the outer catch block.
error.message += " (handled in setupAdminPage)";
}
}
// --- Event Handlers ---
function handleChatInputKeydown(event) {
// console.log("Keydown:", event.key); // Debugging keystrokes
if (event.key === 'Enter' && !event.shiftKey) {
console.log("Enter pressed"); // Debugging Enter key
event.preventDefault();
processUserInput();
}
}
function processUserInput() {
const chatInput = document.getElementById('chatInput'); // Re-select or ensure it's available
if (!chatInput) {
console.error("Cannot process user input: chatInput not found.");
return;
}
const messageText = sanitizeInput(chatInput.value.trim());
console.log("Processing user input:", messageText); // Debugging input processing
if (messageText) {
handleSendMessage(messageText) // Call chat manager function
.then(() => {
console.log("Message handled by manager.");
ui.clearChatInput();
})
.catch(error => {
console.error("Error sending message via manager:", error);
// UI should already be enabled by chat_manager's finally block
});
} else {
console.log("Input is empty, not sending.");
}
}
// --- User Data and Auth ---
async function loadUserDataForChat() {
const usernameSpan = document.getElementById('username');
const isAdminSpan = document.getElementById('isAdmin');
const adminPermissionsBtn = document.getElementById('adminPermissionsBtn');
try {
console.log("Loading user data for chat page...");
const data = await api.getUserData();
console.log("User data received:", data);
if (usernameSpan) usernameSpan.textContent = data.user || 'Unbekannt';
if (isAdminSpan) isAdminSpan.textContent = data.isAdmin ? 'Ja' : 'Nein';
if (adminPermissionsBtn) {
// Control display based on fetched data
adminPermissionsBtn.style.display = data.isAdmin ? 'inline-block' : 'none';
console.log("Admin button display set to:", adminPermissionsBtn.style.display);
}
} catch (error) {
console.error('Fehler beim Löschen des Benutzers:', error);
console.error("Failed to load user data:", error);
// Display error state in UI
if (usernameSpan) usernameSpan.textContent = 'Fehler';
if (isAdminSpan) isAdminSpan.textContent = 'Fehler';
handleApiError(error, 'Benutzerdaten laden'); // Show alert, potentially redirect
}
}
// --- Definition of checkAdminStatus --- Moved here to ensure it's defined before use
async function checkAdminStatus() {
console.log("Checking admin status...");
try {
// Assuming api.getAdminData() makes the /api/auth/admin call
// and throws an error (e.g., 401, 403) if the user is not an admin.
await api.getAdminData();
console.log("Admin access confirmed.");
// No need to return true, successful execution implies admin status
} catch (error) {
console.warn("Admin status check failed:", error.status, error.message);
if (error.status === 403 || error.status === 401) { // Forbidden or Unauthorized
showAlert('Zugriff verweigert. Nur Admins erlaubt.', 'error');
window.location.href = 'welcome.html'; // Redirect non-admins immediately
} else {
// Handle other errors (e.g., network error)
handleApiError(error, 'Admin-Status prüfen');
}
// IMPORTANT: Re-throw the error AFTER handling redirection/alert
// This signals to the caller (setupAdminPage) that the check failed.
throw error;
}
}
async function loadUserList() {
const token = localStorage.getItem('token');
const response = await fetch(`${baseUrl}/api/auth/users`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const userListDiv = document.getElementById('user-list'); // Select inside function
if (!userListDiv) {
console.error("User list container not found!");
return;
}
userListDiv.innerHTML = '<p style="padding: 15px; color: var(--text-secondary);">Lade Benutzer...</p>'; // Loading state
if (response.ok) {
const users = await response.json();
const userList = document.getElementById('user-list');
userList.innerHTML = ''; // Liste leeren
try {
const users = await api.getAllUsers();
userListDiv.innerHTML = ''; // Clear loading/previous list
users.forEach(user => {
const userDiv = document.createElement('div');
userDiv.style.display = 'flex';
userDiv.style.alignItems = 'center';
userDiv.setAttribute('id', `user-list-element-${user._id}`); // Give each div a unique ID
userDiv.innerHTML = `
<p style="flex: 0 0 25%;">Benutzername: ${user.username}</p>
<p style="flex: 0 0 25%;">Email: ${user.email}</p>
<p style="flex: 0 0 20%;">Admin: ${user.isAdmin}</p>
<button class="button-delete" style="flex: 0 0 30%;">Benutzer löschen</button>
`;
userList.appendChild(userDiv);
if (users && users.length > 0) {
users.forEach(user => {
const userDiv = document.createElement('div');
userDiv.classList.add('user-list-item');
userDiv.dataset.userId = user._id;
// Add event listener to the specific delete button
const deleteButton = userDiv.querySelector(".button-delete");
deleteButton.addEventListener("click", function() {
deleteUser(user._id, user.isAdmin);
const usernameP = document.createElement('p');
usernameP.innerHTML = `<strong>User:</strong> ${sanitizeInput(user.username)}`;
const emailP = document.createElement('p');
emailP.innerHTML = `<strong>Email:</strong> ${sanitizeInput(user.email)}`;
const adminP = document.createElement('p');
adminP.innerHTML = `<strong>Admin:</strong> ${user.isAdmin ? 'Ja' : 'Nein'}`;
const deleteButton = document.createElement('button');
deleteButton.classList.add('button-delete', 'header-btn');
deleteButton.style.backgroundColor = 'var(--error-color)';
deleteButton.textContent = 'Löschen';
deleteButton.addEventListener('click', () => handleDeleteUser(user._id, user.isAdmin, user.username)); // Pass userListDiv here if needed
userDiv.append(usernameP, emailP, adminP, deleteButton);
userListDiv.appendChild(userDiv);
});
});
} else {
showAlert('Fehler beim Laden der Benutzerliste');
} else {
userListDiv.innerHTML = '<p style="padding: 15px; color: var(--text-secondary);">Keine Benutzer gefunden.</p>';
}
} catch (error) {
handleApiError(error, 'Benutzerliste laden');
userListDiv.innerHTML = '<p style="padding: 15px; color: var(--error-color);">Fehler beim Laden der Benutzerliste.</p>';
}
}
// Überprüfen, ob wir uns auf der Admin-Seite befinden, und dann den Event-Listener hinzufügen
const loadUserListBtn = document.getElementById('loadUserListBtn');
if (loadUserListBtn) {
loadUserListBtn.addEventListener('click', loadUserList); // Benutzerliste laden, wenn der Button geklickt wird
async function handleDeleteUser(userId, isAdmin, username) {
const userListDiv = document.getElementById('user-list'); // Select inside function if needed
const message = `Möchten Sie den Benutzer "${sanitizeInput(username)}" wirklich löschen? ${isAdmin ? ' Dies ist ein Admin!' : ''}`;
const confirmed = await showConfirm(message, 'error');
if (confirmed) {
try {
await api.deleteUserById(userId);
showAlert(`Benutzer "${sanitizeInput(username)}" erfolgreich gelöscht.`, 'success');
// Remove user from list visually immediately
const userDiv = userListDiv?.querySelector(`.user-list-item[data-user-id="${userId}"]`);
if (userDiv) userDiv.remove();
// Or reload the whole list: await loadUserList();
} catch (error) {
handleApiError(error, 'Benutzer löschen');
}
}
}
// Abhängig von der Seite entweder Admin- oder Benutzerdaten laden
if (window.location.pathname.includes('admin.html')) {
// Funktion um auf die Willkommensseite zurückzukommen
document.getElementById("welcomeButton").addEventListener("click", toWelcome);
await loadAdminData();
await loadUserList();
} else if (window.location.pathname.includes('welcome.html')) {
await loadUserData();
// --- Theme Toggling ---
function toggleTheme() {
const isDarkMode = bodyElement.classList.toggle('dark-theme');
if (themeToggleButton) {
themeToggleButton.textContent = isDarkMode ? '🌙' : '☀️';
}
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
}
function loadThemePreference() {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = savedTheme === 'dark' || (!savedTheme && prefersDark);
bodyElement.classList.toggle('dark-theme', isDark); // Set class based on isDark boolean
if (themeToggleButton) { // Check if button exists before setting text
themeToggleButton.textContent = isDark ? '🌙' : '☀️';
}
}
// --- Generic Error Handling ---
function handleApiError(error, context) {
console.error(`API Error during ${context}:`, error);
if (error.status === 401 || error.status === 403) {
redirectToLogin(`Sitzung abgelaufen oder nicht autorisiert (${context}). Bitte erneut einloggen.`);
} else if (error.message.includes('Network Error')) {
showAlert(`Netzwerkfehler bei "${context}". Server nicht erreichbar.`, 'error');
} else {
// Show specific message from API if available, otherwise generic
showAlert(`Fehler bei "${context}": ${error.data?.message || error.message || 'Unbekannter Fehler'}`, 'error');
}
}
// --- Utility Functions --- (logout, redirectToLogin same as before)
function logout() {
localStorage.removeItem('token');
window.location.href = 'index.html';
}
function redirectToLogin(message) {
if (message) showAlert(message, 'error');
setTimeout(() => { window.location.href = 'index.html'; }, 1500);
}

View File

@ -1,51 +1,59 @@
// Alert Box
// Function to show the custom alert with a message and type
export function showAlert(message, type = 'error') {
export function showAlert(message, type = 'info') { // Default to info or error?
const alertBox = document.getElementById('custom-alert');
if (!alertBox) return; // Guard clause
const alertMessage = document.getElementById('alert-message');
const closeButton = document.getElementById('close-alert');
alertMessage.textContent = message; // Set the message to display
alertBox.style.display = 'block'; // Show the alert box
if(alertMessage) alertMessage.textContent = message;
alertBox.className = 'custom-alert'; // Reset classes first
alertBox.classList.add(type); // Add type class ('success', 'error', 'info')
alertBox.style.display = 'block';
// Reset previous styles
alertBox.classList.remove('success', 'error');
alertBox.classList.add(type); // Add the appropriate class (success/error)
// Add event listener to close the alert box
closeButton.addEventListener('click', () => {
alertBox.style.display = 'none'; // Hide the alert box when the button is clicked
});
// Ensure listener is only added once or managed properly
// A simple approach: remove previous before adding new
const newCloseButton = closeButton.cloneNode(true); // Clone to remove old listeners
closeButton.parentNode.replaceChild(newCloseButton, closeButton);
newCloseButton.addEventListener('click', () => {
alertBox.style.display = 'none';
}, { once: true }); // Automatically remove listener after one click
}
// Function to show a confirmation dialog
export function showConfirm(message, type = 'error') {
return new Promise((resolve) => {
const alertBox = document.getElementById('custom-alert-confirm');
const alertMessage = document.getElementById('alert-message-confirm');
const confirmBox = document.getElementById('custom-alert-confirm');
if (!confirmBox) { resolve(false); return; } // Guard clause
const confirmMessage = document.getElementById('alert-message-confirm');
const closeButton = document.getElementById('close-alert-confirm');
const confirmButton = document.getElementById('confirm-alert-confirm');
alertMessage.textContent = message; // Set the message to display
alertBox.style.display = 'block'; // Show the alert box
if(confirmMessage) confirmMessage.textContent = message;
confirmBox.className = 'custom-alert-confirm'; // Reset classes
confirmBox.classList.add(type);
confirmBox.style.display = 'block';
// Reset previous styles
alertBox.classList.remove('success', 'error');
alertBox.classList.add(type); // Add the appropriate class (success/error)
// Use cloning to ensure listeners are fresh
const newConfirmButton = confirmButton.cloneNode(true);
confirmButton.parentNode.replaceChild(newConfirmButton, confirmButton);
newConfirmButton.addEventListener('click', () => {
confirmBox.style.display = 'none';
resolve(true);
}, { once: true });
// Display the confirm button
confirmButton.style.display = 'inline-block';
closeButton.style.display = 'inline-block';
// When "Confirm" button is clicked
confirmButton.addEventListener('click', () => {
alertBox.style.display = 'none'; // Hide the alert box
resolve(true); // Resolve the promise as true (user confirmed)
});
// When "Close" button is clicked
closeButton.addEventListener('click', () => {
alertBox.style.display = 'none'; // Hide the alert box
resolve(false); // Resolve the promise as false (user canceled)
});
const newCloseButton = closeButton.cloneNode(true);
closeButton.parentNode.replaceChild(newCloseButton, closeButton);
newCloseButton.addEventListener('click', () => {
confirmBox.style.display = 'none';
resolve(false);
}, { once: true });
});
}
// Basic input sanitizer (replace HTML tags)
export function sanitizeInput(str) {
if (!str) return '';
return str.replace(/</g, "<").replace(/>/g, ">");
}

View File

@ -3,63 +3,86 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen</title>
<title>AI Chat Interface</title>
<!-- Google Font -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Link to CSS -->
<link rel="stylesheet" href="css/styles.css">
<!-- Add any necessary CSS for loading indicator here or ensure it's in styles.css -->
<style>
/* Basic Loading Spinner (if not already in styles.css) */
.loading-spinner {
border: 4px solid var(--bg-tertiary);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
margin: 0 8px; /* Adjust spacing */
flex-shrink: 0;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<!-- The modal for the alert -->
<div id="custom-alert" class="custom-alert">
<div class="alert-content">
<span id="alert-message"></span>
<button id="close-alert" class="close-btn">OK</button>
<body id="chat-page"> <!-- ****** ADD THIS ID ****** -->
<!-- Fixed Header -->
<header class="header">
<div class="header-logo">AI Chat</div>
<div class="header-user-info">
<p><strong>User:</strong> <span id="username">Laden...</span></p> <!-- Indicate Loading -->
<p><strong>Admin:</strong> <span id="isAdmin">Laden...</span></p> <!-- Indicate Loading -->
</div>
</div>
<!-- Header für Benutzer-Info und Buttons -->
<header class="header">
<h1 class="header-welcome">Ein simples Chat-Interface.</h1>
<div class="header-buttons">
<button id="themeToggleBtn" class="header-btn theme-toggle" aria-label="Toggle Theme">☀️</button>
<button id="adminPermissionsBtn" class="header-btn" style="display: none;">Admin</button>
<button id="logoutBtn" class="header-btn">Abmelden</button>
</div>
</header>
<div class="header-user-info">
<p><strong>Benutzername:</strong> <span id="username"></span></p>
<p><strong>Admin:</strong> <span id="isAdmin"></span></p>
</div>
<div class="header-buttons">
<button id="adminPermissionsBtn" class="header-btn" style="display: none;">Admin-Einstellungen</button>
<button id="logoutBtn" class="header-btn">Abmelden</button>
</div>
</header>
<!-- Freier Platz für zukünftige Features -->
<!-- Main Content Area -->
<div class="main-container">
<!-- Linke Seitenleiste für Chat-Sessions -->
<div class="sidebar" id="chatSidebar">
<h3>Gespeicherte Chats</h3>
<button id="newChatButton">Neuer Chat</button>
<!-- Sidebar -->
<aside class="sidebar" id="chatSidebar">
<h3>Chats</h3>
<button id="newChatButton" class="sidebar-btn new-chat-btn">Neuer Chat</button>
<div id="chatList"></div>
</div>
</aside>
<!-- Chatbereich -->
<div class="chat-container">
<!-- Main Chat Area -->
<main class="chat-container">
<!-- Progress Bar -->
<div id="progress-main-div">
<div id="progress-container" style="width: 100%; background-color: #f3f3f3; border-radius: 5px; height: 20px;">
<div id="progress-bar" style="width: 0; background-color: #4caf50; height: 100%; border-radius: 5px;"></div>
</div>
<p id="usage-text" style="text-align: center; margin-top: 10px;">0 / 70144 used</p>
<div id="progress-container"><div id="progress-bar"></div></div>
<p id="usage-text">Token Usage: 0 / 0</p>
</div>
<div class="chat-box" id="chatBox">
<!-- Nachrichten werden hier angezeigt -->
<!-- Chat Box -->
<div class="chat-box" id="chatBox" aria-live="polite">
<div class="chat-message assistant">Wähle einen Chat oder starte einen neuen.</div>
</div>
<!-- Input Area -->
<div class="chat-input-container">
<textarea id="chatInput" class="chat-input" placeholder="Schreibe eine Nachricht..."></textarea>
<button id="sendButton" class="chat-send-button">Senden</button>
<textarea id="chatInput" class="chat-input" placeholder="Schreibe eine Nachricht..." rows="1" aria-label="Chat message input"></textarea>
<div id="loadingIndicator" class="loading-spinner" style="display: none;"></div>
<button id="sendButton" class="chat-send-button" aria-label="Send message"></button>
</div>
</div>
</main>
</div>
<!-- Modals (ensure these are correctly defined) -->
<div id="custom-alert" class="custom-alert"><div class="alert-content"><span id="alert-message"></span><button id="close-alert" class="close-btn">OK</button></div></div>
<div id="custom-alert-confirm" class="custom-alert-confirm"><div class="alert-content"><p id="alert-message-confirm"></p><div style="display: flex; gap: 10px; margin-top: 10px;"><button id="confirm-alert-confirm">Bestätigen</button><button id="close-alert-confirm">Abbrechen</button></div></div></div>
<!-- Scripts -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
<script type="module" src="js/config.js"></script>
<script type="module" src="js/shared_functions.js"></script>
<script type="module" src="js/chat.js"></script>
<script type="module" src="js/main.js"></script>
<script type="module" src="js/api_service.js"></script>
<script type="module" src="js/chat_ui.js"></script>
<script type="module" src="js/chat_manager.js"></script>
<script type="module" src="js/main.js"></script> <!-- main.js should be last for setup -->
</body>
</html>
</html>