Umer359 Claude Sonnet 5 commited on
Commit
f51d56f
Β·
1 Parent(s): f940d85

Add hover/tap tooltip explaining what "limited" means in local AI mode

Browse files

Hovering (desktop) or tapping (mobile) the word "limited" in the local-AI
banner now shows the 5 concrete capability gaps versus the server model:
smaller model, no images, no live data, no date awareness, and
device-dependent speed. Positioned via JS from the term's actual on-screen
rect and clamped to the viewport, rather than a fixed CSS offset, since
the term sits at an unpredictable horizontal spot in the sentence.

Fixed a real bug caught by testing on a touch-emulated device: the term
has tabindex=0, so a tap fires a native `focus` event before `click`,
which independently opened the tooltip β€” the click handler's toggle then
read "already open" and immediately closed it again on every tap. Fixed
by tracking the click-toggle's own open/closed state in a dedicated flag
instead of inferring it from the tooltip's current display state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016b3u4n81zwJ6rpE9NeCokb

Files changed (1) hide show
  1. index.html +91 -1
index.html CHANGED
@@ -141,6 +141,17 @@
141
  .local-mode-link{background:none;border:none;color:var(--accent);font-size:12px;font-weight:700;cursor:pointer;padding:6px 0 0;font-family:inherit;text-decoration:underline;min-height:auto;display:block;}
142
  .local-progress-track{background:rgba(127,127,127,.25);border-radius:999px;height:8px;overflow:hidden;margin-top:8px;}
143
  .local-progress-fill{background:var(--accent);height:100%;width:0%;transition:width .25s;}
 
 
 
 
 
 
 
 
 
 
 
144
  .local-mode-btn{background:transparent;border:1px solid var(--border);color:var(--text);border-radius:12px;padding:0 20px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit;min-height:40px;width:100%;margin-top:10px;}
145
  .local-mode-btn:hover{border-color:var(--accent);}
146
  .sidebar-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:900;}
@@ -289,7 +300,15 @@
289
  <div class="local-progress-track"><div class="local-progress-fill" id="localProgressFill"></div></div>
290
  </div>
291
  <div class="cap-banner-active" id="capBannerActive" style="display:none;align-items:center;justify-content:space-between;gap:10px;">
292
- <div class="cap-title">πŸ–₯️ Local AI mode β€” limited, runs on your device</div>
 
 
 
 
 
 
 
 
293
  <button type="button" class="local-mode-link" id="exitLocalModeBtn">Use server again</button>
294
  </div>
295
  </div>
@@ -1514,6 +1533,77 @@ if (aiModeLocalBtn) aiModeLocalBtn.addEventListener('click', enableLocalMode);
1514
  if (aiModeServerBtn) aiModeServerBtn.addEventListener('click', () => { if (localModeActive) exitLocalMode(); });
1515
  if (aiModeRowEl && HAS_WEBGPU) aiModeRowEl.style.display = 'flex';
1516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
  // Single source of truth for both the inline composer banner and the
1518
  // centered modal popup, for either reset cause. Both show the SAME
1519
  // UTC-midnight countdown β€” for the message cap (this app's own counter)
 
141
  .local-mode-link{background:none;border:none;color:var(--accent);font-size:12px;font-weight:700;cursor:pointer;padding:6px 0 0;font-family:inherit;text-decoration:underline;min-height:auto;display:block;}
142
  .local-progress-track{background:rgba(127,127,127,.25);border-radius:999px;height:8px;overflow:hidden;margin-top:8px;}
143
  .local-progress-fill{background:var(--accent);height:100%;width:0%;transition:width .25s;}
