{"slug": "detecting-which-ai-chat-platform-you-re-on-url-and-dom-patterns-for-chatgpt-and", "title": "Detecting Which AI Chat Platform You're On: URL and DOM Patterns for ChatGPT, Claude, Gemini, and Copilot", "summary": "A developer created a detection system for identifying which AI chat platform a user is on, using URL hostname matching as a primary check and DOM signatures as a fallback. The system covers ChatGPT, Claude, Gemini, and Copilot, with a majority-vote approach to reduce false positives. It also addresses SPA loading timing by recommending document_idle injection and MutationObserver for dynamic state changes.", "body_md": "If you're building a Chrome extension that works with AI chat platforms — prompt injection, session export, UI customization — you need to know which platform the user is on before you do anything. A content script that injects a prompt into ChatGPT's textarea will silently fail on Claude, which has a completely different DOM.\n\nHere's how to reliably detect the platform from a content script, with patterns that hold up through UI redesigns.\n\nThe fastest check: `location.hostname`\n\n. AI platforms use distinct domains with predictable structures:\n\n``` js\nconst AI_PLATFORM_HOSTS = {\n  'chatgpt.com': 'chatgpt',\n  'chat.openai.com': 'chatgpt',       // legacy domain\n  'claude.ai': 'claude',\n  'gemini.google.com': 'gemini',\n  'copilot.microsoft.com': 'copilot',\n  'bard.google.com': 'gemini',        // legacy redirect\n  'poe.com': 'poe',\n  'character.ai': 'characterai',\n};\n\nfunction detectPlatformByHost() {\n  return AI_PLATFORM_HOSTS[location.hostname] ?? null;\n}\n```\n\nThis covers the common case: user is on `claude.ai`\n\n, hostname matches, platform identified. No DOM access needed.\n\nEdge cases this misses:\n\nFor those, fall through to DOM-based detection.\n\nEach platform has distinctive DOM signatures that are more stable than class names or element IDs (which change frequently). Look for structural patterns, not implementation details:\n\n``` js\nconst PLATFORM_SIGNATURES = [\n  {\n    platform: 'chatgpt',\n    checks: [\n      () => !!document.querySelector('[data-testid=\"send-button\"]'),\n      () => !!document.querySelector('#prompt-textarea'),\n      () => document.title.includes('ChatGPT'),\n    ],\n  },\n  {\n    platform: 'claude',\n    checks: [\n      () => !!document.querySelector('[data-placeholder*=\"Reply\"]'),\n      () => !!document.querySelector('.claude-message'),\n      () => document.title.toLowerCase().includes('claude'),\n    ],\n  },\n  {\n    platform: 'gemini',\n    checks: [\n      () => !!document.querySelector('rich-textarea'),  // custom element\n      () => document.title.includes('Gemini'),\n    ],\n  },\n  {\n    platform: 'copilot',\n    checks: [\n      () => !!document.querySelector('[data-testid=\"composer-input\"]'),\n      () => location.hostname.includes('copilot.microsoft.com'),\n    ],\n  },\n];\n\nfunction detectPlatformByDOM() {\n  for (const sig of PLATFORM_SIGNATURES) {\n    const passedChecks = sig.checks.filter(fn => {\n      try { return fn(); } catch { return false; }\n    });\n    if (passedChecks.length >= 2) return sig.platform; // majority vote\n  }\n  return null;\n}\n```\n\nThe majority vote (requiring at least 2 checks to pass) reduces false positives when only one signature element happens to be present on an unrelated page.\n\nWrapping each check in `try/catch`\n\nhandles DOM state issues — some checks might throw if the element doesn't exist in the way you expect.\n\n``` js\nfunction detectPlatform() {\n  // Tier 1: URL-based (instant)\n  const byHost = detectPlatformByHost();\n  if (byHost) return byHost;\n\n  // Tier 2: DOM-based (for edge cases)\n  const byDOM = detectPlatformByDOM();\n  return byDOM;\n}\n```\n\nIn practice, tier 1 handles 95%+ of real usage. Tier 2 is the safety net.\n\nAI chat platforms are SPAs. The initial HTML is usually a shell; the actual UI loads asynchronously. Running detection immediately on `document_start`\n\nwill fail — the DOM isn't built yet.\n\nOptions:\n\n`document_idle`\n\ninjection (simplest):\n\n```\n// manifest.json\n\"content_scripts\": [{\n  \"matches\": [\"https://chatgpt.com/*\", \"https://claude.ai/*\", \"...\"],\n  \"js\": [\"content.js\"],\n  \"run_at\": \"document_idle\"\n}]\n```\n\n`document_idle`\n\nfires after `DOMContentLoaded`\n\nand after any deferred scripts run, but before images load. For most AI platforms this is sufficient — the UI is rendered by then.\n\n**MutationObserver for SPAs:**\n\nIf the platform changes state after initial load (navigating between conversations, switching models), you need to react to DOM changes:\n\n``` js\nconst observer = new MutationObserver(() => {\n  const platform = detectPlatform();\n  if (platform && platform !== currentPlatform) {\n    currentPlatform = platform;\n    onPlatformChanged(platform);\n  }\n});\n\nobserver.observe(document.body, { childList: true, subtree: false });\n```\n\n`subtree: false`\n\nis intentional here — you only need to detect major layout changes, not every DOM mutation inside the conversation thread.\n\nOnce you know the platform, you need the textarea to inject a prompt:\n\n``` js\nconst INPUT_SELECTORS = {\n  chatgpt: [\n    '#prompt-textarea',\n    'div[contenteditable=\"true\"][data-id=\"root\"]',\n  ],\n  claude: [\n    'div[contenteditable=\"true\"].ProseMirror',\n    '[data-placeholder*=\"Reply to Claude\"]',\n  ],\n  gemini: [\n    'rich-textarea .ql-editor',\n    'div[contenteditable=\"true\"].textarea',\n  ],\n  copilot: [\n    'textarea[data-testid=\"composer-input\"]',\n    'cib-text-input textarea',\n  ],\n};\n\nfunction getInputElement(platform) {\n  const selectors = INPUT_SELECTORS[platform] ?? [];\n  for (const selector of selectors) {\n    const el = document.querySelector(selector);\n    if (el) return el;\n  }\n  return null;\n}\n```\n\nContentEditable divs (used by Claude and Gemini) need different injection code than standard textareas:\n\n```\nfunction injectPrompt(element, text) {\n  if (element.tagName === 'TEXTAREA') {\n    // Standard textarea\n    element.value = text;\n    element.dispatchEvent(new Event('input', { bubbles: true }));\n  } else if (element.contentEditable === 'true') {\n    // ContentEditable (ProseMirror, Quill, etc.)\n    element.focus();\n    document.execCommand('selectAll', false, null);\n    document.execCommand('insertText', false, text);\n  }\n}\n```\n\n`document.execCommand`\n\nis technically deprecated but remains the most reliable way to trigger React/Vue reactivity on contentEditable elements. The synthetic event approach (`new InputEvent('input', { data: text, bubbles: true })`\n\n) doesn't consistently trigger framework state updates.\n\nChatGPT and Claude both navigate without page reloads (new conversation = SPA navigation, not a full reload). Your detection logic needs to handle this:\n\n``` js\nlet currentPlatform = detectPlatform();\nlet lastUrl = location.href;\n\nconst navObserver = new MutationObserver(() => {\n  if (location.href !== lastUrl) {\n    lastUrl = location.href;\n    // Re-run detection after navigation\n    setTimeout(() => {\n      currentPlatform = detectPlatform();\n    }, 500); // brief delay for new route to render\n  }\n});\n\nnavObserver.observe(document, { subtree: true, childList: true });\n```\n\nThis is the detection layer under [PromptStash](https://chromewebstore.google.com/detail/promptstash/ocgkponbnolpgobllplcamfobolbjbcj) — a Chrome extension for saving and reusing prompts across AI platforms. One shortcut inserts your saved prompt wherever you are.\n\nWhat platform-specific quirks have you run into? The Gemini `<rich-textarea>`\n\ncustom element was the most surprising — it wraps a Quill editor inside a shadow DOM subtree, which breaks standard querySelector behavior.", "url": "https://wpnews.pro/news/detecting-which-ai-chat-platform-you-re-on-url-and-dom-patterns-for-chatgpt-and", "canonical_source": "https://dev.to/ktg0215/detecting-which-ai-chat-platform-youre-on-url-and-dom-patterns-for-chatgpt-claude-gemini-and-3fkp", "published_at": "2026-06-30 16:25:30+00:00", "updated_at": "2026-06-30 16:48:59.056018+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "large-language-models", "ai-products"], "entities": ["ChatGPT", "Claude", "Gemini", "Copilot", "OpenAI", "Google", "Microsoft"], "alternates": {"html": "https://wpnews.pro/news/detecting-which-ai-chat-platform-you-re-on-url-and-dom-patterns-for-chatgpt-and", "markdown": "https://wpnews.pro/news/detecting-which-ai-chat-platform-you-re-on-url-and-dom-patterns-for-chatgpt-and.md", "text": "https://wpnews.pro/news/detecting-which-ai-chat-platform-you-re-on-url-and-dom-patterns-for-chatgpt-and.txt", "jsonld": "https://wpnews.pro/news/detecting-which-ai-chat-platform-you-re-on-url-and-dom-patterns-for-chatgpt-and.jsonld"}}