cn0303 commited on
Commit
ca2bb8e
·
verified ·
1 Parent(s): bca1ea2

GPU spam guard (serialize + button lock), live progress tickers, describe-first layout, real-machine bindings, chart fix for custom VRAM

Browse files
Files changed (5) hide show
  1. app.py +2 -2
  2. engine/speed.py +8 -6
  3. static/app.js +56 -12
  4. static/index.html +7 -6
  5. static/style.css +22 -2
app.py CHANGED
@@ -75,7 +75,7 @@ def api_lookup(payload: LookupIn):
75
  return lookup(p.get("repo", ""), p, spec_from_payload(p))
76
 
77
 
78
- @app.api(name="parse")
79
  def api_parse(text: str = "") -> dict:
80
  """Messy machine description -> form fields, via the fine-tuned spec
81
  parser (cn0303/fitcheck-spec-parser). ZeroGPU via the Gradio queue."""
@@ -83,7 +83,7 @@ def api_parse(text: str = "") -> dict:
83
  return parse_specs(text)
84
 
85
 
86
- @app.api(name="ask")
87
  def api_ask(question: str, facts: str = "") -> dict:
88
  """Plain-English follow-up, grounded in the facts /api/advise returned.
89
 
 
75
  return lookup(p.get("repo", ""), p, spec_from_payload(p))
76
 
77
 
78
+ @app.api(name="parse", concurrency_limit=1)
79
  def api_parse(text: str = "") -> dict:
80
  """Messy machine description -> form fields, via the fine-tuned spec
81
  parser (cn0303/fitcheck-spec-parser). ZeroGPU via the Gradio queue."""
 
83
  return parse_specs(text)
84
 
85
 
86
+ @app.api(name="ask", concurrency_limit=1)
87
  def api_ask(question: str, facts: str = "") -> dict:
88
  """Plain-English follow-up, grounded in the facts /api/advise returned.
89
 
engine/speed.py CHANGED
@@ -96,12 +96,14 @@ def bandwidth_for_spec(spec, gpu_label: str = "") -> tuple[float | None, str]:
96
  return _apple_bw(gpu_label or spec.gpu_label), "Apple unified memory (conservative M2-gen figure)"
97
  if spec.gpu_vendor in ("nvidia", "amd", "intel") and spec.vram_gb > 0:
98
  n = _norm(gpu_label or spec.gpu_label)
99
- for key, bw, vram in _bw_index():
100
- if key and key in n:
101
- # disambiguate VRAM variants (e.g. 5060 Ti 8 vs 16 GB)
102
- if vram and spec.vram_gb and abs(vram - spec.vram_gb) > 4:
103
- continue
104
- return bw, "vendor spec sheet"
 
 
105
  return None, ""
106
  return None, ""
107
 
 
96
  return _apple_bw(gpu_label or spec.gpu_label), "Apple unified memory (conservative M2-gen figure)"
97
  if spec.gpu_vendor in ("nvidia", "amd", "intel") and spec.vram_gb > 0:
98
  n = _norm(gpu_label or spec.gpu_label)
99
+ # pass 1: name + VRAM proximity (disambiguates 8 vs 16 GB variants);
100
+ # pass 2: name only — a custom VRAM override must not hide the chart.
101
+ for check_vram in (True, False):
102
+ for key, bw, vram in _bw_index():
103
+ if key and key in n:
104
+ if check_vram and vram and spec.vram_gb and abs(vram - spec.vram_gb) > 4:
105
+ continue
106
+ return bw, "vendor spec sheet"
107
  return None, ""
108
  return None, ""
109
 
static/app.js CHANGED
@@ -167,11 +167,27 @@ function fillGpu() {
167
  if (!list.length) { sel.style.display = "none"; sel.innerHTML = ""; }
168
  else { sel.style.display = "block"; sel.innerHTML = list.map(g => `<option>${g}</option>`).join(""); }
169
  }
 
 
 
 
 
 
 
 
 
 
170
  function syncProviderForComputer() {
171
- const isMac = state.computer === "Mac";
172
- const isPi = state.computer.includes("Mini PC");
173
- if (isMac) { state.provider = "apple"; setActive("#provider-seg", "apple"); }
174
- else if (isPi) { state.provider = "none"; setActive("#provider-seg", "none"); }
 
 
 
 
 
 
175
  fillGpu();
176
  }
177
 
@@ -569,20 +585,42 @@ async function drawRoofline(speed) {
569
  host.innerHTML = s;
570
  }
