""" Gradio UI for Character Creator """ import gradio as gr from typing import Optional, Tuple import traceback from src.agents.character_agent import CharacterAgent from src.agents.campaign_agent import CampaignAgent from src.models.character import DnDRace, DnDClass from src.models.campaign import CampaignTheme from src.utils.validators import get_available_races, get_available_classes from src.utils.image_generator import RACE_SKIN_TONES from src.utils.character_sheet_exporter import CharacterSheetExporter class CharacterCreatorUI: """Gradio interface for character creation""" def __init__(self): self.agent = CharacterAgent() self.campaign_agent = CampaignAgent() self.exporter = CharacterSheetExporter() def _get_alignment_description(self, alignment: str) -> str: """Get personality guidance based on alignment""" descriptions = { "Lawful Good": "a strong sense of justice, honor, and desire to help others within the rules", "Neutral Good": "genuine kindness and desire to help, but flexibility in how they achieve good", "Chaotic Good": "rebellious goodness, fighting for freedom and helping others by breaking unjust rules", "Lawful Neutral": "strict adherence to law, order, and tradition above good or evil", "True Neutral": "balance and pragmatism, avoiding extreme positions", "Chaotic Neutral": "unpredictability, freedom-loving nature, and self-interest", "Lawful Evil": "tyrannical control, following their own code while causing harm", "Neutral Evil": "pure self-interest and willingness to harm others for personal gain", "Chaotic Evil": "destructive chaos, cruelty, and disregard for any rules or others' wellbeing" } return descriptions.get(alignment, "their moral compass") def generate_name_ui( self, race: str, character_class: str, gender: str, alignment: str, ) -> str: """ Generate a character name using AI This is a self-contained function that calls the agent's generate_name method Alignment can influence name generation (e.g., darker names for evil characters) """ try: race_enum = DnDRace(race) class_enum = DnDClass(character_class) # Add alignment hint to the generation prompt alignment_hint = None if "Evil" in alignment: alignment_hint = "with a darker, more menacing tone" elif "Good" in alignment: alignment_hint = "with a heroic, noble tone" elif alignment == "Chaotic Neutral": alignment_hint = "with a wild, unpredictable feel" # Build custom prompt if we have alignment influence if alignment_hint: prompt = f"""Generate a single fantasy character name for a D&D character. Race: {race_enum.value} Class: {class_enum.value} Gender: {gender if gender != "Not specified" else "any"} Alignment: {alignment} - name should reflect this {alignment_hint} Requirements: - Just the name, nothing else - Make it sound appropriate for the race, gender, and alignment - {alignment_hint} - Make it memorable and fitting for an adventurer - 2-3 words maximum Examples: - Evil: "Malakai Shadowbane", "Drusilla Nightwhisper" - Good: "Elara Lightbringer", "Theron Brightheart" - Chaotic: "Raven Wildfire", "Zephyr Stormblade" Generate only the name:""" name = self.agent.ai_client.generate_creative(prompt).strip() name = name.split('\n')[0].strip('"\'') return name else: # Use standard generation name = self.agent.generate_name( race=race_enum, character_class=class_enum, gender=gender if gender != "Not specified" else None ) return name except Exception as e: return f"Error: {str(e)}" def create_character_ui( self, name: str, race: str, character_class: str, level: int, gender: str, skin_tone: str, alignment: str, background_dropdown: str, custom_background: str, personality_prompt: str, stats_method: str, use_ai_background: bool, ) -> Tuple[str, str]: """ Create character with UI inputs Returns: Tuple of (character_sheet_markdown, status_message) """ try: # Validate inputs if not name.strip(): return "", "❌ Error: Please provide a character name (use 'Generate Name' button or type one)" if level < 1 or level > 20: return "", "❌ Error: Level must be between 1 and 20" # Convert race, class, and alignment try: race_enum = DnDRace(race) class_enum = DnDClass(character_class) from src.models.character import Alignment alignment_enum = Alignment(alignment) except ValueError as e: return "", f"❌ Error: Invalid race, class, or alignment - {e}" # Determine final background type if background_dropdown == "Custom (enter below)": final_background = custom_background.strip() if custom_background.strip() else "Adventurer" else: final_background = background_dropdown # Create character with gender AND alignment in personality prompt gender_hint = f"Character is {gender}. " if gender != "Not specified" else "" alignment_hint = f"Character's alignment is {alignment}, so their personality and backstory should reflect {self._get_alignment_description(alignment)}. " full_personality_prompt = gender_hint + alignment_hint + (personality_prompt if personality_prompt else "") character = self.agent.create_character( name=name, race=race_enum, character_class=class_enum, level=level, background_type=final_background, personality_prompt=full_personality_prompt if use_ai_background else None, stats_method=stats_method, ) # Override alignment, gender, and skin tone if user specified character.alignment = alignment_enum character.gender = gender if gender != "Not specified" else None character.skin_tone = skin_tone if skin_tone else None # Generate markdown markdown = character.to_markdown() status = f"""✅ Character Created Successfully! **ID:** {character.id} **Name:** {character.name} **Race:** {character.race.value} **Class:** {character.character_class.value} **Level:** {character.level} Character has been saved to database.""" return markdown, status except Exception as e: error_msg = f"❌ Error creating character:\n\n{str(e)}\n\n{traceback.format_exc()}" return "", error_msg def load_character_ui(self, character_id: str) -> Tuple[str, str]: """Load character by ID""" try: if not character_id.strip(): return "", "❌ Error: Please provide a character ID" character = self.agent.load_character(character_id) if character: markdown = character.to_markdown() status = f"✅ Loaded character: {character.name}" return markdown, status else: return "", f"❌ Character not found: {character_id}" except Exception as e: return "", f"❌ Error loading character: {e}" def list_characters_ui(self) -> Tuple[str, str]: """List all saved characters""" try: characters = self.agent.list_characters() if not characters: return "", "No characters found in database." # Create table markdown = "# Saved Characters\n\n" markdown += "| Name | Race | Class | Level | ID |\n" markdown += "|------|------|-------|-------|----|\n" for char in characters[-20:]: # Last 20 characters markdown += f"| {char.name} | {char.race.value} | {char.character_class.value} | {char.level} | `{char.id}` |\n" status = f"✅ Found {len(characters)} character(s)" return markdown, status except Exception as e: return "", f"❌ Error listing characters: {e}" def delete_character_ui(self, character_id: str) -> str: """Delete character by ID""" try: if not character_id.strip(): return "❌ Error: Please provide a character ID" # Check if exists character = self.agent.load_character(character_id) if not character: return f"❌ Character not found: {character_id}" # Delete self.agent.delete_character(character_id) return f"✅ Deleted character: {character.name} ({character_id})" except Exception as e: return f"❌ Error deleting character: {e}" def generate_portrait_ui( self, character_id: str, style: str = "fantasy art", quality: str = "standard", provider: str = "auto" ) -> Tuple[Optional[str], str]: """ Generate character portrait Returns: Tuple of (image_path, status_message) """ try: if not character_id.strip(): return None, "❌ Error: Please provide a character ID" # Load character character = self.agent.load_character(character_id) if not character: return None, f"❌ Character not found: {character_id}" # Generate portrait file_path, status = self.agent.generate_portrait( character=character, style=style, quality=quality, provider=provider ) return file_path, status except Exception as e: import traceback error_msg = f"❌ Error generating portrait:\n\n{str(e)}\n\n{traceback.format_exc()}" return None, error_msg def export_character_sheet_ui( self, character_id: str, export_format: str = "markdown" ) -> str: """ Export character sheet to file Returns: Status message with file path """ try: if not character_id.strip(): return "❌ Error: Please provide a character ID" # Load character character = self.agent.load_character(character_id) if not character: return f"❌ Character not found: {character_id}" # Export to selected format file_path = self.exporter.save_export(character, format=export_format) return f"""✅ Character sheet exported successfully! **Character:** {character.name} **Format:** {export_format.upper()} **File:** {file_path} You can find the exported file in the data/exports/ directory.""" except Exception as e: return f"❌ Error exporting character sheet:\n\n{str(e)}\n\n{traceback.format_exc()}" def preview_export_ui( self, character_id: str, export_format: str = "markdown" ) -> Tuple[str, str]: """ Preview character sheet export without saving Returns: Tuple of (preview_content, status_message) """ try: if not character_id.strip(): return "", "❌ Error: Please provide a character ID" # Load character character = self.agent.load_character(character_id) if not character: return "", f"❌ Character not found: {character_id}" # Generate preview based on format if export_format == "markdown": preview = self.exporter.export_to_markdown(character) elif export_format == "json": preview = f"```json\n{self.exporter.export_to_json(character)}\n```" elif export_format == "html": preview = f"```html\n{self.exporter.export_to_html(character)}\n```" else: return "", f"❌ Unknown format: {export_format}" status = f"✅ Preview generated for {character.name}" return preview, status except Exception as e: return "", f"❌ Error generating preview:\n\n{str(e)}\n\n{traceback.format_exc()}" # Campaign Management UI Methods def create_campaign_ui( self, name: str, theme: str, setting: str, summary: str, main_conflict: str, game_master: str, world_name: str, starting_location: str, level_range: str, party_size: int ) -> str: """Create a new campaign""" try: if not name.strip(): return "❌ Error: Please provide a campaign name" campaign = self.campaign_agent.create_campaign( name=name, theme=theme, setting=setting, summary=summary, main_conflict=main_conflict, game_master=game_master, world_name=world_name, starting_location=starting_location, level_range=level_range, party_size=party_size ) return f"""✅ Campaign Created Successfully! **ID:** {campaign.id} **Name:** {campaign.name} **Theme:** {campaign.theme.value} **Setting:** {campaign.setting} Campaign has been saved to database. Use the campaign ID to manage characters and sessions.""" except Exception as e: return f"❌ Error creating campaign:\n\n{str(e)}\n\n{traceback.format_exc()}" def list_campaigns_ui(self, active_only: bool = False) -> Tuple[str, str]: """List all campaigns""" try: campaigns = self.campaign_agent.list_campaigns(active_only=active_only) if not campaigns: return "", "No campaigns found in database." # Create table markdown = "# Campaigns\n\n" markdown += "| Name | Theme | Session | Status | ID |\n" markdown += "|------|-------|---------|--------|----|\n" for campaign in campaigns[-20:]: # Last 20 campaigns status = "Active" if campaign.is_active else "Inactive" markdown += f"| {campaign.name} | {campaign.theme.value} | {campaign.current_session} | {status} | `{campaign.id}` |\n" status = f"✅ Found {len(campaigns)} campaign(s)" return markdown, status except Exception as e: return "", f"❌ Error listing campaigns: {e}" def load_campaign_ui(self, campaign_id: str) -> Tuple[str, str]: """Load campaign details""" try: if not campaign_id.strip(): return "", "❌ Error: Please provide a campaign ID" campaign = self.campaign_agent.load_campaign(campaign_id) if campaign: markdown = campaign.to_markdown() status = f"✅ Loaded campaign: {campaign.name}" return markdown, status else: return "", f"❌ Campaign not found: {campaign_id}" except Exception as e: return "", f"❌ Error loading campaign: {e}" def add_character_to_campaign_ui(self, campaign_id: str, character_id: str) -> str: """Add a character to a campaign""" try: if not campaign_id.strip() or not character_id.strip(): return "❌ Error: Please provide both campaign ID and character ID" # Verify character exists character = self.agent.load_character(character_id) if not character: return f"❌ Character not found: {character_id}" # Add to campaign success = self.campaign_agent.add_character_to_campaign(campaign_id, character_id) if success: return f"✅ Added {character.name} to campaign!" else: return f"❌ Campaign not found: {campaign_id}" except Exception as e: return f"❌ Error: {str(e)}" def start_session_ui(self, campaign_id: str) -> str: """Start a new session""" try: if not campaign_id.strip(): return "❌ Error: Please provide a campaign ID" campaign = self.campaign_agent.load_campaign(campaign_id) if not campaign: return f"❌ Campaign not found: {campaign_id}" self.campaign_agent.start_new_session(campaign_id) return f"""✅ Started Session {campaign.current_session + 1}! **Campaign:** {campaign.name} **New Session Number:** {campaign.current_session + 1} **Total Sessions:** {campaign.total_sessions + 1}""" except Exception as e: return f"❌ Error: {str(e)}" def auto_generate_session_ui(self, campaign_id: str) -> str: """Auto-generate next session using AI""" try: if not campaign_id.strip(): return "❌ Error: Please select a campaign" campaign = self.campaign_agent.load_campaign(campaign_id) if not campaign: return f"❌ Campaign not found: {campaign_id}" # Generate session using autonomous AI session_data = self.campaign_agent.auto_generate_next_session(campaign_id) if 'error' in session_data: return f"❌ Error: {session_data['error']}" # Format output for display output = [] output.append(f"# 🤖 Auto-Generated Session {session_data.get('session_number', 'N/A')}") output.append(f"\n**Campaign:** {campaign.name}") output.append(f"\n**Session Title:** {session_data.get('session_title', 'Untitled')}") output.append(f"\n---\n") # Opening Scene if 'opening_scene' in session_data: output.append(f"## 🎬 Opening Scene\n\n{session_data['opening_scene']}\n\n") # Key Encounters if 'key_encounters' in session_data and session_data['key_encounters']: output.append("## ⚔️ Key Encounters\n\n") for i, encounter in enumerate(session_data['key_encounters'], 1): output.append(f"{i}. {encounter}\n") output.append("\n") # NPCs Featured if 'npcs_featured' in session_data and session_data['npcs_featured']: output.append("## 👥 NPCs Featured\n\n") for npc in session_data['npcs_featured']: output.append(f"- {npc}\n") output.append("\n") # Locations if 'locations' in session_data and session_data['locations']: output.append("## 🗺️ Locations\n\n") for loc in session_data['locations']: output.append(f"- {loc}\n") output.append("\n") # Plot Developments if 'plot_developments' in session_data and session_data['plot_developments']: output.append("## 📖 Plot Developments\n\n") for i, dev in enumerate(session_data['plot_developments'], 1): output.append(f"{i}. {dev}\n") output.append("\n") # Potential Outcomes if 'potential_outcomes' in session_data and session_data['potential_outcomes']: output.append("## 🎲 Potential Outcomes\n\n") for i, outcome in enumerate(session_data['potential_outcomes'], 1): output.append(f"{i}. {outcome}\n") output.append("\n") # Rewards if 'rewards' in session_data and session_data['rewards']: output.append("## 💰 Rewards\n\n") for reward in session_data['rewards']: output.append(f"- {reward}\n") output.append("\n") # Cliffhanger if 'cliffhanger' in session_data and session_data['cliffhanger']: output.append(f"## 🎭 Cliffhanger\n\n{session_data['cliffhanger']}\n\n") output.append("---\n\n") output.append("✅ **Session plan generated successfully!**\n\n") output.append("💡 **Next Steps:**\n") output.append("- Review the session plan above\n") output.append("- Adjust encounters/NPCs as needed for your table\n") output.append("- Copy relevant sections to your session notes\n") output.append("- Start the session when ready!\n") return "".join(output) except Exception as e: import traceback return f"❌ Error generating session:\n\n{str(e)}\n\n{traceback.format_exc()}" def add_event_ui( self, campaign_id: str, event_type: str, title: str, description: str, importance: int ) -> str: """Add an event to the campaign""" try: if not campaign_id.strip(): return "❌ Error: Please provide a campaign ID" if not title.strip() or not description.strip(): return "❌ Error: Please provide event title and description" event = self.campaign_agent.add_event( campaign_id=campaign_id, event_type=event_type, title=title, description=description, importance=importance ) if event: return f"""✅ Event Added! **Title:** {title} **Type:** {event_type} **Importance:** {'⭐' * importance} Event has been recorded in campaign history.""" else: return f"❌ Campaign not found: {campaign_id}" except Exception as e: return f"❌ Error: {str(e)}" def get_character_choices_ui(self) -> list: """Get list of characters for selection""" try: characters = self.agent.list_characters() if not characters: return [] # Create choices as "Name (Race Class, Level X) - ID" choices = [] for char in characters: label = f"{char.name} ({char.race.value} {char.character_class.value}, Level {char.level})" choices.append((label, char.id)) return choices except Exception as e: return [] def get_character_dropdown_choices(self) -> list: """Get character choices for dropdown (returns IDs only)""" try: characters = self.agent.list_characters() if not characters: return [] # Create dropdown choices with nice labels choices = [] for char in characters: label = f"{char.name} ({char.race.value} {char.character_class.value}, Lvl {char.level})" choices.append(label) return choices except Exception as e: return [] def get_character_id_from_label(self, label: str) -> str: """Extract character ID from dropdown label""" try: # Parse the label to get character name if not label: return "" name = label.split(" (")[0] if " (" in label else label # Find character by name characters = self.agent.list_characters() for char in characters: if char.name == name: return char.id return "" except Exception as e: return "" def get_campaign_dropdown_choices(self) -> list: """Get campaign choices for dropdown""" try: campaigns = self.campaign_agent.list_campaigns() if not campaigns: return [] choices = [] for campaign in campaigns: label = f"{campaign.name} ({campaign.theme.value}, Session {campaign.current_session})" choices.append(label) return choices except Exception as e: return [] def get_campaign_id_from_label(self, label: str) -> str: """Extract campaign ID from dropdown label""" try: if not label: return "" name = label.split(" (")[0] if " (" in label else label campaigns = self.campaign_agent.list_campaigns() for campaign in campaigns: if campaign.name == name: return campaign.id return "" except Exception as e: return "" def synthesize_campaign_ui( self, selected_character_ids: list, game_master: str, additional_notes: str ) -> str: """Synthesize a campaign from selected characters""" try: # Check if any characters selected if not selected_character_ids: return "❌ Error: Please select at least one character" # Load all characters characters = [] for char_id in selected_character_ids: char = self.agent.load_character(char_id) if char: characters.append(char) if not characters: return "❌ Error: No valid characters found" # Synthesize campaign campaign = self.campaign_agent.synthesize_campaign_from_characters( characters=characters, game_master=game_master, additional_notes=additional_notes ) # Create response with character list char_list = "\n".join([f"- {char.name} (Level {char.level} {char.race.value} {char.character_class.value})" for char in characters]) # Build comprehensive output with all campaign details output = [f"""✅ Campaign Synthesized Successfully! **Campaign ID:** {campaign.id} **Campaign Name:** {campaign.name} **Theme:** {campaign.theme.value} **World:** {campaign.world_name} **Starting Location:** {campaign.starting_location} **Party Members ({len(characters)}):** {char_list} **Level Range:** {campaign.level_range} --- ## Campaign Overview **Summary:** {campaign.summary} **Main Conflict:** {campaign.main_conflict} **Current Story Arc:** {campaign.current_arc if campaign.current_arc else "See detailed notes below"} """] # Add factions if present if campaign.key_factions: output.append("\n## Key Factions\n") for faction in campaign.key_factions: output.append(f"- {faction}\n") # Add villains if present if campaign.major_villains: output.append("\n## Major Villains\n") for villain in campaign.major_villains: output.append(f"- {villain}\n") # Add mysteries if present if campaign.central_mysteries: output.append("\n## Central Mysteries\n") for mystery in campaign.central_mysteries: output.append(f"- {mystery}\n") # Add detailed campaign notes (includes character connections, hooks, sessions, NPCs, locations) if campaign.notes: output.append("\n---\n\n") output.append(campaign.notes) output.append(f""" --- ✅ **Campaign Created!** All characters have been added to the campaign. 💡 **Next Steps:** - View full details in the "Manage Campaign" tab - Start your first session in "Session Tracking" - Add campaign events as your story unfolds""") return "".join(output) except Exception as e: return f"❌ Error synthesizing campaign:\n\n{str(e)}\n\n{traceback.format_exc()}" def create_interface(self) -> gr.Blocks: """Create Gradio interface""" with gr.Blocks(title="D'n'D Campaign Manager - Character Creator") as interface: gr.Markdown(""" # 🎲 D'n'D Campaign Manager ## Complete D&D Character Creator Create and manage complete D&D 5e characters for your campaigns! """) with gr.Tabs(): # Tab 1: Create Character with gr.Tab("Create Character"): gr.Markdown("### Character Creation") with gr.Row(): with gr.Column(): gr.Markdown("#### Basic Information") with gr.Row(): name_input = gr.Textbox( label="Character Name", placeholder="Thorin Ironforge", info="Type a name or generate one below", scale=3 ) race_dropdown = gr.Dropdown( choices=get_available_races(), label="Race", value="Human", info="Character's race" ) class_dropdown = gr.Dropdown( choices=get_available_classes(), label="Class", value="Fighter", info="Character's class" ) gender_dropdown = gr.Dropdown( choices=["Male", "Female", "Non-binary", "Not specified"], label="Gender", value="Not specified", info="Character's gender" ) skin_tone_dropdown = gr.Dropdown( choices=RACE_SKIN_TONES[DnDRace.HUMAN], # Default to Human label="Skin Tone / Color", value=None, info="Select appropriate color for the race" ) generate_name_btn = gr.Button("🎲 Generate Name", variant="secondary", size="sm") level_slider = gr.Slider( minimum=1, maximum=20, value=1, step=1, label="Level", info="Character level (1-20)" ) alignment_dropdown = gr.Dropdown( choices=[ "Lawful Good", "Neutral Good", "Chaotic Good", "Lawful Neutral", "True Neutral", "Chaotic Neutral", "Lawful Evil", "Neutral Evil", "Chaotic Evil" ], label="Alignment", value="True Neutral", info="Character's moral alignment" ) with gr.Column(): gr.Markdown("#### Background & Personality") background_dropdown = gr.Dropdown( choices=[ "Acolyte", "Charlatan", "Criminal", "Entertainer", "Folk Hero", "Guild Artisan", "Hermit", "Noble", "Outlander", "Sage", "Sailor", "Soldier", "Urchin", "Custom (enter below)" ], label="Background Type", value="Soldier", info="Select from D&D 5e backgrounds or choose Custom" ) custom_background_input = gr.Textbox( label="Custom Background", placeholder="Enter your custom background...", value="", visible=False, info="Only used if 'Custom' is selected above" ) use_ai_background = gr.Checkbox( label="Generate detailed backstory", value=True, info="Create a unique backstory for this character" ) personality_input = gr.Textbox( label="Personality Guidance (Optional)", placeholder="A mysterious ranger who protects the forest...", lines=3, info="Guide AI in creating personality (if enabled)" ) stats_method = gr.Radio( choices=["standard_array", "roll", "point_buy"], label="Ability Score Method", value="standard_array", info="How to generate ability scores" ) create_btn = gr.Button("⚔️ Create Character", variant="primary", size="lg") gr.Markdown("---") with gr.Row(): character_output = gr.Markdown(label="Character Sheet") status_output = gr.Textbox(label="Status", lines=8) # Toggle custom background visibility def toggle_custom_background(background_choice): return gr.update(visible=background_choice == "Custom (enter below)") # Update skin tone options when race changes def update_skin_tone_choices(race: str): try: race_enum = DnDRace(race) skin_tones = RACE_SKIN_TONES.get(race_enum, RACE_SKIN_TONES[DnDRace.HUMAN]) return gr.update(choices=skin_tones, value=skin_tones[0] if skin_tones else None) except: return gr.update(choices=RACE_SKIN_TONES[DnDRace.HUMAN], value=RACE_SKIN_TONES[DnDRace.HUMAN][0]) background_dropdown.change( fn=toggle_custom_background, inputs=[background_dropdown], outputs=[custom_background_input] ) race_dropdown.change( fn=update_skin_tone_choices, inputs=[race_dropdown], outputs=[skin_tone_dropdown] ) # Generate name action - includes alignment for more thematic names generate_name_btn.click( fn=self.generate_name_ui, inputs=[race_dropdown, class_dropdown, gender_dropdown, alignment_dropdown], outputs=[name_input] ) # Create character action create_btn.click( fn=self.create_character_ui, inputs=[ name_input, race_dropdown, class_dropdown, level_slider, gender_dropdown, skin_tone_dropdown, alignment_dropdown, background_dropdown, custom_background_input, personality_input, stats_method, use_ai_background, ], outputs=[character_output, status_output] ) # Tab 2: Load Character with gr.Tab("Load Character"): gr.Markdown("### Load Saved Character") load_char_refresh_btn = gr.Button("🔄 Refresh Character List", variant="secondary") character_dropdown = gr.Dropdown( choices=[], label="Select Character", info="Choose a character from the list (type to search)", allow_custom_value=False, interactive=True ) with gr.Row(): load_btn = gr.Button("📂 Load Character", variant="primary") list_btn = gr.Button("📋 List All Characters") gr.Markdown("---") with gr.Row(): loaded_character_output = gr.Markdown(label="Character Sheet") load_status_output = gr.Textbox(label="Status", lines=6) # Refresh character dropdown def refresh_character_dropdown(): choices = self.get_character_dropdown_choices() return gr.update(choices=choices, value=None) load_char_refresh_btn.click( fn=refresh_character_dropdown, inputs=[], outputs=[character_dropdown] ) # Load character action - convert dropdown label to ID def load_character_from_dropdown(label): char_id = self.get_character_id_from_label(label) return self.load_character_ui(char_id) load_btn.click( fn=load_character_from_dropdown, inputs=[character_dropdown], outputs=[loaded_character_output, load_status_output] ) # List characters action list_btn.click( fn=self.list_characters_ui, inputs=[], outputs=[loaded_character_output, load_status_output] ) # Tab 3: Manage Characters with gr.Tab("Manage Characters"): gr.Markdown("### Character Management") delete_refresh_btn = gr.Button("🔄 Refresh Character List", variant="secondary") delete_character_dropdown = gr.Dropdown( choices=[], label="Select Character to Delete", info="⚠️ Warning: This action cannot be undone! (type to search)", allow_custom_value=False, interactive=True ) delete_btn = gr.Button("🗑️ Delete Character", variant="stop") delete_status_output = gr.Textbox(label="Status", lines=3) # Refresh delete character dropdown def refresh_delete_dropdown(): choices = self.get_character_dropdown_choices() return gr.update(choices=choices, value=None) delete_refresh_btn.click( fn=refresh_delete_dropdown, inputs=[], outputs=[delete_character_dropdown] ) # Delete character action - convert dropdown label to ID def delete_character_from_dropdown(label): char_id = self.get_character_id_from_label(label) return self.delete_character_ui(char_id) delete_btn.click( fn=delete_character_from_dropdown, inputs=[delete_character_dropdown], outputs=[delete_status_output] ) gr.Markdown("---") with gr.Accordion("Quick Actions", open=False): quick_list_btn = gr.Button("📋 List All Characters") quick_list_output = gr.Markdown(label="Character List") quick_status = gr.Textbox(label="Status", lines=2) quick_list_btn.click( fn=self.list_characters_ui, inputs=[], outputs=[quick_list_output, quick_status] ) # Tab 4: Generate Portrait with gr.Tab("Generate Portrait"): gr.Markdown(""" ### 🎨 AI Character Portrait Generator Generate stunning character portraits using DALL-E 3 or HuggingFace! """) with gr.Row(): with gr.Column(): portrait_refresh_btn = gr.Button("🔄 Refresh Character List", variant="secondary") portrait_character_dropdown = gr.Dropdown( choices=[], label="Select Character", info="Choose a character to generate portrait for (type to search)", allow_custom_value=False, interactive=True ) portrait_provider = gr.Radio( choices=["auto", "openai", "huggingface"], label="Image Provider", value="auto", info="Auto: Try OpenAI first, fallback to HuggingFace if needed" ) portrait_style = gr.Dropdown( choices=[ "fantasy art", "digital painting", "anime style", "oil painting", "watercolor", "comic book art", "concept art" ], label="Art Style", value="fantasy art", info="Choose the artistic style" ) portrait_quality = gr.Radio( choices=["standard", "hd"], label="Image Quality (OpenAI only)", value="standard", info="HD costs more tokens (OpenAI only)" ) generate_portrait_btn = gr.Button( "🎨 Generate Portrait", variant="primary", size="lg" ) portrait_status = gr.Textbox( label="Status", lines=4 ) with gr.Column(): portrait_output = gr.Image( label="Generated Portrait", type="filepath", height=512 ) gr.Markdown(""" **Providers:** - **OpenAI DALL-E 3**: High quality, costs $0.04/image (standard) or $0.08/image (HD) - **HuggingFace (Free!)**: Stable Diffusion XL, ~100 requests/day on free tier - **Auto**: Tries OpenAI first, automatically falls back to HuggingFace if billing issues Portraits are automatically saved to `data/portraits/` directory. **Tips:** - Use "auto" mode for seamless fallback - OpenAI HD quality produces better results but costs 2x - HuggingFace is free but may have a 30-60s warm-up time - Different styles work better for different races/classes """) # Refresh portrait character dropdown def refresh_portrait_dropdown(): choices = self.get_character_dropdown_choices() return gr.update(choices=choices, value=None) portrait_refresh_btn.click( fn=refresh_portrait_dropdown, inputs=[], outputs=[portrait_character_dropdown] ) # Generate portrait action - convert dropdown label to ID def generate_portrait_from_dropdown(label, style, quality, provider): char_id = self.get_character_id_from_label(label) return self.generate_portrait_ui(char_id, style, quality, provider) generate_portrait_btn.click( fn=generate_portrait_from_dropdown, inputs=[portrait_character_dropdown, portrait_style, portrait_quality, portrait_provider], outputs=[portrait_output, portrait_status] ) # Tab 5: Export Character Sheet with gr.Tab("Export Character Sheet"): gr.Markdown(""" ### 📄 Export Character Sheets Export your characters to formatted character sheets in multiple formats! """) with gr.Row(): with gr.Column(): export_char_refresh_btn = gr.Button("🔄 Refresh Character List", variant="secondary") export_character_dropdown = gr.Dropdown( choices=[], label="Select Character", info="Choose a character to export (type to search)", allow_custom_value=False, interactive=True ) export_format = gr.Radio( choices=["markdown", "json", "html"], label="Export Format", value="markdown", info="Choose the format for your character sheet" ) with gr.Row(): preview_btn = gr.Button("👁️ Preview", variant="secondary") export_btn = gr.Button("💾 Export to File", variant="primary") export_status = gr.Textbox( label="Status", lines=6 ) with gr.Column(): preview_output = gr.Markdown( label="Preview", value="Character sheet preview will appear here..." ) gr.Markdown(""" ### Format Descriptions **Markdown (.md)** - Clean, readable text format with tables - Perfect for sharing in Discord, GitHub, or note apps - Includes all character stats, features, and background - Easy to read and edit **JSON (.json)** - Structured data format - Perfect for importing into other tools or programs - Contains all character data in a machine-readable format - Great for backup or data transfer **HTML (.html)** - Styled character sheet that can be opened in a browser - Print-ready format (mimics official D&D character sheet) - Beautiful parchment styling with maroon borders - Can be converted to PDF using browser's print function All exports are saved to the `data/exports/` directory. """) # Refresh export character dropdown def refresh_export_dropdown(): choices = self.get_character_dropdown_choices() return gr.update(choices=choices, value=None) export_char_refresh_btn.click( fn=refresh_export_dropdown, inputs=[], outputs=[export_character_dropdown] ) # Preview action - convert dropdown label to ID def preview_from_dropdown(label, format): char_id = self.get_character_id_from_label(label) return self.preview_export_ui(char_id, format) preview_btn.click( fn=preview_from_dropdown, inputs=[export_character_dropdown, export_format], outputs=[preview_output, export_status] ) # Export action - convert dropdown label to ID def export_from_dropdown(label, format): char_id = self.get_character_id_from_label(label) return self.export_character_sheet_ui(char_id, format) export_btn.click( fn=export_from_dropdown, inputs=[export_character_dropdown, export_format], outputs=[export_status] ) # Tab 6: Campaign Management with gr.Tab("Campaign Management"): gr.Markdown(""" ### 🎲 Campaign Management Create and manage your D&D campaigns, track sessions, and record events! """) with gr.Tabs(): # Sub-tab: Synthesize Campaign with gr.Tab("🤖 Synthesize Campaign"): gr.Markdown(""" ### Campaign Synthesis Select characters and automatically create a custom campaign tailored to your party! """) # Load character button load_characters_btn = gr.Button("🔄 Load Available Characters", variant="secondary") with gr.Row(): with gr.Column(): synth_character_select = gr.CheckboxGroup( choices=[], label="Select Characters for Campaign", info="Choose characters to include in your campaign" ) synth_gm_name = gr.Textbox( label="Game Master Name", placeholder="Your name", info="DM/GM running the campaign" ) synth_notes = gr.Textbox( label="Additional Notes (Optional)", placeholder="Any specific themes, settings, or elements you'd like included...", lines=4, info="Guide the campaign creation process" ) synthesize_btn = gr.Button("✨ Synthesize Campaign", variant="primary", size="lg") with gr.Column(): gr.Markdown(""" ### How It Works 1. **Load Characters**: Click "Load Available Characters" to see all your characters 2. **Select Party**: Check the boxes for characters you want in the campaign 3. **Add Context**: Optionally provide DM notes about themes or preferences 4. **Analyze Party**: The system analyzes: - Character backstories and motivations - Party composition and level - Alignments and backgrounds - Character personalities 5. **Campaign Created**: Get a fully-fledged campaign that: - Ties into character backstories - Provides appropriate challenges - Creates opportunities for each character - Includes factions, villains, and mysteries **Example Party:** - Thorin Ironforge (Dwarf Fighter, Level 3) - Elara Moonwhisper (Elf Wizard, Level 3) - Grimm Shadowstep (Halfling Rogue, Level 3) The system will create a campaign that weaves their stories together! """) synth_status = gr.Textbox(label="Campaign Details", lines=20) # Load characters button handler def update_character_choices(): choices = self.get_character_choices_ui() if not choices: return gr.update(choices=[], value=[]) return gr.update(choices=choices, value=[]) load_characters_btn.click( fn=update_character_choices, inputs=[], outputs=[synth_character_select] ) synthesize_btn.click( fn=self.synthesize_campaign_ui, inputs=[synth_character_select, synth_gm_name, synth_notes], outputs=[synth_status] ) # Sub-tab: Create Campaign with gr.Tab("Create Campaign"): gr.Markdown("### Create New Campaign") with gr.Row(): with gr.Column(): campaign_name = gr.Textbox( label="Campaign Name", placeholder="The Shattered Crown", info="Name of your campaign" ) campaign_theme = gr.Dropdown( choices=[theme.value for theme in CampaignTheme], label="Campaign Theme", value="High Fantasy", info="Select the theme of your campaign" ) campaign_gm = gr.Textbox( label="Game Master Name", placeholder="Your name", info="DM/GM running the campaign" ) campaign_party_size = gr.Slider( minimum=1, maximum=10, value=4, step=1, label="Party Size", info="Expected number of players" ) campaign_level_range = gr.Textbox( label="Level Range", value="1-5", placeholder="1-5", info="Expected level range for the campaign" ) with gr.Column(): campaign_world = gr.Textbox( label="World Name", placeholder="Forgotten Realms", info="Name of your world/realm" ) campaign_starting = gr.Textbox( label="Starting Location", placeholder="Phandalin", info="Where the adventure begins" ) campaign_setting = gr.Textbox( label="Setting Description", placeholder="A war-torn kingdom...", lines=3, info="Describe the campaign setting" ) campaign_summary = gr.Textbox( label="Campaign Summary", placeholder="The adventurers must...", lines=3, info="Brief campaign hook/summary" ) campaign_conflict = gr.Textbox( label="Main Conflict", placeholder="A succession crisis threatens the kingdom", lines=2, info="Central conflict/tension" ) create_campaign_btn = gr.Button("⚔️ Create Campaign", variant="primary", size="lg") campaign_create_status = gr.Textbox(label="Status", lines=8) create_campaign_btn.click( fn=self.create_campaign_ui, inputs=[ campaign_name, campaign_theme, campaign_setting, campaign_summary, campaign_conflict, campaign_gm, campaign_world, campaign_starting, campaign_level_range, campaign_party_size ], outputs=[campaign_create_status] ) # Sub-tab: Manage Campaign with gr.Tab("Manage Campaign"): gr.Markdown("### Manage Campaign") manage_campaign_refresh_btn = gr.Button("🔄 Refresh Campaign List", variant="secondary") manage_campaign_dropdown = gr.Dropdown( choices=[], label="Select Campaign", info="Choose a campaign to view (type to search)", allow_custom_value=False, interactive=True ) with gr.Row(): load_campaign_btn = gr.Button("📂 Load Campaign", variant="primary") list_campaigns_btn = gr.Button("📋 List All Campaigns") gr.Markdown("---") with gr.Row(): campaign_details = gr.Markdown(label="Campaign Details") campaign_status = gr.Textbox(label="Status", lines=6) # Refresh campaign dropdown def refresh_manage_campaign_dropdown(): choices = self.get_campaign_dropdown_choices() return gr.update(choices=choices, value=None) manage_campaign_refresh_btn.click( fn=refresh_manage_campaign_dropdown, inputs=[], outputs=[manage_campaign_dropdown] ) # Load campaign - convert dropdown label to ID def load_campaign_from_dropdown(label): campaign_id = self.get_campaign_id_from_label(label) return self.load_campaign_ui(campaign_id) load_campaign_btn.click( fn=load_campaign_from_dropdown, inputs=[manage_campaign_dropdown], outputs=[campaign_details, campaign_status] ) list_campaigns_btn.click( fn=self.list_campaigns_ui, inputs=[], outputs=[campaign_details, campaign_status] ) # Sub-tab: Add Characters with gr.Tab("Add Characters"): gr.Markdown("### Add Characters to Campaign") add_char_refresh_btn = gr.Button("🔄 Refresh Lists", variant="secondary") with gr.Row(): add_char_campaign_dropdown = gr.Dropdown( choices=[], label="Select Campaign", info="Choose the campaign to add characters to (type to search)", allow_custom_value=False, interactive=True ) add_char_character_dropdown = gr.Dropdown( choices=[], label="Select Character", info="Choose the character to add (type to search)", allow_custom_value=False, interactive=True ) add_char_btn = gr.Button("➕ Add Character to Campaign", variant="primary") add_char_status = gr.Textbox(label="Status", lines=4) # Refresh both dropdowns def refresh_add_char_dropdowns(): campaign_choices = self.get_campaign_dropdown_choices() character_choices = self.get_character_dropdown_choices() return ( gr.update(choices=campaign_choices, value=None), gr.update(choices=character_choices, value=None) ) add_char_refresh_btn.click( fn=refresh_add_char_dropdowns, inputs=[], outputs=[add_char_campaign_dropdown, add_char_character_dropdown] ) # Add character - convert dropdown labels to IDs def add_char_from_dropdowns(campaign_label, character_label): campaign_id = self.get_campaign_id_from_label(campaign_label) character_id = self.get_character_id_from_label(character_label) return self.add_character_to_campaign_ui(campaign_id, character_id) add_char_btn.click( fn=add_char_from_dropdowns, inputs=[add_char_campaign_dropdown, add_char_character_dropdown], outputs=[add_char_status] ) gr.Markdown(""" **Tip:** Click "Refresh Lists" to load your campaigns and characters. """) # Sub-tab: Session Tracking with gr.Tab("Session Tracking"): gr.Markdown("### Track Campaign Sessions") session_refresh_btn = gr.Button("🔄 Refresh Campaign List", variant="secondary") session_campaign_dropdown = gr.Dropdown( choices=[], label="Select Campaign", info="Choose the campaign for session tracking (type to search)", allow_custom_value=False, interactive=True ) start_session_btn = gr.Button("🎬 Start New Session", variant="primary") session_status = gr.Textbox(label="Status", lines=4) # Refresh session campaign dropdown def refresh_session_dropdown(): choices = self.get_campaign_dropdown_choices() return gr.update(choices=choices, value=None) session_refresh_btn.click( fn=refresh_session_dropdown, inputs=[], outputs=[session_campaign_dropdown] ) # Start session - convert dropdown label to ID def start_session_from_dropdown(label): campaign_id = self.get_campaign_id_from_label(label) return self.start_session_ui(campaign_id) start_session_btn.click( fn=start_session_from_dropdown, inputs=[session_campaign_dropdown], outputs=[session_status] ) gr.Markdown("---") gr.Markdown("### 🤖 Auto-Generate Next Session") gr.Markdown(""" **Autonomous Feature:** AI analyzes your campaign and automatically generates a complete session plan. This includes: - Opening scene narration - Key encounters (combat, social, exploration) - NPCs featured in the session - Locations to visit - Plot developments - Potential outcomes and rewards """) auto_session_refresh_btn = gr.Button("🔄 Refresh Campaign List", variant="secondary") auto_session_campaign_dropdown = gr.Dropdown( choices=[], label="Select Campaign", info="Choose campaign to generate next session for", allow_custom_value=False, interactive=True ) auto_generate_session_btn = gr.Button("✨ Auto-Generate Next Session", variant="primary") auto_session_output = gr.Textbox(label="Generated Session Plan", lines=20) # Refresh auto-session campaign dropdown def refresh_auto_session_dropdown(): choices = self.get_campaign_dropdown_choices() return gr.update(choices=choices, value=None) auto_session_refresh_btn.click( fn=refresh_auto_session_dropdown, inputs=[], outputs=[auto_session_campaign_dropdown] ) # Auto-generate session def auto_generate_session_from_dropdown(label): campaign_id = self.get_campaign_id_from_label(label) return self.auto_generate_session_ui(campaign_id) auto_generate_session_btn.click( fn=auto_generate_session_from_dropdown, inputs=[auto_session_campaign_dropdown], outputs=[auto_session_output] ) gr.Markdown("---") gr.Markdown("### Add Session Event") event_refresh_btn = gr.Button("🔄 Refresh Campaign List", variant="secondary") event_campaign_dropdown = gr.Dropdown( choices=[], label="Select Campaign", info="Choose the campaign to add event to (type to search)", allow_custom_value=False, interactive=True ) event_type = gr.Dropdown( choices=["Combat", "Social", "Exploration", "Discovery", "Plot Development", "Character Moment", "NPC Interaction", "Quest Update"], label="Event Type", value="Combat", info="Type of event" ) event_title = gr.Textbox( label="Event Title", placeholder="Battle at the Bridge", info="Short title for the event" ) event_description = gr.Textbox( label="Event Description", placeholder="The party encountered a group of bandits...", lines=4, info="Detailed description of what happened" ) event_importance = gr.Slider( minimum=1, maximum=5, value=3, step=1, label="Importance", info="How important is this event? (1-5 stars)" ) add_event_btn = gr.Button("📝 Add Event", variant="primary") event_status = gr.Textbox(label="Status", lines=6) # Refresh event campaign dropdown def refresh_event_dropdown(): choices = self.get_campaign_dropdown_choices() return gr.update(choices=choices, value=None) event_refresh_btn.click( fn=refresh_event_dropdown, inputs=[], outputs=[event_campaign_dropdown] ) # Add event - convert dropdown label to ID def add_event_from_dropdown(campaign_label, event_type_val, title, description, importance): campaign_id = self.get_campaign_id_from_label(campaign_label) return self.add_event_ui(campaign_id, event_type_val, title, description, importance) add_event_btn.click( fn=add_event_from_dropdown, inputs=[ event_campaign_dropdown, event_type, event_title, event_description, event_importance ], outputs=[event_status] ) # Tab 7: About with gr.Tab("About"): gr.Markdown(""" ## About D'n'D Campaign Manager **Version:** 2.0.0 **Built for:** Gradio + Anthropic MCP Hackathon ### Features - 🎲 Complete D&D 5e character creation - 📖 Automated name and backstory generation - 🎨 Character portrait generation (DALL-E 3 / HuggingFace SDXL) - 📊 Multiple ability score methods (Standard Array, Roll, Point Buy) - 💾 Database persistence - 📄 Markdown character sheet export - ✅ Full data validation ### Stat Methods - **Standard Array:** 15, 14, 13, 12, 10, 8 (balanced) - **Roll:** 4d6 drop lowest (random) - **Point Buy:** 27 points (customizable) ### Supported Races Human, Elf, Dwarf, Halfling, Dragonborn, Gnome, Half-Elf, Half-Orc, Tiefling ### Supported Classes Barbarian, Bard, Cleric, Druid, Fighter, Monk, Paladin, Ranger, Rogue, Sorcerer, Warlock, Wizard ### Tech Stack - **AI:** Anthropic Claude / Google Gemini / OpenAI DALL-E 3 - **Framework:** Gradio - **Database:** SQLite - **Validation:** Pydantic --- *Built with ❤️ for the TTRPG community* """) gr.Markdown(""" --- ### Tips - Enable "Use AI" options for more creative and detailed characters - Use Standard Array for balanced characters - Use Roll for random variation - Save your character ID to load it later - Check the character list to find previously created characters """) # Auto-populate dropdowns on interface load def populate_all_dropdowns(): """Populate all dropdowns when interface loads""" campaign_choices = self.get_campaign_dropdown_choices() character_choices = self.get_character_dropdown_choices() return [ gr.update(choices=character_choices), # character_dropdown gr.update(choices=character_choices), # delete_character_dropdown gr.update(choices=character_choices), # portrait_character_dropdown gr.update(choices=character_choices), # export_character_dropdown gr.update(choices=campaign_choices), # manage_campaign_dropdown gr.update(choices=campaign_choices), # add_char_campaign_dropdown gr.update(choices=character_choices), # add_char_character_dropdown gr.update(choices=campaign_choices), # session_campaign_dropdown gr.update(choices=campaign_choices), # auto_session_campaign_dropdown gr.update(choices=campaign_choices), # event_campaign_dropdown ] interface.load( fn=populate_all_dropdowns, inputs=[], outputs=[ character_dropdown, delete_character_dropdown, portrait_character_dropdown, export_character_dropdown, manage_campaign_dropdown, add_char_campaign_dropdown, add_char_character_dropdown, session_campaign_dropdown, auto_session_campaign_dropdown, event_campaign_dropdown, ] ) return interface def launch_ui(): """Launch the Gradio interface""" ui = CharacterCreatorUI() interface = ui.create_interface() interface.launch( server_name="0.0.0.0", server_port=7860, share=False, show_error=True ) if __name__ == "__main__": launch_ui()