File size: 13,930 Bytes
9923316
e1a7d71
a49f2d6
 
9923316
 
 
 
 
 
 
 
a49f2d6
9923316
 
 
 
 
a49f2d6
 
 
 
9923316
a49f2d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9923316
 
a49f2d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9923316
 
a49f2d6
9923316
a49f2d6
9923316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a49f2d6
 
 
 
9923316
 
a49f2d6
9923316
a49f2d6
9923316
a49f2d6
 
 
 
 
9923316
a49f2d6
 
 
9923316
 
a49f2d6
 
 
9923316
a49f2d6
9923316
a49f2d6
9923316
 
a49f2d6
 
 
9923316
 
a49f2d6
 
 
9923316
a49f2d6
 
9923316
a49f2d6
9923316
 
a49f2d6
 
 
9923316
 
a49f2d6
 
 
 
 
9923316
a49f2d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9923316
 
a49f2d6
9923316
a49f2d6
 
 
 
 
9923316
a49f2d6
 
 
 
 
 
 
9923316
a49f2d6
 
9923316
a49f2d6
9923316
a49f2d6
 
 
 
 
 
 
 
 
 
9923316
a49f2d6
 
 
 
 
 
 
9923316
a49f2d6
 
 
 
 
9923316
a49f2d6
 
9923316
a49f2d6
 
 
 
 
 
 
e1a7d71
9923316
a49f2d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9923316
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

import streamlit as st
import pandas as pd
import re
import io
import tempfile
import os

# Configure Streamlit to prevent permission issues
os.environ['STREAMLIT_SERVER_HEADLESS'] = 'true'
os.environ['STREAMLIT_SERVER_ENABLE_CORS'] = 'false'
os.environ['STREAMLIT_SERVER_ENABLE_XSRF_PROTECTION'] = 'false'

st.set_page_config(
    page_title="๐Ÿงช LaTeX MCQs", 
    layout="wide",
    initial_sidebar_state="collapsed"
)
st.title("๐Ÿงช Chemistry MCQs with LaTeX Rendering")

# Helper to safely convert mixed text + LaTeX into proper LaTeX
def format_for_latex(text):
    """Convert mixed text and LaTeX expressions into properly formatted LaTeX."""
    if pd.isna(text) or text == "":
        return ""
    
    text = str(text).strip()
    
    # Remove existing \( \) or $ $ wrappers if any
    text = re.sub(r'\\\(|\\\)', '', text)
    text = re.sub(r'\$(.*?)\$', r'\1', text)
    
    # Handle common chemistry and math patterns
    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[^{]*(?:\{[^}]*\})*)'
    
    parts = re.split(f'({latex_commands})', text)
    
    formatted = ""
    for part in parts:
        if not part:
            continue
            
        if re.match(r'\\[a-zA-Z]+', part):
            formatted += part
        else:
            if part.strip():
                part = re.sub(r'([A-Za-z])(\d+)', r'\1_{\2}', part)
                part = re.sub(r'\^(\d+)', r'^{\1}', part)
                
                if not re.search(r'[{}\\]', part):
                    formatted += f"\\text{{{part}}}"
                else:
                    formatted += part
    
    return formatted

def validate_dataframe(df):
    """Validate the uploaded dataframe structure."""
    required_columns = ["question_latex", "option_1", "option_2", "option_3", "option_4"]
    missing_columns = [col for col in required_columns if col not in df.columns]
    
    if missing_columns:
        return False, f"Missing required columns: {missing_columns}"
    
    empty_questions = df["question_latex"].isna().sum()
    if empty_questions > 0:
        st.warning(f"โš ๏ธ Found {empty_questions} empty questions that will be skipped.")
    
    return True, "Valid"

def render_question(idx, row):
    """Render a single question with its options."""
    if pd.isna(row["question_latex"]) or str(row["question_latex"]).strip() == "":
        return
    
    st.markdown(f"### Question {idx+1}")
    
    # Render question
    try:
        question_latex = format_for_latex(row["question_latex"])
        if question_latex:
            st.latex(question_latex)
        else:
            st.markdown(f"**{row['question_latex']}**")
    except Exception as e:
        st.error(f"Error rendering question {idx+1}: {e}")
        st.markdown(f"**{row['question_latex']}**")
    
    # Render options
    options = {
        "A": row["option_1"],
        "B": row["option_2"],
        "C": row["option_3"],
        "D": row["option_4"]
    }
    
    col1, col2 = st.columns(2)
    
    for i, (key, value) in enumerate(options.items()):
        with col1 if i % 2 == 0 else col2:
            st.markdown(f"**{key}.**")
            try:
                option_latex = format_for_latex(value)
                if option_latex:
                    st.latex(option_latex)
                else:
                    st.markdown(f"{value}")
            except Exception as e:
                st.warning(f"Error rendering option {key}: {e}")
                st.markdown(f"{value}")
    
    st.markdown("---")