571
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  // ---- Paste box: the fine-tuned spec parser fills the form -----------------
573
  async function parsePaste() {
574
  const text = $("#paste").value.trim();
575
  const hint = $("#parse-hint");
576
- if (!text) { hint.textContent = "Paste or type something first."; return; }
577
- hint.textContent = "Reading it… (first use after a quiet spell wakes the model, up to a minute)";
 
 
 
578
  try {
579
  const client = await getClient();
580
  const r = await client.predict("/parse", { text });
581
  const d = Array.isArray(r.data) ? r.data[0] : r.data;
582
- if (d.error) { hint.textContent = d.error; return; }
 
583
  applyParsed(d, hint);
584
  } catch (e) {
585
- hint.textContent = `Parser unavailable: ${e && e.message ? e.message : e}`;
 
 
 
586
  }
587
  }
588
 
@@ -635,19 +673,25 @@ function wireAsk() {
635
  c.addEventListener("click", () => { input.value = c.textContent; askQuestion(c.textContent); }));
636
  }
637
 
 
638
  async function askQuestion(question) {
639
  question = (question || "").trim();
640
  const box = $("#ask-answer");
641
- if (!question || !box) return;
 
642
  box.hidden = false;
643
- box.innerHTML = `<div class="ans-loading"><span class="spinner"></span>Thinking it through…
644
- <span class="ans-cold">first question after a quiet spell wakes the model (up to a minute); after that it's a few seconds</span></div>`;
645
  try {
646
  const a = await callAsk(question, JSON.stringify(lastAdvice || {}));
 
647
  renderAnswer(box, a);
648
  } catch (e) {
 
649
  // Surface the real error, never a generic stand-in.
650
  box.innerHTML = `<div class="ans-card ans-error"><h3>The explainer hit an error</h3><p>${e && e.message ? e.message : e}</p></div>`;
 
 
651
  }
652
  }
653
 
@@ -714,7 +758,7 @@ function init() {
714
  $("#paste").addEventListener("input", maybeLiveUpdate);
715
  $("#check-btn").addEventListener("click", check);
716
  const pb = $("#parse-btn"); if (pb) pb.addEventListener("click", parsePaste);
717
- fillGpu();
718
  $("#find-specs-body").innerHTML = findSpecsText();
719
  detectHardware();
720
  // Pre-filled share/preview links: ?go renders immediately; optional
 
167
  if (!list.length) { sel.style.display = "none"; sel.innerHTML = ""; }
168
  else { sel.style.display = "block"; sel.innerHTML = list.map(g => `<option>${g}</option>`).join(""); }
169
  }
170
+ // Real machines only: a Windows laptop can't have Apple Silicon, a Pi can't
171
+ // take a desktop card. Impossible provider buttons get disabled per computer.
172
+ const PROVIDER_ALLOWED = {
173
+ "Windows laptop": ["none", "nvidia", "amd", "intel", "unsure"],
174
+ "Windows desktop": ["none", "nvidia", "amd", "intel", "unsure"],
175
+ "Linux PC": ["none", "nvidia", "amd", "intel", "unsure"],
176
+ "Mac": ["apple"],
177
+ "Mini PC / Raspberry Pi": ["none", "nvidia", "unsure"], // nvidia = Jetson boards
178
+ };
179
+
180
  function syncProviderForComputer() {
181
+ const allowed = PROVIDER_ALLOWED[state.computer] || ["none", "nvidia", "amd", "intel", "apple", "unsure"];
182
+ $("#provider-seg").querySelectorAll(".seg-btn").forEach(b => {
183
+ const ok = allowed.includes(b.dataset.val);
184
+ b.classList.toggle("disabled", !ok);
185
+ b.disabled = !ok;
186
+ });
187
+ if (!allowed.includes(state.provider)) {
188
+ state.provider = allowed[0];
189
+ setActive("#provider-seg", state.provider);
190
+ }
191
  fillGpu();
192
  }
193
 
 
585
  host.innerHTML = s;
586
  }
587
 
588
+ // ---- Live progress ticker (gradio-style elapsed seconds) -------------------
589
+ function startTicker(el, base) {
590
+ const t0 = Date.now();
591
+ el.dataset.ticking = "1";
592
+ const tick = () => {
593
+ if (el.dataset.ticking !== "1") return;
594
+ const s = Math.round((Date.now() - t0) / 1000);
595
+ el.innerHTML = `<span class="spinner"></span> ${base} — ${s}s` +
596
+ (s > 8 ? " <span class='tick-note'>(cold start: the model is waking, up to ~1 min)</span>" : "");
597
+ setTimeout(tick, 500);
598
+ };
599
+ tick();
600
+ return () => { el.dataset.ticking = "0"; };
601
+ }
602
+
603
  // ---- Paste box: the fine-tuned spec parser fills the form -----------------
