Spaces:
Sleeping
Sleeping
File size: 22,797 Bytes
71b378e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 |
"""
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
|