mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
pending changes for chrome extension
This commit is contained in:
parent
23d0574a7c
commit
ae52cf5a14
8 changed files with 337 additions and 145 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -24,6 +24,7 @@ wheels/
|
|||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
node_modules/
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import PreferenceBasedModelSelector from './components/PreferenceBasedModelSelec
|
|||
export default function App() {
|
||||
return (
|
||||
<div className="bg-gray-100 min-h-screen flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-4xl">
|
||||
<div className="w-full max-w-6xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-800">Set</h1>
|
||||
<p className="text-gray-600 mt-2">This is an interactive preview of the Preference-Based Model Selector.</p>
|
||||
<h1 className="text-3xl font-bold text-gray-800">ChatGPT Usage Preferences</h1>
|
||||
<p className="text-gray-600 mt-2">Define your usage preferences for each type of ChatGPT model.</p>
|
||||
</div>
|
||||
<PreferenceBasedModelSelector />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
||||
|
|
@ -20,9 +35,7 @@ const PlusCircle = ({ className }) => (
|
|||
|
||||
// --- Mocked UI Components ---
|
||||
const Card = ({ children, className = '' }) => (
|
||||
<div className={`bg-white border border-gray-200 rounded-lg shadow-sm ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
<div className={`bg-white border border-gray-200 rounded-lg shadow-sm ${className}`}>{children}</div>
|
||||
);
|
||||
const CardContent = ({ children, className = '' }) => (
|
||||
<div className={`p-4 ${className}`}>{children}</div>
|
||||
|
|
@ -30,7 +43,7 @@ const CardContent = ({ children, className = '' }) => (
|
|||
const Input = (props) => (
|
||||
<input
|
||||
{...props}
|
||||
className={`w-full px-3 py-2 text-sm text-gray-800 bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${props.className || ''}`}
|
||||
className={`w-full h-9 px-3 text-sm text-gray-800 bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${props.className || ''}`}
|
||||
/>
|
||||
);
|
||||
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 (
|
||||
<div className="w-[450px] bg-gray-50 p-4">
|
||||
<div className="w-full max-w-[600px] bg-gray-50 p-4 mx-auto">
|
||||
<h2 className="text-lg font-semibold text-center mb-4">Model Preferences</h2>
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<Card className="w-full">
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="routingEnabled" className="font-medium">
|
||||
Enable preference-based routing
|
||||
</Label>
|
||||
<Switch id="routingEnabled" checked={routingEnabled} onCheckedChange={setRoutingEnabled} />
|
||||
<Label>Enable preference-based routing</Label>
|
||||
<Switch checked={routingEnabled} onCheckedChange={setRoutingEnabled} />
|
||||
</div>
|
||||
{routingEnabled && (
|
||||
<div className="pt-4 mt-4 space-y-3 border-t border-gray-200">
|
||||
{preferences.map((pref) => (
|
||||
<div key={pref.id} className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
|
||||
<div key={pref.id} className="grid grid-cols-[3fr_1fr_auto] gap-4 items-center">
|
||||
<Input
|
||||
placeholder="e.g., summarize articles"
|
||||
value={pref.naturalLanguage}
|
||||
onChange={(e) => 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)}
|
||||
/>
|
||||
<select
|
||||
value={pref.model}
|
||||
onChange={(e) => updatePreference(pref.id, "model", e.target.value)}
|
||||
className="w-full px-3 py-2 text-sm text-gray-800 bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
onChange={(e) => updatePreference(pref.id, 'model', e.target.value)}
|
||||
className="h-9 w-full px-3 text-sm bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option disabled value="">
|
||||
Select Model
|
||||
</option>
|
||||
{modelOptions.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
{modelOptions.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removePreference(pref.id)}
|
||||
className="text-gray-500 hover:text-red-600 disabled:opacity-50"
|
||||
disabled={preferences.length <= 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<Button variant="ghost" size="icon" onClick={() => removePreference(pref.id)} disabled={preferences.length <= 1}>
|
||||
<Trash2 className="h-4 w-4 text-gray-500 hover:text-red-600" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button variant="outline" onClick={addPreference} className="flex gap-2 items-center text-sm mt-2">
|
||||
<Button variant="outline" onClick={addPreference} className="flex items-center gap-2 text-sm mt-2">
|
||||
<PlusCircle className="h-4 w-4" /> Add Preference
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<Card className="w-full">
|
||||
<CardContent>
|
||||
<Label htmlFor="defaultModel" className="font-medium">
|
||||
Default Model on Page Load
|
||||
</Label>
|
||||
<Label>Default Model on Page Load</Label>
|
||||
<select
|
||||
id="defaultModel"
|
||||
value={defaultModel}
|
||||
onChange={(e) => setDefaultModel(e.target.value)}
|
||||
className="w-full mt-2 px-3 py-2 text-sm text-gray-800 bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
className="h-9 w-full mt-2 px-3 text-sm bg-white border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{modelOptions.map((model) => (
|
||||
<option key={model} value={model}>
|
||||
{model}
|
||||
{modelOptions.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -209,4 +202,4 @@ export default function PreferenceBasedModelSelector() {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -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`);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue