mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
first commit of the chatGPT selector
This commit is contained in:
parent
5fb7ce576c
commit
76151c2b8d
12 changed files with 19001 additions and 0 deletions
18680
demos/use_cases/chatgpt-preference-model-selector/package-lock.json
generated
Normal file
18680
demos/use_cases/chatgpt-preference-model-selector/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
16
demos/use_cases/chatgpt-preference-model-selector/src/App.js
Normal file
16
demos/use_cases/chatgpt-preference-model-selector/src/App.js
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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!');
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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.`);
|
||||
})();
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue