diff --git a/modules/story.py b/modules/story.py index ca85ad8..99f4060 100644 --- a/modules/story.py +++ b/modules/story.py @@ -66,6 +66,57 @@ def refresh_style_guidelines(model, folder=None): utils.log("SYSTEM", f"Failed to refresh guidelines: {e}") return current +def merge_selected_changes(original, draft, selected_keys): + """Helper to merge specific fields from draft to original bible.""" + # Sort keys to ensure deterministic order + def sort_key(k): + return [int(p) if p.isdigit() else p for p in k.split('.')] + selected_keys.sort(key=sort_key) + + for key in selected_keys: + parts = key.split('.') + + # Metadata: meta.title + if parts[0] == 'meta' and len(parts) == 2: + field = parts[1] + if field == 'tone': + original['project_metadata']['style']['tone'] = draft['project_metadata']['style']['tone'] + elif field in original['project_metadata']: + original['project_metadata'][field] = draft['project_metadata'][field] + + # Characters: char.0 + elif parts[0] == 'char' and len(parts) >= 2: + idx = int(parts[1]) + if idx < len(draft['characters']): + if idx < len(original['characters']): + original['characters'][idx] = draft['characters'][idx] + else: + original['characters'].append(draft['characters'][idx]) + + # Books: book.1.title + elif parts[0] == 'book' and len(parts) >= 2: + book_num = int(parts[1]) + orig_book = next((b for b in original['books'] if b['book_number'] == book_num), None) + draft_book = next((b for b in draft['books'] if b['book_number'] == book_num), None) + + if draft_book: + if not orig_book: + original['books'].append(draft_book) + original['books'].sort(key=lambda x: x.get('book_number', 999)) + continue + + if len(parts) == 2: + orig_book['title'] = draft_book['title'] + orig_book['manual_instruction'] = draft_book['manual_instruction'] + + elif len(parts) == 4 and parts[2] == 'beat': + beat_idx = int(parts[3]) + if beat_idx < len(draft_book['plot_beats']): + while len(orig_book['plot_beats']) <= beat_idx: + orig_book['plot_beats'].append("") + orig_book['plot_beats'][beat_idx] = draft_book['plot_beats'][beat_idx] + return original + def filter_characters(chars): """Removes placeholder characters generated by AI.""" blacklist = ['name', 'character name', 'role', 'protagonist', 'antagonist', 'love interest', 'unknown', 'tbd', 'todo', 'hero', 'villain', 'main character', 'side character'] diff --git a/modules/web_app.py b/modules/web_app.py index 080a844..ab2fb62 100644 --- a/modules/web_app.py +++ b/modules/web_app.py @@ -569,30 +569,65 @@ def refine_bible_route(id): if is_project_locked(id): flash("Project is locked. Clone it to make changes.") return redirect(url_for('view_project', id=id)) - - instruction = request.form.get('instruction') - if not instruction: - flash("Instruction required.") - return redirect(url_for('view_project', id=id)) - - bible_path = os.path.join(proj.folder_path, "bible.json") - bible = utils.load_json(bible_path) - if bible: - try: ai.init_models() - except: pass - - if not ai.model_logic: - flash("AI models not initialized.") - return redirect(url_for('view_project', id=id)) + # Handle JSON request (AJAX) or Form request + data = request.json if request.is_json else request.form + instruction = data.get('instruction') + + if not instruction: + return {"error": "Instruction required"}, 400 - new_bible = story.refine_bible(bible, instruction, proj.folder_path) - - if new_bible: - with open(bible_path, 'w') as f: json.dump(new_bible, f, indent=2) + source_type = data.get('source', 'original') + selected_keys = data.get('selected_keys') + if isinstance(selected_keys, str): + try: selected_keys = json.loads(selected_keys) + except: selected_keys = [] + + # Start Background Task + task = refine_bible_task(proj.folder_path, instruction, source_type, selected_keys) + + return {"status": "queued", "task_id": task.id} + +@app.route('/project//refine_bible/confirm', methods=['POST']) +@login_required +def confirm_bible_refinement(id): + proj = db.session.get(Project, id) or Project.query.get_or_404(id) + if proj.user_id != current_user.id: return "Unauthorized", 403 + + action = request.form.get('action') + 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 os.path.exists(draft_path): + shutil.move(draft_path, bible_path) flash("Bible updated successfully.") else: - flash("AI failed to update Bible. Check logs.") + flash("Draft expired or missing.") + + elif action == 'accept_selected': + if os.path.exists(draft_path) and os.path.exists(bible_path): + selected_keys_json = request.form.get('selected_keys', '[]') + try: + selected_keys = json.loads(selected_keys_json) + draft = utils.load_json(draft_path) + original = utils.load_json(bible_path) + + original = _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 + flash(f"Merged {len(selected_keys)} changes into Bible.") + + except Exception as e: + flash(f"Merge failed: {e}") + else: + flash("Files missing.") + + elif action == 'decline': + if os.path.exists(draft_path): + os.remove(draft_path) + flash("Changes discarded.") return redirect(url_for('view_project', id=id)) diff --git a/modules/web_tasks.py b/modules/web_tasks.py index e3bda83..4351d54 100644 --- a/modules/web_tasks.py +++ b/modules/web_tasks.py @@ -367,4 +367,45 @@ def rewrite_chapter_task(run_id, project_path, book_folder, chap_num, instructio return False except Exception as e: utils.log("ERROR", f"Rewrite task exception for run {run_id}/{book_folder}: {e}") + return False + +@huey.task() +def refine_bible_task(project_path, instruction, source_type, selected_keys=None): + """ + Background task to refine the Bible. + Handles partial merging of selected keys into a temp base before refinement. + """ + try: + bible_path = os.path.join(project_path, "bible.json") + draft_path = os.path.join(project_path, "bible_draft.json") + + base_bible = utils.load_json(bible_path) + if not base_bible: return False + + # If refining from draft, load it + if source_type == 'draft' and os.path.exists(draft_path): + draft_bible = utils.load_json(draft_path) + + # 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: + 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 + base_bible = draft_bible + + ai.init_models() + + # Run AI Refinement + new_bible = story.refine_bible(base_bible, instruction, project_path) + + if new_bible: + # Save to draft file (Overwrite previous draft) + with open(draft_path, 'w') as f: json.dump(new_bible, f, indent=2) + return True + + return False + + except Exception as e: + utils.log("ERROR", f"Bible refinement task failed: {e}") return False \ No newline at end of file diff --git a/templates/bible_comparison.html b/templates/bible_comparison.html new file mode 100644 index 0000000..d8208f2 --- /dev/null +++ b/templates/bible_comparison.html @@ -0,0 +1,173 @@ +{% extends "base.html" %} + +{% block content %} + + +
+

