laichai commited on
Commit
883a128
Β·
verified Β·
1 Parent(s): a30c001

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +154 -99
app.py CHANGED
@@ -2,14 +2,8 @@ import streamlit as st
2
  import os
3
  import subprocess
4
  from streamlit_pdf_viewer import pdf_viewer
5
- import shutil
6
 
7
- # Add this right after your imports
8
- if not shutil.which("pdflatex"):
9
- st.error("❌ LaTeX compiler not found!")
10
- st.info("If you are on HuggingFace/Streamlit Cloud, make sure 'packages.txt' is in the root directory and contains 'texlive-latex-base'.")
11
- st.stop()
12
-
13
  # --- Configuration ---
14
  TOPICS_DIR = "topics"
15
  TEMP_DIR = "temp_build"
@@ -18,88 +12,102 @@ os.makedirs(TEMP_DIR, exist_ok=True)
18
  st.set_page_config(page_title="Physics Topics", layout="wide")
19
 
20
  # ==========================================
21
- # πŸ”’ AUTHENTICATION LOGIC
22
  # ==========================================
23
  def check_login():
24
  """
25
- Verifies the password against st.secrets.
26
- Returns True if logged in, False otherwise.
27
  """
28
- # 1. Initialize session state for authentication
29
- if "authenticated" not in st.session_state:
30
- st.session_state.authenticated = False
31
 
32
- # 2. If already logged in, return True immediately
33
- if st.session_state.authenticated:
34
- return True
35
 
36
- # 3. If not logged in, show the login form
37
- st.title("πŸ”’ Login Required")
38
 
39
- # We use a form so the user can hit 'Enter' to submit
40
  with st.form("login_form"):
41
- password_input = st.text_input("Enter Password", type="password")
42
  submit_button = st.form_submit_button("Login")
43
 
44
  if submit_button:
45
- # DIRECT COMPARISON (No Hash as requested)
46
- if password_input == st.secrets["app_password"]:
47
- st.session_state.authenticated = True
48
- st.rerun() # Rerun the app to remove the login form
 
 
 
 
49
  else:
50
- st.error("❌ Incorrect Password")
51
 
52
- return False
53
 
54
- # STOP THE APP HERE IF NOT LOGGED IN
55
- if not check_login():
56
- st.stop() # This prevents the rest of the code from running
 
57
 
58
  # ==========================================
59
- # πŸš€ MAIN APPLICATION
60
- # (Only runs if authenticated)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  # ==========================================
62
 
63
- # --- Helper Functions ---
64
  def get_topics():
65
  if not os.path.exists(TOPICS_DIR): return []
66
  return sorted([d for d in os.listdir(TOPICS_DIR) if os.path.isdir(os.path.join(TOPICS_DIR, d))])
67
 
68
- def get_tex_files(topic):
69
  topic_path = os.path.join(TOPICS_DIR, topic)
70
- return sorted([f for f in os.listdir(topic_path) if f.endswith(".tex")])
71
 
72
  def compile_latex(file_path):
73
  file_name = os.path.basename(file_path)
74
  job_name = os.path.splitext(file_name)[0]
75
-
76
  try:
77
  process = subprocess.run(
78
- [
79
- "pdflatex",
80
- "-interaction=nonstopmode",
81
- f"-output-directory={TEMP_DIR}",
82
- f"-jobname={job_name}",
83
- file_path
84
- ],
85
- stdout=subprocess.PIPE,
86
- stderr=subprocess.PIPE,
87
- text=True
88
  )
89
  pdf_path = os.path.join(TEMP_DIR, f"{job_name}.pdf")
90
  if process.returncode == 0 and os.path.exists(pdf_path):
91
  return pdf_path, None
92
- else:
93
- return None, process.stdout
94
  except Exception as e:
95
  return None, str(e)
96
 
97
- # --- Sidebar Navigation ---
98
- st.sidebar.title("Physics Archive")
99
 
100
- # Logout Button (Optional but useful)
101
  if st.sidebar.button("Logout", type="secondary"):
102
- st.session_state.authenticated = False
103
  st.rerun()
