Thamaraikannan commited on
Commit
9923316
·
verified ·
1 Parent(s): 10067f6

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +110 -92
src/streamlit_app.py CHANGED
@@ -1,16 +1,26 @@
 
1
  import streamlit as st
2
  import pandas as pd
3
  import re
 
 
 
 
 
 
 
 
4
 
5
- st.set_page_config(page_title="🧪 LaTeX MCQs", layout="wide")
 
 
 
 
6
  st.title("🧪 Chemistry MCQs with LaTeX Rendering")
7
 
8
  # Helper to safely convert mixed text + LaTeX into proper LaTeX
9
  def format_for_latex(text):
10
- """
11
- Convert mixed text and LaTeX expressions into properly formatted LaTeX.
12
- Handles common chemistry notation and mathematical expressions.
13
- """
14
  if pd.isna(text) or text == "":
15
  return ""
16
 
@@ -21,10 +31,8 @@ def format_for_latex(text):
21
  text = re.sub(r'\$(.*?)\$', r'\1', text)
22
 
23
  # Handle common chemistry and math patterns
24
- # Preserve existing LaTeX commands
25
  latex_commands = r'(\\(?:ce|text|frac|sqrt|sum|int|lim|mathrm|mathbf|mathit|alpha|beta|gamma|delta|epsilon|theta|lambda|mu|pi|sigma|omega|rightarrow|leftarrow|leftrightarrow|cdot|times|div|pm|neq|leq|geq|approx|infty|partial|nabla|Delta|Omega|therefore|because)\b[^{]*(?:\{[^}]*\})*)'
26
 
27
- # Split text preserving LaTeX commands
28
  parts = re.split(f'({latex_commands})', text)
29
 
30
  formatted = ""
@@ -32,17 +40,13 @@ def format_for_latex(text):
32
  if not part:
33
  continue
34
 
35
- # Check if this part is a LaTeX command
36
  if re.match(r'\\[a-zA-Z]+', part):
37
  formatted += part
38
  else:
39
- # Handle plain text - wrap non-empty parts
40
  if part.strip():
41
- # Handle subscripts and superscripts in chemical formulas
42
- part = re.sub(r'([A-Za-z])(\d+)', r'\1_{\2}', part) # H2O -> H_{2}O
43
- part = re.sub(r'\^(\d+)', r'^{\1}', part) # ^2 -> ^{2}
44
 
45
- # Check if the part already contains LaTeX formatting
46
  if not re.search(r'[{}\\]', part):
47
  formatted += f"\\text{{{part}}}"
48
  else:
@@ -58,7 +62,6 @@ def validate_dataframe(df):
58
  if missing_columns:
59
  return False, f"Missing required columns: {missing_columns}"
60
 
61
- # Check for empty questions
62
  empty_questions = df["question_latex"].isna().sum()
63
  if empty_questions > 0:
64
  st.warning(f"⚠️ Found {empty_questions} empty questions that will be skipped.")
@@ -67,7 +70,6 @@ def validate_dataframe(df):
67
 
68
  def render_question(idx, row):
69
  """Render a single question with its options."""
70
- # Skip if question is empty
71
  if pd.isna(row["question_latex"]) or str(row["question_latex"]).strip() == "":
72
  return
73
 
@@ -107,92 +109,129 @@ def render_question(idx, row):
107
  st.warning(f"Error rendering option {key}: {e}")
108
  st.markdown(f"{value}")
109
 
110
-
111
-
112
  st.markdown("---")
113
 
114
- # Main app
115
- uploaded_file = st.file_uploader("📥 Upload Excel file with LaTeX questions", type=["xlsx", "xls"])
116
-
117
- if uploaded_file:
118
  try:
119
- # Show file info
120
- st.info(f"📄 Loaded file: {uploaded_file.name}")
121
-
122
  df = pd.read_excel(uploaded_file)
123
-
124
- # Validate dataframe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  is_valid, message = validate_dataframe(df)
126
 
127
  if not is_valid:
128
  st.error(f"❌ {message}")
129
- st.markdown("### Required Excel Format:")
130
- st.markdown("""
131
- Your Excel file should contain these columns:
132
- - `question_latex`: The question text with LaTeX formatting
133
- - `option_1`: Option A
134
- - `option_2`: Option B
135
- - `option_3`: Option C
136
- - `option_4`: Option D
137
- - `correct_answer` (optional): A, B, C, or D
138
- """)
139
  else:
140
- st.success(f"✅ Successfully loaded {len(df)} questions")
141
 
