File size: 36,688 Bytes
230b437
 
 
 
5e977a5
230b437
 
50d25e2
 
 
 
 
230b437
 
5e977a5
 
 
 
 
 
230b437
9bbaece
5e977a5
230b437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
 
 
230b437
 
50d25e2
230b437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
5e977a5
50d25e2
5e977a5
 
 
 
 
 
 
 
 
 
50d25e2
230b437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
5e977a5
50d25e2
5e977a5
50d25e2
5e977a5
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
5e977a5
 
50d25e2
5e977a5
 
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
 
 
230b437
50d25e2
 
 
 
 
 
 
fa4b92a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
 
 
230b437
 
 
50d25e2
 
230b437
50d25e2
230b437
50d25e2
 
 
230b437
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230b437
 
 
50d25e2
230b437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
 
230b437
 
 
 
50d25e2
 
230b437
 
 
 
 
50d25e2
230b437
 
 
 
 
 
5e977a5
 
 
230b437
50d25e2
230b437
5e977a5
 
50d25e2
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
17b2713
50d25e2
230b437
9bbaece
230b437
5e977a5
230b437
 
9bbaece
50d25e2
 
 
 
 
 
 
230b437
50d25e2
230b437
 
 
 
 
 
 
 
 
5e977a5
230b437
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
 
230b437
 
 
 
 
50d25e2
230b437
 
 
 
 
 
 
 
 
 
 
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
 
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
230b437
 
50d25e2
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
 
230b437
50d25e2
 
 
 
 
 
 
230b437
 
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230b437
 
 
50d25e2
 
 
 
 
 
 
 
 
 
 
 
fa4b92a
 
 
 
50d25e2
 
 
fa4b92a
50d25e2
 
 
 
 
 
 
230b437
 
50d25e2
230b437
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50d25e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230b437
 
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
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
import os
import streamlit as st
import requests
import json
import re
from docx import Document
from io import BytesIO
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import networkx as nx
from collections import Counter

# --- Set Streamlit environment variables for HuggingFace compatibility ---
# These environment variables are an attempt to prevent permission errors
# by telling Streamlit to use a writable directory for its internal files.
# For full compatibility, you may also need a .streamlit/config.toml file
# in your repository with the content:
# [global]
# dataSavePath = "/tmp"
os.environ["STREAMLIT_BROWSER_GATHER_USAGE_STATS"] = "false"
os.environ["STREAMLIT_SERVER_ENABLE_ARROW_IPC"] = "false"
os.environ["STREAMLIT_SERVER_FOLDER"] = "/tmp"

# --- Page Configuration ---
st.set_page_config(
    page_title="Music Lesson Planner",
    page_icon="🎶",
    layout="wide",
    initial_sidebar_state="expanded"
)

# --- Constants and API Setup ---
# IMPORTANT: Set your Google API Key as an environment variable named GOOGLE_API_KEY
# You can get one from Google AI Studio: https://aistudio.google.com/app/apikey
GEMINI_API_KEY = os.getenv('GOOGLE_API_KEY')

# Base URL for Gemini API
GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/"

# Available Gemini Models for comparison
GEMINI_MODELS = {
    "Gemini 2.5 Flash": "gemini-2.5-flash",
    "Gemini 2.5 Pro": "gemini-2.5-pro",
    "Gemini 2.5 Flash Lite": "gemini-2.5-flash-lite",
}