104
 
105
  topics = get_topics()
@@ -107,67 +115,114 @@ if not topics:
107
  st.sidebar.error("No topics found.")
108
  st.stop()
109
 
110
- # 1. Topic Selection
111
  selected_topic = st.sidebar.selectbox("Select Topic", topics)
112
  st.sidebar.markdown("---")
 
113
 
114
- # 2. Document Selection
115
- tex_files = get_tex_files(selected_topic)
116
- if not tex_files:
117
- st.sidebar.warning("No files in this topic.")
118
  st.stop()
119
 
120
- selected_file = st.sidebar.radio(f"Documents in {selected_topic}", tex_files)
121
 
 
 
 
 
 
122
 
123
- # --- Auto-Compilation & Logic ---
124
- source_path = os.path.join(TOPICS_DIR, selected_topic, selected_file)
 
 
125
 
126
- if "last_compiled_file" not in st.session_state:
127
- st.session_state.last_compiled_file = None
128
- if "compilation_error" not in st.session_state:
129
- st.session_state.compilation_error = None
130
 
131
- if st.session_state.last_compiled_file != source_path:
132
- with st.spinner(f"Rendering {selected_file}..."):
133
- pdf_path, error_log = compile_latex(source_path)
 
 
 
 
134
 
135
- if pdf_path:
136
- st.session_state.current_pdf_path = pdf_path
137
- st.session_state.last_compiled_file = source_path
138
- st.session_state.compilation_error = None
139
- else:
140
- st.session_state.current_pdf_path = None
141
- st.session_state.compilation_error = error_log
142
-
143
- # --- Main View ---
144
- tab_view, tab_code = st.tabs(["πŸ“„ Document Viewer", "πŸ“ Source Code"])
145
-
 
 
 
 
 
146
  with tab_view:
147
- if st.session_state.get("current_pdf_path") and os.path.exists(st.session_state.current_pdf_path):
148
-
149
  col1, col2 = st.columns([6, 1])
150
- with col1:
151
- st.success(f"**{selected_file}** rendered successfully.")
152
  with col2:
153
- with open(st.session_state.current_pdf_path, "rb") as f:
154
- st.download_button(
155
- label="⬇️ Download PDF",
156
- data=f,
157
- file_name=selected_file.replace('.tex', '.pdf'),
158
- mime="application/pdf",
159
- type="primary"
160
- )
161
 
162
  st.markdown("---")
163
- pdf_viewer(st.session_state.current_pdf_path, width=800, height=1000)
164
-
165
- elif st.session_state.get("compilation_error"):
166
  st.error("⚠️ Compilation Failed")
167
- with st.expander("View Error Log", expanded=True):
168
- st.code(st.session_state.compilation_error, language="text")
169
-
170
- with tab_code:
171
- st.caption(f"File: {source_path}")
172
- with open(source_path, "r", encoding="utf-8") as f:
173
- st.code(f.read(), language="latex")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import os
3
  import subprocess
4
  from streamlit_pdf_viewer import pdf_viewer
5
+ from github import Github
6
 
 
 
 
 
 
 
7
  # --- Configuration ---
8
  TOPICS_DIR = "topics"
9
  TEMP_DIR = "temp_build"
 
12
  st.set_page_config(page_title="Physics Topics", layout="wide")
13
 
14
  # ==========================================
15
+ # πŸ”’ AUTHENTICATION LOGIC (DUAL LEVEL)
16
  # ==========================================
17
  def check_login():
18
  """
19
+ Returns: 'admin', 'viewer', or None
 
20
  """
21
+ if "user_role" not in st.session_state:
22
+ st.session_state.user_role = None
 
23
 
24
+ if st.session_state.user_role:
25
+ return st.session_state.user_role
 
26
 
27
+ st.title("πŸ”’ Access Restricted")
 
28
 
 
29
  with st.form("login_form"):
30
+ password_input = st.text_input("Enter Access Password", type="password")
31
  submit_button = st.form_submit_button("Login")
32
 
33
  if submit_button:
