├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── extension ├── .eslintrc.cjs ├── .gitignore ├── components.json ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── public │ ├── logos │ │ ├── logo128.png │ │ ├── logo16.png │ │ └── logo48.png │ └── manifest.json ├── settings.html ├── src │ ├── components │ │ ├── pages │ │ │ ├── popup │ │ │ │ └── index.tsx │ │ │ └── settings │ │ │ │ ├── api-keys.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── prompts.tsx │ │ └── ui │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── command.tsx │ │ │ ├── dialog.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── form.tsx │ │ │ ├── input.tsx │ │ │ ├── label.tsx │ │ │ ├── popover.tsx │ │ │ ├── radio-group.tsx │ │ │ └── switch.tsx │ ├── content-script.ts │ ├── index.css │ ├── lib │ │ ├── constants.ts │ │ └── utils.ts │ ├── popup.tsx │ ├── service-worker.ts │ ├── settings.tsx │ ├── utils.ts │ └── vite-env.d.ts ├── tailwind.config.cjs ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── workflows │ └── build.yml └── landing ├── .gitignore ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── logo128.png └── site.webmanifest ├── src ├── app │ ├── changelog │ │ ├── changelog.md │ │ └── page.tsx │ ├── config │ │ └── site.ts │ ├── globals.css │ ├── header.tsx │ ├── layout.tsx │ ├── page.tsx │ └── privacy │ │ └── page.tsx ├── components │ └── tweet-embed.tsx ├── hooks │ └── use-detect-browser.ts └── types │ └── mdx.d.ts ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .env* 2 | .vscode 3 | .venv 4 | !.envrc 5 | ./extension/dist 6 | */dist -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Hi! Nice to have you here! :) 4 | 5 | First of all, huge thanks for your interest in contributing to unbaited! 6 | 7 | Please take a moment to review this document before submitting your first pull request. Please also check for open issues and pull requests to see if someone else is working on something similar. 8 | 9 | If you need any help, feel free to reach out to [daniel](https://x.com/nonzeroexitcode), or open an issue. 10 | 11 | ## Structure 12 | 13 | This repository consists of two main parts: 14 | 15 | ``` 16 | . 17 | ├── landing # The landing page website 18 | │ ├── src 19 | │ │ ├── app # Next.js application 20 | │ └── ... 21 | └── extension # The browser extension 22 | ├── src # Extension source code 23 | └── ... 24 | ``` 25 | 26 | ## Development 27 | 28 | ### Fork this repo 29 | 30 | You can fork this repo by clicking the fork button in the top right corner of this page. 31 | 32 | ### Clone on your local machine 33 | 34 | ```bash 35 | git clone https://github.com/your-username/unbaited.git 36 | ``` 37 | 38 | ### Navigate to project directory 39 | 40 | ```bash 41 | cd unbaited 42 | ``` 43 | 44 | ### Create a new Branch 45 | 46 | ```bash 47 | git checkout -b my-new-branch 48 | ``` 49 | 50 | ### Landing Page Development 51 | 52 | Navigate to the landing directory and install dependencies: 53 | 54 | ```bash 55 | cd landing 56 | npm install 57 | ``` 58 | 59 | Run the development server: 60 | 61 | ```bash 62 | npm run dev 63 | ``` 64 | 65 | ### Extension Development 66 | 67 | Navigate to the extension directory and install dependencies: 68 | 69 | ```bash 70 | cd extension 71 | npm install 72 | ``` 73 | 74 | Build the extension: 75 | 76 | ```bash 77 | npm run build 78 | ``` 79 | 80 | To load the extension in your browser: 81 | 82 | 1. Chrome/Edge: 83 | - Open `chrome://extensions` 84 | - Enable "Developer mode" 85 | - Click "Load unpacked" 86 | - Select the `extension/dist` directory 87 | 88 | 2. Firefox: 89 | - Open `about:debugging#/runtime/this-firefox` 90 | - Click "Load Temporary Add-on" 91 | - Select any file in the `extension/dist` directory 92 | 93 | ## Commit Convention 94 | 95 | Before you create a Pull Request, please check whether your commits comply with 96 | the commit conventions used in this repository. 97 | 98 | When you create a commit we kindly ask you to follow the convention 99 | `category(scope or module): message` in your commit message while using one of 100 | the following categories: 101 | 102 | - `feat / feature`: all changes that introduce completely new code or new 103 | features 104 | - `fix`: changes that fix a bug (ideally you will additionally reference an 105 | issue if present) 106 | - `refactor`: any code related change that is not a fix nor a feature 107 | - `docs`: changing existing or creating new documentation (i.e. README, docs for 108 | usage of a lib or cli usage) 109 | - `build`: all changes regarding the build of the software, changes to 110 | dependencies or the addition of new dependencies 111 | - `test`: all changes regarding tests (adding new tests or changing existing 112 | ones) 113 | - `ci`: all changes regarding the configuration of continuous integration (i.e. 114 | github actions, ci system) 115 | - `chore`: all changes to the repository that do not fit into any of the above 116 | categories 117 | 118 | e.g. `feat(extension): added firefox support` 119 | 120 | If you are interested in the detailed specification you can visit 121 | https://www.conventionalcommits.org/ or check out the 122 | [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines). 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daniel Petho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unbaited 2 | 3 | Control your X feed with an LLM of your choice from Groq. A browser extension that helps you filter out engagement bait and inflammatory content from your X (formerly Twitter) feed. 4 | 5 | ## How it works 6 | 7 | The extension uses Groq's ultra-fast API to analyze tweets using a model of your choice. When you scroll through X, it: 8 | 9 | 1. Detects new tweets as they appear in your viewport 10 | 2. Sends the tweet content (only text as of now) to Groq's API for analysis 11 | 3. Blurs tweets that are identified as engagement bait, political tweets, etc. 12 | 4. Gives you the option to reveal hidden tweets with a single click 13 | 14 | ## Installation 15 | 16 | ### Chrome/Safari 17 | 1. Install the extension from the [Chrome Web Store](https://chromewebstore.google.com/detail/unbaited-prototype/bpbnggihcaknipcgbpgjgfhgmdgcokcg) 18 | 2. Get your API key from [Groq](https://console.groq.com) 19 | 3. Open the extension settings and enter your API key 20 | 4. Optionally customize the system prompt to adjust how tweets are analyzed 21 | 22 | ### Firefox 23 | 1. Install the extension from [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/unbaited-prototype/) 24 | 2. Get your API key from [Groq](https://console.groq.com) 25 | 3. Open the extension settings and enter your API key 26 | 4. Optionally customize the system prompt to adjust how tweets are analyzed 27 | 28 | ## Browser Support 29 | 30 | - Chrome: ✅ Full support 31 | - Firefox: ✅ Full support 32 | - Safari: ✅ Full support 33 | 34 | ## Development 35 | 36 | The project consists of two parts: 37 | - `extension/`: The Chrome extension 38 | - `landing/`: The landing page built with Next.js 39 | 40 | ### Extension Development 41 | 42 | ```bash 43 | cd extension 44 | npm i 45 | npm build 46 | ``` 47 | 48 | #### Loading in browsers 49 | 50 | ##### Chrome/Safari 51 | Load the `extension/dist` directory as an unpacked extension: 52 | 1. Open Chrome/Safari 53 | 2. Go to Extensions page 54 | 3. Enable Developer Mode 55 | 4. Click "Load unpacked" and select the `extension/dist` directory 56 | 57 | ##### Firefox 58 | Load the extension temporarily: 59 | 1. Open Firefox 60 | 2. Go to `about:debugging` 61 | 3. Click "This Firefox" 62 | 4. Click "Load Temporary Add-on" 63 | 5. Select any file in the `extension/dist` directory 64 | 65 | ### Implementation Notes 66 | 67 | The extension uses different approaches for background processing: 68 | - Chrome/Safari: Uses Service Workers (MV3) 69 | - Firefox: Uses Background Scripts (MV3 with scripts fallback) 70 | 71 | This is handled automatically in the code, but when testing make sure to verify functionality in both environments. 72 | -------------------------------------------------------------------------------- /extension/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /extension/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /extension/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.cjs", 8 | "css": "src/index.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | unbaited 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unbaited", 3 | "private": true, 4 | "version": "0.0.5", 5 | "type": "module", 6 | "author": { 7 | "name": "daniel petho", 8 | "email": "hello@danielpetho.com", 9 | "url": "https://danielpetho.com" 10 | }, 11 | "scripts": { 12 | "dev": "vite", 13 | "build": "tsc && vite build", 14 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 15 | "preview": "vite preview" 16 | }, 17 | "dependencies": { 18 | "@hookform/resolvers": "^3.9.0", 19 | "@radix-ui/react-dialog": "^1.1.2", 20 | "@radix-ui/react-dropdown-menu": "^2.1.4", 21 | "@radix-ui/react-label": "^2.1.0", 22 | "@radix-ui/react-popover": "^1.1.2", 23 | "@radix-ui/react-radio-group": "^1.2.2", 24 | "@radix-ui/react-slot": "^1.1.0", 25 | "@radix-ui/react-switch": "^1.1.2", 26 | "class-variance-authority": "^0.7.0", 27 | "clsx": "^2.1.1", 28 | "cmdk": "1.0.0", 29 | "extension-cli": "^1.2.4", 30 | "groq-sdk": "^0.5.0", 31 | "lucide-react": "^0.424.0", 32 | "marked": "^13.0.2", 33 | "openai": "^4.55.0", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-hook-form": "^7.52.2", 37 | "tailwind-merge": "^2.4.0", 38 | "tailwindcss-animate": "^1.0.7", 39 | "zod": "^3.23.8", 40 | "zustand": "^5.0.1" 41 | }, 42 | "devDependencies": { 43 | "@tailwindcss/typography": "^0.5.16", 44 | "@types/chrome": "^0.0.268", 45 | "@types/node": "^22.1.0", 46 | "@types/react": "^18.2.45", 47 | "@types/react-dom": "^18.2.18", 48 | "@typescript-eslint/eslint-plugin": "^6.14.0", 49 | "@typescript-eslint/parser": "^6.14.0", 50 | "@vitejs/plugin-react": "^4.2.1", 51 | "autoprefixer": "^10.4.20", 52 | "eslint": "^8.55.0", 53 | "eslint-plugin-react-hooks": "^4.6.0", 54 | "eslint-plugin-react-refresh": "^0.4.5", 55 | "postcss": "^8.4.41", 56 | "tailwindcss": "^3.4.7", 57 | "typescript": "^5.2.2", 58 | "vite": "^5.0.8", 59 | "vite-plugin-static-copy": "^1.0.6" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /extension/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /extension/public/logos/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpetho/unbaited/07438d74a4947959c85f04d8d1b75f4e4751084f/extension/public/logos/logo128.png -------------------------------------------------------------------------------- /extension/public/logos/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpetho/unbaited/07438d74a4947959c85f04d8d1b75f4e4751084f/extension/public/logos/logo16.png -------------------------------------------------------------------------------- /extension/public/logos/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielpetho/unbaited/07438d74a4947959c85f04d8d1b75f4e4751084f/extension/public/logos/logo48.png -------------------------------------------------------------------------------- /extension/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unbaited (prototype)", 3 | "description": "Control your X feed with an LLM", 4 | "version": "0.0.5", 5 | "manifest_version": 3, 6 | "icons": { 7 | "16": "logos/logo16.png", 8 | "48": "logos/logo48.png", 9 | "128": "logos/logo128.png" 10 | }, 11 | "action": { 12 | "default_popup": "index.html", 13 | "default_icon": { 14 | "16": "logos/logo16.png", 15 | "48": "logos/logo48.png", 16 | "128": "logos/logo128.png" 17 | } 18 | }, 19 | "options_page": "settings.html", 20 | "permissions": ["storage"], 21 | "host_permissions": [ 22 | "https://*.twitter.com/*", 23 | "https://*.x.com/*", 24 | "https://api.groq.com/*" 25 | ], 26 | "homepage_url": "https://unbaited.danielpetho.com", 27 | "background": { 28 | "service_worker": "serviceWorker.js", 29 | "scripts": ["serviceWorker.js"] 30 | }, 31 | "content_scripts": [ 32 | { 33 | "matches": ["*://*.x.com/*", "*://x.com/*"], 34 | "js": ["contentScript.js"] 35 | } 36 | ], 37 | "web_accessible_resources": [ 38 | { 39 | "resources": [ 40 | "icons/*", 41 | "fonts/*", 42 | "font.css", 43 | "index.css", 44 | "locale.json" 45 | ], 46 | "matches": ["*://*.x.com/*", "*://x.com/*"] 47 | } 48 | ], 49 | "browser_specific_settings": { 50 | "gecko": { 51 | "id": "unbaited@danielpetho.com", 52 | "strict_min_version": "58.0" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /extension/settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Settings 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /extension/src/components/pages/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Switch } from "@/components/ui/switch"; 3 | import React, { useEffect, useState } from 'react'; 4 | 5 | function Popup() { 6 | const [hasApiKey, setHasApiKey] = useState(null); 7 | const [isEnabled, setIsEnabled] = useState(true); 8 | 9 | useEffect(() => { 10 | // Load both API key and enabled state 11 | chrome.storage.sync.get(['groqApiKey', 'isEnabled'], (result) => { 12 | setHasApiKey(!!result.groqApiKey); 13 | // Default to true if not set 14 | setIsEnabled(result.isEnabled ?? true); 15 | }); 16 | }, []); 17 | 18 | const toggleExtension = async (checked: boolean) => { 19 | setIsEnabled(checked); 20 | await chrome.storage.sync.set({ isEnabled: checked }); 21 | 22 | // Notify content script of the change 23 | const tabs = await chrome.tabs.query({ active: true, currentWindow: true }); 24 | tabs.forEach(tab => { 25 | if (tab.id) { 26 | chrome.tabs.sendMessage(tab.id, { 27 | action: "toggleExtension", 28 | isEnabled: checked 29 | }); 30 | } 31 | }); 32 | }; 33 | 34 | const openSettings = () => { 35 | chrome.runtime.openOptionsPage(); 36 | }; 37 | 38 | return ( 39 |
40 |
41 |
42 | Unbaited Logo 47 |

48 | unbaited 49 |

50 |
51 |
52 | 53 | {isEnabled ? 'on' : 'off'} 54 | 55 | 60 |
61 |
62 | 63 |

64 | Control your feed with LLMs on X 65 |

66 | 67 | {hasApiKey === false && ( 68 |
69 |

70 | ⚠️ API key not set 71 |

72 |

73 | Please set your Groq API key in the settings to use this extension. 74 |

75 |
76 | )} 77 | 78 |
79 |

80 | To use this extension, you need to: 81 |

82 |
    83 |
  • Set up your API keys
  • 84 |
  • Customize system prompts (optional)
  • 85 |
86 |
87 | 88 | 95 |
96 | ); 97 | } 98 | 99 | export default Popup; 100 | -------------------------------------------------------------------------------- /extension/src/components/pages/settings/api-keys.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function ApiKeys() { 4 | const [groqKey, setGroqKey] = useState(''); 5 | const [isSaved, setIsSaved] = useState(false); 6 | 7 | useEffect(() => { 8 | // Load saved API key when component mounts 9 | chrome.storage.sync.get(['groqApiKey'], (result) => { 10 | if (result.groqApiKey) { 11 | setGroqKey(result.groqApiKey); 12 | } 13 | }); 14 | }, []); 15 | 16 | const handleSave = async () => { 17 | await chrome.storage.sync.set({ groqApiKey: groqKey }); 18 | setIsSaved(true); 19 | setTimeout(() => setIsSaved(false), 2000); 20 | }; 21 | 22 | const handleClear = () => { 23 | setGroqKey(''); 24 | chrome.storage.local.remove('groqApiKey'); 25 | setIsSaved(true); 26 | setTimeout(() => setIsSaved(false), 2000); 27 | }; 28 | 29 | return ( 30 |
31 |
32 |

API Keys

33 |
34 | 40 | 46 |
47 |
48 | 49 | {isSaved && ( 50 |
51 | ✓ Changes saved successfully 52 |
53 | )} 54 | 55 |
56 |
57 | 60 | setGroqKey(e.target.value)} 65 | className="w-full p-2 border rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-black" 66 | placeholder="Enter your Groq API key" 67 | /> 68 |
69 |
70 | 71 |
72 |

You can find your API key in the Groq Console:

73 | 79 | https://console.groq.com/keys 80 | 81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /extension/src/components/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { ApiKeys } from './api-keys'; 2 | import { PromptsSettings } from './prompts'; 3 | import { useState, useEffect } from 'react'; 4 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" 5 | import { Label } from "@/components/ui/label" 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuLabel, 11 | DropdownMenuSeparator, 12 | DropdownMenuTrigger, 13 | } from "@/components/ui/dropdown-menu" 14 | 15 | const models = [ 16 | { id: 'gemma2-9b-it', name: 'Gemma 2 9B', provider: 'Google' }, 17 | { id: 'llama-3.3-70b-versatile', name: 'Llama 3.3 70B Versatile', provider: 'Meta' }, 18 | { id: 'llama-3.1-8b-instant', name: 'Llama 3.1 8B Instant', provider: 'Meta' }, 19 | { id: 'llama-guard-3-8b', name: 'Llama Guard 3 8B', provider: 'Meta' }, 20 | { id: 'llama3-70b-8192', name: 'Llama 3 70B', provider: 'Meta' }, 21 | { id: 'llama3-8b-8192', name: 'Llama 3 8B', provider: 'Meta' }, 22 | ]; 23 | 24 | export default function Settings() { 25 | const [displayMode, setDisplayMode] = useState<'blur' | 'hide'>('blur'); 26 | const [selectedModel, setSelectedModel] = useState(models[0].id); 27 | 28 | useEffect(() => { 29 | // Load saved settings 30 | chrome.storage.sync.get(['displayMode', 'selectedModel'], (result) => { 31 | if (result.displayMode) { 32 | setDisplayMode(result.displayMode); 33 | } 34 | if (result.selectedModel) { 35 | setSelectedModel(result.selectedModel); 36 | } 37 | }); 38 | }, []); 39 | 40 | const handleDisplayModeChange = (value: string) => { 41 | const mode = value as 'blur' | 'hide'; 42 | setDisplayMode(mode); 43 | chrome.storage.sync.set({ displayMode: mode }); 44 | }; 45 | 46 | const handleModelChange = (modelId: string) => { 47 | setSelectedModel(modelId); 48 | chrome.storage.sync.set({ selectedModel: modelId }); 49 | }; 50 | 51 | const selectedModelName = models.find(m => m.id === selectedModel)?.id || 'Select Model'; 52 | 53 | return ( 54 |
55 |

Settings

56 | 57 |
58 | 59 |
60 | 61 |
62 |

Model Selection

63 |
64 | 65 | 66 | 67 | {selectedModelName} 68 | 69 | 70 | Models 71 | 72 | {models.map((model) => ( 73 | handleModelChange(model.id)} 76 | className="flex justify-between font-mono" 77 | > 78 | {model.id} 79 | {model.provider} 80 | 81 | ))} 82 | 83 | 84 |
85 |
86 | 87 |
88 |

Display Settings

89 | 94 |
95 | 96 | 97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 | 105 |
106 | 107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /extension/src/components/pages/settings/prompts.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Label } from '@/components/ui/label'; 3 | import { 4 | DEFAULT_CRITERIA, 5 | SYSTEM_PROMPT_PREFIX, 6 | SYSTEM_PROMPT_SUFFIX 7 | } from '@/lib/constants'; 8 | 9 | export function PromptsSettings() { 10 | const [criteria, setCriteria] = useState(DEFAULT_CRITERIA); 11 | const [isDefault, setIsDefault] = useState(true); 12 | 13 | useEffect(() => { 14 | chrome.storage.sync.get(['promptCriteria'], (result) => { 15 | if (result.promptCriteria) { 16 | setCriteria(result.promptCriteria); 17 | setIsDefault(false); 18 | } 19 | }); 20 | }, []); 21 | 22 | const handleCriteriaChange = (value: string) => { 23 | setCriteria(value); 24 | setIsDefault(value === DEFAULT_CRITERIA); 25 | chrome.storage.sync.set({ promptCriteria: value }); 26 | }; 27 | 28 | const resetToDefault = () => { 29 | handleCriteriaChange(DEFAULT_CRITERIA); 30 | }; 31 | 32 | return ( 33 |
34 |
35 |

Prompt Settings

36 | {!isDefault && ( 37 | 43 | )} 44 |
45 | 46 |
47 |
48 |
49 | 50 | (fixed) 51 |
52 |
53 | {SYSTEM_PROMPT_PREFIX} 54 |
55 |
56 | 57 |
58 |
59 | 60 | (editable) 61 |
62 |