refactor + new look
This commit is contained in:
parent
e98dad71e5
commit
67c0e9e8af
@ -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
@ -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
168
client/js/api_service.js
Normal 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
256
client/js/chat_manager.js
Normal 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
193
client/js/chat_ui.js
Normal 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
3
client/js/config.js
Normal 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
|
@ -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);
|
||||
}
|
@ -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, ">");
|
||||
}
|
@ -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>
|
Loading…
Reference in New Issue
Block a user