Google AI Mode Auto Switcher

Automatically decide whether a Google search query should open in AI mode by adding udm=50, or stay as normal search, based on URL query analysis (script detection + rule-based scoring). Toggle UI retained.

// ==UserScript==
// @name         Google AI Mode Auto Switcher
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Automatically decide whether a Google search query should open in AI mode by adding udm=50, or stay as normal search, based on URL query analysis (script detection + rule-based scoring). Toggle UI retained.
// @author       djshigel
// @match        https://www.google.com/*
// @match        https://www.google.co.jp/*
// @match        https://www.google.*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
  
    // Configuration
    const sessionKey = 'gm_ai_auto_redirect_flag';
    const STORAGE_KEY_ENABLED = 'gm_ai_auto_enabled_v0_5';
  
    // Auto tooltip timing (per request)
    const AUTO_TOOLTIP_DELAY_MS = 1000; // wait 1s before auto tooltip flow
    const AUTO_TOOLTIP_SHOW_MS = 2000;  // show message for 2s
    const AUTO_TOOLTIP_RESTORE_DELAY_MS = 0; // Immediately after restoring tip, return to non-hover
  
    // -----------------------------
    // Simple hash function for session keys (handles multi-byte characters)
    // -----------------------------
    function simpleHash(str) {
      let hash = 0;
      for (let i = 0; i < str.length; i++) {
        const char = str.charCodeAt(i);
        hash = ((hash << 5) - hash) + char;
        hash = hash & hash; // Convert to 32bit integer
      }
      return Math.abs(hash).toString(36);
    }
  
    // -----------------------------
    // Storage helpers (use GM_getValue/GM_setValue when available)
    // -----------------------------
    function setStorageEnabled(v) {
      try {
        if (typeof GM_setValue === 'function') {
          GM_setValue(STORAGE_KEY_ENABLED, v ? '1' : '0');
          return;
        }
      } catch (e) { /* ignore */ }
      try { localStorage.setItem(STORAGE_KEY_ENABLED, v ? '1' : '0'); } catch (e) {}
    }
  
    function getStorageEnabled() {
      try {
        if (typeof GM_getValue === 'function') {
          const val = GM_getValue(STORAGE_KEY_ENABLED, '0');
          return val === '1';
        }
      } catch (e) { /* ignore */ }
      try { return localStorage.getItem(STORAGE_KEY_ENABLED) === '1'; } catch (e) { return false; }
    }
  
    // -----------------------------
    // Mobile detection
    // -----------------------------
    function isMobile() {
      return navigator.userAgent.includes('Mobile');
    }
  
    // -----------------------------
    // Navigation type detection
    // -----------------------------
    function getNavigationType() {
      try {
        // Modern way using Navigation Timing API
        const perfEntries = performance.getEntriesByType('navigation');
        if (perfEntries && perfEntries.length > 0) {
          const navEntry = perfEntries[0];
          // type: 'navigate', 'reload', 'back_forward', 'prerender'
          return navEntry.type;
        }
      } catch (e) { /* ignore */ }
      
      // Fallback: check performance.navigation (deprecated but still works)
      try {
        if (performance.navigation) {
          // 0: TYPE_NAVIGATE, 1: TYPE_RELOAD, 2: TYPE_BACK_FORWARD
          switch (performance.navigation.type) {
            case 0: return 'navigate';
            case 1: return 'reload';
            case 2: return 'back_forward';
            default: return 'navigate';
          }
        }
      } catch (e) { /* ignore */ }
      
      return 'navigate'; // default fallback
    }
  
    // -----------------------------
    // Decision logic (Score-based)
    // Scoring elements:
    // - Average token length × weight
    // - Token count × weight  
    // - Question marks (+30 points)
    // - Japanese particles (+10 points)
    // - Sentence ending patterns (+10 points)
    // - Too short penalty (-20 points)
    // Threshold: Score >= 30 = AI mode
    //
    // Examples:
    // "ごちうさ op 2期 歌詞" → score < 60 → nav (short keywords)
    // "北海道でのクワガタの捕まえ方教えて" → score 60+ → ai (sentence)
    // "Python エラー解決" → score 15 → nav (short keywords)
    // "Pythonでファイルが読み込めないエラーを解決する方法について" → ai (long sentence)
    // -----------------------------

    function decideModeByQuery(q) {
      if (!q) return 'nav';
      
      // Very long queries (>30 chars) should go to AI mode
      // These are likely code snippets, logs, or document pastes
      if (q.length > 30) return 'ai';
      
      const normalized = q.trim().replace(/\s+/g, ' ');
      const tokens = normalized.split(' ').filter(Boolean);
      const tokenCount = tokens.length;
      const totalLength = normalized.length;
    
      // Quick URL-like detection: dot or slash likely a URL -> nav
      if (/[./]/.test(normalized)) return 'nav';
    
      // Calculate complexity score
      let score = 0;
      
      // 1. Average token length (weight: important)
      const avgTokenLength = totalLength / Math.max(tokenCount, 1);
      
      // For Japanese/Chinese (no spaces), consider character-based scoring
      const hasJapaneseChinese = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(normalized);
      
      if (hasJapaneseChinese) {
        // Japanese/Chinese scoring:
        // - Single token >= 9 chars = +50 points (likely sentence)
        // - Average token length × 8
        // - Token count × 3
        if (tokenCount === 1) {
          if (totalLength >= 9) {
            score += 50;
          }
          // Also add length-based score for single tokens
          score += totalLength * 2;
        } else {
          // Multiple tokens: check if keywords or sentence
          score += avgTokenLength * 8;
          score += tokenCount * 3;
        }
      } else {
        // English/Latin scoring:
        // - Average token length × 3
        // - Token count × 5
        // - Length > 40 chars = +20 points
        score += avgTokenLength * 3;
        score += tokenCount * 5;
        
        if (totalLength > 40) score += 20;
      }
      
      // 2. Question marks = strong AI signal (+30 points)
      if (/[??]/.test(normalized)) {
        score += 30;
      }
      
      // 3. Sentence-like patterns
      // Japanese particles that indicate sentences (+10 points)
      if (/[をはがでにへとのも]/.test(normalized)) {
        score += 10;
      }
      // English question words/auxiliaries (+15 points)
      if (/\b(what|how|why|when|where|who|can|could|should|would|will|is|are|was|were)\b/i.test(normalized)) {
        score += 15;
      }
      
      // 4. Too short penalty (likely navigation, -20 points)
      if (totalLength < 6) {
        score -= 20;
      }
      
      // 5. Sentence ending patterns (+10 points)
      if (/[。..!!]$/.test(normalized)) {
        score += 10;
      }
      // Japanese sentence endings (+10 points)
      if (/て$|か$|す$|ます$|です$|した$|ました$/.test(normalized)) {
        score += 10;
      }
      
      // Decision threshold
      // Score >= 60 = AI mode
      // Score < 60 = nav mode
      return score >= 60 ? 'ai' : 'nav';
    }
  
    // -----------------------------
    // Toggle UI
    // -----------------------------
    function createToggleUI() {
      // Skip UI creation on mobile devices when in AI mode
      const currentUrl = new URL(window.location.href);
      const params = currentUrl.searchParams;
      const hasUdm50 = params.get('udm') === '50';
      
      if (isMobile() && hasUdm50) {
        return null; // Hide toggle on mobile when in AI mode
      }
      
      const existing = document.getElementById('ai-mode-toggle-container');
      if (existing) return existing; // reuse
  
      const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
      let isEnabled = getStorageEnabled();
  
      const container = document.createElement('div');
      container.id = 'ai-mode-toggle-container';
      container.style.position = 'fixed';
      container.style.bottom = '20px';
      container.style.left = '20px';
      container.style.zIndex = '2147483647';
      container.style.opacity = '0.1';
      container.style.transform = 'scale(0.7)';
      container.style.transformOrigin = 'bottom left';
      container.style.transition = 'opacity 0.3s, transform 0.3s, box-shadow 0.3s';
      container.style.display = 'flex';
      container.style.alignItems = 'center';
      container.style.gap = '12px';
      container.style.padding = '8px 16px';
      container.style.borderRadius = '24px';
      container.style.boxShadow = '0 2px 10px rgba(0,0,0,0.2)';
      container.style.background = darkMode ? 'rgba(48,49,52,0.95)' : 'rgba(255,255,255,0.95)';
      container.style.border = '1px solid ' + (darkMode ? '#5f6368' : '#dadce0');
      container.style.fontFamily = 'Arial, sans-serif';
      container.style.fontSize = '14px';
  
      container.innerHTML = `
        <span id="ai-mode-label" style="color: ${darkMode ? '#e8eaed' : '#5f6368'}; white-space:nowrap">Auto AI Mode</span>
        <div id="ai-mode-toggle" style="width:48px;height:24px;border-radius:24px;position:relative;cursor:pointer;background:${isEnabled ? '#1a73e8' : (darkMode ? '#5f6368' : '#ccc')}">
          <div id="ai-mode-toggle-knob" style="width:18px;height:18px;background:#fff;border-radius:50%;position:absolute;top:3px;left:${isEnabled ? '27px' : '3px'};transition:left 0.3s;box-shadow:0 2px 4px rgba(0,0,0,0.2)"></div>
        </div>
        <span id="ai-mode-status" style="color:${isEnabled ? '#1a73e8' : (darkMode ? '#9aa0a6' : '#80868b')};font-weight:500;white-space:nowrap">${isEnabled ? 'ON' : 'OFF'}</span>
      `;
  
      let isHover = false;
      let autoTooltipTimers = [];
  
      function clearAutoTooltipTimers() {
        while (autoTooltipTimers.length) {
          clearTimeout(autoTooltipTimers.shift());
        }
        container.dataset.autoTooltipActive = '0';
      }
  
      container.addEventListener('mouseover', () => {
        isHover = true;
        clearAutoTooltipTimers();
        container.style.opacity = '1';
        container.style.transform = 'scale(1)';
      });
      container.addEventListener('mouseout', () => {
        isHover = false;
        container.style.opacity = '0.1';
        container.style.transform = 'scale(0.7)';
      });
  
      container.querySelector('#ai-mode-toggle').addEventListener('click', () => {
        isEnabled = !isEnabled;
        setStorageEnabled(isEnabled);
        const knob = container.querySelector('#ai-mode-toggle-knob');
        const status = container.querySelector('#ai-mode-status');
        container.querySelector('#ai-mode-toggle').style.background = isEnabled ? '#1a73e8' : (darkMode ? '#5f6368' : '#ccc');
        knob.style.left = isEnabled ? '27px' : '3px';
        status.textContent = isEnabled ? 'ON' : 'OFF';
        status.style.color = isEnabled ? '#1a73e8' : (darkMode ? '#9aa0a6' : '#80868b');
  
        const currentUrl = new URL(window.location.href);
        const params = currentUrl.searchParams;
        const hasSearchQuery =
            (params.has('q') && params.get('q').trim().length > 0) ||
            (params.has('as_q') && params.get('as_q').trim().length > 0);
        const hasUdm50 = params.get('udm') === '50';
        const isNormalSearch = !hasUdm50;
        if (isEnabled && !hasUdm50 && hasSearchQuery && isNormalSearch) {
          try { 
            // Clear session storage for new redirect
            const q = params.get('q') || params.get('as_q') || '';
            const isSiri = params.has('as_q') && !params.has('q');
            const queryHash = simpleHash(q);
            const sessionKeyToUse = isSiri ? 
                                   sessionKey + '_siri_' + queryHash :
                                   sessionKey + '_' + queryHash;
            sessionStorage.removeItem(sessionKeyToUse);
          } catch (e) {}
          
          // For Siri searches, move as_q to q parameter for AI mode compatibility
          if (params.has('as_q') && !params.has('q')) {
            const query = params.get('as_q');
            params.set('q', query);
            params.delete('as_q');
            params.delete('as_occt');
            params.delete('as_qdr');
          }
          
          params.set('udm', '50');
          currentUrl.search = params.toString();
          window.location.href = currentUrl.toString();
        }
      });
  
      // Expose auto-tooltip flow but guard against double runs
      container._triggerAutoTooltip = function() {
        if (isHover) return; // user hovered, cancel
        if (container.dataset.autoTooltipActive === '1') return; // already active
        container.dataset.autoTooltipActive = '1';
  
        clearAutoTooltipTimers();
  
        const originalOpacity = container.style.opacity;
        const originalTransform = container.style.transform;
        const labelEl = container.querySelector('#ai-mode-label');
        const originalLabel = labelEl ? labelEl.textContent : '';
  
        const t1 = setTimeout(() => {
          if (isHover) { clearAutoTooltipTimers(); if (labelEl) labelEl.textContent = originalLabel; return; }
          container.style.opacity = '1';
          container.style.transform = 'scale(1)';
          if (labelEl) labelEl.textContent = 'Long queries activate AI mode';
  
          const t2 = setTimeout(() => {
            if (isHover) { clearAutoTooltipTimers(); if (labelEl) labelEl.textContent = originalLabel; return; }
            if (labelEl) labelEl.textContent = originalLabel;
  
            const t3 = setTimeout(() => {
              if (isHover) { clearAutoTooltipTimers(); return; }
              container.style.opacity = originalOpacity;
              container.style.transform = originalTransform;
              container.dataset.autoTooltipActive = '0';
            }, AUTO_TOOLTIP_RESTORE_DELAY_MS);
            autoTooltipTimers.push(t3);
          }, AUTO_TOOLTIP_SHOW_MS);
          autoTooltipTimers.push(t2);
        }, AUTO_TOOLTIP_DELAY_MS);
        autoTooltipTimers.push(t1);
      };
  
      try { document.body.appendChild(container); } catch (e) { console.error('Failed to append AI toggle UI', e); }
      container.dataset.autoTooltipActive = '0';
      return container;
    }
  
    // Main flow
    (function main() {
      try {
        const currentUrl = new URL(window.location.href);
        const params = currentUrl.searchParams;
        
        // Get query from either 'q' or 'as_q' parameter (Siri uses as_q)
        const q = params.get('q') || params.get('as_q') || '';
        const isSiriSearch = params.has('as_q') && !params.has('q');
        
        if (!q) return; // nothing to do on pages without a search query
  
        const userEnabled = getStorageEnabled();
        const hasUdm50 = params.get('udm') === '50';
        const desiredMode = decideModeByQuery(q); // 'ai'|'nav'

        // Special search detection
        const specialUdmValues = ['2','7','28','36','39']; // images, videos, shopping, etc.
        const specialTbmValues = ['nws','flm','fin','lcl','isch','vid','shop','bks'];

        const udmValue = params.get('udm');
        const tbmValue = params.get('tbm');
        const hasSpecialUdm = udmValue && specialUdmValues.includes(udmValue);
        const hasSpecialTbm = tbmValue && specialTbmValues.includes(tbmValue);

        const isNormalSearch = !hasSpecialUdm && !hasSpecialTbm;
        
        // Check navigation type - only redirect on navigate/reload, not back_forward
        const navType = getNavigationType();
        const isBackForward = navType === 'back_forward';
        
        // Check if this is from tab navigation (source=lnms indicates tab switching)
        const isFromTabNavigation = params.get('source') === 'lnms';
        
        // Check if this is from a new tab action (sa=X parameter is often present)
        const hasNewTabIndicator = params.has('sa') && params.get('sa') === 'X';
        
        // Check if this is a new tab opened from Google search
        // Exception: Siri searches (as_q) should be treated as new searches, not tab navigation
        const isFromGoogleNewTab = !isSiriSearch && (
                                   (document.referrer && 
                                    document.referrer.includes('google.com') && 
                                    document.referrer.includes('/search')) ||
                                   isFromTabNavigation ||
                                   hasNewTabIndicator);
        
        // Don't redirect if:
        // 1. User navigated back/forward (tab switching)
        // 2. Already in AI mode
        // 3. Special search type
        // 4. Feature disabled
        // 5. Query doesn't warrant AI mode
        // 6. New tab opened from Google search results or tab navigation (except Siri)
        const shouldRedirect = (desiredMode === 'ai') && 
                              userEnabled && 
                              isNormalSearch && 
                              !hasUdm50 && 
                              !isBackForward &&
                              !isFromGoogleNewTab;
                              
        if (shouldRedirect) {
          try {
            // Use a more specific session key that includes the query to prevent duplicate redirects
            // For Siri searches, use a different approach since they don't follow normal tab flow
            // Use simple hash to handle multi-byte characters
            const queryHash = simpleHash(q);
            const sessionKeyToUse = isSiriSearch ? 
                                   sessionKey + '_siri_' + queryHash :
                                   sessionKey + '_' + queryHash;
            const alreadyRedirected = sessionStorage.getItem(sessionKeyToUse) === 'true';
            if (!alreadyRedirected) {
              sessionStorage.setItem(sessionKeyToUse, 'true');
              
              // For Siri searches, move as_q to q parameter for AI mode compatibility
              if (isSiriSearch) {
                params.set('q', q);
                params.delete('as_q');
                params.delete('as_occt');
                params.delete('as_qdr');
              }
              
              params.set('udm', '50');
              currentUrl.search = params.toString();
              window.location.href = currentUrl.toString();
              return;
            }
          } catch (e) {
            console.error('Redirect error:', e);
          }
        }
  
        // Create or reuse toggle UI (returns null on mobile+AI mode)
        const container = createToggleUI();
  
        // If we started in nav mode and have UI, trigger the auto-tooltip flow to inform users
        if (container && desiredMode === 'nav') {
          try { container._triggerAutoTooltip(); } catch (e) {}
        }
  
      } catch (e) {
        console.error('AI Mode Auto Switcher main error', e);
      }
    })();
  
  })();