// This file is kept for backward compatibility // It will load the new modular JS files // Load the utility functions const utilsScript = document.createElement('script'); utilsScript.src = 'js/utils.js'; document.head.appendChild(utilsScript); // Load the main script const mainScript = document.createElement('script'); mainScript.src = 'js/main.js'; document.head.appendChild(mainScript); document.addEventListener('DOMContentLoaded', () => { // Auth elements const loginButton = document.getElementById('login-button'); const logoutButton = document.getElementById('logout-button'); const authSection = document.getElementById('auth-section'); const dashboardSection = document.getElementById('dashboard-container'); const usernameSpan = document.getElementById('username'); // Navigation elements const navServerSettings = document.getElementById('nav-server-settings'); const navAiSettings = document.getElementById('nav-ai-settings'); const navConversations = document.getElementById('nav-conversations'); // Section elements const serverSettingsSection = document.getElementById('server-settings-section'); const aiSettingsSection = document.getElementById('ai-settings-section'); const conversationsSection = document.getElementById('conversations-section'); // Server settings elements const guildSelect = document.getElementById('guild-select'); const settingsForm = document.getElementById('settings-form'); // --- API Base URL (Adjust if needed) --- // Assuming the API runs on the same host/port for simplicity, // otherwise, use the full URL like 'http://localhost:8000' // IMPORTANT: This will need to be updated to the new merged endpoint prefix, e.g., /dashboard/api const API_BASE_URL = '/dashboard/api'; // Tentative new prefix // --- Helper Functions --- async function fetchAPI(endpoint, options = {}) { // Add authentication headers if needed (e.g., from cookies or localStorage) // For now, assuming cookies handle session management automatically try { const response = await fetch(`${API_BASE_URL}${endpoint}`, options); if (response.status === 401) { // Unauthorized showLogin(); throw new Error('Unauthorized'); } if (!response.ok) { const errorData = await response.json().catch(() => ({ detail: 'Unknown error' })); throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); } if (response.status === 204) { // No Content return null; } return await response.json(); } catch (error) { console.error('API Fetch Error:', error); // Display error to user? throw error; // Re-throw for specific handlers } } function showLogin() { authSection.style.display = 'block'; dashboardSection.style.display = 'none'; settingsForm.style.display = 'none'; guildSelect.value = ''; // Reset guild selection } function showDashboard(userData) { authSection.style.display = 'none'; dashboardSection.style.display = 'block'; usernameSpan.textContent = userData.username; // Show server settings section by default showSection('server-settings'); // Load guilds for server settings loadGuilds(); } function displayFeedback(elementId, message, isError = false) { const feedbackElement = document.getElementById(elementId); if (feedbackElement) { feedbackElement.textContent = message; feedbackElement.className = isError ? 'error' : ''; // Clear feedback after a few seconds setTimeout(() => { feedbackElement.textContent = ''; feedbackElement.className = ''; }, 5000); } } // --- Authentication --- async function checkLoginStatus() { try { // Use the new endpoint path const userData = await fetchAPI('/user/me'); if (userData) { showDashboard(userData); } else { showLogin(); } } catch (error) { // If fetching /user/me fails (e.g., 401), show login showLogin(); } } loginButton.addEventListener('click', () => { // Redirect to backend login endpoint which will redirect to Discord // Use the new endpoint path window.location.href = `${API_BASE_URL}/auth/login`; }); logoutButton.addEventListener('click', async () => { try { // Use the new endpoint path await fetchAPI('/auth/logout', { method: 'POST' }); showLogin(); } catch (error) { alert('Logout failed. Please try again.'); } }); // --- Guild Loading and Settings --- async function loadGuilds() { try { // Use the new endpoint path const guilds = await fetchAPI('/user/guilds'); guildSelect.innerHTML = ''; // Reset guilds.forEach(guild => { // Only add guilds where the user is an administrator (assuming API filters this) // Or filter here based on permissions if API doesn't // const isAdmin = (parseInt(guild.permissions) & 0x8) === 0x8; // Check ADMINISTRATOR bit // if (isAdmin) { const option = document.createElement('option'); option.value = guild.id; option.textContent = guild.name; guildSelect.appendChild(option); // } }); } catch (error) { displayFeedback('guild-select-feedback', `Error loading guilds: ${error.message}`, true); // Add a feedback element if needed } } guildSelect.addEventListener('change', async (event) => { const guildId = event.target.value; if (guildId) { await loadSettings(guildId); settingsForm.style.display = 'block'; } else { settingsForm.style.display = 'none'; } }); async function loadSettings(guildId) { console.log(`Loading settings for guild ${guildId}`); // Clear previous settings document.getElementById('prefix-input').value = ''; document.getElementById('welcome-channel').value = ''; document.getElementById('welcome-message').value = ''; document.getElementById('goodbye-channel').value = ''; document.getElementById('goodbye-message').value = ''; document.getElementById('cogs-list').innerHTML = ''; document.getElementById('current-perms').innerHTML = ''; // Clear channel dropdowns document.getElementById('welcome-channel-select').innerHTML = ''; document.getElementById('goodbye-channel-select').innerHTML = ''; try { // Load guild channels for dropdowns await loadGuildChannels(guildId); // Use the new endpoint path const settings = await fetchAPI(`/guilds/${guildId}/settings`); console.log("Received settings:", settings); // Populate Prefix document.getElementById('prefix-input').value = settings.prefix || ''; // Populate Welcome/Goodbye Channel IDs document.getElementById('welcome-channel').value = settings.welcome_channel_id || ''; document.getElementById('welcome-message').value = settings.welcome_message || ''; document.getElementById('goodbye-channel').value = settings.goodbye_channel_id || ''; document.getElementById('goodbye-message').value = settings.goodbye_message || ''; // Set the channel dropdowns to match the channel IDs if (settings.welcome_channel_id) { const welcomeChannelSelect = document.getElementById('welcome-channel-select'); if (welcomeChannelSelect.querySelector(`option[value="${settings.welcome_channel_id}"]`)) { welcomeChannelSelect.value = settings.welcome_channel_id; } } if (settings.goodbye_channel_id) { const goodbyeChannelSelect = document.getElementById('goodbye-channel-select'); if (goodbyeChannelSelect.querySelector(`option[value="${settings.goodbye_channel_id}"]`)) { goodbyeChannelSelect.value = settings.goodbye_channel_id; } } // Populate Cogs // TODO: Need a way to get the *full* list of available cogs from the bot/API // For now, just display the ones returned by the settings endpoint populateCogsList(settings.enabled_cogs || {}); // Populate Command Permissions // TODO: Fetch roles and commands for dropdowns await loadCommandPermissions(guildId); // Load guild roles for the role dropdown await loadGuildRoles(guildId); // Load commands for the command dropdown await loadCommands(guildId); } catch (error) { displayFeedback('prefix-feedback', `Error loading settings: ${error.message}`, true); } } async function loadGuildChannels(guildId) { try { // Fetch channels from the API const channels = await fetchAPI(`/guilds/${guildId}/channels`); // Get the channel select dropdowns const welcomeChannelSelect = document.getElementById('welcome-channel-select'); const goodbyeChannelSelect = document.getElementById('goodbye-channel-select'); // Clear existing options except the default welcomeChannelSelect.innerHTML = ''; goodbyeChannelSelect.innerHTML = ''; // Add text channels to the dropdowns channels.filter(channel => channel.type === 0).forEach(channel => { const option = document.createElement('option'); option.value = channel.id; option.textContent = `#${channel.name}`; // Add to both dropdowns welcomeChannelSelect.appendChild(option.cloneNode(true)); goodbyeChannelSelect.appendChild(option); }); // Add event listeners to sync the dropdowns with the text inputs welcomeChannelSelect.addEventListener('change', function() { document.getElementById('welcome-channel').value = this.value; }); goodbyeChannelSelect.addEventListener('change', function() { document.getElementById('goodbye-channel').value = this.value; }); } catch (error) { console.error('Error loading guild channels:', error); } } async function loadGuildRoles(guildId) { try { // Fetch roles from the API const roles = await fetchAPI(`/guilds/${guildId}/roles`); // Get the role select dropdown const roleSelect = document.getElementById('role-select'); // Clear existing options except the default roleSelect.innerHTML = ''; // Add roles to the dropdown roles.forEach(role => { // Skip @everyone role if (role.name === '@everyone') return; const option = document.createElement('option'); option.value = role.id; option.textContent = role.name; roleSelect.appendChild(option); }); } catch (error) { console.error('Error loading guild roles:', error); } } async function loadCommands(guildId) { try { // Fetch commands from the API const commands = await fetchAPI(`/guilds/${guildId}/commands`); // Get the command select dropdown const commandSelect = document.getElementById('command-select'); // Clear existing options except the default commandSelect.innerHTML = ''; // Add commands to the dropdown commands.forEach(command => { const option = document.createElement('option'); option.value = command.name; option.textContent = command.name; commandSelect.appendChild(option); }); } catch (error) { console.error('Error loading commands:', error); } } function populateCogsList(cogsStatus) { // This function now only displays cogs whose status is stored in the DB // and returned by the API. It doesn't know about *all* possible cogs. const cogsListDiv = document.getElementById('cogs-list'); cogsListDiv.innerHTML = ''; // Clear previous // Assuming CORE_COGS is available globally or passed somehow // TODO: Get this list from the API or config const CORE_COGS = ['SettingsCog', 'HelpCog']; // Example - needs to match backend // TODO: Fetch the *full* list of cogs from the bot/API to display all options // For now, only showing cogs already in the settings response Object.entries(cogsStatus).sort().forEach(([cogName, isEnabled]) => { const div = document.createElement('div'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `cog-${cogName}`; checkbox.name = cogName; checkbox.checked = isEnabled; checkbox.disabled = CORE_COGS.includes(cogName); // Disable core cogs const label = document.createElement('label'); label.htmlFor = `cog-${cogName}`; label.textContent = cogName + (CORE_COGS.includes(cogName) ? ' (Core)' : ''); div.appendChild(checkbox); div.appendChild(label); cogsListDiv.appendChild(div); }); } async function loadCommandPermissions(guildId) { const permsDiv = document.getElementById('current-perms'); permsDiv.innerHTML = 'Loading permissions...'; try { // Use the new endpoint path const permData = await fetchAPI(`/guilds/${guildId}/permissions`); permsDiv.innerHTML = ''; // Clear loading message if (Object.keys(permData.permissions).length === 0) { permsDiv.innerHTML = 'No specific command permissions set. All roles can use all enabled commands (unless restricted by default).'; return; } // TODO: Fetch role names from Discord API or bot API to display names instead of IDs for (const [commandName, roleIds] of Object.entries(permData.permissions).sort()) { const rolesStr = roleIds.map(id => `Role ID: ${id}`).join(', '); // Placeholder until role names are fetched const div = document.createElement('div'); div.innerHTML = `Command ${commandName} allowed for: ${rolesStr}`; permsDiv.appendChild(div); } } catch (error) { permsDiv.innerHTML = `Error loading permissions: ${error.message}`; } } // --- Save Settings Event Listeners --- document.getElementById('save-prefix-button').addEventListener('click', async () => { const guildId = guildSelect.value; const prefix = document.getElementById('prefix-input').value; if (!guildId) return; try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/settings`, { method: 'PATCH', // Use PATCH for partial updates headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prefix: prefix }) }); displayFeedback('prefix-feedback', 'Prefix saved successfully!'); } catch (error) { displayFeedback('prefix-feedback', `Error saving prefix: ${error.message}`, true); } }); document.getElementById('save-welcome-button').addEventListener('click', async () => { const guildId = guildSelect.value; const channelIdInput = document.getElementById('welcome-channel').value; const message = document.getElementById('welcome-message').value; if (!guildId) return; // Basic validation for channel ID (numeric) const channelId = channelIdInput && /^\d+$/.test(channelIdInput) ? channelIdInput : null; try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ welcome_channel_id: channelId, // Send numeric ID or null welcome_message: message }) }); displayFeedback('welcome-feedback', 'Welcome settings saved!'); } catch (error) { displayFeedback('welcome-feedback', `Error saving welcome settings: ${error.message}`, true); } }); document.getElementById('disable-welcome-button').addEventListener('click', async () => { const guildId = guildSelect.value; if (!guildId) return; if (!confirm('Are you sure you want to disable welcome messages?')) return; try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ welcome_channel_id: null, welcome_message: null // Also clear message template maybe? Or just channel? Let's clear both. }) }); // Clear the form fields visually document.getElementById('welcome-channel').value = ''; document.getElementById('welcome-message').value = ''; displayFeedback('welcome-feedback', 'Welcome messages disabled.'); } catch (error) { displayFeedback('welcome-feedback', `Error disabling welcome messages: ${error.message}`, true); } }); document.getElementById('save-goodbye-button').addEventListener('click', async () => { const guildId = guildSelect.value; const channelIdInput = document.getElementById('goodbye-channel').value; const message = document.getElementById('goodbye-message').value; if (!guildId) return; const channelId = channelIdInput && /^\d+$/.test(channelIdInput) ? channelIdInput : null; try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ goodbye_channel_id: channelId, goodbye_message: message }) }); displayFeedback('goodbye-feedback', 'Goodbye settings saved!'); } catch (error) { displayFeedback('goodbye-feedback', `Error saving goodbye settings: ${error.message}`, true); } }); document.getElementById('disable-goodbye-button').addEventListener('click', async () => { const guildId = guildSelect.value; if (!guildId) return; if (!confirm('Are you sure you want to disable goodbye messages?')) return; try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ goodbye_channel_id: null, goodbye_message: null }) }); document.getElementById('goodbye-channel').value = ''; document.getElementById('goodbye-message').value = ''; displayFeedback('goodbye-feedback', 'Goodbye messages disabled.'); } catch (error) { displayFeedback('goodbye-feedback', `Error disabling goodbye messages: ${error.message}`, true); } }); document.getElementById('save-cogs-button').addEventListener('click', async () => { const guildId = guildSelect.value; if (!guildId) return; const cogsPayload = {}; const checkboxes = document.querySelectorAll('#cogs-list input[type="checkbox"]'); checkboxes.forEach(cb => { if (!cb.disabled) { // Don't send status for disabled (core) cogs cogsPayload[cb.name] = cb.checked; } }); try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/settings`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cogs: cogsPayload }) }); displayFeedback('cogs-feedback', 'Module settings saved!'); } catch (error) { displayFeedback('cogs-feedback', `Error saving module settings: ${error.message}`, true); } }); // --- Command Permissions Event Listeners --- document.getElementById('add-perm-button').addEventListener('click', async () => { const guildId = guildSelect.value; const commandName = document.getElementById('command-select').value; const roleId = document.getElementById('role-select').value; if (!guildId || !commandName || !roleId) { displayFeedback('perms-feedback', 'Please select a command and a role.', true); return; } try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/permissions`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command_name: commandName, role_id: roleId }) }); displayFeedback('perms-feedback', `Permission added for ${commandName}.`); await loadCommandPermissions(guildId); // Refresh list } catch (error) { displayFeedback('perms-feedback', `Error adding permission: ${error.message}`, true); } }); document.getElementById('remove-perm-button').addEventListener('click', async () => { const guildId = guildSelect.value; const commandName = document.getElementById('command-select').value; const roleId = document.getElementById('role-select').value; if (!guildId || !commandName || !roleId) { displayFeedback('perms-feedback', 'Please select a command and a role to remove.', true); return; } if (!confirm(`Are you sure you want to remove permission for role ID ${roleId} from command ${commandName}?`)) return; try { // Use the new endpoint path await fetchAPI(`/guilds/${guildId}/permissions`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command_name: commandName, role_id: roleId }) }); displayFeedback('perms-feedback', `Permission removed for ${commandName}.`); await loadCommandPermissions(guildId); // Refresh list } catch (error) { displayFeedback('perms-feedback', `Error removing permission: ${error.message}`, true); } }); // --- Navigation Functions --- function showSection(sectionId) { // Hide all sections serverSettingsSection.style.display = 'none'; aiSettingsSection.style.display = 'none'; conversationsSection.style.display = 'none'; // Remove active class from all nav buttons navServerSettings.classList.remove('active'); navAiSettings.classList.remove('active'); navConversations.classList.remove('active'); // Show the selected section and activate the corresponding nav button switch(sectionId) { case 'server-settings': serverSettingsSection.style.display = 'block'; navServerSettings.classList.add('active'); break; case 'ai-settings': aiSettingsSection.style.display = 'block'; navAiSettings.classList.add('active'); // Load AI settings if not already loaded if (!aiSettingsLoaded) { loadAiSettings(); } break; case 'conversations': conversationsSection.style.display = 'block'; navConversations.classList.add('active'); // Load conversations if not already loaded if (!conversationsLoaded) { loadConversations(); } break; default: serverSettingsSection.style.display = 'block'; navServerSettings.classList.add('active'); } } // --- Navigation Event Listeners --- navServerSettings.addEventListener('click', () => showSection('server-settings')); navAiSettings.addEventListener('click', () => showSection('ai-settings')); navConversations.addEventListener('click', () => showSection('conversations')); // --- AI Settings Functions --- async function loadAiSettings() { try { const response = await fetchAPI('/settings'); const settings = response.settings || response.user_settings; if (settings) { // Populate AI model dropdown const modelSelect = document.getElementById('ai-model-select'); if (settings.model_id) { // Find the option with the matching value or create a new one if it doesn't exist let option = Array.from(modelSelect.options).find(opt => opt.value === settings.model_id); if (!option) { option = new Option(settings.model_id, settings.model_id); modelSelect.add(option); } modelSelect.value = settings.model_id; } // Set temperature const temperatureSlider = document.getElementById('ai-temperature'); const temperatureValue = document.getElementById('temperature-value'); if (settings.temperature !== undefined) { temperatureSlider.value = settings.temperature; temperatureValue.textContent = settings.temperature; } // Set max tokens const maxTokensInput = document.getElementById('ai-max-tokens'); if (settings.max_tokens !== undefined) { maxTokensInput.value = settings.max_tokens; } // Set reasoning settings const reasoningCheckbox = document.getElementById('ai-reasoning-enabled'); const reasoningEffortSelect = document.getElementById('ai-reasoning-effort'); const reasoningEffortGroup = document.getElementById('reasoning-effort-group'); if (settings.reasoning_enabled !== undefined) { reasoningCheckbox.checked = settings.reasoning_enabled; reasoningEffortGroup.style.display = settings.reasoning_enabled ? 'block' : 'none'; } if (settings.reasoning_effort) { reasoningEffortSelect.value = settings.reasoning_effort; } // Set web search const webSearchCheckbox = document.getElementById('ai-web-search-enabled'); if (settings.web_search_enabled !== undefined) { webSearchCheckbox.checked = settings.web_search_enabled; } // Set system prompt const systemPromptTextarea = document.getElementById('ai-system-prompt'); if (settings.system_message) { systemPromptTextarea.value = settings.system_message; } // Set character settings const characterInput = document.getElementById('ai-character'); const characterInfoTextarea = document.getElementById('ai-character-info'); const characterBreakdownCheckbox = document.getElementById('ai-character-breakdown'); if (settings.character) { characterInput.value = settings.character; } if (settings.character_info) { characterInfoTextarea.value = settings.character_info; } if (settings.character_breakdown !== undefined) { characterBreakdownCheckbox.checked = settings.character_breakdown; } // Set custom instructions const customInstructionsTextarea = document.getElementById('ai-custom-instructions'); if (settings.custom_instructions) { customInstructionsTextarea.value = settings.custom_instructions; } aiSettingsLoaded = true; displayFeedback('ai-settings-feedback', 'AI settings loaded successfully.'); } } catch (error) { displayFeedback('ai-settings-feedback', `Error loading AI settings: ${error.message}`, true); } } // --- AI Settings Event Listeners --- // Temperature slider document.getElementById('ai-temperature').addEventListener('input', function() { document.getElementById('temperature-value').textContent = this.value; }); // Reasoning checkbox document.getElementById('ai-reasoning-enabled').addEventListener('change', function() { document.getElementById('reasoning-effort-group').style.display = this.checked ? 'block' : 'none'; }); // Save AI Settings button document.getElementById('save-ai-settings-button').addEventListener('click', async () => { try { const settings = { model_id: document.getElementById('ai-model-select').value, temperature: parseFloat(document.getElementById('ai-temperature').value), max_tokens: parseInt(document.getElementById('ai-max-tokens').value), reasoning_enabled: document.getElementById('ai-reasoning-enabled').checked, reasoning_effort: document.getElementById('ai-reasoning-effort').value, web_search_enabled: document.getElementById('ai-web-search-enabled').checked }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); displayFeedback('ai-settings-feedback', 'AI settings saved successfully!'); } catch (error) { displayFeedback('ai-settings-feedback', `Error saving AI settings: ${error.message}`, true); } }); // Reset AI Settings button document.getElementById('reset-ai-settings-button').addEventListener('click', async () => { if (!confirm('Are you sure you want to reset AI settings to defaults?')) return; try { const defaultSettings = { model_id: "openai/gpt-3.5-turbo", temperature: 0.7, max_tokens: 1000, reasoning_enabled: false, reasoning_effort: "medium", web_search_enabled: false }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings: defaultSettings }) }); // Update UI with default values document.getElementById('ai-model-select').value = defaultSettings.model_id; document.getElementById('ai-temperature').value = defaultSettings.temperature; document.getElementById('temperature-value').textContent = defaultSettings.temperature; document.getElementById('ai-max-tokens').value = defaultSettings.max_tokens; document.getElementById('ai-reasoning-enabled').checked = defaultSettings.reasoning_enabled; document.getElementById('reasoning-effort-group').style.display = defaultSettings.reasoning_enabled ? 'block' : 'none'; document.getElementById('ai-reasoning-effort').value = defaultSettings.reasoning_effort; document.getElementById('ai-web-search-enabled').checked = defaultSettings.web_search_enabled; displayFeedback('ai-settings-feedback', 'AI settings reset to defaults.'); } catch (error) { displayFeedback('ai-settings-feedback', `Error resetting AI settings: ${error.message}`, true); } }); // Save Character Settings button document.getElementById('save-character-settings-button').addEventListener('click', async () => { try { const settings = { character: document.getElementById('ai-character').value, character_info: document.getElementById('ai-character-info').value, character_breakdown: document.getElementById('ai-character-breakdown').checked }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); displayFeedback('character-settings-feedback', 'Character settings saved successfully!'); } catch (error) { displayFeedback('character-settings-feedback', `Error saving character settings: ${error.message}`, true); } }); // Clear Character button document.getElementById('clear-character-settings-button').addEventListener('click', async () => { if (!confirm('Are you sure you want to clear character settings?')) return; try { const settings = { character: null, character_info: null, character_breakdown: false }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); // Clear UI document.getElementById('ai-character').value = ''; document.getElementById('ai-character-info').value = ''; document.getElementById('ai-character-breakdown').checked = false; displayFeedback('character-settings-feedback', 'Character settings cleared.'); } catch (error) { displayFeedback('character-settings-feedback', `Error clearing character settings: ${error.message}`, true); } }); // Save System Prompt button document.getElementById('save-system-prompt-button').addEventListener('click', async () => { try { const settings = { system_message: document.getElementById('ai-system-prompt').value }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); displayFeedback('system-prompt-feedback', 'System prompt saved successfully!'); } catch (error) { displayFeedback('system-prompt-feedback', `Error saving system prompt: ${error.message}`, true); } }); // Reset System Prompt button document.getElementById('reset-system-prompt-button').addEventListener('click', async () => { if (!confirm('Are you sure you want to reset the system prompt to default?')) return; try { const settings = { system_message: null }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); // Clear UI document.getElementById('ai-system-prompt').value = ''; displayFeedback('system-prompt-feedback', 'System prompt reset to default.'); } catch (error) { displayFeedback('system-prompt-feedback', `Error resetting system prompt: ${error.message}`, true); } }); // Save Custom Instructions button document.getElementById('save-custom-instructions-button').addEventListener('click', async () => { try { const settings = { custom_instructions: document.getElementById('ai-custom-instructions').value }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); displayFeedback('custom-instructions-feedback', 'Custom instructions saved successfully!'); } catch (error) { displayFeedback('custom-instructions-feedback', `Error saving custom instructions: ${error.message}`, true); } }); // Clear Custom Instructions button document.getElementById('clear-custom-instructions-button').addEventListener('click', async () => { if (!confirm('Are you sure you want to clear custom instructions?')) return; try { const settings = { custom_instructions: null }; await fetchAPI('/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ settings }) }); // Clear UI document.getElementById('ai-custom-instructions').value = ''; displayFeedback('custom-instructions-feedback', 'Custom instructions cleared.'); } catch (error) { displayFeedback('custom-instructions-feedback', `Error clearing custom instructions: ${error.message}`, true); } }); // --- Conversations Functions --- let currentConversations = []; let selectedConversationId = null; async function loadConversations() { try { const response = await fetchAPI('/conversations'); currentConversations = response.conversations || []; renderConversationsList(); conversationsLoaded = true; if (currentConversations.length === 0) { // Show the "no conversations" message document.querySelector('.no-conversations').style.display = 'block'; document.getElementById('conversation-detail').style.display = 'none'; } else { document.querySelector('.no-conversations').style.display = 'none'; } } catch (error) { console.error('Error loading conversations:', error); document.querySelector('.no-conversations').textContent = `Error loading conversations: ${error.message}`; } } function renderConversationsList() { const conversationsList = document.getElementById('conversations-list'); const noConversationsMessage = document.querySelector('.no-conversations'); // Clear existing conversations except the "no conversations" message Array.from(conversationsList.children).forEach(child => { if (!child.classList.contains('no-conversations')) { conversationsList.removeChild(child); } }); if (currentConversations.length === 0) { noConversationsMessage.style.display = 'block'; return; } noConversationsMessage.style.display = 'none'; // Sort conversations by updated_at (newest first) const sortedConversations = [...currentConversations].sort((a, b) => { return new Date(b.updated_at) - new Date(a.updated_at); }); // Add conversations to the list sortedConversations.forEach(conversation => { const conversationItem = document.createElement('div'); conversationItem.className = 'conversation-item'; conversationItem.dataset.id = conversation.id; if (conversation.id === selectedConversationId) { conversationItem.classList.add('active'); } // Get the last message for preview let previewText = 'No messages'; if (conversation.messages && conversation.messages.length > 0) { const lastMessage = conversation.messages[conversation.messages.length - 1]; previewText = lastMessage.content.substring(0, 100) + (lastMessage.content.length > 100 ? '...' : ''); } // Format the date const date = new Date(conversation.updated_at); const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); conversationItem.innerHTML = `