# --- Helper Function for LLM API Call ---
def call_gemini_api(model_name, prompt_text, response_schema=None):
    """
    Calls the Gemini API with the given model and prompt.
    Handles JSON parsing and error reporting.
    """
    if not GEMINI_API_KEY:
        st.error(
            "Gemini API Key is not set. Please set the GOOGLE_API_KEY environment variable or replace `GEMINI_API_KEY = os.getenv('GOOGLE_API_KEY')` with your actual API key.")
        return None

    model_id = GEMINI_MODELS.get(model_name)
    if not model_id:
        st.error(f"Unknown model: {model_name}")
        return None

    url = f"{GEMINI_API_BASE_URL}{model_id}:generateContent?key={GEMINI_API_KEY}"

    headers = {
        "Content-Type": "application/json",
    }

    payload = {
        "contents": [
            {
                "role": "user",
                "parts": [{"text": prompt_text}]
            }
        ],
        "generationConfig": {}  # Initialize generationConfig
    }

    # If a response_schema is provided, configure for structured output
    if response_schema:
        payload["generationConfig"]["responseMimeType"] = "application/json"
        payload["generationConfig"]["responseSchema"] = response_schema

    try:
        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()  # Raise an exception for HTTP errors (4xx or 5xx)
        response_data = response.json()

        if response_data and response_data.get("candidates"):
            # Access the text part of the response
            if response_schema:
                # For structured responses, the content is directly the JSON string
                raw_json_text = response_data["candidates"][0]["content"]["parts"][0]["text"]
                # Use regex to robustly extract the JSON object or array, ignoring any
                # surrounding text or malformed characters.
                json_match = re.search(r'\[.*\]|\{.*\}', raw_json_text, re.DOTALL)
                if json_match:
                    json_string = json_match.group(0)
                    try:
                        parsed_json = json.loads(json_string)
                        return parsed_json
                    except json.JSONDecodeError as e:
                        st.error(f"Failed to parse JSON for {model_name}. Error: {e}")
                        st.text_area(f"Raw output from {model_name}:", raw_json_text, height=200)
                        return None
                else:
                    st.error(f"Could not find a valid JSON object or array in the response from {model_name}.")
                    st.text_area(f"Raw output from {model_name}:", raw_json_text, height=200)
                    return None
            else:
                # For unstructured responses, return the text directly
                return response_data["candidates"][0]["content"]["parts"][0]["text"]
        else:
            st.error(f"No valid response candidates found for {model_name}.")
            st.json(response_data)  # Display the full response for debugging
            return None

    except requests.exceptions.HTTPError as http_err:
        st.error(f"HTTP error occurred for {model_name}: {http_err}")
        st.error(f"Response content: {response.text}")
        return None
    except requests.exceptions.ConnectionError as conn_err:
        st.error(f"Connection error occurred for {model_name}: {conn_err}")
        return None
    except requests.exceptions.Timeout as timeout_err:
        st.error(f"Timeout error occurred for {model_name}: {timeout_err}")
        return None
    except requests.exceptions.RequestException as req_err:
        st.error(f"An unexpected error occurred for {model_name}: {req_err}")
        return None
    except Exception as e:
        st.error(f"An unexpected error occurred during API call for {model_name}: {e}")
        return None


# --- Custom Markdown Formatting for Outline ---
def format_outline_for_display(outline_data, lesson_number):
    """
    Formats the outline JSON data for a single lesson into a human-readable markdown string.
    """
    if not outline_data:
        return "No outline data available."

    markdown_string = ""
    # Add Intro
    markdown_string += f"**Intro:**\n{outline_data.get('intro', 'N/A')}\n\n"

    # Add Key Teaching Points & Exercises
    markdown_string += "**Key Teaching Points & Exercise Suggestions:**\n"
    for point in outline_data.get('keyTeachingPoints', []):
        markdown_string += f"- **{point.get('point', 'N/A')}**\n"
        for exercise in point.get('exercises', []):
            markdown_string += f"  - {exercise}\n"

    # Add Outro
    markdown_string += f"\n**Outro:**\n{outline_data.get('outro', 'N/A')}"

    return markdown_string


# --- Visualization Functions ---
def analyze_lesson_complexity(lessons_data):
    """
    Analyzes lesson complexity based on key teaching points count and content length.
    Returns complexity scores for the complexity timeline.
    """
    complexity_scores = []
    for i, lesson in enumerate(lessons_data):
        # Base complexity on number of teaching points and content depth
        num_points = len(lesson.get('keyTeachingPoints', []))
        total_exercises = sum(len(point.get('exercises', [])) for point in lesson.get('keyTeachingPoints', []))

        # Simple scoring: more teaching points + more exercises = higher complexity
        complexity_score = num_points * 2 + total_exercises
        complexity_scores.append(complexity_score)

    return complexity_scores

