Spaces:
Running
Running
# ui.py | |
# --- Standard Library Imports --- | |
import logging | |
import random | |
# --- Third-Party Imports --- | |
import streamlit as st | |
try: | |
# Requires: pip install Pillow | |
from PIL import Image, ImageDraw | |
PIL_AVAILABLE = True | |
except ImportError: | |
PIL_AVAILABLE = False | |
Image = None # Placeholder | |
try: | |
# Requires: pip install streamlit-drawable-canvas | |
from streamlit_drawable_canvas import st_canvas | |
DRAWABLE_CANVAS_AVAILABLE = True | |
except ImportError: | |
DRAWABLE_CANVAS_AVAILABLE = False | |
st_canvas = None # Placeholder | |
# --- Local Application Imports --- | |
import config # Import the config module to access its constants | |
from config import TIPS, DISEASE_OPTIONS # Can also import specific constants | |
# Import utils for helper functions | |
from utils import format_translation | |
# Import functions/constants from other local modules conditionally | |
try: | |
from ui_components import display_dicom_metadata, dicom_wl_sliders | |
UI_COMPONENTS_AVAILABLE = True | |
except ImportError: | |
UI_COMPONENTS_AVAILABLE = False | |
# Define simple fallbacks if ui_components is missing | |
def display_dicom_metadata(metadata): | |
st.caption("Basic DICOM Metadata Preview (ui_components missing):") | |
st.json({"metadata_preview": dict(list(metadata.items())[:5])}) | |
def dicom_wl_sliders(wc, ww): | |
st.warning("DICOM W/L controls unavailable (ui_components missing).") | |
return wc, ww | |
try: | |
from translation_models import LANGUAGE_CODES, AUTO_DETECT_INDICATOR | |
TRANSLATION_AVAILABLE_UI = True | |
except ImportError: | |
LANGUAGE_CODES = {"English": "en"} # Minimal fallback | |
AUTO_DETECT_INDICATOR = "Auto-Detect" | |
TRANSLATION_AVAILABLE_UI = False | |
try: | |
from dicom_utils import dicom_to_image # Needed for W/L update | |
DICOM_UTILS_AVAILABLE_UI = True | |
except ImportError: | |
DICOM_UTILS_AVAILABLE_UI = False | |
dicom_to_image = None # Placeholder function | |
# --- Logger Setup --- | |
logger = logging.getLogger(__name__) | |
# ============================================================================== | |
# === Sidebar Rendering Function =============================================== | |
# ============================================================================== | |
def render_sidebar(IS_DICOM, REPORT_UTILS_AVAILABLE): | |
""" | |
Renders the sidebar controls and returns the uploaded file object. | |
Args: | |
IS_DICOM (bool): Flag indicating if the current image is DICOM. | |
REPORT_UTILS_AVAILABLE (bool): Flag indicating if report generation is possible. | |
Returns: | |
UploadedFile or None: The file uploaded by the user. | |
""" | |
with st.sidebar: | |
st.header("βοΈ RadVision Controls") | |
st.markdown("---") | |
# Tip of the day | |
st.info(f"π‘ {random.choice(TIPS)}") | |
st.markdown("---") | |
# --- Upload Section --- | |
st.header("Image Upload & Settings") | |
uploaded_file = st.file_uploader( | |
"Upload Image (JPG, PNG, DCM)", | |
type=["jpg", "jpeg", "png", "dcm", "dicom"], | |
key="file_uploader_widget", # Keep consistent key | |
help="Upload a medical image file for analysis. DICOM (.dcm) is preferred." | |
) | |
# --- Demo Mode --- | |
# Note: Actual demo loading logic resides in app.py or file_handler.py | |
st.checkbox("π Demo Mode", value=st.session_state.get("demo_loaded", False), | |
help="Load a sample chest X-ray image and analysis (Functionality needs specific implementation).", | |
key="demo_mode_checkbox", # Add key for state management | |
# on_change=handle_demo_mode_change # Optional callback if needed | |
) | |
# --- ROI Control --- | |
if st.button("ποΈ Clear ROI", help="Remove the selected Region of Interest (ROI)"): | |
st.session_state.roi_coords = None | |
st.session_state.canvas_drawing = None # Clear drawing state too | |
st.session_state.clear_roi_feedback = True # Flag to show success message | |
logger.info("ROI cleared via sidebar button.") | |
st.rerun() # Rerun to update canvas and messages | |
# Display ROI cleared message if flag is set | |
if st.session_state.get("clear_roi_feedback"): | |
st.success("β ROI cleared successfully!") | |
st.session_state.clear_roi_feedback = False # Reset flag after showing | |
# --- DICOM Window/Level --- | |
# Check necessary conditions: is DICOM, components available, and image loaded | |
if IS_DICOM and UI_COMPONENTS_AVAILABLE and DICOM_UTILS_AVAILABLE_UI and st.session_state.get("display_image"): | |
st.markdown("---") | |
st.subheader("DICOM Display (W/L)") | |
current_wc = st.session_state.get("current_display_wc", 0) # Get current value from state | |
current_ww = st.session_state.get("current_display_ww", 400) # Get current value from state | |
# Use the slider component from ui_components | |
new_wc, new_ww = dicom_wl_sliders(current_wc, current_ww) | |
# Check if values have changed | |
if new_wc != current_wc or new_ww != current_ww: | |
logger.info(f"DICOM W/L changed via UI: WC={new_wc}, WW={new_ww}") | |
# Update session state immediately | |
st.session_state.current_display_wc = new_wc | |
st.session_state.current_display_ww = new_ww | |
# Attempt to update the display image | |
if st.session_state.get("dicom_dataset") and dicom_to_image: | |
with st.spinner("Applying new Window/Level..."): | |
try: | |
new_display_img = dicom_to_image( | |
st.session_state.dicom_dataset, | |
wc=new_wc, | |
ww=new_ww | |
) | |
# Validate the result | |
if isinstance(new_display_img, Image.Image): | |
# Ensure it's RGB for display consistency | |
if new_display_img.mode != 'RGB': | |
new_display_img = new_display_img.convert('RGB') | |
# Update the display image in session state | |
st.session_state.display_image = new_display_img | |
logger.info("DICOM display image updated with new W/L.") | |
st.rerun() # Rerun to show the updated image | |
else: | |
st.error("Failed to update DICOM image with new W/L settings (Invalid Image returned).") | |
logger.error("dicom_to_image returned non-Image after W/L change.") | |
except Exception as e: | |
st.error(f"Error applying W/L settings: {e}") | |
logger.error(f"Error in dicom_to_image during W/L update: {e}", exc_info=True) | |
else: | |
st.warning("Cannot apply W/L: DICOM dataset or converter unavailable.") | |
logger.warning("Could not apply W/L change: DICOM dataset/utils missing.") | |
elif IS_DICOM: | |
# Show message if W/L cannot be adjusted | |
st.caption("DICOM W/L controls require `ui_components` and `dicom_utils`.") | |
st.markdown("---") | |
st.header("π€ AI Analysis Actions") | |
# Determine if actions requiring an image should be enabled | |
action_disabled = not isinstance(st.session_state.get("processed_image"), Image.Image) | |
if action_disabled: | |
st.caption("Upload an image to enable AI actions.") | |
# --- Action Buttons --- | |
if st.button("βΆοΈ Run Initial Analysis", key="analyze_btn", disabled=action_disabled, | |
help="Perform a general analysis of the entire image or selected ROI."): | |
st.session_state.last_action = "analyze" | |
st.rerun() # Trigger action handling in app.py | |
st.subheader("β Ask AI a Question") | |
# Manage text area content via session state to preserve across reruns | |
question_value = st.session_state.get("question_input_widget", "") | |
st.session_state.question_input_widget = st.text_area( | |
"Enter your question:", | |
height=100, | |
key="question_input_widget_area", # Unique key for the widget itself | |
placeholder="E.g., 'Are there any nodules in the upper right lobe?'", | |
value=question_value, # Read from state | |
disabled=action_disabled | |
) | |
if st.button("π¬ Ask Question", key="ask_btn", disabled=action_disabled): | |
if st.session_state.question_input_widget.strip(): | |
st.session_state.last_action = "ask" | |
st.rerun() # Trigger action handling | |
else: | |
st.warning("Please enter a question before submitting.") | |
st.subheader("π― Condition-Specific Analysis") | |
# Manage selectbox value via session state | |
disease_options_with_empty = [""] + sorted(DISEASE_OPTIONS) | |
current_disease_selection = st.session_state.get("disease_select_widget", "") | |
try: | |
# Ensure index is valid | |
selected_index = disease_options_with_empty.index(current_disease_selection) | |
except ValueError: | |
selected_index = 0 # Default to empty if previous selection is invalid | |
st.session_state.disease_select_widget = st.selectbox( | |
"Select condition to focus on:", | |
options=disease_options_with_empty, | |
key="disease_select_widget_selector", | |
index=selected_index, | |
disabled=action_disabled | |
) | |
if st.button("π©Ί Analyze Condition", key="disease_btn", disabled=action_disabled): | |
if st.session_state.disease_select_widget: | |
st.session_state.last_action = "disease" | |
st.rerun() # Trigger action handling | |
else: | |
st.warning("Please select a condition first.") | |
st.markdown("---") | |
st.header("π Confidence & Reporting") | |
# Determine if confidence estimation is possible | |
can_estimate = bool( | |
st.session_state.get("history") or | |
st.session_state.get("initial_analysis") or | |
st.session_state.get("disease_analysis") | |
) | |
confidence_disabled = not can_estimate or action_disabled | |
if st.button("π Estimate AI Confidence", key="confidence_btn", | |
disabled=confidence_disabled): | |
st.session_state.last_action = "confidence" | |
st.rerun() # Trigger action handling | |
elif not can_estimate and not action_disabled: | |
st.caption("Perform analysis or ask questions first to enable confidence estimation.") | |
# Report Generation Button | |
report_generation_disabled = action_disabled or not REPORT_UTILS_AVAILABLE | |
if st.button("π Generate PDF Report Data", key="generate_report_data_btn", | |
disabled=report_generation_disabled): | |
st.session_state.last_action = "generate_report_data" | |
st.rerun() # Trigger action handling | |
elif not REPORT_UTILS_AVAILABLE: | |
st.caption("PDF reporting unavailable (report_utils missing or faulty).") | |
# PDF Download Button (appears only when data is ready) | |
if st.session_state.get("pdf_report_bytes"): | |
report_filename = f"RadVisionAI_Report_{st.session_state.get('session_id', 'session')}.pdf" | |
st.download_button( | |
label="β¬οΈ Download PDF Report", | |
data=st.session_state.pdf_report_bytes, | |
file_name=report_filename, | |
mime="application/pdf", | |
key="download_pdf_button", | |
help="Download the generated PDF report." | |
) | |
elif not report_generation_disabled and not action_disabled: | |
# Show caption only if report generation *could* be possible but hasn't run | |
st.caption("Click 'Generate PDF Report Data' to create the report.") | |
return uploaded_file # Return the file object for processing in app.py | |
# ============================================================================== | |
# === Image Viewer Rendering Function ========================================== | |
# ============================================================================== | |
def render_image_viewer(display_img, is_dicom, dicom_metadata): | |
""" | |
Renders the image viewer, canvas for ROI, and DICOM metadata expander. | |
Args: | |
display_img (PIL.Image.Image or None): The image to display. | |
is_dicom (bool): Flag indicating if the image is DICOM. | |
dicom_metadata (dict): Extracted DICOM metadata. | |
""" | |
st.subheader("πΌοΈ Image Viewer") | |
logger.debug(f"render_image_viewer called. display_img type: {type(display_img)}, is_dicom: {is_dicom}") | |
# Check if a valid PIL Image object is provided | |
if PIL_AVAILABLE and isinstance(display_img, Image.Image): | |
# --- Drawable Canvas for ROI --- | |
if DRAWABLE_CANVAS_AVAILABLE and st_canvas: | |
st.caption("Draw a rectangle below to define a Region of Interest (ROI). Clear using the sidebar button.") | |
MAX_CANVAS_WIDTH = 600 | |
MAX_CANVAS_HEIGHT = 500 | |
try: | |
img_w, img_h = display_img.size | |
if img_w <= 0 or img_h <= 0: | |
st.warning("Image has invalid dimensions (<=0). Cannot draw ROI.") | |
logger.warning(f"Invalid image dimensions for canvas: {img_w}x{img_h}") | |
# Display the image without canvas if dimensions are bad | |
st.image(display_img, caption="Image Preview (Invalid Dimensions)", use_container_width=True) | |
else: | |
# Calculate canvas dimensions preserving aspect ratio, fitting constraints | |
aspect_ratio = img_w / img_h | |
canvas_width = min(img_w, MAX_CANVAS_WIDTH) | |
canvas_height = int(canvas_width / aspect_ratio) | |
if canvas_height > MAX_CANVAS_HEIGHT: | |
canvas_height = MAX_CANVAS_HEIGHT | |
canvas_width = int(canvas_height * aspect_ratio) | |
# Ensure minimum dimensions for usability | |
canvas_width = max(canvas_width, 150) | |
canvas_height = max(canvas_height, 150) | |
logger.debug(f"Canvas dimensions calculated: {canvas_width}x{canvas_height}") | |
# Load initial drawing from session state if it exists (to redraw ROI) | |
initial_drawing = st.session_state.get("canvas_drawing", None) | |
canvas_result = st_canvas( | |
fill_color="rgba(255, 165, 0, 0.2)", # Orange fill | |
stroke_width=2, | |
stroke_color="rgba(239, 83, 80, 0.8)", # Red stroke | |
background_image=display_img, # Display the image here | |
update_streamlit=True, # Send updates while drawing | |
height=int(canvas_height), | |
width=int(canvas_width), | |
drawing_mode="rect", # Only allow rectangles | |
initial_drawing=initial_drawing, # Load previous ROI drawing | |
key="drawable_canvas" # Unique key for the component | |
) | |
# Process canvas results to update ROI state | |
if canvas_result.json_data and canvas_result.json_data.get("objects"): | |
if len(canvas_result.json_data["objects"]) > 0: | |
# Use the latest object as the ROI | |
last_object = canvas_result.json_data["objects"][-1] | |
if last_object["type"] == "rect": | |
# Extract geometry, accounting for canvas scaling internal to the object | |
canvas_left = int(last_object["left"]) | |
canvas_top = int(last_object["top"]) | |
canvas_width_scaled = int(last_object["width"] * last_object.get("scaleX", 1)) | |
canvas_height_scaled = int(last_object["height"] * last_object.get("scaleY", 1)) | |
# Calculate scaling from original image to canvas size | |
scale_x = img_w / canvas_width | |
scale_y = img_h / canvas_height | |
# Convert canvas coords back to original image coords | |
original_left = int(canvas_left * scale_x) | |
original_top = int(canvas_top * scale_y) | |
original_width = int(canvas_width_scaled * scale_x) | |
original_height = int(canvas_height_scaled * scale_y) | |
# Clamp coordinates to image boundaries | |
original_left = max(0, original_left) | |
original_top = max(0, original_top) | |
if original_left + original_width > img_w: | |
original_width = img_w - original_left | |
if original_top + original_height > img_h: | |
original_height = img_h - original_top | |
# Ensure width/height are positive | |
original_width = max(1, original_width) | |
original_height = max(1, original_height) | |
# Create ROI dict | |
new_roi = { | |
"left": original_left, "top": original_top, | |
"width": original_width, "height": original_height | |
} | |
# Update session state only if ROI actually changed | |
# Compare dicts directly | |
if st.session_state.get("roi_coords") != new_roi: | |
st.session_state.roi_coords = new_roi | |
# Store the raw canvas JSON data to redraw it if page reloads | |
st.session_state.canvas_drawing = canvas_result.json_data | |
logger.info(f"New ROI selected (original coords): {new_roi}") | |
# Optionally show feedback - rerun might clear it quickly | |
# st.success(f"ROI updated: ({original_left},{original_top}) {original_width}x{original_height}", icon="π―") | |
# Handle case where drawing was cleared (no objects left) but maybe state wasn't reset | |
elif not canvas_result.json_data.get("objects") and st.session_state.get("roi_coords"): | |
logger.info("Canvas cleared, resetting ROI state.") | |
st.session_state.roi_coords = None | |
st.session_state.canvas_drawing = None | |
except Exception as canvas_err: | |
st.error(f"Error initializing or processing the drawing canvas: {canvas_err}") | |
logger.error(f"Canvas error: {canvas_err}", exc_info=True) | |
# Fallback to simple image display if canvas fails | |
st.image(display_img, caption="Image Preview (Canvas Error)", use_container_width=True) | |
else: | |
# Fallback if canvas is not available or disabled | |
st.image(display_img, caption="Image Preview", use_container_width=True) | |
if not DRAWABLE_CANVAS_AVAILABLE: | |
st.caption("ROI selection disabled (`streamlit-drawable-canvas` not installed).") | |
# Display current ROI coordinates if set | |
current_roi = st.session_state.get("roi_coords") | |
if current_roi: | |
st.info(f"Active ROI: (x={current_roi['left']}, y={current_roi['top']}), W={current_roi['width']}, H={current_roi['height']}", icon="π―") | |
st.markdown("---") # Separator | |
# --- Display DICOM Metadata Expander --- | |
if is_dicom and dicom_metadata: | |
with st.expander("π DICOM Metadata", expanded=False): | |
if UI_COMPONENTS_AVAILABLE: | |
# Use the dedicated function if available | |
display_dicom_metadata(dicom_metadata) | |
else: | |
# Simple fallback display | |
st.caption("Detailed DICOM metadata display requires `ui_components`.") | |
st.json({"metadata_preview": dict(list(dicom_metadata.items())[:10])}) # Show first 10 items | |
elif is_dicom: | |
# Case where it's DICOM but metadata extraction failed | |
st.caption("DICOM file loaded, but no metadata could be extracted.") | |
elif st.session_state.get("uploaded_file_info"): # Check if a file was uploaded but failed processing/display | |
st.error("Image preview failed. The file might be corrupted, in an unsupported format, or processing failed.") | |
logger.error("render_image_viewer called but display_img is not a valid Image object, despite file upload attempt.") | |
else: | |
# Default message when no file is uploaded yet | |
st.info("β¬ οΈ Please upload an image or activate Demo Mode using the sidebar.") | |
# ============================================================================== | |
# === Results Tabs Rendering Function ========================================== | |
# ============================================================================== | |
def render_results_tabs(TRANSLATION_SERVICE_AVAILABLE, translate_function): | |
""" | |
Renders the tabs for displaying analysis results and translation. | |
Args: | |
TRANSLATION_SERVICE_AVAILABLE (bool): Flag if translation can be used. | |
translate_function (callable or None): The actual function to call for translation. | |
""" | |
st.subheader("π Analysis & Results") | |
tab_titles = [ | |
"π¬ Initial Analysis", "π¬ Q&A History", "π©Ί Condition Focus", | |
"π Confidence", "π Translation" | |
] | |
tabs = st.tabs(tab_titles) | |
# --- Initial Analysis Tab --- | |
with tabs[0]: | |
analysis_text = st.session_state.get("initial_analysis", "") | |
display_text = analysis_text if analysis_text else "Run 'Initial Analysis' from the sidebar to see results here." | |
st.text_area( | |
"Overall Findings & Impressions", value=display_text, height=450, | |
disabled=True, key="initial_analysis_area" | |
) | |
# --- Q&A History Tab --- | |
with tabs[1]: | |
latest_answer = st.session_state.get("qa_answer", "") | |
display_text = latest_answer if latest_answer else "Ask a question using the sidebar to see the AI's latest response here." | |
st.text_area( | |
"Latest AI Answer", value=display_text, height=200, | |
disabled=True, key="qa_answer_area" | |
) | |
st.markdown("---") | |
history = st.session_state.get("history", []) | |
if history: | |
with st.expander("Full Conversation History", expanded=True): | |
for i, (q_type, message) in enumerate(reversed(history)): | |
# Simple formatting based on type | |
prefix = f"**{q_type}:**" | |
if q_type.lower() == "user question": | |
prefix = "**You:**" | |
elif q_type.lower() == "ai answer": | |
prefix = "**AI:**" | |
elif "[Fallback]" in q_type: | |
prefix = f"β οΈ **{q_type}:**" # Indicate fallback visually | |
st.markdown(f"{prefix}\n```text\n{message}\n```") | |
if i < len(history) - 1: st.markdown("---") # Separator | |
else: | |
st.caption("No questions asked yet in this session.") | |
# --- Condition Focus Tab --- | |
with tabs[2]: | |
disease_text = st.session_state.get("disease_analysis", "") | |
display_text = disease_text if disease_text else "Select a condition in the sidebar and click 'Analyze Condition' to see focused results." | |
st.text_area( | |
"Condition-Specific Analysis", value=display_text, height=450, | |
disabled=True, key="disease_analysis_area" | |
) | |
# --- Confidence Tab --- | |
with tabs[3]: | |
confidence_text = st.session_state.get("confidence_score", "") | |
display_text = confidence_text if confidence_text else "Click 'Estimate AI Confidence' in the sidebar (after performing analysis) to see results." | |
st.text_area( | |
"Estimated AI Confidence", value=display_text, height=450, | |
disabled=True, key="confidence_score_area" | |
) | |
# --- Translation Tab --- | |
with tabs[4]: | |
st.subheader("π Translate Analysis Text") | |
if not TRANSLATION_SERVICE_AVAILABLE or not TRANSLATION_AVAILABLE_UI: | |
st.warning("Translation features are unavailable. Ensure 'deep-translator' and necessary models/utils are correctly set up.") | |
else: | |
st.caption("Select text, choose languages, then click 'Translate'.") | |
# Options for text selection | |
text_options = { | |
"Initial Analysis": st.session_state.get("initial_analysis", ""), | |
"Latest Q&A Answer": st.session_state.get("qa_answer", ""), | |
"Condition Analysis": st.session_state.get("disease_analysis", ""), | |
"Confidence Estimation": st.session_state.get("confidence_score", ""), | |
"(Enter Custom Text Below)": "" # Placeholder | |
} | |
# Filter out empty options, always keep custom text option | |
available_options = { | |
label: txt for label, txt in text_options.items() if (txt and str(txt).strip()) or label == "(Enter Custom Text Below)" | |
} | |
if not available_options: # Handle case where no analysis has been done yet | |
available_options = {"(Enter Custom Text Below)": ""} | |
# Select box for choosing text source | |
selected_label = st.selectbox( | |
"Select text to translate:", list(available_options.keys()), | |
index=0, key="translate_source_select" | |
) | |
# Determine the text based on selection | |
text_to_translate_input = available_options.get(selected_label, "") | |
# Use state for custom text area to preserve value | |
custom_text_value = st.session_state.get("custom_translate_text", "") | |
if selected_label == "(Enter Custom Text Below)": | |
st.session_state.custom_translate_text = st.text_area( | |
"Enter or paste custom text to translate:", | |
value=custom_text_value, height=150, key="custom_translate_text_area" | |
) | |
text_to_translate_input = st.session_state.custom_translate_text | |
# Display the text that will be translated (read-only) | |
st.text_area( | |
"Text selected for translation:", value=text_to_translate_input, | |
height=100, disabled=True, key="text_to_translate_display" | |
) | |
# Language selection | |
col_lang1, col_lang2 = st.columns(2) | |
with col_lang1: | |
source_language_options = [AUTO_DETECT_INDICATOR] + sorted(list(LANGUAGE_CODES.keys())) | |
source_language_name = st.selectbox( | |
"Source Language:", source_language_options, index=0, | |
key="source_language_select" | |
) | |
with col_lang2: | |
target_language_options = sorted(list(LANGUAGE_CODES.keys())) | |
# Try to default to English or another common language | |
default_target_index = 0 | |
if "English" in target_language_options: | |
default_target_index = target_language_options.index("English") | |
elif len(target_language_options) > 0: | |
default_target_index = 0 # Fallback to first available | |
target_language_name = st.selectbox( | |
"Translate To:", target_language_options, index=default_target_index, | |
key="target_language_select" | |
) | |
# Translate Button | |
if st.button("π Translate Now", key="translate_button"): | |
st.session_state.translation_result = None # Clear previous results | |
st.session_state.translation_error = None | |
if not text_to_translate_input or not str(text_to_translate_input).strip(): | |
st.warning("Please select or enter some text to translate.") | |
st.session_state.translation_error = "Input text is empty." | |
elif source_language_name == target_language_name and source_language_name != AUTO_DETECT_INDICATOR: | |
st.info("Source and target languages are the same. No translation performed.") | |
st.session_state.translation_result = text_to_translate_input # Show original text | |
elif translate_function is None: | |
st.error("Translation function is not available. Cannot translate.") | |
st.session_state.translation_error = "Internal setup error: translate function missing." | |
else: | |
# Perform translation | |
with st.spinner(f"Translating from '{source_language_name}' to '{target_language_name}'..."): | |
try: | |
logger.info(f"Requesting translation: '{source_language_name}' -> '{target_language_name}'") | |
translation_output = translate_function( | |
text=text_to_translate_input, | |
target_language=target_language_name, | |
source_language=source_language_name # Pass name | |
) | |
if translation_output is not None: | |
st.session_state.translation_result = translation_output | |
st.success("Translation complete!") | |
logger.info("Translation successful.") | |
else: | |
st.error("Translation failed: Service returned no result. Check logs.") | |
st.session_state.translation_error = "Translation service returned an empty response." | |
logger.error("Translation function returned None.") | |
except Exception as e: | |
st.error(f"An error occurred during translation: {e}") | |
logger.error(f"Translation error: {e}", exc_info=True) | |
st.session_state.translation_error = f"Error: {e}" | |
# Display translation result or error | |
if st.session_state.get("translation_result"): | |
formatted_result = format_translation(st.session_state.translation_result) | |
st.text_area("Translated Text:", value=formatted_result, height=200, | |
disabled=True, key="translation_result_area") | |
elif st.session_state.get("translation_error"): | |
st.error(f"Translation Error: {st.session_state.translation_error}") | |
# ============================================================================== | |
# === Footer Rendering Function ================================================ | |
# ============================================================================== | |
def render_footer(session_id): | |
"""Renders the application footer.""" | |
st.markdown("---") | |
st.caption(f"βοΈ {config.APP_TITLE} | Session ID: {session_id or 'N/A'}") | |
# Use FOOTER_HTML from the imported config module | |
st.markdown(config.FOOTER_HTML, unsafe_allow_html=True) |