604
  async function parsePaste() {
605
  const text = $("#paste").value.trim();
606
  const hint = $("#parse-hint");
607
+ const btn = $("#parse-btn");
608
+ if (!text) { hint.textContent = "Type or paste something first."; return; }
609
+ if (btn.disabled) return; // one in-flight call, ever — GPU time is real money
610
+ btn.disabled = true;
611
+ const stop = startTicker(hint, "Reading your description");
612
  try {
613
  const client = await getClient();
614
  const r = await client.predict("/parse", { text });
615
  const d = Array.isArray(r.data) ? r.data[0] : r.data;
616
+ stop();
617
+ if (d.error) { hint.textContent = `Failed: ${d.error}`; return; }
618
  applyParsed(d, hint);
619
  } catch (e) {
620
+ stop();
621
+ hint.textContent = `Failed: ${e && e.message ? e.message : e}`;
622
+ } finally {
623
+ btn.disabled = false;
624
  }
625
  }
626
 
 
673
  c.addEventListener("click", () => { input.value = c.textContent; askQuestion(c.textContent); }));
674
  }
675
 
676
+ let askBusy = false;
677
  async function askQuestion(question) {
678
  question = (question || "").trim();
679
  const box = $("#ask-answer");
680
+ if (!question || !box || askBusy) return;
681
+ askBusy = true;
682
  box.hidden = false;
683
+ box.innerHTML = `<div class="ans-loading" id="ask-tick"></div>`;
684
+ const stop = startTicker($("#ask-tick"), "Thinking it through");
685
  try {
686
  const a = await callAsk(question, JSON.stringify(lastAdvice || {}));
687
+ stop();
688
  renderAnswer(box, a);
689
  } catch (e) {
690
+ stop();
691
  // Surface the real error, never a generic stand-in.
692
  box.innerHTML = `<div class="ans-card ans-error"><h3>The explainer hit an error</h3><p>${e && e.message ? e.message : e}</p></div>`;
693
+ } finally {
694
+ askBusy = false;
695
  }
696
  }
697
 
 
758
  $("#paste").addEventListener("input", maybeLiveUpdate);
759
  $("#check-btn").addEventListener("click", check);
760
  const pb = $("#parse-btn"); if (pb) pb.addEventListener("click", parsePaste);
761
+ syncProviderForComputer();
762
  $("#find-specs-body").innerHTML = findSpecsText();
763
  detectHardware();
764
  // Pre-filled share/preview links: ?go renders immediately; optional
static/index.html CHANGED
@@ -45,6 +45,13 @@
45
  <div class="step-head"><span class="step-num">1</span><h2>Your computer</h2></div>
46
  <div class="hint" id="detect-hint" style="display:none; margin-bottom:var(--s-3)"></div>
47
 
 
 
 
 
 
 
 
48
  <div class="field">
49
  <span class="label">What kind of computer?</span>
50
  <div class="segmented" id="computer-seg">
@@ -87,12 +94,6 @@
87
  <input type="number" id="vram" placeholder="e.g. 8" min="0" max="200" step="1" />
88
  <div class="hint">Overrides the GPU picker above. Leave blank to use the picker.</div>
89
  </div>
90
- <div class="field" style="margin-bottom:0">
91
- <span class="label">Or paste / describe your specs</span>
92
- <textarea id="paste" placeholder="Paste output from 'dxdiag' or 'Task Manager → Performance', or just describe it: 'Dell XPS, RTX 3050, 16GB'…"></textarea>
93
- <button class="parse-btn" id="parse-btn">Fill the form from this</button>
94
- <div class="hint" id="parse-hint">A small model fine-tuned for this reads your text and fills the form. It never guesses: anything you didn't say stays blank.</div>
95
- </div>
96
  </div>
97
  </details>
98
  </div>
 
45
  <div class="step-head"><span class="step-num">1</span><h2>Your computer</h2></div>
46
  <div class="hint" id="detect-hint" style="display:none; margin-bottom:var(--s-3)"></div>
47
 
48
+ <div class="field">
49
+ <span class="label">Describe it in your own words <span class="optional">(fastest)</span></span>
50
+ <textarea id="paste" placeholder="'Dell XPS, RTX 3050, 16GB' — or paste your dxdiag / Task Manager output…"></textarea>
51
+ <button class="parse-btn" id="parse-btn">Fill the form from this</button>
52
+ <div class="hint" id="parse-hint">A small model fine-tuned for this reads your text and fills the pickers below. It never guesses: anything you didn't say stays blank.</div>
53
+ </div>
54
+
55
  <div class="field">
56
  <span class="label">What kind of computer?</span>
57
  <div class="segmented" id="computer-seg">
 
94
  <input type="number" id="vram" placeholder="e.g. 8" min="0" max="200" step="1" />
