Summarize with AI

Single-button AI summarization (OpenAI/Gemini) with chat follow-up feature. Uses Alt+S shortcut. Long press 'S' (or tap-and-hold on mobile) to select model. Supports custom models. Dark mode auto-detection. Click chat icon to continue conversation about the article.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Summarize with AI
// @namespace    https://github.com/insign/userscripts
// @version      2025.12.17.2319
// @description  Single-button AI summarization (OpenAI/Gemini) with chat follow-up feature. Uses Alt+S shortcut. Long press 'S' (or tap-and-hold on mobile) to select model. Supports custom models. Dark mode auto-detection. Click chat icon to continue conversation about the article.
// @author       Hélio <[email protected]>
// @license      WTFPL
// @match        *://*/*
// @grant        GM.addStyle
// @grant        GM.xmlHttpRequest
// @grant        GM.setValue
// @grant        GM.getValue
// @connect      api.openai.com
// @connect      generativelanguage.googleapis.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/readability/0.6.0/Readability.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/readability/0.6.0/Readability-readerable.min.js
// ==/UserScript==

(function () {
  'use strict'

  // --- Constants ---
  // UI Element IDs
  const BUTTON_ID            = 'summarize-button'
  const DROPDOWN_ID          = 'model-dropdown'
  const OVERLAY_ID           = 'summarize-overlay'
  const CLOSE_BUTTON_ID      = 'summarize-close'
  const CONTENT_ID           = 'summarize-content'
  const ERROR_ID             = 'summarize-error'
  const ADD_MODEL_ITEM_ID    = 'add-custom-model'
  const RETRY_BUTTON_ID      = 'summarize-retry-button'
  const CHAT_TOGGLE_ID       = 'summarize-chat-toggle'
  const CHAT_CONTAINER_ID    = 'summarize-chat-container'
  const CHAT_MESSAGES_ID     = 'summarize-chat-messages'
  const CHAT_INPUT_ID        = 'summarize-chat-input'
  const CHAT_SEND_ID         = 'summarize-chat-send'

  // GM Storage Key for custom models
  const CUSTOM_MODELS_KEY = 'custom_ai_models'

  // Token Limits
  const DEFAULT_MAX_TOKENS  = 1000
  const HIGH_MAX_TOKENS     = 1500
  // Long press duration (ms)
  const LONG_PRESS_DURATION = 500
  // Request timeouts (ms)
  const DEFAULT_TIMEOUT     = 60000   // 60 seconds for non-thinking models
  const THINKING_TIMEOUT    = 300000  // 300 seconds (5 min) for thinking models (Pro, o3, o4)

  // Default AI model configurations
  const MODEL_GROUPS = {
    openai: {
      name         : 'OpenAI',
      baseUrl      : 'https://api.openai.com/v1/chat/completions',
      models       : [
        { id: 'o4-mini', name: 'o4 mini (better)', params: { max_completion_tokens: HIGH_MAX_TOKENS, reasoning_effort: 'low' } },
        { id: 'o3-mini', name: 'o3 mini', params: { max_completion_tokens: HIGH_MAX_TOKENS, reasoning_effort: 'low' } },
        { id: 'gpt-4.1', name: 'GPT-4.1' },
        { id: 'gpt-4.1-mini', name: 'GPT-4.1 mini' },
        { id: 'gpt-4.1-nano', name: 'GPT-4.1 nano (faster)' },
      ],
      defaultParams: { max_completion_tokens: DEFAULT_MAX_TOKENS }
    },
    gemini: {
      name         : 'Gemini',
      baseUrl      : 'https://generativelanguage.googleapis.com/v1beta/models/',
      models       : [
        {
          id    : 'gemini-flash-lite-latest',
          name  : 'Gemini Flash Lite (faster)',
          params: { maxOutputTokens: HIGH_MAX_TOKENS, thinkingConfig: { thinkingBudget: 0 } } // Thinking explicitly disabled
        },
        {
          id    : 'gemini-flash-latest',
          name  : 'Gemini Flash',
          params: { maxOutputTokens: HIGH_MAX_TOKENS, thinkingConfig: { thinkingBudget: 0 } } // Thinking explicitly disabled
        },
        {
          id    : 'gemini-pro-latest',
          name  : 'Gemini Pro (better)',
          params: { maxOutputTokens: HIGH_MAX_TOKENS } // No thinkingConfig, Gemini API default (thinking enabled) will be used
        },
      ],
      defaultParams: { maxOutputTokens: DEFAULT_MAX_TOKENS } // No default thinkingConfig; handled by model params or custom logic
    },
  }

  // AI Prompt Template
  const PROMPT_TEMPLATE = (title, content, lang) => `You are a summarizer bot that provides clear and affirmative explanations of content.
		Generate a concise summary that includes:
		- **CRITICAL - First paragraph (Direct Answer):** The first paragraph MUST directly and succinctly answer the main question implied by the article's title. Article titles are often clickbait or attention-grabbing hooks designed to make readers curious. Your job is to immediately satisfy that curiosity in 1-2 sentences. Ask yourself: "What does the reader most want to know after reading this title?" and answer that directly. This paragraph should be the TL;DR that gives the reader the core answer they came looking for.
		- Relevant emojis as bullet points with key supporting details
		- No section headers
		- Use HTML formatting, never use \`\`\` code blocks, never use markdown.
		- **CRITICAL - Opinion paragraph:** After the bullet points, add a conclusion paragraph starting with "<strong>Opinion:</strong> " that presents YOUR informed, skeptical but honest perspective. This opinion should:
			* Be like advice from a knowledgeable friend who has expertise in the subject - direct, simple, clear, and confident
			* Be genuinely skeptical when warranted, but NOT contrarian just for the sake of it - if the article is correct, acknowledge it
			* Point out what the article may have omitted, exaggerated, or gotten wrong - but only if truly applicable
			* Provide broader context that helps the reader understand the real significance (or lack thereof)
			* Be stated with conviction and authority, as someone who truly understands the topic would speak
			* Avoid hedging language like "it seems", "perhaps", "one might argue" - be direct and own your opinion
			* Never say "I agree" or "I disagree" - just state your view as fact
		- User language to be used in the entire summary: ${lang}
		- Before everything, add quality of the article, like "<strong>Article Quality:</strong> <span class=article-good>8/10</span>", where 1 is bad and 10 is excellent.
		- For the quality class use:
			<span class=article-excellent>9/10</span> (or 10)
			<span class=article-good>8/10</span>
			<span class=article-average>7/10</span>
			<span class=article-bad>6/10</span>
			<span class=article-very-bad>5/10</span> (or less)
		- "Opinion:", "Article Quality:" should be in user language, e.g. "Opinião:", "Qualidade do artigo:" for Português.
		- **Guidelines for Article Quality Assessment (Score 1-10):**
			*   **Clarity and Coherence:** Is the text easy to understand? Is the argumentation logical and does it flow well?
			*   **Depth and Information:** Does the article explore the topic with adequate depth? Does it provide relevant and sufficient information, or is it superficial?
			*   **Structure and Organization:** Is the article well-structured (e.g., clear introduction, developed body, logical conclusion)? Are paragraphs and sections well-organized?
			*   **Engagement and Interest:** Is the article interesting and capable of holding the reader's attention?
			*   **Language and Grammar:** Is the text well-written and free of significant grammatical errors or typos?
		Consider these points comprehensively to form an overall score reflecting the article's quality. Strive for consistency in your assessment regardless of your own advanced reasoning capabilities.

		Example output format:
		<p><strong>Article Quality:</strong> <span class="article-good">8/10</span></p>
		<p>[DIRECT ANSWER to what the title promises/implies - what the reader most wants to know, answered immediately and concisely]</p>
		<ul>
		<li>emoji_here Key supporting detail or evidence from the article.</li>
		<li>emoji_here Another important point that adds context.</li>
		<li>emoji_here Additional relevant information.</li>
		<li>emoji_here Final key takeaway from the article.</li>
		</ul>
		<p><strong>Opinion:</strong> [Your direct, authoritative take on this topic. Speak with the confidence of an expert giving their honest assessment. Be skeptical where warranted but fair. Point out what matters and what doesn't. No hedging - own your perspective.]</p>

		Here is the content to summarize:
		Article Title: ${title}
		Article Content: ${content}`

  // --- State Variables ---
  let activeModel     = 'gemini-flash-lite-latest'
  let articleData     = null
  let customModels    = [] // Stores {id, service, supportsThinking?: boolean}
  let longPressTimer  = null
  let isLongPress     = false
  let modelPressTimer = null
  let chatHistory     = [] // Stores conversation history for chat feature
  let lastSummary     = '' // Stores the last generated summary for chat context

  // --- Main Functions ---

  /**
   * Initializes the script.
   */
  async function initialize() {
    customModels = await getCustomModels()
    document.addEventListener('keydown', handleKeyPress)
    articleData = getArticleData()
    if (articleData) {
      addSummarizeButton()
      showElement(BUTTON_ID)
      setupFocusListeners()
      activeModel = await GM.getValue('last_used_model', activeModel)
    }
  }

  /**
   * Extracts article data using Readability.js.
   * @returns {object|null} Article data or null.
   */
  function getArticleData() {
    try {
      const docClone = document.cloneNode(true)
      docClone.querySelectorAll('script, style, noscript, iframe, figure, img, svg, header, footer, nav').forEach(el => el.remove())
      // eslint-disable-next-line no-undef
      if (!isProbablyReaderable(docClone)) {
        console.log('Summarize with AI: Page not detected as readerable.')
        return null
      }
      // eslint-disable-next-line no-undef
      const reader  = new Readability(docClone)
      const article = reader.parse()
      return (article?.content && article.textContent?.trim())
        ? { title: article.title, content: article.textContent.trim() }
        : null
    }
    catch (error) {
      console.error('Summarize with AI: Article parsing failed:', error)
      return null
    }
  }

  /**
   * Adds the summarize button and dropdown to the DOM.
   */
  function addSummarizeButton() {
    if (document.getElementById(BUTTON_ID)) return

    const button       = document.createElement('div')
    button.id          = BUTTON_ID
    button.textContent = 'S'
    button.title       = 'Summarize (Alt+S) / Long Press or Tap & Hold to Select Model'
    document.body.appendChild(button)

    const dropdown = createDropdownElement()
    document.body.appendChild(dropdown)
    populateDropdown(dropdown)

    button.addEventListener('click', () => {
      if (!isLongPress) {
        processSummarization()
      }
      isLongPress = false
    })

    const startLongPressTimer = (event) => {
      isLongPress = false
      clearTimeout(longPressTimer)
      longPressTimer = setTimeout(() => {
        isLongPress = true
        toggleDropdown(event)
      }, LONG_PRESS_DURATION)
    }

    const cancelLongPressTimer = () => {
      clearTimeout(longPressTimer)
    }

    button.addEventListener('mousedown', startLongPressTimer)
    button.addEventListener('mouseup', cancelLongPressTimer)
    button.addEventListener('mouseleave', cancelLongPressTimer)
    button.addEventListener('touchstart', startLongPressTimer, { passive: true })
    button.addEventListener('touchend', cancelLongPressTimer)
    button.addEventListener('touchmove', cancelLongPressTimer)
    button.addEventListener('touchcancel', cancelLongPressTimer)

    document.addEventListener('click', handleOutsideClick)
    injectStyles()
  }


  // --- UI Functions (Dropdown, Overlay, Notifications) ---

  /**
   * Creates the base dropdown element.
   * @returns {HTMLElement}
   */
  function createDropdownElement() {
    const dropdown         = document.createElement('div')
    dropdown.id            = DROPDOWN_ID
    dropdown.style.display = 'none'
    return dropdown
  }

  /**
   * Populates the dropdown with models.
   * @param {HTMLElement} dropdownElement
   */
  function populateDropdown(dropdownElement) {
    dropdownElement.innerHTML = ''

    Object.entries(MODEL_GROUPS).forEach(([ service, group ]) => {
      const standardModels      = group.models || []
      const serviceCustomModels = customModels
        .filter(m => m.service === service)
        .map(m => ({ id: m.id, supportsThinking: m.supportsThinking }))

      const allModelObjects = [ ...standardModels, ...serviceCustomModels ]
        .reduce((acc, model) => {
          if (!acc.some(existing => existing.id.toLowerCase() === model.id.toLowerCase())) {
            acc.push(model)
          }
          return acc
        }, [])
        .sort((a, b) => a.id.localeCompare(b.id))

      if (allModelObjects.length > 0) {
        const groupDiv     = document.createElement('div')
        groupDiv.className = 'model-group'
        groupDiv.appendChild(createHeader(group.name, service))
        allModelObjects.forEach(modelObj => groupDiv.appendChild(createModelItem(modelObj, service)))
        dropdownElement.appendChild(groupDiv)
      }
    })

    const separator           = document.createElement('hr')
    separator.style.margin    = '8px 0'
    separator.style.border    = 'none'
    separator.style.borderTop = '1px solid #eee'
    dropdownElement.appendChild(separator)
    dropdownElement.appendChild(createAddModelItem())
  }

  /**
   * Creates a header for a model group.
   * @param {string} text
   * @param {string} service
   * @returns {HTMLElement}
   */
  function createHeader(text, service) {
    const headerContainer     = document.createElement('div')
    headerContainer.className = 'group-header-container'

    const headerText       = document.createElement('span')
    headerText.className   = 'group-header-text'
    headerText.textContent = text

    const resetLink       = document.createElement('a')
    resetLink.href        = '#'
    resetLink.textContent = 'Reset Key'
    resetLink.className   = 'reset-key-link'
    resetLink.title       = `Reset ${text} API Key`
    resetLink.addEventListener('click', (e) => {
      e.preventDefault()
      e.stopPropagation()
      handleApiKeyReset(service)
    })

    headerContainer.appendChild(headerText)
    headerContainer.appendChild(resetLink)
    return headerContainer
  }

  /**
   * Creates a model item for the dropdown.
   * @param {object} modelObj
   * @param {string} service
   * @returns {HTMLElement}
   */
  function createModelItem(modelObj, service) {
    const item       = document.createElement('div')
    item.className   = 'model-item'
    item.textContent = modelObj.name || modelObj.id
    let isModelPress = false

    if (modelObj.id === activeModel) {
      item.style.fontWeight = 'bold'
      item.style.color      = '#1A73E8'
    }

    item.addEventListener('click', () => {
      if (!isModelPress) {
        activeModel = modelObj.id
        GM.setValue('last_used_model', activeModel)
        hideElement(DROPDOWN_ID)
        processSummarization()
      }
      isModelPress = false
    })

    const isCustom = !MODEL_GROUPS[service]?.models.some(m => m.id === modelObj.id)

    if (isCustom) {
      item.title = 'Click to use. Long press to delete.'

      const startModelPressTimer = (e) => {
        e.stopPropagation()
        isModelPress = false
        clearTimeout(modelPressTimer)
        modelPressTimer = setTimeout(() => {
          isModelPress = true
          handleModelRemoval(modelObj.id, service)
        }, LONG_PRESS_DURATION)
      }

      const cancelModelPressTimer = (e) => {
        e.stopPropagation()
        clearTimeout(modelPressTimer)
      }

      item.addEventListener('mousedown', startModelPressTimer)
      item.addEventListener('mouseup', cancelModelPressTimer)
      item.addEventListener('mouseleave', cancelModelPressTimer)
      item.addEventListener('touchstart', startModelPressTimer, { passive: true })
      item.addEventListener('touchend', cancelModelPressTimer)
      item.addEventListener('touchmove', cancelModelPressTimer)
      item.addEventListener('touchcancel', cancelModelPressTimer)
    }
    else {
      item.title = 'Click to use this model.'
    }
    return item
  }


  /**
   * Creates the "Add Custom Model" item.
   * @returns {HTMLElement}
   */
  function createAddModelItem() {
    const item       = document.createElement('div')
    item.id          = ADD_MODEL_ITEM_ID
    item.className   = 'model-item add-model-item'
    item.textContent = '+ Add Custom Model'
    item.addEventListener('click', async (e) => {
      e.stopPropagation()
      hideElement(DROPDOWN_ID)
      await handleAddModel()
    })
    return item
  }

  /**
   * Toggles dropdown visibility.
   * @param {Event} [e]
   */
  function toggleDropdown(e) {
    if (e) e.stopPropagation()
    const dropdown = document.getElementById(DROPDOWN_ID)
    if (dropdown) {
      const isHidden = dropdown.style.display === 'none'
      if (isHidden) {
        populateDropdown(dropdown)
        showElement(DROPDOWN_ID)
      }
      else {
        hideElement(DROPDOWN_ID)
      }
    }
  }

  /**
   * Handles clicks outside the dropdown to close it.
   * @param {Event} event
   */
  function handleOutsideClick(event) {
    const dropdown = document.getElementById(DROPDOWN_ID)
    const button   = document.getElementById(BUTTON_ID)
    if (dropdown && dropdown.style.display !== 'none' &&
      !dropdown.contains(event.target) &&
      !button?.contains(event.target)) {
      hideElement(DROPDOWN_ID)
    }
  }

  /**
   * Builds the header buttons HTML (chat toggle + close).
   * @param {boolean} showChat - Whether to show the chat button.
   * @returns {string}
   */
  function buildHeaderButtonsHTML(showChat = false) {
    const chatBtn = showChat
      ? `<button type="button" id="${CHAT_TOGGLE_ID}" class="summarize-header-btn summarize-chat-btn" title="Continue conversation">💬</button>`
      : ''
    return `<div class="summarize-header-buttons">
      ${chatBtn}
      <button type="button" id="${CLOSE_BUTTON_ID}" class="summarize-header-btn summarize-close-btn" title="Close (Esc)">✕</button>
    </div>`
  }

  /**
   * Builds the chat interface HTML.
   * @returns {string}
   */
  function buildChatHTML() {
    return `<div id="${CHAT_CONTAINER_ID}">
      <div id="${CHAT_MESSAGES_ID}"></div>
      <div id="summarize-chat-input-container">
        <input type="text" id="${CHAT_INPUT_ID}" placeholder="Ask a follow-up question about this article..." />
        <button type="button" id="${CHAT_SEND_ID}">Send</button>
      </div>
    </div>`
  }

  /**
   * Shows the summary overlay.
   * @param {string} contentHTML
   * @param {boolean} [isError=false]
   */
  function showSummaryOverlay(contentHTML, isError = false) {
    if (document.getElementById(OVERLAY_ID)) {
      updateSummaryOverlay(contentHTML, isError)
      return
    }

    // Reset chat history when showing new summary
    chatHistory = []

    const overlay = document.createElement('div')
    overlay.id    = OVERLAY_ID

    const showChatButton   = !isError && !contentHTML.includes('class="glow"')
    let finalContentHTML   = buildHeaderButtonsHTML(showChatButton)
    finalContentHTML      += `<div class="summarize-body">${contentHTML}</div>`

    if (isError) {
      finalContentHTML += `<button id="${RETRY_BUTTON_ID}" class="retry-button">Try Again</button>`
    }
    else if (showChatButton) {
      finalContentHTML += buildChatHTML()
    }

    overlay.innerHTML = `<div id="${CONTENT_ID}">${finalContentHTML}</div>`

    document.body.appendChild(overlay)
    document.body.style.overflow = 'hidden'

    document.getElementById(CLOSE_BUTTON_ID)?.addEventListener('click', closeOverlay)
    overlay.addEventListener('click', e => e.target === overlay && closeOverlay())
    document.getElementById(RETRY_BUTTON_ID)?.addEventListener('click', processSummarization)

    // Setup chat if available
    if (showChatButton) {
      setupChatListeners()
    }
  }

  /**
   * Closes the summary overlay.
   */
  function closeOverlay() {
    const overlay = document.getElementById(OVERLAY_ID)
    if (overlay) {
      overlay.remove()
      document.body.style.overflow = ''
    }
  }

  /**
   * Updates existing summary overlay content.
   * @param {string} contentHTML
   * @param {boolean} [isError=false]
   */
  function updateSummaryOverlay(contentHTML, isError = false) {
    const contentDiv = document.getElementById(CONTENT_ID)
    if (contentDiv) {
      const showChatButton   = !isError && !contentHTML.includes('class="glow"')
      let finalContentHTML   = buildHeaderButtonsHTML(showChatButton)
      finalContentHTML      += `<div class="summarize-body">${contentHTML}</div>`

      if (isError) {
        finalContentHTML += `<button id="${RETRY_BUTTON_ID}" class="retry-button">Try Again</button>`
      }
      else if (showChatButton) {
        finalContentHTML += buildChatHTML()
        // Store summary for chat context
        lastSummary = contentHTML
      }

      contentDiv.innerHTML = finalContentHTML

      document.getElementById(CLOSE_BUTTON_ID)?.addEventListener('click', closeOverlay)
      document.getElementById(RETRY_BUTTON_ID)?.addEventListener('click', processSummarization)

      // Setup chat if available
      if (showChatButton) {
        setupChatListeners()
      }
    }
  }

  /**
   * Shows a temporary error notification.
   * @param {string} message
   */
  function showErrorNotification(message) {
    document.getElementById(ERROR_ID)?.remove()

    const errorDiv     = document.createElement('div')
    errorDiv.id        = ERROR_ID
    errorDiv.innerText = message
    document.body.appendChild(errorDiv)

    setTimeout(() => errorDiv.remove(), 4000)
  }

  /**
   * Hides an element.
   * @param {string} id
   */
  function hideElement(id) {
    const el = document.getElementById(id)
    if (el) el.style.setProperty('display', 'none', 'important')
  }

  /**
   * Shows an element.
   * @param {string} id
   */
  function showElement(id) {
    const el = document.getElementById(id)
    if (el) {
      const displayValue = (id === BUTTON_ID) ? 'flex' : 'block'
      el.style.setProperty('display', displayValue, 'important')
    }
  }

  // --- Chat Functions ---

  /**
   * Sets up event listeners for the chat interface.
   */
  function setupChatListeners() {
    const chatToggle = document.getElementById(CHAT_TOGGLE_ID)
    const chatInput  = document.getElementById(CHAT_INPUT_ID)
    const chatSend   = document.getElementById(CHAT_SEND_ID)

    chatToggle?.addEventListener('click', toggleChat)

    chatSend?.addEventListener('click', () => sendChatMessage())

    chatInput?.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault()
        sendChatMessage()
      }
      // Stop escape from closing overlay when typing
      if (e.key === 'Escape') {
        e.stopPropagation()
        chatInput.blur()
      }
    })
  }

  /**
   * Toggles the chat interface visibility.
   */
  function toggleChat() {
    const chatContainer = document.getElementById(CHAT_CONTAINER_ID)
    if (chatContainer) {
      chatContainer.classList.toggle('active')
      if (chatContainer.classList.contains('active')) {
        document.getElementById(CHAT_INPUT_ID)?.focus()
      }
    }
  }

  /**
   * Adds a message to the chat display.
   * @param {string} text
   * @param {string} role - 'user' or 'assistant'
   */
  function addChatMessage(text, role) {
    const messagesDiv = document.getElementById(CHAT_MESSAGES_ID)
    if (!messagesDiv) return

    const msgDiv       = document.createElement('div')
    msgDiv.className   = `chat-message ${role}`
    msgDiv.innerHTML   = role === 'assistant' ? text : escapeHtml(text)
    messagesDiv.appendChild(msgDiv)
    messagesDiv.scrollTop = messagesDiv.scrollHeight
  }

  /**
   * Escapes HTML special characters.
   * @param {string} text
   * @returns {string}
   */
  function escapeHtml(text) {
    const div       = document.createElement('div')
    div.textContent = text
    return div.innerHTML
  }

  /**
   * Sends a chat message and gets AI response.
   */
  async function sendChatMessage() {
    const chatInput = document.getElementById(CHAT_INPUT_ID)
    const chatSend  = document.getElementById(CHAT_SEND_ID)
    const message   = chatInput?.value?.trim()

    if (!message || !articleData) return

    // Disable input while processing
    chatInput.disabled = true
    chatSend.disabled  = true
    chatInput.value    = ''

    // Add user message to display
    addChatMessage(message, 'user')

    // Add to history
    chatHistory.push({ role: 'user', content: message })

    try {
      const modelConfig = getActiveModelConfig()
      if (!modelConfig) {
        throw new Error('Model configuration not found')
      }

      const apiKey = await getApiKey(modelConfig.service)
      if (!apiKey) {
        throw new Error('API key not found')
      }

      // Add loading indicator
      const loadingDiv     = document.createElement('div')
      loadingDiv.className = 'chat-message assistant'
      loadingDiv.innerHTML = '<span class="glow" style="padding: 0; font-size: 1em;">Thinking...</span>'
      loadingDiv.id        = 'chat-loading'
      document.getElementById(CHAT_MESSAGES_ID)?.appendChild(loadingDiv)

      const response = await sendChatRequest(modelConfig.service, apiKey, modelConfig)

      // Remove loading indicator
      document.getElementById('chat-loading')?.remove()

      // Parse and display response
      const assistantMessage = parseChatResponse(response, modelConfig.service)
      chatHistory.push({ role: 'assistant', content: assistantMessage })
      addChatMessage(assistantMessage, 'assistant')

    }
    catch (error) {
      document.getElementById('chat-loading')?.remove()
      addChatMessage(`Error: ${error.message}`, 'assistant')
    }
    finally {
      chatInput.disabled = false
      chatSend.disabled  = false
      chatInput.focus()
    }
  }

  /**
   * Sends a chat request to the AI API.
   * @param {string} service
   * @param {string} apiKey
   * @param {object} modelConfig
   * @returns {Promise<object>}
   */
  async function sendChatRequest(service, apiKey, modelConfig) {
    const group = MODEL_GROUPS[service]
    const url   = service === 'openai'
      ? group.baseUrl
      : `${group.baseUrl}${modelConfig.id}:generateContent?key=${apiKey}`

    const body    = buildChatRequestBody(service, modelConfig)
    const timeout = getRequestTimeout(modelConfig)

    return new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        method      : 'POST',
        url         : url,
        headers     : getHeaders(service, apiKey),
        data        : JSON.stringify(body),
        responseType: 'json',
        timeout     : timeout,
        onload      : response => {
          const responseData = response.response || response.responseText
          if (response.status < 200 || response.status >= 300) {
            const errorData = typeof responseData === 'object' ? responseData : JSON.parse(responseData || '{}')
            reject(new Error(errorData?.error?.message || `API Error ${response.status}`))
          }
          else {
            resolve({
              status: response.status,
              data  : typeof responseData === 'object' ? responseData : JSON.parse(responseData || '{}')
            })
          }
        },
        onerror  : () => reject(new Error('Network error')),
        onabort  : () => reject(new Error('Request aborted')),
        ontimeout: () => reject(new Error('Request timed out')),
      })
    })
  }

  /**
   * Builds the chat request body.
   * @param {string} service
   * @param {object} modelConfig
   * @returns {object}
   */
  function buildChatRequestBody(service, modelConfig) {
    const lang          = navigator.language || 'en-US'
    const systemContext = `You are a helpful assistant discussing an article. Here's the context:

Article Title: ${articleData.title}
Article Summary: ${lastSummary.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim()}

Respond helpfully to questions about this article. Use ${lang} language. Use HTML formatting for responses (no markdown, no code blocks). Keep responses concise but informative.`

    if (service === 'openai') {
      const messages = [
        { role: 'system', content: systemContext },
        ...chatHistory.map(m => ({ role: m.role, content: m.content }))
      ]

      const serviceDefaults     = MODEL_GROUPS.openai.defaultParams || {}
      const modelSpecificParams = modelConfig.params || {}
      const finalParams         = { ...serviceDefaults, ...modelSpecificParams }

      return {
        model: modelConfig.id,
        messages,
        ...finalParams
      }
    }
    else { // gemini
      const contents = []

      // Add system context as first user message
      contents.push({ role: 'user', parts: [ { text: systemContext } ] })
      contents.push({ role: 'model', parts: [ { text: 'I understand. I will help answer questions about this article.' } ] })

      // Add chat history
      chatHistory.forEach(m => {
        contents.push({
          role : m.role === 'user' ? 'user' : 'model',
          parts: [ { text: m.content } ]
        })
      })

      const geminiDefaults        = MODEL_GROUPS.gemini.defaultParams || {}
      const modelParamsFromConfig = modelConfig.params || {}
      let finalGenerationConfig   = { ...geminiDefaults, ...modelParamsFromConfig }

      if (modelConfig.isCustom) {
        if (modelConfig.supportsThinking === true) {
          delete finalGenerationConfig.thinkingConfig
        }
        else {
          finalGenerationConfig.thinkingConfig = { thinkingBudget: 0 }
        }
      }

      return {
        contents,
        generationConfig: finalGenerationConfig
      }
    }
  }

  /**
   * Parses chat response from API.
   * @param {object} response
   * @param {string} service
   * @returns {string}
   */
  function parseChatResponse(response, service) {
    if (service === 'openai') {
      return response.data?.choices?.[0]?.message?.content || 'No response received'
    }
    else {
      const candidate = response.data?.candidates?.[0]
      if (candidate?.content?.parts?.length > 0) {
        return candidate.content.parts[0].text || 'No response received'
      }
      return 'No response received'
    }
  }

  /**
   * Gets the appropriate timeout for a model.
   * @param {object} modelConfig
   * @returns {number}
   */
  function getRequestTimeout(modelConfig) {
    // Check if model has thinking enabled
    const hasThinking = modelConfig.params && !modelConfig.params.thinkingConfig
    const isProModel  = modelConfig.id.toLowerCase().includes('pro')
    const isO3orO4    = /^o[34]/i.test(modelConfig.id)

    // Custom models with thinking support
    const customWithThinking = modelConfig.isCustom && modelConfig.supportsThinking === true

    if (hasThinking || isProModel || isO3orO4 || customWithThinking) {
      return THINKING_TIMEOUT
    }

    return DEFAULT_TIMEOUT
  }

  // --- Logic Functions (Summarization, API, Models) ---

  /**
   * Gets the active model's configuration.
   * @returns {object|null}
   */
  function getActiveModelConfig() {
    for (const serviceKey in MODEL_GROUPS) {
      const group       = MODEL_GROUPS[serviceKey]
      const modelConfig = group.models.find(m => m.id === activeModel)
      if (modelConfig) {
        return { ...modelConfig, service: serviceKey, isCustom: false }
      }
    }
    // For custom models, `service` and `supportsThinking` are part of the customConfig object itself
    const customConfig = customModels.find(m => m.id === activeModel)
    if (customConfig) {
      return { ...customConfig, isCustom: true } // `params` will be undefined for custom models for now
    }
    console.error(`Summarize with AI: Active model configuration not found for ID: ${activeModel}`)
    return null
  }


  /**
   * Orchestrates the summarization process.
   */
  async function processSummarization() {
    try {
      if (!articleData) {
        showErrorNotification('Article content not found or not readable.')
        return
      }

      const modelConfig = getActiveModelConfig()
      if (!modelConfig) {
        showErrorNotification(`Configuration for model "${activeModel}" not found. Please select another model.`)
        return
      }

      const modelDisplayName = modelConfig.name || modelConfig.id
      const service          = modelConfig.service

      const apiKey = await getApiKey(service)
      if (!apiKey) {
        const errorMsg = `API key for ${service.toUpperCase()} is required. Click the 'Reset Key' link in the model selection menu (long-press 'S' button).`
        if (document.getElementById(OVERLAY_ID)) {
          updateSummaryOverlay(`<p style="color: red;">${errorMsg}</p>`, false)
        }
        else {
          showErrorNotification(errorMsg)
        }
        return
      }

      const loadingMessage = `<p class="glow">Summarizing with ${modelDisplayName}... </p>`
      if (document.getElementById(OVERLAY_ID)) {
        updateSummaryOverlay(loadingMessage)
      }
      else {
        showSummaryOverlay(loadingMessage)
      }

      const payload  = { title: articleData.title, content: articleData.content, lang: navigator.language || 'en-US' }
      const response = await sendApiRequest(service, apiKey, payload, modelConfig)

      handleApiResponse(response, service)

    }
    catch (error) {
      const errorMsg = `Error: ${error.message}`
      console.error('Summarize with AI:', errorMsg, error)
      showSummaryOverlay(`<p style="color: red;">${errorMsg}</p>`, true)
      hideElement(DROPDOWN_ID)
    }
  }

  /**
   * Sends API request.
   * @param {string} service
   * @param {string} apiKey
   * @param {object} payload
   * @param {object} modelConfig
   * @returns {Promise<object>}
   */
  async function sendApiRequest(service, apiKey, payload, modelConfig) {
    const group   = MODEL_GROUPS[service]
    const url     = service === 'openai'
      ? group.baseUrl
      : `${group.baseUrl}${modelConfig.id}:generateContent?key=${apiKey}`
    const timeout = getRequestTimeout(modelConfig)

    return new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        method      : 'POST',
        url         : url,
        headers     : getHeaders(service, apiKey),
        data        : JSON.stringify(buildRequestBody(service, payload, modelConfig)),
        responseType: 'json',
        timeout     : timeout,
        onload      : response => {
          const responseData = response.response || response.responseText
          resolve({
            status    : response.status,
            data      : typeof responseData === 'object' ? responseData : JSON.parse(responseData || '{}'),
            statusText: response.statusText,
          })
        },
        onerror     : error => reject(new Error(`Network error: ${error.statusText || 'Failed to connect'}`)),
        onabort     : () => reject(new Error('Request aborted')),
        ontimeout   : () => reject(new Error(`Request timed out after ${timeout / 1000} seconds`)),
      })
    })
  }

  /**
   * Handles API response.
   * @param {object} response
   * @param {string} service
   * @throws {Error}
   */
  function handleApiResponse(response, service) {
    const { status, data, statusText } = response

    if (status < 200 || status >= 300) {
      const errorDetails = data?.error?.message || data?.message || statusText || 'Unknown API error'
      throw new Error(`API Error (${status}): ${errorDetails}`)
    }

    let rawSummary = ''
    if (service === 'openai') {
      const choice       = data?.choices?.[0]
      rawSummary         = choice?.message?.content
      const finishReason = choice?.finish_reason
      console.log(`Summarize with AI: OpenAI Finish Reason: ${finishReason} (Model: ${activeModel})`)
      if (finishReason === 'length') {
        console.warn('Summarize with AI: Summary may be incomplete because the max token limit was reached.')
      }

    }
    else if (service === 'gemini') {
      const candidate    = data?.candidates?.[0]
      const finishReason = candidate?.finishReason
      console.log(`Summarize with AI: Gemini Finish Reason: ${finishReason} (Model: ${activeModel})`)

      if (finishReason === 'SAFETY') {
        const safetyRatings = candidate.safetyRatings?.map(r => `${r.category}: ${r.probability}`).join(', ')
        throw new Error(`Content blocked due to safety concerns (${safetyRatings || 'No details'}).`)
      }
      if (finishReason === 'MAX_TOKENS') {
        console.warn('Summarize with AI: Summary may be incomplete because the max token limit was reached.')
      }

      if (candidate?.content?.parts?.length > 0 && candidate.content.parts[0].text) {
        rawSummary = candidate.content.parts[0].text
      }
      else if (finishReason && ![ 'STOP', 'SAFETY', 'MAX_TOKENS' ].includes(finishReason)) {
        console.warn(`Summarize with AI: Gemini response structure missing expected text content or unusual finish reason: ${finishReason}`, candidate)
      }
      else if (!rawSummary && !data?.error) {
        console.warn('Summarize with AI: Gemini response structure missing expected text content.', candidate)
      }
    }

    if (!rawSummary && !data?.error) {
      console.error('Summarize with AI: API Response Data:', data)
      throw new Error('API response did not contain a valid summary.')
    }

    const cleanedSummary = rawSummary.replace(/\n/g, ' ').replace(/ {2,}/g, ' ').trim()
    updateSummaryOverlay(cleanedSummary, false)
  }

  /**
   * Builds the request body for API call.
   * @param {string} service
   * @param {object} payload
   * @param {object} modelConfig
   * @returns {object}
   */
  function buildRequestBody(service, { title, content, lang }, modelConfig) {
    const systemPrompt = PROMPT_TEMPLATE(title, content, lang)

    if (service === 'openai') {
      const serviceDefaults     = MODEL_GROUPS.openai.defaultParams || {}
      const modelSpecificParams = modelConfig.params || {} // For custom models, this will be undefined/empty from getActiveModelConfig
      const finalParams         = { ...serviceDefaults, ...modelSpecificParams }

      return {
        model   : modelConfig.id,
        messages: [
          { role: 'system', content: systemPrompt },
          { role: 'user', content: 'Generate the summary as requested.' }
        ],
        ...finalParams
      }
    }
    else { // gemini
      const geminiDefaults        = MODEL_GROUPS.gemini.defaultParams || {}
      const modelParamsFromConfig = modelConfig.params || {} // For standard models from MODEL_GROUPS
      // For custom models, modelConfig.params is undefined from getActiveModelConfig
      let finalGenerationConfig = { ...geminiDefaults, ...modelParamsFromConfig }

      if (modelConfig.isCustom) {
        // For custom Gemini models, modelConfig.params from getActiveModelConfig is undefined.
        // We rely on modelConfig.supportsThinking.
        if (modelConfig.supportsThinking === true) {
          // If user indicated it supports thinking, we want Gemini API's default behavior for thinking.
          // So, remove any thinkingConfig that might have been inherited (e.g., if geminiDefaults had one, which it shouldn't).
          delete finalGenerationConfig.thinkingConfig
        }
        else { // supportsThinking is false or undefined (treat undefined as false for safety for older custom models)
          // Explicitly disable thinking if not supported or unknown for custom model.
          finalGenerationConfig.thinkingConfig = { thinkingBudget: 0 }
        }
      }
      // For standard Gemini models, their 'params' in MODEL_GROUPS (already merged into finalGenerationConfig via modelParamsFromConfig)
      // will dictate thinkingConfig. If a standard model has no thinkingConfig in its params,
      // and geminiDefaults also has no thinkingConfig (which is the new setup),
      // then no thinkingConfig is sent, and Gemini API uses its own default (thinking enabled for 2.5 Pro, potentially others).

      return {
        contents        : [ { parts: [ { text: systemPrompt } ] } ],
        generationConfig: finalGenerationConfig
      }
    }
  }

  /**
   * Returns HTTP headers.
   * @param {string} service
   * @param {string} apiKey
   * @returns {object}
   */
  function getHeaders(service, apiKey) {
    const headers = { 'Content-Type': 'application/json' }
    if (service === 'openai') {
      headers['Authorization'] = `Bearer ${apiKey}`
    }
    return headers
  }

  /**
   * Retrieves API key for a service.
   * @param {string} service
   * @returns {Promise<string|null>}
   */
  async function getApiKey(service) {
    const storageKey = `${service}_api_key`
    let apiKey       = await GM.getValue(storageKey)
    return apiKey?.trim() || null
  }

  /**
   * Handles API key reset for a service.
   * @param {string} service
   */
  async function handleApiKeyReset(service) {
    if (!service || !MODEL_GROUPS[service]) {
      console.error('Invalid service provided for API key reset:', service)
      alert('Internal error: Invalid service provided.')
      return
    }
    const storageKey = `${service}_api_key`
    const newKey     = prompt(`Enter the new ${service.toUpperCase()} API key (leave blank to clear):`)

    if (newKey !== null) {
      const keyToSave = newKey.trim()
      await GM.setValue(storageKey, keyToSave)
      if (keyToSave) {
        alert(`${service.toUpperCase()} API key updated!`)
      }
      else {
        alert(`${service.toUpperCase()} API key cleared!`)
      }
    }
  }

  /**
   * Handles adding a new custom model.
   */
  async function handleAddModel() {
    const service = prompt('Enter the service for the custom model (openai / gemini):')?.toLowerCase()?.trim()
    if (!service || !MODEL_GROUPS[service]) {
      if (service !== null) alert('Invalid service. Please enter "openai" or "gemini".')
      return
    }

    const modelId = prompt(`Enter the exact ID of the ${service.toUpperCase()} model:`)?.trim()
    if (!modelId) {
      if (modelId !== null) alert('Model ID cannot be empty.')
      return
    }

    let supportsThinking = undefined // Stays undefined for OpenAI or if user cancels for Gemini
    if (service === 'gemini') {
      const thinkingInput = prompt(`Does this custom Gemini model ("${modelId}") support 'thinking' (e.g., gemini-2.5-pro)? (yes/no):`)?.toLowerCase()?.trim()
      if (thinkingInput === null) return // User cancelled prompt
      if (![ 'yes', 'no' ].includes(thinkingInput)) {
        alert('Invalid input for thinking support. Please enter "yes" or "no".')
        return
      }
      supportsThinking = (thinkingInput === 'yes')
    }

    await addCustomModel(service, modelId, supportsThinking)
  }

  /**
   * Adds a custom model.
   * @param {string} service
   * @param {string} modelId
   * @param {boolean|undefined} supportsThinking - Undefined if not Gemini or if not applicable.
   */
  async function addCustomModel(service, modelId, supportsThinking) {
    const existsInCustom   = customModels.some(m => m.service === service && m.id.toLowerCase() === modelId.toLowerCase())
    const existsInStandard = MODEL_GROUPS[service]?.models.some(m => m.id.toLowerCase() === modelId.toLowerCase())

    if (existsInCustom || existsInStandard) {
      alert(`Model ID "${modelId}" already exists for ${service.toUpperCase()}.`)
      return
    }

    const newModel = { id: modelId, service }
    if (service === 'gemini' && typeof supportsThinking === 'boolean') {
      newModel.supportsThinking = supportsThinking
    }

    customModels.push(newModel)
    await GM.setValue(CUSTOM_MODELS_KEY, JSON.stringify(customModels))
    alert(`Custom model "${modelId}" (${service.toUpperCase()}) added!`)
  }

  /**
   * Handles removal of a custom model.
   * @param {string} modelId
   * @param {string} service
   */
  async function handleModelRemoval(modelId, service) {
    // eslint-disable-next-line no-restricted-globals
    const confirmed = confirm(`Are you sure you want to delete the custom model "${modelId}"?`)
    if (confirmed) {
      customModels = customModels.filter(m => !(m.id.toLowerCase() === modelId.toLowerCase() && m.service === service))
      await GM.setValue(CUSTOM_MODELS_KEY, JSON.stringify(customModels))

      if (activeModel === modelId) {
        activeModel = 'gemini-flash-lite-latest'
        await GM.setValue('last_used_model', activeModel)
      }

      const dropdown = document.getElementById(DROPDOWN_ID)
      if (dropdown && dropdown.style.display !== 'none') {
        populateDropdown(dropdown)
      }
      alert(`Model "${modelId}" has been removed.`)
    }
  }


  /**
   * Loads custom models from storage.
   * @returns {Promise<Array<object>>}
   */
  async function getCustomModels() {
    try {
      const storedModels = await GM.getValue(CUSTOM_MODELS_KEY, '[]')
      const parsedModels = JSON.parse(storedModels)
      if (Array.isArray(parsedModels) && parsedModels.every(m =>
        typeof m === 'object' && m.id && m.service &&
        (m.service !== 'gemini' || typeof m.supportsThinking === 'boolean' || typeof m.supportsThinking === 'undefined') // Check supportsThinking validity
      )) {
        return parsedModels
      }
      else {
        console.warn('Summarize with AI: Invalid custom model format found in storage. Resetting.', parsedModels)
        await GM.setValue(CUSTOM_MODELS_KEY, '[]')
        return []
      }
    }
    catch (error) {
      console.error('Summarize with AI: Failed to load/parse custom models:', error)
      await GM.setValue(CUSTOM_MODELS_KEY, '[]')
      return []
    }
  }

  // --- Event Handlers & Utilities ---

  /**
   * Handles key press events.
   * @param {KeyboardEvent} e
   */
  function handleKeyPress(e) {
    if (e.altKey && e.code === 'KeyS' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
      e.preventDefault()
      const button = document.getElementById(BUTTON_ID)
      if (button) {
        if (!document.activeElement?.closest('input, textarea, select, [contenteditable="true"]')) {
          processSummarization()
        }
      }
    }
    if (e.key === 'Escape') {
      if (document.getElementById(OVERLAY_ID)) {
        e.preventDefault()
        closeOverlay()
      }
      else if (document.getElementById(DROPDOWN_ID)?.style.display !== 'none') {
        e.preventDefault()
        hideElement(DROPDOWN_ID)
      }
    }
  }

  /**
   * Sets up focus listeners to hide/show button.
   */
  function setupFocusListeners() {
    document.addEventListener('focusin', (event) => {
      if (event.target?.closest('input, textarea, select, [contenteditable="true"]')) {
        hideElement(BUTTON_ID)
        hideElement(DROPDOWN_ID)
      }
    })

    document.addEventListener('focusout', (event) => {
      const isLeavingInput  = event.target?.closest('input, textarea, select, [contenteditable="true"]')
      const isEnteringInput = event.relatedTarget?.closest('input, textarea, select, [contenteditable="true"]')

      if (isLeavingInput && !isEnteringInput && articleData) {
        setTimeout(() => {
          if (!document.activeElement?.closest('input, textarea, select, [contenteditable="true"]')) {
            showElement(BUTTON_ID)
          }
        }, 50)
      }
    }, true)
  }

  /**
   * Injects CSS styles.
   */
  function injectStyles() {
    GM.addStyle(`
      /* --- Main Summarize Button --- */
      #${BUTTON_ID} {
        all: initial !important;
        position: fixed !important;
        bottom: 20px !important;
        right: 20px !important;
        width: 50px !important;
        height: 50px !important;
        background: linear-gradient(145deg, #3a7bd5, #00d2ff) !important;
        color: white !important;
        font-size: 24px !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        border-radius: 50% !important;
        cursor: pointer !important;
        z-index: 2147483640 !important;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25) !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        transition: transform 0.2s ease-out, box-shadow 0.2s ease-out !important;
        line-height: 1 !important;
        user-select: none !important;
        -webkit-tap-highlight-color: transparent !important;
        border: none !important;
      }
      #${BUTTON_ID}:hover {
        transform: scale(1.1) !important;
        box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3) !important;
      }

      /* --- Dropdown Menu --- */
      #${DROPDOWN_ID} {
        all: initial !important;
        position: fixed !important;
        bottom: 80px !important;
        right: 20px !important;
        background: #ffffff !important;
        border: 1px solid #e0e0e0 !important;
        border-radius: 12px !important;
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18) !important;
        z-index: 2147483641 !important;
        max-height: 70vh !important;
        overflow-y: auto !important;
        padding: 10px !important;
        width: 300px !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        display: none !important;
        box-sizing: border-box !important;
      }

      #${DROPDOWN_ID} * {
        box-sizing: border-box !important;
        font-family: inherit !important;
      }

      #${DROPDOWN_ID} .model-group { margin-bottom: 10px !important; }
      #${DROPDOWN_ID} .group-header-container {
        display: flex !important;
        align-items: center !important;
        justify-content: space-between !important;
        padding: 10px 14px !important;
        background: #f5f5f5 !important;
        border-radius: 8px !important;
        margin-bottom: 6px !important;
      }
      #${DROPDOWN_ID} .group-header-text {
        font-weight: 700 !important;
        color: #333 !important;
        font-size: 12px !important;
        text-transform: uppercase !important;
        letter-spacing: 0.8px !important;
      }
      #${DROPDOWN_ID} .reset-key-link {
        font-size: 11px !important;
        color: #888 !important;
        text-decoration: none !important;
        cursor: pointer !important;
      }
      #${DROPDOWN_ID} .reset-key-link:hover { color: #3a7bd5 !important; }
      #${DROPDOWN_ID} .model-item {
        padding: 11px 14px !important;
        margin: 3px 0 !important;
        border-radius: 8px !important;
        font-size: 14px !important;
        cursor: pointer !important;
        color: #444 !important;
        display: block !important;
        background: transparent !important;
      }
      #${DROPDOWN_ID} .model-item:hover {
        background-color: rgba(58, 123, 213, 0.1) !important;
        color: #3a7bd5 !important;
      }
      #${DROPDOWN_ID} .add-model-item { color: #888 !important; font-style: italic !important; }
      #${DROPDOWN_ID} hr { margin: 10px 0 !important; border: none !important; border-top: 1px solid #e5e5e5 !important; }

      /* --- Overlay --- */
      #${OVERLAY_ID} {
        all: initial !important;
        position: fixed !important;
        top: 0 !important;
        left: 0 !important;
        width: 100vw !important;
        height: 100vh !important;
        background-color: rgba(0, 0, 0, 0.75) !important;
        z-index: 2147483645 !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        overflow: hidden !important;
        padding: 20px !important;
        box-sizing: border-box !important;
        backdrop-filter: blur(4px) !important;
      }

      /* --- Content Panel --- */
      #${CONTENT_ID} {
        all: initial !important;
        display: block !important;
        background-color: #fefefe !important;
        color: #2d2d2d !important;
        padding: 28px 36px 36px !important;
        border-radius: 16px !important;
        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3) !important;
        max-width: 720px !important;
        width: 100% !important;
        max-height: 85vh !important;
        overflow-y: auto !important;
        overflow-x: hidden !important;
        position: relative !important;
        font-family: 'Georgia', 'Times New Roman', serif !important;
        font-size: 18px !important;
        line-height: 1.75 !important;
        box-sizing: border-box !important;
        -webkit-font-smoothing: antialiased !important;
      }

      #${CONTENT_ID} * {
        box-sizing: border-box !important;
      }

      /* --- Header Buttons --- */
      #${CONTENT_ID} .summarize-header-buttons {
        position: sticky !important;
        top: 0 !important;
        float: right !important;
        display: flex !important;
        gap: 8px !important;
        margin: -8px -8px 12px 12px !important;
        z-index: 100 !important;
      }

      #${CONTENT_ID} .summarize-header-btn {
        all: initial !important;
        width: 32px !important;
        height: 32px !important;
        border-radius: 50% !important;
        cursor: pointer !important;
        display: flex !important;
        align-items: center !important;
        justify-content: center !important;
        font-size: 16px !important;
        line-height: 1 !important;
        transition: all 0.15s ease !important;
        box-sizing: border-box !important;
      }

      #${CONTENT_ID} .summarize-close-btn {
        background: #f0f0f0 !important;
        color: #666 !important;
        font-family: Arial, sans-serif !important;
        font-weight: 300 !important;
      }
      #${CONTENT_ID} .summarize-close-btn:hover {
        background: #e0e0e0 !important;
        color: #333 !important;
      }

      #${CONTENT_ID} .summarize-chat-btn {
        background: linear-gradient(145deg, #3a7bd5, #00d2ff) !important;
        color: white !important;
        font-size: 14px !important;
      }
      #${CONTENT_ID} .summarize-chat-btn:hover {
        transform: scale(1.1) !important;
        box-shadow: 0 2px 8px rgba(58, 123, 213, 0.4) !important;
      }

      /* --- Summary Body --- */
      #${CONTENT_ID} .summarize-body {
        clear: right !important;
      }

      #${CONTENT_ID} .summarize-body p {
        margin: 0 0 1.2em 0 !important;
        font-family: 'Georgia', 'Times New Roman', serif !important;
        font-size: 18px !important;
        line-height: 1.75 !important;
        color: #2d2d2d !important;
      }

      #${CONTENT_ID} .summarize-body ul {
        margin: 1.2em 0 !important;
        padding-left: 0.5em !important;
        list-style: none !important;
      }

      #${CONTENT_ID} .summarize-body li {
        list-style-type: none !important;
        margin-bottom: 0.8em !important;
        font-family: 'Georgia', 'Times New Roman', serif !important;
        font-size: 17px !important;
        line-height: 1.7 !important;
        color: #3d3d3d !important;
      }

      #${CONTENT_ID} .summarize-body strong {
        font-weight: 700 !important;
        color: #1a1a1a !important;
      }

      /* --- Loading Glow --- */
      #${CONTENT_ID} .glow {
        display: block !important;
        font-size: 1.4em !important;
        text-align: center !important;
        padding: 60px 20px !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        font-weight: 500 !important;
        color: #3a7bd5 !important;
        animation: summarize-glow 2.5s ease-in-out infinite !important;
      }

      /* --- Article Quality Colors --- */
      #${CONTENT_ID} span.article-excellent { color: #059669 !important; font-weight: 700 !important; }
      #${CONTENT_ID} span.article-good { color: #2563eb !important; font-weight: 700 !important; }
      #${CONTENT_ID} span.article-average { color: #d97706 !important; font-weight: 700 !important; }
      #${CONTENT_ID} span.article-bad { color: #dc2626 !important; font-weight: 700 !important; }
      #${CONTENT_ID} span.article-very-bad { color: #991b1b !important; font-weight: 700 !important; }

      /* --- Chat Container (inline, no separate scroll) --- */
      #${CONTENT_ID} #summarize-chat-container {
        display: none !important;
        margin-top: 28px !important;
        padding-top: 24px !important;
        border-top: 2px solid rgba(58, 123, 213, 0.15) !important;
      }

      #${CONTENT_ID} #summarize-chat-container.active {
        display: block !important;
      }

      /* --- Chat Messages (no separate scroll, flows with content) --- */
      #${CONTENT_ID} #summarize-chat-messages {
        display: flex !important;
        flex-direction: column !important;
        gap: 16px !important;
        margin-bottom: 20px !important;
        padding: 0 !important;
      }

      /* --- iMessage Style Chat Bubbles --- */
      #${CONTENT_ID} .chat-message {
        max-width: 80% !important;
        padding: 12px 16px !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        font-size: 15px !important;
        line-height: 1.5 !important;
        word-wrap: break-word !important;
        position: relative !important;
      }

      /* User messages - green/teal on right (like iMessage sender) */
      #${CONTENT_ID} .chat-message.user {
        align-self: flex-end !important;
        background: linear-gradient(135deg, #34c759, #30d158) !important;
        color: white !important;
        border-radius: 18px 18px 4px 18px !important;
        margin-left: auto !important;
      }

      /* Assistant messages - gray on left (like iMessage receiver) */
      #${CONTENT_ID} .chat-message.assistant {
        align-self: flex-start !important;
        background: #e9e9eb !important;
        color: #1c1c1e !important;
        border-radius: 18px 18px 18px 4px !important;
        margin-right: auto !important;
      }

      /* --- Chat Input --- */
      #${CONTENT_ID} #summarize-chat-input-container {
        display: flex !important;
        gap: 10px !important;
        align-items: center !important;
        background: #f5f5f5 !important;
        padding: 8px !important;
        border-radius: 24px !important;
        margin-top: 8px !important;
      }

      #${CONTENT_ID} #summarize-chat-input {
        all: initial !important;
        flex: 1 !important;
        padding: 10px 16px !important;
        border: none !important;
        background: transparent !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        font-size: 15px !important;
        color: #2d2d2d !important;
        outline: none !important;
        box-sizing: border-box !important;
      }

      #${CONTENT_ID} #summarize-chat-input::placeholder {
        color: #999 !important;
      }

      #${CONTENT_ID} #summarize-chat-send {
        all: initial !important;
        padding: 10px 20px !important;
        background: linear-gradient(145deg, #3a7bd5, #00d2ff) !important;
        color: white !important;
        border: none !important;
        border-radius: 20px !important;
        cursor: pointer !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        font-size: 14px !important;
        font-weight: 600 !important;
        transition: all 0.2s !important;
        box-sizing: border-box !important;
      }

      #${CONTENT_ID} #summarize-chat-send:hover {
        transform: scale(1.05) !important;
        box-shadow: 0 2px 8px rgba(58, 123, 213, 0.4) !important;
      }

      #${CONTENT_ID} #summarize-chat-send:disabled {
        opacity: 0.6 !important;
        cursor: not-allowed !important;
        transform: none !important;
      }

      /* --- Retry Button --- */
      #${CONTENT_ID} .retry-button {
        all: initial !important;
        display: block !important;
        margin: 24px auto 0 !important;
        padding: 12px 24px !important;
        background: linear-gradient(145deg, #3a7bd5, #00d2ff) !important;
        color: white !important;
        border: none !important;
        border-radius: 10px !important;
        cursor: pointer !important;
        font-size: 15px !important;
        font-weight: 600 !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        box-sizing: border-box !important;
      }
      #${CONTENT_ID} .retry-button:hover {
        transform: translateY(-2px) !important;
        box-shadow: 0 4px 12px rgba(58, 123, 213, 0.4) !important;
      }

      /* --- Error Notification --- */
      #${ERROR_ID} {
        all: initial !important;
        position: fixed !important;
        bottom: 20px !important;
        left: 50% !important;
        transform: translateX(-50%) !important;
        background-color: #dc2626 !important;
        color: white !important;
        padding: 14px 24px !important;
        border-radius: 10px !important;
        z-index: 2147483646 !important;
        font-size: 14px !important;
        font-weight: 500 !important;
        box-shadow: 0 4px 20px rgba(220, 38, 38, 0.4) !important;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
        box-sizing: border-box !important;
      }

      /* --- Scrollbar Styling --- */
      #${CONTENT_ID}::-webkit-scrollbar,
      #${DROPDOWN_ID}::-webkit-scrollbar {
        width: 8px !important;
      }
      #${CONTENT_ID}::-webkit-scrollbar-track,
      #${DROPDOWN_ID}::-webkit-scrollbar-track {
        background: transparent !important;
      }
      #${CONTENT_ID}::-webkit-scrollbar-thumb,
      #${DROPDOWN_ID}::-webkit-scrollbar-thumb {
        background: rgba(0,0,0,0.15) !important;
        border-radius: 4px !important;
      }
      #${CONTENT_ID}::-webkit-scrollbar-thumb:hover,
      #${DROPDOWN_ID}::-webkit-scrollbar-thumb:hover {
        background: rgba(0,0,0,0.25) !important;
      }

      /* --- Animations --- */
      @keyframes summarize-glow {
        0%, 100% { color: #3a7bd5; text-shadow: 0 0 12px rgba(58, 123, 213, 0.6), 0 0 24px rgba(58, 123, 213, 0.4); }
        33%      { color: #8b5cf6; text-shadow: 0 0 14px rgba(139, 92, 246, 0.7), 0 0 28px rgba(139, 92, 246, 0.5); }
        66%      { color: #ec4899; text-shadow: 0 0 14px rgba(236, 72, 153, 0.7), 0 0 28px rgba(236, 72, 153, 0.5); }
      }

      /* --- Dark Mode --- */
      @media (prefers-color-scheme: dark) {
        #${OVERLAY_ID} {
          background-color: rgba(0, 0, 0, 0.85) !important;
        }

        #${CONTENT_ID} {
          background-color: #1c1c1e !important;
          color: #e5e5e5 !important;
        }

        #${CONTENT_ID} .summarize-body p,
        #${CONTENT_ID} .summarize-body li {
          color: #e5e5e5 !important;
        }

        #${CONTENT_ID} .summarize-body strong {
          color: #ffffff !important;
        }

        #${CONTENT_ID} .summarize-close-btn {
          background: #3a3a3c !important;
          color: #999 !important;
        }
        #${CONTENT_ID} .summarize-close-btn:hover {
          background: #4a4a4c !important;
          color: #fff !important;
        }

        #${CONTENT_ID} .chat-message.assistant {
          background: #3a3a3c !important;
          color: #e5e5e5 !important;
        }

        #${CONTENT_ID} #summarize-chat-input-container {
          background: #2c2c2e !important;
        }

        #${CONTENT_ID} #summarize-chat-input {
          color: #e5e5e5 !important;
        }
        #${CONTENT_ID} #summarize-chat-input::placeholder {
          color: #666 !important;
        }

        #${CONTENT_ID} #summarize-chat-container {
          border-top-color: rgba(96, 165, 250, 0.2) !important;
        }

        #${DROPDOWN_ID} {
          background: #2c2c2e !important;
          border-color: #3a3a3c !important;
        }
        #${DROPDOWN_ID} .model-item { color: #e5e5e5 !important; }
        #${DROPDOWN_ID} .model-item:hover { background-color: rgba(58, 123, 213, 0.2) !important; color: #60a5fa !important; }
        #${DROPDOWN_ID} .group-header-container { background: #3a3a3c !important; }
        #${DROPDOWN_ID} .group-header-text { color: #e5e5e5 !important; }
        #${DROPDOWN_ID} hr { border-top-color: #3a3a3c !important; }

        #${CONTENT_ID}::-webkit-scrollbar-thumb,
        #${DROPDOWN_ID}::-webkit-scrollbar-thumb {
          background: rgba(255,255,255,0.15) !important;
        }
        #${CONTENT_ID}::-webkit-scrollbar-thumb:hover,
        #${DROPDOWN_ID}::-webkit-scrollbar-thumb:hover {
          background: rgba(255,255,255,0.25) !important;
        }

        #${CONTENT_ID} span.article-excellent { color: #34d399 !important; }
        #${CONTENT_ID} span.article-good { color: #60a5fa !important; }
        #${CONTENT_ID} span.article-average { color: #fbbf24 !important; }
        #${CONTENT_ID} span.article-bad { color: #f87171 !important; }
        #${CONTENT_ID} span.article-very-bad { color: #ef4444 !important; }
      }

      /* --- Mobile Responsiveness --- */
      @media (max-width: 600px) {
        #${OVERLAY_ID} {
          padding: 0 !important;
        }

        #${CONTENT_ID} {
          max-width: none !important;
          max-height: none !important;
          height: 100% !important;
          border-radius: 0 !important;
          padding: 20px !important;
        }

        #${CONTENT_ID} .summarize-header-buttons {
          margin: 0 0 16px 12px !important;
        }

        #${CONTENT_ID} .summarize-header-btn {
          width: 36px !important;
          height: 36px !important;
          font-size: 18px !important;
        }

        #${CONTENT_ID} .summarize-body p,
        #${CONTENT_ID} .summarize-body li {
          font-size: 16px !important;
        }

        #${CONTENT_ID} #summarize-chat-input-container {
          flex-direction: column !important;
          border-radius: 16px !important;
          padding: 12px !important;
        }

        #${CONTENT_ID} #summarize-chat-send {
          width: 100% !important;
          padding: 12px !important;
        }

        #${OVERLAY_ID} ~ #${BUTTON_ID},
        #${OVERLAY_ID} ~ #${DROPDOWN_ID} {
          display: none !important;
        }
      }
    `)
  }

  // --- Initialization ---
  // noinspection JSIgnoredPromiseFromCall
  initialize()

})()