diff --git a/modules/web_app.py b/modules/web_app.py index ab2fb62..8ffa11a 100644 --- a/modules/web_app.py +++ b/modules/web_app.py @@ -11,7 +11,7 @@ from flask import Flask, render_template, request, redirect, url_for, flash, sen from flask_login import LoginManager, login_user, login_required, logout_user, current_user from werkzeug.security import generate_password_hash, check_password_hash from .web_db import db, User, Project, Run, LogEntry -from .web_tasks import huey, generate_book_task, regenerate_artifacts_task, rewrite_chapter_task +from .web_tasks import huey, generate_book_task, regenerate_artifacts_task, rewrite_chapter_task, refine_bible_task import config from . import utils from . import ai @@ -560,6 +560,28 @@ def clone_project(id): flash(f"Project cloned as '{new_name}'.") return redirect(url_for('view_project', id=new_proj.id)) +@app.route('/project//bible_comparison') +@login_required +def bible_comparison(id): + proj = db.session.get(Project, id) or Project.query.get_or_404(id) + if proj.user_id != current_user.id: return "Unauthorized", 403 + + bible_path = os.path.join(proj.folder_path, "bible.json") + draft_path = os.path.join(proj.folder_path, "bible_draft.json") + + if not os.path.exists(draft_path): + flash("No draft found. Please refine the bible first.") + return redirect(url_for('review_project', id=id)) + + original = utils.load_json(bible_path) + new_draft = utils.load_json(draft_path) + + if not original or not new_draft: + flash("Error loading bible data. Draft may be corrupt.") + return redirect(url_for('review_project', id=id)) + + return render_template('bible_comparison.html', project=proj, original=original, new=new_draft) + @app.route('/project//refine_bible', methods=['POST']) @login_required def refine_bible_route(id): @@ -580,7 +602,7 @@ def refine_bible_route(id): source_type = data.get('source', 'original') selected_keys = data.get('selected_keys') if isinstance(selected_keys, str): - try: selected_keys = json.loads(selected_keys) + try: selected_keys = json.loads(selected_keys) if selected_keys.strip() else [] except: selected_keys = [] # Start Background Task @@ -598,7 +620,7 @@ def confirm_bible_refinement(id): draft_path = os.path.join(proj.folder_path, "bible_draft.json") bible_path = os.path.join(proj.folder_path, "bible.json") - if action == 'accept_all': + if action == 'accept' or action == 'accept_all': if os.path.exists(draft_path): shutil.move(draft_path, bible_path) flash("Bible updated successfully.") @@ -613,7 +635,7 @@ def confirm_bible_refinement(id): draft = utils.load_json(draft_path) original = utils.load_json(bible_path) - original = _merge_selected_changes(original, draft, selected_keys) + original = story.merge_selected_changes(original, draft, selected_keys) with open(bible_path, 'w') as f: json.dump(original, f, indent=2) os.remove(draft_path) # Cleanup draft after merge diff --git a/modules/web_tasks.py b/modules/web_tasks.py index 4351d54..c1288fe 100644 --- a/modules/web_tasks.py +++ b/modules/web_tasks.py @@ -388,7 +388,7 @@ def refine_bible_task(project_path, instruction, source_type, selected_keys=None # If user selected specific changes, merge them into the base # This creates a "Proposed State" to refine further, WITHOUT modifying bible.json - if selected_keys and draft_bible: + if selected_keys is not None and draft_bible: base_bible = story.merge_selected_changes(base_bible, draft_bible, selected_keys) elif draft_bible: # If no specific keys but source is draft, assume we refine the whole draft diff --git a/templates/bible_comparison.html b/templates/bible_comparison.html index d8208f2..dbe82af 100644 --- a/templates/bible_comparison.html +++ b/templates/bible_comparison.html @@ -25,16 +25,19 @@ - +
+
+ +
@@ -46,9 +49,9 @@
Metadata
-
Title
{{ bible.project_metadata.title }}
-
Genre
{{ bible.project_metadata.genre }}
-
Tone
{{ bible.project_metadata.style.tone }}
+
Title
{{ bible.project_metadata.title }}
+
Genre
{{ bible.project_metadata.genre }}
+
Tone
{{ bible.project_metadata.style.tone }}
@@ -57,8 +60,9 @@ @@ -68,11 +72,12 @@
Plot Structure
{% for book in bible.books %}
+ Book {{ book.book_number }}: {{ book.title }} -

{{ book.manual_instruction }}

+

{{ book.manual_instruction }}

    {% for beat in book.plot_beats %} -
  1. {{ beat }}
  2. +
  3. {{ beat }}
  4. {% endfor %}
