Refactor stream_editor.py for batch file support
Refactor stream_editor.py to improve code structure and readability. Added batch file handling and updated configuration loading.
This commit is contained in:
336
stream_editor.py
336
stream_editor.py
@@ -17,8 +17,8 @@ DEFAULTS = {
|
||||
"flf": 0,
|
||||
"seed": 0,
|
||||
"frame_to_skip": 81,
|
||||
"input_a_frames": 0, # INT
|
||||
"input_b_frames": 0, # INT
|
||||
"input_a_frames": 0,
|
||||
"input_b_frames": 0,
|
||||
"reference path": "",
|
||||
"reference switch": 1,
|
||||
"vace schedule": 1,
|
||||
@@ -39,15 +39,12 @@ DEFAULTS = {
|
||||
"prompt_history": []
|
||||
}
|
||||
|
||||
# Only these two types exist now
|
||||
GENERIC_TEMPLATES = ["prompt_i2v.json", "prompt_vace_extend.json"]
|
||||
GENERIC_TEMPLATES = ["prompt_i2v.json", "prompt_vace_extend.json", "batch_i2v.json", "batch_vace.json"]
|
||||
|
||||
# --- Helper Functions ---
|
||||
def load_config():
|
||||
if CONFIG_FILE.exists():
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
try: with open(CONFIG_FILE, 'r') as f: return json.load(f)
|
||||
except: pass
|
||||
return {"last_dir": str(Path.cwd()), "favorites": []}
|
||||
|
||||
@@ -57,9 +54,7 @@ def save_config(current_dir, favorites):
|
||||
|
||||
def load_snippets():
|
||||
if SNIPPETS_FILE.exists():
|
||||
try:
|
||||
with open(SNIPPETS_FILE, 'r') as f:
|
||||
return json.load(f)
|
||||
try: with open(SNIPPETS_FILE, 'r') as f: return json.load(f)
|
||||
except: pass
|
||||
return {}
|
||||
|
||||
@@ -77,21 +72,28 @@ def load_json(path):
|
||||
return data, get_file_mtime(path)
|
||||
|
||||
def save_json(path, data):
|
||||
clean_data = {k: v for k, v in data.items() if k in DEFAULTS or k == "prompt_history"}
|
||||
# For batch files, we might be saving a list or a dict containing a list
|
||||
# For single files, we save the dict
|
||||
if path.exists():
|
||||
try:
|
||||
with open(path, 'r') as f:
|
||||
existing = json.load(f)
|
||||
existing.update(clean_data)
|
||||
clean_data = existing
|
||||
if isinstance(existing, dict) and isinstance(data, dict):
|
||||
existing.update(data)
|
||||
data = existing
|
||||
except: pass
|
||||
|
||||
with open(path, 'w') as f:
|
||||
json.dump(clean_data, f, indent=4)
|
||||
json.dump(data, f, indent=4)
|
||||
return get_file_mtime(path)
|
||||
|
||||
def generate_templates(directory):
|
||||
for filename in GENERIC_TEMPLATES:
|
||||
path = directory / filename
|
||||
if "batch" in filename:
|
||||
# Batch template is a list of sequence objects
|
||||
data = {"batch_data": []} # Root object to hold list
|
||||
else:
|
||||
data = DEFAULTS.copy()
|
||||
if "vace" in filename:
|
||||
data.update({"frame_to_skip": 81, "vace schedule": 1, "video file path": ""})
|
||||
@@ -103,16 +105,12 @@ def generate_templates(directory):
|
||||
if 'config' not in st.session_state:
|
||||
st.session_state.config = load_config()
|
||||
st.session_state.current_dir = Path(st.session_state.config.get("last_dir", Path.cwd()))
|
||||
|
||||
if 'snippets' not in st.session_state:
|
||||
st.session_state.snippets = load_snippets()
|
||||
|
||||
if 'loaded_file' not in st.session_state:
|
||||
st.session_state.loaded_file = None
|
||||
if 'last_mtime' not in st.session_state:
|
||||
st.session_state.last_mtime = 0
|
||||
if 'edit_history_idx' not in st.session_state:
|
||||
st.session_state.edit_history_idx = None
|
||||
if 'snippets' not in st.session_state: st.session_state.snippets = load_snippets()
|
||||
if 'loaded_file' not in st.session_state: st.session_state.loaded_file = None
|
||||
if 'last_mtime' not in st.session_state: st.session_state.last_mtime = 0
|
||||
if 'edit_history_idx' not in st.session_state: st.session_state.edit_history_idx = None
|
||||
# Cache for single editor data to copy from
|
||||
if 'single_editor_cache' not in st.session_state: st.session_state.single_editor_cache = DEFAULTS.copy()
|
||||
|
||||
# --- Sidebar ---
|
||||
with st.sidebar:
|
||||
@@ -143,7 +141,7 @@ with st.sidebar:
|
||||
st.subheader("🧩 Snippet Library")
|
||||
with st.expander("Add New Snippet"):
|
||||
snip_name = st.text_input("Name", placeholder="e.g. Cinematic")
|
||||
snip_content = st.text_area("Content", placeholder="4k, high quality, dramatic lighting...")
|
||||
snip_content = st.text_area("Content", placeholder="4k, high quality...")
|
||||
if st.button("Save Snippet"):
|
||||
if snip_name and snip_content:
|
||||
st.session_state.snippets[snip_name] = snip_content
|
||||
@@ -169,15 +167,19 @@ with st.sidebar:
|
||||
json_files = [f for f in json_files if f.name != ".editor_config.json" and f.name != ".editor_snippets.json"]
|
||||
|
||||
if not json_files:
|
||||
if st.button("Generate Templates (I2V / VACE)"):
|
||||
if st.button("Generate Templates"):
|
||||
generate_templates(st.session_state.current_dir)
|
||||
st.rerun()
|
||||
|
||||
with st.expander("Create New JSON"):
|
||||
new_filename = st.text_input("Filename", placeholder="my_prompt_vace")
|
||||
is_batch = st.checkbox("Is Batch File?")
|
||||
if st.button("Create"):
|
||||
if not new_filename.endswith(".json"): new_filename += ".json"
|
||||
path = st.session_state.current_dir / new_filename
|
||||
if is_batch:
|
||||
data = {"batch_data": []}
|
||||
else:
|
||||
data = DEFAULTS.copy()
|
||||
if "vace" in new_filename: data.update({"frame_to_skip": 81, "vace schedule": 1, "video file path": ""})
|
||||
elif "i2v" in new_filename: data.update({"reference image path": "", "flf image path": ""})
|
||||
@@ -186,10 +188,11 @@ with st.sidebar:
|
||||
|
||||
selected_file_name = st.radio("Select File", [f.name for f in json_files])
|
||||
|
||||
# --- Main Editor Area ---
|
||||
# --- Load Logic ---
|
||||
if selected_file_name:
|
||||
file_path = st.session_state.current_dir / selected_file_name
|
||||
|
||||
# Reload check
|
||||
if st.session_state.loaded_file != str(file_path):
|
||||
data, mtime = load_json(file_path)
|
||||
st.session_state.data_cache = data
|
||||
@@ -203,15 +206,26 @@ if selected_file_name:
|
||||
|
||||
st.title(f"Editing: {selected_file_name}")
|
||||
|
||||
col1, col2 = st.columns([2, 1])
|
||||
# Determine File Type
|
||||
is_batch_file = "batch_data" in data or isinstance(data, list)
|
||||
|
||||
# --- TABS ---
|
||||
tab_single, tab_batch = st.tabs(["📝 Single Editor", "🚀 Batch Processor"])
|
||||
|
||||
# ==============================================================================
|
||||
# TAB 1: SINGLE EDITOR
|
||||
# ==============================================================================
|
||||
with tab_single:
|
||||
if is_batch_file:
|
||||
st.info("This is a batch file. Switch to the 'Batch Processor' tab to edit sequences.")
|
||||
else:
|
||||
# --- UI LAYOUT ---
|
||||
col1, col2 = st.columns([2, 1])
|
||||
with col1:
|
||||
# --- GENERAL SECTION (Collapsible) ---
|
||||
with st.expander("🌍 General Prompts (Global Layer)", expanded=False):
|
||||
gen_prompt = st.text_area("General Prompt", value=data.get("general_prompt", ""), height=100)
|
||||
gen_negative = st.text_area("General Negative", value=data.get("general_negative", DEFAULTS["general_negative"]), height=100)
|
||||
|
||||
# --- SPECIFIC SECTION ---
|
||||
st.write("📝 **Specific Prompts**")
|
||||
current_prompt_val = data.get("current_prompt", "")
|
||||
if 'append_prompt' in st.session_state:
|
||||
@@ -221,7 +235,6 @@ if selected_file_name:
|
||||
new_prompt = st.text_area("Specific Prompt", value=current_prompt_val, height=150)
|
||||
new_negative = st.text_area("Specific Negative", value=data.get("negative", ""), height=100)
|
||||
|
||||
# --- SEED ---
|
||||
col_seed_val, col_seed_btn = st.columns([4, 1])
|
||||
with col_seed_btn:
|
||||
st.write("")
|
||||
@@ -237,7 +250,6 @@ if selected_file_name:
|
||||
|
||||
st.subheader("LoRAs")
|
||||
st.code("<lora::1.0>", language="text")
|
||||
|
||||
l_col1, l_col2 = st.columns(2)
|
||||
loras = {}
|
||||
keys = ["lora 1 high", "lora 1 low", "lora 2 high", "lora 2 low", "lora 3 high", "lora 3 low"]
|
||||
@@ -247,210 +259,168 @@ if selected_file_name:
|
||||
|
||||
st.subheader("Settings")
|
||||
spec_fields = {}
|
||||
fname = selected_file_name
|
||||
|
||||
# --- DYNAMIC FIELD GENERATION ---
|
||||
# Shared fields
|
||||
spec_fields["camera"] = st.text_input("camera", value=str(data.get("camera", DEFAULTS["camera"])))
|
||||
spec_fields["flf"] = st.text_input("flf", value=str(data.get("flf", DEFAULTS["flf"])))
|
||||
|
||||
if "vace" in fname:
|
||||
# Integers
|
||||
if "vace" in selected_file_name:
|
||||
spec_fields["frame_to_skip"] = st.number_input("frame_to_skip", value=int(data.get("frame_to_skip", 81)))
|
||||
spec_fields["input_a_frames"] = st.number_input("input_a_frames", value=int(data.get("input_a_frames", 0)))
|
||||
spec_fields["input_b_frames"] = st.number_input("input_b_frames", value=int(data.get("input_b_frames", 0)))
|
||||
spec_fields["reference switch"] = st.number_input("reference switch", value=int(data.get("reference switch", 1)))
|
||||
spec_fields["vace schedule"] = st.number_input("vace schedule", value=int(data.get("vace schedule", 1)))
|
||||
|
||||
# Strings
|
||||
for f in ["reference path", "video file path", "reference image path"]:
|
||||
spec_fields[f] = st.text_input(f, value=str(data.get(f, "")))
|
||||
|
||||
elif "i2v" in fname:
|
||||
elif "i2v" in selected_file_name:
|
||||
for f in ["reference image path", "flf image path", "video file path"]:
|
||||
spec_fields[f] = st.text_input(f, value=str(data.get(f, "")))
|
||||
|
||||
with col2:
|
||||
st.subheader("Actions")
|
||||
# Store current state for "Copy to Batch" feature
|
||||
current_state = {
|
||||
"general_prompt": gen_prompt, "general_negative": gen_negative,
|
||||
"current_prompt": new_prompt, "negative": new_negative,
|
||||
"seed": new_seed, **loras, **spec_fields
|
||||
}
|
||||
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: File changed on disk!")
|
||||
c_col1, c_col2 = st.columns(2)
|
||||
if c_col1.button("Force Overwrite", type="primary"):
|
||||
data.update({
|
||||
"current_prompt": new_prompt, "negative": new_negative,
|
||||
"general_prompt": gen_prompt, "general_negative": gen_negative,
|
||||
"seed": new_seed
|
||||
})
|
||||
data.update(loras)
|
||||
data.update(spec_fields)
|
||||
st.error("⚠️ CONFLICT: Disk change detected!")
|
||||
c1, c2 = st.columns(2)
|
||||
if c1.button("Force Save"):
|
||||
data.update(current_state)
|
||||
st.session_state.last_mtime = save_json(file_path, data)
|
||||
st.session_state.data_cache = data
|
||||
st.toast("Forced overwrite success!", icon="⚠️")
|
||||
st.toast("Saved!", icon="⚠️")
|
||||
st.rerun()
|
||||
|
||||
if c_col2.button("Reload File"):
|
||||
if c2.button("Reload"):
|
||||
st.session_state.loaded_file = None
|
||||
st.rerun()
|
||||
|
||||
else:
|
||||
if st.button("💾 Update File", use_container_width=True):
|
||||
data.update({
|
||||
"current_prompt": new_prompt, "negative": new_negative,
|
||||
"general_prompt": gen_prompt, "general_negative": gen_negative,
|
||||
"seed": new_seed
|
||||
})
|
||||
data.update(loras)
|
||||
data.update(spec_fields)
|
||||
data.update(current_state)
|
||||
st.session_state.last_mtime = save_json(file_path, data)
|
||||
st.session_state.data_cache = data
|
||||
st.toast("File updated successfully!", icon="✅")
|
||||
st.toast("Updated!", icon="✅")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
archive_note = st.text_input("Archive Note (Optional)", placeholder="e.g. V1 with high motion")
|
||||
# History Logic
|
||||
archive_note = st.text_input("Archive Note")
|
||||
if st.button("📦 Snapshot to History", use_container_width=True):
|
||||
entry = {
|
||||
"general_prompt": gen_prompt, "general_negative": gen_negative,
|
||||
"prompt": new_prompt, "negative": new_negative,
|
||||
"seed": new_seed,
|
||||
"note": archive_note if archive_note else f"Snapshot {len(data.get('prompt_history', [])) + 1}",
|
||||
"loras": loras,
|
||||
**spec_fields
|
||||
"note": archive_note if archive_note else f"Snapshot",
|
||||
**current_state
|
||||
}
|
||||
if "prompt_history" not in data: data["prompt_history"] = []
|
||||
data["prompt_history"].insert(0, entry)
|
||||
# Update main state too
|
||||
data.update(entry)
|
||||
data["current_prompt"] = new_prompt
|
||||
|
||||
st.session_state.last_mtime = save_json(file_path, data)
|
||||
st.session_state.data_cache = data
|
||||
st.toast("Archived & Saved!", icon="📦")
|
||||
st.toast("Archived!", icon="📦")
|
||||
st.rerun()
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
st.subheader("Media Preview")
|
||||
preview_path = None
|
||||
for k in ["reference path", "video file path", "reference image path", "flf image path"]:
|
||||
if spec_fields.get(k):
|
||||
preview_path = spec_fields[k]
|
||||
break
|
||||
|
||||
if preview_path:
|
||||
full_prev_path = Path(preview_path) if os.path.isabs(preview_path) else st.session_state.current_dir / preview_path
|
||||
if full_prev_path.exists():
|
||||
ext = full_prev_path.suffix.lower()
|
||||
if ext in ['.mp4', '.avi', '.mov']:
|
||||
st.video(str(full_prev_path))
|
||||
else:
|
||||
st.image(str(full_prev_path))
|
||||
else:
|
||||
st.warning(f"File not found: {preview_path}")
|
||||
else:
|
||||
st.info("No media path set.")
|
||||
|
||||
# ---------------- HISTORY SECTION ----------------
|
||||
st.markdown("---")
|
||||
st.subheader("History")
|
||||
history = data.get("prompt_history", [])
|
||||
|
||||
h_head_1, h_head_2 = st.columns([1, 2])
|
||||
h_head_1.subheader(f"History ({len(history)})")
|
||||
search_term = h_head_2.text_input("🔍 Search History", placeholder="Filter...").lower()
|
||||
|
||||
if history:
|
||||
for idx, h in enumerate(history):
|
||||
note_text = str(h.get('note', '')).lower()
|
||||
if search_term and search_term not in note_text: continue
|
||||
|
||||
note_title = h.get('note', 'No Note') or "No Note"
|
||||
expander_label = f"#{idx+1}: {note_title}"
|
||||
|
||||
with st.container():
|
||||
if st.session_state.edit_history_idx == idx:
|
||||
with st.expander(f"📝 EDITING: {note_title}", expanded=True):
|
||||
st.info("Editing History Entry")
|
||||
edit_note = st.text_input("Note", value=h.get('note', ''), key=f"edit_note_{idx}")
|
||||
|
||||
ec_seed1, ec_seed2 = st.columns([1, 3])
|
||||
edit_seed = ec_seed1.number_input("Seed", value=int(h.get('seed', 0)), step=1, key=f"edit_seed_{idx}")
|
||||
|
||||
# EDIT ALL PROMPTS
|
||||
st.caption("General Layer")
|
||||
edit_gen_p = st.text_area("Gen Prompt", value=h.get('general_prompt', ''), height=60, key=f"egp_{idx}")
|
||||
edit_gen_n = st.text_area("Gen Negative", value=h.get('general_negative', ''), height=60, key=f"egn_{idx}")
|
||||
|
||||
st.caption("Specific Layer")
|
||||
edit_prompt = st.text_area("Prompt", value=h.get('prompt', ''), height=100, key=f"edit_prompt_{idx}")
|
||||
edit_negative = st.text_area("Negative", value=h.get('negative', ''), height=60, key=f"edit_neg_{idx}")
|
||||
|
||||
ec1, ec2 = st.columns([1, 4])
|
||||
if ec1.button("💾 Save", key=f"save_edit_{idx}", type="primary"):
|
||||
h.update({
|
||||
'note': edit_note, 'seed': edit_seed,
|
||||
'general_prompt': edit_gen_p, 'general_negative': edit_gen_n,
|
||||
'prompt': edit_prompt, 'negative': edit_negative
|
||||
})
|
||||
with st.expander(f"#{idx+1}: {h.get('note', 'No Note')}"):
|
||||
if st.button(f"Restore #{idx+1}", key=f"rest_{idx}"):
|
||||
data.update(h) # Simplified restore for brevity
|
||||
st.session_state.last_mtime = save_json(file_path, data)
|
||||
st.session_state.data_cache = data
|
||||
st.session_state.edit_history_idx = None
|
||||
st.toast("History entry updated!", icon="✏️")
|
||||
st.rerun()
|
||||
|
||||
if ec2.button("Cancel", key=f"cancel_edit_{idx}"):
|
||||
st.session_state.edit_history_idx = None
|
||||
# ==============================================================================
|
||||
# TAB 2: BATCH PROCESSOR
|
||||
# ==============================================================================
|
||||
with tab_batch:
|
||||
if not is_batch_file:
|
||||
st.warning("This is not a batch file. Please create a new JSON with 'Is Batch File' checked or open a batch json.")
|
||||
if st.button("Convert this file to Batch format?"):
|
||||
# Convert current settings to first batch item
|
||||
first_item = data.copy()
|
||||
if "prompt_history" in first_item: del first_item["prompt_history"]
|
||||
first_item["sequence_number"] = 1
|
||||
new_data = {"batch_data": [first_item], "prompt_history": data.get("prompt_history", [])}
|
||||
st.session_state.last_mtime = save_json(file_path, new_data)
|
||||
st.session_state.data_cache = new_data
|
||||
st.rerun()
|
||||
else:
|
||||
with st.expander(expander_label):
|
||||
col_h1, col_h2 = st.columns([3, 1])
|
||||
# BATCH EDITOR LOGIC
|
||||
batch_list = data.get("batch_data", [])
|
||||
|
||||
with col_h1:
|
||||
st.caption(f"📝 Prompts (Seed: {h.get('seed', 0)})")
|
||||
# Show combined snippet
|
||||
st.text(f"GEN: {h.get('general_prompt', '')[:50]}...\nSPEC: {h.get('prompt', '')[:50]}...")
|
||||
st.info(f"Batch contains {len(batch_list)} sequences.")
|
||||
|
||||
st.caption("🧩 LoRAs & Files")
|
||||
info_dict = {k:v for k,v in h.items() if k not in ['prompt', 'negative', 'general_prompt', 'general_negative', 'note', 'seed'] and v}
|
||||
if 'loras' in h and isinstance(h['loras'], dict):
|
||||
info_dict.update({k:v for k,v in h['loras'].items() if v})
|
||||
if 'loras' in info_dict: del info_dict['loras']
|
||||
st.json(info_dict, expanded=False)
|
||||
# --- RENDER SEQUENCES ---
|
||||
for i, seq in enumerate(batch_list):
|
||||
seq_num = seq.get("sequence_number", i+1)
|
||||
|
||||
with col_h2:
|
||||
if st.button("Restore", key=f"rest_{idx}", use_container_width=True):
|
||||
if is_conflict:
|
||||
st.error("Resolve conflict first.")
|
||||
else:
|
||||
data["current_prompt"] = h.get("prompt", "")
|
||||
data["negative"] = h.get("negative", "")
|
||||
data["general_prompt"] = h.get("general_prompt", "")
|
||||
data["general_negative"] = h.get("general_negative", "")
|
||||
data["seed"] = int(h.get("seed", 0))
|
||||
with st.expander(f"🎬 Sequence #{seq_num} : {seq.get('current_prompt', '')[:40]}...", expanded=False):
|
||||
|
||||
if "loras" in h and isinstance(h["loras"], dict):
|
||||
data.update(h["loras"])
|
||||
for k, v in h.items():
|
||||
if k not in ["note", "prompt", "loras", "negative", "seed", "general_prompt", "general_negative"]:
|
||||
data[k] = v
|
||||
# Action Bar
|
||||
b_col1, b_col2, b_col3 = st.columns([1, 1, 4])
|
||||
if b_col1.button("📥 Copy from Editor", key=f"copy_single_{i}", help="Paste settings from the Single Editor tab"):
|
||||
# Merge defaults + cached single editor state + keep sequence number
|
||||
updated_seq = DEFAULTS.copy()
|
||||
updated_seq.update(st.session_state.single_editor_cache)
|
||||
updated_seq["sequence_number"] = seq_num
|
||||
# Remove non-sequence keys
|
||||
if "prompt_history" in updated_seq: del updated_seq["prompt_history"]
|
||||
batch_list[i] = updated_seq
|
||||
save_json(file_path, data)
|
||||
st.toast(f"Sequence {seq_num} updated from Editor!", icon="📥")
|
||||
st.rerun()
|
||||
|
||||
if b_col2.button("🗑️ Remove", key=f"del_seq_{i}"):
|
||||
batch_list.pop(i)
|
||||
save_json(file_path, data)
|
||||
st.rerun()
|
||||
|
||||
# Editable Fields for this Sequence
|
||||
st.markdown("---")
|
||||
sb_col1, sb_col2 = st.columns([2, 1])
|
||||
|
||||
with sb_col1:
|
||||
# Prompts
|
||||
seq["general_prompt"] = st.text_area("General P", value=seq.get("general_prompt", ""), height=60, key=f"b_gp_{i}")
|
||||
seq["general_negative"] = st.text_area("General N", value=seq.get("general_negative", ""), height=60, key=f"b_gn_{i}")
|
||||
seq["current_prompt"] = st.text_area("Specific P", value=seq.get("current_prompt", ""), height=100, key=f"b_sp_{i}")
|
||||
seq["negative"] = st.text_area("Specific N", value=seq.get("negative", ""), height=60, key=f"b_sn_{i}")
|
||||
|
||||
with sb_col2:
|
||||
# Key settings
|
||||
seq["sequence_number"] = st.number_input("Seq Num", value=int(seq_num), key=f"b_seqn_{i}")
|
||||
seq["seed"] = st.number_input("Seed", value=int(seq.get("seed", 0)), key=f"b_seed_{i}")
|
||||
seq["camera"] = st.text_input("Camera", value=seq.get("camera", ""), key=f"b_cam_{i}")
|
||||
|
||||
# Paths
|
||||
if "video file path" in seq or "vace" in selected_file_name:
|
||||
seq["video file path"] = st.text_input("Video Path", value=seq.get("video file path", ""), key=f"b_vid_{i}")
|
||||
if "reference image path" in seq or "i2v" in selected_file_name:
|
||||
seq["reference image path"] = st.text_input("Ref Img", value=seq.get("reference image path", ""), key=f"b_ref_{i}")
|
||||
|
||||
st.markdown("---")
|
||||
if st.button("➕ Add New Sequence", type="primary"):
|
||||
# Create new blank sequence
|
||||
new_seq = DEFAULTS.copy()
|
||||
if "prompt_history" in new_seq: del new_seq["prompt_history"]
|
||||
|
||||
# Auto-increment
|
||||
max_seq = 0
|
||||
for s in batch_list:
|
||||
if "sequence_number" in s: max_seq = max(max_seq, int(s["sequence_number"]))
|
||||
new_seq["sequence_number"] = max_seq + 1
|
||||
|
||||
batch_list.append(new_seq)
|
||||
save_json(file_path, data)
|
||||
st.rerun()
|
||||
|
||||
# Global Batch Save
|
||||
if st.button("💾 Save Batch Changes"):
|
||||
data["batch_data"] = batch_list
|
||||
st.session_state.last_mtime = save_json(file_path, data)
|
||||
st.session_state.data_cache = data
|
||||
st.toast("Restored settings from history!", icon="⏪")
|
||||
st.rerun()
|
||||
|
||||
if st.button("✏️ Edit", key=f"open_edit_{idx}", use_container_width=True):
|
||||
st.session_state.edit_history_idx = idx
|
||||
st.rerun()
|
||||
|
||||
if st.button("Delete", key=f"del_{idx}", use_container_width=True):
|
||||
if is_conflict:
|
||||
st.error("Resolve conflict first.")
|
||||
else:
|
||||
data["prompt_history"].pop(idx)
|
||||
st.session_state.last_mtime = save_json(file_path, data)
|
||||
st.session_state.data_cache = data
|
||||
st.rerun()
|
||||
st.toast("Batch saved!", icon="🚀")
|
||||
|
||||
Reference in New Issue
Block a user