Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |