mirror of
https://github.com/katanemo/plano.git
synced 2026-06-23 15:38:07 +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