gmedin commited on
Commit
f57451f
·
verified ·
1 Parent(s): 4271db1

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +393 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,395 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
 
 
2
  import streamlit as st
3
+ import requests
4
+ import json
5
+ from docx import Document
6
+ from io import BytesIO
7
 
8
+ # --- Page Configuration ---
9
+ st.set_page_config(
10
+ page_title="Music Lesson Planner",
11
+ page_icon="🎶",
12
+ layout="wide",
13
+ initial_sidebar_state="expanded"
14
+ )
15
+
16
+ # --- Constants and API Setup ---
17
+ # IMPORTANT: Set your Google API Key as an environment variable named GOOGLE_API_KEY
18
+ # You can get one from Google AI Studio: https://aistudio.google.com/app/apikey
19
+ GEMINI_API_KEY = os.getenv('GOOGLE_API_KEY')
20
+
21
+ # Base URL for Gemini API
22
+ GEMINI_API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/"
23
+
24
+ # Available Gemini Models for comparison
25
+ GEMINI_MODELS = {
26
+ "Gemini 2.0 Flash": "gemini-2.0-flash",
27
+ "Gemini 1.5 Pro": "gemini-1.5-pro-latest",
28
+ "Gemini 1.0 Pro": "gemini-1.0-pro",
29
+ }
30
+
31
+
32
+ # --- Helper Function for LLM API Call ---
33
+ def call_gemini_api(model_name, prompt_text, response_schema=None):
34
+ """
35
+ Calls the Gemini API with the given model and prompt.
36
+ Handles JSON parsing and error reporting.
37
+ """
38
+ if not GEMINI_API_KEY:
39
+ st.error(
40
+ "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.")
41
+ return None
42
+
43
+ model_id = GEMINI_MODELS.get(model_name)
44
+ if not model_id:
45
+ st.error(f"Unknown model: {model_name}")
46
+ return None
47
+
48
+ url = f"{GEMINI_API_BASE_URL}{model_id}:generateContent?key={GEMINI_API_KEY}"
49
+
50
+ headers = {
51
+ "Content-Type": "application/json",
52
+ }
53
+
54
+ payload = {
55
+ "contents": [
56
+ {
57
+ "role": "user",
58
+ "parts": [{"text": prompt_text}]
59
+ }
60
+ ],
61
+ "generationConfig": {} # Initialize generationConfig
62
+ }
63
+
64
+ # If a response_schema is provided, configure for structured output
65
+ if response_schema:
66
+ payload["generationConfig"]["responseMimeType"] = "application/json"
67
+ payload["generationConfig"]["responseSchema"] = response_schema
68
+ # Add a clear instruction to the prompt to ensure JSON output
69
+ # This can sometimes help guide the model more effectively.
70
+ prompt_text = f"{prompt_text}\n\nPlease provide the response strictly in JSON format according to the schema provided, with no additional text or markdown outside the JSON object."
71
+ payload["contents"][0]["parts"][0]["text"] = prompt_text
72
+
73
+ try:
74
+ response = requests.post(url, headers=headers, json=payload)
75
+ response.raise_for_status() # Raise an exception for HTTP errors (4xx or 5xx)
76
+ response_data = response.json()
77
+
78
+ if response_data and response_data.get("candidates"):
79
+ # Access the text part of the response
80
+ if response_schema:
81
+ # For structured responses, the content is directly the JSON string
82
+ raw_json_text = response_data["candidates"][0]["content"]["parts"][0]["text"]
83
+ try:
84
+ # Attempt to parse the JSON string
85
+ parsed_json = json.loads(raw_json_text)
86
+ return parsed_json
87
+ except json.JSONDecodeError as e:
88
+ st.error(f"Failed to parse JSON for {model_name} outline. Error: {e}")
89
+ st.text_area(f"Raw output from {model_name}:", raw_json_text, height=200)
90
+ return None
91
+ else:
92
+ # For unstructured responses, return the text directly
93
+ return response_data["candidates"][0]["content"]["parts"][0]["text"]
94
+ else:
95
+ st.error(f"No valid response candidates found for {model_name}.")
96
+ st.json(response_data) # Display the full response for debugging
97
+ return None
98
+
99
+ except requests.exceptions.HTTPError as http_err:
100
+ st.error(f"HTTP error occurred for {model_name}: {http_err}")
101
+ st.error(f"Response content: {response.text}")
102
+ return None
103
+ except requests.exceptions.ConnectionError as conn_err:
104
+ st.error(f"Connection error occurred for {model_name}: {conn_err}")
105
+ return None
106
+ except requests.exceptions.Timeout as timeout_err:
107
+ st.error(f"Timeout error occurred for {model_name}: {timeout_err}")
108
+ return None
109
+ except requests.exceptions.RequestException as req_err:
110
+ st.error(f"An unexpected error occurred for {model_name}: {req_err}")
111
+ return None
112
+ except Exception as e:
113
+ st.error(f"An unexpected error occurred during API call for {model_name}: {e}")
114
+ return None
115
+
116
+
117
+ # --- DOCX Conversion Function ---
118
+ def create_docx_file(outline_data, draft_text, lesson_topic, lesson_length, model_name):
119
+ """
120
+ Creates a DOCX file from outline data and draft text.
121
+ """
122
+ document = Document()
123
+
124
+ # Add Lesson Title
125
+ document.add_heading(outline_data.get("lessonTitle", "Untitled Lesson"), level=1)
126
+ document.add_paragraph(f"Topic: {lesson_topic}")
127
+ document.add_paragraph(f"Length: {lesson_length}")
128
+ document.add_paragraph(f"Generated by: {model_name}")
129
+ document.add_paragraph("\n") # Add a blank line for spacing
130
+
131
+ # Add Learning Objectives
132
+ document.add_heading("Learning Objectives", level=2)
133
+ for obj in outline_data.get("learningObjectives", []):
134
+ document.add_paragraph(obj, style='List Bullet')
135
+ document.add_paragraph("\n")
136
+
137
+ # Add Materials
138
+ document.add_heading("Materials", level=2)
139
+ for material in outline_data.get("materials", []):
140
+ document.add_paragraph(material, style='List Bullet')
141
+ document.add_paragraph("\n")
142
+
143
+ # Add Procedure
144
+ document.add_heading("Procedure", level=2)
145
+ for section in outline_data.get("procedure", []):
146
+ document.add_heading(f"{section.get('sectionTitle', 'Section')} ({section.get('timeAllocation', 'N/A')})",
147
+ level=3)
148
+ for activity in section.get("activities", []):
149
+ document.add_paragraph(activity, style='List Bullet')
150
+ document.add_paragraph("\n")
151
+
152
+ # Add Assessment
153
+ document.add_heading("Assessment", level=2)
154
+ for assessment_item in outline_data.get("assessment", []):
155
+ document.add_paragraph(assessment_item, style='List Bullet')
156
+ document.add_paragraph("\n")
157
+
158
+ # Add Full Draft Content
159
+ document.add_heading("Full Lesson Draft", level=2)
160
+ document.add_paragraph(draft_text)
161
+
162
+ # Save document to a BytesIO object
163
+ byte_io = BytesIO()
164
+ document.save(byte_io)
165
+ byte_io.seek(0) # Rewind the buffer to the beginning
166
+ return byte_io.getvalue()
167
+
168
+
169
+ # --- Password Protection ---
170
+ def authenticate_user():
171
+ st.markdown("## 🔐 Secure Login")
172
+ password = st.text_input("Password", type="password", key="password_input")
173
+ submit = st.button("Login", key="login_button")
174
+
175
+ if submit:
176
+ correct_password = os.getenv("APP_PASSWORD")
177
+
178
+ if password == correct_password:
179
+ st.session_state["authenticated"] = True
180
+ st.rerun() # Rerun to clear password input and show app content
181
+ else:
182
+ st.error("Invalid password")
183
+
184
+
185
+ # Check authentication status
186
+ if "authenticated" not in st.session_state:
187
+ st.session_state["authenticated"] = False
188
+
189
+ if not st.session_state["authenticated"]:
190
+ authenticate_user()
191
+ st.stop() # Stop execution if not authenticated
192
+ else:
193
+ # --- Main Application Logic (Protected by Authentication) ---
194
+ st.title("🎶 Music Lesson Planner")
195
+
196
+ st.markdown("""
197
+ This app helps you draft outlines and detailed lesson plans for online music lessons using different Gemini models.
198
+ Compare outputs to find the best fit for your pedagogical needs!
199
+ """)
200
+
201
+ # Initialize session state for outlines and drafts if not already present
202
+ if 'outlines' not in st.session_state:
203
+ st.session_state.outlines = {}
204
+ if 'drafts' not in st.session_state:
205
+ st.session_state.drafts = {}
206
+
207
+ # Input fields for lesson details
208
+ with st.sidebar:
209
+ st.header("Lesson Details")
210
+ lesson_topic = st.text_input("Lesson Topic", "Introduction to Solfege")
211
+ lesson_length = st.selectbox("Lesson Length", ["2-minute", "5-minute", "10-minute", "15-minute"], index=1)
212
+
213
+ st.header("Model Selection")
214
+ selected_models = st.multiselect(
215
+ "Select Gemini Models",
216
+ list(GEMINI_MODELS.keys()),
217
+ default=["Gemini 2.0 Flash", "Gemini 1.5 Pro"]
218
+ )
219
+
220
+ st.header("Prompt Customization")
221
+ default_outline_system_prompt = (
222
+ "You are an AI assistant specialized in creating concise and structured outlines for online music lessons. "
223
+ "Your goal is to provide a clear, pedagogical framework that music educators can easily follow. "
224
+ "Focus on key components: lesson title, learning objectives, materials, a step-by-step procedure with time allocations and activities, and assessment methods."
225
+ "The lessons are online and asynchronous, so ensure the outline is suitable for self-paced learning."
226
+ )
227
+ outline_system_prompt = st.text_area("Outline System Prompt", default_outline_system_prompt, height=150)
228
+
229
+ default_outline_user_prompt_template = (
230
+ "Create a {lesson_length} online music lesson outline on the topic of '{lesson_topic}'. "
231
+ "The outline should be structured as a JSON object with the following keys: "
232
+ "'lessonTitle', 'learningObjectives' (list of strings), 'materials' (list of strings), "
233
+ "'procedure' (list of objects, each with 'sectionTitle', 'timeAllocation', and 'activities' (list of strings)), "
234
+ "and 'assessment' (list of strings). "
235
+ "Ensure 'timeAllocation' for each procedure section is a string indicating duration (e.g., '5 minutes'). "
236
+ "Make sure the total time allocations add up to the {lesson_length}."
237
+ "Example for 'procedure' section: "
238
+ '{{"sectionTitle": "Introduction", "timeAllocation": "5 minutes", "activities": ["Greet students", "Review previous concepts"]}}'
239
+ )
240
+ outline_user_prompt_template = st.text_area("Outline User Prompt Template",
241
+ default_outline_user_prompt_template, height=250)
242
+
243
+ default_draft_system_prompt = (
244
+ "You are an AI assistant specialized in expanding structured lesson outlines into detailed, engaging rough drafts for online music lessons. "
245
+ "Your goal is to provide specific examples, pedagogical details, and interactive elements for each activity. "
246
+ "The language should be engaging and professional, tailored for music educators."
247
+ "The lessons are online and asynchronous, so ensure the draft is suitable for self-paced learning."
248
+ )
249
+ draft_system_prompt = st.text_area("Draft System Prompt", default_draft_system_prompt, height=150)
250
+
251
+ generate_button = st.button("Generate Lesson Plans")
252
+
253
+ # Add a logout button to the sidebar
254
+ if st.session_state["authenticated"]:
255
+ if st.button("Logout", key="logout_button_sidebar"):
256
+ st.session_state["authenticated"] = False
257
+ st.rerun()
258
+
259
+ # --- Define Outline Schema ---
260
+ outline_response_schema = {
261
+ "type": "OBJECT",
262
+ "properties": {
263
+ "lessonTitle": {"type": "STRING"},
264
+ "learningObjectives": {"type": "ARRAY", "items": {"type": "STRING"}},
265
+ "materials": {"type": "ARRAY", "items": {"type": "STRING"}},
266
+ "procedure": {
267
+ "type": "ARRAY",
268
+ "items": {
269
+ "type": "OBJECT",
270
+ "properties": {
271
+ "sectionTitle": {"type": "STRING"},
272
+ "timeAllocation": {"type": "STRING"},
273
+ "activities": {"type": "ARRAY", "items": {"type": "STRING"}}
274
+ },
275
+ "required": ["sectionTitle", "timeAllocation", "activities"]
276
+ }
277
+ },
278
+ "assessment": {"type": "ARRAY", "items": {"type": "STRING"}}
279
+ },
280
+ "required": ["lessonTitle", "learningObjectives", "materials", "procedure", "assessment"]
281
+ }
282
+
283
+ # --- Lesson Generation Logic (Triggered by button) ---
284
+ if generate_button:
285
+ # Clear previous results when new generation is triggered
286
+ st.session_state.outlines = {}
287
+ st.session_state.drafts = {}
288
+ st.session_state.lesson_topic = lesson_topic # Store for download
289
+ st.session_state.lesson_length = lesson_length # Store for download
290
+ st.session_state.selected_models = selected_models # Store for download
291
+
292
+ # Generate outlines
293
+ for model_name in selected_models:
294
+ current_outline_user_prompt = outline_user_prompt_template.format(
295
+ lesson_length=lesson_length,
296
+ lesson_topic=lesson_topic
297
+ )
298
+ full_outline_prompt = f"{outline_system_prompt}\n{current_outline_user_prompt}"
299
+ outline_data = call_gemini_api(model_name, full_outline_prompt, outline_response_schema)
300
+ if outline_data:
301
+ st.session_state.outlines[model_name] = outline_data
302
+ else:
303
+ st.session_state.outlines[model_name] = None # Mark as failed
304
+
305
+ # Generate drafts
306
+ for model_name in selected_models:
307
+ if model_name in st.session_state.outlines and st.session_state.outlines[model_name]:
308
+ outline_for_draft = json.dumps(st.session_state.outlines[model_name], indent=2)
309
+ draft_prompt = (
310
+ f"{draft_system_prompt}\n\n"
311
+ f"Expand the following lesson outline into a detailed rough draft for a {lesson_length} lesson. "
312
+ "Provide specific examples and pedagogical details for each activity. "
313
+ "Ensure the language is engaging for music educators.\n\n"
314
+ f"Outline:\n```json\n{outline_for_draft}\n```"
315
+ )
316
+ raw_draft_text = call_gemini_api(model_name, draft_prompt)
317
+ if raw_draft_text:
318
+ st.session_state.drafts[model_name] = raw_draft_text
319
+ else:
320
+ st.session_state.drafts[model_name] = None # Mark as failed
321
+ else:
322
+ st.session_state.drafts[model_name] = None # Cannot generate draft without outline
323
+
324
+ # --- Display Generated Content and Download Buttons (Always displayed if in session_state) ---
325
+ if st.session_state.get('outlines') or st.session_state.get('drafts'):
326
+ st.subheader("Generated Outlines")
327
+ outline_cols = st.columns(len(st.session_state.get('selected_models', [])))
328
+ for i, model_name in enumerate(st.session_state.get('selected_models', [])):
329
+ with outline_cols[i]:
330
+ st.markdown(f"### {model_name} Outline")
331
+ if st.session_state.outlines.get(model_name):
332
+ st.json(st.session_state.outlines[model_name])
333
+ else:
334
+ st.markdown(
335
+ f'<div class="result-box text-gray-500">Could not generate outline for {model_name}.</div>',
336
+ unsafe_allow_html=True)
337
+
338
+ st.subheader("Generated Rough Drafts")
339
+ draft_cols = st.columns(len(st.session_state.get('selected_models', [])))
340
+ for i, model_name in enumerate(st.session_state.get('selected_models', [])):
341
+ with draft_cols[i]:
342
+ st.markdown(f"### {model_name} Rough Draft")
343
+ if st.session_state.drafts.get(model_name):
344
+ st.markdown(f'<div class="result-box">{st.session_state.drafts[model_name]}</div>',
345
+ unsafe_allow_html=True)
346
+ else:
347
+ st.markdown(
348
+ f'<div class="result-box text-gray-500">Could not generate draft for {model_name}.</div>',
349
+ unsafe_allow_html=True)
350
+
351
+ st.subheader("Download Results")
352
+ download_cols = st.columns(len(st.session_state.get('selected_models', [])))
353
+ for i, model_name in enumerate(st.session_state.get('selected_models', [])):
354
+ with download_cols[i]:
355
+ outline_data = st.session_state.outlines.get(model_name)
356
+ draft_text = st.session_state.drafts.get(model_name)
357
+
358
+ if outline_data and draft_text:
359
+ docx_file = create_docx_file(
360
+ outline_data,
361
+ draft_text,
362
+ st.session_state.lesson_topic,
363
+ st.session_state.lesson_length,
364
+ model_name
365
+ )
366
+ st.download_button(
367
+ label=f"Download {model_name} (DOCX)",
368
+ data=docx_file,
369
+ file_name=f"{model_name.replace(' ', '_')}_lesson_plan_{st.session_state.lesson_topic.replace(' ', '_')}.docx",
370
+ mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
371
+ key=f"download_docx_{model_name}"
372
+ )
373
+ else:
374
+ st.markdown(
375
+ f'<div class="text-gray-500">Cannot download DOCX for {model_name} (missing data).</div>',
376
+ unsafe_allow_html=True)
377
+ elif generate_button:
378
+ st.info("No content generated. Please check for API errors or adjust prompts.")
379
+
380
+ st.markdown("""
381
+ <style>
382
+ .result-box {
383
+ border: 1px solid #ddd;
384
+ border-radius: 8px;
385
+ padding: 15px;
386
+ margin-bottom: 10px;
387
+ background-color: #f9f9f9;
388
+ white-space: pre-wrap;
389
+ word-wrap: break-word;
390
+ }
391
+ .text-gray-500 {
392
+ color: #6b7280;
393
+ }
394
+ </style>
395
+ """, unsafe_allow_html=True)