144
+ .info-term{position:relative;display:inline-block;border-bottom:1px dotted currentColor;cursor:help;}
145
+ /* Positioned via JS (left/top computed from the term's actual on-screen
146
+ rect, clamped to the viewport) rather than pure CSS anchoring β€” the
147
+ term sits at an unpredictable horizontal spot in the sentence (varies
148
+ by language, font size), so a fixed left:0 offset would overflow the
149
+ right edge on a narrow phone screen. See showLocalLimitedTooltip(). */
150
+ .tooltip-bubble{display:none;position:fixed;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:12px 14px;box-shadow:0 12px 32px rgba(0,0,0,.4);z-index:1200;font-size:12px;line-height:1.5;font-weight:500;color:var(--text);white-space:normal;max-height:60vh;overflow-y:auto;}
151
+ .tooltip-bubble ol{margin:0;padding-left:18px;}
152
+ .tooltip-bubble li{margin-bottom:8px;}
153
+ .tooltip-bubble li:last-child{margin-bottom:0;}
154
+ .tooltip-bubble strong{color:var(--accent);}
155
  .local-mode-btn{background:transparent;border:1px solid var(--border);color:var(--text);border-radius:12px;padding:0 20px;font-size:12.5px;font-weight:700;cursor:pointer;font-family:inherit;min-height:40px;width:100%;margin-top:10px;}
156
  .local-mode-btn:hover{border-color:var(--accent);}
157
  .sidebar-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:900;}
 
300
  <div class="local-progress-track"><div class="local-progress-fill" id="localProgressFill"></div></div>
301
  </div>
302
  <div class="cap-banner-active" id="capBannerActive" style="display:none;align-items:center;justify-content:space-between;gap:10px;">
303
+ <div class="cap-title">πŸ–₯️ Local AI mode β€” <span class="info-term" id="localLimitedTerm" tabindex="0" role="button" aria-expanded="false" aria-describedby="localLimitedTooltip">limited<div class="tooltip-bubble" id="localLimitedTooltip" role="tooltip">
304
+ <ol>
305
+ <li><strong>Much smaller model</strong> β€” it's a 360-million-parameter model versus the 1B/3B/8B-class models Nox normally runs server-side. Noticeably less coherent, worse at reasoning, and more prone to getting facts wrong or losing the thread in longer replies.</li>
306
+ <li><strong>No images</strong> β€” it's text-only. Attaching a photo/document in local mode gets declined with a message telling you to switch back to the server.</li>
307
+ <li><strong>No live data</strong> β€” the server injects real-time weather, news, currency rates, holidays, and general web facts into its answers. Local mode has none of that β€” it's just the raw model guessing from its own training, so it'll be wrong or evasive on anything time-sensitive ("what's the weather," "who won yesterday," etc.).</li>
308
+ <li><strong>No date awareness</strong> β€” the server also tells the model today's actual date; local mode doesn't get that either, so it may not know what day it is.</li>
309
+ <li><strong>Speed/quality depend on your device</strong> β€” no server-grade GPU behind it, just whatever's in your laptop/phone.</li>
310
+ </ol>
311
+ </div></span>, runs on your device</div>
312
  <button type="button" class="local-mode-link" id="exitLocalModeBtn">Use server again</button>
313
  </div>
314
  </div>
 
1533
  if (aiModeServerBtn) aiModeServerBtn.addEventListener('click', () => { if (localModeActive) exitLocalMode(); });
1534
  if (aiModeRowEl && HAS_WEBGPU) aiModeRowEl.style.display = 'flex';
1535
 
