DnD_Campaign_Manager / src /ui /tabs /session_tracking_tab.py
official.ghost.logic
Deploy D&D Campaign Manager v2
71b378e
"""
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