New comparison feature.

This commit is contained in:
2026-02-04 21:21:57 -05:00
parent 48dca539cd
commit ca221f0fb3
5 changed files with 356 additions and 21 deletions

View File

@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% block content %}
<style>
.diff-changed { background-color: #fff3cd; transition: background 0.5s; }
.diff-added { background-color: #d1e7dd; transition: background 0.5s; }
.diff-removed { background-color: #f8d7da; text-decoration: line-through; opacity: 0.7; transition: background 0.5s; }
.select-checkbox { transform: scale(1.2); cursor: pointer; }
</style>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-balance-scale me-2"></i>Review Changes</h2>
</div>
<div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i>Review the AI's proposed changes below. You can accept them, discard them, or ask for further refinements on the <strong>New Draft</strong>.
</div>
<!-- Actions Bar -->
<div class="card shadow-sm mb-4 sticky-top" style="top: 20px; z-index: 100;">
<div class="card-body bg-light">
<div class="row align-items-center">
<div class="col-md-6">
<form id="refineForm" onsubmit="submitRefine(event)" class="d-flex">
<input type="hidden" name="source" value="draft">
<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>
<button type="submit" class="btn btn-warning text-nowrap"><i class="fas fa-magic me-1"></i> Refine Draft</button>
</form>
</div>
<div class="col-md-6 text-end">
<form action="/project/{{ project.id }}/refine_bible/confirm" method="POST" class="d-inline">
<div class="form-check form-switch d-inline-block me-3 align-middle">
<input class="form-check-input" type="checkbox" id="syncScroll" checked>
<label class="form-check-label small" for="syncScroll">Sync Scroll</label>
</div>
<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" class="btn btn-success"><i class="fas fa-check me-1"></i> Accept Changes</button>
</form>
</div>
</div>
</div>
</div>
{% macro render_bible(bible) %}
<div class="mb-3">
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Metadata</h6>
<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">Genre</dt><dd class="col-sm-8" data-diff-key="meta.genre">{{ bible.project_metadata.genre }}</dd>
<dt class="col-sm-4">Tone</dt><dd class="col-sm-8" data-diff-key="meta.tone">{{ bible.project_metadata.style.tone }}</dd>
</dl>
</div>
<div class="mb-3">
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Characters ({{ bible.characters|length }})</h6>
<ul class="list-unstyled small">
{% for c in bible.characters %}
<li class="mb-2" data-diff-key="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>
<span class="text-muted" data-diff-key="char.{{ loop.index0 }}.desc">{{ c.description }}</span>
</li>
{% endfor %}
</ul>
</div>
<div class="mb-3">
<h6 class="text-muted text-uppercase small fw-bold border-bottom pb-1">Plot Structure</h6>
{% for book in bible.books %}
<div class="mb-2" data-diff-key="book.{{ book.book_number }}">
<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>
<ol class="small ps-3 mb-0">
{% for beat in book.plot_beats %}
<li data-diff-key="book.{{ book.book_number }}.beat.{{ loop.index0 }}">{{ beat }}</li>
{% endfor %}
</ol>
</div>
{% endfor %}
</div>
{% endmacro %}
<div class="row">
<!-- ORIGINAL -->
<div class="col-md-6">
<div class="card border-secondary mb-4">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">Original</h5>
</div>
<div class="card-body bg-light" id="original-col" style="max-height: 800px; overflow-y: auto;">
{{ render_bible(original) }}
</div>
</div>
</div>
<!-- NEW DRAFT -->
<div class="col-md-6">
<div class="card border-success mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">New Draft</h5>
</div>
<div class="card-body bg-white" id="new-col" style="max-height: 800px; overflow-y: auto;">
{{ render_bible(new) }}
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let refinePollInterval = null;
function showLoading(form) {
const btn = form.querySelector('button[type="submit"]');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Refining...';
}
document.addEventListener('DOMContentLoaded', function() {
const original = document.getElementById('original-col');
const newDraft = document.getElementById('new-col');
// 1. Highlight Differences
const newElements = newDraft.querySelectorAll('[data-diff-key]');
newElements.forEach(el => {
const key = el.getAttribute('data-diff-key');
const origEl = original.querySelector(`[data-diff-key="${key}"]`);
if (!origEl) {
el.classList.add('diff-added');
el.title = "New item added";
} else if (el.innerText.trim() !== origEl.innerText.trim()) {
el.classList.add('diff-changed');
origEl.classList.add('diff-changed');
el.title = "Changed from original";
}
});
// Check for removed items
const origElements = original.querySelectorAll('[data-diff-key]');
origElements.forEach(el => {
const key = el.getAttribute('data-diff-key');
const newEl = newDraft.querySelector(`[data-diff-key="${key}"]`);
if (!newEl) {
el.classList.add('diff-removed');
el.title = "Removed in new draft";
}
});
// 2. Sync Scroll
let isSyncingLeft = false;
let isSyncingRight = false;
original.onscroll = function() {
if (!isSyncingLeft && document.getElementById('syncScroll').checked) {
isSyncingRight = true;
newDraft.scrollTop = this.scrollTop;
}
isSyncingLeft = false;
};
newDraft.onscroll = function() {
if (!isSyncingRight && document.getElementById('syncScroll').checked) {
isSyncingLeft = true;
original.scrollTop = this.scrollTop;
}
isSyncingRight = false;
};
});
</script>
{% endblock %}

View File

@@ -12,7 +12,7 @@
<p class="text-muted">The AI has generated your characters and plot structure. Review them below and refine if needed.</p>
<!-- Refinement Bar -->
<form action="/project/{{ project.id }}/refine_bible" method="POST" class="mb-4" onsubmit="showLoading(this)">
<form id="refineForm" onsubmit="submitRefine(event)" class="mb-4">
<div class="input-group shadow-sm">
<span class="input-group-text bg-warning text-dark"><i class="fas fa-magic"></i></span>
<input type="text" name="instruction" class="form-control" placeholder="AI Instruction: e.g. 'Change the ending of Book 1', 'Add a plot point about the ring', 'Make the tone darker'" required>
@@ -88,5 +88,40 @@ function showLoading(form) {
btn.disabled = true;
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;
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 = `
<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 Bible...</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();
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);
}
});
}
</script>
{% endblock %}