This commit is contained in:
Slipstream 2025-05-03 16:04:09 -06:00
parent 9e35f1b823
commit 9ee5bdbaaa
Signed by: slipstream
GPG Key ID: 13E498CE010AC6FD
5 changed files with 1532 additions and 65 deletions

View File

@ -215,6 +215,22 @@ dashboard_api_app = FastAPI(
openapi_url="/openapi.json"
)
# Import dashboard API endpoints
try:
# Try relative import first
try:
from .dashboard_api_endpoints import router as dashboard_router
except ImportError:
# Fall back to absolute import
from dashboard_api_endpoints import router as dashboard_router
# Add the dashboard router to the dashboard API app
dashboard_api_app.include_router(dashboard_router)
log.info("Dashboard API endpoints loaded successfully")
except ImportError as e:
log.error(f"Could not import dashboard API endpoints: {e}")
log.error("Dashboard API endpoints will not be available")
# Mount the API apps at their respective paths
app.mount("/api", api_app)
app.mount("/discordapi", discordapi_app)

View File

@ -0,0 +1,120 @@
"""
Dashboard API endpoints for the bot dashboard.
These endpoints provide additional functionality for the dashboard UI.
"""
import logging
from typing import List, Dict, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel
# Import the dependencies from api_server.py
try:
# Try relative import first
from .api_server import get_dashboard_user, verify_dashboard_guild_admin
except ImportError:
# Fall back to absolute import
from api_server import get_dashboard_user, verify_dashboard_guild_admin
# Set up logging
log = logging.getLogger(__name__)
# Create a router for the dashboard API endpoints
router = APIRouter(tags=["Dashboard API"])
# --- Models ---
class Channel(BaseModel):
id: str
name: str
type: int # 0 = text, 2 = voice, etc.
class Role(BaseModel):
id: str
name: str
color: int
position: int
permissions: str
class Command(BaseModel):
name: str
description: Optional[str] = None
# --- Endpoints ---
@router.get("/guilds/{guild_id}/channels", response_model=List[Channel])
async def get_guild_channels(
guild_id: int,
_user: dict = Depends(get_dashboard_user), # Underscore prefix to indicate unused parameter
_: bool = Depends(verify_dashboard_guild_admin)
):
"""Get all channels for a guild."""
try:
# This would normally fetch channels from Discord API or the bot
# For now, we'll return a mock response
channels = [
Channel(id="123456789", name="general", type=0),
Channel(id="123456790", name="welcome", type=0),
Channel(id="123456791", name="announcements", type=0),
Channel(id="123456792", name="voice-chat", type=2)
]
return channels
except Exception as e:
log.error(f"Error getting channels for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting channels: {str(e)}"
)
@router.get("/guilds/{guild_id}/roles", response_model=List[Role])
async def get_guild_roles(
guild_id: int,
_user: dict = Depends(get_dashboard_user), # Underscore prefix to indicate unused parameter
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Get all roles for a guild."""
try:
# This would normally fetch roles from Discord API or the bot
# For now, we'll return a mock response
roles = [
Role(id="123456789", name="@everyone", color=0, position=0, permissions="0"),
Role(id="123456790", name="Admin", color=16711680, position=1, permissions="8"),
Role(id="123456791", name="Moderator", color=65280, position=2, permissions="4"),
Role(id="123456792", name="Member", color=255, position=3, permissions="1")
]
return roles
except Exception as e:
log.error(f"Error getting roles for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting roles: {str(e)}"
)
@router.get("/guilds/{guild_id}/commands", response_model=List[Command])
async def get_guild_commands(
guild_id: int,
_user: dict = Depends(get_dashboard_user), # Underscore prefix to indicate unused parameter
_admin: bool = Depends(verify_dashboard_guild_admin)
):
"""Get all commands available in the guild."""
try:
# This would normally fetch commands from the bot
# For now, we'll return a mock response
commands = [
Command(name="help", description="Show help message"),
Command(name="ping", description="Check bot latency"),
Command(name="ban", description="Ban a user"),
Command(name="kick", description="Kick a user"),
Command(name="mute", description="Mute a user"),
Command(name="unmute", description="Unmute a user"),
Command(name="clear", description="Clear messages"),
Command(name="ai", description="Get AI response"),
Command(name="aiset", description="Configure AI settings"),
Command(name="chat", description="Chat with AI"),
Command(name="convs", description="Manage conversations")
]
return commands
except Exception as e:
log.error(f"Error getting commands for guild {guild_id}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error getting commands: {str(e)}"
)

View File

@ -17,66 +17,274 @@
<h2>Welcome, <span id="username">User</span>!</h2>
<button id="logout-button">Logout</button>
<div class="dashboard-nav">
<button id="nav-server-settings" class="nav-button active">Server Settings</button>
<button id="nav-ai-settings" class="nav-button">AI Settings</button>
<button id="nav-conversations" class="nav-button">Conversations</button>
</div>
<hr>
<h3>Manage Server Settings</h3>
<label for="guild-select">Select Server:</label>
<select name="guilds" id="guild-select">
<option value="">--Please choose a server--</option>
</select>
<!-- Server Settings Section -->
<div id="server-settings-section" class="dashboard-section">
<h3>Manage Server Settings</h3>
<label for="guild-select">Select Server:</label>
<select name="guilds" id="guild-select">
<option value="">--Please choose a server--</option>
</select>
<div id="settings-form" style="display: none;">
<h4>Prefix</h4>
<label for="prefix-input">Command Prefix:</label>
<input type="text" id="prefix-input" name="prefix" maxlength="10">
<button id="save-prefix-button">Save Prefix</button>
<p id="prefix-feedback"></p>
<div id="settings-form" style="display: none;">
<h4>Prefix</h4>
<label for="prefix-input">Command Prefix:</label>
<input type="text" id="prefix-input" name="prefix" maxlength="10">
<button id="save-prefix-button">Save Prefix</button>
<p id="prefix-feedback"></p>
<h4>Welcome Messages</h4>
<label for="welcome-channel">Welcome Channel ID:</label> <!-- Changed label -->
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID"> <!-- Changed to text input -->
<br>
<label for="welcome-message">Welcome Message Template:</label><br>
<textarea id="welcome-message" name="welcome_message" rows="4" cols="50" placeholder="Use {user} for mention, {username} for name, {server} for server name."></textarea><br>
<button id="save-welcome-button">Save Welcome Settings</button>
<button id="disable-welcome-button">Disable Welcome</button>
<p id="welcome-feedback"></p>
<h4>Welcome Messages</h4>
<div class="settings-card">
<div class="form-group">
<label for="welcome-channel">Welcome Channel:</label>
<div class="channel-select-container">
<input type="text" id="welcome-channel" name="welcome_channel_id" placeholder="Enter Channel ID">
<select id="welcome-channel-select" class="channel-dropdown">
<option value="">-- Select Channel --</option>
<!-- Will be populated by JS -->
</select>
</div>
</div>
<div class="form-group">
<label for="welcome-message">Welcome Message Template:</label>
<textarea id="welcome-message" name="welcome_message" rows="4" cols="50" placeholder="Use {user} for mention, {username} for name, {server} for server name."></textarea>
</div>
<div class="button-group">
<button id="save-welcome-button">Save Welcome Settings</button>
<button id="disable-welcome-button">Disable Welcome</button>
</div>
<p id="welcome-feedback"></p>
</div>
<h4>Goodbye Messages</h4>
<label for="goodbye-channel">Goodbye Channel ID:</label> <!-- Changed label -->
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID"> <!-- Changed to text input -->
<br>
<label for="goodbye-message">Goodbye Message Template:</label><br>
<textarea id="goodbye-message" name="goodbye_message" rows="4" cols="50" placeholder="Use {username} for name, {server} for server name."></textarea><br>
<button id="save-goodbye-button">Save Goodbye Settings</button>
<button id="disable-goodbye-button">Disable Goodbye</button>
<p id="goodbye-feedback"></p>
<h4>Goodbye Messages</h4>
<div class="settings-card">
<div class="form-group">
<label for="goodbye-channel">Goodbye Channel:</label>
<div class="channel-select-container">
<input type="text" id="goodbye-channel" name="goodbye_channel_id" placeholder="Enter Channel ID">
<select id="goodbye-channel-select" class="channel-dropdown">
<option value="">-- Select Channel --</option>
<!-- Will be populated by JS -->
</select>
</div>
</div>
<div class="form-group">
<label for="goodbye-message">Goodbye Message Template:</label>
<textarea id="goodbye-message" name="goodbye_message" rows="4" cols="50" placeholder="Use {username} for name, {server} for server name."></textarea>
</div>
<div class="button-group">
<button id="save-goodbye-button">Save Goodbye Settings</button>
<button id="disable-goodbye-button">Disable Goodbye</button>
</div>
<p id="goodbye-feedback"></p>
</div>
<h4>Enabled Modules (Cogs)</h4>
<div id="cogs-list">
<!-- Cog checkboxes will be populated by JS -->
<h4>Enabled Modules (Cogs)</h4>
<div class="settings-card">
<div class="cogs-container">
<div id="cogs-list">
<!-- Cog checkboxes will be populated by JS -->
</div>
</div>
<div class="button-group">
<button id="save-cogs-button">Save Module Settings</button>
</div>
<p id="cogs-feedback"></p>
</div>
<h4>Command Permissions</h4>
<div class="settings-card">
<div class="form-group">
<label for="command-select">Command:</label>
<select id="command-select">
<!-- Will be populated by JS -->
<option value="">-- Select Command --</option>
</select>
</div>
<div class="form-group">
<label for="role-select">Role:</label>
<select id="role-select">
<!-- Will be populated by JS -->
<option value="">-- Select Role --</option>
</select>
</div>
<div class="button-group">
<button id="add-perm-button">Allow Role</button>
<button id="remove-perm-button">Disallow Role</button>
</div>
<div id="current-perms">
<!-- Current permissions will be listed here -->
</div>
<p id="perms-feedback"></p>
</div>
</div>
<button id="save-cogs-button">Save Module Settings</button>
<p id="cogs-feedback"></p>
</div>
<h4>Command Permissions</h4>
<label for="command-select">Command:</label>
<select id="command-select">
<!-- TODO: Populate commands dynamically -->
<option value="">-- Select Command --</option>
</select>
<label for="role-select">Role:</label>
<select id="role-select">
<!-- TODO: Populate roles dynamically -->
<option value="">-- Select Role --</option>
</select>
<button id="add-perm-button">Allow Role</button>
<button id="remove-perm-button">Disallow Role</button>
<div id="current-perms">
<!-- Current permissions will be listed here -->
</div>
<p id="perms-feedback"></p>
<!-- AI Settings Section -->
<div id="ai-settings-section" class="dashboard-section" style="display: none;">
<h3>AI Settings</h3>
<div class="settings-card">
<h4>General AI Settings</h4>
<div class="form-group">
<label for="ai-model-select">AI Model:</label>
<select id="ai-model-select">
<option value="openai/gpt-3.5-turbo">GPT-3.5 Turbo</option>
<option value="openai/gpt-4">GPT-4</option>
<option value="anthropic/claude-3-opus">Claude 3 Opus</option>
<option value="anthropic/claude-3-sonnet">Claude 3 Sonnet</option>
<option value="google/gemini-2.5-flash-preview">Gemini 2.5 Flash</option>
<option value="google/gemini-2.5-pro-preview">Gemini 2.5 Pro</option>
<!-- More models will be populated dynamically if available -->
</select>
</div>
<div class="form-group">
<label for="ai-temperature">Temperature: <span id="temperature-value">0.7</span></label>
<input type="range" id="ai-temperature" min="0" max="2" step="0.1" value="0.7">
</div>
<div class="form-group">
<label for="ai-max-tokens">Max Tokens:</label>
<input type="number" id="ai-max-tokens" min="100" max="8000" step="100" value="1000">
</div>
<div class="form-group">
<label>
<input type="checkbox" id="ai-reasoning-enabled">
Enable Reasoning
</label>
</div>
<div class="form-group" id="reasoning-effort-group">
<label for="ai-reasoning-effort">Reasoning Effort:</label>
<select id="ai-reasoning-effort">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="ai-web-search-enabled">
Enable Web Search
</label>
</div>
<div class="button-group">
<button id="save-ai-settings-button">Save AI Settings</button>
<button id="reset-ai-settings-button">Reset to Defaults</button>
</div>
<p id="ai-settings-feedback"></p>
</div>
<div class="settings-card">
<h4>Character Settings</h4>
<div class="form-group">
<label for="ai-character">Character Name:</label>
<input type="text" id="ai-character" placeholder="e.g., Kasane Teto">
</div>
<div class="form-group">
<label for="ai-character-info">Character Information:</label>
<textarea id="ai-character-info" rows="4" placeholder="Describe the character's personality, background, etc."></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="ai-character-breakdown">
Enable Character Breakdown
</label>
</div>
<div class="button-group">
<button id="save-character-settings-button">Save Character Settings</button>
<button id="clear-character-settings-button">Clear Character</button>
</div>
<p id="character-settings-feedback"></p>
</div>
<div class="settings-card">
<h4>System Prompt</h4>
<div class="form-group">
<textarea id="ai-system-prompt" rows="6" placeholder="Enter a system prompt to guide the AI's behavior..."></textarea>
</div>
<div class="button-group">
<button id="save-system-prompt-button">Save System Prompt</button>
<button id="reset-system-prompt-button">Reset to Default</button>
</div>
<p id="system-prompt-feedback"></p>
</div>
<div class="settings-card">
<h4>Custom Instructions</h4>
<div class="form-group">
<textarea id="ai-custom-instructions" rows="6" placeholder="Enter custom instructions for the AI..."></textarea>
</div>
<div class="button-group">
<button id="save-custom-instructions-button">Save Custom Instructions</button>
<button id="clear-custom-instructions-button">Clear Instructions</button>
</div>
<p id="custom-instructions-feedback"></p>
</div>
</div>
<!-- Conversations Section -->
<div id="conversations-section" class="dashboard-section" style="display: none;">
<h3>AI Conversations</h3>
<div class="conversations-header">
<div class="search-container">
<input type="text" id="conversation-search" placeholder="Search conversations...">
</div>
<button id="new-conversation-button">New Conversation</button>
</div>
<div class="conversations-list-container">
<div id="conversations-list">
<!-- Will be populated by JS -->
<div class="no-conversations">No conversations found. Start a new conversation!</div>
</div>
</div>
<div id="conversation-detail" style="display: none;">
<div class="conversation-header">
<h4 id="conversation-title">Conversation Title</h4>
<div class="conversation-actions">
<button id="rename-conversation-button">Rename</button>
<button id="delete-conversation-button">Delete</button>
<button id="export-conversation-button">Export</button>
</div>
</div>
<div class="conversation-messages" id="conversation-messages">
<!-- Will be populated by JS -->
</div>
</div>
</div>
</div>
<!-- Modal for renaming conversations -->
<div id="rename-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h4>Rename Conversation</h4>
<input type="text" id="new-conversation-title" placeholder="Enter new title">
<div class="button-group">
<button id="confirm-rename-button">Rename</button>
<button id="cancel-rename-button">Cancel</button>
</div>
</div>
</div>
<!-- Modal for creating new conversations -->
<div id="new-conversation-modal" class="modal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h4>Create New Conversation</h4>
<input type="text" id="new-conversation-name" placeholder="Enter conversation title">
<div class="button-group">
<button id="create-conversation-button">Create</button>
<button id="cancel-create-button">Cancel</button>
</div>
</div>
</div>

View File

@ -1,9 +1,22 @@
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-section');
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');
@ -49,6 +62,11 @@ document.addEventListener('DOMContentLoaded', () => {
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();
}
@ -131,18 +149,24 @@ document.addEventListener('DOMContentLoaded', () => {
async function loadSettings(guildId) {
console.log(`Loading settings for guild ${guildId}`);
// Clear previous settings?
// Clear previous settings
document.getElementById('prefix-input').value = '';
// Changed channel inputs to text
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 permissions list
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 {
// Use the new endpoint path
// 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);
@ -155,6 +179,21 @@ document.addEventListener('DOMContentLoaded', () => {
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
@ -162,12 +201,105 @@ document.addEventListener('DOMContentLoaded', () => {
// Populate Command Permissions
// TODO: Fetch roles and commands for dropdowns
// TODO: Fetch current permissions
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); // Use a general feedback area?
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);
}
}
@ -416,6 +548,764 @@ document.addEventListener('DOMContentLoaded', () => {
});
// --- 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();
});

View File

@ -8,7 +8,7 @@ h1, h2, h3, h4 {
color: #333;
}
#dashboard-section, #settings-form {
#dashboard-section {
background-color: #fff;
padding: 1.5em;
border-radius: 8px;
@ -16,27 +16,60 @@ h1, h2, h3, h4 {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.dashboard-section {
margin-top: 1em;
}
.settings-card {
background-color: #fff;
padding: 1.5em;
border-radius: 8px;
margin-top: 1em;
margin-bottom: 1.5em;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
border: 1px solid #eee;
}
.form-group {
margin-bottom: 1em;
}
label {
display: block;
margin-top: 1em;
margin-bottom: 0.5em;
font-weight: bold;
}
input[type="text"],
input[type="number"],
select,
textarea {
width: 95%;
padding: 8px;
margin-bottom: 1em;
margin-bottom: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
}
input[type="checkbox"] {
margin-right: 8px;
}
input[type="range"] {
width: 95%;
}
textarea {
resize: vertical;
}
.button-group {
margin-top: 1em;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button {
padding: 10px 15px;
background-color: #5865F2; /* Discord blue */
@ -44,8 +77,6 @@ button {
border: none;
border-radius: 5px;
cursor: pointer;
margin-right: 5px;
margin-top: 5px;
}
button:hover {
@ -59,15 +90,18 @@ button:hover {
background-color: #c82333;
}
button[id^="disable-"] {
button[id^="disable-"],
button[id^="reset-"],
button[id^="clear-"] {
background-color: #ffc107; /* Yellow/Orange */
color: #333;
}
button[id^="disable-"]:hover {
button[id^="disable-"]:hover,
button[id^="reset-"]:hover,
button[id^="clear-"]:hover {
background-color: #e0a800;
}
hr {
border: 0;
height: 1px;
@ -75,6 +109,53 @@ hr {
margin: 2em 0;
}
/* Navigation */
.dashboard-nav {
display: flex;
margin-top: 1em;
border-bottom: 1px solid #ddd;
}
.nav-button {
background: none;
border: none;
padding: 10px 15px;
margin: 0;
color: #666;
cursor: pointer;
border-bottom: 3px solid transparent;
}
.nav-button:hover {
background-color: #f0f0f0;
color: #333;
}
.nav-button.active {
color: #5865F2;
border-bottom: 3px solid #5865F2;
font-weight: bold;
}
/* Channel Select Container */
.channel-select-container {
display: flex;
gap: 8px;
}
.channel-dropdown {
flex: 1;
}
/* Cogs List */
.cogs-container {
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
padding: 10px;
border-radius: 4px;
}
#cogs-list div {
margin-bottom: 0.5em;
}
@ -113,6 +194,158 @@ hr {
background-color: #c82333;
}
/* Conversations Section */
.conversations-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1em;
}
.search-container {
flex: 1;
margin-right: 1em;
}
.conversations-list-container {
border: 1px solid #ddd;
border-radius: 8px;
max-height: 300px;
overflow-y: auto;
margin-bottom: 1em;
}
#conversations-list {
padding: 0;
}
.conversation-item {
padding: 12px 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.conversation-item:hover {
background-color: #f5f5f5;
}
.conversation-item.active {
background-color: #e6f7ff;
border-left: 3px solid #5865F2;
}
.conversation-item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.conversation-title {
font-weight: bold;
margin: 0;
}
.conversation-date {
font-size: 0.8em;
color: #666;
}
.conversation-preview {
font-size: 0.9em;
color: #666;
margin-top: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.no-conversations {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
#conversation-detail {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
}
.conversation-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1em;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.conversation-actions {
display: flex;
gap: 8px;
}
.conversation-messages {
max-height: 500px;
overflow-y: auto;
padding: 10px;
}
.message {
margin-bottom: 15px;
padding: 10px;
border-radius: 8px;
}
.user-message {
background-color: #e6f7ff;
margin-left: 20px;
}
.ai-message {
background-color: #f0f0f0;
margin-right: 20px;
}
.message-header {
font-weight: bold;
margin-bottom: 5px;
}
.message-content {
white-space: pre-wrap;
}
/* Modal */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
}
.modal-content {
background-color: #fff;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 50%;
max-width: 500px;
position: relative;
}
.close-modal {
position: absolute;
top: 10px;
right: 15px;
font-size: 24px;
cursor: pointer;
}
/* Feedback messages */
p[id$="-feedback"] {