def create_complexity_timeline(lessons_data):
    """Creates a complexity timeline visualization."""
    complexity_scores = analyze_lesson_complexity(lessons_data)

    # Create the timeline chart
    fig = go.Figure()

    # Add the complexity line
    fig.add_trace(go.Scatter(
        x=list(range(1, 6)),
        y=complexity_scores,
        mode='lines+markers',
        line=dict(color='#0277bd', width=3),
        marker=dict(size=12, color='#0277bd'),
        name='Complexity Score'
    ))

    # Add annotations for lesson types
    annotations = []
    for i, score in enumerate(complexity_scores):
        lesson_type = "Building" if i < 3 else "Reinforcing"
        annotations.append(dict(
            x=i+1, y=score,
            text=f"L{i+1}<br>{lesson_type}",
            showarrow=True,
            arrowhead=2,
            arrowsize=1,
            arrowwidth=2,
            arrowcolor='#666',
            bgcolor='white',
            bordercolor='#666',
            borderwidth=1
        ))

    fig.update_layout(
        title="Lesson Complexity Timeline",
        xaxis_title="Lesson Number",
        yaxis_title="Complexity Score",
        xaxis=dict(tickmode='linear', tick0=1, dtick=1),
        height=400,
        annotations=annotations
    )

    return fig

def extract_skills_from_lesson(lesson):
    """Extract key skills from a lesson's teaching points."""
    skills = []
    for point in lesson.get('keyTeachingPoints', []):
        skill = point.get('point', '').strip()
        if skill:
            skills.append(skill)
    return skills

def create_skill_flow_diagram(lessons_data):
    """Creates a skill building flow diagram using networkx and plotly."""
    # Create a directed graph
    G = nx.DiGraph()

    # Add nodes for each lesson and extract skills
    lesson_skills = {}
    all_skills = []

    for i, lesson in enumerate(lessons_data):
        lesson_name = f"Lesson {i+1}"
        skills = extract_skills_from_lesson(lesson)
        lesson_skills[lesson_name] = skills
        all_skills.extend(skills)

    # Add lesson nodes
    for i in range(5):
        lesson_name = f"Lesson {i+1}"
        G.add_node(lesson_name, node_type='lesson', lesson_num=i+1)

    # Add skill dependencies (lessons flow into next lesson)
    for i in range(4):
        G.add_edge(f"Lesson {i+1}", f"Lesson {i+2}")

    # Create layout
    pos = nx.spring_layout(G, k=3, iterations=50)

    # Extract node and edge information for plotly
    edge_x, edge_y = [], []
    for edge in G.edges():
        x0, y0 = pos[edge[0]]
        x1, y1 = pos[edge[1]]
        edge_x.extend([x0, x1, None])
        edge_y.extend([y0, y1, None])

    node_x = [pos[node][0] for node in G.nodes()]
    node_y = [pos[node][1] for node in G.nodes()]
    node_text = [f"{node}<br>Skills: {len(lesson_skills[node])}" for node in G.nodes()]

    # Create the figure
    fig = go.Figure()

    # Add edges
    fig.add_trace(go.Scatter(
        x=edge_x, y=edge_y,
        line=dict(width=2, color='#666'),
        hoverinfo='none',
        mode='lines',
        showlegend=False
    ))

    # Add nodes
    fig.add_trace(go.Scatter(
        x=node_x, y=node_y,
        mode='markers+text',
        marker=dict(size=[20 + len(lesson_skills[node])*3 for node in G.nodes()],
                   color=['#0277bd', '#0288d1', '#039be5', '#03a9f4', '#29b6f6'],
                   line=dict(width=2, color='white')),
        text=[node.replace(' ', '<br>') for node in G.nodes()],
        textposition='middle center',
        textfont=dict(size=10, color='white'),
        hovertext=node_text,
        hoverinfo='text',
        showlegend=False
    ))

    fig.update_layout(
        title="Skill Building Flow",
        showlegend=False,
        xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        height=400,
        margin=dict(l=20, r=20, t=40, b=20)
    )

    return fig