def safe_read_excel(uploaded_file):
    """Safely read Excel file with multiple fallback methods."""
    try:
        # Method 1: Direct reading
        df = pd.read_excel(uploaded_file)
        return df, None
    except Exception as e1:
        try:
            # Method 2: BytesIO
            bytes_data = uploaded_file.getvalue()
            df = pd.read_excel(io.BytesIO(bytes_data))
            return df, None
        except Exception as e2:
            try:
                # Method 3: Temporary file in /tmp (writable in containers)
                with tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx', dir='/tmp') as tmp_file:
                    tmp_file.write(uploaded_file.getbuffer())
                    tmp_file.flush()
                    
                    df = pd.read_excel(tmp_file.name)
                    
                    # Cleanup
                    try:
                        os.unlink(tmp_file.name)
                    except:
                        pass
                    
                    return df, None
            except Exception as e3:
                error_msg = f"All methods failed - Method 1: {str(e1)[:100]}, Method 2: {str(e2)[:100]}, Method 3: {str(e3)[:100]}"
                return None, error_msg

# Main app
st.markdown("""
### ๐Ÿ“‹ Instructions
1. Upload an Excel file (.xlsx or .xls) with your questions
2. Make sure your file has the required columns (see sample format below)
3. Navigate through questions using the controls
""")

uploaded_file = st.file_uploader(
    "๐Ÿ“ฅ Upload Excel file with LaTeX questions", 
    type=["xlsx", "xls"],
    help="Upload an Excel file containing your MCQ questions with LaTeX formatting"
)