Review Changes

+
+ +
+ Review the AI's proposed changes below. You can accept them, discard them, or ask for further refinements on the New Draft. +
+ + +
+
+
+
+
+ + + + +
+
+
+
+
+ + +
+ + +
+
+
+
+
+ +{% macro render_bible(bible) %} +
+
Metadata
+
+
Title
{{ bible.project_metadata.title }}
+
Genre
{{ bible.project_metadata.genre }}
+
Tone
{{ bible.project_metadata.style.tone }}
+
+
+ +
+
Characters ({{ bible.characters|length }})
+ +
+ +
+
Plot Structure
+ {% for book in bible.books %} +
+ Book {{ book.book_number }}: {{ book.title }} +

{{ book.manual_instruction }}

+
    + {% for beat in book.plot_beats %} +
  1. {{ beat }}
  2. + {% endfor %} +
+
+ {% endfor %} +
+{% endmacro %} + +
+ +
+
+
+
Original
+
+
+ {{ render_bible(original) }} +
+
+
+ + +
+
+
+
New Draft
+
+
+ {{ render_bible(new) }} +
+
+
+
+ +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/project_review.html b/templates/project_review.html index 4f51604..e179e84 100644 --- a/templates/project_review.html +++ b/templates/project_review.html @@ -12,7 +12,7 @@

The AI has generated your characters and plot structure. Review them below and refine if needed.

-
+
@@ -88,5 +88,40 @@ function showLoading(form) { btn.disabled = true; btn.innerHTML = 'Refining...'; } + +function submitRefine(event) { + event.preventDefault(); + const form = event.target; + showLoading(form); + + const instruction = form.querySelector('input[name="instruction"]').value; + + fetch(`/project/{{ project.id }}/refine_bible`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ instruction: instruction, source: 'original' }) + }) + .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(); + + setInterval(() => { + fetch(`/task_status/${data.task_id}`).then(r => r.json()).then(status => { + if (status.status === 'completed') window.location.href = `/project/{{ project.id }}/bible_comparison`; + }); + }, 2000); + } + }); +} {% endblock %} \ No newline at end of file