awacke1 commited on
Commit
2783bba
·
verified ·
1 Parent(s): 0a0dfe3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -166
app.py CHANGED
@@ -1,222 +1,278 @@
1
- import streamlit as st # 🌐 Streamlit magic
2
- import streamlit.components.v1 as components # 🖼️ Embed custom HTML/JS
3
- import os # 📂 File operations
4
- import json # 🔄 JSON encoding/decoding
5
- import pandas as pd # 📊 DataFrame handling
6
- import uuid # 🆔 Unique IDs
7
- import math # ➗ Math utils
8
- import time # ⏳ Time utilities
9
-
10
- from gamestate import GameState # 💼 Shared game‐state singleton
11
-
12
- # 🚀 Page setup
 
 
13
  st.set_page_config(page_title="Infinite World Builder", layout="wide")
14
 
15
- # 📏 Constants for world dimensions & CSV schema
16
  SAVE_DIR = "saved_worlds"
17
- PLOT_WIDTH = 50.0 # ↔️ Plot width in world units
18
- PLOT_DEPTH = 50.0 # ↕️ Plot depth in world units
19
- CSV_COLUMNS = [
20
- 'obj_id', 'type',
21
- 'pos_x', 'pos_y', 'pos_z',
22
- 'rot_x', 'rot_y', 'rot_z', 'rot_order'
23
- ]
24
-
25
- # 🗂️ Ensure directory for plots exists
26
  os.makedirs(SAVE_DIR, exist_ok=True)
27
 
28
- @st.cache_data(ttl=3600) # 🕒 Cache for 1h
29
  def load_plot_metadata():
30
- """Scan SAVE_DIR for existing plot CSVs and parse their grid coords."""
 
31
  try:
32
- files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
33
  except FileNotFoundError:
 
 
 
 
34
  return []
35
- parsed = []
36
- for fn in files:
37
- parts = fn[:-4].split('_')
38
  try:
39
- gx = int(parts[1][1:])
40
- gz = int(parts[2][1:])
41
- name = "_".join(parts[3:]) if len(parts) > 3 else f"Plot({gx},{gz})"
42
- parsed.append({
43
- 'id': fn[:-4],
44
- 'filename': fn,
45
- 'grid_x': gx,
46
- 'grid_z': gz,
47
- 'name': name,
48
- 'x_offset': gx * PLOT_WIDTH,
49
- 'z_offset': gz * PLOT_DEPTH
 
50
  })
51
- except:
 
52
  continue
53
- parsed.sort(key=lambda p: (p['grid_x'], p['grid_z']))
54
- return parsed
 
55
 
56
  def load_plot_objects(filename, x_offset, z_offset):
57
- """Read a plot CSV and shift each object's position by the plot offset."""
58
- path = os.path.join(SAVE_DIR, filename)
 
59
  try:
60
- df = pd.read_csv(path)
61
- except (FileNotFoundError, pd.errors.EmptyDataError):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  return []
63
- # ensure columns
64
- for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
65
- if col not in df.columns:
66
- df[col] = default
67
- if 'obj_id' not in df.columns:
68
- df['obj_id'] = [str(uuid.uuid4()) for _ in df.index]
69
- objs = []
70
- for row in df.to_dict(orient='records'):
71
- objs.append({
72
- 'obj_id': row['obj_id'],
73
- 'type': row['type'],
74
- 'pos_x': row['pos_x'] + x_offset,
75
- 'pos_y': row['pos_y'],
76
- 'pos_z': row['pos_z'] + z_offset,
77
- 'rot_x': row['rot_x'],
78
- 'rot_y': row['rot_y'],
79
- 'rot_z': row['rot_z'],
80
- 'rot_order': row['rot_order']
81
- })
82
- return objs
83
-
84
- def save_plot_data(filename, objects_list, px, pz):
85
- """Save newly placed objects back to the corresponding plot CSV."""
86
- path = os.path.join(SAVE_DIR, filename)
87
- records = []
88
- for o in objects_list:
89
- pos = o.get('position', {})
90
- rot = o.get('rotation', {})
91
- if 'x' not in pos or 'y' not in pos or 'z' not in pos:
92
  continue