@@ -118,9 +123,57 @@ function showLoading(form) { btn.innerHTML = 'Refining...'; } +function submitRefine(event) { + event.preventDefault(); + const form = event.target; + showLoading(form); + + const instruction = form.querySelector('input[name="instruction"]').value; + const source = form.querySelector('input[name="source"]').value; + const selectedKeys = form.querySelector('input[name="selected_keys"]').value; + + fetch(`/project/{{ project.id }}/refine_bible`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ instruction: instruction, source: source, selected_keys: selectedKeys }) + }) + .then(res => res.json()) + .then(data => { + if (data.task_id) { + const modalHtml = ` + `; + document.body.insertAdjacentHTML('beforeend', modalHtml); + const modal = new bootstrap.Modal(document.getElementById('refineProgressModal')); + modal.show(); + + refinePollInterval = setInterval(() => { + fetch(`/task_status/${data.task_id}`).then(r => r.json()).then(status => { + if (status.status === 'completed') window.location.reload(); + }); + }, 2000); + } + }) + .catch(err => { + alert("Request failed: " + err); + const btn = form.querySelector('button[type="submit"]'); + btn.disabled = false; + btn.innerHTML = ' Refine Draft'; + }); +} + document.addEventListener('DOMContentLoaded', function() { const original = document.getElementById('original-col'); const newDraft = document.getElementById('new-col'); + const confirmInput = document.getElementById('confirmSelectedKeys'); + const refineInput = document.getElementById('refineSelectedKeys'); + const btnAcceptSelected = document.getElementById('btnAcceptSelected'); + const btnRefine = document.getElementById('btnRefine'); + const btnSelectAll = document.getElementById('btnSelectAll'); // 1. Highlight Differences const newElements = newDraft.querySelectorAll('[data-diff-key]'); @@ -128,13 +181,39 @@ document.addEventListener('DOMContentLoaded', function() { const key = el.getAttribute('data-diff-key'); const origEl = original.querySelector(`[data-diff-key="${key}"]`); + // Find associated checkbox (it might be a sibling or parent wrapper) + // In our macro, checkbox is usually a sibling of the span with data-diff-key + let checkbox = el.parentElement.querySelector(`input[value="${key}"]`); + if (!checkbox) { + // Try finding it in the parent li/div if the key matches the container + // Also handle nested keys: char.0.name -> char.0 + let parentKey = key; + if (key.startsWith('char.') && key.split('.').length > 2) { + parentKey = key.split('.').slice(0, 2).join('.'); + } else if (key.startsWith('book.') && key.split('.').length === 3 && key.split('.')[2] !== 'beat') { + // book.1.title -> book.1 (but book.1.beat.0 stays book.1.beat.0) + parentKey = key.split('.').slice(0, 2).join('.'); + } + + const container = el.closest('li, div'); + checkbox = container ? container.querySelector(`input[value="${parentKey}"]`) : null; + } + if (!origEl) { el.classList.add('diff-added'); el.title = "New item added"; + if (checkbox) { + checkbox.classList.remove('d-none'); + checkbox.checked = true; + } } else if (el.innerText.trim() !== origEl.innerText.trim()) { el.classList.add('diff-changed'); origEl.classList.add('diff-changed'); el.title = "Changed from original"; + if (checkbox) { + checkbox.classList.remove('d-none'); + checkbox.checked = true; + } } }); @@ -148,8 +227,52 @@ document.addEventListener('DOMContentLoaded', function() { el.title = "Removed in new draft"; } }); + + // Show Select All if there are visible checkboxes + if (newDraft.querySelector('.select-checkbox:not(.d-none)')) { + btnSelectAll.style.display = 'inline-block'; + } - // 2. Sync Scroll + function updateSelectionState() { + const checkboxes = newDraft.querySelectorAll('.select-checkbox:checked'); + const keys = Array.from(checkboxes).map(cb => cb.value); + const jsonKeys = JSON.stringify(keys); + + confirmInput.value = jsonKeys; + refineInput.value = jsonKeys; + + const count = keys.length; + + // Update Accept Button + btnAcceptSelected.disabled = count === 0; + btnAcceptSelected.innerHTML = count > 0 ? + ` Accept ${count} Selected` : + ` Accept Selected`; + + // Update Refine Button + btnRefine.innerHTML = count > 0 ? + ` Refine ${count} Selected` : + ` Refine Draft`; + } + + // 2. Handle Checkbox Selection + newDraft.addEventListener('change', function(e) { + if (e.target.classList.contains('select-checkbox')) { + updateSelectionState(); + } + }); + + btnSelectAll.addEventListener('click', function() { + const checkboxes = newDraft.querySelectorAll('.select-checkbox:not(.d-none)'); + const allChecked = Array.from(checkboxes).every(cb => cb.checked); + checkboxes.forEach(cb => cb.checked = !allChecked); + updateSelectionState(); + }); + + // Initialize state with default selections + updateSelectionState(); + + // 3. Sync Scroll let isSyncingLeft = false; let isSyncingRight = false;