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)}
/>
-
))}
-
+
Add Preference
)}
-
+
-
+
@@ -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`);
+})();