""" Session Tracking Tab for D'n'D Campaign Manager """ import gradio as gr import traceback from src.agents.campaign_agent import CampaignAgent from src.ui.components.dropdown_manager import DropdownManager class SessionTrackingTab: """Session Tracking tab for managing campaign sessions and events""" def __init__(self, campaign_agent: CampaignAgent, dropdown_manager: DropdownManager): self.campaign_agent = campaign_agent self.dropdown_manager = dropdown_manager 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 get_session_notes_status(self, campaign_id: str) -> str: """Get status of uploaded session notes for a campaign""" try: if not campaign_id.strip(): return "" campaign = self.campaign_agent.load_campaign(campaign_id) if not campaign: return "" # Get all session notes all_notes = self.campaign_agent.get_session_notes(campaign_id) if not all_notes: return """ πŸ“Š **Session Notes Status:** No notes uploaded yet πŸ’‘ **Tip:** Upload your session notes below to get better AI-generated sessions! AI will use your notes to create sessions that respond to what actually happened.""" # Build status message status = "πŸ“Š **Session Notes Available:**\n\n" for note in sorted(all_notes, key=lambda x: x.session_number): char_count = len(note.notes) status += f"βœ… Session {note.session_number} - {char_count} characters" if note.file_name: status += f" ({note.file_name})" status += "\n" status += f"\nπŸ’‘ AI will use these {len(all_notes)} session note(s) to generate contextual next sessions!" return status except Exception as e: return f"❌ Error checking notes: {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: return f"❌ Error generating session:\n\n{str(e)}\n\n{traceback.format_exc()}" def save_session_notes_ui( self, campaign_id: str, session_number: int, file_path: str, notes_text: str ) -> str: """Save session notes from file upload or text input""" try: if not campaign_id.strip(): return "❌ Please select a campaign" campaign = self.campaign_agent.load_campaign(campaign_id) if not campaign: return f"❌ Campaign not found: {campaign_id}" # Use file content if uploaded, otherwise use text area content = "" file_name = None file_type = None if file_path: try: from src.utils.file_parsers import parse_uploaded_file, get_file_info from pathlib import Path content = parse_uploaded_file(file_path) file_info = get_file_info(file_path) file_name = file_info['name'] file_type = file_info['extension'] except Exception as e: return f"❌ Error parsing file: {str(e)}" elif notes_text.strip(): content = notes_text else: return "❌ Please upload a file or paste notes" # Save to database try: self.campaign_agent.save_session_notes( campaign_id=campaign_id, session_number=int(session_number), notes=content, file_name=file_name, file_type=file_type ) # Get updated session notes count all_notes = self.campaign_agent.get_session_notes(campaign_id) notes_count = len(all_notes) # Build success message with context message = f"""βœ… **Session notes saved successfully!** **Campaign:** {campaign.name} **Session:** {session_number} **Content length:** {len(content)} characters **Source:** {'πŸ“Ž File upload' if file_path else '✍️ Direct paste'} {f'**File:** {file_name}' if file_name else ''} --- πŸ“Š **Your Campaign Now Has:** - {notes_count} session{'s' if notes_count != 1 else ''} with uploaded notes - Session {session_number} notes just added --- 🎯 **What You Can Do Next:** 1. **Generate Session {int(session_number) + 1}:** - Scroll up to **"Step 1: πŸ€– Auto-Generate Next Session"** - Select "{campaign.name}" - Click "✨ Auto-Generate Next Session" - AI will use your Session {session_number} notes to create contextual content! 2. **Upload More Sessions:** - Have notes from other sessions? Upload them too! - More notes = better AI-generated sessions 3. **Review Your Notes:** - Your notes are saved and will be used automatically - AI analyzes: player choices, NPCs, unresolved hooks, consequences --- πŸ’‘ **How It Works:** When you generate the next session (Step 1), the AI will: βœ… Read your uploaded notes (last 2-3 sessions) βœ… Build on what actually happened at your table βœ… Address unresolved plot hooks you mentioned βœ… Create encounters that respond to player decisions **Ready to generate Session {int(session_number) + 1}? Scroll up to Step 1!** ⬆️""" return message except Exception as e: return f"❌ Error saving notes: {str(e)}" except Exception as e: return f"❌ Error: {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 create(self) -> tuple: """Create and return the Session Tracking tab component""" with gr.Tab("Session Tracking"): gr.Markdown(""" # 🎲 Session Tracking & Planning **Workflow:** Auto-generate next session β†’ Play the session β†’ Upload notes afterward --- """) # SECTION 1: Auto-Generate Next Session gr.Markdown("## Step 1: πŸ€– Auto-Generate Next Session") gr.Markdown(""" **Before playing:** Let the AI create your session plan! **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 πŸ’‘ **Tip:** The AI uses your uploaded session notes from previous sessions to create contextual, story-driven content! """) 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 ) # Session notes status display session_notes_status = gr.Markdown( value="", label="Session Notes Status" ) 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 auto_session_refresh_btn.click( fn=self.dropdown_manager.refresh_campaign_dropdown, inputs=[], outputs=[auto_session_campaign_dropdown] ) # Update session notes status when campaign is selected def update_notes_status(campaign_label): campaign_id = self.dropdown_manager.get_campaign_id_from_label(campaign_label) return self.get_session_notes_status(campaign_id) auto_session_campaign_dropdown.change( fn=update_notes_status, inputs=[auto_session_campaign_dropdown], outputs=[session_notes_status] ) # Auto-generate session def auto_generate_session_from_dropdown(label): campaign_id = self.dropdown_manager.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("---") # SECTION 2: Start New Session gr.Markdown("## Step 2: 🎬 Start New Session") gr.Markdown(""" **Ready to play?** Start your session here! This will: - Increment the session counter - Track that a new session has begun - Prepare for event logging πŸ’‘ **When to use:** Right before you start playing with your group. """) 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 session_refresh_btn.click( fn=self.dropdown_manager.refresh_campaign_dropdown, inputs=[], outputs=[session_campaign_dropdown] ) # Start session - convert dropdown label to ID def start_session_from_dropdown(label): campaign_id = self.dropdown_manager.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("---") # SECTION 3: Upload Session Notes gr.Markdown("## Step 3: πŸ“ Upload Session Notes (After Playing)") gr.Markdown(""" **Just finished a session?** Upload your DM notes here! The AI will use your notes to generate better, more contextual next sessions. **Supported formats:** .txt, .md, .docx, .pdf **What to include:** - What actually happened in the session - Player choices and consequences - Improvised content that worked well - Unresolved plot hooks - NPC interactions and developments πŸ’‘ **Why this matters:** When you generate the next session (Step 1), the AI reads these notes to create content that responds to your actual gameplay! """) notes_refresh_btn = gr.Button("πŸ”„ Refresh Campaign List", variant="secondary") notes_campaign_dropdown = gr.Dropdown( choices=[], label="Select Campaign", info="Choose campaign to upload notes for" ) notes_session_number = gr.Number( label="Session Number", value=1, precision=0, info="Which session are these notes for?" ) notes_file_upload = gr.File( label="Upload Session Notes File (Optional)", file_types=['.txt', '.md', '.docx', '.pdf'], type='filepath' ) notes_text_area = gr.Textbox( label="Or Paste Notes Directly", lines=15, placeholder="""Session 3 - The Lost Temple The party arrived at the temple ruins after a week of travel... Key moments: - Grimm discovered a hidden passage behind the altar - Elara deciphered ancient runes warning of a curse - Combat with temple guardians (party took heavy damage, used most healing) - Found the Crystal of Shadows but Thorin decided not to take it - NPC Velorin revealed he's been following them - claims to protect the crystal Unresolved: - Why was Velorin really following them? - What happens if they don't take the crystal? Will someone else? - The guardian mentioned "the ritual" before dying - what ritual? - Party suspects Velorin isn't telling the whole truth Next session setup: - Party needs to rest and heal - Velorin wants to talk - Strange shadows gathering outside temple""", info="Freeform notes about what happened in the session" ) save_notes_btn = gr.Button("πŸ’Ύ Save Session Notes", variant="primary") notes_status = gr.Textbox(label="Status", lines=6) # Refresh notes campaign dropdown notes_refresh_btn.click( fn=self.dropdown_manager.refresh_campaign_dropdown, inputs=[], outputs=[notes_campaign_dropdown] ) # Save notes event handler def save_notes_from_dropdown(campaign_label, session_num, file_path, notes_text): campaign_id = self.dropdown_manager.get_campaign_id_from_label(campaign_label) return self.save_session_notes_ui(campaign_id, session_num, file_path, notes_text) save_notes_btn.click( fn=save_notes_from_dropdown, inputs=[notes_campaign_dropdown, notes_session_number, notes_file_upload, notes_text_area], outputs=[notes_status] ) gr.Markdown("---") # SECTION 4: Add Session Event (Manual Tracking) gr.Markdown("## Step 4: πŸ“‹ Add Session Event (Optional)") gr.Markdown(""" **Want to manually track specific events?** Add them here! This is optional - use it if you want to log specific moments during or after your session. πŸ’‘ **Note:** This is separate from session notes. Use this for quick event logging, and use session notes (Step 3) for comprehensive session summaries. """) 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 event_refresh_btn.click( fn=self.dropdown_manager.refresh_campaign_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.dropdown_manager.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] ) # Return dropdowns for auto-population return session_campaign_dropdown, auto_session_campaign_dropdown, notes_campaign_dropdown, event_campaign_dropdown