init
This commit is contained in:
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM rust:1.85 as builder
|
||||
WORKDIR /app
|
||||
COPY backend ./backend
|
||||
COPY ayto ./ayto
|
||||
WORKDIR /app/backend
|
||||
RUN cargo build --release
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /app
|
||||
# We need to run the binary from a directory that has ../ayto relative to it
|
||||
RUN mkdir /app/backend_run
|
||||
COPY --from=builder /app/backend/target/release/backend /app/backend_run/backend
|
||||
COPY --from=builder /app/ayto /app/ayto
|
||||
WORKDIR /app/backend_run
|
||||
EXPOSE 8080
|
||||
CMD ["./backend"]
|
||||
BIN
ayto/favicon.ico
Normal file
BIN
ayto/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
660
ayto/index.html
Normal file
660
ayto/index.html
Normal file
@@ -0,0 +1,660 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full" data-theme="mocha">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Are You The One? Calculator</title>
|
||||
<link rel="icon" href="/ayto/favicon.ico" type="image/x-icon">
|
||||
<!-- Load Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Use Inter font -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root, [data-theme="mocha"] {
|
||||
--crust: #11111b;
|
||||
--mantle: #181825;
|
||||
--base: #1e1e2e;
|
||||
--surface0: #313244;
|
||||
--surface1: #45475a;
|
||||
--surface2: #585b70;
|
||||
--overlay0: #6c7086;
|
||||
--overlay1: #7f849c;
|
||||
--overlay2: #9399b2;
|
||||
--subtext0: #a6adc8;
|
||||
--subtext1: #bac2de;
|
||||
--text: #cdd6f4;
|
||||
--lavender: #b4befe;
|
||||
--blue: #89b4fa;
|
||||
--sapphire: #74c7ec;
|
||||
--sky: #89dceb;
|
||||
--teal: #94e2d5;
|
||||
--green: #a6e3a1;
|
||||
--yellow: #f9e2af;
|
||||
--peach: #fab387;
|
||||
--maroon: #eba0ac;
|
||||
--red: #f38ba8;
|
||||
--mauve: #cba6f7;
|
||||
--pink: #f5c2e7;
|
||||
--flamingo: #f2cdcd;
|
||||
--rosewater: #f5e0dc;
|
||||
}
|
||||
|
||||
[data-theme="latte"] {
|
||||
--crust: #dce0e8;
|
||||
--mantle: #e6e9ef;
|
||||
--base: #eff1f5;
|
||||
--surface0: #ccd0da;
|
||||
--surface1: #bcc0cc;
|
||||
--surface2: #acb0be;
|
||||
--overlay0: #9ca0b0;
|
||||
--overlay1: #8c8fa1;
|
||||
--overlay2: #7c7f93;
|
||||
--subtext0: #6c6f85;
|
||||
--subtext1: #5c5f77;
|
||||
--text: #4c4f69;
|
||||
--lavender: #7287fd;
|
||||
--blue: #1e66f5;
|
||||
--sapphire: #209fb5;
|
||||
--sky: #04a5e5;
|
||||
--teal: #179299;
|
||||
--green: #40a02b;
|
||||
--yellow: #df8e1d;
|
||||
--peach: #fe640b;
|
||||
--maroon: #e64553;
|
||||
--red: #d20f39;
|
||||
--mauve: #8839ef;
|
||||
--pink: #ea76cb;
|
||||
--flamingo: #dd7878;
|
||||
--rosewater: #dc8a78;
|
||||
}
|
||||
|
||||
[data-theme="frappe"] {
|
||||
--crust: #232634;
|
||||
--mantle: #292c3c;
|
||||
--base: #303446;
|
||||
--surface0: #414559;
|
||||
--surface1: #51576d;
|
||||
--surface2: #626880;
|
||||
--overlay0: #737994;
|
||||
--overlay1: #838ba7;
|
||||
--overlay2: #949cbb;
|
||||
--subtext0: #a5adce;
|
||||
--subtext1: #b5bfe2;
|
||||
--text: #c6d0f5;
|
||||
--lavender: #babbf1;
|
||||
--blue: #8caaee;
|
||||
--sapphire: #85c1dc;
|
||||
--sky: #99d1db;
|
||||
--teal: #81c8be;
|
||||
--green: #a6d189;
|
||||
--yellow: #e5c890;
|
||||
--peach: #ef9f76;
|
||||
--maroon: #ea999c;
|
||||
--red: #e78284;
|
||||
--mauve: #ca9ee6;
|
||||
--pink: #f4b8e4;
|
||||
--flamingo: #eebebe;
|
||||
--rosewater: #f2d5cf;
|
||||
}
|
||||
|
||||
[data-theme="macchiato"] {
|
||||
--crust: #181926;
|
||||
--mantle: #1e2030;
|
||||
--base: #24273a;
|
||||
--surface0: #363a4f;
|
||||
--surface1: #494d64;
|
||||
--surface2: #5b6078;
|
||||
--overlay0: #6e738d;
|
||||
--overlay1: #8087a2;
|
||||
--overlay2: #939ab7;
|
||||
--subtext0: #a5adcb;
|
||||
--subtext1: #b8c0e0;
|
||||
--text: #cad3f5;
|
||||
--lavender: #b7bdf8;
|
||||
--blue: #8aadf4;
|
||||
--sapphire: #7dc4e4;
|
||||
--sky: #91d7e3;
|
||||
--teal: #8bd5ca;
|
||||
--green: #a6da95;
|
||||
--yellow: #eed49f;
|
||||
--peach: #f5a97f;
|
||||
--maroon: #ee99a0;
|
||||
--red: #ed8796;
|
||||
--mauve: #c6a0f6;
|
||||
--pink: #f5bde6;
|
||||
--flamingo: #f0c6c6;
|
||||
--rosewater: #f4dbd6;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.table-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.table-container::-webkit-scrollbar-thumb { background-color: var(--overlay1); border-radius: 3px; }
|
||||
.table-container::-webkit-scrollbar-track { background-color: var(--mantle); }
|
||||
.prob-100 { background-color: var(--green); color: var(--base); font-weight: bold; }
|
||||
.prob-0 { background-color: var(--red); color: var(--base); }
|
||||
.prob-possible { background-color: var(--peach); color: var(--base); }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
rosewater: 'var(--rosewater)',
|
||||
flamingo: 'var(--flamingo)',
|
||||
pink: 'var(--pink)',
|
||||
mauve: 'var(--mauve)',
|
||||
red: 'var(--red)',
|
||||
maroon: 'var(--maroon)',
|
||||
peach: 'var(--peach)',
|
||||
yellow: 'var(--yellow)',
|
||||
green: 'var(--green)',
|
||||
teal: 'var(--teal)',
|
||||
sky: 'var(--sky)',
|
||||
sapphire: 'var(--sapphire)',
|
||||
blue: 'var(--blue)',
|
||||
lavender: 'var(--lavender)',
|
||||
text: 'var(--text)',
|
||||
subtext1: 'var(--subtext1)',
|
||||
subtext0: 'var(--subtext0)',
|
||||
overlay2: 'var(--overlay2)',
|
||||
overlay1: 'var(--overlay1)',
|
||||
overlay0: 'var(--overlay0)',
|
||||
surface2: 'var(--surface2)',
|
||||
surface1: 'var(--surface1)',
|
||||
surface0: 'var(--surface0)',
|
||||
base: 'var(--base)',
|
||||
mantle: 'var(--mantle)',
|
||||
crust: 'var(--crust)',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-base text-text min-h-full">
|
||||
|
||||
<!-- Theme Switcher -->
|
||||
<div class="w-full bg-mantle border-b border-surface0 p-2 flex justify-end items-center pr-4">
|
||||
<label for="theme-select" class="text-sm font-medium text-subtext1 mr-2">Theme:</label>
|
||||
<select id="theme-select" class="p-1 bg-surface1 border border-surface2 rounded text-text text-sm focus:ring-blue focus:border-blue">
|
||||
<option value="mocha">Mocha</option>
|
||||
<option value="macchiato">Macchiato</option>
|
||||
<option value="frappe">Frappé</option>
|
||||
<option value="latte">Latte</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="max-w-7xl mx-auto p-4 sm:p-8">
|
||||
<!-- Header -->
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-lavender mb-2">Are You The One? Calculator</h1>
|
||||
<p class="text-lg text-subtext1">Find the remaining possibilities and probabilities for your season.</p>
|
||||
</header>
|
||||
|
||||
<!-- Error Message Popup -->
|
||||
<div id="error-message" class="hidden fixed top-5 right-5 bg-red text-base p-4 rounded-md shadow-lg z-50 transition-all duration-300 ease-out opacity-0">
|
||||
<span id="error-text"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<div id="loading-spinner" class="hidden fixed inset-0 bg-base bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue"></div>
|
||||
<p class="ml-4 text-text text-lg">Calculating with Rust Backend...</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Left Column: Inputs -->
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Section 1: Setup Contestants -->
|
||||
<section id="setup-section" class="bg-surface1 p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">1. Setup Contestants</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="group1-names" class="block text-sm font-medium text-subtext1 mb-1">Group 1 (Matchers)</label>
|
||||
<textarea id="group1-names" rows="10" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="Enter one name per line...
|
||||
(Must be <= Group 2)
|
||||
Jake
|
||||
Brett
|
||||
..."></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="group2-names" class="block text-sm font-medium text-subtext1 mb-1">Group 2 (Pool)</label>
|
||||
<textarea id="group2-names" rows="10" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="Enter one name per line...
|
||||
(Must be >= Group 1)
|
||||
Jenna
|
||||
Kayla
|
||||
..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="setup-button" class="mt-4 w-full bg-blue hover:bg-sapphire text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Set Contestants
|
||||
</button>
|
||||
|
||||
<!-- Save/Load Buttons -->
|
||||
<div class="mt-4 grid grid-cols-2 gap-4">
|
||||
<button id="save-button" class="w-full bg-surface2 hover:bg-overlay0 text-subtext1 font-bold py-2 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out" disabled>
|
||||
Save Configuration
|
||||
</button>
|
||||
<button id="load-button" class="w-full bg-surface2 hover:bg-overlay0 text-subtext1 font-bold py-2 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Load Configuration
|
||||
</button>
|
||||
</div>
|
||||
<!-- Hidden file input for loading -->
|
||||
<input type="file" id="file-loader" class="hidden" accept=".json">
|
||||
|
||||
<p id="initial-possibilities-text" class="text-center mt-3 text-subtext0 text-sm italic"></p>
|
||||
</section>
|
||||
|
||||
<!-- Section 2: Truth Booths -->
|
||||
<section id="truth-booth-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">2. Add Truth Booths</h2>
|
||||
<form id="truth-booth-form" class="grid grid-cols-1 sm:grid-cols-3 gap-4 items-end">
|
||||
<div>
|
||||
<label for="tb-group1" class="block text-sm font-medium text-subtext1 mb-1">Group 1</label>
|
||||
<select id="tb-group1" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tb-group2" class="block text-sm font-medium text-subtext1 mb-1">Group 2</label>
|
||||
<select id="tb-group2" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tb-result" class="block text-sm font-medium text-subtext1 mb-1">Result</label>
|
||||
<select id="tb-result" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue">
|
||||
<option value="no-match">No Match</option>
|
||||
<option value="match">Perfect Match</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="sm:col-span-3 w-full bg-green hover:bg-teal text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Add Truth Booth Result
|
||||
</button>
|
||||
</form>
|
||||
<div id="truth-booth-list" class="mt-4 space-y-2"></div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3: Matchup Ceremonies -->
|
||||
<section id="ceremony-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">3. Add Matchup Ceremonies</h2>
|
||||
<form id="ceremony-form">
|
||||
<p class="text-subtext1 mb-4">Set the pairs for this ceremony. Ensure each person from Group 2 is selected at most once.</p>
|
||||
<div id="ceremony-pairs-container" class="space-y-3 mb-4"></div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label for="ceremony-beams" class="block text-sm font-medium text-subtext1 whitespace-nowrap">Correct Beams:</label>
|
||||
<input type="number" id="ceremony-beams" min="0" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="e.g., 3">
|
||||
</div>
|
||||
<button type="submit" class="mt-4 w-full bg-mauve hover:bg-pink text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Add Ceremony Result
|
||||
</button>
|
||||
</form>
|
||||
<div id="ceremony-list" class="mt-4 space-y-2"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Results -->
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Section 4: Calculate -->
|
||||
<section id="calculate-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden sticky top-8">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">4. Calculate Results</h2>
|
||||
<button id="calculate-button" class="w-full bg-blue hover:bg-sapphire text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Calculate Probabilities
|
||||
</button>
|
||||
|
||||
<div id="results-display" class="mt-6 text-center hidden">
|
||||
<h3 class="text-lg font-medium text-subtext1">Remaining Possibilities:</h3>
|
||||
<p id="total-possibilities" class="text-5xl font-bold text-text my-2">--</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5: Probability Grid -->
|
||||
<section id="grid-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">Probability Grid</h2>
|
||||
<p class="text-subtext1 mb-4 text-sm">This table shows the probability of each pair being a "Perfect Match" based on the remaining possibilities.</p>
|
||||
<div id="probability-grid-container" class="table-container overflow-x-auto rounded-lg border border-surface2">
|
||||
<table id="probability-table" class="w-full text-sm text-center">
|
||||
<thead id="probability-table-head" class="bg-surface0 text-subtext1 uppercase tracking-wider"></thead>
|
||||
<tbody id="probability-table-body" class="bg-surface1 divide-y divide-surface2"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
let group1Names = [];
|
||||
let group2Names = [];
|
||||
let truthBooths = [];
|
||||
let ceremonies = [];
|
||||
|
||||
const $ = (selector) => document.getElementById(selector);
|
||||
|
||||
const setupButton = $("setup-button");
|
||||
const calculateButton = $("calculate-button");
|
||||
const saveButton = $("save-button");
|
||||
const loadButton = $("load-button");
|
||||
const fileLoader = $("file-loader");
|
||||
const themeSelect = $("theme-select");
|
||||
|
||||
const g1NamesText = $("group1-names");
|
||||
const g2NamesText = $("group2-names");
|
||||
|
||||
const tbSection = $("truth-booth-section");
|
||||
const tbForm = $("truth-booth-form");
|
||||
const tbGroup1 = $("tb-group1");
|
||||
const tbGroup2 = $("tb-group2");
|
||||
const tbResult = $("tb-result");
|
||||
const tbList = $("truth-booth-list");
|
||||
|
||||
const ceremonySection = $("ceremony-section");
|
||||
const ceremonyForm = $("ceremony-form");
|
||||
const ceremonyPairsContainer = $("ceremony-pairs-container");
|
||||
const ceremonyBeams = $("ceremony-beams");
|
||||
const ceremonyList = $("ceremony-list");
|
||||
|
||||
const calculateSection = $("calculate-section");
|
||||
const gridSection = $("grid-section");
|
||||
const resultsDisplay = $("results-display");
|
||||
const totalPossibilitiesText = $("total-possibilities");
|
||||
|
||||
const probTableHead = $("probability-table-head");
|
||||
const probTableBody = $("probability-table-body");
|
||||
|
||||
const errorMessage = $("error-message");
|
||||
const errorText = $("error-text");
|
||||
const loadingSpinner = $("loading-spinner");
|
||||
const initialPossibilitiesText = $("initial-possibilities-text");
|
||||
|
||||
// Theme Handling
|
||||
themeSelect.addEventListener('change', (e) => {
|
||||
document.documentElement.setAttribute('data-theme', e.target.value);
|
||||
localStorage.setItem('ayto-theme', e.target.value);
|
||||
});
|
||||
|
||||
// Load saved theme
|
||||
const savedTheme = localStorage.getItem('ayto-theme');
|
||||
if (savedTheme) {
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
themeSelect.value = savedTheme;
|
||||
}
|
||||
|
||||
function showLoading() { loadingSpinner.classList.remove("hidden"); }
|
||||
function hideLoading() { loadingSpinner.classList.add("hidden"); }
|
||||
|
||||
function showError(message) {
|
||||
errorText.textContent = message;
|
||||
errorMessage.classList.remove("hidden", "opacity-0");
|
||||
setTimeout(() => {
|
||||
errorMessage.classList.add("opacity-0");
|
||||
setTimeout(() => errorMessage.classList.add("hidden"), 3000);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function handleSetupContestants() {
|
||||
const g1 = g1NamesText.value.split('\n').map(s => s.trim()).filter(s => s.length > 0);
|
||||
const g2 = g2NamesText.value.split('\n').map(s => s.trim()).filter(s => s.length > 0);
|
||||
|
||||
if (g1.length === 0 || g2.length === 0) {
|
||||
showError("Please enter names for both groups.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g1.length > g2.length) {
|
||||
showError("Group 1 must have fewer or an equal number of members as Group 2.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (new Set(g1).size !== g1.length || new Set(g2).size !== g2.length) {
|
||||
showError("Duplicate names are not allowed within a group.");
|
||||
return false;
|
||||
}
|
||||
|
||||
group1Names = g1;
|
||||
group2Names = g2;
|
||||
truthBooths = [];
|
||||
ceremonies = [];
|
||||
tbList.innerHTML = '';
|
||||
ceremonyList.innerHTML = '';
|
||||
|
||||
populateSelectors();
|
||||
populateCeremonyPairBuilder();
|
||||
|
||||
initialPossibilitiesText.textContent = `Calculator ready for ${group1Names.length} (G1) and ${group2Names.length} (G2) contestants.`;
|
||||
|
||||
tbSection.classList.remove("hidden");
|
||||
ceremonySection.classList.remove("hidden");
|
||||
calculateSection.classList.remove("hidden");
|
||||
gridSection.classList.remove("hidden");
|
||||
resultsDisplay.classList.add("hidden");
|
||||
totalPossibilitiesText.textContent = '--';
|
||||
probTableHead.innerHTML = '';
|
||||
probTableBody.innerHTML = '';
|
||||
|
||||
saveButton.disabled = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function populateSelectors() {
|
||||
tbGroup1.innerHTML = group1Names.map(name => `<option value="${name}">${name}</option>`).join('');
|
||||
tbGroup2.innerHTML = group2Names.map(name => `<option value="${name}">${name}</option>`).join('');
|
||||
}
|
||||
|
||||
function populateCeremonyPairBuilder() {
|
||||
ceremonyPairsContainer.innerHTML = group1Names.map(name => `
|
||||
<div class="grid grid-cols-3 gap-2 items-center">
|
||||
<label class="text-subtext1 text-right col-span-1">${name}</label>
|
||||
<span class="text-overlay0 text-center col-auto">-</span>
|
||||
<select data-g1-name="${name}" class="ceremony-pair-select w-full p-2 bg-surface0 border border-surface2 rounded-md text-text col-span-1 focus:ring-blue focus:border-blue">
|
||||
<option value="">Select match...</option>
|
||||
${group2Names.map(g2name => `<option value="${g2name}">${g2name}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderTruthBoothUI(booth) {
|
||||
const el = document.createElement('div');
|
||||
el.className = `p-3 rounded-md flex justify-between items-center text-base ${booth.isMatch ? 'bg-green' : 'bg-red'}`;
|
||||
el.innerHTML = `
|
||||
<span class="text-base"><strong>${booth.p1}</strong> & <strong>${booth.p2}</strong> = ${booth.isMatch ? 'PERFECT MATCH' : 'NO MATCH'}</span>
|
||||
<button data-id="${booth.id}" class="remove-tb-btn text-surface0 hover:text-crust text-2xl leading-none font-bold">×</button>
|
||||
`;
|
||||
tbList.appendChild(el);
|
||||
el.querySelector('.remove-tb-btn').addEventListener('click', handleRemoveTruthBooth);
|
||||
}
|
||||
|
||||
function handleAddTruthBooth(e) {
|
||||
e.preventDefault();
|
||||
const booth = { id: Date.now(), p1: tbGroup1.value, p2: tbGroup2.value, isMatch: tbResult.value === 'match' };
|
||||
truthBooths.push(booth);
|
||||
renderTruthBoothUI(booth);
|
||||
tbForm.reset();
|
||||
}
|
||||
|
||||
function handleRemoveTruthBooth(e) {
|
||||
const idToRemove = Number(e.target.dataset.id);
|
||||
truthBooths = truthBooths.filter(b => b.id !== idToRemove);
|
||||
e.target.parentElement.remove();
|
||||
}
|
||||
|
||||
function renderCeremonyUI(ceremony, index) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'p-3 rounded-md bg-surface2 ceremony-item';
|
||||
el.innerHTML = `
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold text-mauve"><strong>Ceremony ${index}</strong>: ${ceremony.beams} Beam${ceremony.beams === 1 ? '' : 's'}</span>
|
||||
<button data-id="${ceremony.id}" class="remove-ceremony-btn text-red hover:text-maroon text-2xl leading-none font-bold">×</button>
|
||||
</div>
|
||||
<div class="text-sm text-subtext0 mt-2 pl-4">
|
||||
${Object.entries(ceremony.pairs).map(([p1, p2]) => `<div>${p1} - ${p2}</div>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
ceremonyList.appendChild(el);
|
||||
el.querySelector('.remove-ceremony-btn').addEventListener('click', handleRemoveCeremony);
|
||||
}
|
||||
|
||||
function handleAddCeremony(e) {
|
||||
e.preventDefault();
|
||||
const pairSelectors = document.querySelectorAll('.ceremony-pair-select');
|
||||
const pairs = {};
|
||||
const selectedG2Names = [];
|
||||
|
||||
for (const select of pairSelectors) {
|
||||
const g1Name = select.dataset.g1Name;
|
||||
const g2Name = select.value;
|
||||
if (!g2Name) return showError(`Please select a match for ${g1Name}.`);
|
||||
selectedG2Names.push(g2Name);
|
||||
pairs[g1Name] = g2Name;
|
||||
}
|
||||
|
||||
if (new Set(selectedG2Names).size !== selectedG2Names.length) {
|
||||
return showError("Each person from Group 2 can only be matched once per ceremony.");
|
||||
}
|
||||
|
||||
const beams = parseInt(ceremonyBeams.value, 10);
|
||||
if (isNaN(beams) || beams < 0 || beams > group1Names.length) {
|
||||
return showError(`Beams must be a number between 0 and ${group1Names.length}.`);
|
||||
}
|
||||
|
||||
const ceremony = { id: Date.now(), pairs, beams };
|
||||
ceremonies.push(ceremony);
|
||||
renderCeremonyUI(ceremony, ceremonies.length);
|
||||
ceremonyForm.reset();
|
||||
}
|
||||
|
||||
function handleRemoveCeremony(e) {
|
||||
const idToRemove = Number(e.target.dataset.id);
|
||||
ceremonies = ceremonies.filter(c => c.id !== idToRemove);
|
||||
e.target.closest('.ceremony-item').remove();
|
||||
ceremonyList.innerHTML = '';
|
||||
ceremonies.forEach((c, i) => renderCeremonyUI(c, i + 1));
|
||||
}
|
||||
|
||||
async function handleCalculate() {
|
||||
showLoading();
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
group1: group1Names,
|
||||
group2: group2Names,
|
||||
truth_booths: truthBooths.map(tb => ({ p1: tb.p1, p2: tb.p2, match_: tb.isMatch })),
|
||||
ceremonies: ceremonies.map(c => ({ pairs: c.pairs, beams: c.beams }))
|
||||
};
|
||||
|
||||
const res = await fetch('/api/solve', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Server error: ${res.statusText}`);
|
||||
}
|
||||
|
||||
const results = await res.json();
|
||||
|
||||
totalPossibilitiesText.textContent = results.possibilities.toLocaleString();
|
||||
resultsDisplay.classList.remove("hidden");
|
||||
|
||||
if (results.possibilities === 0) {
|
||||
showError("Impossible scenario. The data entered contradicts itself.");
|
||||
probTableBody.innerHTML = '';
|
||||
probTableHead.innerHTML = '';
|
||||
} else {
|
||||
displayProbabilityGrid(results.grid_data);
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError("An error occurred connecting to the backend API.");
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function displayProbabilityGrid(gridData) {
|
||||
probTableHead.innerHTML = `
|
||||
<tr class="sticky top-0 bg-surface0 z-10">
|
||||
<th class="p-3"></th>
|
||||
${gridData.columns.map(name => `<th class="p-3 whitespace-nowrap">${name}</th>`).join('')}
|
||||
</tr>
|
||||
`;
|
||||
|
||||
probTableBody.innerHTML = gridData.index.map((g1Name, rowIndex) => `
|
||||
<tr class="hover:bg-surface2">
|
||||
<th class="p-3 whitespace-nowrap bg-surface0 sticky left-0 z-5">${g1Name}</th>
|
||||
${gridData.columns.map((g2Name, colIndex) => {
|
||||
const prob = gridData.data[rowIndex][colIndex] * 100;
|
||||
let cellClass = 'prob-possible';
|
||||
if (prob === 100) cellClass = 'prob-100';
|
||||
else if (prob === 0) cellClass = 'prob-0';
|
||||
return `<td class="${cellClass} p-3">${prob.toFixed(1)}%</td>`;
|
||||
}).join('')}
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function handleSaveConfig() {
|
||||
if (group1Names.length === 0) return showError("Please set contestants before saving.");
|
||||
const state = { group1Names, group2Names, truthBooths, ceremonies };
|
||||
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'ayto-save.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function handleLoadConfig() { fileLoader.click(); }
|
||||
|
||||
function handleFileSelected(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
try { loadState(JSON.parse(event.target.result)); }
|
||||
catch (err) { showError("Failed to load file."); }
|
||||
};
|
||||
reader.readAsText(file);
|
||||
e.target.value = null;
|
||||
}
|
||||
|
||||
function loadState(state) {
|
||||
if (!state.group1Names || !state.group2Names || !state.truthBooths || !state.ceremonies) {
|
||||
return showError("Invalid save file. Required fields are missing.");
|
||||
}
|
||||
g1NamesText.value = state.group1Names.join('\n');
|
||||
g2NamesText.value = state.group2Names.join('\n');
|
||||
if (!handleSetupContestants()) return;
|
||||
|
||||
truthBooths = state.truthBooths;
|
||||
ceremonies = state.ceremonies;
|
||||
|
||||
truthBooths.forEach(renderTruthBoothUI);
|
||||
ceremonies.forEach((c, i) => renderCeremonyUI(c, i + 1));
|
||||
|
||||
handleCalculate();
|
||||
}
|
||||
|
||||
setupButton.addEventListener("click", handleSetupContestants);
|
||||
tbForm.addEventListener("submit", handleAddTruthBooth);
|
||||
ceremonyForm.addEventListener("submit", handleAddCeremony);
|
||||
calculateButton.addEventListener("click", handleCalculate);
|
||||
saveButton.addEventListener("click", handleSaveConfig);
|
||||
loadButton.addEventListener("click", handleLoadConfig);
|
||||
fileLoader.addEventListener("change", handleFileSelected);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
698
ayto/index.html.bak
Normal file
698
ayto/index.html.bak
Normal file
@@ -0,0 +1,698 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="h-full">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Are You The One? Calculator (Catppuccin)</title>
|
||||
<!-- Load Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- Use Inter font -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- Load Pyodide (Python in the browser) -->
|
||||
<script src="https://cdn.jsdelivr.net/pyodide/v0.25.1/full/pyodide.js"></script>
|
||||
|
||||
<!-- Configure Tailwind with Catppuccin Mocha colors -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'rosewater': '#f5e0dc',
|
||||
'flamingo': '#f2cdcd',
|
||||
'pink': '#f5c2e7',
|
||||
'mauve': '#cba6f7',
|
||||
'red': '#f38ba8',
|
||||
'maroon': '#eba0ac',
|
||||
'peach': '#fab387',
|
||||
'yellow': '#f9e2af',
|
||||
'green': '#a6e3a1',
|
||||
'teal': '#94e2d5',
|
||||
'sky': '#89dceb',
|
||||
'sapphire': '#74c7ec',
|
||||
'blue': '#89b4fa',
|
||||
'lavender': '#b4befe',
|
||||
'text': '#cdd6f4',
|
||||
'subtext1': '#bac2de',
|
||||
'subtext0': '#a6adc8',
|
||||
'overlay2': '#9399b2',
|
||||
'overlay1': '#7f849c',
|
||||
'overlay0': '#6c7086',
|
||||
'surface2': '#585b70',
|
||||
'surface1': '#45475a',
|
||||
'surface0': '#313244',
|
||||
'base': '#1e1e2e',
|
||||
'mantle': '#181825',
|
||||
'crust': '#11111b',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
/* Custom scrollbar for probability table */
|
||||
.table-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.table-container::-webkit-scrollbar-thumb {
|
||||
background-color: #7f849c; /* overlay1 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
.table-container::-webkit-scrollbar-track {
|
||||
background-color: #181825; /* mantle */
|
||||
}
|
||||
/* Style for 100% match */
|
||||
.prob-100 {
|
||||
background-color: #a6e3a1; /* green */
|
||||
color: #1e1e2e; /* base */
|
||||
font-weight: bold;
|
||||
}
|
||||
/* Style for 0% match */
|
||||
.prob-0 {
|
||||
background-color: #f38ba8; /* red */
|
||||
color: #1e1e2e; /* base */
|
||||
}
|
||||
/* Style for possible match */
|
||||
.prob-possible {
|
||||
background-color: #fab387; /* peach */
|
||||
color: #1e1e2e; /* base */
|
||||
}
|
||||
|
||||
/* Style for Pyodide loading status */
|
||||
#pyodide-status {
|
||||
background-color: #181825; /* mantle */
|
||||
border-bottom: 1px solid #313244; /* surface0 */
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-base text-text min-h-full">
|
||||
|
||||
<!-- Pyodide Loading Status Bar -->
|
||||
<div id="pyodide-status" class="w-full text-center p-2">
|
||||
<p id="pyodide-status-text" class="text-subtext1 text-sm">Loading Python runtime...</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Container -->
|
||||
<div class="max-w-7xl mx-auto p-4 sm:p-8">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-4xl font-bold text-lavender mb-2">Are You The One? Calculator</h1>
|
||||
<p class="text-lg text-subtext1">Find the remaining possibilities and probabilities for your season.</p>
|
||||
</header>
|
||||
|
||||
<!-- Error Message Popup -->
|
||||
<div id="error-message" class="hidden fixed top-5 right-5 bg-red text-base p-4 rounded-md shadow-lg z-50 transition-all duration-300 ease-out opacity-0">
|
||||
<span id="error-text"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinner (for calculation) -->
|
||||
<div id="loading-spinner" class="hidden fixed inset-0 bg-base bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-blue"></div>
|
||||
<p class="ml-4 text-text text-lg">Calculating with Python...</p>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
|
||||
<!-- Left Column: Inputs -->
|
||||
<div class="flex flex-col gap-8">
|
||||
|
||||
<!-- Section 1: Setup Contestants -->
|
||||
<section id="setup-section" class="bg-surface1 p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">1. Setup Contestants</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="group1-names" class="block text-sm font-medium text-subtext1 mb-1">Group 1 (Matchers)</label>
|
||||
<textarea id="group1-names" rows="10" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="Enter one name per line...
|
||||
(Must be <= Group 2)
|
||||
Jake
|
||||
Brett
|
||||
Ryan
|
||||
..."></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="group2-names" class="block text-sm font-medium text-subtext1 mb-1">Group 2 (Pool)</label>
|
||||
<textarea id="group2-names" rows="10" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="Enter one name per line...
|
||||
(Must be >= Group 1)
|
||||
Jenna
|
||||
Kayla
|
||||
Amber
|
||||
..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button id="setup-button" class="mt-4 w-full bg-blue hover:bg-sapphire text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out" disabled>
|
||||
Set Contestants
|
||||
</button>
|
||||
<p id="initial-possibilities-text" class="text-center mt-3 text-subtext0 text-sm italic"></p>
|
||||
</section>
|
||||
|
||||
<!-- Section 2: Truth Booths -->
|
||||
<section id="truth-booth-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">2. Add Truth Booths</h2>
|
||||
<form id="truth-booth-form" class="grid grid-cols-1 sm:grid-cols-3 gap-4 items-end">
|
||||
<div>
|
||||
<label for="tb-group1" class="block text-sm font-medium text-subtext1 mb-1">Group 1</label>
|
||||
<select id="tb-group1" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tb-group2" class="block text-sm font-medium text-subtext1 mb-1">Group 2</label>
|
||||
<select id="tb-group2" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tb-result" class="block text-sm font-medium text-subtext1 mb-1">Result</label>
|
||||
<select id="tb-result" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text focus:ring-blue focus:border-blue">
|
||||
<option value="no-match">No Match</option>
|
||||
<option value="match">Perfect Match</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="sm:col-span-3 w-full bg-green hover:bg-teal text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Add Truth Booth Result
|
||||
</button>
|
||||
</form>
|
||||
<div id="truth-booth-list" class="mt-4 space-y-2">
|
||||
<!-- Truth booth results will be dynamically added here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 3: Matchup Ceremonies -->
|
||||
<section id="ceremony-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">3. Add Matchup Ceremonies</h2>
|
||||
<form id="ceremony-form">
|
||||
<p class="text-subtext1 mb-4">Set the pairs for this ceremony. Ensure each person from Group 2 is selected at most once.</p>
|
||||
<div id="ceremony-pairs-container" class="space-y-3 mb-4">
|
||||
<!-- Ceremony pair selectors will be dynamically added here -->
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<label for="ceremony-beams" class="block text-sm font-medium text-subtext1 whitespace-nowrap">Correct Beams:</label>
|
||||
<input type="number" id="ceremony-beams" min="0" class="w-full p-3 bg-surface0 border border-surface2 rounded-md text-text placeholder-subtext0 focus:ring-blue focus:border-blue" placeholder="e.g., 3">
|
||||
</div>
|
||||
<button type="submit" class="mt-4 w-full bg-mauve hover:bg-pink text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Add Ceremony Result
|
||||
</button>
|
||||
</form>
|
||||
<div id="ceremony-list" class="mt-4 space-y-2">
|
||||
<!-- Ceremony results will be dynamically added here -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Results -->
|
||||
<div class="flex flex-col gap-8">
|
||||
<!-- Section 4: Calculate -->
|
||||
<section id="calculate-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden sticky top-8">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">4. Calculate Results</h2>
|
||||
<button id="calculate-button" class="w-full bg-blue hover:bg-sapphire text-base font-bold py-3 px-4 rounded-md shadow-lg transition-all duration-200 ease-in-out">
|
||||
Calculate Probabilities
|
||||
</button>
|
||||
|
||||
<div id="results-display" class="mt-6 text-center hidden">
|
||||
<h3 class="text-lg font-medium text-subtext1">Remaining Possibilities:</h3>
|
||||
<p id="total-possibilities" class="text-5xl font-bold text-text my-2">--</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section 5: Probability Grid -->
|
||||
<section id="grid-section" class="bg-surface1 p-6 rounded-lg shadow-md hidden">
|
||||
<h2 class="text-2xl font-semibold mb-4 pb-2 border-b border-surface2 text-mauve">Probability Grid</h2>
|
||||
<p class="text-subtext1 mb-4 text-sm">This table shows the probability of each pair being a "Perfect Match" based on the remaining possibilities.</p>
|
||||
<div id="probability-grid-container" class="table-container overflow-x-auto rounded-lg border border-surface2">
|
||||
<table id="probability-table" class="w-full text-sm text-center">
|
||||
<thead id="probability-table-head" class="bg-surface0 text-subtext1 uppercase tracking-wider">
|
||||
<!-- Header row will be dynamically generated -->
|
||||
</thead>
|
||||
<tbody id="probability-table-body" class="bg-surface1 divide-y divide-surface2">
|
||||
<!-- Data rows will be dynamically generated -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
// --- STATE VARIABLES ---
|
||||
let group1Names = [];
|
||||
let group2Names = [];
|
||||
let truthBooths = [];
|
||||
let ceremonies = [];
|
||||
let pyodide = null; // Will hold the Pyodide instance
|
||||
let pyodideSolve = null; // Will hold the Python solve function
|
||||
|
||||
// --- DOM ELEMENTS ---
|
||||
const $ = (selector) => document.getElementById(selector);
|
||||
|
||||
const setupButton = $("setup-button");
|
||||
const calculateButton = $("calculate-button");
|
||||
|
||||
const g1NamesText = $("group1-names");
|
||||
const g2NamesText = $("group2-names");
|
||||
|
||||
const tbSection = $("truth-booth-section");
|
||||
const tbForm = $("truth-booth-form");
|
||||
const tbGroup1 = $("tb-group1");
|
||||
const tbGroup2 = $("tb-group2");
|
||||
const tbResult = $("tb-result");
|
||||
const tbList = $("truth-booth-list");
|
||||
|
||||
const ceremonySection = $("ceremony-section");
|
||||
const ceremonyForm = $("ceremony-form");
|
||||
const ceremonyPairsContainer = $("ceremony-pairs-container");
|
||||
const ceremonyBeams = $("ceremony-beams");
|
||||
const ceremonyList = $("ceremony-list");
|
||||
|
||||
const calculateSection = $("calculate-section");
|
||||
const gridSection = $("grid-section");
|
||||
const resultsDisplay = $("results-display");
|
||||
const totalPossibilitiesText = $("total-possibilities");
|
||||
|
||||
const probTableHead = $("probability-table-head");
|
||||
const probTableBody = $("probability-table-body");
|
||||
|
||||
const errorMessage = $("error-message");
|
||||
const errorText = $("error-text");
|
||||
const loadingSpinner = $("loading-spinner");
|
||||
const initialPossibilitiesText = $("initial-possibilities-text");
|
||||
const pyodideStatusText = $("pyodide-status-text");
|
||||
|
||||
// --- CORE LOGIC ---
|
||||
|
||||
/**
|
||||
* Shows a spinning loader
|
||||
*/
|
||||
function showLoading() {
|
||||
loadingSpinner.classList.remove("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the spinning loader
|
||||
*/
|
||||
function hideLoading() {
|
||||
loadingSpinner.classList.add("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays an error message popup
|
||||
* @param {string} message - The error message to display
|
||||
*/
|
||||
function showError(message) {
|
||||
errorText.textContent = message;
|
||||
errorMessage.classList.remove("hidden");
|
||||
errorMessage.classList.remove("opacity-0");
|
||||
|
||||
setTimeout(() => {
|
||||
errorMessage.classList.add("opacity-0");
|
||||
// Wait for transition to finish before hiding
|
||||
setTimeout(() => errorMessage.classList.add("hidden"), 3000);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes Pyodide, installs 'ayto', and defines the Python solver function.
|
||||
*/
|
||||
async function initPyodide() {
|
||||
try {
|
||||
pyodideStatusText.textContent = "Loading Python runtime...";
|
||||
pyodide = await loadPyodide();
|
||||
|
||||
pyodideStatusText.textContent = "Loading 'micropip' (Python package installer)...";
|
||||
await pyodide.loadPackage("micropip");
|
||||
|
||||
// Explicitly import micropip into the global scope after loading
|
||||
pyodide.runPython("import micropip");
|
||||
// Get a handle to the now-initialized micropip
|
||||
const micropip = pyodide.globals.get("micropip");
|
||||
|
||||
pyodideStatusText.textContent = "Installing 'ayto' library...";
|
||||
// The library is called 'ayto' on PyPI
|
||||
await micropip.install("ayto");
|
||||
|
||||
pyodideStatusText.textContent = "Defining Python solver function...";
|
||||
// Define the Python function that will do the hard work
|
||||
await pyodide.runPythonAsync(`
|
||||
from ayto import AYTO
|
||||
import pandas as pd
|
||||
import json
|
||||
|
||||
def solve(g1_names, g2_names, truth_booths, ceremonies):
|
||||
|
||||
calculator = AYTO(g1_names, g2_names)
|
||||
|
||||
for booth in truth_booths:
|
||||
# 'calc_probs' defaults to True, no need to pass
|
||||
calculator.apply_truth_booth(booth['p1'], booth['p2'], booth['match'])
|
||||
|
||||
for ceremony in ceremonies:
|
||||
# Convert ceremony pairs from dict to list of tuples
|
||||
matchup_list = list(ceremony['pairs'].items())
|
||||
calculator.apply_matchup_ceremony(matchup_list, ceremony['beams'])
|
||||
|
||||
# Note: No need to call calculate(), as apply_ methods do it.
|
||||
|
||||
num_possibilities = calculator.num_scenarios
|
||||
prob_df = calculator.probabilities
|
||||
|
||||
# Convert DataFrame to a JSON string for easy return
|
||||
prob_json = prob_df.to_json(orient='split')
|
||||
|
||||
return json.dumps({
|
||||
"possibilities": num_possibilities,
|
||||
"grid_json": prob_json
|
||||
})
|
||||
`);
|
||||
|
||||
// Get a reference to the Python function
|
||||
pyodideSolve = pyodide.globals.get('solve');
|
||||
|
||||
// Clean up micropip handle
|
||||
micropip.destroy();
|
||||
|
||||
pyodideStatusText.textContent = "Python ready. You may now set contestants.";
|
||||
setupButton.disabled = false;
|
||||
setupButton.textContent = "Set Contestants & Prepare Calculator";
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
pyodideStatusText.textContent = "Error loading Python environment. Please refresh.";
|
||||
showError("Failed to load Python. Check console for details.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses contestant names and validates them.
|
||||
*/
|
||||
function handleSetupContestants() {
|
||||
try {
|
||||
const g1 = g1NamesText.value.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
const g2 = g2NamesText.value.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(s => s.length > 0);
|
||||
|
||||
if (g1.length === 0 || g2.length === 0) {
|
||||
showError("Please enter names for both groups.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (g1.length > g2.length) {
|
||||
showError("Group 1 must have fewer or an equal number of members as Group 2.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for duplicate names
|
||||
if (new Set(g1).size !== g1.length || new Set(g2).size !== g2.length) {
|
||||
showError("Duplicate names are not allowed within a group.");
|
||||
return;
|
||||
}
|
||||
|
||||
group1Names = g1;
|
||||
group2Names = g2;
|
||||
|
||||
// Reset state
|
||||
truthBooths = [];
|
||||
ceremonies = [];
|
||||
tbList.innerHTML = '';
|
||||
ceremonyList.innerHTML = '';
|
||||
|
||||
// Update UI
|
||||
populateSelectors();
|
||||
populateCeremonyPairBuilder();
|
||||
|
||||
initialPossibilitiesText.textContent = `Calculator ready for ${group1Names.length} (G1) and ${group2Names.length} (G2) contestants.`;
|
||||
|
||||
// Show hidden sections
|
||||
tbSection.classList.remove("hidden");
|
||||
ceremonySection.classList.remove("hidden");
|
||||
calculateSection.classList.remove("hidden");
|
||||
gridSection.classList.remove("hidden");
|
||||
resultsDisplay.classList.add("hidden"); // Hide results until first calculation
|
||||
totalPossibilitiesText.textContent = '--';
|
||||
probTableHead.innerHTML = '';
|
||||
probTableBody.innerHTML = '';
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showError("An unexpected error occurred during setup.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates all dropdown selectors with contestant names.
|
||||
*/
|
||||
function populateSelectors() {
|
||||
// Populate Truth Booth selectors
|
||||
tbGroup1.innerHTML = group1Names.map(name => `<option value="${name}">${name}</option>`).join('');
|
||||
tbGroup2.innerHTML = group2Names.map(name => `<option value="${name}">${name}</option>`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the dynamic form for setting ceremony pairs.
|
||||
*/
|
||||
function populateCeremonyPairBuilder() {
|
||||
ceremonyPairsContainer.innerHTML = group1Names.map(name => `
|
||||
<div class="grid grid-cols-3 gap-2 items-center">
|
||||
<label class="text-subtext1 text-right col-span-1">${name}</label>
|
||||
<span class="text-overlay0 text-center col-auto">-</span>
|
||||
<select data-g1-name="${name}" class="ceremony-pair-select w-full p-2 bg-surface0 border border-surface2 rounded-md text-text col-span-1 focus:ring-blue focus:border-blue">
|
||||
<option value="">Select match...</option>
|
||||
${group2Names.map(g2name => `<option value="${g2name}">${g2name}</option>`).join('')}
|
||||
</select>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the "Add Truth Booth" form submission.
|
||||
*/
|
||||
function handleAddTruthBooth(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const p1 = tbGroup1.value;
|
||||
const p2 = tbGroup2.value;
|
||||
const isMatch = tbResult.value === 'match';
|
||||
|
||||
// Note: 'isMatch' is the local JS variable name.
|
||||
// It will be passed to Python as 'match' in handleCalculate.
|
||||
const booth = { id: Date.now(), p1, p2, isMatch };
|
||||
truthBooths.push(booth);
|
||||
|
||||
// Update UI
|
||||
const el = document.createElement('div');
|
||||
el.className = `p-3 rounded-md flex justify-between items-center text-base ${isMatch ? 'bg-green' : 'bg-red'}`;
|
||||
el.innerHTML = `
|
||||
<span><strong>${p1}</strong> & <strong>${p2}</strong> = ${isMatch ? 'PERFECT MATCH' : 'NO MATCH'}</span>
|
||||
<button data-id="${booth.id}" class="remove-tb-btn text-surface0 hover:text-crust text-2xl leading-none font-bold">×</button>
|
||||
`;
|
||||
tbList.appendChild(el);
|
||||
|
||||
el.querySelector('.remove-tb-btn').addEventListener('click', handleRemoveTruthBooth);
|
||||
tbForm.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a Truth Booth from the list
|
||||
*/
|
||||
function handleRemoveTruthBooth(e) {
|
||||
const idToRemove = Number(e.target.dataset.id);
|
||||
truthBooths = truthBooths.filter(b => b.id !== idToRemove);
|
||||
e.target.parentElement.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the "Add Ceremony" form submission.
|
||||
*/
|
||||
function handleAddCeremony(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const pairSelectors = document.querySelectorAll('.ceremony-pair-select');
|
||||
const pairs = {}; // This is a JS dict, will be converted to list of tuples for Python
|
||||
const selectedG2Names = [];
|
||||
|
||||
for (const select of pairSelectors) {
|
||||
const g1Name = select.dataset.g1Name;
|
||||
const g2Name = select.value;
|
||||
|
||||
if (!g2Name) {
|
||||
showError(`Please select a match for ${g1Name}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
selectedG2Names.push(g2Name);
|
||||
pairs[g1Name] = g2Name;
|
||||
}
|
||||
|
||||
if (new Set(selectedG2Names).size !== selectedG2Names.length) {
|
||||
showError("Each person from Group 2 can only be matched once per ceremony.");
|
||||
return;
|
||||
}
|
||||
|
||||
const beams = parseInt(ceremonyBeams.value, 10);
|
||||
if (isNaN(beams) || beams < 0 || beams > group1Names.length) {
|
||||
showError(`Beams must be a number between 0 and ${group1Names.length}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const ceremony = { id: Date.now(), pairs, beams };
|
||||
ceremonies.push(ceremony);
|
||||
|
||||
// Update UI
|
||||
const el = document.createElement('div');
|
||||
el.className = 'p-3 rounded-md bg-surface2 ceremony-item';
|
||||
el.innerHTML = `
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="font-semibold text-mauve"><strong>Ceremony ${ceremonies.length}</strong>: ${beams} Beam${beams === 1 ? '' : 's'}</span>
|
||||
<button data-id="${ceremony.id}" class="remove-ceremony-btn text-red hover:text-maroon text-2xl leading-none font-bold">×</button>
|
||||
</div>
|
||||
<div class="text-sm text-subtext0 mt-2 pl-4">
|
||||
${Object.entries(pairs).map(([p1, p2]) => `<div>${p1} - ${p2}</div>`).join('')}
|
||||
</div>
|
||||
`;
|
||||
ceremonyList.appendChild(el);
|
||||
|
||||
el.querySelector('.remove-ceremony-btn').addEventListener('click', handleRemoveCeremony);
|
||||
ceremonyForm.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a Ceremony from the list
|
||||
*/
|
||||
function handleRemoveCeremony(e) {
|
||||
const idToRemove = Number(e.target.dataset.id);
|
||||
ceremonies = ceremonies.filter(c => c.id !== idToRemove);
|
||||
e.target.closest('.ceremony-item').remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* The main calculation function. Passes data to Python and gets results.
|
||||
*/
|
||||
async function handleCalculate() {
|
||||
if (!pyodideSolve) {
|
||||
showError("Python environment is not ready. Please wait.");
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading();
|
||||
|
||||
// Use a timeout to allow the spinner to render before the (blocking) Python call
|
||||
setTimeout(async () => {
|
||||
let g1_py, g2_py, tb_py, c_py;
|
||||
let result_json_py;
|
||||
let results;
|
||||
|
||||
try {
|
||||
// 1. Convert JS data to Pyodide (Python) proxies
|
||||
g1_py = pyodide.toPy(group1Names);
|
||||
g2_py = pyodide.toPy(group2Names);
|
||||
|
||||
const simpleTruthBooths = truthBooths.map(tb => ({
|
||||
p1: tb.p1,
|
||||
p2: tb.p2,
|
||||
match: tb.isMatch
|
||||
}));
|
||||
|
||||
const simpleCeremonies = ceremonies.map(c => ({
|
||||
pairs: c.pairs,
|
||||
beams: c.beams
|
||||
}));
|
||||
|
||||
tb_py = pyodide.toPy(simpleTruthBooths);
|
||||
c_py = pyodide.toPy(simpleCeremonies);
|
||||
|
||||
// 2. Call the Python 'solve' function
|
||||
// result_json_py will be a native JS string
|
||||
result_json_py = await pyodideSolve(g1_py, g2_py, tb_py, c_py);
|
||||
|
||||
// 3. Parse the JS string directly, no .toJs()
|
||||
results = JSON.parse(result_json_py);
|
||||
const grid_data = JSON.parse(results.grid_json);
|
||||
|
||||
// 4. Update Results Display
|
||||
totalPossibilitiesText.textContent = results.possibilities.toLocaleString();
|
||||
resultsDisplay.classList.remove("hidden");
|
||||
|
||||
// 5. Calculate and display probabilities
|
||||
displayProbabilityGrid(grid_data);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Check for the impossible scenario error
|
||||
if (e.message && e.message.includes("Impossible scenario provided")) {
|
||||
showError("Impossible scenario. The data entered contradicts itself.");
|
||||
totalPossibilitiesText.textContent = '0';
|
||||
resultsDisplay.classList.remove("hidden");
|
||||
probTableBody.innerHTML = '';
|
||||
probTableHead.innerHTML = '';
|
||||
} else {
|
||||
showError("An error occurred during Python calculation. Check console.");
|
||||
}
|
||||
} finally {
|
||||
// 6. Clean up Pyodide proxies to free memory
|
||||
g1_py?.destroy();
|
||||
g2_py?.destroy();
|
||||
tb_py?.destroy();
|
||||
c_py?.destroy();
|
||||
// result_json_py is a JS string, it has no .destroy() method.
|
||||
|
||||
hideLoading();
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the probability grid from the Python (pandas) data.
|
||||
* @param {object} gridData - Parsed JSON from pandas DataFrame (orient='split')
|
||||
*/
|
||||
function displayProbabilityGrid(gridData) {
|
||||
// gridData has { index: [g1_names], columns: [g2_names], data: [[...], [...]] }
|
||||
|
||||
// 1. Render Table Header
|
||||
probTableHead.innerHTML = `
|
||||
<tr class="sticky top-0 bg-surface0 z-10">
|
||||
<th class="p-3"></th>
|
||||
${gridData.columns.map(name => `<th class="p-3 whitespace-nowrap">${name}</th>`).join('')}
|
||||
</tr>
|
||||
`;
|
||||
|
||||
// 2. Render Table Body
|
||||
probTableBody.innerHTML = gridData.index.map((g1Name, rowIndex) => `
|
||||
<tr class="hover:bg-surface2">
|
||||
<th class="p-3 whitespace-nowrap bg-surface0 sticky left-0 z-5">${g1Name}</th>
|
||||
${gridData.columns.map((g2Name, colIndex) => {
|
||||
// The 'data' from pandas is the probability (0.0 to 1.0)
|
||||
const prob = gridData.data[rowIndex][colIndex] * 100; // Convert prob to percentage
|
||||
let cellClass = 'prob-possible';
|
||||
if (prob === 100) cellClass = 'prob-100';
|
||||
else if (prob === 0) cellClass = 'prob-0';
|
||||
|
||||
return `<td class="${cellClass} p-3">${prob.toFixed(1)}%</td>`;
|
||||
}).join('')}
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// --- EVENT LISTENERS ---
|
||||
setupButton.addEventListener("click", handleSetupContestants);
|
||||
tbForm.addEventListener("submit", handleAddTruthBooth);
|
||||
ceremonyForm.addEventListener("submit", handleAddCeremony);
|
||||
calculateButton.addEventListener("click", handleCalculate);
|
||||
|
||||
// --- START PYODIDE ---
|
||||
initPyodide();
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
708
backend/Cargo.lock
generated
Normal file
708
backend/Cargo.lock
generated
Normal file
@@ -0,0 +1,708 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.8.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
"memchr",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"mime",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-body-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "httpdate"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchit"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_path_to_error"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"pin-project-lite",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
|
||||
|
||||
[[package]]
|
||||
name = "tower-service"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
12
backend/Cargo.toml
Normal file
12
backend/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = "0.8.8"
|
||||
rayon = "1.11.0"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
tower-http = { version = "0.6.8", features = ["cors", "fs"] }
|
||||
259
backend/src/main.rs
Normal file
259
backend/src/main.rs
Normal file
@@ -0,0 +1,259 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use rayon::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct TruthBooth {
|
||||
p1: String, // from group 1
|
||||
p2: String, // from group 2
|
||||
match_: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Ceremony {
|
||||
pairs: HashMap<String, String>, // p1 -> p2
|
||||
beams: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct SolveRequest {
|
||||
group1: Vec<String>,
|
||||
group2: Vec<String>,
|
||||
truth_booths: Vec<TruthBooth>,
|
||||
ceremonies: Vec<Ceremony>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct GridData {
|
||||
index: Vec<String>,
|
||||
columns: Vec<String>,
|
||||
data: Vec<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SolveResponse {
|
||||
possibilities: usize,
|
||||
grid_data: GridData,
|
||||
}
|
||||
|
||||
struct Solver {
|
||||
n: usize,
|
||||
m: usize,
|
||||
allowed: Vec<u32>, // bitmask of allowed g1 for each g2
|
||||
ceremonies: Vec<(Vec<(usize, usize)>, usize)>,
|
||||
}
|
||||
|
||||
impl Solver {
|
||||
fn new(req: &SolveRequest) -> Result<(Self, HashMap<String, usize>, HashMap<String, usize>), String> {
|
||||
let n = req.group1.len();
|
||||
let m = req.group2.len();
|
||||
|
||||
if n > m {
|
||||
return Err("Group 1 cannot be larger than Group 2".to_string());
|
||||
}
|
||||
if n > 32 {
|
||||
return Err("Group 1 size too large".to_string()); // fit in u32
|
||||
}
|
||||
|
||||
let mut g1_map = HashMap::new();
|
||||
for (i, name) in req.group1.iter().enumerate() {
|
||||
g1_map.insert(name.clone(), i);
|
||||
}
|
||||
|
||||
let mut g2_map = HashMap::new();
|
||||
for (i, name) in req.group2.iter().enumerate() {
|
||||
g2_map.insert(name.clone(), i);
|
||||
}
|
||||
|
||||
let mut allowed = vec![(1 << n) - 1; m];
|
||||
|
||||
for tb in &req.truth_booths {
|
||||
let g1_idx = *g1_map.get(&tb.p1).ok_or("Invalid p1 in TB")?;
|
||||
let g2_idx = *g2_map.get(&tb.p2).ok_or("Invalid p2 in TB")?;
|
||||
|
||||
if tb.match_ {
|
||||
allowed[g2_idx] = 1 << g1_idx;
|
||||
} else {
|
||||
allowed[g2_idx] &= !(1 << g1_idx);
|
||||
}
|
||||
}
|
||||
|
||||
let mut ceremonies = Vec::new();
|
||||
for c in &req.ceremonies {
|
||||
let mut pairs = Vec::new();
|
||||
for (p1, p2) in &c.pairs {
|
||||
let g1_idx = *g1_map.get(p1).ok_or("Invalid p1 in ceremony")?;
|
||||
let g2_idx = *g2_map.get(p2).ok_or("Invalid p2 in ceremony")?;
|
||||
pairs.push((g2_idx, g1_idx));
|
||||
}
|
||||
ceremonies.push((pairs, c.beams));
|
||||
}
|
||||
|
||||
Ok((Solver {
|
||||
n,
|
||||
m,
|
||||
allowed,
|
||||
ceremonies,
|
||||
}, g1_map, g2_map))
|
||||
}
|
||||
|
||||
fn solve(&self) -> (usize, Vec<Vec<usize>>) {
|
||||
// We will generate combinations using a parallel recursive approach.
|
||||
// A full state is an array of size M, where A[g2] = g1.
|
||||
// To parallelize, we generate all valid prefixes of length `prefix_len`.
|
||||
let prefix_len = self.m.min(4); // generate up to length 4 sequentially, then parallelize
|
||||
|
||||
let mut prefixes = Vec::new();
|
||||
self.generate_prefixes(0, 0, 0, &mut Vec::new(), prefix_len, &mut prefixes);
|
||||
|
||||
let results: Vec<(usize, Vec<Vec<usize>>)> = prefixes.into_par_iter().map(|(mask_used, mut prefix)| {
|
||||
let mut counts = vec![vec![0; self.m]; self.n];
|
||||
let mut valid_count = 0;
|
||||
self.dfs(prefix_len, mask_used, &mut prefix, &mut valid_count, &mut counts);
|
||||
(valid_count, counts)
|
||||
}).collect();
|
||||
|
||||
let mut total_valid = 0;
|
||||
let mut total_counts = vec![vec![0; self.m]; self.n];
|
||||
|
||||
for (v_count, counts) in results {
|
||||
total_valid += v_count;
|
||||
for i in 0..self.n {
|
||||
for j in 0..self.m {
|
||||
total_counts[i][j] += counts[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(total_valid, total_counts)
|
||||
}
|
||||
|
||||
fn generate_prefixes(&self, g2_idx: usize, mask_used: u32, current_allowed: u32, current: &mut Vec<usize>, target_len: usize, out: &mut Vec<(u32, Vec<usize>)>) {
|
||||
if g2_idx == target_len {
|
||||
out.push((mask_used, current.clone()));
|
||||
return;
|
||||
}
|
||||
|
||||
let mut allowed = self.allowed[g2_idx];
|
||||
|
||||
// Minor optimization: if we have (M - g2_idx) elements left, and we need to cover (N - bits_set) elements,
|
||||
// we can prune if it's impossible.
|
||||
let needed = self.n as u32 - mask_used.count_ones();
|
||||
let remaining = (self.m - g2_idx) as u32;
|
||||
if needed > remaining {
|
||||
return; // cannot form surjective mapping
|
||||
}
|
||||
|
||||
while allowed > 0 {
|
||||
let bit = allowed & (!allowed + 1); // get lowest set bit
|
||||
allowed &= !bit;
|
||||
let g1_idx = bit.trailing_zeros() as usize;
|
||||
|
||||
current.push(g1_idx);
|
||||
self.generate_prefixes(g2_idx + 1, mask_used | bit, 0, current, target_len, out);
|
||||
current.pop();
|
||||
}
|
||||
}
|
||||
|
||||
fn dfs(&self, g2_idx: usize, mask_used: u32, current: &mut Vec<usize>, valid_count: &mut usize, counts: &mut Vec<Vec<usize>>) {
|
||||
if g2_idx == self.m {
|
||||
if mask_used.count_ones() as usize != self.n {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check ceremonies
|
||||
for (pairs, beams) in &self.ceremonies {
|
||||
let mut actual_beams = 0;
|
||||
for &(g2, g1) in pairs {
|
||||
if current[g2] == g1 {
|
||||
actual_beams += 1;
|
||||
}
|
||||
}
|
||||
if actual_beams != *beams {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Valid scenario
|
||||
*valid_count += 1;
|
||||
for (g2, &g1) in current.iter().enumerate() {
|
||||
counts[g1][g2] += 1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let needed = self.n as u32 - mask_used.count_ones();
|
||||
let remaining = (self.m - g2_idx) as u32;
|
||||
if needed > remaining {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut allowed = self.allowed[g2_idx];
|
||||
|
||||
// To aggressively prune with ceremonies, if we are at the end of checking pairs, but that's complex since we evaluate g2 sequentially.
|
||||
// It's usually fast enough to just prune at the leaf for M=11.
|
||||
|
||||
while allowed > 0 {
|
||||
let bit = allowed & (!allowed + 1);
|
||||
allowed &= !bit;
|
||||
let g1_idx = bit.trailing_zeros() as usize;
|
||||
|
||||
current.push(g1_idx);
|
||||
self.dfs(g2_idx + 1, mask_used | bit, current, valid_count, counts);
|
||||
current.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn solve_handler(Json(payload): Json<SolveRequest>) -> Result<Json<SolveResponse>, axum::http::StatusCode> {
|
||||
// We process the solve in a blocking thread to avoid blocking the async runtime
|
||||
let (solver, _g1_map, _g2_map) = Solver::new(&payload).map_err(|_| axum::http::StatusCode::BAD_REQUEST)?;
|
||||
|
||||
let (total, counts) = tokio::task::spawn_blocking(move || {
|
||||
solver.solve()
|
||||
}).await.unwrap();
|
||||
|
||||
let mut data = vec![vec![0.0; payload.group2.len()]; payload.group1.len()];
|
||||
if total > 0 {
|
||||
for i in 0..payload.group1.len() {
|
||||
for j in 0..payload.group2.len() {
|
||||
data[i][j] = counts[i][j] as f64 / total as f64;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(SolveResponse {
|
||||
possibilities: total,
|
||||
grid_data: GridData {
|
||||
index: payload.group1,
|
||||
columns: payload.group2,
|
||||
data,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/solve", post(solve_handler))
|
||||
.fallback_service(ServeDir::new("../ayto").fallback(ServeFile::new("../ayto/index.html")))
|
||||
.layer(cors);
|
||||
|
||||
let addr = "0.0.0.0:8080";
|
||||
println!("Starting server on {}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
version: '3.8'
|
||||
|
||||
networks:
|
||||
narl:
|
||||
external: true
|
||||
|
||||
services:
|
||||
ayto-calculator:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- narl
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
# Listen on the domain ayto.narl.io
|
||||
- "traefik.http.routers.ayto.rule=Host(`ayto.narl.io`)"
|
||||
- "traefik.http.routers.ayto.entrypoints=websecure"
|
||||
- "traefik.http.routers.ayto.tls.certresolver=https_resolver"
|
||||
# The Rust backend listens on port 8080 inside the container
|
||||
- "traefik.http.services.ayto.loadbalancer.server.port=8080"
|
||||
Reference in New Issue
Block a user