feat: implement batch build and comparison

This commit is contained in:
matt 2025-10-20 18:29:53 -07:00
parent 1d95c5cbd0
commit f1e21873e7
20 changed files with 2691 additions and 6 deletions

View file

@ -0,0 +1,8 @@
{# Batch Build Progress Indicator - Multiple Builds Running in Parallel #}
<div id="batch-progress-container" style="min-height: 300px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 2rem;">
<div hx-get="/build/batch-progress?batch_id={{ batch_id }}"
hx-trigger="load, every 1s"
hx-swap="innerHTML">
{% include "build/_batch_progress_content.html" %}
</div>
</div>

View file

@ -0,0 +1,37 @@
{# Batch Build Progress Content (inner content only, for HTMX updates) #}
<div style="text-align: center; max-width: 600px;">
<h3 style="margin-bottom: 1.5rem;">Building {{ build_count }} Decks...</h3>
<div style="margin: 2rem 0;">
<div style="font-size: 3rem; font-weight: bold; color: var(--primary);">
{{ completed }} / {{ build_count }}
</div>
<div class="muted" style="font-size: 0.9rem; margin-top: 0.5rem;">
{{ status }}
</div>
</div>
{# Progress Bar #}
<div style="width: 100%; height: 8px; background: var(--bg-secondary); border-radius: 4px; overflow: hidden; margin: 1.5rem 0;">
<div style="height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); transition: width 0.3s ease; width: {{ progress_pct }}%;"></div>
</div>
<div style="margin-top: 2rem; padding: 1rem; background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.3); border-radius: 8px;">
<p style="margin: 0; font-size: 0.9rem; line-height: 1.6;">
<strong>What's happening?</strong><br>
We're running your deck configuration {{ build_count }} times in parallel to see how card selection varies.
Each build uses the same commander, themes, and preferences but produces different results due to randomness in card selection.
</p>
</div>
{% if has_errors %}
<div class="error" style="margin-top: 1rem; text-align: left;">
<strong>⚠️ Some builds encountered errors</strong>
<p style="font-size: 0.85rem; margin-top: 0.5rem;">{{ error_count }} of {{ build_count }} builds failed. Completed builds will still be available for comparison.</p>
</div>
{% endif %}
<p class="muted" style="font-size: 0.8rem; margin-top: 1.5rem;">
This may take {{ time_estimate|default("1-3 minutes") }} depending on number of decks, theme complexity, and color count...
</p>
</div>

View file

@ -214,11 +214,37 @@
{% endif %}
{% endif %}
{% include "build/_new_deck_skip_controls.html" %}
{% if enable_batch_build %}
<fieldset>
<legend>Build Options</legend>
<div style="display:flex; flex-direction:column; gap:0.75rem;">
<label style="display:block;">
<span>Number of decks to build</span>
<small class="muted" style="display:block; font-size:11px; margin-top:.25rem;">Run the same configuration multiple times to see variance in results</small>
</label>
{% if ideals_ui_mode == 'slider' %}
<div style="display:flex; align-items:center; gap:1rem;">
<input type="range" name="build_count" id="build_count_slider" min="1" max="10" value="{{ form.build_count if form and form.build_count else 1 }}" style="flex:1;"
oninput="document.getElementById('build_count_value').textContent = this.value; updateBuildCountLabel(this.value); updateButtonState(this.value);" />
<span id="build_count_value" style="min-width:2.5rem; text-align:center; font-weight:500; font-size:1.1em;">{{ form.build_count if form and form.build_count else 1 }}</span>
</div>
<small id="build_count_label" class="muted" style="font-size:11px; text-align:center;">Build 1 deck (normal build)</small>
{% else %}
<input type="number" name="build_count" id="build_count_input" min="1" max="10" value="{{ form.build_count if form and form.build_count else 1 }}" style="width:6rem;"
oninput="updateButtonState(this.value);" />
<small class="muted" style="font-size:11px;">Enter 1 for normal build, 2-10 to compare multiple results</small>
{% endif %}
</div>
</fieldset>
{% else %}
{# Hidden input to always send build_count=1 when feature disabled #}
<input type="hidden" name="build_count" value="1" />
{% endif %}
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:space-between; margin-top:1rem;">
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
<div style="display:flex; gap:.5rem;">
<button type="submit" name="quick_build" value="1" class="btn-continue" id="quick-build-btn" title="Build entire deck automatically without approval steps">Quick Build</button>
<button type="submit" class="btn-continue">Create</button>
<button type="submit" class="btn-continue" id="create-btn">Create</button>
</div>
</div>
</form>
@ -248,6 +274,63 @@
}
})();
// Update build count label based on slider value
function updateBuildCountLabel(count) {
var label = document.getElementById('build_count_label');
if (!label) return;
count = parseInt(count);
if (count === 1) {
label.textContent = 'Build 1 deck (normal build)';
label.className = 'muted';
} else {
label.textContent = 'Build ' + count + ' decks and compare results';
label.className = 'muted';
label.style.color = '#60a5fa';
label.style.fontWeight = '500';
}
}
// Update button state based on build count
function updateButtonState(count) {
var quickBuildBtn = document.getElementById('quick-build-btn');
var createBtn = document.getElementById('create-btn');
count = parseInt(count);
if (count > 1) {
// Multi-build: force Quick Build, hide Create button
if (createBtn) {
createBtn.style.display = 'none';
}
if (quickBuildBtn) {
quickBuildBtn.textContent = 'Build ' + count + ' Decks';
quickBuildBtn.title = 'Build ' + count + ' decks automatically and compare results';
}
} else {
// Single build: show both buttons normally
if (createBtn) {
createBtn.style.display = '';
}
if (quickBuildBtn) {
quickBuildBtn.textContent = 'Quick Build';
quickBuildBtn.title = 'Build entire deck automatically without approval steps';
}
}
}
// Initialize label and button state on page load
(function() {
var slider = document.getElementById('build_count_slider');
var input = document.getElementById('build_count_input');
var initialValue = slider ? slider.value : (input ? input.value : 1);
if (slider) {
updateBuildCountLabel(initialValue);
}
updateButtonState(initialValue);
})();
// Utility function for parsing card lists
function parseCardList(content) {
const newlineRegex = /\r?\n/;