def create_topic_coverage_heatmap(lessons_data):
    """Creates a heatmap showing topic coverage across lessons."""
    # Extract all unique topics/concepts from teaching points
    all_topics = []
    lesson_topics = {}

    # Common music theory categories to look for
    topic_categories = {
        'scales': ['scale', 'major', 'minor', 'chromatic'],
        'rhythm': ['beat', 'tempo', 'rhythm', 'timing', 'bpm'],
        'technique': ['finger', 'hand', 'practice', 'exercise'],
        'theory': ['chord', 'interval', 'sharp', 'flat', 'key'],
        'performance': ['play', 'sound', 'listen', 'hear']
    }

    # Analyze each lesson
    coverage_matrix = []
    for i, lesson in enumerate(lessons_data):
        lesson_coverage = {}
        lesson_text = json.dumps(lesson).lower()

        for category, keywords in topic_categories.items():
            coverage_score = sum(lesson_text.count(keyword) for keyword in keywords)
            lesson_coverage[category] = coverage_score

        coverage_matrix.append(lesson_coverage)

    # Create DataFrame
    df = pd.DataFrame(coverage_matrix, index=[f'Lesson {i+1}' for i in range(5)])

    # Create heatmap
    fig = px.imshow(
        df.T,
        labels=dict(x="Lesson", y="Topic Category", color="Coverage Intensity"),
        title="Topic Coverage Heatmap",
        color_continuous_scale="Blues",
        aspect="auto"
    )

    fig.update_layout(height=400)

    return fig


# --- DOCX Conversion Function ---
def strip_html_tags(text):
    """
    Remove HTML tags from text for DOCX export.
    """
    if not text:
        return text
    # Remove span tags with class attributes
    text = re.sub(r'<span class=["\'][^"\']*["\']>', '', text)
    text = re.sub(r'</span>', '', text)
    return text

def sanitize_filename(text, max_length=100):
    """
    Sanitizes text to create a safe filename for both filesystems and HTTP headers.

    Args:
        text: The text to sanitize
        max_length: Maximum length for the sanitized text (default 100)

    Returns:
        A sanitized string safe for use in filenames and HTTP Content-Disposition headers
    """
    if not text:
        return "lesson"

    # Replace newlines, tabs, and other whitespace with single space
    text = re.sub(r'[\n\r\t\v\f]+', ' ', text)

    # Replace multiple spaces with single space
    text = re.sub(r'\s+', ' ', text)

    # Remove or replace unsafe characters for filenames and HTTP headers
    # Keep only alphanumeric, spaces, hyphens, and underscores
    text = re.sub(r'[^\w\s\-]', '', text)

    # Replace spaces with underscores
    text = text.replace(' ', '_')

    # Remove leading/trailing underscores
    text = text.strip('_')

    # Truncate to max_length while avoiding cutting mid-word
    if len(text) > max_length:
        text = text[:max_length].rsplit('_', 1)[0]

    # Ensure we have at least some text
    if not text:
        return "lesson"

    return text

def create_docx_file(lessons_data, lesson_topic, lesson_length, model_name):
    """
    Creates a DOCX file from a sequence of lessons.
    """
    document = Document()

    document.add_heading(f"Lesson Plan Sequence: {lesson_topic}", level=1)
    document.add_paragraph(f"Length per lesson: {lesson_length}")
    document.add_paragraph(f"Generated by: {model_name}")
    document.add_paragraph("\n")

    for i in range(5):
        outline_data = lessons_data['outlines'][i]
        draft_text = lessons_data['drafts'][i]

        document.add_heading(f"Lesson {i + 1}", level=2)
        document.add_paragraph("\n")

        # Add Outline Sections
        document.add_heading("Outline", level=3)
        document.add_paragraph(f"**Intro:**\n{strip_html_tags(outline_data.get('intro', 'N/A'))}")

        document.add_heading("Key Teaching Points & Exercise Suggestions", level=4)
        for point in outline_data.get('keyTeachingPoints', []):
            document.add_paragraph(f"- {strip_html_tags(point.get('point', 'N/A'))}", style='List Bullet')
            for exercise in point.get('exercises', []):
                document.add_paragraph(strip_html_tags(exercise), style='List Bullet')

        document.add_heading("Outro", level=4)
        document.add_paragraph(strip_html_tags(outline_data.get('outro', 'N/A')))

        # Add Full Draft Content (strip HTML tags)
        document.add_heading("Full Lesson Draft", level=2)
        document.add_paragraph(strip_html_tags(draft_text))
        document.add_paragraph("\n")

    byte_io = BytesIO()
    document.save(byte_io)
    byte_io.seek(0)
    return byte_io.getvalue()


# --- Password Protection ---
def authenticate_user():
    st.markdown("## 🔐 Secure Login")
    password = st.text_input("Password", type="password", key="password_input")
    submit = st.button("Login", key="login_button")

    if submit:
        correct_password = os.getenv("APP_PASSWORD")

        if password == correct_password:
            st.session_state["authenticated"] = True
            st.rerun()  # Rerun to clear password input and show app content
        else:
            st.error("Invalid password")


# Check authentication status
if "authenticated" not in st.session_state:
    st.session_state["authenticated"] = False

if not st.session_state["authenticated"]:
    authenticate_user()
    st.stop()  # Stop execution if not authenticated
else:
    # --- Main Application Logic (Protected by Authentication) ---
    st.title("🎶 Music Lesson Planner")

    st.markdown("""
    This app helps you draft outlines and detailed lesson plans for online music lessons using different Gemini models.
    Compare outputs to find the best fit for your pedagogical needs!
    """)

    # Initialize session state for outlines and drafts if not already present
    if 'lessons_data' not in st.session_state:
        st.session_state.lessons_data = {}

    # Input fields for lesson details
    with st.sidebar:
        st.header("Lesson Details")
        lesson_topic = st.text_area("Lesson Topic", "Introduction to Solfege", height=100)
        lesson_length = st.selectbox("Lesson Length", ["5-minute", "7-minute", "10-minute"], index=0)

        st.header("Model Selection")
        selected_models = st.multiselect(
            "Select Gemini Models",
            list(GEMINI_MODELS.keys()),
            default=["Gemini 2.5 Pro", "Gemini 2.5 Flash"]
        )

        st.header("Prompt Customization")
        default_outline_system_prompt = (
            "You are an AI assistant specialized in creating concise and structured outlines for online music lessons. "
            "Your goal is to provide a clear, pedagogical framework that music educators can easily follow. "
            "Focus on three main sections: an introduction, a list of key teaching points with suggested exercises, and a conclusion (outro)."
            "The lessons are online and asynchronous, so ensure the content is suitable for self-paced learning."
            "DO NOT include any quizzes, assessments, images, or audio/video."
        )
        outline_system_prompt = st.text_area("Outline System Prompt", default_outline_system_prompt, height=150)

        # Static user prompt template for the outline
        outline_user_prompt_template = (
            "Create a sequence of five online music lessons on the topic of '{lesson_topic}'. "
            "Each lesson in the sequence should be a {lesson_length} lesson. "
            "The sequence should build in complexity from lessons 1 through 3, with lessons 4 and 5 focusing on reinforcement and review. "
            "The entire sequence is a skill pack: a structured sequence of 5 short videos, designed to teach a specific skill through guided, real-time repetition."
            "Ideally, a student will view one video from the skill pack per day. This gives them some time in between videos, rather than bingeing all 5 at once."
            "This is a play-to-learn format, with the student playing along with the instructor for the entire session. This requires the instructor to coach the viewer along while simultaneously playing through the examples."
            "All practice is done in real time, ensuring learners can build muscle memory while they play—no extra practice required. As a result, the increase in difficulty from video to video should be very minimal."

            "The entire response must be a JSON array containing five lesson objects. "
            "Each lesson object must contain the following three keys: 'intro' (a single paragraph string), 'keyTeachingPoints' (a list of objects, each with 'point' and 'exercises'), and 'outro' (a single paragraph string)."
            "Please provide the response strictly in JSON format according to the schema provided, with no additional text or markdown outside the JSON array."
        )

        # Sample script to provide tone reference for draft generation (not displayed to the user)
        sample_script_text = """Hey everyone, [instructor name] here, community manager for Piano and welcome to Unit 2 of the Piano Curriculum. Hope you've had plenty of time to practice and integrate the concepts and exercises and scales that we learned in that first unit. Got those chords and different majors, scale sounds under your hands. First unit, we're going to dive a little further into learning some new keys, some new ways to shape chords. And to help me do that, I have my friend [instructor name] here. Hello, how are you doing? I'm doing well. Great. It's good to be here. Tell us a little bit about yourself, how long you've been playing piano, how long you've been teaching for. I've been playing piano since I was about nine. I begged and begged for piano lessons and in retrospect I might have gotten myself into a bit of a disaster. But I'm very thankful that my parents have helped me to stick to it. So I've been playing from about nine and I've been teaching for about ten years. Nice. Yeah. So a lot of experience as a teacher. Lisa is going to teach us a little bit about the G major scale today, which is a whole new key. Lisa, tell us a little bit about G major. Okay. G major is, it's a really cool scale because it introduces us to our first sharp. So you're probably wondering what is a sharp. You've seen them before. They look like a hashtag. They are the original hashtag. And what a sharp does is it raises the tone by a half step. So what I mean by that is if we were to start on C, a sharp would mean that we're going to play the C sharp here. So it simply raises a note by a half step or a half tone. In our scale today, we're going to need to play an F sharp. So I'm going to show you where that is. So C, D, E, F. To play an F sharp, we go here. Let's F sharp. So the reason why it's important to know what a sharp is, is it because it allows us to follow the major scale tone pattern that we need in order to make the major scale sound like a major scale. So if we were to play a G scale starting on G and just play all the white keys, it would sound like this. Which doesn't, it doesn't sound quite right. So let's take a look at that step by step and we'll see how following that formula for a major scale introduces the sharp. So if we go from G to A, that's the whole step. From A to B, whole step. B to C, there's our half step. C to D, whole step. D to E, whole step. Now we need another whole step here. So in order to make that happen, we have to move to our F sharp. And then the scale ends nicely on the half step. So that's your G scale. I'm going to play it once more for you so you can hear it all together. And so we go back down the same way we came. And the coordination between our fourth finger and our third finger can be a little bit tricky at first. So you have to be careful that your four lands on the F sharp and your three is going to hit the E and we're going to come all the way down like this. And there's your G scale. Awesome. So in order to play anything in the key of G, we have to have that F sharp. Yes. So if you're playing in the key of G and an F comes around, it's going to be a sharp unless otherwise stated. Right. And how does that scale look in the left hand? Looks just like this. We're going to start with our five finger on G and we're going to go five, four, three, two, one. Fly over with our three. Remember our F sharp and then we've got our G. And we go down the same way we went up. Awesome. So it has the same fingering pattern that you guys learned playing the C major scale. Yes. Keep an eye out for that F sharp, the seventh note in the scale. So how do you recommend practicing the G major scale? So I recommend practicing the G major scale first with your right hand. It's up to you. You can start right hand or left hand. I'm definitely more dominant in my right. So it's easier. You want to make it easier on yourself when you start. So I'd suggest that you play the scale a bunch of times with your right hand, maybe five, bunch of times with your left hand and then we're going to try hands together. So hands together can be tricky at first. Don't get frustrated. It takes practice. You will get it. Go very slowly. And I always suggest letting yourself put weight in the keys. So if you're really struggling, slow down and be intentional. Pretend like your hands are heavy. And that's going to help your mind connect to your hand muscles so that you can develop the muscle memory you need for the scale. And that's how it sounds hands together. Awesome. So the way that I recommend practicing the G major scale, play it at a tempo, at a nice slow tempo that's comfortable for you. Try about 60 BPM. It'll sound really slow, but it's really good for building that muscle memory and just the basic theory knowledge. Go up and down the scale one octave, five times. If you can do that without making any mistakes, if you can keep yourself a solid sense of rhythm as you're playing, you'll be ready to move on to the next video. Lisa, can you demonstrate that, this scale, with a quick? Yes, absolutely. Okay. Perfect. So practice that G major scale up and down five times at a slow tempo. When you can do that without any mistakes and keep a stable rhythm, you'll be ready to move on to the next video. If you have any questions or need any clarification, let me know with an email to Jordan at Piano.com. I'll be happy to help you out and we'll see you at the next video."""

        draft_system_prompt_base = (
            "You are an AI assistant specialized in expanding structured lesson outlines into detailed, engaging rough drafts for online music lessons. "
            "Your goal is to provide specific examples and pedagogical details for each teaching point and exercise. "
            "The language should be engaging and professional, tailored for music educators."
            "The lessons are online and asynchronous, so ensure the draft is suitable for self-paced learning."
            "DO NOT include any quizzes, assessments, images, or audio/video."
            "\n\n"
            "IMPORTANT: Apply automatic color coding by wrapping relevant terms in HTML spans with specific CSS classes:\n"
            "- Music theory terms (scales, intervals, chords, keys, notes, etc.): <span class='music-theory'>term</span>\n"
            "- Tempo & timing terms (BPM, rhythm, beat, tempo, etc.): <span class='tempo-timing'>term</span>\n"
            "- Practice instructions (practice, repeat, try, work on, etc.): <span class='practice-instruction'>term</span>\n"
            "- Reminders and callbacks (last time we did, in the previous lesson, last time, earlier we learned, etc.): <span class='student-engagement'>phrase</span>\n"
            "Apply this formatting naturally throughout your response without changing the content or flow."
        )
        draft_system_prompt = st.text_area("Draft System Prompt", draft_system_prompt_base, height=200)

        generate_button = st.button("Generate Lesson Plans")

        # Add a logout button to the sidebar
        if st.session_state["authenticated"]:
            if st.button("Logout", key="logout_button_sidebar"):
                st.session_state["authenticated"] = False
                st.rerun()

    # --- Define Outline Schema (Revised) ---
    outline_response_schema = {
        "type": "ARRAY",
        "items": {
            "type": "OBJECT",
            "properties": {
                "intro": {"type": "STRING"},
                "keyTeachingPoints": {
                    "type": "ARRAY",
                    "items": {
                        "type": "OBJECT",
                        "properties": {
                            "point": {"type": "STRING"},
                            "exercises": {"type": "ARRAY", "items": {"type": "STRING"}}
                        },
                        "required": ["point", "exercises"]
                    }
                },
                "outro": {"type": "STRING"}
            },
            "required": ["intro", "keyTeachingPoints", "outro"]
        }
    }

    # --- Lesson Generation Logic (Triggered by button) ---
    if generate_button:
        # Clear previous results when new generation is triggered
        st.session_state.lessons_data = {}
        st.session_state.lesson_topic = lesson_topic  # Store for download
        st.session_state.lesson_length = lesson_length  # Store for download
        st.session_state.selected_models = selected_models  # Store for download

        # Generate outlines
        for model_name in selected_models:
            current_outline_user_prompt = outline_user_prompt_template.format(
                lesson_length=lesson_length,
                lesson_topic=lesson_topic
            )
            full_outline_prompt = f"{outline_system_prompt}\n{current_outline_user_prompt}"
            all_outlines = call_gemini_api(model_name, full_outline_prompt, outline_response_schema)
            if all_outlines and len(all_outlines) == 5:
                st.session_state.lessons_data[model_name] = {'outlines': all_outlines, 'drafts': []}
                # Generate drafts
                for i, outline_data in enumerate(all_outlines):
                    outline_for_draft = json.dumps(outline_data, indent=2)

                    # Combine the editable prompt with the hidden sample script
                    full_draft_system_prompt = (
                        f"{draft_system_prompt}\n\n"
                        "Use the following example script as a reference for tone and style: \n\n"
                        f"--- START OF SAMPLE SCRIPT ---\n{sample_script_text}\n--- END OF SAMPLE SCRIPT ---"
                    )

                    draft_prompt = (
                        f"{full_draft_system_prompt}\n\n"
                        f"Expand the following outline for Lesson {i + 1} into a detailed rough draft for a {lesson_length} lesson. "
                        "Ensure the language is engaging for music educators.\n\n"
                        f"Outline:\n```json\n{outline_for_draft}\n```"
                    )

                    raw_draft_text = call_gemini_api(model_name, draft_prompt)
                    if raw_draft_text:
                        st.session_state.lessons_data[model_name]['drafts'].append(raw_draft_text)
                    else:
                        st.session_state.lessons_data[model_name]['drafts'].append(None)
            else:
                st.error(f"Failed to generate a complete 5-lesson sequence for {model_name}. Please try again.")

    # --- Display Generated Content and Download Buttons (Always displayed if in session_state) ---
    if st.session_state.get('lessons_data'):
        for model_name, lessons in st.session_state.lessons_data.items():
            st.subheader(f"Lesson Plans from {model_name}")

            for i in range(5):
                outline_data = lessons['outlines'][i] if len(lessons['outlines']) > i else None
                draft_text = lessons['drafts'][i] if len(lessons['drafts']) > i else None

                expander_title = f"Lesson {i + 1}: "
                if outline_data and 'intro' in outline_data:
                    expander_title += outline_data['intro'][:50] + "..."
                else:
                    expander_title += "Outline could not be generated."

                with st.expander(expander_title):
                    outline_col, draft_col = st.columns(2)
                    with outline_col:
                        st.markdown(f"**Outline for Lesson {i + 1}**")
                        if outline_data:
                            human_readable_outline = format_outline_for_display(outline_data, i + 1)
                            st.markdown(f'<div class="result-box">{human_readable_outline}</div>',
                                        unsafe_allow_html=True)
                        else:
                            st.markdown(
                                f'<div class="result-box text-gray-500">Could not generate outline for this lesson.</div>',
                                unsafe_allow_html=True)

                    with draft_col:
                        st.markdown(f"**Rough Draft for Lesson {i + 1}**")
                        if draft_text:
                            st.markdown(f'<div class="result-box">{draft_text}</div>', unsafe_allow_html=True)
                        else:
                            st.markdown(
                                f'<div class="result-box text-gray-500">Could not generate draft for this lesson.</div>',
                                unsafe_allow_html=True)

        # --- Visualizations Section ---
        st.subheader("📊 Lesson Analysis & Visualizations")

        # Show visualizations for each model
        for model_name, lessons in st.session_state.lessons_data.items():
            if len(lessons['outlines']) == 5:  # Only show if we have complete data
                st.write(f"**Analysis for {model_name}:**")

                try:
                    heatmap_fig = create_topic_coverage_heatmap(lessons['outlines'])
                    st.plotly_chart(heatmap_fig, use_container_width=True)
                    st.write("This heatmap shows which music theory topics are emphasized in each lesson.")
                except Exception as e:
                    st.error(f"Error generating topic coverage heatmap: {e}")

                st.divider()  # Add visual separation between models

        st.subheader("Download All Lessons")
        download_cols = st.columns(len(st.session_state.get('selected_models', [])))
        for i, model_name in enumerate(st.session_state.get('selected_models', [])):
            with download_cols[i]:
                lessons_data = st.session_state.lessons_data.get(model_name)
                if lessons_data:
                    num_outlines = len(lessons_data.get('outlines', []))
                    num_drafts = len(lessons_data.get('drafts', []))

                    if num_outlines == 5 and num_drafts == 5:
                        docx_file = create_docx_file(
                            lessons_data,
                            st.session_state.lesson_topic,
                            st.session_state.lesson_length,
                            model_name
                        )
                        # Sanitize filename components
                        safe_model_name = sanitize_filename(model_name, max_length=50)
                        safe_topic = sanitize_filename(st.session_state.lesson_topic, max_length=80)

                        st.download_button(
                            label=f"Download {model_name} (DOCX)",
                            data=docx_file,
                            file_name=f"{safe_model_name}_lesson_sequence_{safe_topic}.docx",
                            mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
                            key=f"download_docx_{model_name}"
                        )
                    else:
                        st.markdown(
                            f'<div class="text-gray-500">Cannot download DOCX for {model_name}<br>Outlines: {num_outlines}/5, Drafts: {num_drafts}/5</div>',
                            unsafe_allow_html=True)
                else:
                    st.markdown(
                        f'<div class="text-gray-500">No data for {model_name}</div>',
                        unsafe_allow_html=True)
    elif generate_button:
        st.info("No content generated. Please check for API errors or adjust prompts.")

    st.markdown("""
    <style>
    .result-box {
        border: 1px solid #ddd;
        border-radius: 8px;
        padding: 15px;
        margin-bottom: 10px;
        background-color: #f9f9f9;
        white-space: pre-wrap;
        word-wrap: break-word;
    }
    .text-gray-500 {
        color: #6b7280;
    }

    /* Color coding for music lesson terms */
    .music-theory {
        background-color: #e0f2fe;
        color: #0277bd;
        padding: 1px 3px;
        border-radius: 3px;
        font-weight: 500;
    }
    .tempo-timing {
        background-color: #f3e5f5;
        color: #7b1fa2;
        padding: 1px 3px;
        border-radius: 3px;
        font-weight: 500;
    }
    .practice-instruction {
        background-color: #fff3e0;
        color: #f57c00;
        padding: 1px 3px;
        border-radius: 3px;
        font-weight: 500;
    }
    .student-engagement {
        background-color: #e8f5e8;
        color: #2e7d32;
        padding: 1px 3px;
        border-radius: 3px;
        font-weight: 500;
    }
    </style>
    """, unsafe_allow_html=True)