From ae52cf5a144c29a99f3547c094b4cbc9ae895eb3 Mon Sep 17 00:00:00 2001 From: Salman Paracha Date: Thu, 26 Jun 2025 11:48:00 -0700 Subject: [PATCH] pending changes for chrome extension --- .gitignore | 1 + .../public/manifest.json | 22 ++- .../src/App.js | 6 +- .../src/build.js | 43 +++-- .../PreferenceBasedModelSelector.js | 143 ++++++++-------- .../src/scripts/background.js | 36 +++++ .../src/scripts/content.js | 152 ++++++++++++------ .../src/scripts/pageFetchOverride.js | 79 +++++++++ 8 files changed, 337 insertions(+), 145 deletions(-) create mode 100644 demos/use_cases/chatgpt-preference-model-selector/src/scripts/background.js create mode 100644 demos/use_cases/chatgpt-preference-model-selector/src/scripts/pageFetchOverride.js diff --git a/.gitignore b/.gitignore index b140bbbe..dc20274c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ wheels/ .installed.cfg *.egg MANIFEST +node_modules/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/demos/use_cases/chatgpt-preference-model-selector/public/manifest.json b/demos/use_cases/chatgpt-preference-model-selector/public/manifest.json index 7c16d748..b2308182 100644 --- a/demos/use_cases/chatgpt-preference-model-selector/public/manifest.json +++ b/demos/use_cases/chatgpt-preference-model-selector/public/manifest.json @@ -1,19 +1,28 @@ { "manifest_version": 3, "name": "ChatGPT Preference-Based Model Selector", - "version": "1.0", - "description": "Define usage preferences for your chatGPT models.", + "version": "1.2", + "description": "Define usage preferences for your ChatGPT models and proxy conversation requests through ArchGW.", "permissions": [ "storage", - "tabs" + "tabs", + "scripting" ], "host_permissions": [ - "https://chatgpt.com/*" + "https://chatgpt.com/*", + "http://localhost:12000/*" ], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'; connect-src 'self' http://localhost:12000" + }, "web_accessible_resources": [ { "resources": ["index.html"], - "matches": ["https://chat.openai.com/*"] + "matches": ["https://chatgpt.com/*"] + }, + { + "resources": ["pageFetchOverride.js"], + "matches": ["https://chatgpt.com/*"] } ], "action": { @@ -22,7 +31,8 @@ "content_scripts": [ { "matches": ["https://chatgpt.com/*"], - "js": ["./static/js/content.js"] + "js": ["static/js/content.js"], + "run_at": "document_start" } ] } diff --git a/demos/use_cases/chatgpt-preference-model-selector/src/App.js b/demos/use_cases/chatgpt-preference-model-selector/src/App.js index ae12e859..3617e554 100644 --- a/demos/use_cases/chatgpt-preference-model-selector/src/App.js +++ b/demos/use_cases/chatgpt-preference-model-selector/src/App.js @@ -4,10 +4,10 @@ import PreferenceBasedModelSelector from './components/PreferenceBasedModelSelec export default function App() { return (
-
+
-

Set

-

This is an interactive preview of the Preference-Based Model Selector.

+

ChatGPT Usage Preferences

+

Define your usage preferences for each type of ChatGPT model.

diff --git a/demos/use_cases/chatgpt-preference-model-selector/src/build.js b/demos/use_cases/chatgpt-preference-model-selector/src/build.js index 19fa6071..36f2ef70 100644 --- a/demos/use_cases/chatgpt-preference-model-selector/src/build.js +++ b/demos/use_cases/chatgpt-preference-model-selector/src/build.js @@ -4,32 +4,43 @@ const path = require('path'); console.log('Starting the custom build process for the Chrome Extension...'); -// Define paths -const contentScriptSource = path.join(__dirname, '..', 'src', 'scripts', 'content.js'); -const buildDir = path.join(__dirname, '..', 'build'); -const contentScriptDestDir = path.join(buildDir, 'static', 'js'); +const reactAppDir = path.join(__dirname, '..'); +const contentScriptSource = path.join(reactAppDir, 'src', 'scripts', 'content.js'); +const pageOverrideSource = path.join(reactAppDir, 'src', 'scripts', 'pageFetchOverride.js'); +const buildDir = path.join(reactAppDir, 'build'); +const contentScriptDest = path.join(buildDir, 'static', 'js'); -// Step 1: Run the standard React build script +// 1️⃣ Run React build try { console.log('Running react-scripts build...'); execSync('react-scripts build', { stdio: 'inherit' }); console.log('React build completed successfully.'); -} catch (error) { - console.error('React build failed. Please check the errors above.'); +} catch (err) { + console.error('React build failed:', err); process.exit(1); } -// Step 2: Copy the content script to the build directory +// 2️⃣ Copy content.js try { - // Ensure the destination directory exists (it should after the build) - if (fs.existsSync(contentScriptDestDir)) { - fs.copyFileSync(contentScriptSource, path.join(contentScriptDestDir, 'content.js')); - console.log(`Successfully copied content.js to ${contentScriptDestDir}`); - } else { - throw new Error(`Destination directory not found: ${contentScriptDestDir}. The build might have failed.`); + if (!fs.existsSync(contentScriptDest)) { + throw new Error(`Missing directory: ${contentScriptDest}`); } -} catch (error) { - console.error('Failed to copy content script:', error); + fs.copyFileSync(contentScriptSource, path.join(contentScriptDest, 'content.js')); + console.log(`Copied content.js → ${contentScriptDest}`); +} catch (err) { + console.error('Failed to copy content.js:', err); + process.exit(1); +} + +// 3️⃣ Copy pageFetchOverride.js +try { + if (!fs.existsSync(buildDir)) { + throw new Error(`Missing build directory: ${buildDir}`); + } + fs.copyFileSync(pageOverrideSource, path.join(buildDir, 'pageFetchOverride.js')); + console.log(`Copied pageFetchOverride.js → ${buildDir}`); +} catch (err) { + console.error('Failed to copy pageFetchOverride.js:', err); process.exit(1); } diff --git a/demos/use_cases/chatgpt-preference-model-selector/src/components/PreferenceBasedModelSelector.js b/demos/use_cases/chatgpt-preference-model-selector/src/components/PreferenceBasedModelSelector.js index 9c773442..f1d1b920 100644 --- a/demos/use_cases/chatgpt-preference-model-selector/src/components/PreferenceBasedModelSelector.js +++ b/demos/use_cases/chatgpt-preference-model-selector/src/components/PreferenceBasedModelSelector.js @@ -1,6 +1,21 @@ /*global chrome*/ import React, { useState, useEffect } from 'react'; +// --- Hard‑coded list of ChatGPT models --- +const MODEL_LIST = [ + 'gpt-4o', + 'gpt-4.1', + 'gpt-4.1 mini', + 'gpt-4.5 preview', + 'o3', + 'o3-pro', + 'o4-mini', + 'o4-mini-high', + 'o1', + 'o1-mini', + 'o1-pro' +]; + // --- Mocked lucide-react icons as SVG components --- const Trash2 = ({ className }) => ( @@ -20,9 +35,7 @@ const PlusCircle = ({ className }) => ( // --- Mocked UI Components --- const Card = ({ children, className = '' }) => ( -
- {children} -
+
{children}
); const CardContent = ({ children, className = '' }) => (
{children}
@@ -30,7 +43,7 @@ const CardContent = ({ children, className = '' }) => ( const Input = (props) => ( ); const Button = ({ children, variant = 'default', size = 'default', className = '', ...props }) => { @@ -72,129 +85,109 @@ const Label = (props) => ( export default function PreferenceBasedModelSelector() { const [routingEnabled, setRoutingEnabled] = useState(false); - const [preferences, setPreferences] = useState([{ id: 1, naturalLanguage: "write poems", model: "gpt-4" }]); - const [defaultModel, setDefaultModel] = useState("gpt-4"); - const modelOptions = ["gpt-3.5-turbo", "gpt-4", "gpt-4o"]; + const [preferences, setPreferences] = useState([ + { id: 1, usage: 'generate code snippets', model: 'gpt-4o' } + ]); + const [defaultModel, setDefaultModel] = useState('gpt-4o'); + const [modelOptions] = useState(MODEL_LIST); // static list, no dynamic fetch - // Load settings from chrome.storage when the component mounts + // Load saved settings useEffect(() => { - if (chrome.storage) { - chrome.storage.sync.get(['routingEnabled', 'preferences', 'defaultModel'], (result) => { - if (result.routingEnabled !== undefined) setRoutingEnabled(result.routingEnabled); - if (result.preferences) setPreferences(result.preferences); - if (result.defaultModel) setDefaultModel(result.defaultModel); - }); - } + chrome.storage.sync.get(['routingEnabled', 'preferences', 'defaultModel'], (result) => { + if (result.routingEnabled !== undefined) setRoutingEnabled(result.routingEnabled); + if (result.preferences) setPreferences(result.preferences); + if (result.defaultModel) setDefaultModel(result.defaultModel); + }); }, []); const updatePreference = (id, key, value) => { - setPreferences((prev) => - prev.map((p) => (p.id === id ? { ...p, [key]: value } : p)) - ); + setPreferences((prev) => prev.map((p) => (p.id === id ? { ...p, [key]: value } : p))); }; const addPreference = () => { - const newId = - preferences.length > 0 ? Math.max(...preferences.map((p) => p.id)) + 1 : 1; - setPreferences([...preferences, { id: newId, naturalLanguage: "", model: defaultModel }]); + const newId = preferences.length ? Math.max(...preferences.map((p) => p.id)) + 1 : 1; + setPreferences((prev) => [...prev, { id: newId, usage: '', model: defaultModel }]); }; const removePreference = (id) => { if (preferences.length > 1) { - setPreferences(preferences.filter((p) => p.id !== id)); + setPreferences((prev) => prev.filter((p) => p.id !== id)); } }; + // Save settings: generate name slug and store tuples const handleSave = () => { - const settings = { routingEnabled, preferences, defaultModel }; - // Save to chrome.storage - if (chrome.storage) { - chrome.storage.sync.set(settings, () => { - console.log("[PBMS] Settings saved."); - }); - } - - // Send a message to the active tab's content script - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - const modelToApply = defaultModel; // Replace with your routing logic - chrome.tabs.sendMessage(tabs[0].id, { action: "applyModelSelection", model: modelToApply }); + const tuples = preferences.map((p) => { + const slug = p.usage.split(/\s+/).slice(0, 3).join('-').toLowerCase() || 'route'; + return { name: slug, usage: p.usage, model: p.model }; }); - - // Close the injected modal by messaging the parent page - console.log("[PBMS] Save clicked → closing modal"); - window.parent.postMessage({ action: "CLOSE_PBMS_MODAL" }, "*"); + chrome.storage.sync.set({ routingEnabled, preferences: tuples, defaultModel }, () => { + console.log('[PBMS] Saved tuples:', tuples); + }); + // Notify content script + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + chrome.tabs.sendMessage(tabs[0].id, { action: 'applyModelSelection', model: defaultModel }); + }); + window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*'); }; - const handleCancel = () => { - console.log("[PBMS] Cancel clicked → closing modal"); - window.parent.postMessage({ action: "CLOSE_PBMS_MODAL" }, "*"); + window.parent.postMessage({ action: 'CLOSE_PBMS_MODAL' }, '*'); }; return ( -
+

Model Preferences

- +
- - + +
{routingEnabled && (
{preferences.map((pref) => ( -
+
updatePreference(pref.id, "naturalLanguage", e.target.value)} + placeholder="Usage (e.g. generate tests)" + value={pref.usage} + onChange={(e) => updatePreference(pref.id, 'usage', e.target.value)} /> -
))} -
)} - + - + @@ -209,4 +202,4 @@ export default function PreferenceBasedModelSelector() {
); -} +} \ No newline at end of file diff --git a/demos/use_cases/chatgpt-preference-model-selector/src/scripts/background.js b/demos/use_cases/chatgpt-preference-model-selector/src/scripts/background.js new file mode 100644 index 00000000..2efa549a --- /dev/null +++ b/demos/use_cases/chatgpt-preference-model-selector/src/scripts/background.js @@ -0,0 +1,36 @@ +// background.js +chrome.runtime.onMessage.addListener((msg, sender) => { + if (msg.type === 'ARCHGW_STREAM') { + const { url, init, port } = msg; + + fetch(url, { + method: init.method || 'POST', + headers: init.headers, + body: init.body, + credentials: init.credentials ?? 'same-origin', + // ...copy other init options as needed + }).then(res => { + const reader = res.body.getReader(); + function read() { + reader.read().then(({ done, value }) => { + if (done) { + port.postMessage({ done: true }); + port.close(); + } else { + // Send each chunk (Uint8Array) to the content script + port.postMessage({ chunk: value.buffer }, [value.buffer]); + read(); + } + }); + } + read(); + }).catch(err => { + // In case of error, signal done + port.postMessage({ done: true }); + port.close(); + }); + + // Indicate we’re handling this asynchronously + return true; + } +}); diff --git a/demos/use_cases/chatgpt-preference-model-selector/src/scripts/content.js b/demos/use_cases/chatgpt-preference-model-selector/src/scripts/content.js index 929cebba..4fba5b28 100644 --- a/demos/use_cases/chatgpt-preference-model-selector/src/scripts/content.js +++ b/demos/use_cases/chatgpt-preference-model-selector/src/scripts/content.js @@ -1,56 +1,118 @@ +// content.js + (() => { - const TAG = "[ModelSelector]"; + const TAG = '[ModelSelector]'; - // Find the model selector button by visible label - const findSelectorButton = () => - [...document.querySelectorAll('button')].find( - btn => btn.textContent.match(/GPT|Default|4o/i) - ); - - let selectorButton = findSelectorButton(); - if (!selectorButton) { - console.warn(`${TAG} Model selector button not found—will retry on DOM changes.`); - } else { - console.log(`${TAG} Listener attached to model selector.`); - selectorButton.addEventListener('click', () => { - console.log(`${TAG} Model selector clicked (dropdown opening).`); + /**─────────────────────── 0️⃣ Broadcast initial settings ───────────────────────**/ + chrome.storage.sync.get(['preferences','defaultModel'], settings => { + window.postMessage({ type: 'PBMS_SETTINGS', settings }, '*'); + }); + chrome.storage.onChanged.addListener(() => { + chrome.storage.sync.get(['preferences','defaultModel'], settings => { + window.postMessage({ type: 'PBMS_SETTINGS', settings }, '*'); }); - } - - // Observe for late loads or UI updates - const initObserver = new MutationObserver(() => { - if (!selectorButton) { - selectorButton = findSelectorButton(); - if (selectorButton) { - console.log(`${TAG} (late) Listener attached to model selector.`); - selectorButton.addEventListener('click', () => { - console.log(`${TAG} Model selector clicked (dropdown opening).`); - }); - } - } }); - initObserver.observe(document.body, { childList: true, subtree: true }); - // Observe dropdown insertions - const menuObserver = new MutationObserver(mutations => { - for (const m of mutations) { - for (const node of m.addedNodes) { - if ( - node.nodeType === 1 && - node.querySelector && - node.querySelector('[role="menu"]') - ) { - console.log(`${TAG} Dropdown menu opened.`); - node.querySelectorAll('[role="menuitem"]').forEach(item => { - item.addEventListener('click', () => { - console.log(`${TAG} Model selected →`, item.innerText.trim()); - }); - }); + /**─────────────────────── 1️⃣ Inject page-context fetch override ───────────────────────**/ + (function injectPageFetchOverride() { + const injectorTag = '[ModelSelector][Injector]'; + const s = document.createElement('script'); + s.src = chrome.runtime.getURL('pageFetchOverride.js'); + s.onload = () => { + console.log(`${injectorTag} loaded pageFetchOverride.js`); + s.remove(); + }; + (document.head || document.documentElement).appendChild(s); + })(); + + /**─────────────────────── 2️⃣ Handle proxied fetch from the page ───────────────────────**/ + window.addEventListener('message', ev => { + console.log('[ModelSelector] page→content message', ev.data, ev.ports); + if (ev.source !== window || ev.data?.type !== 'ARCHGW_FETCH') return; + const { url, init } = ev.data; + const port = ev.ports[0]; + + (async () => { + try { + const res = await fetch(url, init); + const reader = res.body.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + port.postMessage({ done: true }); + break; + } else { + port.postMessage({ chunk: value.buffer }, [value.buffer]); + } } + } catch (err) { + console.error(`${TAG} proxy fetch error`, err); + port.postMessage({ done: true }); } + })(); + }); + + /**─────────────────────── 3️⃣ DOM patch for model selector label ───────────────────────**/ + let desiredModel = null; + function patchDom() { + if (desiredModel == null) return; + const btn = document.querySelector('[data-testid="model-switcher-dropdown-button"]'); + if (!btn) return; + const span = btn.querySelector('span.text-token-text-tertiary') || btn.querySelector('span'); + const wantLabel = `Model selector, current model is ${desiredModel}`; + if (span && span.textContent !== desiredModel) span.textContent = desiredModel; + if (btn.getAttribute('aria-label') !== wantLabel) btn.setAttribute('aria-label', wantLabel); + } + const observer = new MutationObserver(patchDom); + observer.observe(document.body || document.documentElement, { + subtree: true, childList: true, characterData: true, attributes: true + }); + chrome.storage.sync.get(['defaultModel'], ({ defaultModel }) => { + if (defaultModel) { desiredModel = defaultModel; patchDom(); } + }); + chrome.runtime.onMessage.addListener(msg => { + if (msg.action === 'applyModelSelection' && msg.model) { + desiredModel = msg.model; + patchDom(); } }); - menuObserver.observe(document.body, { childList: true, subtree: true }); - console.log(`${TAG} Content script injected.`); + /**─────────────────────── 4️⃣ Modal / dropdown interception ───────────────────────**/ + function showModal() { + if (document.getElementById('pbms-overlay')) return; + const overlay = document.createElement('div'); + overlay.id = 'pbms-overlay'; + Object.assign(overlay.style, { + position:'fixed', top:0, left:0, + width:'100vw', height:'100vh', + background:'rgba(0,0,0,0.4)', + display:'flex', alignItems:'center', justifyContent:'center', + zIndex:2147483647 + }); + const iframe = document.createElement('iframe'); + iframe.src = chrome.runtime.getURL('index.html'); + Object.assign(iframe.style,{ + width:'500px', height:'600px', + border:0, borderRadius:'8px', + boxShadow:'0 4px 16px rgba(0,0,0,0.2)', + background:'white', zIndex:2147483648 + }); + overlay.addEventListener('click', e => e.target===overlay && overlay.remove()); + overlay.appendChild(iframe); + document.body.appendChild(overlay); + } + function interceptDropdown(ev) { + if (!ev.target.closest('button[aria-haspopup="menu"]')) return; + ev.preventDefault(); ev.stopPropagation(); + showModal(); + } + document.addEventListener('pointerdown', interceptDropdown, true); + document.addEventListener('mousedown', interceptDropdown, true); + window.addEventListener('message', ev => { + if (ev.data?.action === 'CLOSE_PBMS_MODAL') { + document.getElementById('pbms-overlay')?.remove(); + } + }); + + console.log(`${TAG} content script initialized`); })(); diff --git a/demos/use_cases/chatgpt-preference-model-selector/src/scripts/pageFetchOverride.js b/demos/use_cases/chatgpt-preference-model-selector/src/scripts/pageFetchOverride.js new file mode 100644 index 00000000..56c6c002 --- /dev/null +++ b/demos/use_cases/chatgpt-preference-model-selector/src/scripts/pageFetchOverride.js @@ -0,0 +1,79 @@ +// pageFetchOverride.js +(function() { + const TAG = '[ModelSelector][Page]'; + console.log(`${TAG} installing fetch override`); + + window.archgwSettings = window.archgwSettings || { preferences: [], defaultModel: null }; + window.addEventListener('message', ev => { + if (ev.source === window && ev.data?.type === 'PBMS_SETTINGS') { + window.archgwSettings = ev.data.settings; + console.log(`${TAG} got updated settings`, window.archgwSettings); + } + }); + + const origFetch = window.fetch; + window.fetch = async function(input, init = {}) { + const urlString = typeof input === 'string' ? input : input.url; + console.log(`${TAG} fetch →`, urlString); + + let pathname; + try { + pathname = new URL(urlString).pathname; + } catch { + pathname = urlString; + } + + if (pathname === '/backend-api/conversation') { + console.log(`${TAG} matched conversation → proxy via content script`); + + // patch metadata + let body = {}; + try { body = JSON.parse(init.body); } catch {} + body.metadata = { + archgw_preference_config: window.archgwSettings.preferences + .map(p => `- name: ${p.name}\n model: ${p.model}\n usage: ${p.usage}`) + .join('\n') + }; + init.body = JSON.stringify(body); + + // send only the serializable parts of `init` + const safeInit = { + method: init.method, + headers: init.headers, + body: init.body, + credentials: init.credentials + }; + + // set up MessageChannel + const { port1, port2 } = new MessageChannel(); + window.postMessage({ + type: 'ARCHGW_FETCH', + url: 'http://localhost:12000/v1/chat/completions', + init: safeInit + }, '*', [port2]); + + // return streaming response + return new Response(new ReadableStream({ + start(controller) { + port1.onmessage = ({ data }) => { + if (data.done) { + controller.close(); + port1.close(); + } else { + controller.enqueue(new Uint8Array(data.chunk)); + } + }; + }, + cancel() { + port1.close(); + } + }), { + headers: { 'Content-Type': 'text/event-stream' } + }); + } + + return origFetch(input, init); + }; + + console.log(`${TAG} fetch override installed`); +})();