mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat: implement batch build and comparison
This commit is contained in:
parent
1d95c5cbd0
commit
f1e21873e7
20 changed files with 2691 additions and 6 deletions
8
code/web/templates/build/_batch_progress.html
Normal file
8
code/web/templates/build/_batch_progress.html
Normal 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>
|
||||
37
code/web/templates/build/_batch_progress_content.html
Normal file
37
code/web/templates/build/_batch_progress_content.html
Normal 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>
|
||||
|
|
@ -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/;
|
||||
|
|
|
|||
111
code/web/templates/compare/_synergy_preview.html
Normal file
111
code/web/templates/compare/_synergy_preview.html
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{# Synergy Deck Preview - Shows the optimized "best-of" deck from batch builds #}
|
||||
|
||||
<div style="padding: 2rem; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 4px 12px rgba(0,0,0,.3);">
|
||||
<h2 style="margin-bottom: 1rem; color: var(--primary);">✨ Synergy Deck Preview</h2>
|
||||
|
||||
<div class="muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;">
|
||||
This deck is built from the most synergistic cards across all {{ total_builds }} builds, scored by frequency, EDHREC rank, and theme alignment.
|
||||
</div>
|
||||
|
||||
{# Summary Stats #}
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
||||
<div style="padding: 1rem; text-align: center; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;">
|
||||
<div style="font-size: 1.8rem; font-weight: bold; color: var(--primary);">{{ synergy_deck.total_cards }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">Total Cards</div>
|
||||
</div>
|
||||
<div style="padding: 1rem; text-align: center; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;">
|
||||
<div style="font-size: 1.8rem; font-weight: bold; color: #10b981;">{{ (synergy_deck.avg_frequency * 100) | round(1) }}%</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">Avg Frequency</div>
|
||||
</div>
|
||||
<div style="padding: 1rem; text-align: center; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;">
|
||||
<div style="font-size: 1.8rem; font-weight: bold; color: #3b82f6;">{{ synergy_deck.avg_score }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">Avg Synergy Score</div>
|
||||
</div>
|
||||
<div style="padding: 1rem; text-align: center; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;">
|
||||
<div style="font-size: 1.8rem; font-weight: bold; color: #8b5cf6;">{{ synergy_deck.high_frequency_count }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">High-Frequency Cards (80%+)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Cards by Category #}
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h3 style="margin-bottom: 1rem;">Cards by Type</h3>
|
||||
<div style="display: grid; gap: 1.5rem;">
|
||||
{% for category, cards in synergy_deck.cards_by_category.items() %}
|
||||
<details>
|
||||
<summary style="cursor: pointer; font-size: 1rem; font-weight: 500; margin-bottom: 0.75rem; color: var(--primary);">
|
||||
{{ category }} ({{ cards | sum(attribute='count') }})
|
||||
</summary>
|
||||
<div style="padding: 1rem; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem;">
|
||||
<thead>
|
||||
<tr style="border-bottom: 2px solid var(--border);">
|
||||
<th style="text-align: left; padding: 0.5rem;">Card Name</th>
|
||||
<th style="text-align: center; padding: 0.5rem;">Frequency</th>
|
||||
<th style="text-align: center; padding: 0.5rem;">Synergy Score</th>
|
||||
<th style="text-align: center; padding: 0.5rem;">Appears In</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for card in cards %}
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<td style="padding: 0.5rem;">
|
||||
<div>
|
||||
{% if card.count and card.count > 1 %}
|
||||
<span style="font-weight: 600; color: var(--primary);">{{ card.count }}x</span>
|
||||
{% endif %}
|
||||
<span data-card-name="{{ card.name }}" style="cursor: help;">{{ card.name }}</span>
|
||||
</div>
|
||||
{% if card.type_line %}
|
||||
<div class="muted" style="font-size: 0.8rem; margin-top: 0.15rem;">{{ card.type_line }}</div>
|
||||
{% endif %}
|
||||
{% if card.role %}
|
||||
<div class="muted" style="font-size: 0.75rem; margin-top: 0.1rem; font-style: italic;">{{ card.role }}</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center; padding: 0.5rem;">
|
||||
<span style="
|
||||
font-weight: 500;
|
||||
color: {% if card.frequency >= 0.8 %}#10b981{% elif card.frequency >= 0.5 %}#3b82f6{% else %}#6b7280{% endif %};
|
||||
">
|
||||
{{ (card.frequency * 100) | round(0) | int }}%
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: center; padding: 0.5rem;">
|
||||
<span style="
|
||||
font-weight: 500;
|
||||
color: {% if card.synergy_score >= 70 %}#8b5cf6{% elif card.synergy_score >= 50 %}#3b82f6{% else %}#6b7280{% endif %};
|
||||
">
|
||||
{{ card.synergy_score }}
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: center; padding: 0.5rem; color: #6b7280;">
|
||||
{{ card.appearance_count }}/{{ total_builds }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Actions #}
|
||||
<div style="display: flex; gap: 1rem; justify-content: center;">
|
||||
<button class="btn" onclick="document.getElementById('synergy-preview').innerHTML = ''">
|
||||
Close Preview
|
||||
</button>
|
||||
<button class="btn-continue" onclick="exportSynergyDeck('{{ batch_id }}')" id="export-synergy-btn">
|
||||
Export Synergy Deck
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Attach card hover to new elements
|
||||
if (window.attachCardHover) {
|
||||
window.attachCardHover();
|
||||
}
|
||||
</script>
|
||||
221
code/web/templates/compare/index.html
Normal file
221
code/web/templates/compare/index.html
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Compare Builds - {{ config.commander }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container" style="max-width: 1400px; padding: 2rem;">
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<h1 style="margin-bottom: 0.5rem;">Compare {{ build_count }} Builds</h1>
|
||||
<div class="muted" style="font-size: 0.9rem;">
|
||||
<strong>Commander:</strong> {{ config.commander }}
|
||||
{% if config.tags %}
|
||||
| <strong>Themes:</strong> {{ config.tags | join(", ") }}
|
||||
{% endif %}
|
||||
| <strong>Bracket:</strong> {{ config.bracket }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Overview Stats #}
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem;">
|
||||
<div class="card" style="padding: 1rem; text-align: center;">
|
||||
<div style="font-size: 2rem; font-weight: bold; color: var(--primary);">{{ overlap_stats.total_unique_cards }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">Unique Cards Total</div>
|
||||
</div>
|
||||
<div class="card" style="padding: 1rem; text-align: center;">
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #10b981;">{{ overlap_stats.cards_in_all }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">In All Builds</div>
|
||||
</div>
|
||||
<div class="card" style="padding: 1rem; text-align: center;">
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #3b82f6;">{{ overlap_stats.cards_in_most }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">In Most Builds (80%+)</div>
|
||||
</div>
|
||||
<div class="card" style="padding: 1rem; text-align: center;">
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #f59e0b;">{{ overlap_stats.cards_in_some }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">In Some Builds</div>
|
||||
</div>
|
||||
<div class="card" style="padding: 1rem; text-align: center;">
|
||||
<div style="font-size: 2rem; font-weight: bold; color: #ef4444;">{{ overlap_stats.cards_in_few }}</div>
|
||||
<div class="muted" style="font-size: 0.85rem;">In Few Builds</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Most Common Cards #}
|
||||
<details open style="margin-bottom: 2rem;">
|
||||
<summary style="cursor: pointer; font-size: 1.1rem; font-weight: 500; margin-bottom: 1rem;">
|
||||
📊 Most Common Cards
|
||||
</summary>
|
||||
<div class="card" style="padding: 1rem;">
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 0.5rem;">
|
||||
{% for card_name, count in overlap_stats.most_common[:20] %}
|
||||
<div style="display: flex; justify-content: space-between; padding: 0.5rem; background: rgba(59, 130, 246, 0.1); border-radius: 4px;">
|
||||
<span data-card-name="{{ card_name }}" style="cursor: help;">{{ card_name }}</span>
|
||||
<span style="font-weight: 500; color: var(--primary);">{{ count }}/{{ build_count }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{# Individual Build Comparisons #}
|
||||
<h2 style="margin-bottom: 1rem;">Individual Builds</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 1.5rem; margin-bottom: 2rem;">
|
||||
{% for build in builds %}
|
||||
<div class="card" style="padding: 1.5rem;">
|
||||
<h3 style="margin-bottom: 1rem; color: var(--primary);">Build #{{ build.build_number }}</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 1rem;">
|
||||
<div>
|
||||
<div class="muted" style="font-size: 0.8rem;">Total Cards</div>
|
||||
<div style="font-size: 1.5rem; font-weight: bold;">{{ build.total_cards }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted" style="font-size: 0.8rem;">Creatures</div>
|
||||
<div style="font-size: 1.5rem; font-weight: bold;">{{ build.creatures }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem; font-size: 0.85rem;">
|
||||
<div>
|
||||
<div class="muted">Lands</div>
|
||||
<div>{{ build.lands }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted">Artifacts</div>
|
||||
<div>{{ build.artifacts }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted">Enchantments</div>
|
||||
<div>{{ build.enchantments }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted">Instants</div>
|
||||
<div>{{ build.instants }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted">Sorceries</div>
|
||||
<div>{{ build.sorceries }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="muted">Planeswalkers</div>
|
||||
<div>{{ build.planeswalkers }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details style="margin-top: 1rem;">
|
||||
<summary style="cursor: pointer; font-size: 0.9rem; color: var(--primary);">
|
||||
View All Cards ({{ build.total_cards }})
|
||||
</summary>
|
||||
<div style="margin-top: 0.75rem; max-height: 300px; overflow-y: auto; font-size: 0.85rem;">
|
||||
{% for card in build.cards %}
|
||||
<div style="padding: 0.25rem 0; border-bottom: 1px solid var(--border);">
|
||||
<span data-card-name="{{ card.name if card is mapping else card }}" style="cursor: help;">
|
||||
{{ card.name if card is mapping else card }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Actions #}
|
||||
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-top: 2rem;">
|
||||
<a href="/build" class="btn">Build New Deck</a>
|
||||
<form method="POST" action="/compare/{{ batch_id }}/rebuild" style="display: inline;">
|
||||
<button type="submit" class="btn" title="Run {{ build_count }} more builds with the same configuration">
|
||||
🔄 Rebuild {{ build_count }}x
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
id="build-synergy-btn"
|
||||
class="btn-continue"
|
||||
hx-post="/compare/{{ batch_id }}/build-synergy"
|
||||
hx-target="#synergy-preview"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
✨ Build Synergy Deck
|
||||
</button>
|
||||
<form method="POST" action="/compare/{{ batch_id }}/export" style="display: inline;">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-continue"
|
||||
{% if synergy_exported %}disabled title="Individual batch files have been deleted after synergy export"{% endif %}
|
||||
style="{% if synergy_exported %}opacity: 0.5; cursor: not-allowed;{% endif %}"
|
||||
>
|
||||
{% if synergy_exported %}Batch Files Deleted{% else %}Export All Decks as ZIP{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{# Synergy Preview Container #}
|
||||
<div id="synergy-preview" style="margin-top: 2rem;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global function for exporting synergy deck (needed for HTMX-loaded content)
|
||||
function exportSynergyDeck(batchId) {
|
||||
const btn = document.getElementById('export-synergy-btn');
|
||||
|
||||
if (!batchId) {
|
||||
alert('Batch ID not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show warning about deleting batch files
|
||||
if (!confirm('⚠️ Warning: Exporting the synergy deck will delete all individual batch build files.\n\nThis action cannot be undone. After export, you will only be able to download the synergy deck.\n\nContinue with export?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button and show loading state
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Exporting...';
|
||||
|
||||
// Create a form and submit it to trigger download
|
||||
fetch(`/compare/${batchId}/export-synergy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Export failed');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
// Create download link
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = `synergy_deck_${batchId}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Update button state
|
||||
btn.textContent = '✓ Exported';
|
||||
btn.style.opacity = '0.6';
|
||||
|
||||
// Reload page to update batch export button state
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Export error:', error);
|
||||
alert('Failed to export synergy deck. Check console for details.');
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Export Synergy Deck';
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure card hover is attached after page load
|
||||
if (window.attachCardHover) {
|
||||
window.attachCardHover();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue