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