1325 lines
54 KiB
JavaScript

// 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 = '<option value="">--Please choose a server--</option>'; // 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 = '<option value="">-- Select Channel --</option>';
document.getElementById('goodbye-channel-select').innerHTML = '<option value="">-- Select Channel --</option>';
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 = '<option value="">-- Select Channel --</option>';
goodbyeChannelSelect.innerHTML = '<option value="">-- Select Channel --</option>';
// 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 = '<option value="">-- Select Role --</option>';
// 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 = '<option value="">-- Select Command --</option>';
// 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 = '<i>No specific command permissions set. All roles can use all enabled commands (unless restricted by default).</i>';
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 <span>${commandName}</span> allowed for: ${rolesStr}`;
permsDiv.appendChild(div);
}
} catch (error) {
permsDiv.innerHTML = `<i class="error">Error loading permissions: ${error.message}</i>`;
}
}
// --- 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 = `
<div class="conversation-item-header">
<h4 class="conversation-title">${conversation.title}</h4>
<span class="conversation-date">${formattedDate}</span>
</div>
<div class="conversation-preview">${previewText}</div>
`;
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 = `
<div class="message-header">${message.role === 'user' ? 'You' : 'AI'}</div>
<div class="message-content">${message.content}</div>
`;
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();
});