93
- records.append({
94
- 'obj_id': o.get('obj_id', str(uuid.uuid4())),
95
- 'type': o.get('type', 'Unknown'),
96
- 'pos_x': pos['x'] - px,
97
- 'pos_y': pos['y'],
98
- 'pos_z': pos['z'] - pz,
99
- 'rot_x': rot.get('_x', 0.0),
100
- 'rot_y': rot.get('_y', 0.0),
101
- 'rot_z': rot.get('_z', 0.0),
102
- 'rot_order': rot.get('_order', 'XYZ')
103
- })
104
  try:
105
- pd.DataFrame(records, columns=CSV_COLUMNS).to_csv(path, index=False)
106
- st.success(f"🎉 Saved {len(records)} objects to {filename}")
 
107
  return True
108
  except Exception as e:
109
- st.error(f"Save failed: {e}")
110
  return False
111
 
112
- # 🔒 Singleton for global world state
113
  @st.cache_resource
114
  def get_game_state():
 
115
  return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
116
 
117
  game_state = get_game_state()
118
 
119
- # 🧠 Session state defaults
120
- st.session_state.setdefault('selected_object', 'None')
121
- st.session_state.setdefault('js_save_processor', None)
 
 
 
 
122
 
123
- # 🔄 Load all saved plots + objects
124
  plots_metadata = load_plot_metadata()
125
  all_initial_objects = []
126
- for p in plots_metadata:
127
- all_initial_objects += load_plot_objects(p['filename'], p['x_offset'], p['z_offset'])
128
 
129
- # 🖥️ Sidebar UI
130
  with st.sidebar:
131
  st.title("🏗️ World Controls")
132
- st.header("📍 Navigate Plots")
133
- cols = st.columns(2)
134
- for idx, p in enumerate(plots_metadata):
135
- label = f"➡️ {p['name']} ({p['grid_x']},{p['grid_z']})"
136
- if cols[idx % 2].button(label, key=f"nav_{p['id']}"):
137
- from streamlit_js_eval import streamlit_js_eval
138
- js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});"
 
 
 
 
139
  try:
140
- streamlit_js_eval(js_code=js, key=f"tp_{p['id']}")
 
 
141
  except Exception as e:
142
- st.error(f"Teleport failed: {e}")
 
143
 
144
  st.markdown("---")
145
- st.header("🌲 Place Objects")
146
-
147
- # ←─── four new builder-tool options added here ────→
148
- options = [
149
- "None",
150
- "Simple House",
151
- "Tree",
152
- "Rock",
153
- "Fence Post",
154
- "Cyberpunk City Builder Kit",
155
- "POLYGON - Fantasy Kingdom Pack",
156
- "HEROIC FANTASY CREATURES FULL PACK VOL 1",
157
- "POLYGON - Apocalypse Pack"
158
- ]
159
- sel = st.selectbox("Select:", options,
160
- index=options.index(st.session_state.selected_object)
161
- if st.session_state.selected_object in options else 0,
162
- key="selected_object_widget"
163
- )
164
- if sel != st.session_state.selected_object:
165
- st.session_state.selected_object = sel
166
 
167
  st.markdown("---")
168
- st.header("💾 Save Work")
169
- if st.button("💾 Save Current Work"):
 
170
  from streamlit_js_eval import streamlit_js_eval
171
- streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor")
172
- st.experimental_rerun()
 
173
 
174
- # 📨 Handle incoming save-data callback
175
- raw = st.session_state.get("js_save_processor")
176
- if raw:
 
 
177
  try:
178
- payload = json.loads(raw) if isinstance(raw, str) else raw
179
- pos = payload.get('playerPosition')
180
- objs = payload.get('objectsToSave', [])
181
- gx = math.floor(pos['x'] / PLOT_WIDTH)
182
- gz = math.floor(pos['z'] / PLOT_DEPTH)
183
- fn = f"plot_X{gx}_Z{gz}.csv"
184
- if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH):
185
- load_plot_metadata.clear()
186
- st.session_state.js_save_processor = None
187
- st.experimental_rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  except Exception as e:
189
- st.error(f"Error saving data: {e}")
 
 
 
