├── .github └── workflows │ └── code-test.yaml ├── .gitignore ├── .prettierrc ├── README.md ├── components.json ├── index.html ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon32.png │ └── icon48.png └── vite.svg ├── src ├── App.tsx ├── assets │ ├── bot.png │ ├── leetcode.png │ ├── leetcode.webp │ ├── react.svg │ └── user.webp ├── background.js ├── components │ ├── Show.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ └── spinner.tsx ├── constants │ ├── prompt.ts │ └── valid_modals.ts ├── content.tsx ├── content │ ├── content.tsx │ └── util.ts ├── hooks │ ├── useChromeStorage.ts │ └── useIndexDB.tsx ├── index.css ├── interface │ ├── ModalInterface.ts │ └── chatHistory.ts ├── lib │ ├── indexedDB.ts │ └── utils.ts ├── main.tsx ├── modals │ ├── BaseModal.ts │ ├── index.ts │ ├── modal │ │ ├── GeminiAI_1_5_pro.ts │ │ ├── OpenAI_3_5_turbo.ts │ │ └── OpenAI_40.ts │ └── utils.ts ├── providers │ └── theme.tsx ├── schema │ └── modeOutput.ts ├── services │ └── ModalService.ts └── vite-env.d.ts ├── tailwind.config.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.github/workflows/code-test.yaml: -------------------------------------------------------------------------------- 1 | name: Test and Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | test-and-build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '22.11.0' 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v2 22 | with: 23 | version: 9.12.3 24 | 25 | - name: Cache dependencies 26 | uses: actions/cache@v3 27 | id: cache 28 | with: 29 | path: ~/.pnpm-store 30 | key: ${{ runner.os }}-pnpm-${{ hashFiles('package.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-pnpm- 33 | 34 | - name: Install dependencies 35 | if: steps.cache.outputs.cache-hit != 'true' 36 | run: pnpm install 37 | 38 | - name: Build 39 | run: pnpm run build 40 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "LeetCode Whisper", 4 | "version": "1.0.0", 5 | "description": "Chrome extension providing AI-driven hints on LeetCode problems. Get step-by-step help to boost problem-solving skills effectively.", 6 | "permissions": ["storage"], 7 | "action": { 8 | "default_popup": "index.html" 9 | }, 10 | "icons": { 11 | "16": "icons/icon16.png", 12 | "32": "icons/icon32.png", 13 | "48": "icons/icon48.png", 14 | "128": "icons/icon128.png" 15 | }, 16 | 17 | "content_scripts": [ 18 | { 19 | "js": ["src/content.tsx"], 20 | "matches": ["https://leetcode.com/problems/*"] 21 | } 22 | ], 23 | "background": { 24 | "service_worker": "src/background.js" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chrome-extension", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "prettier": "prettier . --write" 12 | }, 13 | "dependencies": { 14 | "@ai-sdk/google": "^0.0.55", 15 | "@ai-sdk/openai": "^0.0.72", 16 | "@crxjs/vite-plugin": "^2.0.0-beta.28", 17 | "@radix-ui/react-accordion": "^1.2.1", 18 | "@radix-ui/react-dropdown-menu": "^2.1.2", 19 | "@radix-ui/react-icons": "^1.3.2", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-scroll-area": "^1.2.1", 22 | "@radix-ui/react-select": "^2.1.2", 23 | "@radix-ui/react-slot": "^1.1.0", 24 | "ai": "^3.4.33", 25 | "autoprefixer": "^10.4.20", 26 | "chrome-ai": "^1.11.1", 27 | "chrome-types": "^0.1.319", 28 | "class-variance-authority": "^0.7.0", 29 | "framer-motion": "^11.11.17", 30 | "idb": "^8.0.0", 31 | "lucide-react": "^0.456.0", 32 | "next-themes": "^0.3.0", 33 | "openai": "^4.72.0", 34 | "prism-react-renderer": "^2.4.0", 35 | "react": "^18.3.1", 36 | "react-dom": "^18.3.1", 37 | "tailwind-merge": "^2.5.4", 38 | "tailwindcss": "^3.4.15", 39 | "tailwindcss-animate": "^1.0.7", 40 | "zod": "^3.23.8" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^22.9.0", 44 | "@types/react": "^18.3.12", 45 | "@types/react-dom": "^18.3.1", 46 | "@vitejs/plugin-react": "^4.3.3", 47 | "eslint": "^9.13.0", 48 | "eslint-plugin-react-hooks": "^5.0.0", 49 | "eslint-plugin-react-refresh": "^0.4.14", 50 | "prettier": "^3.3.3", 51 | "vite": "^5.4.10" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/public/icons/icon128.png -------------------------------------------------------------------------------- /public/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/public/icons/icon16.png -------------------------------------------------------------------------------- /public/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/public/icons/icon32.png -------------------------------------------------------------------------------- /public/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/public/icons/icon48.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import leetCode from '@/assets/leetcode.png' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import Show from '@/components/Show' 7 | import { 8 | Select, 9 | SelectContent, 10 | SelectGroup, 11 | SelectLabel, 12 | SelectItem, 13 | SelectSeparator, 14 | SelectValue, 15 | SelectTrigger, 16 | } from '@/components/ui/select' 17 | import { VALID_MODELS, type ValidModel } from './constants/valid_modals' 18 | import { HideApiKey } from '@/components/ui/input' 19 | import { useChromeStorage } from './hooks/useChromeStorage' 20 | 21 | const Popup: React.FC = () => { 22 | const [apikey, setApikey] = React.useState(null) 23 | const [model, setModel] = React.useState(null) 24 | const [isLoaded, setIsLoaded] = React.useState(false) 25 | 26 | const [isloading, setIsloading] = useState(false) 27 | const [submitMessage, setSubmitMessage] = useState<{ 28 | state: 'error' | 'success' 29 | message: string 30 | } | null>(null) 31 | 32 | const [selectedModel, setSelectedModel] = useState() 33 | 34 | const updatestorage = async (e: React.FormEvent) => { 35 | e.preventDefault() 36 | try { 37 | setIsloading(true) 38 | 39 | const { setKeyModel } = useChromeStorage() 40 | if (apikey && model) { 41 | await setKeyModel(apikey, model) 42 | } 43 | 44 | setSubmitMessage({ 45 | state: 'success', 46 | message: 'API Key saved successfully', 47 | }) 48 | } catch (error: any) { 49 | setSubmitMessage({ 50 | state: 'error', 51 | message: error.message, 52 | }) 53 | } finally { 54 | setIsloading(false) 55 | } 56 | } 57 | 58 | React.useEffect(() => { 59 | const loadChromeStorage = async () => { 60 | if (!chrome) return 61 | 62 | const { selectModel, getKeyModel } = useChromeStorage() 63 | 64 | setModel(await selectModel()) 65 | setSelectedModel(await selectModel()) 66 | setApikey((await getKeyModel(await selectModel())).apiKey) 67 | 68 | setIsLoaded(true) 69 | } 70 | 71 | loadChromeStorage() 72 | }, []) 73 | 74 | const heandelModel = async (v: ValidModel) => { 75 | if (v) { 76 | const { setSelectModel, getKeyModel, selectModel } = useChromeStorage() 77 | setSelectModel(v) 78 | setModel(v) 79 | setSelectedModel(v) 80 | setApikey((await getKeyModel(await selectModel())).apiKey) 81 | } 82 | } 83 | 84 | return ( 85 |
86 | 87 |
88 |
89 | 95 |
96 |
97 |

98 | LeetCode Whisper 99 |

100 |

101 | Your Companion to Beat LeetCode! 102 |

103 |
104 |
updatestorage(e)} 106 | className="mt-10 flex flex-col gap-2 w-full" 107 | > 108 |
109 | 112 | 134 |
135 |
136 | 139 | setApikey(e.target.value)} 142 | placeholder="Enter OpenAI API Key" 143 | disabled={!model} 144 | required 145 | /> 146 |
147 | 150 |
151 | {submitMessage ? ( 152 |
166 | {submitMessage.state === 'error' ? ( 167 |

{submitMessage.message}

168 | ) : ( 169 |

{submitMessage.message}

170 | )} 171 |
172 | ) : ( 173 | '' 174 | )} 175 |
176 |

177 | Want more features?  178 | 183 | Request a feature! 184 | 185 |

186 |
187 |
188 |
189 |
190 | ) 191 | } 192 | 193 | export default Popup 194 | -------------------------------------------------------------------------------- /src/assets/bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/src/assets/bot.png -------------------------------------------------------------------------------- /src/assets/leetcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/src/assets/leetcode.png -------------------------------------------------------------------------------- /src/assets/leetcode.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/src/assets/leetcode.webp -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/user.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piyushgarg-dev/leetcode-whisper-chrome-extension/1ec1df5e1576dcfc638334652a65804613829860/src/assets/user.webp -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 2 | if (message.action === 'openPopup') { 3 | chrome.action.openPopup() // Opens the popup programmatically 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /src/components/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Spinner } from './ui/spinner' 2 | 3 | type Props = { 4 | show: boolean 5 | children: React.ReactNode 6 | } 7 | 8 | /** 9 | * A component that conditionally renders its children based on the `show` prop. 10 | * 11 | * @param {Props} props - The component properties. 12 | * @param {boolean} props.show - Whether to show the children. 13 | * @param {ReactNode} props.children - The child elements to render. 14 | * @returns {React.ReactNode} - The rendered component. 15 | */ 16 | const Show: React.FC = ({ show, children }: Props): React.ReactNode => { 17 | return show ? ( 18 | children 19 | ) : ( 20 |
21 | 22 |
23 | ) 24 | } 25 | export default Show 26 | -------------------------------------------------------------------------------- /src/components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as AccordionPrimitive from '@radix-ui/react-accordion' 3 | import { ChevronDown } from 'lucide-react' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const Accordion = AccordionPrimitive.Root 8 | 9 | const AccordionItem = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 18 | )) 19 | AccordionItem.displayName = 'AccordionItem' 20 | 21 | const AccordionTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, children, ...props }, ref) => ( 25 | 26 | svg]:rotate-180', 30 | className 31 | )} 32 | {...props} 33 | > 34 | {children} 35 | 36 | 37 | 38 | )) 39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 40 | 41 | const AccordionContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, children, ...props }, ref) => ( 45 | 50 |
{children}
51 |
52 | )) 53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 54 | 55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 56 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Slot } from '@radix-ui/react-slot' 4 | import { cva, type VariantProps } from 'class-variance-authority' 5 | import React from 'react' 6 | import { cn } from '@/lib/utils' 7 | import { Spinner } from './spinner' 8 | import { motion } from 'framer-motion' 9 | 10 | export const buttonVariants = cva( 11 | 'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/70 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', 12 | { 13 | variants: { 14 | variant: { 15 | default: 16 | 'bg-primary text-primary-foreground shadow-sm shadow-black/[0.04] hover:bg-primary/90', 17 | outline: 18 | 'border border-input bg-background shadow-sm shadow-black/[0.04] hover:bg-accent hover:text-accent-foreground', 19 | secondary: 20 | 'bg-secondary text-secondary-foreground shadow-sm shadow-black/[0.04] hover:bg-secondary/80', 21 | tertiary: 'hover:bg-accent hover:text-accent-foreground', 22 | link: 'text-primary underline-offset-4 hover:underline', 23 | error: 'bg-[#d93036] hover:bg-[#ff6166]', 24 | warning: 'bg-[#ff990a] text-primary-foreground hover:bg-[#d27504]', 25 | }, 26 | size: { 27 | default: 'h-9 px-4 py-2', 28 | small: 'h-8 rounded-lg px-3 text-xs', 29 | large: 'h-10 rounded-lg px-8', 30 | icon: 'h-9 w-9', 31 | }, 32 | }, 33 | defaultVariants: { 34 | variant: 'default', 35 | size: 'default', 36 | }, 37 | } 38 | ) 39 | 40 | export interface ButtonProps 41 | extends Omit< 42 | React.ButtonHTMLAttributes, 43 | 'prefix' | 'suffix' 44 | >, 45 | VariantProps { 46 | asChild?: boolean 47 | prefix?: React.ReactNode 48 | suffix?: React.ReactNode 49 | disabled?: boolean 50 | loading?: boolean 51 | } 52 | 53 | /** 54 | * A customizable button component with different variants and sizes. 55 | * 56 | * @param {string} className - Additional class names for the button. 57 | * @param {"default" | "secondary" | "tertiary" | "error" | "warning"} variant - Button style variant. 58 | * @param {"default" | "small" | "large" | "icon"} size - Button size variant. 59 | * @param {boolean} asChild - Render as child component. 60 | * @param {React.ReactNode} prefix - Element to render before the button text. 61 | * @param {React.ReactNode} suffix - Element to render after the button text. 62 | * @param {boolean} disabled - Disable the button. 63 | * @param {boolean} loading - Show loading spinner inside the button. 64 | * @param {React.Ref} ref - Forwarded ref. 65 | * 66 | * @example 67 | * ```tsx 68 | * 71 | * ``` 72 | */ 73 | export const Button = React.forwardRef( 74 | ( 75 | { 76 | className, 77 | variant = 'default', 78 | size = 'default', 79 | asChild = false, 80 | prefix, 81 | suffix, 82 | disabled = false, 83 | loading = false, 84 | ...props 85 | }, 86 | ref 87 | ) => { 88 | const Comp = asChild ? Slot : 'button' 89 | 90 | const buttonContent = ( 91 | 103 | {loading ? : null} 104 | {prefix ? ( 105 | 106 | {prefix} 107 | 108 | ) : null} 109 | {props.children} 110 | {suffix ? ( 111 | 112 | {suffix} 113 | 114 | ) : null} 115 | 116 | ) 117 | 118 | return ( 119 |
120 | {buttonContent} 121 |
122 | ) 123 | } 124 | ) 125 | 126 | Button.displayName = 'Button' 127 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = 'Card' 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = 'CardHeader' 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = 'CardTitle' 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = 'CardDescription' 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = 'CardContent' 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = 'CardFooter' 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 3 | import { Check, ChevronRight, Circle } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const DropdownMenu = DropdownMenuPrimitive.Root 8 | 9 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 10 | 11 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 12 | 13 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 14 | 15 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 16 | 17 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 18 | 19 | const DropdownMenuSubTrigger = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef & { 22 | inset?: boolean 23 | } 24 | >(({ className, inset, children, ...props }, ref) => ( 25 | 34 | {children} 35 | 36 | 37 | )) 38 | DropdownMenuSubTrigger.displayName = 39 | DropdownMenuPrimitive.SubTrigger.displayName 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )) 54 | DropdownMenuSubContent.displayName = 55 | DropdownMenuPrimitive.SubContent.displayName 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 72 | 73 | )) 74 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 75 | 76 | const DropdownMenuItem = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef & { 79 | inset?: boolean 80 | } 81 | >(({ className, inset, ...props }, ref) => ( 82 | svg]:size-4 [&>svg]:shrink-0", 86 | inset && "pl-8", 87 | className 88 | )} 89 | {...props} 90 | /> 91 | )) 92 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 93 | 94 | const DropdownMenuCheckboxItem = React.forwardRef< 95 | React.ElementRef, 96 | React.ComponentPropsWithoutRef 97 | >(({ className, children, checked, ...props }, ref) => ( 98 | 107 | 108 | 109 | 110 | 111 | 112 | {children} 113 | 114 | )) 115 | DropdownMenuCheckboxItem.displayName = 116 | DropdownMenuPrimitive.CheckboxItem.displayName 117 | 118 | const DropdownMenuRadioItem = React.forwardRef< 119 | React.ElementRef, 120 | React.ComponentPropsWithoutRef 121 | >(({ className, children, ...props }, ref) => ( 122 | 130 | 131 | 132 | 133 | 134 | 135 | {children} 136 | 137 | )) 138 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 139 | 140 | const DropdownMenuLabel = React.forwardRef< 141 | React.ElementRef, 142 | React.ComponentPropsWithoutRef & { 143 | inset?: boolean 144 | } 145 | >(({ className, inset, ...props }, ref) => ( 146 | 155 | )) 156 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 157 | 158 | const DropdownMenuSeparator = React.forwardRef< 159 | React.ElementRef, 160 | React.ComponentPropsWithoutRef 161 | >(({ className, ...props }, ref) => ( 162 | 167 | )) 168 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 169 | 170 | const DropdownMenuShortcut = ({ 171 | className, 172 | ...props 173 | }: React.HTMLAttributes) => { 174 | return ( 175 | 179 | ) 180 | } 181 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 182 | 183 | export { 184 | DropdownMenu, 185 | DropdownMenuTrigger, 186 | DropdownMenuContent, 187 | DropdownMenuItem, 188 | DropdownMenuCheckboxItem, 189 | DropdownMenuRadioItem, 190 | DropdownMenuLabel, 191 | DropdownMenuSeparator, 192 | DropdownMenuShortcut, 193 | DropdownMenuGroup, 194 | DropdownMenuPortal, 195 | DropdownMenuSub, 196 | DropdownMenuSubContent, 197 | DropdownMenuSubTrigger, 198 | DropdownMenuRadioGroup, 199 | } 200 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { cn } from '@/lib/utils' 5 | import { EyeClosedIcon, EyeOpenIcon } from '@radix-ui/react-icons' 6 | 7 | /** 8 | * Props for the Input component. 9 | */ 10 | export interface InputProps 11 | extends Omit< 12 | React.InputHTMLAttributes, 13 | 'children' | 'prefix' | 'suffix' 14 | > { 15 | /** 16 | * Additional class names to apply to the outer container. 17 | */ 18 | className?: string 19 | /** 20 | * Additional class names to apply to the input element. 21 | */ 22 | iclassName?: string 23 | /** 24 | * Node or string to render as prefix inside the input container. 25 | */ 26 | prefix?: React.ReactNode | string 27 | /** 28 | * Node or string to render as suffix inside the input container. 29 | */ 30 | suffix?: React.ReactNode | string 31 | /** 32 | * Flag to apply styling to the prefix. 33 | */ 34 | prefixStyling?: boolean 35 | /** 36 | * Label for the input element. 37 | */ 38 | label?: string 39 | /** 40 | * Flag to apply styling to the suffix. 41 | */ 42 | suffixStyling?: boolean 43 | /** 44 | * error - Error message to display below the input. 45 | */ 46 | error?: string 47 | } 48 | /** 49 | * Input component with optional prefix and suffix. 50 | * 51 | * @param className - Additional class names for the container. 52 | * @param iclassName - Additional class names for the input element. 53 | * @param prefix - Element or string to render as prefix. 54 | * @param suffix - Element or string to render as suffix. 55 | * @param prefixStyling - Whether to apply styling to the prefix. 56 | * @param suffixStyling - Whether to apply styling to the suffix. 57 | * @param label - Label for the input element. 58 | * @param type - The type of the input element. 59 | * @param error - Error message to display below the input. 60 | * @param props - Other props to be applied to the input element. 61 | * @param ref - Ref to the input element. 62 | */ 63 | const Input = React.forwardRef( 64 | ( 65 | { 66 | className, 67 | iclassName, 68 | prefix, 69 | suffix, 70 | prefixStyling = true, 71 | suffixStyling = true, 72 | label, 73 | type, 74 | error, 75 | ...props 76 | }, 77 | ref 78 | ) => { 79 | /** 80 | * Refs for the prefix and suffix elements. 81 | */ 82 | const prefixRef = React.useRef(null) 83 | const suffixRef = React.useRef(null) 84 | /** 85 | * State to store the width of the prefix and suffix elements. 86 | */ 87 | const [prefixWidth, setPrefixWidth] = React.useState(0) 88 | const [suffixWidth, setSuffixWidth] = React.useState(0) 89 | // Update the width of the prefix and suffix elements when they change 90 | React.useEffect(() => { 91 | if (prefixRef.current) { 92 | setPrefixWidth(prefixRef.current.offsetWidth) 93 | } 94 | if (suffixRef.current) { 95 | setSuffixWidth(suffixRef.current.offsetWidth) 96 | } 97 | }, [prefix, suffix]) 98 | 99 | return ( 100 |
101 | {label && ( 102 | 108 | )} 109 | {prefix && ( 110 |
117 | {prefix} 118 | {prefixStyling &&
} 119 |
120 | )} 121 | 135 | {suffix && ( 136 |
143 | {suffixStyling && ( 144 |
145 | )} 146 | {suffix} 147 |
148 | )} 149 | {error && ( 150 |
154 | 162 | 168 | 169 | 172 |
173 | )} 174 |
175 | ) 176 | } 177 | ) 178 | 179 | Input.displayName = 'Input' 180 | 181 | const HideApiKey = React.forwardRef< 182 | HTMLInputElement, 183 | Omit 184 | >(({ className, ...props }, ref) => { 185 | const [showPassword, setShowPassword] = React.useState(false) 186 | 187 | return ( 188 |
189 | 196 | 219 | 220 | {/* hides browsers password toggles */} 221 | 229 |
230 | ) 231 | }) 232 | HideApiKey.displayName = 'HideApiKey' 233 | 234 | export { Input, HideApiKey } 235 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as LabelPrimitive from '@radix-ui/react-label' 3 | import { cva, type VariantProps } from 'class-variance-authority' 4 | 5 | import { cn } from '@/lib/utils' 6 | 7 | const labelVariants = cva( 8 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70' 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )) 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )) 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName 45 | 46 | export { ScrollArea, ScrollBar } 47 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { 5 | CaretSortIcon, 6 | CheckIcon, 7 | ChevronDownIcon, 8 | ChevronUpIcon, 9 | } from '@radix-ui/react-icons' 10 | import * as SelectPrimitive from '@radix-ui/react-select' 11 | import { cn } from '@/lib/utils' 12 | import { motion } from 'framer-motion' 13 | 14 | const Select = SelectPrimitive.Root 15 | const SelectGroup = SelectPrimitive.Group 16 | const SelectValue = SelectPrimitive.Value 17 | 18 | export const selectAnimationVariants = { 19 | zoom: { 20 | initial: { opacity: 0, scale: 0.9 }, 21 | animate: { opacity: 1, scale: 1 }, 22 | exit: { opacity: 0, scale: 0.9 }, 23 | transition: { type: 'spring', stiffness: 600, damping: 25 }, 24 | }, 25 | scaleBounce: { 26 | initial: { opacity: 0, scale: 0.5 }, 27 | animate: { opacity: 1, scale: [1.2, 0.9, 1] }, 28 | exit: { opacity: 0, scale: 0.5 }, 29 | transition: { type: 'spring', stiffness: 600, damping: 20 }, 30 | }, 31 | fade: { 32 | initial: { opacity: 0 }, 33 | animate: { opacity: 1 }, 34 | exit: { opacity: 0 }, 35 | transition: { duration: 0.3 }, 36 | }, 37 | slideUp: { 38 | initial: { opacity: 0, y: 20 }, 39 | animate: { opacity: 1, y: 0 }, 40 | exit: { opacity: 0, y: 20 }, 41 | transition: { type: 'spring', stiffness: 500, damping: 20 }, 42 | }, 43 | slideDown: { 44 | initial: { opacity: 0, y: -20 }, 45 | animate: { opacity: 1, y: 0 }, 46 | exit: { opacity: 0, y: -20 }, 47 | transition: { type: 'spring', stiffness: 500, damping: 20 }, 48 | }, 49 | slideRight: { 50 | initial: { opacity: 0, x: -30 }, 51 | animate: { opacity: 1, x: 0 }, 52 | exit: { opacity: 0, x: -30 }, 53 | transition: { type: 'spring', stiffness: 400, damping: 20 }, 54 | }, 55 | slideLeft: { 56 | initial: { opacity: 0, x: 30 }, 57 | animate: { opacity: 1, x: 0 }, 58 | exit: { opacity: 0, x: 30 }, 59 | transition: { type: 'spring', stiffness: 400, damping: 20 }, 60 | }, 61 | flip: { 62 | initial: { opacity: 0, rotateY: 90 }, 63 | animate: { opacity: 1, rotateY: 0 }, 64 | exit: { opacity: 0, rotateY: 90 }, 65 | transition: { type: 'spring', stiffness: 500, damping: 30 }, 66 | }, 67 | rotate: { 68 | initial: { opacity: 0, rotate: -180 }, 69 | animate: { opacity: 1, rotate: 0 }, 70 | exit: { opacity: 0, rotate: -180 }, 71 | transition: { type: 'spring', stiffness: 500, damping: 25 }, 72 | }, 73 | } 74 | 75 | const SelectTrigger = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, ...props }, ref) => { 79 | return ( 80 | span]:line-clamp-1', 84 | className 85 | )} 86 | {...props} 87 | > 88 | {children} 89 | 90 | 91 | 92 | 93 | ) 94 | }) 95 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 96 | 97 | const SelectScrollUpButton = React.forwardRef< 98 | React.ElementRef, 99 | React.ComponentPropsWithoutRef 100 | >(({ className, ...props }, ref) => ( 101 | 109 | 110 | 111 | )) 112 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 113 | 114 | const SelectScrollDownButton = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, ...props }, ref) => ( 118 | 126 | 127 | 128 | )) 129 | SelectScrollDownButton.displayName = 130 | SelectPrimitive.ScrollDownButton.displayName 131 | 132 | // Animated SelectContent component 133 | const AnimatedSelectContent = React.forwardRef< 134 | React.ElementRef, 135 | React.ComponentPropsWithoutRef & { 136 | selectedVariant: keyof typeof selectAnimationVariants 137 | } 138 | >( 139 | ( 140 | { className, children, position = 'popper', selectedVariant, ...props }, 141 | ref 142 | ) => ( 143 | 144 | 156 | 163 | 164 | 171 | {children} 172 | 173 | 174 | 175 | 176 | 177 | ) 178 | ) 179 | AnimatedSelectContent.displayName = SelectPrimitive.Content.displayName 180 | 181 | // Non-animated SelectContent component 182 | const StaticSelectContent = React.forwardRef< 183 | React.ElementRef, 184 | React.ComponentPropsWithoutRef 185 | >(({ className, children, position = 'popper', ...props }, ref) => ( 186 | 187 | 198 | 199 | 206 | {children} 207 | 208 | 209 | 210 | 211 | )) 212 | StaticSelectContent.displayName = SelectPrimitive.Content.displayName 213 | 214 | const SelectContent = React.forwardRef< 215 | React.ElementRef, 216 | React.ComponentPropsWithoutRef & { 217 | variants?: keyof typeof selectAnimationVariants 218 | } 219 | >((props, ref) => { 220 | const selectedVariant = props.variants || 'zoom' 221 | 222 | return ( 223 | 228 | ) 229 | }) 230 | 231 | SelectContent.displayName = 'SelectContent' 232 | 233 | const SelectLabel = React.forwardRef< 234 | React.ElementRef, 235 | React.ComponentPropsWithoutRef 236 | >(({ className, ...props }, ref) => ( 237 | 242 | )) 243 | SelectLabel.displayName = SelectPrimitive.Label.displayName 244 | 245 | const SelectItem = React.forwardRef< 246 | React.ElementRef, 247 | React.ComponentPropsWithoutRef 248 | >(({ className, children, ...props }, ref) => ( 249 | 257 | 258 | 259 | 260 | 261 | 262 | {children} 263 | 264 | )) 265 | SelectItem.displayName = SelectPrimitive.Item.displayName 266 | 267 | const SelectSeparator = React.forwardRef< 268 | React.ElementRef, 269 | React.ComponentPropsWithoutRef 270 | >(({ className, ...props }, ref) => ( 271 | 276 | )) 277 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 278 | 279 | export { 280 | Select, 281 | SelectGroup, 282 | SelectValue, 283 | SelectTrigger, 284 | SelectContent, 285 | SelectLabel, 286 | SelectItem, 287 | SelectSeparator, 288 | SelectScrollUpButton, 289 | SelectScrollDownButton, 290 | } 291 | -------------------------------------------------------------------------------- /src/components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | type SpinnerProps = React.ComponentPropsWithoutRef<"div"> & { 5 | 6 | className?: string; 7 | 8 | size?: number; 9 | }; 10 | 11 | 12 | const Spinner = React.forwardRef( 13 | ({ className, size, ...props }, ref) => { 14 | 15 | const computeDelay = (i: number): string => `${-1.2 + i * 0.1}s`; 16 | 17 | 18 | const computeRotation = (i: number): string => `${i * 30}deg`; 19 | 20 | return ( 21 |
32 |
33 | {[...Array(12)].map((_, i) => ( 34 |
42 | ))} 43 |
44 |
45 | ); 46 | }, 47 | ); 48 | 49 | Spinner.displayName = "Spinner"; 50 | 51 | export { Spinner }; 52 | -------------------------------------------------------------------------------- /src/constants/prompt.ts: -------------------------------------------------------------------------------- 1 | export const SYSTEM_PROMPT = ` 2 | You are LeetCode Whisper, a friendly and conversational AI helper for students solving LeetCode problems. Your goal is to guide students step-by-step toward a solution without giving the full answer immediately. 3 | 4 | Input Context: 5 | 6 | Problem Statement: {{problem_statement}} 7 | User Code: {{user_code}} 8 | Programming Language: {{programming_language}} 9 | 10 | Your Tasks: 11 | 12 | Analyze User Code: 13 | 14 | - Spot mistakes or inefficiencies in {{user_code}}. 15 | - Start with small feedback and ask friendly follow-up questions, like where the user needs help. 16 | - Keep the conversation flowing naturally, like you're chatting with a friend. 😊 17 | 18 | Provide Hints: 19 | 20 | - Share concise, relevant hints based on {{problem_statement}}. 21 | - Let the user lead the conversation—give hints only when necessary. 22 | - Avoid overwhelming the user with too many hints at once. 23 | 24 | Suggest Code Snippets: 25 | 26 | - Share tiny, focused code snippets only when they’re needed to illustrate a point. 27 | 28 | Output Requirements: 29 | 30 | - Keep the feedback short, friendly, and easy to understand. 31 | - snippet should always be code only and is optional. 32 | - Do not say hey everytime 33 | - Keep making feedback more personal and short overrime. 34 | - Limit the words in feedback. Only give what is really required to the user as feedback. 35 | - Hints must be crisp, short and clear 36 | 37 | Tone & Style: 38 | 39 | - Be kind, supportive, and approachable. 40 | - Use emojis like 🌟, 🙌, or ✅ to make the conversation fun and engaging. 41 | - Avoid long, formal responses—be natural and conversational. 42 | 43 | ` 44 | -------------------------------------------------------------------------------- /src/constants/valid_modals.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List of valid models that can be used in the application. 3 | */ 4 | export const VALID_MODELS = [ 5 | { 6 | model: 'gpt-3.5-turbo', 7 | name: 'openai_3.5_turbo', 8 | display: 'GPT-3.5 Turbo', 9 | }, 10 | { 11 | model: 'gpt-4o', 12 | name: 'openai_4o', 13 | display: 'GPT-4 Optimized', 14 | }, 15 | { 16 | model: 'gemini-1.5-pro-latest', 17 | name: 'gemini_1.5_pro', 18 | display: 'Gemini 1.5 Pro (Latest)', 19 | }, 20 | ] 21 | 22 | /** 23 | * Type of valid models that can be used in the application. 24 | */ 25 | export type ValidModel = 'openai_3.5_turbo' | 'openai_4o' | 'gemini_1.5_pro' 26 | -------------------------------------------------------------------------------- /src/content.tsx: -------------------------------------------------------------------------------- 1 | import './index.css' 2 | 3 | import { createRoot } from 'react-dom/client' 4 | import { StrictMode } from 'react' 5 | import ContentPage from '@/content/content' 6 | 7 | const root = document.createElement('div') 8 | root.id = '__leetcode_ai_whisper_container' 9 | document.body.append(root) 10 | 11 | createRoot(root).render( 12 | 13 | 14 | 15 | ) 16 | -------------------------------------------------------------------------------- /src/content/content.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react' 2 | import { Button } from '@/components/ui/button' 3 | import { 4 | Bot, 5 | Copy, 6 | EllipsisVertical, 7 | Eraser, 8 | Send, 9 | Settings, 10 | } from 'lucide-react' 11 | import { Highlight, themes } from 'prism-react-renderer' 12 | import { Input } from '@/components/ui/input' 13 | import { SYSTEM_PROMPT } from '@/constants/prompt' 14 | import { extractCode } from './util' 15 | 16 | import { 17 | Accordion, 18 | AccordionContent, 19 | AccordionItem, 20 | AccordionTrigger, 21 | } from '@/components/ui/accordion' 22 | 23 | import { cn } from '@/lib/utils' 24 | import { Card, CardContent, CardFooter } from '@/components/ui/card' 25 | 26 | import { ModalService } from '@/services/ModalService' 27 | import { useChromeStorage } from '@/hooks/useChromeStorage' 28 | import { ChatHistory, parseChatHistory } from '@/interface/chatHistory' 29 | import { VALID_MODELS, ValidModel } from '@/constants/valid_modals' 30 | import { ScrollArea } from '@/components/ui/scroll-area' 31 | import { 32 | Select, 33 | SelectContent, 34 | SelectGroup, 35 | SelectItem, 36 | SelectLabel, 37 | SelectSeparator, 38 | SelectTrigger, 39 | SelectValue, 40 | } from '@/components/ui/select' 41 | import { LIMIT_VALUE } from '@/lib/indexedDB' 42 | import { useIndexDB } from '@/hooks/useIndexDB' 43 | import { 44 | DropdownMenu, 45 | DropdownMenuContent, 46 | DropdownMenuGroup, 47 | DropdownMenuItem, 48 | DropdownMenuLabel, 49 | DropdownMenuPortal, 50 | DropdownMenuRadioGroup, 51 | DropdownMenuRadioItem, 52 | DropdownMenuSeparator, 53 | DropdownMenuShortcut, 54 | DropdownMenuSub, 55 | DropdownMenuSubContent, 56 | DropdownMenuSubTrigger, 57 | DropdownMenuTrigger, 58 | } from '@/components/ui/dropdown-menu' 59 | 60 | interface ChatBoxProps { 61 | visible: boolean 62 | context: { 63 | problemStatement: string 64 | } 65 | model: ValidModel 66 | apikey: string 67 | heandelModel: (v: ValidModel) => void 68 | selectedModel: ValidModel | undefined 69 | } 70 | 71 | const ChatBox: React.FC = ({ 72 | context, 73 | visible, 74 | model, 75 | apikey, 76 | heandelModel, 77 | selectedModel, 78 | }) => { 79 | const [value, setValue] = React.useState('') 80 | const [chatHistory, setChatHistory] = React.useState([]) 81 | const [priviousChatHistory, setPreviousChatHistory] = React.useState< 82 | ChatHistory[] 83 | >([]) 84 | const [isResponseLoading, setIsResponseLoading] = 85 | React.useState(false) 86 | // const chatBoxRef = useRef(null) 87 | 88 | const scrollAreaRef = useRef(null) 89 | const lastMessageRef = useRef(null) 90 | 91 | const [offset, setOffset] = React.useState(0) 92 | const [totalMessages, setTotalMessages] = React.useState(0) 93 | const [isPriviousMsgLoading, setIsPriviousMsgLoading] = 94 | React.useState(false) 95 | const { fetchChatHistory, saveChatHistory } = useIndexDB() 96 | 97 | const getProblemName = () => { 98 | const url = window.location.href 99 | const match = /\/problems\/([^/]+)/.exec(url) 100 | return match ? match[1] : 'Unknown Problem' 101 | } 102 | 103 | const problemName = getProblemName() 104 | const inputFieldRef = useRef(null) 105 | 106 | useEffect(() => { 107 | if (lastMessageRef.current && !isPriviousMsgLoading) { 108 | lastMessageRef.current.scrollIntoView({ behavior: 'smooth' }) 109 | } 110 | setTimeout(() => { 111 | inputFieldRef.current?.focus() 112 | }, 0) 113 | }, [chatHistory, isResponseLoading, visible]) 114 | 115 | const heandelClearChat = async () => { 116 | const { clearChatHistory } = useIndexDB() 117 | await clearChatHistory(problemName) 118 | setChatHistory([]) 119 | setPreviousChatHistory([]) 120 | } 121 | 122 | /** 123 | * Handles the generation of an AI response. 124 | * 125 | * This function performs the following steps: 126 | * 1. Initializes a new instance of `ModalService`. 127 | * 2. Selects a modal using the provided model and API key. 128 | * 3. Determines the programming language from the UI. 129 | * 4. Extracts the user's current code from the document. 130 | * 5. Modifies the system prompt with the problem statement, programming language, and extracted code. 131 | * 6. Generates a response using the modified system prompt. 132 | * 7. Updates the chat history with the generated response or error message. 133 | * 8. Scrolls the chat box into view. 134 | * 135 | * @async 136 | * @function handleGenerateAIResponse 137 | * @returns {Promise} A promise that resolves when the AI response generation is complete. 138 | */ 139 | const handleGenerateAIResponse = async (): Promise => { 140 | const modalService = new ModalService() 141 | 142 | modalService.selectModal(model, apikey) 143 | 144 | let programmingLanguage = 'UNKNOWN' 145 | 146 | const changeLanguageButton = document.querySelector( 147 | 'button.rounded.items-center.whitespace-nowrap.inline-flex.bg-transparent.dark\\:bg-dark-transparent.text-text-secondary.group' 148 | ) 149 | if (changeLanguageButton) { 150 | if (changeLanguageButton.textContent) 151 | programmingLanguage = changeLanguageButton.textContent 152 | } 153 | const userCurrentCodeContainer = document.querySelectorAll('.view-line') 154 | 155 | const extractedCode = extractCode(userCurrentCodeContainer) 156 | 157 | const systemPromptModified = SYSTEM_PROMPT.replace( 158 | /{{problem_statement}}/gi, 159 | context.problemStatement 160 | ) 161 | .replace(/{{programming_language}}/g, programmingLanguage) 162 | .replace(/{{user_code}}/g, extractedCode) 163 | 164 | const PCH = parseChatHistory(chatHistory) 165 | 166 | const { error, success } = await modalService.generate({ 167 | prompt: `${value}`, 168 | systemPrompt: systemPromptModified, 169 | messages: PCH, 170 | extractedCode: extractedCode, 171 | }) 172 | 173 | if (error) { 174 | const errorMessage: ChatHistory = { 175 | role: 'assistant', 176 | content: error.message, 177 | } 178 | await saveChatHistory(problemName, [ 179 | ...priviousChatHistory, 180 | { role: 'user', content: value }, 181 | errorMessage, 182 | ]) 183 | setPreviousChatHistory((prev) => [...prev, errorMessage]) 184 | setChatHistory((prev) => { 185 | const updatedChatHistory: ChatHistory[] = [...prev, errorMessage] 186 | return updatedChatHistory 187 | }) 188 | lastMessageRef.current?.scrollIntoView({ behavior: 'smooth' }) 189 | } 190 | 191 | if (success) { 192 | const res: ChatHistory = { 193 | role: 'assistant', 194 | content: success, 195 | } 196 | await saveChatHistory(problemName, [ 197 | ...priviousChatHistory, 198 | { role: 'user', content: value }, 199 | res, 200 | ]) 201 | setPreviousChatHistory((prev) => [...prev, res]) 202 | setChatHistory((prev) => [...prev, res]) 203 | setValue('') 204 | lastMessageRef.current?.scrollIntoView({ behavior: 'smooth' }) 205 | } 206 | 207 | setIsResponseLoading(false) 208 | setTimeout(() => { 209 | inputFieldRef.current?.focus() 210 | }, 0) 211 | } 212 | 213 | const loadInitialChatHistory = async () => { 214 | const { totalMessageCount, chatHistory, allChatHistory } = 215 | await fetchChatHistory(problemName, LIMIT_VALUE, 0) 216 | setPreviousChatHistory(allChatHistory || []) 217 | 218 | setTotalMessages(totalMessageCount) 219 | setChatHistory(chatHistory) 220 | setOffset(LIMIT_VALUE) 221 | } 222 | 223 | useEffect(() => { 224 | loadInitialChatHistory() 225 | }, [problemName]) 226 | 227 | const loadMoreMessages = async () => { 228 | if (totalMessages < offset) { 229 | return 230 | } 231 | setIsPriviousMsgLoading(true) 232 | const { chatHistory: moreMessages } = await fetchChatHistory( 233 | problemName, 234 | LIMIT_VALUE, 235 | offset 236 | ) 237 | 238 | if (moreMessages.length > 0) { 239 | setChatHistory((prev) => [...moreMessages, ...prev]) // Correctly merge the new messages with the previous ones 240 | setOffset((prevOffset) => prevOffset + LIMIT_VALUE) 241 | } 242 | 243 | setTimeout(() => { 244 | setIsPriviousMsgLoading(false) 245 | }, 500) 246 | } 247 | 248 | const handleScroll = (e: React.UIEvent) => { 249 | const target = e.currentTarget 250 | if (target.scrollTop === 0) { 251 | console.log('Reached the top, loading more messages...') 252 | loadMoreMessages() 253 | } 254 | } 255 | 256 | const onSendMessage = async (value: string) => { 257 | setIsResponseLoading(true) 258 | const newMessage: ChatHistory = { role: 'user', content: value } 259 | 260 | setPreviousChatHistory((prev) => { 261 | return [...prev, newMessage] 262 | }) 263 | setChatHistory([...chatHistory, newMessage]) 264 | 265 | lastMessageRef.current?.scrollIntoView({ behavior: 'smooth' }) 266 | handleGenerateAIResponse() 267 | } 268 | 269 | if (!visible) return <> 270 | 271 | return ( 272 | 273 |
274 |
275 |
276 | 277 |
278 |
279 |

Need Help?

280 |
Always online
281 |
282 |
283 | 284 | 285 | 288 | 289 | 290 | 291 | {' '} 292 | { 293 | VALID_MODELS.find((model) => model.name === selectedModel) 294 | ?.display 295 | } 296 | 297 | 298 | 299 | 300 | 301 | Change Model 302 | 303 | 304 | 305 | heandelModel(v as ValidModel)} 308 | > 309 | {VALID_MODELS.map((modelOption) => ( 310 | 314 | {modelOption.display} 315 | 316 | ))} 317 | 318 | 319 | 320 | 321 | 322 | 323 | 326 | (e.currentTarget.style.backgroundColor = 327 | 'rgb(185 28 28 / 0.35)') 328 | } 329 | onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = '')} 330 | > 331 | Clear Chat 332 | 333 | 334 | 335 |
336 | 337 | {chatHistory.length > 0 ? ( 338 | 343 | {totalMessages > offset && ( 344 |
345 | 351 |
352 | )} 353 | {chatHistory.map((message, index) => ( 354 |
363 | <> 364 |

365 | {typeof message.content === 'string' 366 | ? message.content 367 | : message.content.feedback} 368 |

369 | 370 | {!(typeof message.content === 'string') && ( 371 | 372 | {message.content?.hints && 373 | message.content.hints.length > 0 && ( 374 | 375 | Hints 👀 376 | 377 |
    378 | {message.content?.hints?.map((e) => ( 379 |
  • {e}
  • 380 | ))} 381 |
382 |
383 |
384 | )} 385 | {message.content?.snippet && ( 386 | 387 | Code 🧑🏻‍💻 388 | 389 | 390 |
391 |
392 | { 394 | if (typeof message.content !== 'string') 395 | navigator.clipboard.writeText( 396 | `${message.content?.snippet}` 397 | ) 398 | }} 399 | className="absolute right-2 top-2 h-4 w-4" 400 | /> 401 | 409 | {({ 410 | className, 411 | style, 412 | tokens, 413 | getLineProps, 414 | getTokenProps, 415 | }) => ( 416 |
423 |                                       {tokens.map((line, i) => (
424 |                                         
428 | {line.map((token, key) => ( 429 | 433 | ))} 434 |
435 | ))} 436 |
437 | )} 438 |
439 |
440 |
441 |
442 |
443 | )} 444 |
445 | )} 446 | 447 |
448 | ))} 449 | {isResponseLoading && ( 450 |
451 |
452 |
453 | )} 454 |
455 | 456 | ) : ( 457 |
458 |

459 | No messages yet. 460 |

461 |
462 | )} 463 | 464 | 465 |
{ 467 | event.preventDefault() 468 | if (value.trim().length === 0) return 469 | onSendMessage(value) 470 | setValue('') 471 | }} 472 | className="flex w-full items-center space-x-2" 473 | > 474 | setValue(event.target.value)} 481 | disabled={isResponseLoading} 482 | required 483 | ref={inputFieldRef} 484 | /> 485 | 494 |
495 |
496 | 497 | ) 498 | } 499 | 500 | const ContentPage: React.FC = () => { 501 | const [chatboxExpanded, setChatboxExpanded] = React.useState(false) 502 | 503 | const metaDescriptionEl = document.querySelector('meta[name=description]') 504 | const problemStatement = metaDescriptionEl?.getAttribute('content') as string 505 | 506 | const [modal, setModal] = React.useState(null) 507 | const [apiKey, setApiKey] = React.useState(null) 508 | const [selectedModel, setSelectedModel] = React.useState() 509 | 510 | const ref = useRef(null) 511 | 512 | const handleDocumentClick = (e: MouseEvent) => { 513 | if ( 514 | ref.current && 515 | e.target instanceof Node && 516 | !ref.current.contains(e.target) 517 | ) { 518 | // if (chatboxExpanded) setChatboxExpanded(false) 519 | } 520 | } 521 | 522 | React.useEffect(() => { 523 | document.addEventListener('click', handleDocumentClick) 524 | return () => { 525 | document.removeEventListener('click', handleDocumentClick) 526 | } 527 | }, []) 528 | ;(async () => { 529 | const { getKeyModel, selectModel } = useChromeStorage() 530 | const { model, apiKey } = await getKeyModel(await selectModel()) 531 | 532 | setModal(model) 533 | setApiKey(apiKey) 534 | })() 535 | 536 | const heandelModel = (v: ValidModel) => { 537 | if (v) { 538 | const { setSelectModel } = useChromeStorage() 539 | setSelectModel(v) 540 | setSelectedModel(v) 541 | } 542 | } 543 | 544 | React.useEffect(() => { 545 | const loadChromeStorage = async () => { 546 | if (!chrome) return 547 | 548 | const { selectModel } = useChromeStorage() 549 | 550 | setSelectedModel(await selectModel()) 551 | } 552 | 553 | loadChromeStorage() 554 | }, []) 555 | 556 | return ( 557 |
566 | {!modal || !apiKey ? ( 567 | !chatboxExpanded ? null : ( 568 | <> 569 | 570 | 571 |
572 | {!selectedModel && ( 573 | <> 574 |

575 | Please configure the extension before using this 576 | feature. 577 |

578 | 585 | 586 | )} 587 | {selectedModel && ( 588 | <> 589 |

590 | We couldn't find any API key for selected model{' '} 591 | 592 | {selectedModel} 593 | 594 |

595 |

you can select another models

596 | 618 | 619 | )} 620 |
621 |
622 |
623 | 624 | ) 625 | ) : ( 626 | 634 | )} 635 |
636 | 642 |
643 |
644 | ) 645 | } 646 | 647 | export default ContentPage 648 | -------------------------------------------------------------------------------- /src/content/util.ts: -------------------------------------------------------------------------------- 1 | export function extractCode(htmlContent: NodeListOf) { 2 | // Extract the text content of each line with the 'view-line' class 3 | const code = Array.from(htmlContent) 4 | .map((line) => line.textContent || '') // Ensure textContent is not null 5 | .join('\n'); 6 | 7 | return code 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useChromeStorage.ts: -------------------------------------------------------------------------------- 1 | import { ValidModel } from '@/constants/valid_modals' 2 | 3 | export const useChromeStorage = () => { 4 | return { 5 | setKeyModel: async (apiKey: string, model: ValidModel) => { 6 | chrome.storage.local.set({ [model]: apiKey }) 7 | }, 8 | 9 | getKeyModel: async (model: ValidModel) => { 10 | const result = await chrome.storage.local.get(model) 11 | return { model: model, apiKey: result[model] } 12 | }, 13 | 14 | setSelectModel: async (model: ValidModel) => { 15 | await chrome.storage.local.set({ ['selectedModel']: model }) 16 | }, 17 | 18 | selectModel: async () => { 19 | const result = await chrome.storage.local.get('selectedModel') 20 | return result['selectedModel'] as ValidModel 21 | }, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/hooks/useIndexDB.tsx: -------------------------------------------------------------------------------- 1 | import { ChatHistory } from '@/interface/chatHistory' 2 | import { 3 | clearChatHistory, 4 | getChatHistory, 5 | saveChatHistory, 6 | } from '@/lib/indexedDB' 7 | 8 | export const useIndexDB = () => { 9 | return { 10 | saveChatHistory: async (problemName: string, history: ChatHistory[]) => { 11 | await saveChatHistory(problemName, history) 12 | }, 13 | 14 | fetchChatHistory: async ( 15 | problemName: string, 16 | limit: number, 17 | offset: number 18 | ) => { 19 | return await getChatHistory(problemName, limit, offset) 20 | }, 21 | 22 | clearChatHistory: async (problemName: string) => { 23 | await clearChatHistory(problemName) 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 98%; 8 | --foreground: 0 0% 3.9%; 9 | --muted: 0 0% 96.1%; 10 | --muted-foreground: 0 0% 45.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 0 0% 15.1%; 13 | --card: 0 0% 99.7%; 14 | --card-foreground: 0 0% 3.9%; 15 | --border: 0 0% 89.8%; 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | --secondary: 0 0% 96.1%; 19 | --secondary-foreground: 0 0% 9%; 20 | --accent: 0 0% 94.1%; 21 | --accent-foreground: 0 0% 9%; 22 | --destructive: 0 84.2% 60.2%; 23 | --destructive-foreground: 0 0% 98%; 24 | --border: 0 0% 89.8%; 25 | --input: 0 0% 89.8%; 26 | --ring: 0 0% 3.9%; 27 | --radius: 0.8rem; 28 | } 29 | 30 | .dark { 31 | --background: 0 0% 3.9%; 32 | --foreground: 0 0% 94%; 33 | --muted: 0 0% 12.9%; 34 | --muted-foreground: 0 0% 60.9%; 35 | --card: 0 0% 6%; 36 | --card-foreground: 0 0% 98%; 37 | --popover: 0 0% 7%; 38 | --popover-foreground: 0 0% 88%; 39 | --primary: 0 0% 98%; 40 | --primary-foreground: 0 0% 9%; 41 | --secondary: 0 0% 12.9%; 42 | --secondary-foreground: 0 0% 98%; 43 | --accent: 0 0% 12.9%; 44 | --accent-foreground: 0 0% 98%; 45 | --destructive: 0 62.8% 30.6%; 46 | --destructive-foreground: 0 0% 98%; 47 | --border: 0 0% 14.9%; 48 | --input: 0 0% 14.9%; 49 | --ring: 0 0% 83.1%; 50 | } 51 | } 52 | 53 | @layer base { 54 | * { 55 | @apply border-border; 56 | } 57 | body { 58 | @apply bg-background text-foreground; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/interface/ModalInterface.ts: -------------------------------------------------------------------------------- 1 | import { outputSchema } from '@/schema/modeOutput' 2 | import { z } from 'zod' 3 | import { ChatHistoryParsed } from './chatHistory' 4 | 5 | /** 6 | * Defines the contract for AI modal implementations. 7 | * 8 | * Each modal must have a unique `name` and provide methods for initialization 9 | * and response generation. 10 | */ 11 | export abstract class ModalInterface { 12 | /** 13 | * The unique name of the modal. 14 | */ 15 | abstract name: string 16 | 17 | /** 18 | * Initializes the modal with the provided API key. 19 | * 20 | * @param apiKey - The API key used to authenticate with the AI service. 21 | */ 22 | abstract init(apiKey?: string): void 23 | 24 | /** 25 | * Generates a response using the AI model. 26 | * 27 | * @param prompt - The main prompt provided by the user. 28 | * @param systemPrompt - A system-level instruction to guide the AI. 29 | * @param messages - A parsed history of the chat for context. 30 | * @param extractedCode - (Optional) A code snippet to assist the AI in its response. 31 | * 32 | * @returns A promise resolving to an object containing either: 33 | * - `error`: Any error encountered during the API call. 34 | * - `success`: The successful response data adhering to `outputSchema`. 35 | */ 36 | abstract generateResponse(props: GenerateResponseParamsType): Promise<{ 37 | error: Error | null 38 | success: z.infer | null 39 | }> 40 | } 41 | 42 | /** 43 | * Defines the contract for AI modal implementations. 44 | */ 45 | export type GenerateResponseReturnType = Promise<{ 46 | error: Error | null 47 | success: z.infer | null | any 48 | }> 49 | 50 | /** 51 | * Defines the parameters for generating a response. 52 | */ 53 | export type GenerateResponseParamsType = { 54 | prompt: string 55 | systemPrompt: string 56 | messages: ChatHistoryParsed[] | [] 57 | extractedCode?: string 58 | } 59 | -------------------------------------------------------------------------------- /src/interface/chatHistory.ts: -------------------------------------------------------------------------------- 1 | import { outputSchema } from '@/schema/modeOutput' 2 | import { z } from 'zod' 3 | 4 | export type Roles = 5 | | 'function' 6 | | 'system' 7 | | 'user' 8 | | 'assistant' 9 | | 'data' 10 | | 'tool' 11 | 12 | export interface ChatHistory { 13 | role: Roles 14 | content: string | z.infer 15 | } 16 | 17 | // parse ChatHistory to new interface where content if z.infer than make it string 18 | 19 | export interface ChatHistoryParsed { 20 | role: Roles 21 | content: string 22 | } 23 | 24 | export const parseChatHistory = ( 25 | chatHistory: ChatHistory[] 26 | ): ChatHistoryParsed[] => { 27 | return chatHistory.map((history) => { 28 | return { 29 | role: history.role, 30 | content: 31 | typeof history.content === 'string' 32 | ? history.content 33 | : JSON.stringify(history.content), 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/indexedDB.ts: -------------------------------------------------------------------------------- 1 | import { openDB, DBSchema } from 'idb' 2 | import { ChatHistory } from '@/interface/chatHistory' 3 | 4 | interface ChatDB extends DBSchema { 5 | chats: { 6 | key: string 7 | value: { problemName: string; chatHistory: ChatHistory[] } 8 | } 9 | } 10 | 11 | const dbPromise = openDB('chat-db', 1, { 12 | upgrade(db) { 13 | db.createObjectStore('chats', { keyPath: 'problemName' }) 14 | }, 15 | }) 16 | 17 | export const saveChatHistory = async ( 18 | problemName: string, 19 | history: ChatHistory[] 20 | ) => { 21 | const db = await dbPromise 22 | await db.put('chats', { problemName, chatHistory: history }) 23 | } 24 | 25 | export const getChatHistory = async ( 26 | problemName: string, 27 | limit: number, 28 | offset: number 29 | ) => { 30 | const db = await dbPromise 31 | const chatData = await db.get('chats', problemName) 32 | if (!chatData) return { totalMessageCount: 0, chatHistory: [] } 33 | 34 | const { chatHistory } = chatData 35 | const totalMessageCount = chatHistory.length 36 | 37 | // Fetch the slice of chat history based on limit and offset 38 | const slicedHistory = chatHistory.slice( 39 | Math.max(totalMessageCount - offset - limit, 0), 40 | totalMessageCount - offset 41 | ) 42 | return { 43 | totalMessageCount, 44 | chatHistory: slicedHistory, 45 | allChatHistory: chatHistory || [], 46 | } 47 | } 48 | 49 | export const clearChatHistory = async (problemName: string) => { 50 | const db = await dbPromise 51 | await db.delete('chats', problemName) 52 | } 53 | 54 | export const LIMIT_VALUE = 10 55 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ClassNameValue, twMerge } from "tailwind-merge"; 2 | 3 | export const cn: (...classLists: ClassNameValue[]) => string = twMerge; 4 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | import { ThemeProvider } from './providers/theme.tsx' 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | 13 | ) 14 | -------------------------------------------------------------------------------- /src/modals/BaseModal.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenerateResponseParamsType, 3 | GenerateResponseReturnType, 4 | ModalInterface, 5 | } from '../interface/ModalInterface' 6 | 7 | /** 8 | * Abstract base class for modals that interact with an API. 9 | * This class is the base class for all modals. 10 | * It implements the interface defined above. 11 | * It provides a base implementation for the `generateResponse` method. 12 | * It also defines an abstract method that must be implemented by all subclasses. 13 | * 14 | * @abstract 15 | * @extends {ModalInterface} 16 | */ 17 | export abstract class BaseModal extends ModalInterface { 18 | /** 19 | * The API key used for making API calls. 20 | * 21 | * @protected 22 | * @type {string} 23 | */ 24 | protected apiKey: string = '' 25 | 26 | /** 27 | * Initializes the modal with the provided API key. 28 | * 29 | * @param {string} apiKey - The API key to be used for API calls. 30 | */ 31 | init(apiKey: string) { 32 | this.apiKey = apiKey 33 | } 34 | 35 | /** 36 | * Makes an API call with the provided parameters. 37 | * 38 | * @protected 39 | * @abstract 40 | * @param {GenerateResponseParamsType} props - The parameters for the API call. 41 | * @returns {GenerateResponseReturnType} The response from the API call. 42 | */ 43 | protected abstract makeApiCall( 44 | props: GenerateResponseParamsType 45 | ): GenerateResponseReturnType 46 | 47 | /** 48 | * Generates a response by making an API call with the provided parameters. 49 | * 50 | * @async 51 | * @param {GenerateResponseParamsType} props - The parameters for the API call. 52 | * @returns {Promise} The response from the API call. 53 | */ 54 | async generateResponse( 55 | props: GenerateResponseParamsType 56 | ): GenerateResponseReturnType { 57 | return this.makeApiCall(props) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/modals/index.ts: -------------------------------------------------------------------------------- 1 | import { ModalInterface } from '@/interface/ModalInterface' 2 | import { ValidModel } from '@/constants/valid_modals' 3 | 4 | import { OpenAI_3_5_turbo } from '@/modals/modal/OpenAI_3_5_turbo' 5 | import { GeminiAI_1_5_pro } from '@/modals/modal/GeminiAI_1_5_pro' 6 | import { OpenAi_4o } from './modal/OpenAI_40' 7 | 8 | /** 9 | * This object contains all the modals that are available in the extension. 10 | * @type {Record} 11 | */ 12 | export const modals: Record = { 13 | 'openai_3.5_turbo': new OpenAI_3_5_turbo(), 14 | openai_4o: new OpenAi_4o(), 15 | 'gemini_1.5_pro': new GeminiAI_1_5_pro(), 16 | } 17 | -------------------------------------------------------------------------------- /src/modals/modal/GeminiAI_1_5_pro.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenerateResponseParamsType, 3 | GenerateResponseReturnType, 4 | ModalInterface, 5 | } from '../../interface/ModalInterface' 6 | import { createGoogleGenerativeAI } from '@ai-sdk/google' 7 | import { generateObjectResponce } from '../utils' 8 | import { VALID_MODELS } from '@/constants/valid_modals' 9 | 10 | export class GeminiAI_1_5_pro implements ModalInterface { 11 | name = 'gemini_1.5_pro' 12 | private apiKey: string = '' 13 | 14 | init(apiKey: string) { 15 | this.apiKey = apiKey 16 | } 17 | 18 | async generateResponse( 19 | props: GenerateResponseParamsType 20 | ): GenerateResponseReturnType { 21 | try { 22 | const google = createGoogleGenerativeAI({ 23 | apiKey: this.apiKey, 24 | }) 25 | 26 | let data = await generateObjectResponce({ 27 | model: google( 28 | VALID_MODELS.find((model) => model.name === this.name)?.model! 29 | ), 30 | messages: props.messages, 31 | systemPrompt: props.systemPrompt, 32 | prompt: props.prompt, 33 | extractedCode: props.extractedCode, 34 | }) 35 | 36 | return { 37 | error: null, 38 | success: data.object, 39 | } 40 | } catch (error: any) { 41 | return { error, success: null } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/modals/modal/OpenAI_3_5_turbo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenerateResponseParamsType, 3 | GenerateResponseReturnType, 4 | ModalInterface, 5 | } from '../../interface/ModalInterface' 6 | import { createOpenAI } from '@ai-sdk/openai' 7 | import { generateObjectResponce } from '../utils' 8 | import { VALID_MODELS } from '@/constants/valid_modals' 9 | 10 | export class OpenAI_3_5_turbo implements ModalInterface { 11 | name = 'openai_3.5_turbo' 12 | private apiKey: string = '' 13 | 14 | init(apiKey: string) { 15 | this.apiKey = apiKey 16 | } 17 | 18 | async generateResponse( 19 | props: GenerateResponseParamsType 20 | ): GenerateResponseReturnType { 21 | try { 22 | const openai = createOpenAI({ 23 | compatibility: 'strict', 24 | apiKey: this.apiKey, 25 | }) 26 | 27 | let data = await generateObjectResponce({ 28 | model: openai( 29 | VALID_MODELS.find((model) => model.name === this.name)?.model! 30 | ), 31 | messages: props.messages, 32 | systemPrompt: props.systemPrompt, 33 | prompt: props.prompt, 34 | extractedCode: props.extractedCode, 35 | }) 36 | 37 | return { 38 | error: null, 39 | success: data.object, 40 | } 41 | } catch (error: any) { 42 | return { error, success: null } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modals/modal/OpenAI_40.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GenerateResponseParamsType, 3 | GenerateResponseReturnType, 4 | ModalInterface, 5 | } from '../../interface/ModalInterface' 6 | import { createOpenAI } from '@ai-sdk/openai' 7 | import { generateObjectResponce } from '../utils' 8 | import { VALID_MODELS } from '@/constants/valid_modals' 9 | 10 | export class OpenAi_4o implements ModalInterface { 11 | name = 'openai_4o' 12 | private apiKey: string = '' 13 | 14 | init(apiKey: string) { 15 | this.apiKey = apiKey 16 | } 17 | 18 | async generateResponse( 19 | props: GenerateResponseParamsType 20 | ): GenerateResponseReturnType { 21 | try { 22 | const openai = createOpenAI({ 23 | compatibility: 'strict', 24 | apiKey: this.apiKey, 25 | }) 26 | 27 | let data = await generateObjectResponce({ 28 | model: openai( 29 | VALID_MODELS.find((model) => model.name === this.name)?.model! 30 | ), 31 | messages: props.messages, 32 | systemPrompt: props.systemPrompt, 33 | prompt: props.prompt, 34 | extractedCode: props.extractedCode, 35 | }) 36 | 37 | return { 38 | error: null, 39 | success: data.object, 40 | } 41 | } catch (error: any) { 42 | return { error, success: null } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/modals/utils.ts: -------------------------------------------------------------------------------- 1 | import { ChatHistoryParsed } from '@/interface/chatHistory' 2 | import { outputSchema } from '@/schema/modeOutput' 3 | import { generateObject, GenerateObjectResult, LanguageModelV1 } from 'ai' 4 | 5 | /** 6 | * Generates an object response based on the provided parameters. 7 | * 8 | * @param {Object} params - The parameters for generating the object response. 9 | * @param {ChatHistoryParsed[] | []} params.messages - The chat history messages. 10 | * @param {string} params.systemPrompt - The system prompt to use. 11 | * @param {string} params.prompt - The user prompt to use. 12 | * @param {string} [params.extractedCode] - Optional extracted code to include in the messages. 13 | * @param {LanguageModelV1} params.model - The language model to use. 14 | * @returns {Promise} A promise that resolves with the generated object response. 15 | */ 16 | export const generateObjectResponce = async ({ 17 | messages, 18 | systemPrompt, 19 | prompt, 20 | extractedCode, 21 | model, 22 | }: { 23 | messages: ChatHistoryParsed[] | [] 24 | systemPrompt: string 25 | prompt: string 26 | extractedCode?: string 27 | model: LanguageModelV1 28 | }): Promise< 29 | GenerateObjectResult<{ 30 | feedback: string 31 | hints?: string[] | undefined 32 | snippet?: string | undefined 33 | programmingLanguage?: string | undefined 34 | }> 35 | > => { 36 | const data = await generateObject({ 37 | model: model, 38 | schema: outputSchema, 39 | output: 'object', 40 | messages: [ 41 | { role: 'system', content: systemPrompt }, 42 | { 43 | role: 'system', 44 | content: `extractedCode (this code is writen by user): ${extractedCode}`, 45 | }, 46 | ...messages, 47 | { role: 'user', content: prompt }, 48 | ], 49 | }) 50 | 51 | return data 52 | } 53 | -------------------------------------------------------------------------------- /src/providers/theme.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { ThemeProvider as NextThemeProvider } from "next-themes"; 3 | import React from "react"; 4 | 5 | /** 6 | * Properties for the `ThemeProvider` component. 7 | * 8 | * @interface ThemeProviderProps 9 | * @property {ReactNode} children - The child elements to render within the theme provider. 10 | * @property {string} [attribute="class"] - The attribute to use for applying the theme (e.g., "class", "data-theme"). 11 | * @property {string} [defaultTheme="system"] - The default theme to apply if no theme is specified. 12 | * @property {boolean} [enableSystem=true] - Whether to enable system theme detection. 13 | * @property {boolean} [disableTransitionOnChange=true] - Whether to disable transitions when changing themes. 14 | */ 15 | export interface ThemeProviderProps { 16 | children: React.ReactNode; 17 | attribute?: string; 18 | defaultTheme?: string; 19 | enableSystem?: boolean; 20 | disableTransitionOnChange?: boolean; 21 | } 22 | 23 | 24 | /** 25 | * A wrapper component to provide theme context using `next-themes`. 26 | * 27 | * @param {ThemeProviderProps} props - The component properties. 28 | * @returns {React.ReactElement} - The theme provider component. 29 | */ 30 | export const ThemeProvider: React.FC = ({ 31 | children, 32 | attribute = "class", 33 | defaultTheme = "dark", 34 | enableSystem = true, 35 | disableTransitionOnChange = true, 36 | }) => { 37 | return ( 38 | 44 |
{children}
45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /src/schema/modeOutput.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | 3 | const SupportedLanguages = [ 4 | 'c', 5 | 'cpp', 6 | 'csharp', 7 | 'cs', 8 | 'dart', 9 | 'elixir', 10 | 'erlang', 11 | 'go', 12 | 'java', 13 | 'javascript', 14 | 'jsonp', 15 | 'jsx', 16 | 'php', 17 | 'python', 18 | 'racket', 19 | 'rkt', 20 | 'ruby', 21 | 'rb', 22 | 'rust', 23 | 'scala', 24 | 'sql', 25 | 'Swift', 26 | 'typescript', 27 | 'tsx', 28 | ] as const 29 | 30 | export const outputSchema = z.object({ 31 | feedback: z.string(), 32 | hints: z 33 | .array(z.string()) 34 | .max(2, 'You can only provide up to 2 hints.') 35 | .optional() 36 | .describe('max 2 hints'), 37 | snippet: z.string().optional().describe('code snippet should be in format.'), 38 | programmingLanguage: z 39 | .enum(SupportedLanguages) 40 | .optional() 41 | .describe('Programming language code as supports by prismjs'), 42 | }) 43 | -------------------------------------------------------------------------------- /src/services/ModalService.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod' 2 | import { ValidModel } from '@/constants/valid_modals' 3 | import { modals } from '@/modals' 4 | import { 5 | GenerateResponseParamsType, 6 | ModalInterface, 7 | } from '@/interface/ModalInterface' 8 | import { outputSchema } from '@/schema/modeOutput' 9 | 10 | /** 11 | * Service to manage and interact with modals. 12 | */ 13 | export class ModalService { 14 | /** 15 | * The currently active modal. 16 | * @private 17 | */ 18 | private activeModal: ModalInterface | null = null 19 | 20 | /** 21 | * Selects a modal by its name and initializes it with an optional API key. 22 | * @param modalName - The name of the modal to select. 23 | * @param apiKey - An optional API key to initialize the modal with. 24 | * @throws Will throw an error if the modal with the given name is not found. 25 | */ 26 | selectModal(modalName: ValidModel, apiKey?: string) { 27 | if (modals[modalName]) { 28 | this.activeModal = modals[modalName] 29 | this.activeModal.init(apiKey) 30 | } else { 31 | throw new Error(`Modal "${modalName}" not found`) 32 | } 33 | } 34 | 35 | /** 36 | * Generates a response using the currently active modal. 37 | * @param props - The parameters required to generate the response. 38 | * @returns A promise that resolves to an object containing either an error or the successful response. 39 | * @throws Will throw an error if no modal is selected. 40 | */ 41 | async generate(props: GenerateResponseParamsType): Promise< 42 | Promise<{ 43 | error: Error | null 44 | success: z.infer | null 45 | }> 46 | > { 47 | if (!this.activeModal) { 48 | throw new Error('No modal selected') 49 | } 50 | return this.activeModal.generateResponse(props) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | const colors = require('tailwindcss/colors') 3 | 4 | const config = { 5 | darkMode: ['class'], 6 | content: [ 7 | './pages/**/*.{ts,tsx}', 8 | './components/**/*.{ts,tsx}', 9 | './app/**/*.{ts,tsx}', 10 | './src/**/*.{ts,tsx}', 11 | ], 12 | prefix: '', 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: '2rem', 17 | screens: { 18 | '2xl': '1400px', 19 | }, 20 | }, 21 | extend: { 22 | colors: { 23 | border: 'hsl(var(--border))', 24 | input: 'hsl(var(--input))', 25 | ring: 'hsl(var(--ring))', 26 | background: 'hsl(var(--background))', 27 | foreground: 'hsl(var(--foreground))', 28 | primary: { 29 | DEFAULT: 'hsl(var(--primary))', 30 | foreground: 'hsl(var(--primary-foreground))', 31 | }, 32 | secondary: { 33 | DEFAULT: 'hsl(var(--secondary))', 34 | foreground: 'hsl(var(--secondary-foreground))', 35 | }, 36 | destructive: { 37 | DEFAULT: 'hsl(var(--destructive))', 38 | foreground: 'hsl(var(--destructive-foreground))', 39 | }, 40 | muted: { 41 | DEFAULT: 'hsl(var(--muted))', 42 | foreground: 'hsl(var(--muted-foreground))', 43 | }, 44 | accent: { 45 | DEFAULT: 'hsl(var(--accent))', 46 | foreground: 'hsl(var(--accent-foreground))', 47 | }, 48 | popover: { 49 | DEFAULT: 'hsl(var(--popover))', 50 | foreground: 'hsl(var(--popover-foreground))', 51 | }, 52 | card: { 53 | DEFAULT: 'hsl(var(--card))', 54 | foreground: 'hsl(var(--card-foreground))', 55 | }, 56 | ...colors, 57 | }, 58 | borderRadius: { 59 | lg: 'var(--radius)', 60 | md: 'calc(var(--radius) - 2px)', 61 | sm: 'calc(var(--radius) - 4px)', 62 | }, 63 | keyframes: { 64 | 'accordion-down': { 65 | from: { height: '0' }, 66 | to: { height: 'var(--radix-accordion-content-height)' }, 67 | }, 68 | 'accordion-up': { 69 | from: { height: 'var(--radix-accordion-content-height)' }, 70 | to: { height: '0' }, 71 | }, 72 | spinner: { 73 | from: { opacity: '1' }, 74 | to: { opacity: '0.15' }, 75 | }, 76 | }, 77 | animation: { 78 | 'accordion-down': 'accordion-down 0.2s ease-out', 79 | 'accordion-up': 'accordion-up 0.2s ease-out', 80 | spinner: 'spinner 1.2s linear infinite', 81 | }, 82 | }, 83 | }, 84 | plugins: [require('tailwindcss-animate')], 85 | } satisfies Config 86 | 87 | export default config 88 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | "types": ["chrome-types"], 10 | 11 | /* Bundler mode */ 12 | "moduleResolution": "Bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true, 25 | 26 | "baseUrl": ".", 27 | "paths": { 28 | "@/*": ["./src/*"] 29 | } 30 | }, 31 | "include": ["src"] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./src/*"] 6 | }, 7 | "types": ["chrome-types"] 8 | }, 9 | "files": [], 10 | "references": [ 11 | { "path": "./tsconfig.app.json" }, 12 | { "path": "./tsconfig.node.json" } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { crx } from '@crxjs/vite-plugin' 4 | import manifest from './manifest.json' 5 | import path from 'path' 6 | 7 | // https://vite.dev/config/ 8 | export default defineConfig({ 9 | plugins: [react(), crx({ manifest })], 10 | resolve: { 11 | alias: { 12 | '@': path.resolve(__dirname, './src'), 13 | }, 14 | }, 15 | }) 16 | --------------------------------------------------------------------------------