first commit of the chatGPT selector

This commit is contained in:
Salman Paracha 2025-06-25 12:13:11 -07:00
parent 5fb7ce576c
commit 76151c2b8d
12 changed files with 19001 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
{
"name": "preference-selector-extension",
"version": "0.1.0",
"private": true,
"homepage": ".",
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"scripts": {
"start": "react-scripts start",
"build": "node src/build.js",
"test": "react-scripts test"
},
"devDependencies": {
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"react-scripts": "5.0.1",
"tailwindcss": "^3.4.4"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-g" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -0,0 +1,28 @@
{
"manifest_version": 3,
"name": "ChatGPT Preference-Based Model Selector",
"version": "1.0",
"description": "Define usage preferences for your chatGPT models.",
"permissions": [
"storage",
"tabs"
],
"host_permissions": [
"https://chatgpt.com/*"
],
"web_accessible_resources": [
{
"resources": ["index.html"],
"matches": ["https://chat.openai.com/*"]
}
],
"action": {
"default_popup": "index.html"
},
"content_scripts": [
{
"matches": ["https://chatgpt.com/*"],
"js": ["./static/js/content.js"]
}
]
}

View file

@ -0,0 +1,16 @@
import React from 'react';
import PreferenceBasedModelSelector from './components/PreferenceBasedModelSelector';
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="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>
</div>
<PreferenceBasedModelSelector />
</div>
</div>
);
}

View file

@ -0,0 +1,36 @@
const { execSync } = require('child_process');
const fs = require('fs');
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');
// Step 1: Run the standard React build script
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.');
process.exit(1);
}
// Step 2: Copy the content script to the build directory
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.`);
}
} catch (error) {
console.error('Failed to copy content script:', error);
process.exit(1);
}
console.log('Extension build process finished successfully!');

View file

@ -0,0 +1,95 @@
/*global chrome*/
import React, { useState, useEffect } from 'react';
// --- 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}><path d="M3 6h18" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" y1="11" x2="10" y2="17" /><line x1="14" y1="11" x2="14" y2="17" /></svg> );
const PlusCircle = ({ 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}><circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="16" /><line x1="8" y1="12" x2="16" y2="12" /></svg> );
// --- Mocked UI Components ---
const Card = ({ children, className = '' }) => (<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>);
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 || ''}`} />);
const Button = ({ children, variant = 'default', size = 'default', className = '', ...props }) => { const baseClasses = 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'; const variantClasses = { default: 'bg-gray-900 text-white hover:bg-gray-800 focus:ring-gray-900', outline: 'border border-gray-300 bg-transparent hover:bg-gray-100 focus:ring-gray-400', ghost: 'hover:bg-gray-100 hover:text-gray-900 focus:ring-gray-400' }; const sizeClasses = { default: 'h-9 px-3', icon: 'h-9 w-9' }; return (<button {...props} className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`}>{children}</button>); };
const Switch = ({ checked, onCheckedChange, id }) => (<button type="button" role="switch" aria-checked={checked} onClick={() => onCheckedChange(!checked)} id={id} className={`relative inline-flex items-center h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${checked ? 'bg-gray-900' : 'bg-gray-300'}`}><span aria-hidden="true" className={`inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out ${checked ? 'translate-x-5' : 'translate-x-0'}`} /></button>);
const Label = (props) => (<label {...props} className={`text-sm font-medium leading-none text-gray-700 ${props.className || ''}`} />);
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"];
// Load settings from chrome storage when the component mounts
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);
});
}
}, []);
const updatePreference = (id, key, value) => { 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)) : 0) + 1; setPreferences([...preferences, { id: newId, naturalLanguage: "", model: defaultModel }]); };
const removePreference = (id) => { if (preferences.length > 1) { setPreferences(preferences.filter((p) => p.id !== id)); } };
const handleSave = () => {
const settings = { routingEnabled, preferences, defaultModel };
// Save to chrome storage
if (chrome.storage) {
chrome.storage.sync.set(settings, () => {
console.log("Settings saved.");
});
}
// Send a message to the active tab's content script
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
// TODO: In a real app, you wouldn't apply preference-based routing here.
// This example applies the default model for simplicity.
const modelToApply = defaultModel; // Add logic for preference routing if needed
chrome.tabs.sendMessage(tabs[0].id, { action: "applyModelSelection", model: modelToApply });
});
window.close(); // Close the popup after saving
};
return (
<div className="w-[450px] bg-gray-50 p-4">
<h2 className="text-lg font-semibold text-center mb-4">Model Preferences</h2>
<div className="space-y-4">
<Card>
<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} />
</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">
<Input placeholder="e.g., summarize articles" value={pref.naturalLanguage} onChange={(e) => updatePreference(pref.id, "naturalLanguage", 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"><option disabled value="">Select Model</option>{modelOptions.map((model) => (<option key={model} value={model}>{model}</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>
</div>
))}
<Button variant="outline" onClick={addPreference} className="flex gap-2 items-center text-sm mt-2"><PlusCircle className="h-4 w-4" /> Add Preference</Button>
</div>
)}
</CardContent>
</Card>
<Card>
<CardContent>
<Label htmlFor="defaultModel" className="font-medium">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">{modelOptions.map((model) => (<option key={model} value={model}>{model}</option>))}
</select>
</CardContent>
</Card>
<div className="flex justify-end gap-2 pt-4 border-t border-gray-200">
<Button variant="ghost" onClick={() => window.close()}>Cancel</Button>
<Button onClick={handleSave}>Save and Apply</Button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View file

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View file

@ -0,0 +1,56 @@
(() => {
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).`);
});
}
// 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());
});
});
}
}
}
});
menuObserver.observe(document.body, { childList: true, subtree: true });
console.log(`${TAG} Content script injected.`);
})();

View file

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}