import streamlit as st import random from utils import DEFAULTS, save_json, get_file_mtime def render_single_editor(data, file_path): is_batch_file = "batch_data" in data or isinstance(data, list) if is_batch_file: st.info("This is a batch file. Switch to the 'Batch Processor' tab.") return col1, col2 = st.columns([2, 1]) # Unique prefix for this file's widgets + Version Token (Fixes Restore bug) fk = f"{file_path.name}_v{st.session_state.ui_reset_token}" # --- FORM --- with col1: with st.expander("🌍 General Prompts (Global Layer)", expanded=False): gen_prompt = st.text_area("General Prompt", value=data.get("general_prompt", ""), height=100, key=f"{fk}_gp") gen_negative = st.text_area("General Negative", value=data.get("general_negative", DEFAULTS["general_negative"]), height=100, key=f"{fk}_gn") st.write("📝 **Specific Prompts**") current_prompt_val = data.get("current_prompt", "") if 'append_prompt' in st.session_state: current_prompt_val = (current_prompt_val.strip() + ", " + st.session_state.append_prompt).strip(', ') del st.session_state.append_prompt new_prompt = st.text_area("Specific Prompt", value=current_prompt_val, height=150, key=f"{fk}_sp") new_negative = st.text_area("Specific Negative", value=data.get("negative", ""), height=100, key=f"{fk}_sn") # Seed col_seed_val, col_seed_btn = st.columns([4, 1]) seed_key = f"{fk}_seed" with col_seed_btn: st.write("") st.write("") if st.button("🎲 Randomize", key=f"{fk}_rand"): st.session_state[seed_key] = random.randint(0, 999999999999) st.rerun() with col_seed_val: seed_val = st.session_state.get('rand_seed', int(data.get("seed", 0))) new_seed = st.number_input("Seed", value=seed_val, step=1, min_value=0, format="%d", key=seed_key) data["seed"] = new_seed # LoRAs st.subheader("LoRAs") l_col1, l_col2 = st.columns(2) loras = {} lora_keys = ["lora 1 high", "lora 1 low", "lora 2 high", "lora 2 low", "lora 3 high", "lora 3 low"] for i, k in enumerate(lora_keys): with (l_col1 if i % 2 == 0 else l_col2): loras[k] = st.text_input(k.title(), value=data.get(k, ""), key=f"{fk}_{k}") # Settings st.subheader("Settings") spec_fields = {} spec_fields["camera"] = st.text_input("Camera", value=str(data.get("camera", DEFAULTS["camera"])), key=f"{fk}_cam") spec_fields["flf"] = st.text_input("FLF", value=str(data.get("flf", DEFAULTS["flf"])), key=f"{fk}_flf") # Explicitly track standard setting keys to exclude them from custom list standard_keys = { "general_prompt", "general_negative", "current_prompt", "negative", "prompt", "seed", "camera", "flf", "batch_data", "prompt_history", "sequence_number", "ui_reset_token", "model_name", "vae_name", "steps", "cfg", "denoise", "sampler_name", "scheduler" } standard_keys.update(lora_keys) if "vace" in file_path.name: vace_keys = ["frame_to_skip", "input_a_frames", "input_b_frames", "reference switch", "vace schedule", "reference path", "video file path", "reference image path"] standard_keys.update(vace_keys) spec_fields["frame_to_skip"] = st.number_input("Frame to Skip", value=int(data.get("frame_to_skip", 81)), key=f"{fk}_fts") spec_fields["input_a_frames"] = st.number_input("Input A Frames", value=int(data.get("input_a_frames", 0)), key=f"{fk}_ia") spec_fields["input_b_frames"] = st.number_input("Input B Frames", value=int(data.get("input_b_frames", 0)), key=f"{fk}_ib") spec_fields["reference switch"] = st.number_input("Reference Switch", value=int(data.get("reference switch", 1)), key=f"{fk}_rsw") spec_fields["vace schedule"] = st.number_input("VACE Schedule", value=int(data.get("vace schedule", 1)), key=f"{fk}_vsc") for f in ["reference path", "video file path", "reference image path"]: spec_fields[f] = st.text_input(f.title(), value=str(data.get(f, "")), key=f"{fk}_{f}") elif "i2v" in file_path.name: i2v_keys = ["reference image path", "flf image path", "video file path"] standard_keys.update(i2v_keys) for f in i2v_keys: spec_fields[f] = st.text_input(f.title(), value=str(data.get(f, "")), key=f"{fk}_{f}") # --- CUSTOM PARAMETERS LOGIC --- st.markdown("---") st.subheader("🔧 Custom Parameters") # Filter keys: Only those NOT in the standard set custom_keys = [k for k in data.keys() if k not in standard_keys] keys_to_remove = [] if custom_keys: for k in custom_keys: c1, c2, c3 = st.columns([1, 2, 0.5]) c1.text_input("Key", value=k, disabled=True, key=f"{fk}_ck_lbl_{k}", label_visibility="collapsed") val = c2.text_input("Value", value=str(data[k]), key=f"{fk}_cv_{k}", label_visibility="collapsed") data[k] = val if c3.button("🗑️", key=f"{fk}_cdel_{k}"): keys_to_remove.append(k) else: st.caption("No custom keys added.") # Add New Key Interface with st.expander("➕ Add New Parameter"): nk_col, nv_col = st.columns(2) new_k = nk_col.text_input("Key Name", key=f"{fk}_new_k") new_v = nv_col.text_input("Value", key=f"{fk}_new_v") if st.button("Add Parameter", key=f"{fk}_add_cust"): if new_k and new_k not in data: data[new_k] = new_v st.rerun() elif new_k in data: st.error(f"Key '{new_k}' already exists!") # Apply Removals if keys_to_remove: for k in keys_to_remove: del data[k] st.rerun() # --- ACTIONS & HISTORY --- with col2: current_state = { "general_prompt": gen_prompt, "general_negative": gen_negative, "current_prompt": new_prompt, "negative": new_negative, "seed": new_seed, **loras, **spec_fields } # MERGE CUSTOM KEYS for k in custom_keys: if k not in keys_to_remove: current_state[k] = data[k] st.session_state.single_editor_cache = current_state st.subheader("Actions") current_disk_mtime = get_file_mtime(file_path) is_conflict = current_disk_mtime > st.session_state.last_mtime if is_conflict: st.error("⚠️ CONFLICT: Disk changed!") if st.button("Force Save"): data.update(current_state) save_json(file_path, data) # No return val in new utils st.session_state.last_mtime = get_file_mtime(file_path) # Manual Update st.session_state.data_cache = data st.toast("Saved!", icon="⚠️") st.rerun() if st.button("Reload File"): st.session_state.loaded_file = None st.rerun() else: if st.button("💾 Update File", use_container_width=True): data.update(current_state) save_json(file_path, data) st.session_state.last_mtime = get_file_mtime(file_path) st.session_state.data_cache = data st.toast("Updated!", icon="✅") st.markdown("---") archive_note = st.text_input("Archive Note") if st.button("📦 Snapshot to History", use_container_width=True): entry = {"note": archive_note if archive_note else "Snapshot", **current_state} if "prompt_history" not in data: data["prompt_history"] = [] data["prompt_history"].insert(0, entry) data.update(entry) save_json(file_path, data) st.session_state.last_mtime = get_file_mtime(file_path) st.session_state.data_cache = data st.toast("Archived!", icon="📦") st.rerun() # --- FULL HISTORY PANEL --- st.markdown("---") st.subheader("History") history = data.get("prompt_history", []) if not history: st.caption("No history yet.") for idx, h in enumerate(history): note = h.get('note', 'No Note') with st.container(): if st.session_state.edit_history_idx == idx: with st.expander(f"📝 Editing: {note}", expanded=True): edit_note = st.text_input("Note", value=note, key=f"h_en_{idx}") edit_seed = st.number_input("Seed", value=int(h.get('seed', 0)), key=f"h_es_{idx}") edit_gp = st.text_area("General P", value=h.get('general_prompt', ''), height=60, key=f"h_egp_{idx}") edit_gn = st.text_area("General N", value=h.get('general_negative', ''), height=60, key=f"h_egn_{idx}") edit_sp = st.text_area("Specific P", value=h.get('prompt', ''), height=100, key=f"h_esp_{idx}") edit_sn = st.text_area("Specific N", value=h.get('negative', ''), height=60, key=f"h_esn_{idx}") hc1, hc2 = st.columns([1, 4]) if hc1.button("💾 Save", key=f"h_save_{idx}"): h.update({ 'note': edit_note, 'seed': edit_seed, 'general_prompt': edit_gp, 'general_negative': edit_gn, 'prompt': edit_sp, 'negative': edit_sn }) save_json(file_path, data) st.session_state.last_mtime = get_file_mtime(file_path) st.session_state.data_cache = data st.session_state.edit_history_idx = None st.rerun() if hc2.button("Cancel", key=f"h_can_{idx}"): st.session_state.edit_history_idx = None st.rerun() else: with st.expander(f"#{idx+1}: {note}"): st.caption(f"Seed: {h.get('seed', 0)}") st.text(f"SPEC: {h.get('prompt', '')[:40]}...") view_data = {k:v for k,v in h.items() if k not in ['prompt', 'negative', 'general_prompt', 'general_negative', 'note']} st.json(view_data, expanded=False) bh1, bh2, bh3 = st.columns([2, 1, 1]) if bh1.button("Restore", key=f"h_rest_{idx}", use_container_width=True): data.update(h) if 'prompt' in h: data['current_prompt'] = h['prompt'] save_json(file_path, data) st.session_state.last_mtime = get_file_mtime(file_path) st.session_state.data_cache = data # Refresh UI st.session_state.ui_reset_token += 1 st.toast("Restored!", icon="⏪") st.rerun() if bh2.button("✏️", key=f"h_edit_{idx}"): st.session_state.edit_history_idx = idx st.rerun() if bh3.button("🗑️", key=f"h_del_{idx}"): history.pop(idx) save_json(file_path, data) st.session_state.last_mtime = get_file_mtime(file_path) st.session_state.data_cache = data st.rerun()