34
+ if password_input == st.secrets["admin_password"]:
35
+ st.session_state.user_role = "admin"
36
+ st.success("Logged in as Administrator")
37
+ st.rerun()
38
+ elif password_input == st.secrets["viewer_password"]:
39
+ st.session_state.user_role = "viewer"
40
+ st.success("Logged in as Viewer")
41
+ st.rerun()
42
  else:
43
+ st.error("❌ Invalid Password")
44
 
45
+ return None
46
 
47
+ # STOP IF NOT LOGGED IN
48
+ current_role = check_login()
49
+ if not current_role:
50
+ st.stop()
51
 
52
  # ==========================================
53
+ # πŸ™ GITHUB SYNC FUNCTIONS
54
+ # ==========================================
55
+ def push_to_github(local_path, content, commit_message):
56
+ """
57
+ Updates the file on GitHub.
58
+ local_path: 'topics/Topic1/file.tex'
59
+ """
60
+ try:
61
+ g = Github(st.secrets["github_token"])
62
+ repo = g.get_repo(st.secrets["github_repo"])
63
+
64
+ # Get the file from the repo to retrieve its SHA (needed for update)
65
+ contents = repo.get_contents(local_path, ref=st.secrets["github_branch"])
66
+
67
+ # Update the file
68
+ repo.update_file(
69
+ path=contents.path,
70
+ message=commit_message,
71
+ content=content,
72
+ sha=contents.sha,
73
+ branch=st.secrets["github_branch"]
74
+ )
75
+ return True, "Successfully pushed to GitHub!"
76
+ except Exception as e:
77
+ return False, f"GitHub Error: {str(e)}"
78
+
79
+ # ==========================================
80
+ # πŸš€ MAIN APP LOGIC
81
  # ==========================================
82
 
 
83
  def get_topics():
84
  if not os.path.exists(TOPICS_DIR): return []
85
  return sorted([d for d in os.listdir(TOPICS_DIR) if os.path.isdir(os.path.join(TOPICS_DIR, d))])
86
 
87
+ def get_topic_files(topic):
88
  topic_path = os.path.join(TOPICS_DIR, topic)
89
+ return sorted([f for f in os.listdir(topic_path) if f.lower().endswith(('.tex', '.pdf'))])
90
 
91
  def compile_latex(file_path):
92
  file_name = os.path.basename(file_path)
93
  job_name = os.path.splitext(file_name)[0]
 
94
  try:
95
  process = subprocess.run(
96
+ ["pdflatex", "-interaction=nonstopmode", f"-output-directory={TEMP_DIR}", f"-jobname={job_name}", file_path],
97
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
 
 
 
 
 
 
 
 
98
  )
99
  pdf_path = os.path.join(TEMP_DIR, f"{job_name}.pdf")
100
  if process.returncode == 0 and os.path.exists(pdf_path):
101
  return pdf_path, None
102
+ return None, process.stdout
 
103
  except Exception as e:
104
  return None, str(e)
105
 
106
+ # --- SIDEBAR ---
107
+ st.sidebar.title(f"Physics Archive ({current_role.title()})")
108
 
 
109
  if st.sidebar.button("Logout", type="secondary"):
110
+ st.session_state.user_role = None
111
  st.rerun()
112
 
113
  topics = get_topics()
 
115
  st.sidebar.error("No topics found.")
116
  st.stop()
117
 
 
118
  selected_topic = st.sidebar.selectbox("Select Topic", topics)
119
  st.sidebar.markdown("---")
120
+ files = get_topic_files(selected_topic)
121
 
122
+ if not files:
123
+ st.sidebar.warning("No files found.")
 
 
124
  st.stop()
125
 
126
+ selected_file = st.sidebar.radio(f"Documents in {selected_topic}", files)
127
 
128
+ # --- FILE PATHS ---
129
+ # Relative path (for GitHub): "topics/Topic1/File.tex"
130
+ rel_path = os.path.join(TOPICS_DIR, selected_topic, selected_file).replace("\\", "/")
131
+ # Absolute path (for Python/OS):
132
+ abs_path = os.path.abspath(rel_path)
133
 