142
- # Initialize session state for current question and navigator page
143
  if 'current_question' not in st.session_state:
144
  st.session_state.current_question = 0
145
  if 'navigator_page' not in st.session_state:
146
  st.session_state.navigator_page = 0
147
 
148
- # Question number selector at the top
149
  st.markdown("### 📋 Question Navigator")
150
 
151
- # Navigator pagination settings
152
  total_questions = len(df)
153
- questions_per_page = 50 # Questions shown per navigator page
154
- questions_per_row = 10 # Question buttons per row
155
 
156
- # Calculate total pages for navigator
157
  total_nav_pages = (total_questions + questions_per_page - 1) // questions_per_page
158
 
159
- # Navigator page controls (only show if more than 50 questions)
160
  if total_questions > questions_per_page:
161
- nav_col1, nav_col2, nav_col3, nav_col4, nav_col5 = st.columns([1, 1, 2, 1, 1])
162
 
163
- with nav_col1:
164
- if st.button("⏪ First Set", disabled=st.session_state.navigator_page == 0, key="nav_first"):
165
  st.session_state.navigator_page = 0
166
  st.rerun()
167
 
168
- with nav_col2:
169
- if st.button("◀️ Prev Set", disabled=st.session_state.navigator_page == 0, key="nav_prev"):
170
  st.session_state.navigator_page -= 1
171
  st.rerun()
172
 
173
- with nav_col3:
174
- # Show current navigator page info
175
  start_q = st.session_state.navigator_page * questions_per_page + 1
176
  end_q = min((st.session_state.navigator_page + 1) * questions_per_page, total_questions)
177
- st.markdown(f"**Questions {start_q}-{end_q} of {total_questions} | Page {st.session_state.navigator_page + 1}/{total_nav_pages}**")
178
 
179
- with nav_col4:
180
- if st.button("Next Set ▶️", disabled=st.session_state.navigator_page >= total_nav_pages - 1, key="nav_next"):
181
  st.session_state.navigator_page += 1
182
  st.rerun()
183
 
184
- with nav_col5:
185
- if st.button("Last Set ⏩", disabled=st.session_state.navigator_page >= total_nav_pages - 1, key="nav_last"):
186
  st.session_state.navigator_page = total_nav_pages - 1
187
  st.rerun()
188
 
189
  st.markdown("---")
190
 
191
- # Calculate question range for current navigator page
192
  start_idx = st.session_state.navigator_page * questions_per_page
193
  end_idx = min(start_idx + questions_per_page, total_questions)
194
 
195
- # Create clickable question number buttons for current page
196
  questions_to_show = end_idx - start_idx
197
  rows_needed = (questions_to_show + questions_per_row - 1) // questions_per_row
198
 
@@ -207,7 +246,6 @@ if uploaded_file:
207
  question_num = question_idx + 1
208
 
209
  with col:
210
- # Highlight current question
211
  if question_idx == st.session_state.current_question:
212
  button_label = f"**Q{question_num}**"
213
  button_type = "primary"
@@ -221,65 +259,57 @@ if uploaded_file:
221
 
222
  st.markdown("---")
223
 
224
- # Navigation controls for current question
225
- col1, col2, col3, col4, col5 = st.columns([1, 1, 2, 1, 1])
226
 
227
- with col1:
228
  if st.button("⏮️ First", disabled=st.session_state.current_question == 0):
229
  st.session_state.current_question = 0
230
- # Update navigator page if needed
231
  st.session_state.navigator_page = 0
232
  st.rerun()
233
 
234
- with col2:
235
  if st.button("⬅️ Previous", disabled=st.session_state.current_question == 0):
236
  st.session_state.current_question -= 1
237
- # Update navigator page if needed
238
  new_page = st.session_state.current_question // questions_per_page
239
  if new_page != st.session_state.navigator_page:
240
  st.session_state.navigator_page = new_page
241
  st.rerun()
242
 
243
- with col3:
244
- # Dropdown selector for quick navigation
245
  question_options = [f"Question {i+1}" for i in range(total_questions)]
246
  selected_q = st.selectbox(
247
- "Jump to question:",
248
  question_options,
249
- index=st.session_state.current_question,
250
- key="question_dropdown"
251
  )
252
 
253
- # Update current question if dropdown selection changes
254
  new_index = question_options.index(selected_q)
255
  if new_index != st.session_state.current_question:
256
  st.session_state.current_question = new_index
257
- # Update navigator page if needed
258
  new_page = new_index // questions_per_page
259
  if new_page != st.session_state.navigator_page:
260
  st.session_state.navigator_page = new_page