190
 
191
- # 🏠 Main view & HTML injection
192
- st.header("🌍 Infinite Shared 3D World")
193
- st.caption("➡️ Explore, click to build, 💾 to save!")
194
 
195
- state = {
 
196
  "ALL_INITIAL_OBJECTS": all_initial_objects,
197
- "PLOTS_METADATA": plots_metadata,
198
  "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
199
- "PLOT_WIDTH": PLOT_WIDTH,
200
- "PLOT_DEPTH": PLOT_DEPTH,
201
- "GAME_STATE": game_state.get_state()
202
  }
203
 
 
 
204
  try:
205
- with open('index.html', 'r', encoding='utf-8') as f:
206
- html = f.read()
207
- inject = f"""
 
208
  <script>
209
- window.ALL_INITIAL_OBJECTS = {json.dumps(state["ALL_INITIAL_OBJECTS"])};
210
- window.PLOTS_METADATA = {json.dumps(state["PLOTS_METADATA"])};
211
- window.SELECTED_OBJECT_TYPE = {json.dumps(state["SELECTED_OBJECT_TYPE"])};
212
- window.PLOT_WIDTH = {json.dumps(state["PLOT_WIDTH"])};
213
- window.PLOT_DEPTH = {json.dumps(state["PLOT_DEPTH"])};
214
- window.GAME_STATE = {json.dumps(state["GAME_STATE"])};
 
 
 
 
 
 
215
  </script>
216
  """
217
- html = html.replace("</head>", inject + "\n</head>", 1)
218
- components.html(html, height=750, scrolling=False)
219
  except FileNotFoundError:
220
- st.error(" index.html not found!")
 
221
  except Exception as e:
222
- st.error(f"HTML injection failed: {e}")
 
 
1
+ # app.py
2
+ import streamlit as st
3
+ import streamlit.components.v1 as components
4
+ import os
5
+ import json
6
+ import pandas as pd
7
+ import uuid
8
+ import math
9
+ import time
10
+
11
+ # Import our GameState class
12
+ from gamestate import GameState
13
+
14
+ # --- Page Config ---
15
  st.set_page_config(page_title="Infinite World Builder", layout="wide")
16
 
17
+ # --- Constants ---
18
  SAVE_DIR = "saved_worlds"
19
+ PLOT_WIDTH = 50.0 # Width of each plot in 3D space
20
+ PLOT_DEPTH = 50.0 # Depth of each plot
21
+ CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'rot_z', 'rot_order']
22
+
23
+ # --- Ensure Save Directory Exists ---
 
 
 
 
24
  os.makedirs(SAVE_DIR, exist_ok=True)
25
 
26
+ @st.cache_data(ttl=3600)
27
  def load_plot_metadata():
28
+ """Scans SAVE_DIR for plot files and returns metadata."""
29
+ plots = []
30
  try:
31
+ plot_files = [f for f in os.listdir(SAVE_DIR) if f.endswith(".csv") and f.startswith("plot_X")]
32
  except FileNotFoundError:
33
+ st.error(f"Save directory '{SAVE_DIR}' not found.")
34
+ return []
35
+ except Exception as e:
36
+ st.error(f"Error listing save directory '{SAVE_DIR}': {e}")
37
  return []
38
+
39
+ parsed_plots = []
40
+ for filename in plot_files:
41
  try:
42
+ parts = filename[:-4].split('_') # Remove .csv
43
+ grid_x = int(parts[1][1:]) # After 'X'
44
+ grid_z = int(parts[2][1:]) # After 'Z'
45
+ plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
46
+ parsed_plots.append({
47
+ 'id': filename[:-4],
48
+ 'filename': filename,
49
+ 'grid_x': grid_x,
50
+ 'grid_z': grid_z,
51
+ 'name': plot_name,
52
+ 'x_offset': grid_x * PLOT_WIDTH,
53
+ 'z_offset': grid_z * PLOT_DEPTH
54
  })
