Fix buy advice: Minimum and Comfortable are now distinct builds
Browse files- engine/real_advisor.py +25 -9
- static/app.js +4 -0
- static/style.css +2 -0
engine/real_advisor.py
CHANGED
|
@@ -791,8 +791,16 @@ def min_specs(usecases, mode: str = "run") -> dict:
|
|
| 791 |
return advise_real({"usecase": uc.key}, spec)
|
| 792 |
|
| 793 |
def walk(tiers):
|
| 794 |
-
|
| 795 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
spec = _spec_for_tier(hw)
|
| 797 |
per_goal, all_meet, all_great = [], True, True
|
| 798 |
for uc in ucs:
|
|
@@ -806,13 +814,21 @@ def min_specs(usecases, mode: str = "run") -> dict:
|
|
| 806 |
"goals": per_goal,
|
| 807 |
"runs": "; ".join(f"{g['goal']}: {g['model']}" for g in per_goal)
|
| 808 |
if len(per_goal) > 1 else per_goal[0]["model"]}
|
| 809 |
-
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 816 |
|
| 817 |
platforms = []
|
| 818 |
for p in _PLATFORMS:
|
|
|
|
| 791 |
return advise_real({"usecase": uc.key}, spec)
|
| 792 |
|
| 793 |
def walk(tiers):
|
| 794 |
+
# Score every tier once, then choose two DISTINCT builds:
|
| 795 |
+
# minimum = cheapest tier that genuinely works (great or tight).
|
| 796 |
+
# comfortable = the next step UP that adds real headroom (prefers a
|
| 797 |
+
# tier that runs everything great; falls back to the next
|
| 798 |
+
# one that at least works). It is always strictly above
|
| 799 |
+
# the minimum, so the two are never the same build -- the
|
| 800 |
+
# old logic collapsed them whenever the cheapest tier
|
| 801 |
+
# already ran everything great.
|
| 802 |
+
built = []
|
| 803 |
+
for i, (label, price, hw, spec_line, detail) in enumerate(tiers):
|
| 804 |
spec = _spec_for_tier(hw)
|
| 805 |
per_goal, all_meet, all_great = [], True, True
|
| 806 |
for uc in ucs:
|
|
|
|
| 814 |
"goals": per_goal,
|
| 815 |
"runs": "; ".join(f"{g['goal']}: {g['model']}" for g in per_goal)
|
| 816 |
if len(per_goal) > 1 else per_goal[0]["model"]}
|
| 817 |
+
built.append({"idx": i, "tier": tier, "all_meet": all_meet,
|
| 818 |
+
"all_great": all_great})
|
| 819 |
+
|
| 820 |
+
mn = next((b for b in built if b["all_meet"]), None)
|
| 821 |
+
if mn is None:
|
| 822 |
+
return None, None
|
| 823 |
+
# Comfortable: cheapest tier strictly above the minimum that runs all
|
| 824 |
+
# goals great; else the next one above that at least works.
|
| 825 |
+
cf = (next((b for b in built if b["idx"] > mn["idx"] and b["all_great"]), None)
|
| 826 |
+
or next((b for b in built if b["idx"] > mn["idx"] and b["all_meet"]), None))
|
| 827 |
+
if cf is None:
|
| 828 |
+
# Minimum is the top of this platform's ladder; say so honestly
|
| 829 |
+
# instead of repeating the same build as "comfortable".
|
| 830 |
+
return mn["tier"], {"ceiling": True, "label": mn["tier"]["label"]}
|
| 831 |
+
return mn["tier"], cf["tier"]
|
| 832 |
|
| 833 |
platforms = []
|
| 834 |
for p in _PLATFORMS:
|
static/app.js
CHANGED
|
@@ -429,6 +429,10 @@ function renderBuy(d) {
|
|
| 429 |
<div class="bc-head"><span class="bc-kind ${kind}">${kind === "min" ? "Minimum" : "Comfortable"}</span></div>
|
| 430 |
<p class="bc-none">No build on this platform ${ft ? "trains" : "runs"} ${multi ? "all of these" : "this"}${ft ? "" : " well"}, it wants more than this ladder offers.</p>
|
| 431 |
</div>`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 432 |
const goalsHtml = multi
|
| 433 |
? `<ul class="bc-goals">${t.goals.map(g => `<li><span>${esc(g.goal)}</span><b>${esc(g.model)}</b>${g.verdict === "tight" ? " <i>(tight)</i>" : ""}</li>`).join("")}</ul>`
|
| 434 |
: `<div class="bc-runs">${ft ? "Can train up to" : "Runs"} <b>${esc(t.runs)}</b>${t.goals[0] && t.goals[0].verdict === "tight" ? " <i>(tight)</i>" : ""}</div>`;
|
|
|
|
| 429 |
<div class="bc-head"><span class="bc-kind ${kind}">${kind === "min" ? "Minimum" : "Comfortable"}</span></div>
|
| 430 |
<p class="bc-none">No build on this platform ${ft ? "trains" : "runs"} ${multi ? "all of these" : "this"}${ft ? "" : " well"}, it wants more than this ladder offers.</p>
|
| 431 |
</div>`;
|
| 432 |
+
if (t.ceiling) return `<div class="buy-card ceiling">
|
| 433 |
+
<div class="bc-head"><span class="bc-kind ${kind}">Comfortable</span><span class="ic" data-ic="check"></span></div>
|
| 434 |
+
<p class="bc-none">The minimum build is already the most capable ${ft ? "training " : ""}box on this platform for ${multi ? "these goals" : "this goal"}. Nothing bigger is needed here.</p>
|
| 435 |
+
</div>`;
|
| 436 |
const goalsHtml = multi
|
| 437 |
? `<ul class="bc-goals">${t.goals.map(g => `<li><span>${esc(g.goal)}</span><b>${esc(g.model)}</b>${g.verdict === "tight" ? " <i>(tight)</i>" : ""}</li>`).join("")}</ul>`
|
| 438 |
: `<div class="bc-runs">${ft ? "Can train up to" : "Runs"} <b>${esc(t.runs)}</b>${t.goals[0] && t.goals[0].verdict === "tight" ? " <i>(tight)</i>" : ""}</div>`;
|
static/style.css
CHANGED
|
@@ -496,6 +496,8 @@ details.disc > summary:hover { color: var(--text-primary); }
|
|
| 496 |
border-radius: var(--r-md); padding: var(--s-3) var(--s-4);
|
| 497 |
}
|
| 498 |
.buy-card.empty { opacity: .7; }
|
|
|
|
|
|
|
| 499 |
.bc-head { display: flex; align-items: center; justify-content: space-between; }
|
| 500 |
.bc-kind { font-family: var(--font-crt); font-size: 14px; font-weight: 400; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); }
|
| 501 |
.bc-kind.min { color: var(--warn); } .bc-kind.comfy { color: var(--ok); }
|
|
|
|
| 496 |
border-radius: var(--r-md); padding: var(--s-3) var(--s-4);
|
| 497 |
}
|
| 498 |
.buy-card.empty { opacity: .7; }
|
| 499 |
+
.buy-card.ceiling { border-color: var(--accent-strong); }
|
| 500 |
+
.buy-card.ceiling .bc-head .ic { color: var(--accent); font-size: 16px; }
|
| 501 |
.bc-head { display: flex; align-items: center; justify-content: space-between; }
|
| 502 |
.bc-kind { font-family: var(--font-crt); font-size: 14px; font-weight: 400; text-transform: uppercase; letter-spacing: .08em; color: var(--text-muted); }
|
| 503 |
.bc-kind.min { color: var(--warn); } .bc-kind.comfy { color: var(--ok); }
|