261
  st.rerun()
262
 
263
- with col4:
264
  if st.button("Next ➡️", disabled=st.session_state.current_question >= total_questions - 1):
265
  st.session_state.current_question += 1
266
- # Update navigator page if needed
267
  new_page = st.session_state.current_question // questions_per_page
268
  if new_page != st.session_state.navigator_page:
269
  st.session_state.navigator_page = new_page
270
  st.rerun()
271
 
272
- with col5:
273
  if st.button("Last ⏭️", disabled=st.session_state.current_question >= total_questions - 1):
274
  st.session_state.current_question = total_questions - 1
275
- # Update navigator page if needed
276
  st.session_state.navigator_page = total_nav_pages - 1
277
  st.rerun()
278
 
279
- # Progress indicator
280
  progress = (st.session_state.current_question + 1) / total_questions
281
  st.progress(progress)
282
- st.caption(f"Progress: {st.session_state.current_question + 1}/{total_questions} questions ({progress:.1%})")
283
 
284
  st.markdown("---")
285
 
@@ -287,19 +317,8 @@ if uploaded_file:
287
  if 0 <= st.session_state.current_question < len(df):
288
  current_row = df.iloc[st.session_state.current_question]
289
  render_question(st.session_state.current_question, current_row)
290
-
291
-
292
-
293
- except Exception as e:
294
- st.error(f"❌ Error reading file: {e}")
295
- st.markdown("### Troubleshooting:")
296
- st.markdown("""
297
- - Make sure your file is a valid Excel (.xlsx or .xls) file
298
- - Check that all required columns are present
299
- - Ensure the file is not corrupted or password-protected
300
- """)
301
 
302
- # Add sample data section
303
  with st.expander("📋 Sample Excel Format"):