55
+ except (IndexError, ValueError):
56
+ st.warning(f"Could not parse grid coordinates from filename: {filename}. Skipping.")
57
  continue
58
+
59
+ parsed_plots.sort(key=lambda p: (p['grid_x'], p['grid_z']))
60
+ return parsed_plots
61
 
62
  def load_plot_objects(filename, x_offset, z_offset):
63
+ """Loads objects from a CSV file and applies world offsets."""
64
+ file_path = os.path.join(SAVE_DIR, filename)
65
+ objects = []
66
  try:
67
+ df = pd.read_csv(file_path)
68
+ if not all(col in df.columns for col in ['type', 'pos_x', 'pos_y', 'pos_z']):
69
+ st.warning(f"CSV '{filename}' missing essential columns. Skipping.")
70
+ return []
71
+ df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in range(len(df))]))
72
+ for col, default in [('rot_x', 0.0), ('rot_y', 0.0), ('rot_z', 0.0), ('rot_order', 'XYZ')]:
73
+ if col not in df.columns:
74
+ df[col] = default
75
+
76
+ for _, row in df.iterrows():
77
+ obj_data = row.to_dict()
78
+ obj_data['pos_x'] += x_offset
79
+ obj_data['pos_z'] += z_offset
80
+ objects.append(obj_data)
81
+ return objects
82
+ except FileNotFoundError:
83
+ st.error(f"File not found during object load: {filename}")
84
+ return []
85
+ except pd.errors.EmptyDataError:
86
  return []
87
+ except Exception as e:
88
+ st.error(f"Error loading objects from {filename}: {e}")
89
+ return []
90
+
91
+ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
92
+ """Saves object data list to a CSV file, making positions relative to the plot origin."""
93
+ file_path = os.path.join(SAVE_DIR, filename)
94
+ relative_objects = []
95
+ if not isinstance(objects_data_list, list):
96
+ st.error("Invalid data format received for saving (expected a list).")
97
+ return False
98
+
99
+ for obj in objects_data_list:
100
+ pos = obj.get('position', {})
101
+ rot = obj.get('rotation', {})
102
+ obj_type = obj.get('type', 'Unknown')
103
+ obj_id = obj.get('obj_id', str(uuid.uuid4()))
104
+
105
+ if not all(k in pos for k in ['x', 'y', 'z']) or obj_type == 'Unknown':
106
+ print(f"Skipping malformed object during save prep: {obj}")
 
 
 
 
 
 
 
 
 
107
  continue
108
+
109
+ relative_obj = {
110
+ 'obj_id': obj_id, 'type': obj_type,
111
+ 'pos_x': pos.get('x', 0.0) - plot_x_offset,
112
+ 'pos_y': pos.get('y', 0.0),
113
+ 'pos_z': pos.get('z', 0.0) - plot_z_offset,
114
+ 'rot_x': rot.get('_x', 0.0), 'rot_y': rot.get('_y', 0.0),
115
+ 'rot_z': rot.get('_z', 0.0), 'rot_order': rot.get('_order', 'XYZ')
116
+ }
117
+ relative_objects.append(relative_obj)
118
+
119
  try:
120
+ df = pd.DataFrame(relative_objects, columns=CSV_COLUMNS)
121
+ df.to_csv(file_path, index=False)
122
+ st.success(f"Saved {len(relative_objects)} objects to {filename}")
123
  return True
124
  except Exception as e:
125
+ st.error(f"Failed to save plot data to {filename}: {e}")
126
  return False
127
 
128
+ # --- Initialize GameState Singleton ---
129
  @st.cache_resource
130
  def get_game_state():
131
+ # This instance is shared across all sessions and reruns.
132
  return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
133
 
134
  game_state = get_game_state()
135
 
