import discord from discord.ext import commands from discord import app_commands, ui import aiohttp import json import os import datetime import uuid from typing import Dict, List, Optional, Any, Union import asyncio import builtins from dotenv import load_dotenv # Import Discord sync API functions try: from discord_bot_sync_api import save_discord_conversation, load_conversations, user_conversations as synced_conversations, user_settings as synced_user_settings, load_user_settings as load_synced_user_settings SYNC_API_AVAILABLE = True except ImportError: print("Discord sync API not available. Sync features will be disabled.") SYNC_API_AVAILABLE = False synced_conversations = {} synced_user_settings = {} # Load environment variables load_dotenv() # File paths CONVERSATIONS_FILE = "ai_multi_conversations.json" # File to store multiple conversations USER_SETTINGS_FILE = "ai_multi_user_settings.json" # File to store user settings # Customization Variables AI_API_KEY = os.getenv("AI_API_KEY", "") # API key for OpenAI or compatible service AI_API_URL = os.getenv("AI_API_URL", "https://api.openai.com/v1/chat/completions") # API endpoint AI_DEFAULT_MODEL = os.getenv("AI_DEFAULT_MODEL", "gpt-3.5-turbo") # Default model to use AI_DEFAULT_SYSTEM_PROMPT = os.getenv("AI_DEFAULT_SYSTEM_PROMPT", r"""You are a helpful assistant.""") # Default system prompt AI_MAX_TOKENS = int(os.getenv("AI_MAX_TOKENS", "1000")) # Maximum tokens in response AI_TEMPERATURE = float(os.getenv("AI_TEMPERATURE", "0.7")) # Temperature for response generation AI_TIMEOUT = int(os.getenv("AI_TIMEOUT", "60")) # Timeout for API requests in seconds AI_COMPATIBILITY_MODE = os.getenv("AI_COMPATIBILITY_MODE", "openai").lower() # API compatibility mode (openai, custom) # Store user conversations user_conversations = {} # Store active conversation IDs for each user active_conversations = {} # Define the conversation data structure class Conversation: def __init__( self, id: str = None, title: str = "New Conversation", messages: List[Dict[str, Any]] = None, created_at: datetime.datetime = None, updated_at: datetime.datetime = None, model_id: str = AI_DEFAULT_MODEL, system_message: str = AI_DEFAULT_SYSTEM_PROMPT, reasoning_enabled: bool = False, reasoning_effort: str = "medium", temperature: float = AI_TEMPERATURE, max_tokens: int = AI_MAX_TOKENS, web_search_enabled: bool = False, character: str = "", character_info: str = "", character_breakdown: bool = False, custom_instructions: str = "" ): self.id = id or str(uuid.uuid4()) self.title = title self.messages = messages or [] self.created_at = created_at or datetime.datetime.now() self.updated_at = updated_at or datetime.datetime.now() self.model_id = model_id self.system_message = system_message self.reasoning_enabled = reasoning_enabled self.reasoning_effort = reasoning_effort self.temperature = temperature self.max_tokens = max_tokens self.web_search_enabled = web_search_enabled self.character = character self.character_info = character_info self.character_breakdown = character_breakdown self.custom_instructions = custom_instructions def to_dict(self): """Convert conversation to dictionary for serialization""" return { "id": self.id, "title": self.title, "messages": self.messages, "created_at": self.created_at.isoformat(), "updated_at": self.updated_at.isoformat(), "model_id": self.model_id, "system_message": self.system_message, "reasoning_enabled": self.reasoning_enabled, "reasoning_effort": self.reasoning_effort, "temperature": self.temperature, "max_tokens": self.max_tokens, "web_search_enabled": self.web_search_enabled, "character": self.character, "character_info": self.character_info, "character_breakdown": self.character_breakdown, "custom_instructions": self.custom_instructions } @classmethod def from_dict(cls, data): """Create conversation from dictionary""" return cls( id=data.get("id"), title=data.get("title", "New Conversation"), messages=data.get("messages", []), created_at=datetime.datetime.fromisoformat(data.get("created_at")) if data.get("created_at") else None, updated_at=datetime.datetime.fromisoformat(data.get("updated_at")) if data.get("updated_at") else None, model_id=data.get("model_id", AI_DEFAULT_MODEL), system_message=data.get("system_message", AI_DEFAULT_SYSTEM_PROMPT), reasoning_enabled=data.get("reasoning_enabled", False), reasoning_effort=data.get("reasoning_effort", "medium"), temperature=data.get("temperature", AI_TEMPERATURE), max_tokens=data.get("max_tokens", AI_MAX_TOKENS), web_search_enabled=data.get("web_search_enabled", False), character=data.get("character", ""), character_info=data.get("character_info", ""), character_breakdown=data.get("character_breakdown", False), custom_instructions=data.get("custom_instructions", "") ) # Load conversations from JSON file def load_user_conversations(): """Load user conversations from JSON file""" global user_conversations if os.path.exists(CONVERSATIONS_FILE): try: with open(CONVERSATIONS_FILE, "r", encoding="utf-8") as f: data = json.load(f) # Convert string keys (user IDs) back to integers user_conversations = {} for user_id_str, convs in data.items(): user_id = int(user_id_str) user_conversations[user_id] = [Conversation.from_dict(conv) for conv in convs] print(f"Loaded conversations for {len(user_conversations)} users") except Exception as e: print(f"Error loading user conversations: {e}") user_conversations = {} # Save conversations to JSON file def save_user_conversations(): """Save user conversations to JSON file""" try: # Convert to JSON-serializable format serializable_data = {} for user_id, convs in user_conversations.items(): serializable_data[str(user_id)] = [conv.to_dict() for conv in convs] with open(CONVERSATIONS_FILE, "w", encoding="utf-8") as f: json.dump(serializable_data, f, indent=4, default=str, ensure_ascii=False) except Exception as e: print(f"Error saving user conversations: {e}") # Load active conversations from settings file def load_active_conversations(): """Load active conversation IDs from settings file""" global active_conversations if os.path.exists(USER_SETTINGS_FILE): try: with open(USER_SETTINGS_FILE, "r", encoding="utf-8") as f: data = json.load(f) # Convert string keys (user IDs) back to integers active_conversations = {int(k): v.get("active_conversation") for k, v in data.items() if "active_conversation" in v} print(f"Loaded active conversations for {len(active_conversations)} users") except Exception as e: print(f"Error loading active conversations: {e}") active_conversations = {} # Save active conversations to settings file def save_active_conversations(): """Save active conversation IDs to settings file""" try: # Load existing settings first settings = {} if os.path.exists(USER_SETTINGS_FILE): with open(USER_SETTINGS_FILE, "r") as f: settings = json.load(f) # Update with active conversations for user_id, conv_id in active_conversations.items(): if str(user_id) not in settings: settings[str(user_id)] = {} settings[str(user_id)]["active_conversation"] = conv_id # Save back to file with open(USER_SETTINGS_FILE, "w", encoding="utf-8") as f: json.dump(settings, f, indent=4, ensure_ascii=False) except Exception as e: print(f"Error saving active conversations: {e}") # Initialize by loading saved data load_user_conversations() load_active_conversations() # Load synced user settings if available if SYNC_API_AVAILABLE: try: load_synced_user_settings() except Exception as e: print(f"Error loading synced user settings: {e}") class MultiConversationCog(commands.Cog, name="MultiConversation"): def __init__(self, bot: commands.Bot): self.bot = bot self.session = None async def cog_load(self): """Create aiohttp session when cog is loaded""" self.session = aiohttp.ClientSession() async def cog_unload(self): """Close aiohttp session when cog is unloaded""" if self.session: await self.session.close() # Save conversations and settings when unloading save_user_conversations() save_active_conversations() def get_active_conversation(self, user_id: int) -> Optional[Conversation]: """Get the active conversation for a user""" # Get the active conversation ID active_id = active_conversations.get(user_id) if not active_id: return None # Get the user's conversations conversations = user_conversations.get(user_id, []) if not conversations: return None # Find the active conversation return next((c for c in conversations if c.id == active_id), None) def create_new_conversation(self, user_id: int, title: str = "New Conversation") -> Conversation: """Create a new conversation for a user""" # Check if we have synced settings for this user synced_settings = None if SYNC_API_AVAILABLE: str_user_id = str(user_id) if str_user_id in synced_user_settings: synced_settings = synced_user_settings[str_user_id] # Create a new conversation with default settings new_conv = Conversation(title=title) # Apply synced settings if available if synced_settings: # Apply model settings if hasattr(synced_settings, 'model_id') and synced_settings.model_id: new_conv.model_id = synced_settings.model_id # Apply temperature settings if hasattr(synced_settings, 'temperature'): new_conv.temperature = synced_settings.temperature # Apply max tokens settings if hasattr(synced_settings, 'max_tokens'): new_conv.max_tokens = synced_settings.max_tokens # Apply reasoning settings if hasattr(synced_settings, 'reasoning_enabled'): new_conv.reasoning_enabled = synced_settings.reasoning_enabled if hasattr(synced_settings, 'reasoning_effort'): new_conv.reasoning_effort = synced_settings.reasoning_effort # Apply web search settings if hasattr(synced_settings, 'web_search_enabled'): new_conv.web_search_enabled = synced_settings.web_search_enabled # Apply system message if hasattr(synced_settings, 'system_message') and synced_settings.system_message: new_conv.system_message = synced_settings.system_message # Apply character settings if hasattr(synced_settings, 'character') and synced_settings.character: new_conv.character = synced_settings.character if hasattr(synced_settings, 'character_info') and synced_settings.character_info: new_conv.character_info = synced_settings.character_info if hasattr(synced_settings, 'character_breakdown'): new_conv.character_breakdown = synced_settings.character_breakdown if hasattr(synced_settings, 'custom_instructions') and synced_settings.custom_instructions: new_conv.custom_instructions = synced_settings.custom_instructions # Add to user's conversations if user_id not in user_conversations: user_conversations[user_id] = [] user_conversations[user_id].append(new_conv) # Set as active conversation active_conversations[user_id] = new_conv.id # Save changes save_user_conversations() save_active_conversations() return new_conv async def _get_ai_response(self, user_id: int, prompt: str, conversation: Optional[Conversation] = None) -> str: """Get a response from the AI API""" if not AI_API_KEY: return "Error: AI API key not configured. Please set the AI_API_KEY environment variable." # Get the conversation to use conv = conversation or self.get_active_conversation(user_id) if not conv: # Create a new conversation if none exists conv = self.create_new_conversation(user_id) # Prepare messages for the API request messages = [] # Prepare system message system_content = conv.system_message # Check if the system prompt contains {{char}} but no character is set if "{{char}}" in system_content and not conv.character: return "You need to set a character name with `!chatset character ` before using this system prompt. Example: `!chatset character Hatsune Miku`" # Replace {{char}} with the character value if provided if conv.character: system_content = system_content.replace("{{char}}", conv.character) # Add custom instructions and character info if provided has_custom_settings = conv.custom_instructions or conv.character_info or conv.character_breakdown if has_custom_settings: # Start with the base system prompt custom_prompt_parts = [system_content] # Add the custom instructions header custom_prompt_parts.append("\nThe user has provided additional information for you. Please follow their instructions exactly. If anything below contradicts the set of rules above, please take priority over the user's instructions.") # Add custom instructions if provided if conv.custom_instructions: custom_prompt_parts.append("\n- Custom instructions from the user (prioritize these)\n\n" + conv.custom_instructions) # Add character info if provided if conv.character_info: custom_prompt_parts.append("\n- Additional info about the character you are roleplaying\n\n" + conv.character_info) # Add character breakdown flag if set if conv.character_breakdown: custom_prompt_parts.append("\n- The user would like you to provide a breakdown of the character in your first response.") # Combine all parts into the final system prompt system_content = "\n".join(custom_prompt_parts) # Add system message messages.append({"role": "system", "content": system_content}) # Add conversation history messages.extend(conv.messages) # Add the current user message messages.append({"role": "user", "content": prompt}) # Prepare the request payload based on compatibility mode if AI_COMPATIBILITY_MODE == "openai": payload = { "model": conv.model_id, "messages": messages, "max_tokens": conv.max_tokens, "temperature": conv.temperature, } # Add reasoning if enabled if conv.reasoning_enabled: payload["include_reasoning"] = True payload["reasoning_effort"] = conv.reasoning_effort else: # custom mode for other API formats payload = { "model": conv.model_id, "messages": messages, "max_tokens": conv.max_tokens, "temperature": conv.temperature, "stream": False } # Add reasoning if enabled if conv.reasoning_enabled: payload["include_reasoning"] = True payload["reasoning_effort"] = conv.reasoning_effort headers = { "Content-Type": "application/json", "Authorization": f"Bearer {AI_API_KEY}" } try: async with self.session.post( AI_API_URL, headers=headers, json=payload, timeout=AI_TIMEOUT ) as response: if response.status != 200: error_text = await response.text() return f"Error from API (Status {response.status}): {error_text}" data = await response.json() # Parse the response based on compatibility mode ai_response = None reasoning_tokens = None safety_cutoff = False usage_data = None if AI_COMPATIBILITY_MODE == "openai": # OpenAI format if "choices" not in data: error_message = f"Unexpected API response format: {data}" print(f"Error: {error_message}") if "error" in data: return f"API Error: {data['error'].get('message', 'Unknown error')}" return error_message if not data["choices"] or "message" not in data["choices"][0]: error_message = f"No valid choices in API response: {data}" print(f"Error: {error_message}") return error_message ai_response = data["choices"][0]["message"]["content"] # Check for reasoning tokens if requested if conv.reasoning_enabled and "reasoning" in data["choices"][0]["message"]: reasoning_tokens = data["choices"][0]["message"]["reasoning"] # Check for safety cutoff if "finish_reason" in data["choices"][0] and data["choices"][0]["finish_reason"] == "content_filter": safety_cutoff = True if "native_finish_reason" in data["choices"][0] and data["choices"][0]["native_finish_reason"] == "SAFETY": safety_cutoff = True # Get usage data if available if "usage" in data: usage_data = data["usage"] else: # Custom format - try different response structures # Try standard OpenAI format first if "choices" in data and data["choices"] and "message" in data["choices"][0]: ai_response = data["choices"][0]["message"]["content"] # Check for reasoning tokens if requested if conv.reasoning_enabled and "reasoning" in data["choices"][0]["message"]: reasoning_tokens = data["choices"][0]["message"]["reasoning"] # Check for safety cutoff if "finish_reason" in data["choices"][0] and data["choices"][0]["finish_reason"] == "content_filter": safety_cutoff = True if "native_finish_reason" in data["choices"][0] and data["choices"][0]["native_finish_reason"] == "SAFETY": safety_cutoff = True # Get usage data if available if "usage" in data: usage_data = data["usage"] # Try other formats elif "response" in data: ai_response = data["response"] elif "text" in data: ai_response = data["text"] elif "content" in data: ai_response = data["content"] elif "output" in data: ai_response = data["output"] elif "result" in data: ai_response = data["result"] else: # If we can't find a known format, return the raw response for debugging error_message = f"Could not parse API response: {data}" print(f"Error: {error_message}") return error_message if not ai_response: return "Error: Empty response from AI API." # Add safety cutoff note if needed if safety_cutoff: ai_response = f"{ai_response}\n\nThe response was cut off for safety reasons." # Add reasoning tokens if available and requested final_response = ai_response if conv.reasoning_enabled and reasoning_tokens: final_response = f"{ai_response}\n\n**Reasoning:**\n```\n{reasoning_tokens}\n```" # Update conversation history # Add user message conv.messages.append({"role": "user", "content": prompt}) # Add assistant message with reasoning and usage if available assistant_message = {"role": "assistant", "content": ai_response} if reasoning_tokens: assistant_message["reasoning"] = reasoning_tokens if usage_data: assistant_message["usage_data"] = usage_data conv.messages.append(assistant_message) # Update conversation timestamp conv.updated_at = datetime.datetime.now() # Auto-generate title for new conversations with only one message if conv.title == "New Conversation" and len(conv.messages) == 2: # Use a simple title based on the prompt if len(prompt) > 30: conv.title = prompt[:27] + "..." else: conv.title = prompt # Save conversation save_user_conversations() # Sync with API if available if SYNC_API_AVAILABLE: try: save_discord_conversation( str(user_id), conv.messages, conv.model_id, conv.id, conv.title, conv.reasoning_enabled, conv.reasoning_effort, conv.temperature, conv.max_tokens, conv.web_search_enabled, conv.system_message, conv.character, conv.character_info, conv.character_breakdown, conv.custom_instructions ) print(f"Synced conversation {conv.id} for user {user_id}") except Exception as e: print(f"Error syncing conversation: {e}") return final_response except asyncio.TimeoutError: return "Error: Request to AI API timed out. Please try again later." except Exception as e: error_message = f"Error communicating with AI API: {str(e)}" print(f"Exception in _get_ai_response: {error_message}") print(f"Exception type: {type(e).__name__}") import traceback traceback.print_exc() return error_message @commands.command(name="convs") async def ai_conversations(self, ctx: commands.Context): """Manage your AI conversations""" user_id = ctx.author.id # Get user's conversations conversations = user_conversations.get(user_id, []) if not conversations: # No conversations, create a new one new_conv = self.create_new_conversation(user_id) await ctx.reply(f"Created your first conversation: {new_conv.title}", view=ConversationManagementView(user_id)) else: # Show conversation management UI await ctx.reply("Here are your conversations:", view=ConversationManagementView(user_id)) @commands.command(name="chat") async def ai_command(self, ctx: commands.Context, *, prompt: str): """Get a response from the AI using your active conversation""" user_id = ctx.author.id # Get active conversation conv = self.get_active_conversation(user_id) if not conv: # No active conversation, create a new one conv = self.create_new_conversation(user_id) await ctx.reply(f"Created a new conversation: {conv.title}") # Show typing indicator async with ctx.typing(): # Get AI response response = await self._get_ai_response(user_id, prompt, conv) # Check if the response is too long if len(response) > 1900: # Split into chunks or send as file try: # Create a text file with the content file_name = f'ai_response_{ctx.author.id}_{int(datetime.datetime.now().timestamp())}.txt' with open(file_name, 'w', encoding='utf-8') as f: f.write(response) # Send the file await ctx.reply( f"Response for conversation '{conv.title}' (too long for Discord):", file=discord.File(file_name) ) # Clean up the file try: os.remove(file_name) except: pass except Exception as e: print(f"Error sending file: {e}") # Fall back to splitting the message chunks = [response[i:i+1900] for i in range(0, len(response), 1900)] await ctx.reply(f"Response for conversation '{conv.title}' (split into {len(chunks)} parts):") for i, chunk in enumerate(chunks): await ctx.send(f"**Part {i+1}/{len(chunks)}**\n{chunk}") else: # Send the response directly await ctx.reply(response) @commands.command(name="chatset") async def ai_settings(self, ctx: commands.Context, setting: str = None, *, value: str = None): """Update settings for your active conversation Examples: !chatset - Show current settings !chatset temperature 0.8 - Set temperature to 0.8 !chatset reasoning on - Enable reasoning !chatset web_search on - Enable web search !chatset system - Set system message !chatset character - Set character name !chatset character_info - Set character information !chatset character_breakdown on - Enable character breakdown !chatset custom_instructions - Set custom instructions """ user_id = ctx.author.id # Get active conversation conv = self.get_active_conversation(user_id) if not conv: # No active conversation, create a new one conv = self.create_new_conversation(user_id) await ctx.reply(f"Created a new conversation: {conv.title}") if setting is None: # Show current settings embed = discord.Embed( title=f"Settings for: {conv.title}", description="Current settings for your active conversation", color=discord.Color.blue() ) # Add fields for each setting embed.add_field(name="Model", value=conv.model_id, inline=True) embed.add_field(name="Temperature", value=str(conv.temperature), inline=True) embed.add_field(name="Max Tokens", value=str(conv.max_tokens), inline=True) embed.add_field(name="Reasoning", value="Enabled" if conv.reasoning_enabled else "Disabled", inline=True) embed.add_field(name="Reasoning Effort", value=conv.reasoning_effort.capitalize(), inline=True) embed.add_field(name="Web Search", value="Enabled" if conv.web_search_enabled else "Disabled", inline=True) # Add character settings embed.add_field(name="Character", value=conv.character or "None", inline=True) embed.add_field(name="Character Breakdown", value="Enabled" if conv.character_breakdown else "Disabled", inline=True) # Show system message (truncated if too long) system_msg = conv.system_message if len(system_msg) > 1000: system_msg = system_msg[:997] + "..." embed.add_field(name="System Message", value=system_msg, inline=False) # Show character info if available (truncated if too long) if conv.character_info: char_info = conv.character_info if len(char_info) > 1000: char_info = char_info[:997] + "..." embed.add_field(name="Character Info", value=char_info, inline=False) # Show custom instructions if available (truncated if too long) if conv.custom_instructions: custom_instr = conv.custom_instructions if len(custom_instr) > 1000: custom_instr = custom_instr[:997] + "..." embed.add_field(name="Custom Instructions", value=custom_instr, inline=False) await ctx.reply(embed=embed) return # Update the specified setting setting = setting.lower() if setting == "temperature" or setting == "temp": if not value: await ctx.reply(f"Current temperature: {conv.temperature}") return try: temp = float(value) if 0 <= temp <= 2: conv.temperature = temp await ctx.reply(f"Temperature set to {temp}") else: await ctx.reply("Temperature must be between 0 and 2") except ValueError: await ctx.reply("Temperature must be a number") elif setting == "max_tokens" or setting == "tokens": if not value: await ctx.reply(f"Current max tokens: {conv.max_tokens}") return try: tokens = int(value) if 100 <= tokens <= 4000: conv.max_tokens = tokens await ctx.reply(f"Max tokens set to {tokens}") else: await ctx.reply("Max tokens must be between 100 and 4000") except ValueError: await ctx.reply("Max tokens must be a number") elif setting == "reasoning": if not value: await ctx.reply(f"Reasoning is currently {'enabled' if conv.reasoning_enabled else 'disabled'}") return if value.lower() in ["on", "true", "yes", "enable", "enabled"]: conv.reasoning_enabled = True await ctx.reply("Reasoning enabled") elif value.lower() in ["off", "false", "no", "disable", "disabled"]: conv.reasoning_enabled = False await ctx.reply("Reasoning disabled") else: await ctx.reply("Value must be 'on' or 'off'") elif setting == "reasoning_effort" or setting == "effort": if not value: await ctx.reply(f"Current reasoning effort: {conv.reasoning_effort}") return if value.lower() in ["low", "medium", "high"]: conv.reasoning_effort = value.lower() await ctx.reply(f"Reasoning effort set to {value.lower()}") else: await ctx.reply("Reasoning effort must be 'low', 'medium', or 'high'") elif setting == "web_search" or setting == "search": if not value: await ctx.reply(f"Web search is currently {'enabled' if conv.web_search_enabled else 'disabled'}") return if value.lower() in ["on", "true", "yes", "enable", "enabled"]: conv.web_search_enabled = True await ctx.reply("Web search enabled") elif value.lower() in ["off", "false", "no", "disable", "disabled"]: conv.web_search_enabled = False await ctx.reply("Web search disabled") else: await ctx.reply("Value must be 'on' or 'off'") elif setting == "model": if not value: await ctx.reply(f"Current model: {conv.model_id}") return conv.model_id = value await ctx.reply(f"Model set to {value}") elif setting == "system" or setting == "system_message": if not value: # Show current system message system_msg = conv.system_message if len(system_msg) > 1900: # Send as file if too long file_name = f'system_message_{ctx.author.id}.txt' with open(file_name, 'w', encoding='utf-8') as f: f.write(system_msg) await ctx.reply("Current system message (too long for Discord):", file=discord.File(file_name)) # Clean up the file try: os.remove(file_name) except: pass else: await ctx.reply(f"Current system message:\n```\n{system_msg}\n```") return # Update system message conv.system_message = value await ctx.reply(f"System message updated") elif setting == "character": if not value: await ctx.reply(f"Current character: {conv.character or 'None'}") return # Update character conv.character = value await ctx.reply(f"Character set to: {value}") elif setting == "character_info" or setting == "charinfo": if not value: # Show current character info if not conv.character_info: await ctx.reply("No character info set.") return char_info = conv.character_info if len(char_info) > 1900: # Send as file if too long file_name = f'character_info_{ctx.author.id}.txt' with open(file_name, 'w', encoding='utf-8') as f: f.write(char_info) await ctx.reply("Current character info (too long for Discord):", file=discord.File(file_name)) # Clean up the file try: os.remove(file_name) except: pass else: await ctx.reply(f"Current character info:\n```\n{char_info}\n```") return # Update character info conv.character_info = value await ctx.reply(f"Character info updated") elif setting == "character_breakdown" or setting == "breakdown": if not value: await ctx.reply(f"Character breakdown is currently {'enabled' if conv.character_breakdown else 'disabled'}") return if value.lower() in ["on", "true", "yes", "enable", "enabled"]: conv.character_breakdown = True await ctx.reply("Character breakdown enabled") elif value.lower() in ["off", "false", "no", "disable", "disabled"]: conv.character_breakdown = False await ctx.reply("Character breakdown disabled") else: await ctx.reply("Value must be 'on' or 'off'") elif setting == "custom_instructions" or setting == "instructions": if not value: # Show current custom instructions if not conv.custom_instructions: await ctx.reply("No custom instructions set.") return custom_instr = conv.custom_instructions if len(custom_instr) > 1900: # Send as file if too long file_name = f'custom_instructions_{ctx.author.id}.txt' with open(file_name, 'w', encoding='utf-8') as f: f.write(custom_instr) await ctx.reply("Current custom instructions (too long for Discord):", file=discord.File(file_name)) # Clean up the file try: os.remove(file_name) except: pass else: await ctx.reply(f"Current custom instructions:\n```\n{custom_instr}\n```") return # Update custom instructions conv.custom_instructions = value await ctx.reply(f"Custom instructions updated") elif setting == "title": if not value: await ctx.reply(f"Current title: {conv.title}") return # Update title conv.title = value await ctx.reply(f"Conversation title set to: {value}") else: await ctx.reply(f"Unknown setting: {setting}\nAvailable settings: temperature, max_tokens, reasoning, reasoning_effort, web_search, model, system, title") return # Update conversation timestamp conv.updated_at = datetime.datetime.now() # Save changes save_user_conversations() # Sync with API if available if SYNC_API_AVAILABLE: try: save_discord_conversation( str(user_id), conv.messages, conv.model_id, conv.id, conv.title, conv.reasoning_enabled, conv.reasoning_effort, conv.temperature, conv.max_tokens, conv.web_search_enabled, conv.system_message, conv.character, conv.character_info, conv.character_breakdown, conv.custom_instructions ) print(f"Synced conversation {conv.id} for user {user_id} after settings update") except Exception as e: print(f"Error syncing conversation after settings update: {e}") @commands.command(name="chatimport") async def ai_import(self, ctx: commands.Context): """Import conversations from the sync API""" if not SYNC_API_AVAILABLE: await ctx.reply("Discord sync API is not available. Sync features are disabled.") return user_id = ctx.author.id user_id_str = str(user_id) # Check if user has any synced conversations if user_id_str not in synced_conversations or not synced_conversations[user_id_str]: await ctx.reply("You don't have any synced conversations to import.") return # Count how many conversations will be imported synced_count = len(synced_conversations[user_id_str]) # Create a confirmation message await ctx.reply( f"Found {synced_count} synced conversations. Do you want to import them?", view=ImportConfirmationView(self, user_id) ) async def import_conversations(self, user_id: int): """Import conversations from the sync API for a user""" user_id_str = str(user_id) if user_id_str not in synced_conversations or not synced_conversations[user_id_str]: return "No synced conversations found." # Get existing conversations if user_id not in user_conversations: user_conversations[user_id] = [] # Track imported conversations imported_count = 0 updated_count = 0 # Import each synced conversation for synced_conv in synced_conversations[user_id_str]: # Check if this conversation already exists existing_conv = next((c for c in user_conversations[user_id] if c.id == synced_conv.id), None) if existing_conv: # Update existing conversation if synced one is newer if synced_conv.updated_at > existing_conv.updated_at: # Convert messages to the right format messages = [] for msg in synced_conv.messages: message = { "role": msg.role, "content": msg.content } if msg.reasoning: message["reasoning"] = msg.reasoning if msg.usage_data: message["usage_data"] = msg.usage_data messages.append(message) # Update the conversation existing_conv.messages = messages existing_conv.title = synced_conv.title existing_conv.model_id = synced_conv.model_id existing_conv.updated_at = synced_conv.updated_at existing_conv.reasoning_enabled = synced_conv.reasoning_enabled existing_conv.reasoning_effort = synced_conv.reasoning_effort existing_conv.temperature = synced_conv.temperature existing_conv.max_tokens = synced_conv.max_tokens existing_conv.web_search_enabled = synced_conv.web_search_enabled existing_conv.system_message = synced_conv.system_message or AI_DEFAULT_SYSTEM_PROMPT updated_count += 1 else: # Create a new conversation # Convert messages to the right format messages = [] for msg in synced_conv.messages: message = { "role": msg.role, "content": msg.content } if msg.reasoning: message["reasoning"] = msg.reasoning if msg.usage_data: message["usage_data"] = msg.usage_data messages.append(message) # Create the conversation new_conv = Conversation( id=synced_conv.id, title=synced_conv.title, messages=messages, created_at=synced_conv.created_at, updated_at=synced_conv.updated_at, model_id=synced_conv.model_id, system_message=synced_conv.system_message or AI_DEFAULT_SYSTEM_PROMPT, reasoning_enabled=synced_conv.reasoning_enabled, reasoning_effort=synced_conv.reasoning_effort, temperature=synced_conv.temperature, max_tokens=synced_conv.max_tokens, web_search_enabled=synced_conv.web_search_enabled ) # Add to user's conversations user_conversations[user_id].append(new_conv) imported_count += 1 # Save changes save_user_conversations() # Set active conversation if user doesn't have one if user_id not in active_conversations or not active_conversations[user_id]: if user_conversations[user_id]: active_conversations[user_id] = user_conversations[user_id][0].id save_active_conversations() return f"Imported {imported_count} new conversations and updated {updated_count} existing conversations." @app_commands.command(name="chat", description="Get a response from the AI using your active conversation") @app_commands.describe(prompt="Your message to the AI") async def ai_slash(self, interaction: discord.Interaction, prompt: str): """Slash command to get a response from the AI""" user_id = interaction.user.id # Get active conversation conv = self.get_active_conversation(user_id) if not conv: # No active conversation, create a new one conv = self.create_new_conversation(user_id) # Defer the response since API calls can take time await interaction.response.defer(thinking=True) # Get AI response response = await self._get_ai_response(user_id, prompt, conv) # Check if the response is too long if len(response) > 3900: # Discord's limit for interactions is 4000, use 3900 to be safe try: # Create a text file with the content file_name = f'ai_response_{interaction.user.id}_{int(datetime.datetime.now().timestamp())}.txt' with open(file_name, 'w', encoding='utf-8') as f: f.write(response) # Send the file await interaction.followup.send( f"Response for conversation '{conv.title}' (too long for Discord):", file=discord.File(file_name) ) # Clean up the file try: os.remove(file_name) except: pass except Exception as e: print(f"Error sending file: {e}") # Fall back to splitting the message chunks = [response[i:i+1900] for i in range(0, len(response), 1900)] await interaction.followup.send(f"Response for conversation '{conv.title}' (split into {len(chunks)} parts):") for i, chunk in enumerate(chunks): await interaction.followup.send(f"**Part {i+1}/{len(chunks)}**\n{chunk}") else: # Send the response directly await interaction.followup.send(response) async def setup(bot): await bot.add_cog(MultiConversationCog(bot)) # UI Components for conversation management class ConversationSelectMenu(ui.Select): def __init__(self, user_id: int, conversations: List[Conversation], current_id: str = None): self.user_id = user_id self.conversations = conversations # Create options for each conversation options = [] for conv in conversations: # Truncate title if too long title = conv.title if len(title) > 100: title = title[:97] + "..." # Create option option = discord.SelectOption( label=title, value=conv.id, description=f"Created: {conv.created_at.strftime('%Y-%m-%d')}", default=(conv.id == current_id) ) options.append(option) # Add option to create new conversation options.append(discord.SelectOption( label="➕ New Conversation", value="new", description="Start a new conversation" )) super().__init__(placeholder="Select a conversation", options=options, min_values=1, max_values=1) async def callback(self, interaction: discord.Interaction): # Get the selected value selected_id = self.values[0] if selected_id == "new": # Get the MultiConversationCog instance cog = None for c in interaction.client.cogs.values(): if isinstance(c, MultiConversationCog): cog = c break if cog: # Use the cog's method to create a new conversation with user settings new_conv = cog.create_new_conversation(self.user_id, "New Conversation") else: # Fallback to creating a conversation directly # Check if we have synced settings for this user synced_settings = None if SYNC_API_AVAILABLE: str_user_id = str(self.user_id) if str_user_id in synced_user_settings: synced_settings = synced_user_settings[str_user_id] # Create a new conversation with default settings new_conv = Conversation(title="New Conversation") # Apply synced settings if available if synced_settings: # Apply model settings if hasattr(synced_settings, 'model_id') and synced_settings.model_id: new_conv.model_id = synced_settings.model_id # Apply temperature settings if hasattr(synced_settings, 'temperature'): new_conv.temperature = synced_settings.temperature # Apply max tokens settings if hasattr(synced_settings, 'max_tokens'): new_conv.max_tokens = synced_settings.max_tokens # Apply reasoning settings if hasattr(synced_settings, 'reasoning_enabled'): new_conv.reasoning_enabled = synced_settings.reasoning_enabled if hasattr(synced_settings, 'reasoning_effort'): new_conv.reasoning_effort = synced_settings.reasoning_effort # Apply web search settings if hasattr(synced_settings, 'web_search_enabled'): new_conv.web_search_enabled = synced_settings.web_search_enabled # Apply system message if hasattr(synced_settings, 'system_message') and synced_settings.system_message: new_conv.system_message = synced_settings.system_message # Apply character settings if hasattr(synced_settings, 'character') and synced_settings.character: new_conv.character = synced_settings.character if hasattr(synced_settings, 'character_info') and synced_settings.character_info: new_conv.character_info = synced_settings.character_info if hasattr(synced_settings, 'character_breakdown'): new_conv.character_breakdown = synced_settings.character_breakdown if hasattr(synced_settings, 'custom_instructions') and synced_settings.custom_instructions: new_conv.custom_instructions = synced_settings.custom_instructions # Add to user's conversations if self.user_id not in user_conversations: user_conversations[self.user_id] = [] user_conversations[self.user_id].append(new_conv) # Set as active conversation active_conversations[self.user_id] = new_conv.id # Save changes save_user_conversations() save_active_conversations() await interaction.response.edit_message( content=f"Created new conversation: {new_conv.title}", view=ConversationManagementView(self.user_id) ) else: # Set the selected conversation as active active_conversations[self.user_id] = selected_id save_active_conversations() # Find the selected conversation selected_conv = next((c for c in self.conversations if c.id == selected_id), None) if selected_conv: await interaction.response.edit_message( content=f"Switched to conversation: {selected_conv.title}", view=ConversationManagementView(self.user_id) ) else: await interaction.response.edit_message(content="Error: Conversation not found") class ConversationManagementView(ui.View): def __init__(self, user_id: int): super().__init__(timeout=300) # 5 minute timeout self.user_id = user_id # Get user's conversations conversations = user_conversations.get(user_id, []) # Get active conversation ID active_id = active_conversations.get(user_id) # Add conversation select menu self.add_item(ConversationSelectMenu(user_id, conversations, active_id)) @ui.button(label="Settings", style=discord.ButtonStyle.primary, emoji="⚙️") async def settings_button(self, interaction: discord.Interaction, button: ui.Button): # Get active conversation active_id = active_conversations.get(self.user_id) if not active_id: await interaction.response.send_message("No active conversation. Please select or create one first.", ephemeral=True) return # Find the active conversation conversations = user_conversations.get(self.user_id, []) active_conv = next((c for c in conversations if c.id == active_id), None) if not active_conv: await interaction.response.send_message("Error: Active conversation not found", ephemeral=True) return # Show settings for the active conversation embed = discord.Embed( title=f"Settings for: {active_conv.title}", description="Adjust settings for this conversation", color=discord.Color.blue() ) # Add fields for each setting embed.add_field(name="Model", value=active_conv.model_id, inline=True) embed.add_field(name="Temperature", value=str(active_conv.temperature), inline=True) embed.add_field(name="Max Tokens", value=str(active_conv.max_tokens), inline=True) embed.add_field(name="Reasoning", value="Enabled" if active_conv.reasoning_enabled else "Disabled", inline=True) embed.add_field(name="Reasoning Effort", value=active_conv.reasoning_effort.capitalize(), inline=True) embed.add_field(name="Web Search", value="Enabled" if active_conv.web_search_enabled else "Disabled", inline=True) # Add character settings embed.add_field(name="Character", value=active_conv.character or "None", inline=True) embed.add_field(name="Character Breakdown", value="Enabled" if active_conv.character_breakdown else "Disabled", inline=True) # Show system message (truncated if too long) system_msg = active_conv.system_message if len(system_msg) > 1000: system_msg = system_msg[:997] + "..." embed.add_field(name="System Message", value=system_msg, inline=False) # Show character info if available (truncated if too long) if active_conv.character_info: char_info = active_conv.character_info if len(char_info) > 1000: char_info = char_info[:997] + "..." embed.add_field(name="Character Info", value=char_info, inline=False) # Show custom instructions if available (truncated if too long) if active_conv.custom_instructions: custom_instr = active_conv.custom_instructions if len(custom_instr) > 1000: custom_instr = custom_instr[:997] + "..." embed.add_field(name="Custom Instructions", value=custom_instr, inline=False) await interaction.response.send_message(embed=embed, view=ConversationSettingsView(self.user_id, active_id), ephemeral=True) @ui.button(label="Rename", style=discord.ButtonStyle.secondary, emoji="✏️") async def rename_button(self, interaction: discord.Interaction, button: ui.Button): # Get active conversation active_id = active_conversations.get(self.user_id) if not active_id: await interaction.response.send_message("No active conversation. Please select or create one first.", ephemeral=True) return # Show rename modal await interaction.response.send_modal(RenameConversationModal(self.user_id, active_id)) @ui.button(label="Delete", style=discord.ButtonStyle.danger, emoji="🗑️") async def delete_button(self, interaction: discord.Interaction, button: ui.Button): # Get active conversation active_id = active_conversations.get(self.user_id) if not active_id: await interaction.response.send_message("No active conversation. Please select or create one first.", ephemeral=True) return # Find the active conversation conversations = user_conversations.get(self.user_id, []) active_conv = next((c for c in conversations if c.id == active_id), None) if not active_conv: await interaction.response.send_message("Error: Active conversation not found", ephemeral=True) return # Confirm deletion await interaction.response.send_message( f"Are you sure you want to delete the conversation '{active_conv.title}'?", view=DeleteConfirmationView(self.user_id, active_id), ephemeral=True ) class RenameConversationModal(ui.Modal, title="Rename Conversation"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add title input self.title_input = ui.TextInput( label="New Title", placeholder="Enter a new title for this conversation", default=self.conversation.title if self.conversation else "", required=True, max_length=100 ) self.add_item(self.title_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Update the conversation title self.conversation.title = self.title_input.value self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( f"Renamed conversation to: {self.conversation.title}", view=ConversationManagementView(self.user_id), ephemeral=True ) class DeleteConfirmationView(ui.View): def __init__(self, user_id: int, conversation_id: str): super().__init__(timeout=60) # 1 minute timeout self.user_id = user_id self.conversation_id = conversation_id @ui.button(label="Yes, Delete", style=discord.ButtonStyle.danger) async def confirm_button(self, interaction: discord.Interaction, button: ui.Button): # Get user's conversations conversations = user_conversations.get(self.user_id, []) # Find the conversation to delete conversation = next((c for c in conversations if c.id == self.conversation_id), None) if not conversation: await interaction.response.edit_message(content="Error: Conversation not found", view=None) return # Remove the conversation user_conversations[self.user_id] = [c for c in conversations if c.id != self.conversation_id] # If this was the active conversation, set a new active conversation if active_conversations.get(self.user_id) == self.conversation_id: if user_conversations[self.user_id]: # Set the first remaining conversation as active active_conversations[self.user_id] = user_conversations[self.user_id][0].id else: # No conversations left, remove active conversation if self.user_id in active_conversations: del active_conversations[self.user_id] # Save changes save_user_conversations() save_active_conversations() await interaction.response.edit_message( content=f"Deleted conversation: {conversation.title}", view=ConversationManagementView(self.user_id) if user_conversations.get(self.user_id) else None ) @ui.button(label="Cancel", style=discord.ButtonStyle.secondary) async def cancel_button(self, interaction: discord.Interaction, button: ui.Button): await interaction.response.edit_message(content="Deletion cancelled", view=None) class ConversationSettingsView(ui.View): def __init__(self, user_id: int, conversation_id: str): super().__init__(timeout=300) # 5 minute timeout self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) @ui.button(label="Edit System Message", style=discord.ButtonStyle.primary, row=0) async def system_message_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show system message modal await interaction.response.send_modal(SystemMessageModal(self.user_id, self.conversation_id)) @ui.button(label="Set Character", style=discord.ButtonStyle.primary, row=0) async def character_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show character modal await interaction.response.send_modal(CharacterModal(self.user_id, self.conversation_id)) @ui.button(label="Character Info", style=discord.ButtonStyle.primary, row=0) async def character_info_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show character info modal await interaction.response.send_modal(CharacterInfoModal(self.user_id, self.conversation_id)) @ui.button(label="Custom Instructions", style=discord.ButtonStyle.primary, row=1) async def custom_instructions_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show custom instructions modal await interaction.response.send_modal(CustomInstructionsModal(self.user_id, self.conversation_id)) @ui.button(label="Toggle Reasoning", style=discord.ButtonStyle.secondary, row=0) async def reasoning_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Toggle reasoning self.conversation.reasoning_enabled = not self.conversation.reasoning_enabled self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( f"Reasoning {'enabled' if self.conversation.reasoning_enabled else 'disabled'} for this conversation", ephemeral=True ) @ui.button(label="Toggle Web Search", style=discord.ButtonStyle.secondary, row=0) async def web_search_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Toggle web search self.conversation.web_search_enabled = not self.conversation.web_search_enabled self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( f"Web search {'enabled' if self.conversation.web_search_enabled else 'disabled'} for this conversation", ephemeral=True ) @ui.button(label="Set Temperature", style=discord.ButtonStyle.secondary, row=1) async def temperature_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show temperature modal await interaction.response.send_modal(TemperatureModal(self.user_id, self.conversation_id)) @ui.button(label="Set Max Tokens", style=discord.ButtonStyle.secondary, row=1) async def max_tokens_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show max tokens modal await interaction.response.send_modal(MaxTokensModal(self.user_id, self.conversation_id)) @ui.button(label="Set Model", style=discord.ButtonStyle.secondary, row=1) async def model_button(self, interaction: discord.Interaction, button: ui.Button): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Show model modal await interaction.response.send_modal(ModelModal(self.user_id, self.conversation_id)) class SystemMessageModal(ui.Modal, title="Edit System Message"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add system message input self.system_message_input = ui.TextInput( label="System Message", placeholder="Enter a system message for this conversation", default=self.conversation.system_message if self.conversation else AI_DEFAULT_SYSTEM_PROMPT, required=True, style=discord.TextStyle.paragraph, max_length=4000 ) self.add_item(self.system_message_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Update the conversation system message self.conversation.system_message = self.system_message_input.value self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( "System message updated", ephemeral=True ) class TemperatureModal(ui.Modal, title="Set Temperature"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add temperature input self.temperature_input = ui.TextInput( label="Temperature (0.0 - 2.0)", placeholder="Enter a value between 0.0 and 2.0", default=str(self.conversation.temperature) if self.conversation else str(AI_TEMPERATURE), required=True, max_length=4 ) self.add_item(self.temperature_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return try: # Parse and validate temperature temperature = float(self.temperature_input.value) if temperature < 0.0 or temperature > 2.0: await interaction.response.send_message("Temperature must be between 0.0 and 2.0", ephemeral=True) return # Update the conversation temperature self.conversation.temperature = temperature self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( f"Temperature set to {temperature}", ephemeral=True ) except ValueError: await interaction.response.send_message("Invalid temperature value. Please enter a number between 0.0 and 2.0", ephemeral=True) class MaxTokensModal(ui.Modal, title="Set Max Tokens"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add max tokens input self.max_tokens_input = ui.TextInput( label="Max Tokens (100 - 4000)", placeholder="Enter a value between 100 and 4000", default=str(self.conversation.max_tokens) if self.conversation else str(AI_MAX_TOKENS), required=True, max_length=4 ) self.add_item(self.max_tokens_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return try: # Parse and validate max tokens max_tokens = int(self.max_tokens_input.value) if max_tokens < 100 or max_tokens > 4000: await interaction.response.send_message("Max tokens must be between 100 and 4000", ephemeral=True) return # Update the conversation max tokens self.conversation.max_tokens = max_tokens self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( f"Max tokens set to {max_tokens}", ephemeral=True ) except ValueError: await interaction.response.send_message("Invalid max tokens value. Please enter a number between 100 and 4000", ephemeral=True) class ModelModal(ui.Modal, title="Set Model"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add model input self.model_input = ui.TextInput( label="Model", placeholder="Enter a model name (e.g., gpt-3.5-turbo, gpt-4)", default=self.conversation.model_id if self.conversation else AI_DEFAULT_MODEL, required=True, max_length=50 ) self.add_item(self.model_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Update the conversation model self.conversation.model_id = self.model_input.value self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message( f"Model set to {self.conversation.model_id}", ephemeral=True ) class CharacterModal(ui.Modal, title="Set Character"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add character input self.character_input = ui.TextInput( label="Character Name", placeholder="Enter a character name (e.g., Hatsune Miku)", default=self.conversation.character if self.conversation else "", required=False, max_length=100 ) self.add_item(self.character_input) # Add character breakdown toggle self.breakdown_input = ui.TextInput( label="Character Breakdown", placeholder="Type 'yes' to enable, 'no' to disable", default="yes" if self.conversation and self.conversation.character_breakdown else "no", required=False, max_length=3 ) self.add_item(self.breakdown_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Update the conversation character self.conversation.character = self.character_input.value # Update character breakdown setting breakdown_value = self.breakdown_input.value.lower() if breakdown_value in ["yes", "y", "true", "on", "1"]: self.conversation.character_breakdown = True elif breakdown_value in ["no", "n", "false", "off", "0"]: self.conversation.character_breakdown = False self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() # Prepare response message if self.conversation.character: message = f"Character set to: {self.conversation.character}\n" else: message = "Character cleared.\n" message += f"Character breakdown: {'Enabled' if self.conversation.character_breakdown else 'Disabled'}" await interaction.response.send_message(message, ephemeral=True) class CharacterInfoModal(ui.Modal, title="Character Information"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add character info input self.character_info_input = ui.TextInput( label="Character Information", placeholder="Enter information about the character being roleplayed", default=self.conversation.character_info if self.conversation else "", required=False, style=discord.TextStyle.paragraph, max_length=4000 ) self.add_item(self.character_info_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Update the conversation character info self.conversation.character_info = self.character_info_input.value self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message("Character information updated.", ephemeral=True) class CustomInstructionsModal(ui.Modal, title="Custom Instructions"): def __init__(self, user_id: int, conversation_id: str): super().__init__() self.user_id = user_id self.conversation_id = conversation_id # Find the conversation conversations = user_conversations.get(user_id, []) self.conversation = next((c for c in conversations if c.id == conversation_id), None) # Add custom instructions input self.custom_instructions_input = ui.TextInput( label="Custom Instructions", placeholder="Enter custom instructions for the AI to follow", default=self.conversation.custom_instructions if self.conversation else "", required=False, style=discord.TextStyle.paragraph, max_length=4000 ) self.add_item(self.custom_instructions_input) async def on_submit(self, interaction: discord.Interaction): if not self.conversation: await interaction.response.send_message("Error: Conversation not found", ephemeral=True) return # Update the conversation custom instructions self.conversation.custom_instructions = self.custom_instructions_input.value self.conversation.updated_at = datetime.datetime.now() # Save changes save_user_conversations() await interaction.response.send_message("Custom instructions updated.", ephemeral=True) class ImportConfirmationView(ui.View): def __init__(self, cog, user_id: int): super().__init__(timeout=60) # 1 minute timeout self.cog = cog self.user_id = user_id @ui.button(label="Yes, Import", style=discord.ButtonStyle.primary) async def confirm_button(self, interaction: discord.Interaction, button: ui.Button): # Defer response since this might take a while await interaction.response.defer(thinking=True) # Import conversations result = await self.cog.import_conversations(self.user_id) # Send result await interaction.followup.send(result) @ui.button(label="Cancel", style=discord.ButtonStyle.secondary) async def cancel_button(self, interaction: discord.Interaction, button: ui.Button): await interaction.response.edit_message(content="Import cancelled", view=None)