Spaces:
Sleeping
Sleeping
Commit ·
9d3b8a9
1
Parent(s): 9bca8e4
Add CropDrive integration: upload limits enforcement and analysis completion messaging
Browse files- Update cropdrive_integration.py to handle CONFIG messages with upload limits
- Add can_start_analysis() function to check upload limits before analysis
- Add get_upload_limit_info() helper function
- Update send_analysis_complete() to include ISO timestamp
- Update upload.py to enforce upload limits before allowing analysis
- Display upload usage information on upload page
- Update results.py to ensure ANALYSIS_COMPLETE message includes timestamp
- Handle unlimited plans (infinity/-1) properly
- modules/results.py +12 -2
- modules/upload.py +43 -1
- utils/cropdrive_integration.py +130 -3
modules/results.py
CHANGED
|
@@ -625,15 +625,25 @@ def show_results_page():
|
|
| 625 |
analysis_type = 'leaf'
|
| 626 |
|
| 627 |
# Send completion message
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
send_analysis_complete(
|
| 629 |
-
title=f'
|
| 630 |
analysis_type=analysis_type,
|
| 631 |
summary=summary or "Comprehensive soil and leaf analysis completed successfully.",
|
| 632 |
recommendations_count=recommendations_count,
|
| 633 |
file_url=None, # Could add URL to PDF if generated
|
| 634 |
analysis_data={
|
| 635 |
'resultId': results_data.get('id'),
|
| 636 |
-
'timestamp':
|
| 637 |
'status': 'completed'
|
| 638 |
}
|
| 639 |
)
|
|
|
|
| 625 |
analysis_type = 'leaf'
|
| 626 |
|
| 627 |
# Send completion message
|
| 628 |
+
# Get timestamp in ISO format
|
| 629 |
+
timestamp = results_data.get('timestamp')
|
| 630 |
+
if timestamp:
|
| 631 |
+
if hasattr(timestamp, 'isoformat'):
|
| 632 |
+
timestamp_str = timestamp.isoformat()
|
| 633 |
+
else:
|
| 634 |
+
timestamp_str = str(timestamp)
|
| 635 |
+
else:
|
| 636 |
+
timestamp_str = datetime.now().isoformat()
|
| 637 |
+
|
| 638 |
send_analysis_complete(
|
| 639 |
+
title=f'Analysis Report - {datetime.now().strftime("%Y-%m-%d")}',
|
| 640 |
analysis_type=analysis_type,
|
| 641 |
summary=summary or "Comprehensive soil and leaf analysis completed successfully.",
|
| 642 |
recommendations_count=recommendations_count,
|
| 643 |
file_url=None, # Could add URL to PDF if generated
|
| 644 |
analysis_data={
|
| 645 |
'resultId': results_data.get('id'),
|
| 646 |
+
'timestamp': timestamp_str,
|
| 647 |
'status': 'completed'
|
| 648 |
}
|
| 649 |
)
|
modules/upload.py
CHANGED
|
@@ -1060,10 +1060,52 @@ def upload_section():
|
|
| 1060 |
|
| 1061 |
land_yield_provided = land_size > 0 and current_yield > 0
|
| 1062 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1063 |
if soil_uploaded and leaf_uploaded and land_yield_provided:
|
| 1064 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1065 |
# Ensure files are valid before passing to analysis
|
| 1066 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
soil_file = st.session_state.soil_file
|
| 1068 |
leaf_file = st.session_state.leaf_file
|
| 1069 |
|
|
|
|
| 1060 |
|
| 1061 |
land_yield_provided = land_size > 0 and current_yield > 0
|
| 1062 |
|
| 1063 |
+
# Check upload limits before allowing analysis
|
| 1064 |
+
can_start = True
|
| 1065 |
+
limit_info = None
|
| 1066 |
+
try:
|
| 1067 |
+
from utils.cropdrive_integration import can_start_analysis, get_upload_limit_info
|
| 1068 |
+
can_start = can_start_analysis()
|
| 1069 |
+
limit_info = get_upload_limit_info()
|
| 1070 |
+
except ImportError:
|
| 1071 |
+
# CropDrive integration not available, allow analysis
|
| 1072 |
+
pass
|
| 1073 |
+
except Exception:
|
| 1074 |
+
# Error checking limits, allow analysis to proceed
|
| 1075 |
+
pass
|
| 1076 |
+
|
| 1077 |
if soil_uploaded and leaf_uploaded and land_yield_provided:
|
| 1078 |
+
# Show upload limit info if available
|
| 1079 |
+
if limit_info:
|
| 1080 |
+
uploads_used = limit_info.get('uploads_used', 0)
|
| 1081 |
+
uploads_limit = limit_info.get('uploads_limit', 0)
|
| 1082 |
+
uploads_remaining = limit_info.get('uploads_remaining', 0)
|
| 1083 |
+
|
| 1084 |
+
if uploads_limit > 0:
|
| 1085 |
+
if uploads_remaining == float('inf') or uploads_remaining == -1:
|
| 1086 |
+
st.info(f"📊 **Analyses:** Unlimited")
|
| 1087 |
+
else:
|
| 1088 |
+
st.info(f"📊 **Analyses:** {uploads_used}/{uploads_limit} used ({uploads_remaining} remaining)")
|
| 1089 |
+
|
| 1090 |
+
# Check if limit exceeded
|
| 1091 |
+
if not can_start:
|
| 1092 |
+
st.error("❌ **Analysis Limit Reached**")
|
| 1093 |
+
st.info("💡 You have reached your analysis limit. Please upgrade your plan to continue analyzing lab reports.")
|
| 1094 |
+
st.button(f"🚀 {t('upload_start_analysis')}", disabled=True, use_container_width=True, key="start_analysis_disabled_limit")
|
| 1095 |
+
elif st.button(f"🚀 {t('upload_start_analysis')}", type="primary", use_container_width=True, key="start_analysis"):
|
| 1096 |
# Ensure files are valid before passing to analysis
|
| 1097 |
try:
|
| 1098 |
+
# Double-check limit before starting (in case it changed)
|
| 1099 |
+
try:
|
| 1100 |
+
from utils.cropdrive_integration import can_start_analysis
|
| 1101 |
+
if not can_start_analysis():
|
| 1102 |
+
st.error("❌ **Analysis Limit Reached**")
|
| 1103 |
+
st.info("💡 You have reached your analysis limit. Please upgrade your plan to continue analyzing lab reports.")
|
| 1104 |
+
st.stop()
|
| 1105 |
+
except Exception:
|
| 1106 |
+
# If check fails, allow to proceed
|
| 1107 |
+
pass
|
| 1108 |
+
|
| 1109 |
soil_file = st.session_state.soil_file
|
| 1110 |
leaf_file = st.session_state.leaf_file
|
| 1111 |
|
utils/cropdrive_integration.py
CHANGED
|
@@ -43,7 +43,11 @@ def inject_parent_communication():
|
|
| 43 |
userId: data.userId,
|
| 44 |
plan: data.plan || 'none',
|
| 45 |
userEmail: data.userEmail || '',
|
| 46 |
-
userName: data.userName || ''
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
};
|
| 48 |
currentLanguage = userConfig.language;
|
| 49 |
if (currentLanguage !== 'en' && currentLanguage !== 'ms') {
|
|
@@ -56,6 +60,10 @@ def inject_parent_communication():
|
|
| 56 |
localStorage.setItem('cropdrive_userEmail', userConfig.userEmail || '');
|
| 57 |
localStorage.setItem('cropdrive_userName', userConfig.userName || '');
|
| 58 |
localStorage.setItem('cropdrive_plan', userConfig.plan || 'none');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
// Update URL parameters
|
| 61 |
const url = new URL(window.location.href);
|
|
@@ -67,8 +75,18 @@ def inject_parent_communication():
|
|
| 67 |
url.searchParams.set('userEmail', userConfig.userEmail || '');
|
| 68 |
url.searchParams.set('userName', userConfig.userName || '');
|
| 69 |
url.searchParams.set('plan', userConfig.plan || 'none');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
window.history.replaceState({}, '', url);
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
// Notify parent that language was set
|
| 73 |
window.parent.postMessage({
|
| 74 |
type: 'STREAMLIT_LANGUAGE_UPDATE',
|
|
@@ -115,6 +133,11 @@ def inject_parent_communication():
|
|
| 115 |
const userIdParam = urlParams.get('userId');
|
| 116 |
const userEmailParam = urlParams.get('userEmail');
|
| 117 |
const userNameParam = urlParams.get('userName');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
if (userIdParam) {
|
| 119 |
localStorage.setItem('cropdrive_userId', userIdParam);
|
| 120 |
}
|
|
@@ -124,6 +147,18 @@ def inject_parent_communication():
|
|
| 124 |
if (userNameParam) {
|
| 125 |
localStorage.setItem('cropdrive_userName', userNameParam);
|
| 126 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
});
|
| 128 |
})();
|
| 129 |
</script>
|
|
@@ -158,6 +193,37 @@ def initialize_integration():
|
|
| 158 |
features_str = query_params.get('features', '')
|
| 159 |
features = features_str.split(',') if features_str else []
|
| 160 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
# Store user config in session state
|
| 162 |
if 'user_config' not in st.session_state:
|
| 163 |
st.session_state.user_config = {
|
|
@@ -178,6 +244,12 @@ def initialize_integration():
|
|
| 178 |
st.session_state.user_config['userEmail'] = user_email
|
| 179 |
st.session_state.user_config['userName'] = user_name
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
return current_lang, user_plan, features
|
| 182 |
|
| 183 |
# ============================================================================
|
|
@@ -203,20 +275,30 @@ def send_analysis_complete(
|
|
| 203 |
file_url: URL to analysis file/image (optional)
|
| 204 |
analysis_data: Additional analysis data dict (optional)
|
| 205 |
"""
|
|
|
|
|
|
|
| 206 |
# Include user ID in the analysis complete message
|
| 207 |
user_id = get_user_id()
|
| 208 |
user_email = get_user_email()
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
message = {
|
| 211 |
'type': 'ANALYSIS_COMPLETE',
|
|
|
|
| 212 |
'title': title,
|
| 213 |
'analysisType': analysis_type,
|
| 214 |
'summary': summary or '',
|
| 215 |
'recommendationsCount': recommendations_count,
|
| 216 |
'fileUrl': file_url,
|
| 217 |
'analysisData': analysis_data or {},
|
| 218 |
-
'
|
| 219 |
-
'userEmail': user_email
|
| 220 |
}
|
| 221 |
|
| 222 |
# Send to all allowed origins
|
|
@@ -320,6 +402,51 @@ def get_user_name():
|
|
| 320 |
user_name = st.session_state.get('user_config', {}).get('userName', '')
|
| 321 |
return user_name
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
def send_language_change(new_language: str):
|
| 324 |
"""Send language change notification to parent window when language changes within Streamlit"""
|
| 325 |
# Validate language
|
|
|
|
| 43 |
userId: data.userId,
|
| 44 |
plan: data.plan || 'none',
|
| 45 |
userEmail: data.userEmail || '',
|
| 46 |
+
userName: data.userName || '',
|
| 47 |
+
uploadsUsed: data.uploadsUsed || 0,
|
| 48 |
+
uploadsLimit: data.uploadsLimit || 0,
|
| 49 |
+
uploadLimitExceeded: data.uploadLimitExceeded || false,
|
| 50 |
+
uploadsRemaining: data.uploadsRemaining !== undefined ? data.uploadsRemaining : (data.uploadsLimit || 0) - (data.uploadsUsed || 0)
|
| 51 |
};
|
| 52 |
currentLanguage = userConfig.language;
|
| 53 |
if (currentLanguage !== 'en' && currentLanguage !== 'ms') {
|
|
|
|
| 60 |
localStorage.setItem('cropdrive_userEmail', userConfig.userEmail || '');
|
| 61 |
localStorage.setItem('cropdrive_userName', userConfig.userName || '');
|
| 62 |
localStorage.setItem('cropdrive_plan', userConfig.plan || 'none');
|
| 63 |
+
localStorage.setItem('cropdrive_uploadsUsed', String(userConfig.uploadsUsed));
|
| 64 |
+
localStorage.setItem('cropdrive_uploadsLimit', String(userConfig.uploadsLimit));
|
| 65 |
+
localStorage.setItem('cropdrive_uploadLimitExceeded', String(userConfig.uploadLimitExceeded));
|
| 66 |
+
localStorage.setItem('cropdrive_uploadsRemaining', String(userConfig.uploadsRemaining));
|
| 67 |
|
| 68 |
// Update URL parameters
|
| 69 |
const url = new URL(window.location.href);
|
|
|
|
| 75 |
url.searchParams.set('userEmail', userConfig.userEmail || '');
|
| 76 |
url.searchParams.set('userName', userConfig.userName || '');
|
| 77 |
url.searchParams.set('plan', userConfig.plan || 'none');
|
| 78 |
+
url.searchParams.set('uploadsUsed', String(userConfig.uploadsUsed));
|
| 79 |
+
url.searchParams.set('uploadsLimit', String(userConfig.uploadsLimit));
|
| 80 |
+
url.searchParams.set('uploadLimitExceeded', String(userConfig.uploadLimitExceeded));
|
| 81 |
+
url.searchParams.set('uploadsRemaining', String(userConfig.uploadsRemaining));
|
| 82 |
window.history.replaceState({}, '', url);
|
| 83 |
|
| 84 |
+
// Notify parent that config was received
|
| 85 |
+
window.parent.postMessage({
|
| 86 |
+
type: 'CONFIG_RECEIVED',
|
| 87 |
+
userId: userConfig.userId
|
| 88 |
+
}, '*');
|
| 89 |
+
|
| 90 |
// Notify parent that language was set
|
| 91 |
window.parent.postMessage({
|
| 92 |
type: 'STREAMLIT_LANGUAGE_UPDATE',
|
|
|
|
| 133 |
const userIdParam = urlParams.get('userId');
|
| 134 |
const userEmailParam = urlParams.get('userEmail');
|
| 135 |
const userNameParam = urlParams.get('userName');
|
| 136 |
+
const uploadsUsedParam = urlParams.get('uploadsUsed');
|
| 137 |
+
const uploadsLimitParam = urlParams.get('uploadsLimit');
|
| 138 |
+
const uploadLimitExceededParam = urlParams.get('uploadLimitExceeded');
|
| 139 |
+
const uploadsRemainingParam = urlParams.get('uploadsRemaining');
|
| 140 |
+
|
| 141 |
if (userIdParam) {
|
| 142 |
localStorage.setItem('cropdrive_userId', userIdParam);
|
| 143 |
}
|
|
|
|
| 147 |
if (userNameParam) {
|
| 148 |
localStorage.setItem('cropdrive_userName', userNameParam);
|
| 149 |
}
|
| 150 |
+
if (uploadsUsedParam !== null) {
|
| 151 |
+
localStorage.setItem('cropdrive_uploadsUsed', uploadsUsedParam);
|
| 152 |
+
}
|
| 153 |
+
if (uploadsLimitParam !== null) {
|
| 154 |
+
localStorage.setItem('cropdrive_uploadsLimit', uploadsLimitParam);
|
| 155 |
+
}
|
| 156 |
+
if (uploadLimitExceededParam !== null) {
|
| 157 |
+
localStorage.setItem('cropdrive_uploadLimitExceeded', uploadLimitExceededParam);
|
| 158 |
+
}
|
| 159 |
+
if (uploadsRemainingParam !== null) {
|
| 160 |
+
localStorage.setItem('cropdrive_uploadsRemaining', uploadsRemainingParam);
|
| 161 |
+
}
|
| 162 |
});
|
| 163 |
})();
|
| 164 |
</script>
|
|
|
|
| 193 |
features_str = query_params.get('features', '')
|
| 194 |
features = features_str.split(',') if features_str else []
|
| 195 |
|
| 196 |
+
# Get upload limits from URL parameters
|
| 197 |
+
uploads_used = query_params.get('uploadsUsed', '0')
|
| 198 |
+
uploads_limit = query_params.get('uploadsLimit', '0')
|
| 199 |
+
upload_limit_exceeded = query_params.get('uploadLimitExceeded', 'false')
|
| 200 |
+
uploads_remaining = query_params.get('uploadsRemaining', '0')
|
| 201 |
+
|
| 202 |
+
# Convert to appropriate types
|
| 203 |
+
try:
|
| 204 |
+
uploads_used = int(uploads_used) if uploads_used else 0
|
| 205 |
+
except (ValueError, TypeError):
|
| 206 |
+
uploads_used = 0
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
uploads_limit = int(uploads_limit) if uploads_limit else 0
|
| 210 |
+
except (ValueError, TypeError):
|
| 211 |
+
uploads_limit = 0
|
| 212 |
+
|
| 213 |
+
try:
|
| 214 |
+
upload_limit_exceeded = upload_limit_exceeded.lower() == 'true' if upload_limit_exceeded else False
|
| 215 |
+
except (AttributeError, ValueError):
|
| 216 |
+
upload_limit_exceeded = False
|
| 217 |
+
|
| 218 |
+
try:
|
| 219 |
+
uploads_remaining_str = str(uploads_remaining)
|
| 220 |
+
if uploads_remaining_str.lower() == 'infinity' or uploads_remaining_str == '-1':
|
| 221 |
+
uploads_remaining = float('inf')
|
| 222 |
+
else:
|
| 223 |
+
uploads_remaining = int(uploads_remaining) if uploads_remaining else 0
|
| 224 |
+
except (ValueError, TypeError):
|
| 225 |
+
uploads_remaining = 0
|
| 226 |
+
|
| 227 |
# Store user config in session state
|
| 228 |
if 'user_config' not in st.session_state:
|
| 229 |
st.session_state.user_config = {
|
|
|
|
| 244 |
st.session_state.user_config['userEmail'] = user_email
|
| 245 |
st.session_state.user_config['userName'] = user_name
|
| 246 |
|
| 247 |
+
# Store upload limits in session state
|
| 248 |
+
st.session_state.uploads_used = uploads_used
|
| 249 |
+
st.session_state.uploads_limit = uploads_limit
|
| 250 |
+
st.session_state.upload_limit_exceeded = upload_limit_exceeded
|
| 251 |
+
st.session_state.uploads_remaining = uploads_remaining
|
| 252 |
+
|
| 253 |
return current_lang, user_plan, features
|
| 254 |
|
| 255 |
# ============================================================================
|
|
|
|
| 275 |
file_url: URL to analysis file/image (optional)
|
| 276 |
analysis_data: Additional analysis data dict (optional)
|
| 277 |
"""
|
| 278 |
+
from datetime import datetime
|
| 279 |
+
|
| 280 |
# Include user ID in the analysis complete message
|
| 281 |
user_id = get_user_id()
|
| 282 |
user_email = get_user_email()
|
| 283 |
|
| 284 |
+
# Ensure analysis_data includes timestamp if not already present
|
| 285 |
+
if analysis_data is None:
|
| 286 |
+
analysis_data = {}
|
| 287 |
+
|
| 288 |
+
# Add timestamp if not already in analysis_data
|
| 289 |
+
if 'timestamp' not in analysis_data:
|
| 290 |
+
analysis_data['timestamp'] = datetime.now().isoformat()
|
| 291 |
+
|
| 292 |
message = {
|
| 293 |
'type': 'ANALYSIS_COMPLETE',
|
| 294 |
+
'userId': user_id,
|
| 295 |
'title': title,
|
| 296 |
'analysisType': analysis_type,
|
| 297 |
'summary': summary or '',
|
| 298 |
'recommendationsCount': recommendations_count,
|
| 299 |
'fileUrl': file_url,
|
| 300 |
'analysisData': analysis_data or {},
|
| 301 |
+
'timestamp': datetime.now().isoformat()
|
|
|
|
| 302 |
}
|
| 303 |
|
| 304 |
# Send to all allowed origins
|
|
|
|
| 402 |
user_name = st.session_state.get('user_config', {}).get('userName', '')
|
| 403 |
return user_name
|
| 404 |
|
| 405 |
+
def can_start_analysis():
|
| 406 |
+
"""
|
| 407 |
+
Check if user can start a new analysis based on upload limits
|
| 408 |
+
|
| 409 |
+
Returns:
|
| 410 |
+
True if analysis can start, False otherwise
|
| 411 |
+
"""
|
| 412 |
+
# Check if upload limit is exceeded
|
| 413 |
+
upload_limit_exceeded = st.session_state.get('upload_limit_exceeded', False)
|
| 414 |
+
if upload_limit_exceeded:
|
| 415 |
+
return False
|
| 416 |
+
|
| 417 |
+
# Check remaining uploads
|
| 418 |
+
uploads_remaining = st.session_state.get('uploads_remaining', 0)
|
| 419 |
+
|
| 420 |
+
# If uploads_remaining is infinity or -1, allow unlimited analyses
|
| 421 |
+
if uploads_remaining == float('inf') or uploads_remaining == -1:
|
| 422 |
+
return True
|
| 423 |
+
|
| 424 |
+
# If uploads_remaining is 0 or less, block analysis
|
| 425 |
+
if uploads_remaining <= 0:
|
| 426 |
+
return False
|
| 427 |
+
|
| 428 |
+
# Otherwise, allow analysis
|
| 429 |
+
return True
|
| 430 |
+
|
| 431 |
+
def get_upload_limit_info():
|
| 432 |
+
"""
|
| 433 |
+
Get upload limit information for display
|
| 434 |
+
|
| 435 |
+
Returns:
|
| 436 |
+
Dictionary with upload limit information
|
| 437 |
+
"""
|
| 438 |
+
uploads_used = st.session_state.get('uploads_used', 0)
|
| 439 |
+
uploads_limit = st.session_state.get('uploads_limit', 0)
|
| 440 |
+
upload_limit_exceeded = st.session_state.get('upload_limit_exceeded', False)
|
| 441 |
+
uploads_remaining = st.session_state.get('uploads_remaining', 0)
|
| 442 |
+
|
| 443 |
+
return {
|
| 444 |
+
'uploads_used': uploads_used,
|
| 445 |
+
'uploads_limit': uploads_limit,
|
| 446 |
+
'upload_limit_exceeded': upload_limit_exceeded,
|
| 447 |
+
'uploads_remaining': uploads_remaining
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
def send_language_change(new_language: str):
|
| 451 |
"""Send language change notification to parent window when language changes within Streamlit"""
|
| 452 |
# Validate language
|