polats Claude Opus 4.8 (1M context) commited on
Commit
7b49a92
·
1 Parent(s): dbd7553

Settings: "Use recommended for this device" button (GPU/device-tier heuristic)

Browse files

Inside the "Your device" panel, a button picks a preset from the hardware:
- Mobile → Low, but Medium if a Qualcomm Adreno 700-series (or higher) is found.
- Desktop → Medium, but High if an NVIDIA RTX 3090 (or higher) is found
(newer generation, or 30-gen ≥90; so 3090/3090 Ti and all 40xx/50xx qualify).
GPU is read from the WebGL ANGLE renderer string + WebGPU adapter info. Applies
and persists the chosen preset, and shows what it picked and why. Heuristic
unit-tested across 13 GPU strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files changed (3) hide show
  1. web/deviceInfo.js +40 -0
  2. web/qualityBar.js +20 -4
  3. web/shell/persona.css +3 -0
web/deviceInfo.js CHANGED
@@ -25,6 +25,46 @@ async function webgpuInfo() {
25
  } catch { return 'error' }
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  // Returns an ordered array of [label, value] rows.
29
  export async function gatherDeviceInfo() {
30
  const rows = []
 
25
  } catch { return 'error' }
26
  }
27
 
28
+ export function isMobile() {
29
+ try { if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') return navigator.userAgentData.mobile } catch { /* ignore */ }
30
+ return /Mobi|Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(navigator.userAgent || '')
31
+ }
32
+
33
+ // A combined, lowercased GPU descriptor (WebGL renderer + WebGPU adapter info) for
34
+ // heuristic matching. WebGL's ANGLE string usually carries the real chip name, e.g.
35
+ // "…nvidia geforce rtx 3090…" on desktop or "adreno (tm) 730" on Android.
36
+ async function gpuDescriptor() {
37
+ let s = webglRenderer()
38
+ try {
39
+ if (navigator.gpu) {
40
+ const ad = await navigator.gpu.requestAdapter()
41
+ const info = ad && (ad.info || (ad.requestAdapterInfo ? await ad.requestAdapterInfo() : null))
42
+ if (info) s += ' ' + [info.vendor, info.architecture, info.description].filter(Boolean).join(' ')
43
+ }
44
+ } catch { /* ignore */ }
45
+ return s.toLowerCase()
46
+ }
47
+
48
+ // Recommend a quality preset for this device:
49
+ // • Mobile → Low, but Medium if a Qualcomm Adreno 700-series (or higher) is detected.
50
+ // • Desktop → Medium, but High if an NVIDIA RTX 3090 (or higher) is detected.
51
+ // "or higher" for RTX = a newer generation, or the same 30-gen at ≥90 (so 3090/3090 Ti
52
+ // and all 40xx/50xx qualify; 3080 and below don't).
53
+ export async function recommendPreset() {
54
+ const gpu = await gpuDescriptor()
55
+ if (isMobile()) {
56
+ const a = gpu.match(/adreno[^\d]*(\d{3,4})/)
57
+ if (a && parseInt(a[1], 10) >= 700) return { id: 'medium', reason: `mobile · Adreno ${a[1]} (≥700)` }
58
+ return { id: 'low', reason: 'mobile device' }
59
+ }
60
+ const r = gpu.match(/rtx\s*0*(\d{3,4})/)
61
+ if (r) {
62
+ const n = parseInt(r[1], 10), gen = Math.floor(n / 100), model = n % 100
63
+ if (gen > 30 || (gen === 30 && model >= 90)) return { id: 'high', reason: `desktop · RTX ${r[1]} (≥3090)` }
64
+ }
65
+ return { id: 'medium', reason: 'desktop' }
66
+ }
67
+
68
  // Returns an ordered array of [label, value] rows.
69
  export async function gatherDeviceInfo() {
70
  const rows = []
web/qualityBar.js CHANGED
@@ -2,7 +2,9 @@
2
  // voice provider together) plus an expandable "Your device" readout for debugging and
3
  // benchmarking. Mounted at the TOP of the Settings page, above Local AI Model.
4
  import { PRESETS, applyPreset, getActivePreset, onPresetChange } from '/web/qualityPreset.js'
5
- import { gatherDeviceInfo } from '/web/deviceInfo.js'
 
 
6
 
7
  function el(tag, props = {}, kids = []) {
8
  const n = document.createElement(tag)
@@ -34,10 +36,24 @@ export function mountQualityBar(host) {
34
  highlight(getActivePreset())
35
  onPresetChange((id) => highlight(id))
36
 
37
- // Expandable device readout (debug / benchmark).
 
 
 
 
 
 
 
 
 
 
 
38
  const devBody = el('div', { class: 'tac-device-body' }, 'reading…')
39
- const dev = el('details', { class: 'tac-device' },
40
- [el('summary', {}, 'Your device (GPU / RAM / storage)'), devBody])
 
 
 
41
  dev.addEventListener('toggle', async () => {
42
  if (!dev.open || dev.dataset.loaded) return
43
  dev.dataset.loaded = '1'
 
2
  // voice provider together) plus an expandable "Your device" readout for debugging and
3
  // benchmarking. Mounted at the TOP of the Settings page, above Local AI Model.
4
  import { PRESETS, applyPreset, getActivePreset, onPresetChange } from '/web/qualityPreset.js'
5
+ import { gatherDeviceInfo, recommendPreset } from '/web/deviceInfo.js'
6
+
7
+ const presetLabel = (id) => (PRESETS.find((p) => p.id === id) || {}).label || id
8
 
9
  function el(tag, props = {}, kids = []) {
10
  const n = document.createElement(tag)
 
36
  highlight(getActivePreset())
37
  onPresetChange((id) => highlight(id))
38
 
39
+ // Expandable device readout (debug / benchmark) + a one-click "recommend for me".
40
+ const recBtn = el('button', { class: 'persona-go tac-device-rec', type: 'button' }, '✦ Use recommended for this device')
41
+ const recNote = el('div', { class: 'persona-status tac-device-rec-note' })
42
+ recBtn.addEventListener('click', async () => {
43
+ recBtn.disabled = true; recNote.textContent = 'detecting…'
44
+ try {
45
+ const { id, reason } = await recommendPreset()
46
+ applyPreset(id)
47
+ recNote.textContent = `Chose ${presetLabel(id)} (${reason}).`
48
+ } catch { recNote.textContent = 'Could not detect — pick a preset above.' }
49
+ recBtn.disabled = false
50
+ })
51
  const devBody = el('div', { class: 'tac-device-body' }, 'reading…')
52
+ const dev = el('details', { class: 'tac-device' }, [
53
+ el('summary', {}, 'Your device (GPU / RAM / storage)'),
54
+ el('div', { class: 'tac-device-actions' }, [recBtn, recNote]),
55
+ devBody,
56
+ ])
57
  dev.addEventListener('toggle', async () => {
58
  if (!dev.open || dev.dataset.loaded) return
59
  dev.dataset.loaded = '1'
web/shell/persona.css CHANGED
@@ -280,6 +280,9 @@
280
  cursor: pointer; font-family: var(--p-mono); font-size: 11px; letter-spacing: .08em;
281
  text-transform: uppercase; color: var(--p-transmit);
282
  }
 
 
 
283
  .tac-device-body { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
284
  .tac-device-row { display: flex; gap: 10px; font-family: var(--p-mono); font-size: 11px; line-height: 1.4; }
285
  .tac-device-k { flex: 0 0 130px; color: var(--p-muted); }
 
280
  cursor: pointer; font-family: var(--p-mono); font-size: 11px; letter-spacing: .08em;
281
  text-transform: uppercase; color: var(--p-transmit);
282
  }
283
+ .tac-device-actions { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 4px; }
284
+ .tac-device-rec { align-self: flex-start; margin-top: 0 !important; }
285
+ .tac-device-rec-note { min-height: 0; }
286
  .tac-device-body { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
287
  .tac-device-row { display: flex; gap: 10px; font-family: var(--p-mono); font-size: 11px; line-height: 1.4; }
288
  .tac-device-k { flex: 0 0 130px; color: var(--p-muted); }