136
+ # --- Session State Initialization ---
137
+ if 'selected_object' not in st.session_state:
138
+ st.session_state.selected_object = 'None'
139
+ if 'new_plot_name' not in st.session_state:
140
+ st.session_state.new_plot_name = ""
141
+ if 'js_save_data_result' not in st.session_state:
142
+ st.session_state.js_save_data_result = None
143
 
 
144
  plots_metadata = load_plot_metadata()
145
  all_initial_objects = []
146
+ for plot in plots_metadata:
147
+ all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
148
 
149
+ # --- Sidebar ---
150
  with st.sidebar:
151
  st.title("🏗️ World Controls")
152
+ st.header("Navigation (Plots)")
153
+ st.caption("Click to teleport player to a plot.")
154
+ max_cols = 2
155
+ cols = st.columns(max_cols)
156
+ col_idx = 0
157
+ sorted_plots_for_nav = sorted(plots_metadata, key=lambda p: (p['grid_x'], p['grid_z']))
158
+ for plot in sorted_plots_for_nav:
159
+ button_label = f"➡️ {plot.get('name', plot['id'])} ({plot['grid_x']},{plot['grid_z']})"
160
+ if cols[col_idx].button(button_label, key=f"nav_{plot['id']}"):
161
+ target_x = plot['x_offset']
162
+ target_z = plot['z_offset']
163
  try:
164
+ js_code = f"teleportPlayer({target_x + PLOT_WIDTH/2}, {target_z + PLOT_DEPTH/2});"
165
+ from streamlit_js_eval import streamlit_js_eval
166
+ streamlit_js_eval(js_code=js_code, key=f"teleport_{plot['id']}")
167
  except Exception as e:
168
+ st.error(f"Failed to send teleport command: {e}")
169
+ col_idx = (col_idx + 1) % max_cols
170
 
171
  st.markdown("---")
172
+ st.header("Place Objects")
173
+ object_types = ["None", "Simple House", "Tree", "Rock", "Fence Post"]
174
+ current_object_index = object_types.index(st.session_state.selected_object) if st.session_state.selected_object in object_types else 0
175
+ selected_object_type_widget = st.selectbox("Select Object:", options=object_types, index=current_object_index, key="selected_object_widget")
176
+ if selected_object_type_widget != st.session_state.selected_object:
177
+ st.session_state.selected_object = selected_object_type_widget
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
  st.markdown("---")
180
+ st.header("Save Work")
181
+ st.caption("Saves newly placed objects to the current plot. A new plot file is created for new areas.")
182
+ if st.button("💾 Save Current Work", key="save_button"):
183
  from streamlit_js_eval import streamlit_js_eval
184
+ js_get_data_code = "getSaveDataAndPosition();"
185
+ streamlit_js_eval(js_code=js_get_data_code, key="js_save_processor")
186
+ st.rerun()
187
 
188
+ # --- Process Save Data from JS ---
189
+ save_data_from_js = st.session_state.get("js_save_processor", None)
190
+ if save_data_from_js is not None:
191
+ st.info("Received save data from client...")
192
+ save_processed_successfully = False
193
  try:
