Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
Binary file added ChatMultiAI-extension.zip
Binary file not shown.
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
# ChatMultiAI
Send prompts to multiple AI assistants at once

[Chrome web store - ChatMultiAI](https://chromewebstore.google.com/detail/chatmultiai/jlmpfilkbghnodgcgdbkhebbllanpfbf?authuser=0&hl=en)
## Install (Manual, from GitHub)
1. Download the latest release ZIP from GitHub Releases: https://github.com/inspiretelapps/ChatMultiAI/releases
2. Unzip the file.
3. Open `chrome://extensions`.
4. Enable **Developer mode** and click **Load unpacked**.
5. Select the unzipped folder.

![Screenshot 2025-04-01 at 16.20.36.png](images/Screenshot%202025-04-01%20at%2016.20.36.png)
![Screenshot 2025-04-01 at 16.21.52.png](images/Screenshot%202025-04-01%20at%2016.21.52.png)
**Release ZIP name:** The asset is published as `ChatMultiAI-extension.zip` in each GitHub Release.

## Release Checklist (ZIP)
1. Run `scripts/release-zip.sh` to build and package the ZIP.
2. Publish a GitHub Release and upload `ChatMultiAI-extension.zip`.

## Recent Changes

### Fork Updates (inspiretelapps)

- **Fixed Grok integration**: Implemented a main-world script approach to properly handle Grok's React-based input. Uses contenteditable element detection and clipboard paste simulation for reliable prompt filling.
- **Removed model chooser**: Simplified the interface by removing the model selection dropdown - the extension now works with the default model on each AI platform.
- **Updated chat window styling**: Changed the background of the chat window for a cleaner look.
- **Removed DeepSeek support**: Focused on the four main AI providers (ChatGPT, Claude, Gemini, Grok).

## Supported AI Providers

- ChatGPT
- Claude
- Gemini
- Grok
3 changes: 3 additions & 0 deletions chat-multi-ai/assets/automation.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified chat-multi-ai/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 8 additions & 2 deletions chat-multi-ai/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,20 @@
"manifest": {
"permissions": [
"sidePanel",
"tabs"
"tabs",
"scripting"
],
"host_permissions": [
"https://chatgpt.com/*",
"https://grok.com/*",
"https://chat.deepseek.com/*",
"https://claude.ai/*",
"https://gemini.google.com/*"
],
"web_accessible_resources": [
{
"resources": ["grok-main-world.*.js"],
"matches": ["https://grok.com/*"]
}
]
}
}
1 change: 0 additions & 1 deletion chat-multi-ai/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const getDomainFromUrl = (url: string): string => {
// Extract base domain for matching
if (hostname.includes('chatgpt.com')) return 'chatgpt.com'
if (hostname.includes('grok.com')) return 'grok.com'
if (hostname.includes('chat.deepseek.com')) return 'chat.deepseek.com'
if (hostname.includes('claude.ai')) return 'claude.ai'
if (hostname.includes('gemini.google.com')) return 'gemini.google.com'
return hostname
Expand Down
2 changes: 1 addition & 1 deletion chat-multi-ai/src/components/ui/textarea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const Textarea = React.forwardRef<
return (
<textarea
className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
Expand Down
117 changes: 57 additions & 60 deletions chat-multi-ai/src/contents/ai-provider-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export const config: PlasmoCSConfig = {
matches: [
"https://chatgpt.com/*",
"https://grok.com/*",
"https://chat.deepseek.com/*",
"https://claude.ai/*",
"https://gemini.google.com/*"
],
Expand All @@ -16,6 +15,29 @@ export const config: PlasmoCSConfig = {

// Track if we've already processed this page
let processed = false
const GROK_MESSAGE_SOURCE = "chatmultiai"
const GROK_FILL_MESSAGE = "GROK_FILL_PROMPT"
const GROK_SENT_MESSAGE = "GROK_PROMPT_SENT"

const isGrokPage = window.location.hostname.includes("grok.com")

// Set up message listener for Grok
// The grok-main-world.ts is automatically injected by Plasmo with world: "MAIN"
if (isGrokPage) {
// Listen for messages from the main-world script
window.addEventListener("message", (event) => {
if (event.source !== window) return
const data = event.data
if (!data || data.source !== GROK_MESSAGE_SOURCE) return
if (data.type === GROK_SENT_MESSAGE) {
chrome.runtime.sendMessage({ type: "PROMPT_SENT" }).catch((err) => {
console.log("Failed to notify background script that prompt was sent:", err)
})
}
})

console.log("ChatMultiAI: Grok content script loaded, main-world script is handled by Plasmo")
}

// Wait for the DOM to be fully loaded and interactive
function waitForPageLoad() {
Expand Down Expand Up @@ -116,28 +138,17 @@ async function fillInputBox(prompt: string, autoSend: boolean = false, isFollowU
}
}
else if (domain.includes("grok.com")) {
// Grok input selector
const textarea = await waitForElement("textarea.w-full.bg-transparent.focus\\:outline-none.text-primary")
if (textarea instanceof HTMLTextAreaElement) {
textarea.value = prompt
textarea.style.height = "auto" // Reset height
textarea.dispatchEvent(new Event("input", { bubbles: true }))
// Trigger resize if needed
textarea.dispatchEvent(new Event("change", { bubbles: true }))
console.log("ChatMultiAI: Successfully filled Grok input")

// Auto-submit only if autoSend is true
if (autoSend) {
const sendButton = await waitForElement("button[type='submit']")
if (sendButton instanceof HTMLButtonElement && !sendButton.disabled) {
sendButton.click()
console.log("ChatMultiAI: Auto-sent prompt to Grok")
promptWasSent = true
} else {
console.log("ChatMultiAI: Could not find or click send button for Grok")
}
}
}
// Grok requires main-world execution to update React state reliably.
console.log("ChatMultiAI: Posting Grok prompt to main world")
window.postMessage(
{
source: GROK_MESSAGE_SOURCE,
type: GROK_FILL_MESSAGE,
prompt,
autoSend
},
"*"
)
}
else if (domain.includes("gemini.google.com")) {
// Gemini input selector
Expand Down Expand Up @@ -170,47 +181,33 @@ async function fillInputBox(prompt: string, autoSend: boolean = false, isFollowU
}
}
}
else if (domain.includes("chat.deepseek.com")) {
// DeepSeek input selector
const textarea = await waitForElement("textarea#chat-input")
if (textarea instanceof HTMLTextAreaElement) {
textarea.value = prompt
textarea.dispatchEvent(new Event("input", { bubbles: true }))
console.log("ChatMultiAI: Successfully filled DeepSeek input")

// Auto-submit only if autoSend is true
if (autoSend) {
const sendButton = await waitForElement("div[role='button'][aria-disabled='false']")
if (sendButton instanceof HTMLElement) {
sendButton.click()
console.log("ChatMultiAI: Auto-sent prompt to DeepSeek")
promptWasSent = true
} else {
console.log("ChatMultiAI: Could not find or click send button for DeepSeek")
}
}
}
}
else if (domain.includes("claude.ai")) {
// Claude input selector
const contentEditableDiv = await waitForElement("div.ProseMirror[contenteditable='true']")
// Claude uses ProseMirror editor
const contentEditableDiv = await waitForElement("div.ProseMirror[contenteditable='true']") as HTMLElement
if (contentEditableDiv) {
// Clear existing content
contentEditableDiv.innerHTML = ""

// Create a paragraph element
const paragraph = document.createElement("p")
paragraph.textContent = prompt

// Append the paragraph to the contenteditable div
contentEditableDiv.appendChild(paragraph)

// Trigger input event
contentEditableDiv.dispatchEvent(new Event("input", { bubbles: true }))
// Focus the editor first
contentEditableDiv.focus()

// Wait a moment for focus to take effect
await new Promise(resolve => setTimeout(resolve, 50))

// Use execCommand to select all and replace - this properly updates ProseMirror state
// execCommand is the most reliable way to update contenteditable editors
document.execCommand('selectAll', false, undefined)
document.execCommand('insertText', false, prompt)

// Dispatch input event to notify any listeners
contentEditableDiv.dispatchEvent(new InputEvent('input', {
bubbles: true,
inputType: 'insertText',
data: prompt
}))

console.log("ChatMultiAI: Successfully filled Claude input")

// Auto-submit only if autoSend is true
if (autoSend) {
await new Promise(resolve => setTimeout(resolve, 100))
const sendButton = await waitForElement("button[type='button'][aria-label='Send Message']")
if (sendButton instanceof HTMLButtonElement) {
sendButton.click()
Expand Down Expand Up @@ -263,4 +260,4 @@ async function main() {
})
}

main()
main()
Loading