if uploaded_file is not None:
    st.info(f"๐Ÿ“„ Processing: {uploaded_file.name} ({uploaded_file.size} bytes)")
    
    with st.spinner("๐Ÿ”„ Reading Excel file..."):
        df, error_msg = safe_read_excel(uploaded_file)
    
    if df is None:
        st.error(f"โŒ Error: {error_msg}")
        st.markdown("### Troubleshooting:")
        st.markdown("""
        - Ensure file is valid Excel format (.xlsx/.xls)
        - Check file isn't corrupted or password-protected
        - Try re-uploading the file
        - Verify all required columns exist
        """)
    else:
        is_valid, message = validate_dataframe(df)
        
        if not is_valid:
            st.error(f"โŒ {message}")
            st.markdown("### Required Columns:")
            st.code("question_latex, option_1, option_2, option_3, option_4")
        else:
            st.success(f"โœ… Loaded {len(df)} questions successfully!")
            
            # Initialize session state
            if 'current_question' not in st.session_state:
                st.session_state.current_question = 0
            if 'navigator_page' not in st.session_state:
                st.session_state.navigator_page = 0
            
            # Navigation system
            st.markdown("### ๐Ÿ“‹ Question Navigator")
            
            total_questions = len(df)
            questions_per_page = 50
            questions_per_row = 10
            
            total_nav_pages = (total_questions + questions_per_page - 1) // questions_per_page
            
            # Navigator pagination (if needed)
            if total_questions > questions_per_page:
                nav_cols = st.columns([1, 1, 2, 1, 1])
                
                with nav_cols[0]:
                    if st.button("โช First Set", disabled=st.session_state.navigator_page == 0):
                        st.session_state.navigator_page = 0
                        st.rerun()
                
                with nav_cols[1]:
                    if st.button("โ—€๏ธ Prev Set", disabled=st.session_state.navigator_page == 0):
                        st.session_state.navigator_page -= 1
                        st.rerun()
                
                with nav_cols[2]:
                    start_q = st.session_state.navigator_page * questions_per_page + 1
                    end_q = min((st.session_state.navigator_page + 1) * questions_per_page, total_questions)
                    st.markdown(f"**Questions {start_q}-{end_q} of {total_questions}**")
                
                with nav_cols[3]:
                    if st.button("Next Set โ–ถ๏ธ", disabled=st.session_state.navigator_page >= total_nav_pages - 1):
                        st.session_state.navigator_page += 1
                        st.rerun()
                
                with nav_cols[4]:
                    if st.button("Last Set โฉ", disabled=st.session_state.navigator_page >= total_nav_pages - 1):
                        st.session_state.navigator_page = total_nav_pages - 1
                        st.rerun()
                
                st.markdown("---")
            
            # Question number buttons
            start_idx = st.session_state.navigator_page * questions_per_page
            end_idx = min(start_idx + questions_per_page, total_questions)
            
            questions_to_show = end_idx - start_idx
            rows_needed = (questions_to_show + questions_per_row - 1) // questions_per_row
            
            for row in range(rows_needed):
                row_start_idx = start_idx + (row * questions_per_row)
                row_end_idx = min(row_start_idx + questions_per_row, end_idx)
                
                cols = st.columns(row_end_idx - row_start_idx)
                
                for i, col in enumerate(cols):
                    question_idx = row_start_idx + i
                    question_num = question_idx + 1
                    
                    with col:
                        if question_idx == st.session_state.current_question:
                            button_label = f"**Q{question_num}**"
                            button_type = "primary"
                        else:
                            button_label = f"Q{question_num}"
                            button_type = "secondary"
                        
                        if st.button(button_label, key=f"q_btn_{question_idx}", type=button_type):
                            st.session_state.current_question = question_idx
                            st.rerun()
            
            st.markdown("---")
            
            # Main navigation controls
            nav_cols = st.columns([1, 1, 2, 1, 1])
            
            with nav_cols[0]:
                if st.button("โฎ๏ธ First", disabled=st.session_state.current_question == 0):
                    st.session_state.current_question = 0
                    st.session_state.navigator_page = 0
                    st.rerun()
            
            with nav_cols[1]:
                if st.button("โฌ…๏ธ Previous", disabled=st.session_state.current_question == 0):
                    st.session_state.current_question -= 1
                    new_page = st.session_state.current_question // questions_per_page
                    if new_page != st.session_state.navigator_page:
                        st.session_state.navigator_page = new_page
                    st.rerun()
            
            with nav_cols[2]:
                question_options = [f"Question {i+1}" for i in range(total_questions)]
                selected_q = st.selectbox(
                    "Jump to:",
                    question_options,
                    index=st.session_state.current_question
                )
                
                new_index = question_options.index(selected_q)
                if new_index != st.session_state.current_question:
                    st.session_state.current_question = new_index
                    new_page = new_index // questions_per_page
                    if new_page != st.session_state.navigator_page:
                        st.session_state.navigator_page = new_page
                    st.rerun()
            
            with nav_cols[3]:
                if st.button("Next โžก๏ธ", disabled=st.session_state.current_question >= total_questions - 1):
                    st.session_state.current_question += 1
                    new_page = st.session_state.current_question // questions_per_page
                    if new_page != st.session_state.navigator_page:
                        st.session_state.navigator_page = new_page
                    st.rerun()
            
            with nav_cols[4]:
                if st.button("Last โญ๏ธ", disabled=st.session_state.current_question >= total_questions - 1):
                    st.session_state.current_question = total_questions - 1
                    st.session_state.navigator_page = total_nav_pages - 1
                    st.rerun()
            
            # Progress bar
            progress = (st.session_state.current_question + 1) / total_questions
            st.progress(progress)
            st.caption(f"Progress: {st.session_state.current_question + 1}/{total_questions} ({progress:.1%})")
            
            st.markdown("---")
            
            # Render current question
            if 0 <= st.session_state.current_question < len(df):
                current_row = df.iloc[st.session_state.current_question]
                render_question(st.session_state.current_question, current_row)

# Sample data section
with st.expander("๐Ÿ“‹ Sample Excel Format"):
    sample_data = pd.DataFrame({
        "question_latex": [
            "What is the molecular formula for water?",
            "Calculate the pH of a 0.1 M HCl solution. Use \\text{pH} = -\\log[\\text{H}^+]",
            "Balance the equation: \\ce{C2H6 + O2 -> CO2 + H2O}"
        ],
        "option_1": ["H2O", "1", "\\ce{2C2H6 + 7O2 -> 4CO2 + 6H2O}"],
        "option_2": ["HO2", "0.1", "\\ce{C2H6 + O2 -> CO2 + H2O}"],
        "option_3": ["H2O2", "-1", "\\ce{C2H6 + 3O2 -> 2CO2 + 3H2O}"],
        "option_4": ["OH2", "14", "\\ce{2C2H6 + 5O2 -> 4CO2 + 2H2O}"],
        "correct_answer": ["A", "A", "A"]
    })
    st.dataframe(sample_data)
    
    csv = sample_data.to_csv(index=False)
    st.download_button(
        label="๐Ÿ“ฅ Download Sample CSV",
        data=csv,
        file_name="sample_mcq_format.csv",
        mime="text/csv"
    )