${conversation.title}

${formattedDate}
${previewText}
`; conversationItem.addEventListener('click', () => { // Deselect previously selected conversation const previouslySelected = document.querySelector('.conversation-item.active'); if (previouslySelected) { previouslySelected.classList.remove('active'); } // Select this conversation conversationItem.classList.add('active'); selectedConversationId = conversation.id; // Show conversation details showConversationDetail(conversation); }); conversationsList.appendChild(conversationItem); }); } function showConversationDetail(conversation) { const conversationDetail = document.getElementById('conversation-detail'); const conversationTitle = document.getElementById('conversation-title'); const conversationMessages = document.getElementById('conversation-messages'); // Show the detail section conversationDetail.style.display = 'block'; // Set the title conversationTitle.textContent = conversation.title; // Clear existing messages conversationMessages.innerHTML = ''; // Add messages if (conversation.messages && conversation.messages.length > 0) { conversation.messages.forEach(message => { const messageDiv = document.createElement('div'); messageDiv.className = `message ${message.role === 'user' ? 'user-message' : 'ai-message'}`; messageDiv.innerHTML = `
${message.role === 'user' ? 'You' : 'AI'}
${message.content}
`; conversationMessages.appendChild(messageDiv); }); // Scroll to the bottom conversationMessages.scrollTop = conversationMessages.scrollHeight; } else { // No messages const emptyMessage = document.createElement('div'); emptyMessage.className = 'no-messages'; emptyMessage.textContent = 'This conversation has no messages.'; conversationMessages.appendChild(emptyMessage); } } async function deleteConversation(conversationId) { try { await fetchAPI(`/conversations/${conversationId}`, { method: 'DELETE' }); // Remove from the current conversations array currentConversations = currentConversations.filter(conv => conv.id !== conversationId); // If the deleted conversation was selected, clear the selection if (selectedConversationId === conversationId) { selectedConversationId = null; document.getElementById('conversation-detail').style.display = 'none'; } // Re-render the list renderConversationsList(); if (currentConversations.length === 0) { document.querySelector('.no-conversations').style.display = 'block'; } return true; } catch (error) { console.error('Error deleting conversation:', error); return false; } } async function renameConversation(conversationId, newTitle) { try { // Find the conversation const conversation = currentConversations.find(conv => conv.id === conversationId); if (!conversation) { throw new Error('Conversation not found'); } // Update the title conversation.title = newTitle; // Save to the server await fetchAPI('/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation }) }); // Re-render the list renderConversationsList(); // Update the detail view if this conversation is selected if (selectedConversationId === conversationId) { document.getElementById('conversation-title').textContent = newTitle; } return true; } catch (error) { console.error('Error renaming conversation:', error); return false; } } async function createNewConversation(title) { try { // Create a new conversation object const newConversation = { id: crypto.randomUUID ? crypto.randomUUID() : `conv-${Date.now()}`, title: title || 'New Conversation', messages: [], created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; // Save to the server const savedConversation = await fetchAPI('/conversations', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation: newConversation }) }); // Add to the current conversations array currentConversations.push(savedConversation); // Re-render the list renderConversationsList(); // Select the new conversation selectedConversationId = savedConversation.id; showConversationDetail(savedConversation); // Hide the "no conversations" message document.querySelector('.no-conversations').style.display = 'none'; return savedConversation; } catch (error) { console.error('Error creating conversation:', error); return null; } } function exportConversation(conversation) { // Create a JSON string of the conversation const conversationJson = JSON.stringify(conversation, null, 2); // Create a blob with the JSON data const blob = new Blob([conversationJson], { type: 'application/json' }); // Create a URL for the blob const url = URL.createObjectURL(blob); // Create a temporary link element const link = document.createElement('a'); link.href = url; link.download = `conversation-${conversation.id}.json`; // Append the link to the body document.body.appendChild(link); // Click the link to trigger the download link.click(); // Clean up document.body.removeChild(link); URL.revokeObjectURL(url); } // --- Conversation Event Listeners --- // New Conversation button document.getElementById('new-conversation-button').addEventListener('click', () => { // Show the new conversation modal const modal = document.getElementById('new-conversation-modal'); modal.style.display = 'block'; // Focus the input document.getElementById('new-conversation-name').focus(); }); // Create Conversation button (in modal) document.getElementById('create-conversation-button').addEventListener('click', async () => { const title = document.getElementById('new-conversation-name').value.trim() || 'New Conversation'; const newConversation = await createNewConversation(title); if (newConversation) { // Close the modal document.getElementById('new-conversation-modal').style.display = 'none'; // Clear the input document.getElementById('new-conversation-name').value = ''; } }); // Cancel Create button (in modal) document.getElementById('cancel-create-button').addEventListener('click', () => { // Close the modal document.getElementById('new-conversation-modal').style.display = 'none'; // Clear the input document.getElementById('new-conversation-name').value = ''; }); // Close modal buttons document.querySelectorAll('.close-modal').forEach(closeButton => { closeButton.addEventListener('click', () => { // Find the parent modal const modal = closeButton.closest('.modal'); modal.style.display = 'none'; }); }); // Delete Conversation button document.getElementById('delete-conversation-button').addEventListener('click', async () => { if (!selectedConversationId) return; if (confirm('Are you sure you want to delete this conversation? This action cannot be undone.')) { const success = await deleteConversation(selectedConversationId); if (success) { // Hide the detail view document.getElementById('conversation-detail').style.display = 'none'; } else { alert('Failed to delete conversation. Please try again.'); } } }); // Rename Conversation button document.getElementById('rename-conversation-button').addEventListener('click', () => { if (!selectedConversationId) return; // Show the rename modal const modal = document.getElementById('rename-modal'); modal.style.display = 'block'; // Set the current title as the default value const conversation = currentConversations.find(conv => conv.id === selectedConversationId); if (conversation) { document.getElementById('new-conversation-title').value = conversation.title; } // Focus the input document.getElementById('new-conversation-title').focus(); }); // Confirm Rename button (in modal) document.getElementById('confirm-rename-button').addEventListener('click', async () => { if (!selectedConversationId) return; const newTitle = document.getElementById('new-conversation-title').value.trim(); if (!newTitle) { alert('Please enter a title for the conversation.'); return; } const success = await renameConversation(selectedConversationId, newTitle); if (success) { // Close the modal document.getElementById('rename-modal').style.display = 'none'; } else { alert('Failed to rename conversation. Please try again.'); } }); // Cancel Rename button (in modal) document.getElementById('cancel-rename-button').addEventListener('click', () => { // Close the modal document.getElementById('rename-modal').style.display = 'none'; }); // Export Conversation button document.getElementById('export-conversation-button').addEventListener('click', () => { if (!selectedConversationId) return; const conversation = currentConversations.find(conv => conv.id === selectedConversationId); if (conversation) { exportConversation(conversation); } }); // Conversation Search document.getElementById('conversation-search').addEventListener('input', function() { const searchTerm = this.value.toLowerCase().trim(); if (!searchTerm) { // If search is empty, show all conversations renderConversationsList(); return; } // Filter conversations by title and content const filteredConversations = currentConversations.filter(conversation => { // Check title if (conversation.title.toLowerCase().includes(searchTerm)) { return true; } // Check message content if (conversation.messages && conversation.messages.length > 0) { return conversation.messages.some(message => message.content.toLowerCase().includes(searchTerm) ); } return false; }); // Update the current conversations array temporarily for rendering const originalConversations = currentConversations; currentConversations = filteredConversations; // Render the filtered list renderConversationsList(); // Restore the original conversations array currentConversations = originalConversations; }); // --- Initial Load --- let aiSettingsLoaded = false; let conversationsLoaded = false; checkLoginStatus(); });