95
  <div class="hint">Overrides the GPU picker above. Leave blank to use the picker.</div>
96
  </div>
 
 
 
 
 
 
97
  </div>
98
  </details>
99
  </div>
static/style.css CHANGED
@@ -137,6 +137,10 @@ button { font-family: inherit; cursor: pointer; }
137
  box-shadow: var(--shadow-md);
138
  }
139
  .form-panel { padding: var(--s-5); position: sticky; top: var(--s-4); }
 
 
 
 
140
  @media (max-width: 900px) { .form-panel { position: static; } }
141
 
142
  .step { margin-bottom: var(--s-5); }
@@ -167,9 +171,23 @@ button { font-family: inherit; cursor: pointer; }
167
  color: var(--text-primary); box-shadow: var(--shadow-sm);
168
  }
169
  .seg-btn .ic { font-size: 17px; color: var(--text-muted); }
170
- .seg-btn.active .ic { color: var(--accent); }
171
  .seg-btn .ic.brand { color: var(--text-secondary); }
172
- .seg-btn.active .ic.brand { color: var(--text-primary); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  /* Native select / input styling */
175
  select, input[type="text"], input[type="number"], textarea {
@@ -501,6 +519,8 @@ details.disc > summary:hover { color: var(--text-primary); }
501
  .ans-card.ans-error p { color: var(--text-secondary); font-size: 13.5px; word-break: break-word; }
502
  .ans-loading { display: flex; align-items: center; gap: var(--s-2); color: var(--text-muted); font-size: 14px; padding: var(--s-2) 0; flex-wrap: wrap; }
503
  .ans-cold { font-size: 12px; color: var(--text-muted); opacity: .75; }
 
 
504
  .spinner {
505
  width: 15px; height: 15px; flex: none; border-radius: 50%;
506
  border: 2px solid var(--border-hi); border-top-color: var(--accent);
 
137
  box-shadow: var(--shadow-md);
138
  }
139
  .form-panel { padding: var(--s-5); position: sticky; top: var(--s-4); }
140
+ @media (min-width: 901px) {
141
+ /* tall forms (long descriptions, many pickers) scroll inside the panel */
142
+ .form-panel { max-height: calc(100vh - 32px); overflow-y: auto; scrollbar-width: thin; }
143
+ }
144
  @media (max-width: 900px) { .form-panel { position: static; } }
145
 
146
  .step { margin-bottom: var(--s-5); }
 
171
  color: var(--text-primary); box-shadow: var(--shadow-sm);
172
  }
173
  .seg-btn .ic { font-size: 17px; color: var(--text-muted); }
174
+ .seg-btn.active .ic { color: var(--segc, var(--accent)); }
175
  .seg-btn .ic.brand { color: var(--text-secondary); }
176
+ .seg-btn.active .ic.brand { color: var(--segc, var(--text-primary)); }
177
+ .seg-btn.disabled { opacity: .3; pointer-events: none; }
178
+
179
+ /* Each position gets its own accent when selected — coloured boundaries,
180
+ line icons stay line icons. */
181
+ .segmented .seg-btn:nth-child(1) { --segc: #60A5FA; }
182
+ .segmented .seg-btn:nth-child(2) { --segc: #4ADE80; }
183
+ .segmented .seg-btn:nth-child(3) { --segc: #F472B6; }
184
+ .segmented .seg-btn:nth-child(4) { --segc: #FBBF24; }
185
+ .segmented .seg-btn:nth-child(5) { --segc: #2DD4BF; }
186
+ .segmented .seg-btn:nth-child(6) { --segc: #C084FC; }
187
+ .seg-btn.active {
188
+ border-color: var(--segc, var(--accent)) !important;
189
+ background: color-mix(in srgb, var(--segc, var(--accent)) 13%, transparent);
190
+ }
191
 
192
  /* Native select / input styling */
193
  select, input[type="text"], input[type="number"], textarea {
 
519
  .ans-card.ans-error p { color: var(--text-secondary); font-size: 13.5px; word-break: break-word; }
520
  .ans-loading { display: flex; align-items: center; gap: var(--s-2); color: var(--text-muted); font-size: 14px; padding: var(--s-2) 0; flex-wrap: wrap; }
521
  .ans-cold { font-size: 12px; color: var(--text-muted); opacity: .75; }
522
+ .tick-note { font-size: 12px; color: var(--text-muted); opacity: .8; }
523
+ #parse-hint .spinner, .ans-loading .spinner { display: inline-block; vertical-align: -2px; margin-right: 6px; }
524
  .spinner {
525
  width: 15px; height: 15px; flex: none; border-radius: 50%;
526
  border: 2px solid var(--border-hi); border-top-color: var(--accent);