import streamlit as st import requests from PIL import Image from io import BytesIO import urllib.parse import time # <--- NEW IMPORT from utils import save_config def render_single_instance(instance_config, index, all_instances, timeout_minutes): url = instance_config.get("url", "http://127.0.0.1:8188") name = instance_config.get("name", f"Server {index+1}") COMFY_URL = url.rstrip("/") # --- TIMEOUT LOGIC --- # Generate unique keys for session state toggle_key = f"live_toggle_{index}" start_time_key = f"live_start_{index}" # Check if we need to auto-close if st.session_state.get(toggle_key, False) and timeout_minutes > 0: start_time = st.session_state.get(start_time_key, 0) elapsed = time.time() - start_time if elapsed > (timeout_minutes * 60): st.session_state[toggle_key] = False # We don't need st.rerun() here because the fragment loop will pick up the state change on the next pass # but an explicit rerun makes it snappy. st.rerun() c_head, c_set = st.columns([3, 1]) c_head.markdown(f"### 🔌 {name}") with c_set.popover("⚙️ Settings"): st.caption("Press Update to apply changes!") new_name = st.text_input("Name", value=name, key=f"name_{index}") new_url = st.text_input("URL", value=url, key=f"url_{index}") if new_url != url: st.warning("⚠️ Unsaved URL! Click Update below.") if st.button("💾 Update & Save", key=f"save_{index}", type="primary"): all_instances[index]["name"] = new_name all_instances[index]["url"] = new_url st.session_state.config["comfy_instances"] = all_instances save_config( st.session_state.current_dir, st.session_state.config['favorites'], st.session_state.config ) st.toast("Server config saved!", icon="💾") st.rerun() st.divider() if st.button("🗑️ Remove Server", key=f"del_{index}"): all_instances.pop(index) st.session_state.config["comfy_instances"] = all_instances save_config( st.session_state.current_dir, st.session_state.config['favorites'], st.session_state.config ) st.rerun() # --- 1. STATUS DASHBOARD --- with st.expander("📊 Server Status", expanded=True): col1, col2, col3, col4 = st.columns([1, 1, 1, 1]) try: res = requests.get(f"{COMFY_URL}/queue", timeout=1.5) queue_data = res.json() running_cnt = len(queue_data.get("queue_running", [])) pending_cnt = len(queue_data.get("queue_pending", [])) col1.metric("Status", "🟢 Online" if running_cnt > 0 else "💤 Idle") col2.metric("Pending", pending_cnt) col3.metric("Running", running_cnt) if col4.button("🔄 Check Img", key=f"refresh_{index}", use_container_width=True): st.session_state[f"force_img_refresh_{index}"] = True except Exception: col1.metric("Status", "🔴 Offline") col2.metric("Pending", "-") col3.metric("Running", "-") st.error(f"Could not connect to API at {COMFY_URL}") # --- 2. LIVE VIEW (VIA REMOTE BROWSER) --- st.write("") c_label, c_ctrl = st.columns([1, 2]) c_label.subheader("📺 Live View") # Capture the toggle interaction to set start time def on_toggle_change(): if st.session_state[toggle_key]: st.session_state[start_time_key] = time.time() enable_preview = c_ctrl.checkbox( "Enable Live Preview", value=False, key=toggle_key, on_change=on_toggle_change ) if enable_preview: # Display Countdown if timeout is active if timeout_minutes > 0: elapsed = time.time() - st.session_state.get(start_time_key, time.time()) remaining = (timeout_minutes * 60) - elapsed st.caption(f"⏱️ Auto-off in: **{int(remaining)}s**") # Height Slider iframe_h = st.slider( "Height (px)", min_value=600, max_value=2500, value=1000, step=50, key=f"h_slider_{index}" ) # Get Configured Viewer URL viewer_base = st.session_state.config.get("viewer_url", "http://192.168.1.51:5800") final_src = viewer_base st.info(f"Viewing via Remote Browser: `{final_src}`") st.markdown( f""" """, unsafe_allow_html=True ) else: st.info("Live Preview is disabled.") st.markdown("---") # --- 3. LATEST OUTPUT --- if st.session_state.get(f"force_img_refresh_{index}", False): st.caption("🖼️ Most Recent Output") try: hist_res = requests.get(f"{COMFY_URL}/history", timeout=2) history = hist_res.json() if history: last_prompt_id = list(history.keys())[-1] outputs = history[last_prompt_id].get("outputs", {}) found_img = None for node_id, node_output in outputs.items(): if "images" in node_output: for img_info in node_output["images"]: if img_info["type"] == "output": found_img = img_info break if found_img: break if found_img: img_name = found_img['filename'] folder = found_img['subfolder'] img_type = found_img['type'] img_url = f"{COMFY_URL}/view?filename={img_name}&subfolder={folder}&type={img_type}" img_res = requests.get(img_url) image = Image.open(BytesIO(img_res.content)) st.image(image, caption=f"Last Output: {img_name}") else: st.warning("Last run had no image output.") else: st.info("No history found.") st.session_state[f"force_img_refresh_{index}"] = False except Exception as e: st.error(f"Error fetching image: {e}") # Check for fragment support (Streamlit 1.37+) if hasattr(st, "fragment"): # This decorator ensures this function re-runs every 10 seconds automatically # allowing it to catch the timeout even if you are away from the keyboard. @st.fragment(run_every=300) def _monitor_fragment(): _render_content() else: # Fallback for older Streamlit versions (Won't auto-refresh while idle) def _monitor_fragment(): _render_content() def _render_content(): # --- GLOBAL SETTINGS FOR MONITOR --- with st.expander("🔧 Monitor Settings", expanded=False): c_set1, c_set2 = st.columns(2) current_viewer = st.session_state.config.get("viewer_url", "http://192.168.1.51:5800") new_viewer = c_set1.text_input("Remote Browser URL", value=current_viewer, help="e.g., http://192.168.1.51:5800") # New Timeout Slider current_timeout = st.session_state.config.get("monitor_timeout", 0) new_timeout = c_set2.slider("Live Preview Timeout (Minutes)", 0, 60, value=current_timeout, help="0 = Always On. Sets how long the preview stays open before auto-closing.") if st.button("💾 Save Monitor Settings"): st.session_state.config["viewer_url"] = new_viewer st.session_state.config["monitor_timeout"] = new_timeout save_config( st.session_state.current_dir, st.session_state.config['favorites'], st.session_state.config ) st.success("Settings saved!") st.rerun() # --- INSTANCE MANAGEMENT --- if "comfy_instances" not in st.session_state.config: st.session_state.config["comfy_instances"] = [ {"name": "Main Server", "url": "http://192.168.1.100:8188"} ] instances = st.session_state.config["comfy_instances"] tab_names = [i["name"] for i in instances] + ["➕ Add Server"] tabs = st.tabs(tab_names) timeout_val = st.session_state.config.get("monitor_timeout", 0) for i, tab in enumerate(tabs[:-1]): with tab: render_single_instance(instances[i], i, instances, timeout_val) with tabs[-1]: st.header("Add New ComfyUI Instance") with st.form("add_server_form"): new_name = st.text_input("Server Name", placeholder="e.g. Render Node 2") new_url = st.text_input("URL", placeholder="http://192.168.1.50:8188") if st.form_submit_button("Add Instance"): if new_name and new_url: instances.append({"name": new_name, "url": new_url}) st.session_state.config["comfy_instances"] = instances save_config( st.session_state.current_dir, st.session_state.config['favorites'], st.session_state.config ) st.success("Server Added!") st.rerun() else: st.error("Please fill in both Name and URL.") def render_comfy_monitor(): # We call the wrapper which decides if it's a fragment or not _monitor_fragment()