194
+ payload = json.loads(save_data_from_js) if isinstance(save_data_from_js, str) else save_data_from_js
195
+ if isinstance(payload, dict) and 'playerPosition' in payload and 'objectsToSave' in payload:
196
+ player_pos = payload['playerPosition']
197
+ objects_to_save = payload['objectsToSave']
198
+ if isinstance(objects_to_save, list):
199
+ target_grid_x = math.floor(player_pos.get('x', 0.0) / PLOT_WIDTH)
200
+ target_grid_z = math.floor(player_pos.get('z', 0.0) / PLOT_DEPTH)
201
+ target_filename = f"plot_X{target_grid_x}_Z{target_grid_z}.csv"
202
+ target_plot_x_offset = target_grid_x * PLOT_WIDTH
203
+ target_plot_z_offset = target_grid_z * PLOT_DEPTH
204
+ st.write(f"Attempting to save plot: {target_filename} (Player at: x={player_pos.get('x', 0):.1f}, z={player_pos.get('z', 0):.1f})")
205
+ is_new_plot_file = not os.path.exists(os.path.join(SAVE_DIR, target_filename))
206
+ save_ok = save_plot_data(target_filename, objects_to_save, target_plot_x_offset, target_plot_z_offset)
207
+ if save_ok:
208
+ load_plot_metadata.clear() # Clear cache so metadata reloads
209
+ try:
210
+ from streamlit_js_eval import streamlit_js_eval
211
+ streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js_state")
212
+ except Exception as js_e:
213
+ st.warning(f"Could not reset JS state after save: {js_e}")
214
+ if is_new_plot_file:
215
+ st.success(f"New plot created and saved: {target_filename}")
216
+ else:
217
+ st.success(f"Updated existing plot: {target_filename}")
218
+ # Update shared game state with new objects from this session
219
+ game_state.update_state(objects_to_save)
220
+ save_processed_successfully = True
221
+ else:
222
+ st.error(f"Failed to save plot data to file: {target_filename}")
223
+ else:
224
+ st.error("Invalid 'objectsToSave' format received (expected list).")
225
+ else:
226
+ st.error("Invalid save payload structure received from client.")
227
+ except json.JSONDecodeError:
228
+ st.error("Failed to decode save data from client.")
229
  except Exception as e:
230
+ st.error(f"Error processing save: {e}")
231
+ st.session_state.js_save_processor = None
232
+ if save_processed_successfully:
233
+ st.rerun()
234
 
235
+ # --- Main Area ---
236
+ st.header("Infinite Shared 3D World")
237
+ st.caption("Move to empty areas to expand the world. Use the sidebar 'Save' to store your work.")
238
 
239
+ # Inject state into JS—including the shared GAME_STATE from our GameState singleton.
240
+ injected_state = {
241
  "ALL_INITIAL_OBJECTS": all_initial_objects,
242
+ "PLOTS_METADATA": plots_metadata,
243
  "SELECTED_OBJECT_TYPE": st.session_state.selected_object,
244
+ "PLOT_WIDTH": PLOT_WIDTH,
245
+ "PLOT_DEPTH": PLOT_DEPTH,
246
+ "GAME_STATE": game_state.get_state()
247
  }
248
 
249
+ html_file_path = 'index.html'
250
+ html_content_with_state = None
251
  try:
252
+ with open(html_file_path, 'r', encoding='utf-8') as f:
253
+ html_template = f.read()
254
+
255
+ js_injection_script = f"""
256
  <script>
257
+ window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
258
+ window.PLOTS_METADATA = {json.dumps(injected_state["PLOTS_METADATA"])};
259
+ window.SELECTED_OBJECT_TYPE = {json.dumps(injected_state["SELECTED_OBJECT_TYPE"])};
260
+ window.PLOT_WIDTH = {json.dumps(injected_state["PLOT_WIDTH"])};
261
+ window.PLOT_DEPTH = {json.dumps(injected_state["PLOT_DEPTH"])};
262
+ window.GAME_STATE = {json.dumps(injected_state["GAME_STATE"])};
263
+ console.log("Streamlit State Injected:", {{
264
+ selectedObject: window.SELECTED_OBJECT_TYPE,
265
+ initialObjectsCount: window.ALL_INITIAL_OBJECTS ? window.ALL_INITIAL_OBJECTS.length : 0,
266
+ plotCount: window.PLOTS_METADATA ? window.PLOTS_METADATA.length : 0,
267
+ gameStateObjects: window.GAME_STATE ? window.GAME_STATE.length : 0
268
+ }});
269
  </script>
270
  """
271
+ html_content_with_state = html_template.replace('</head>', js_injection_script + '\n</head>', 1)
272
+ components.html(html_content_with_state, height=750, scrolling=False)
273
  except FileNotFoundError:
274
+ st.error(f"CRITICAL ERROR: Could not find the file '{html_file_path}'.")
275
+ st.warning(f"Make sure `{html_file_path}` is in the same directory as `app.py` and `{SAVE_DIR}` exists.")
276
  except Exception as e:
277
+ st.error(f"An critical error occurred during HTML preparation or component rendering: {e}")
278
+ st.exception(e)