ghmk Claude commited on
Commit
d327dce
·
1 Parent(s): 6c4db38

Fix: Preserve uploaded images across UI interactions

Browse files

Images were being cleared when clicking radio buttons or other UI elements
because Streamlit re-runs the entire script on every interaction and the
file uploader widget was being recreated without preserving uploaded files.

This fix stores uploaded images in session_state with persistent keys, so they
remain available across reruns. Now users can:
- Upload an image
- Change radio buttons, dropdowns, or other UI elements
- The uploaded image stays visible and available

Applied to both single and multi-image uploaders.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

character_forge_image/ui/components/image_uploader.py CHANGED
@@ -34,6 +34,10 @@ def render_image_uploader(
34
  Returns:
35
  PIL Image object or None
36
  """
 
 
 
 
37
  uploaded_file = st.file_uploader(
38
  label=label,
39
  type=["png", "jpg", "jpeg", "webp"],
@@ -51,6 +55,10 @@ def render_image_uploader(
51
  image = Image.open(uploaded_file)
52
  logger.debug(f"Image uploaded: {uploaded_file.name}, size: {image.size}")
53
 
 
 
 
 
54
  # Show preview immediately after upload (high quality, max 512px)
55
  if show_preview:
56
  # Calculate display size (clamp to 512px while maintaining aspect ratio)
@@ -71,6 +79,26 @@ def render_image_uploader(
71
  logger.error(f"Image upload error: {e}", exc_info=True)
72
  return None
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  return None
75
 
76
 
@@ -94,6 +122,10 @@ def render_multi_image_uploader(
94
  Returns:
95
  List of PIL Image objects (None for empty slots)
96
  """
 
 
 
 
97
  uploaded_files = st.file_uploader(
98
  label=label,
99
  type=["png", "jpg", "jpeg", "webp"],
@@ -103,6 +135,7 @@ def render_multi_image_uploader(
103
  )
104
 
105
  images = []
 
106
 
107
  if uploaded_files:
108
  # Limit to max_images
@@ -112,11 +145,17 @@ def render_multi_image_uploader(
112
  try:
113
  image = Image.open(uploaded_file)
114
  images.append(image)
 
115
  logger.debug(f"Image uploaded: {uploaded_file.name}, size: {image.size}")
116
  except Exception as e:
117
  st.error(f"Failed to load {uploaded_file.name}: {str(e)}")
118
  logger.error(f"Image upload error: {e}", exc_info=True)
119
  images.append(None)
 
 
 
 
 
120
 
121
  # Show previews if requested (high quality)
122
  if show_previews and any(img is not None for img in images):
@@ -140,6 +179,31 @@ def render_multi_image_uploader(
140
  if len(uploaded_files) > max_images:
141
  st.warning(f"Only the first {max_images} images will be used")
142
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  # Pad to max_images with None
144
  while len(images) < max_images:
145
  images.append(None)
 
34
  Returns:
35
  PIL Image object or None
36
  """
37
+ # Create persistent key for storing the image in session state
38
+ persistent_key = f"{key}_persistent_image"
39
+ persistent_name_key = f"{key}_persistent_name"
40
+
41
  uploaded_file = st.file_uploader(
42
  label=label,
43
  type=["png", "jpg", "jpeg", "webp"],
 
55
  image = Image.open(uploaded_file)
56
  logger.debug(f"Image uploaded: {uploaded_file.name}, size: {image.size}")
57
 
58
+ # Store image in session state so it persists across reruns
59
+ st.session_state[persistent_key] = image
60
+ st.session_state[persistent_name_key] = uploaded_file.name
61
+
62
  # Show preview immediately after upload (high quality, max 512px)
63
  if show_preview:
64
  # Calculate display size (clamp to 512px while maintaining aspect ratio)
 
79
  logger.error(f"Image upload error: {e}", exc_info=True)
80
  return None
81
 
82
+ # If no file uploaded but we have a persistent image, return it
83
+ elif persistent_key in st.session_state and st.session_state[persistent_key] is not None:
84
+ image = st.session_state[persistent_key]
85
+ filename = st.session_state.get(persistent_name_key, "uploaded image")
86
+
87
+ # Show preview of persistent image
88
+ if show_preview:
89
+ max_size = 512
90
+ scale = min(max_size / image.width, max_size / image.height, 1.0)
91
+ display_width = int(image.width * scale)
92
+
93
+ st.image(
94
+ image,
95
+ width=display_width,
96
+ caption=f"✅ {filename} ({image.width}x{image.height})",
97
+ use_container_width=False
98
+ )
99
+
100
+ return image
101
+
102
  return None
103
 
104
 
 
122
  Returns:
123
  List of PIL Image objects (None for empty slots)
124
  """
125
+ # Create persistent key for storing images in session state
126
+ persistent_key = f"{key}_persistent_images"
127
+ persistent_names_key = f"{key}_persistent_names"
128
+
129
  uploaded_files = st.file_uploader(
130
  label=label,
131
  type=["png", "jpg", "jpeg", "webp"],
 
135
  )
136
 
137
  images = []
138
+ filenames = []
139
 
140
  if uploaded_files:
141
  # Limit to max_images
 
145
  try:
146
  image = Image.open(uploaded_file)
147
  images.append(image)
148
+ filenames.append(uploaded_file.name)
149
  logger.debug(f"Image uploaded: {uploaded_file.name}, size: {image.size}")
150
  except Exception as e:
151
  st.error(f"Failed to load {uploaded_file.name}: {str(e)}")
152
  logger.error(f"Image upload error: {e}", exc_info=True)
153
  images.append(None)
154
+ filenames.append(None)
155
+
156
+ # Store images in session state so they persist across reruns
157
+ st.session_state[persistent_key] = images
158
+ st.session_state[persistent_names_key] = filenames
159
 
160
  # Show previews if requested (high quality)
161
  if show_previews and any(img is not None for img in images):
 
179
  if len(uploaded_files) > max_images:
180
  st.warning(f"Only the first {max_images} images will be used")
181
 
182
+ # If no files uploaded but we have persistent images, use them
183
+ elif persistent_key in st.session_state and st.session_state[persistent_key]:
184
+ images = st.session_state[persistent_key]
185
+ filenames = st.session_state.get(persistent_names_key, [])
186
+
187
+ # Show previews of persistent images
188
+ if show_previews and any(img is not None for img in images):
189
+ cols = st.columns(len([img for img in images if img is not None]))
190
+ col_idx = 0
191
+ for i, img in enumerate(images):
192
+ if img is not None:
193
+ with cols[col_idx]:
194
+ max_size = 300
195
+ scale = min(max_size / img.width, max_size / img.height, 1.0)
196
+ display_width = int(img.width * scale)
197
+
198
+ fname = filenames[i] if i < len(filenames) else f"Image {i+1}"
199
+ st.image(
200
+ img,
201
+ width=display_width,
202
+ caption=f"✅ {fname}",
203
+ use_container_width=False
204
+ )
205
+ col_idx += 1
206
+
207
  # Pad to max_images with None
208
  while len(images) < max_images:
209
  images.append(None)