1536
+ // The ":hover"/":focus" CSS alone covers desktop and most keyboard nav,
1537
+ // but a tap on mobile doesn't reliably keep an element "hovered" long
1538
+ // enough to read a multi-paragraph tooltip β€” an explicit toggle (open on
1539
+ // tap, close on any tap elsewhere) makes it usable there too.
1540
+ const localLimitedTerm = document.getElementById('localLimitedTerm');
1541
+ const localLimitedTooltip = document.getElementById('localLimitedTooltip');
1542
+ // True hover only on devices that actually have a pointer capable of it β€”
1543
+ // on touch, "hover" fires on tap in a way that fights with the click
1544
+ // toggle below (tap would open-then-immediately-close), so touch relies
1545
+ // purely on the click handler instead.
1546
+ const supportsHover = !!(window.matchMedia && window.matchMedia('(hover: hover)').matches);
1547
+
1548
+ function showLocalLimitedTooltip() {
1549
+ if (!localLimitedTerm || !localLimitedTooltip) return;
1550
+ const margin = 12;
1551
+ const termRect = localLimitedTerm.getBoundingClientRect();
1552
+ const width = Math.min(300, window.innerWidth - margin * 2);
1553
+ localLimitedTooltip.style.width = width + 'px';
1554
+ localLimitedTooltip.style.display = 'block';
1555
+ const height = localLimitedTooltip.offsetHeight;
1556
+ let left = termRect.left;
1557
+ if (left + width > window.innerWidth - margin) left = window.innerWidth - margin - width;
1558
+ if (left < margin) left = margin;
1559
+ let top = termRect.top - height - 9;
1560
+ if (top < margin) top = termRect.bottom + 9; // flip below if there's no room above
1561
+ localLimitedTooltip.style.left = left + 'px';
1562
+ localLimitedTooltip.style.top = top + 'px';
1563
+ }
1564
+
1565
+ function hideLocalLimitedTooltip() {
1566
+ if (localLimitedTooltip) localLimitedTooltip.style.display = 'none';
1567
+ if (localLimitedTerm) localLimitedTerm.setAttribute('aria-expanded', 'false');
1568
+ }
1569
+
1570
+ // Tracked separately from the tooltip's own display state, not inferred
1571
+ // from it β€” the term has tabindex="0", so a real click/tap ALSO fires a
1572
+ // native `focus` event first (browsers focus a focusable element before
1573
+ // dispatching its click), which independently shows the tooltip. Reading
1574
+ // "is it currently visible" inside the click handler was therefore always
1575
+ // true by the time click ran, making every tap open-then-immediately-close
1576
+ // it. A dedicated flag that only the click handler itself flips sidesteps
1577
+ // that ordering entirely β€” confirmed with a real Playwright touch tap
1578
+ // after this was still broken with the naive "toggle by reading display" approach.
1579
+ let localTooltipPinnedOpen = false;
1580
+
1581
+ if (localLimitedTerm) {
1582
+ if (supportsHover) {
1583
+ localLimitedTerm.addEventListener('mouseenter', showLocalLimitedTooltip);
1584
+ localLimitedTerm.addEventListener('mouseleave', () => { if (!localTooltipPinnedOpen) hideLocalLimitedTooltip(); });
1585
+ }
1586
+ localLimitedTerm.addEventListener('focus', showLocalLimitedTooltip);
1587
+ localLimitedTerm.addEventListener('blur', () => { if (!localTooltipPinnedOpen) hideLocalLimitedTooltip(); });
1588
+ localLimitedTerm.addEventListener('click', (e) => {
1589
+ e.stopPropagation();
1590
+ localTooltipPinnedOpen = !localTooltipPinnedOpen;
1591
+ if (localTooltipPinnedOpen) {
1592
+ showLocalLimitedTooltip();
1593
+ localLimitedTerm.setAttribute('aria-expanded', 'true');
1594
+ } else {
1595
+ hideLocalLimitedTooltip();
1596
+ }
1597
+ });
1598
+ document.addEventListener('click', () => {
1599
+ localTooltipPinnedOpen = false;
1600
+ hideLocalLimitedTooltip();
1601
+ });
1602
+ window.addEventListener('resize', () => {
1603
+ if (localLimitedTooltip && localLimitedTooltip.style.display === 'block') showLocalLimitedTooltip();
1604
+ });
1605
+ }
1606
+
1607
  // Single source of truth for both the inline composer banner and the
1608
  // centered modal popup, for either reset cause. Both show the SAME
1609
  // UTC-midnight countdown β€” for the message cap (this app's own counter)