├── .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 |
151 | {submitMessage ? (
152 |
166 | {submitMessage.state === 'error' ? (
167 |
{submitMessage.message}
168 | ) : (
169 |
{submitMessage.message}
170 | )}
171 |
172 | ) : (
173 | ''
174 | )}
175 |
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 | * } suffix={}>
69 | * Click Me
70 | *
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 |
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 |
453 | )}
454 |
455 |
456 | ) : (
457 |
458 |
459 | No messages yet.
460 |
461 |
462 | )}
463 |
464 |
465 |
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 |
--------------------------------------------------------------------------------