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