304
  sample_data = pd.DataFrame({
305
  "question_latex": [
@@ -315,11 +334,10 @@ with st.expander("📋 Sample Excel Format"):
315
  })
316
  st.dataframe(sample_data)
317
 
318
- # Download sample
319
  csv = sample_data.to_csv(index=False)
320
  st.download_button(
321
  label="📥 Download Sample CSV",
322
  data=csv,
323
  file_name="sample_mcq_format.csv",
324
  mime="text/csv"
325
- )
 
1
+
2
  import streamlit as st
3
  import pandas as pd
4
  import re
5
+ import io
6
+ import tempfile
7
+ import os
8
+
9
+ # Configure Streamlit to prevent permission issues
10
+ os.environ['STREAMLIT_SERVER_HEADLESS'] = 'true'
11
+ os.environ['STREAMLIT_SERVER_ENABLE_CORS'] = 'false'
12
+ os.environ['STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION'] = 'false'
13
 
14
+ st.set_page_config(
15
+ page_title="🧪 LaTeX MCQs",
16
+ layout="wide",
17
+ initial_sidebar_state="collapsed"
18
+ )
19
  st.title("🧪 Chemistry MCQs with LaTeX Rendering")
20
 
21
  # Helper to safely convert mixed text + LaTeX into proper LaTeX
22
  def format_for_latex(text):
23
+ """Convert mixed text and LaTeX expressions into properly formatted LaTeX."""
 
 
 
24
  if pd.isna(text) or text == "":
25
  return ""
26
 
 
31
  text = re.sub(r'\$(.*?)\$', r'\1', text)
32
 
33
  # Handle common chemistry and math patterns
 
34
  latex_commands = r'(\\(?:ce|text|frac|sqrt|sum|int|lim|mathrm|mathbf|mathit|alpha|beta|gamma|delta|epsilon|theta|lambda|mu|pi|sigma|omega|rightarrow|leftarrow|leftrightarrow|cdot|times|div|pm|neq|leq|geq|approx|infty|partial|nabla|Delta|Omega|therefore|because)\b[^{]*(?:\{[^}]*\})*)'
35
 
 
36
  parts = re.split(f'({latex_commands})', text)
37
 
38
  formatted = ""
 
40
  if not part:
41
  continue
42
 
 
43
  if re.match(r'\\[a-zA-Z]+', part):
44
  formatted += part
45
  else:
 
46
  if part.strip():
47
+ part = re.sub(r'([A-Za-z])(\d+)', r'\1_{\2}', part)
48
+ part = re.sub(r'\^(\d+)', r'^{\1}', part)
 
49
 
 
50
  if not re.search(r'[{}\\]', part):
51
  formatted += f"\\text{{{part}}}"
52
  else:
 
62
  if missing_columns:
63
  return False, f"Missing required columns: {missing_columns}"
64
 
 
65
  empty_questions = df["question_latex"].isna().sum()
66
  if empty_questions > 0:
67
  st.warning(f"⚠️ Found {empty_questions} empty questions that will be skipped.")
 
70
 
71
  def render_question(idx, row):
72
  """Render a single question with its options."""
 
73
  if pd.isna(row["question_latex"]) or str(row["question_latex"]).strip() == "":
74
  return
75
 
 
109
  st.warning(f"Error rendering option {key}: {e}")
110
  st.markdown(f"{value}")
111
 
 
 
112
  st.markdown("---")
113
 
114
+ def safe_read_excel(uploaded_file):
115
+ """Safely read Excel file with multiple fallback methods."""
 
 
116
  try:
117
+ # Method 1: Direct reading
 
 
118
  df = pd.read_excel(uploaded_file)
119
+ return df, None
120
+ except Exception as e1:
121
+ try:
122
+ # Method 2: BytesIO
123
+ bytes_data = uploaded_file.getvalue()
124
+ df = pd.read_excel(io.BytesIO(bytes_data))
125
+ return df, None
126
+ except Exception as e2:
127
+ try:
128
+ # Method 3: Temporary file in /tmp (writable in containers)
129
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx', dir='/tmp') as tmp_file:
130
+ tmp_file.write(uploaded_file.getbuffer())
131
+ tmp_file.flush()
132
+
133
+ df = pd.read_excel(tmp_file.name)
134
+
135
+ # Cleanup
136
+ try:
137
+ os.unlink(tmp_file.name)
138
+ except:
139
+ pass
140
+
141
+ return df, None
142
+ except Exception as e3:
143
+ error_msg = f"All methods failed - Method 1: {str(e1)[:100]}, Method 2: {str(e2)[:100]}, Method 3: {str(e3)[:100]}"
144
+ return None, error_msg
145
+
146
+ # Main app
147
+ st.markdown("""
148
+ ### 📋 Instructions
149
+ 1. Upload an Excel file (.xlsx or .xls) with your questions
150
+ 2. Make sure your file has the required columns (see sample format below)
151
+ 3. Navigate through questions using the controls
152
+ """)
153
+
154
+ uploaded_file = st.file_uploader(
155
+ "📥 Upload Excel file with LaTeX questions",
156
+ type=["xlsx", "xls"],
157
+ help="Upload an Excel file containing your MCQ questions with LaTeX formatting"
158
+ )
159
+
160
+ if uploaded_file is not None:
161
+ st.info(f"📄 Processing: {uploaded_file.name} ({uploaded_file.size} bytes)")
162
+
163
+ with st.spinner("🔄 Reading Excel file..."):
164
+ df, error_msg = safe_read_excel(uploaded_file)
165
+
166
+ if df is None:
167
+ st.error(f"❌ Error: {error_msg}")
168
+ st.markdown("### Troubleshooting:")
169
+ st.markdown("""
170
+ - Ensure file is valid Excel format (.xlsx/.xls)
171
+ - Check file isn't corrupted or password-protected
172
+ - Try re-uploading the file
173
+ - Verify all required columns exist
174
+ """)
175
+ else:
176
  is_valid, message = validate_dataframe(df)
177
 
178
  if not is_valid:
179
  st.error(f"❌ {message}")
180
+ st.markdown("### Required Columns:")
181
+ st.code("question_latex, option_1, option_2, option_3, option_4")
 
 
 
 
 
 
 
 
182
  else:
183
+ st.success(f"✅ Loaded {len(df)} questions successfully!")
184
 
185
+ # Initialize session state
186
  if 'current_question' not in st.session_state:
187
  st.session_state.current_question = 0
188
  if 'navigator_page' not in st.session_state:
189
  st.session_state.navigator_page = 0
190
 
191
+ # Navigation system
192
  st.markdown("### 📋 Question Navigator")
193
 
 
194
  total_questions = len(df)
195
+ questions_per_page = 50
196
+ questions_per_row = 10
197
 
 
198
  total_nav_pages = (total_questions + questions_per_page - 1) // questions_per_page
199
 
200
+ # Navigator pagination (if needed)
201
  if total_questions > questions_per_page:
202
+ nav_cols = st.columns([1, 1, 2, 1, 1])
203
 
204
+ with nav_cols[0]:
205
+ if st.button("⏪ First Set", disabled=st.session_state.navigator_page == 0):
206
  st.session_state.navigator_page = 0
207
  st.rerun()
208
 
209
+ with nav_cols[1]:
210
+ if st.button("◀️ Prev Set", disabled=st.session_state.navigator_page == 0):
211
  st.session_state.navigator_page -= 1
212
  st.rerun()
213
 
214
+ with nav_cols[2]:
 
215
  start_q = st.session_state.navigator_page * questions_per_page + 1
216
  end_q = min((st.session_state.navigator_page + 1) * questions_per_page, total_questions)
217
+ st.markdown(f"**Questions {start_q}-{end_q} of {total_questions}**")
218
 
219
+ with nav_cols[3]:
220
+ if st.button("Next Set ▶️", disabled=st.session_state.navigator_page >= total_nav_pages - 1):
221
  st.session_state.navigator_page += 1
222
  st.rerun()
223
 
224
+ with nav_cols[4]:
225
+ if st.button("Last Set ⏩", disabled=st.session_state.navigator_page >= total_nav_pages - 1):
226
  st.session_state.navigator_page = total_nav_pages - 1
227
  st.rerun()
228
 
229
  st.markdown("---")
230
 
231
+ # Question number buttons
232
  start_idx = st.session_state.navigator_page * questions_per_page
233
  end_idx = min(start_idx + questions_per_page, total_questions)
234
 
 
235
  questions_to_show = end_idx - start_idx
236
  rows_needed = (questions_to_show + questions_per_row - 1) // questions_per_row
237
 
 
246
  question_num = question_idx + 1
247
 
248
  with col:
 
249
  if question_idx == st.session_state.current_question:
250
  button_label = f"**Q{question_num}**"
251
  button_type = "primary"
 
259
 
260
  st.markdown("---")
261
 
262
+ # Main navigation controls
263
+ nav_cols = st.columns([1, 1, 2, 1, 1])
264
 
265
+ with nav_cols[0]:
266
  if st.button("⏮️ First", disabled=st.session_state.current_question == 0):
267
  st.session_state.current_question = 0
 
268
  st.session_state.navigator_page = 0
269
  st.rerun()
270
 
271
+ with nav_cols[1]:
272
  if st.button("⬅️ Previous", disabled=st.session_state.current_question == 0):
273
  st.session_state.current_question -= 1
 
274
  new_page = st.session_state.current_question // questions_per_page
275
  if new_page != st.session_state.navigator_page:
276
  st.session_state.navigator_page = new_page
277
  st.rerun()
278
 
279
+ with nav_cols[2]:
 
280
  question_options = [f"Question {i+1}" for i in range(total_questions)]
281
  selected_q = st.selectbox(
282
+ "Jump to:",
283
  question_options,
284
+ index=st.session_state.current_question
 
285
  )
286
 
 
287
  new_index = question_options.index(selected_q)
288
  if new_index != st.session_state.current_question:
289
  st.session_state.current_question = new_index
 
290
  new_page = new_index // questions_per_page
291
  if new_page != st.session_state.navigator_page:
292
  st.session_state.navigator_page = new_page
293
  st.rerun()
294
 
295
+ with nav_cols[3]:
296
  if st.button("Next ➡️", disabled=st.session_state.current_question >= total_questions - 1):
297
  st.session_state.current_question += 1
 
298
  new_page = st.session_state.current_question // questions_per_page
299
  if new_page != st.session_state.navigator_page:
300
  st.session_state.navigator_page = new_page
301
  st.rerun()
302
 
303
+ with nav_cols[4]:
304
  if st.button("Last ⏭️", disabled=st.session_state.current_question >= total_questions - 1):
305
  st.session_state.current_question = total_questions - 1
 
306
  st.session_state.navigator_page = total_nav_pages - 1
307
  st.rerun()
308
 
309
+ # Progress bar
310
  progress = (st.session_state.current_question + 1) / total_questions
311
  st.progress(progress)
312
+ st.caption(f"Progress: {st.session_state.current_question + 1}/{total_questions} ({progress:.1%})")
313
 
314
  st.markdown("---")
315
 
 
317
  if 0 <= st.session_state.current_question < len(df):
318
  current_row = df.iloc[st.session_state.current_question]
319
  render_question(st.session_state.current_question, current_row)
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ # Sample data section
322
  with st.expander("📋 Sample Excel Format"):
323
  sample_data = pd.DataFrame({
324
  "question_latex": [
 
334
  })
335
  st.dataframe(sample_data)
336
 
 
337
  csv = sample_data.to_csv(index=False)
338
  st.download_button(
339
  label="📥 Download Sample CSV",
340
  data=csv,
341
  file_name="sample_mcq_format.csv",
342
  mime="text/csv"
343
+ )