134
+ # --- COMPILATION LOGIC (VIEWER & ADMIN) ---
135
+ # We track if a file was just saved to trigger a re-compile
136
+ if "force_recompile" not in st.session_state:
137
+ st.session_state.force_recompile = False
138
 
139
+ if "last_processed" not in st.session_state:
140
+ st.session_state.last_processed = None
 
 
141
 
142
+ # If file changed OR we just saved an edit:
143
+ if st.session_state.last_processed != rel_path or st.session_state.force_recompile:
144
+
145
+ # 1. PDF Files
146
+ if selected_file.lower().endswith(".pdf"):
147
+ st.session_state.current_pdf = abs_path
148
+ st.session_state.compilation_error = None
149
 
150
+ # 2. LaTeX Files
151
+ elif selected_file.lower().endswith(".tex"):
152
+ with st.spinner(f"Compiling {selected_file}..."):
153
+ pdf, log = compile_latex(abs_path)
154
+ st.session_state.current_pdf = pdf
155
+ st.session_state.compilation_error = log
156
+
157
+ st.session_state.last_processed = rel_path
158
+ st.session_state.force_recompile = False
159
+
160
+ # --- TABS ---
161
+ # Admin gets "Edit Source", Viewer gets "Source Code"
162
+ tab_label = "✏️ Edit Source" if current_role == 'admin' else "πŸ“ Source Code"
163
+ tab_view, tab_edit = st.tabs(["πŸ“„ Document Viewer", tab_label])
164
+
165
+ # 1. VIEWER TAB
166
  with tab_view:
167
+ if st.session_state.current_pdf and os.path.exists(st.session_state.current_pdf):
 
168
  col1, col2 = st.columns([6, 1])
169
+ with col1: st.success(f"**{selected_file}** loaded.")
 
170
  with col2:
171
+ with open(st.session_state.current_pdf, "rb") as f:
172
+ st.download_button("⬇️ PDF", f, file_name=selected_file.replace('.tex','.pdf'), mime="application/pdf", type="primary")
 
 
 
 
 
 
173
 
174
  st.markdown("---")
175
+ pdf_viewer(st.session_state.current_pdf, width=800, height=1000)
176
+ elif st.session_state.compilation_error:
 
177
  st.error("⚠️ Compilation Failed")
178
+ with st.expander("Error Log"):
179
+ st.code(st.session_state.compilation_error)
180
+
181
+ # 2. EDIT / SOURCE TAB
182
+ with tab_edit:
183
+ if selected_file.lower().endswith(".pdf"):
184
+ st.info("PDF files cannot be edited directly.")
185
+ else:
186
+ # Read current content
187
+ with open(abs_path, "r", encoding="utf-8") as f:
188
+ file_content = f.read()
189
+
190
+ if current_role == 'admin':
191
+ st.warning(f"⚠️ You are editing: {selected_file}")
192
+
193
+ # The Text Editor
194
+ new_content = st.text_area(
195
+ "LaTeX Source",
196
+ value=file_content,
197
+ height=600,
198
+ key="editor_area"
199
+ )
200
+
201
+ # SAVE BUTTON
202
+ if st.button("πŸ’Ύ Save to GitHub & Re-Render", type="primary"):
203
+ if new_content != file_content:
204
+ # 1. Save locally (so valid for compilation immediately)
205
+ with open(abs_path, "w", encoding="utf-8") as f:
206
+ f.write(new_content)
207
+
208
+ # 2. Push to GitHub
209
+ with st.spinner("Pushing to GitHub..."):
210
+ success, msg = push_to_github(
211
+ rel_path,
212
+ new_content,
213
+ f"Update {selected_file} via Streamlit Admin"
214
+ )
215
+
216
+ if success:
217
+ st.success(msg)
218
+ # Trigger re-compile on next run
219
+ st.session_state.force_recompile = True
220
+ st.rerun()
221
+ else:
222
+ st.error(msg)
223
+ else:
224
+ st.info("No changes detected.")
225
+ else:
226
+ # Viewer Mode (Read Only)
227
+ st.caption(f"Path: {rel_path}")
228
+ st.code(file_content, language="latex")