Fixed refinement
This commit is contained in:
@@ -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 flask_login import LoginManager, login_user, login_required, logout_user, current_user
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from .web_db import db, User, Project, Run, LogEntry
|
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
|
import config
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import ai
|
from . import ai
|
||||||
@@ -560,6 +560,28 @@ def clone_project(id):
|
|||||||
flash(f"Project cloned as '{new_name}'.")
|
flash(f"Project cloned as '{new_name}'.")
|
||||||
return redirect(url_for('view_project', id=new_proj.id))
|
return redirect(url_for('view_project', id=new_proj.id))
|
||||||
|
|
||||||
|
@app.route('/project/<int:id>/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/<int:id>/refine_bible', methods=['POST'])
|
@app.route('/project/<int:id>/refine_bible', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def refine_bible_route(id):
|
def refine_bible_route(id):
|
||||||
@@ -580,7 +602,7 @@ def refine_bible_route(id):
|
|||||||
source_type = data.get('source', 'original')
|
source_type = data.get('source', 'original')
|
||||||
selected_keys = data.get('selected_keys')
|
selected_keys = data.get('selected_keys')
|
||||||
if isinstance(selected_keys, str):
|
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 = []
|
except: selected_keys = []
|
||||||
|
|
||||||
# Start Background Task
|
# Start Background Task
|
||||||
@@ -598,7 +620,7 @@ def confirm_bible_refinement(id):
|
|||||||
draft_path = os.path.join(proj.folder_path, "bible_draft.json")
|
draft_path = os.path.join(proj.folder_path, "bible_draft.json")
|
||||||
bible_path = os.path.join(proj.folder_path, "bible.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):
|
if os.path.exists(draft_path):
|
||||||
shutil.move(draft_path, bible_path)
|
shutil.move(draft_path, bible_path)
|
||||||
flash("Bible updated successfully.")
|
flash("Bible updated successfully.")
|
||||||
@@ -613,7 +635,7 @@ def confirm_bible_refinement(id):
|
|||||||
draft = utils.load_json(draft_path)
|
draft = utils.load_json(draft_path)
|
||||||
original = utils.load_json(bible_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)
|
with open(bible_path, 'w') as f: json.dump(original, f, indent=2)
|
||||||
os.remove(draft_path) # Cleanup draft after merge
|
os.remove(draft_path) # Cleanup draft after merge
|
||||||
|
|||||||
@@ -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
|
# If user selected specific changes, merge them into the base
|
||||||
# This creates a "Proposed State" to refine further, WITHOUT modifying bible.json
|
# 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)
|
base_bible = story.merge_selected_changes(base_bible, draft_bible, selected_keys)
|
||||||
elif draft_bible:
|
elif draft_bible:
|
||||||
# If no specific keys but source is draft, assume we refine the whole draft
|
# If no specific keys but source is draft, assume we refine the whole draft
|
||||||
|
|||||||
@@ -25,16 +25,19 @@
|
|||||||
<input type="hidden" name="source" value="draft">
|
<input type="hidden" name="source" value="draft">
|
||||||
<input type="hidden" name="selected_keys" id="refineSelectedKeys">
|
<input type="hidden" name="selected_keys" id="refineSelectedKeys">
|
||||||
<input type="text" name="instruction" class="form-control me-2" placeholder="Refine this draft further (e.g. 'Fix the name spelling')..." required>
|
<input type="text" name="instruction" class="form-control me-2" placeholder="Refine this draft further (e.g. 'Fix the name spelling')..." required>
|
||||||
<button type="submit" class="btn btn-warning text-nowrap"><i class="fas fa-magic me-1"></i> Refine Draft</button>
|
<button type="submit" id="btnRefine" class="btn btn-warning text-nowrap"><i class="fas fa-magic me-1"></i> Refine Draft</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 text-end">
|
<div class="col-md-6 text-end">
|
||||||
<form action="/project/{{ project.id }}/refine_bible/confirm" method="POST" class="d-inline">
|
<form action="/project/{{ project.id }}/refine_bible/confirm" method="POST" class="d-inline">
|
||||||
|
<input type="hidden" name="selected_keys" id="confirmSelectedKeys">
|
||||||
<div class="form-check form-switch d-inline-block me-3 align-middle">
|
<div class="form-check form-switch d-inline-block me-3 align-middle">
|
||||||
<input class="form-check-input" type="checkbox" id="syncScroll" checked>
|
<input class="form-check-input" type="checkbox" id="syncScroll" checked>
|
||||||
<label class="form-check-label small" for="syncScroll">Sync Scroll</label>
|
<label class="form-check-label small" for="syncScroll">Sync Scroll</label>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="btn btn-outline-secondary me-2" id="btnSelectAll" style="display:none;"><i class="fas fa-check-square me-1"></i> Select All</button>
|
||||||
<button type="submit" name="action" value="decline" class="btn btn-outline-danger me-2"><i class="fas fa-times me-1"></i> Discard</button>
|
<button type="submit" name="action" value="decline" class="btn btn-outline-danger me-2"><i class="fas fa-times me-1"></i> Discard</button>
|
||||||
|
<button type="submit" name="action" value="accept_selected" class="btn btn-outline-success me-2" id="btnAcceptSelected" disabled><i class="fas fa-check-double me-1"></i> Accept Selected</button>
|
||||||
<button type="submit" name="action" value="accept" class="btn btn-success"><i class="fas fa-check me-1"></i> Accept Changes</button>
|
<button type="submit" name="action" value="accept" class="btn btn-success"><i class="fas fa-check me-1"></i> Accept Changes</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,9 +49,9 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Metadata</h6>
|
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Metadata</h6>
|
||||||
<dl class="row small mb-0">
|
<dl class="row small mb-0">
|
||||||
<dt class="col-sm-4">Title</dt><dd class="col-sm-8" data-diff-key="meta.title">{{ bible.project_metadata.title }}</dd>
|
<dt class="col-sm-4">Title</dt><dd class="col-sm-8"><input type="checkbox" class="select-checkbox me-2 d-none" value="meta.title"><span data-diff-key="meta.title">{{ bible.project_metadata.title }}</span></dd>
|
||||||
<dt class="col-sm-4">Genre</dt><dd class="col-sm-8" data-diff-key="meta.genre">{{ bible.project_metadata.genre }}</dd>
|
<dt class="col-sm-4">Genre</dt><dd class="col-sm-8"><input type="checkbox" class="select-checkbox me-2 d-none" value="meta.genre"><span data-diff-key="meta.genre">{{ bible.project_metadata.genre }}</span></dd>
|
||||||
<dt class="col-sm-4">Tone</dt><dd class="col-sm-8" data-diff-key="meta.tone">{{ bible.project_metadata.style.tone }}</dd>
|
<dt class="col-sm-4">Tone</dt><dd class="col-sm-8"><input type="checkbox" class="select-checkbox me-2 d-none" value="meta.tone"><span data-diff-key="meta.tone">{{ bible.project_metadata.style.tone }}</span></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -57,8 +60,9 @@
|
|||||||
<ul class="list-unstyled small">
|
<ul class="list-unstyled small">
|
||||||
{% for c in bible.characters %}
|
{% for c in bible.characters %}
|
||||||
<li class="mb-2" data-diff-key="char.{{ loop.index0 }}">
|
<li class="mb-2" data-diff-key="char.{{ loop.index0 }}">
|
||||||
|
<input type="checkbox" class="select-checkbox me-2 d-none" value="char.{{ loop.index0 }}">
|
||||||
<strong data-diff-key="char.{{ loop.index0 }}.name">{{ c.name }}</strong> <span class="badge bg-light text-dark border" data-diff-key="char.{{ loop.index0 }}.role">{{ c.role }}</span><br>
|
<strong data-diff-key="char.{{ loop.index0 }}.name">{{ c.name }}</strong> <span class="badge bg-light text-dark border" data-diff-key="char.{{ loop.index0 }}.role">{{ c.role }}</span><br>
|
||||||
<span class="text-muted" data-diff-key="char.{{ loop.index0 }}.desc">{{ c.description }}</span>
|
<span class="text-muted ms-4" data-diff-key="char.{{ loop.index0 }}.desc">{{ c.description }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -68,11 +72,12 @@
|
|||||||
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Plot Structure</h6>
|
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Plot Structure</h6>
|
||||||
{% for book in bible.books %}
|
{% for book in bible.books %}
|
||||||
<div class="mb-2" data-diff-key="book.{{ book.book_number }}">
|
<div class="mb-2" data-diff-key="book.{{ book.book_number }}">
|
||||||
|
<input type="checkbox" class="select-checkbox me-2 d-none" value="book.{{ book.book_number }}">
|
||||||
<strong data-diff-key="book.{{ book.book_number }}.title">Book {{ book.book_number }}: {{ book.title }}</strong>
|
<strong data-diff-key="book.{{ book.book_number }}.title">Book {{ book.book_number }}: {{ book.title }}</strong>
|
||||||
<p class="fst-italic small text-muted mb-1" data-diff-key="book.{{ book.book_number }}.instr">{{ book.manual_instruction }}</p>
|
<p class="fst-italic small text-muted mb-1 ms-4" data-diff-key="book.{{ book.book_number }}.instr">{{ book.manual_instruction }}</p>
|
||||||
<ol class="small ps-3 mb-0">
|
<ol class="small ps-3 mb-0">
|
||||||
{% for beat in book.plot_beats %}
|
{% for beat in book.plot_beats %}
|
||||||
<li data-diff-key="book.{{ book.book_number }}.beat.{{ loop.index0 }}">{{ beat }}</li>
|
<li><input type="checkbox" class="select-checkbox me-2 d-none" value="book.{{ book.book_number }}.beat.{{ loop.index0 }}"><span data-diff-key="book.{{ book.book_number }}.beat.{{ loop.index0 }}">{{ beat }}</span></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,9 +123,57 @@ function showLoading(form) {
|
|||||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Refining...';
|
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>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 = `
|
||||||
|
<div class="modal fade" id="refineProgressModal" tabindex="-1" data-bs-backdrop="static">
|
||||||
|
<div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-body text-center p-4">
|
||||||
|
<div class="spinner-border text-warning mb-3" style="width: 3rem; height: 3rem;"></div>
|
||||||
|
<h4>Refining Draft...</h4><p class="text-muted">The AI is processing your changes.</p>
|
||||||
|
</div></div></div>
|
||||||
|
</div>`;
|
||||||
|
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 = '<i class="fas fa-magic me-1"></i> Refine Draft';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const original = document.getElementById('original-col');
|
const original = document.getElementById('original-col');
|
||||||
const newDraft = document.getElementById('new-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
|
// 1. Highlight Differences
|
||||||
const newElements = newDraft.querySelectorAll('[data-diff-key]');
|
const newElements = newDraft.querySelectorAll('[data-diff-key]');
|
||||||
@@ -128,13 +181,39 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const key = el.getAttribute('data-diff-key');
|
const key = el.getAttribute('data-diff-key');
|
||||||
const origEl = original.querySelector(`[data-diff-key="${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) {
|
if (!origEl) {
|
||||||
el.classList.add('diff-added');
|
el.classList.add('diff-added');
|
||||||
el.title = "New item added";
|
el.title = "New item added";
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.classList.remove('d-none');
|
||||||
|
checkbox.checked = true;
|
||||||
|
}
|
||||||
} else if (el.innerText.trim() !== origEl.innerText.trim()) {
|
} else if (el.innerText.trim() !== origEl.innerText.trim()) {
|
||||||
el.classList.add('diff-changed');
|
el.classList.add('diff-changed');
|
||||||
origEl.classList.add('diff-changed');
|
origEl.classList.add('diff-changed');
|
||||||
el.title = "Changed from original";
|
el.title = "Changed from original";
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.classList.remove('d-none');
|
||||||
|
checkbox.checked = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,7 +228,51 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Sync Scroll
|
// Show Select All if there are visible checkboxes
|
||||||
|
if (newDraft.querySelector('.select-checkbox:not(.d-none)')) {
|
||||||
|
btnSelectAll.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ?
|
||||||
|
`<i class="fas fa-check-double me-1"></i> Accept ${count} Selected` :
|
||||||
|
`<i class="fas fa-check-double me-1"></i> Accept Selected`;
|
||||||
|
|
||||||
|
// Update Refine Button
|
||||||
|
btnRefine.innerHTML = count > 0 ?
|
||||||
|
`<i class="fas fa-magic me-1"></i> Refine ${count} Selected` :
|
||||||
|
`<i class="fas fa-magic me-1"></i> 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 isSyncingLeft = false;
|
||||||
let isSyncingRight = false;
|
let isSyncingRight = false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user