├── .gitignore ├── README.md ├── biome.json ├── components.json ├── dist ├── assets │ ├── popup.css │ └── popup.js ├── index.html └── powered-by-groq.png ├── icons ├── 128.png ├── 16.png ├── 32.png └── 48.png ├── index.html ├── manifest.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.cjs ├── public └── powered-by-groq.png ├── src ├── App.tsx ├── background.js ├── components │ ├── layout │ │ ├── chat │ │ │ ├── chat-component.tsx │ │ │ ├── chat-layout.tsx │ │ │ └── chat-message.tsx │ │ ├── enter-key │ │ │ ├── enter-key-layout.tsx │ │ │ └── key-input-form.tsx │ │ ├── main-layout.tsx │ │ ├── prompt-layout.tsx │ │ ├── settings │ │ │ ├── delete-key-button.tsx │ │ │ ├── models-combo.tsx │ │ │ ├── reset-settings-button.tsx │ │ │ ├── settings-layout.tsx │ │ │ ├── summary-prompt.tsx │ │ │ └── truncate-slider.tsx │ │ └── summary │ │ │ └── summary-layout.tsx │ ├── shelf │ │ ├── code-block.tsx │ │ ├── content-block.tsx │ │ ├── markdown-block.tsx │ │ ├── mode-toggle.tsx │ │ ├── powered-by-groq.tsx │ │ └── textarea-autosize.tsx │ └── ui │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── separator.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ └── textarea.tsx ├── contentScript.js ├── hooks │ ├── use-chrome-storage.tsx │ ├── use-chrome-tab.tsx │ ├── use-chunk-summarizer.ts │ ├── use-copy-to-clipboard.tsx │ ├── use-groq-completion.tsx │ ├── use-groq-key-tester.tsx │ ├── use-groq-models.tsx │ ├── use-groq.tsx │ ├── use-text-summary.tsx │ └── use-window-resize.ts ├── lib │ ├── defaults.ts │ └── utils.ts ├── main.tsx ├── models │ └── index.ts ├── providers │ ├── app-nav.tsx │ ├── chat-provider.tsx │ ├── provider-factory.tsx │ ├── settings-provider.tsx │ ├── summary-provider.tsx │ └── theme-provider.tsx ├── style.css └── test │ └── test-data.ts ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Groq Summary Chrome Extension 2 | 3 | ## Overview 4 | The Groq Chrome Extension is a powerful tool designed to enhance your web browsing experience by summarizing content from websites using Groq. With this extension, you can: 5 | 6 | - **Summarize Entire Web Pages**: Quickly get the gist of any website's content. 7 | - **Summarize Selected Text**: Highlight specific text on a webpage, and the extension will summarize just that portion. 8 | - **Chat with Content**: Ask questions or engage in a conversation with the content of the page or the selected text to deepen your understanding. 9 | 10 | ### Intended Audience 11 | This extension is ideal for users who need to quickly comprehend or interact with online content, such as researchers, students, or professionals. 12 | The Groq Chrome Extension is a powerful tool designed to enhance your web browsing experience by summarizing content from websites using Groq. With this extension, you can: 13 | 14 | - **Summarize Entire Web Pages**: Quickly get the gist of any website's content. 15 | - **Summarize Selected Text**: Highlight specific text on a webpage, and the extension will summarize just that portion. 16 | - **Chat with Content**: Ask questions or engage in a conversation with the content of the page or the selected text to deepen your understanding. 17 | 18 | ## Setup Requirements 19 | To use this extension, you must provide a Groq API key. The key will be securely stored in the extension's local storage for authentication purposes. 20 | 21 | ## Key Features 22 | - **React-Based**: Built using the React library for a modern and efficient user interface. 23 | - **ShadCN Components**: Leverages ShadCN for consistent and customizable component styling. 24 | - **Dark Mode Support**: Includes dark mode for a better user experience in low-light environments. 25 | - **Hot Reload**: Supports hot reloading during development when running `pnpm dev` for seamless coding. 26 | 27 | ## Development Setup 28 | To set up and run the extension locally for development: 29 | 30 | 1. Install dependencies and start the development server: 31 | ```bash 32 | pnpm dev 33 | ``` 34 | 35 | 2. Open your browser and navigate to: 36 | ``` 37 | http://localhost:5173 38 | ``` 39 | 40 | ## Building for Production 41 | To build the extension for production: 42 | 43 | 1. Run the build command: 44 | ```bash 45 | pnpm build 46 | ``` 47 | 48 | 2. The extension popup will be available at: 49 | ``` 50 | dist/index.html 51 | ``` 52 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /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/style.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 | } -------------------------------------------------------------------------------- /dist/assets/popup.css: -------------------------------------------------------------------------------- 1 | @import"https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap";*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 0 0% 9%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 0 0% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--chart-1: 12 76% 61%;--chart-2: 173 58% 39%;--chart-3: 197 37% 24%;--chart-4: 43 74% 66%;--chart-5: 27 87% 67%;--radius: .5rem;--font-montserrat: "Montserrat", sans-serif;--font-inter: "Inter", sans-serif}.dark{--background: 251 5% 9%;--foreground: 0 0% 98%;--card: 0 0% 3.9%;--card-foreground: 0 0% 98%;--popover: 0 0% 3.9%;--popover-foreground: 0 0% 98%;--primary: 0 0% 98%;--primary-foreground: 0 0% 9%;--secondary: 0 0% 14.9%;--secondary-foreground: 0 0% 98%;--muted: 0 0% 14.9%;--muted-foreground: 0 0% 63.9%;--accent: 0 0% 14.9%;--accent-foreground: 0 0% 98%;--destructive: 0 62.8% 30.6%;--destructive-foreground: 0 0% 98%;--border: 0 0% 14.9%;--input: 0 0% 14.9%;--ring: 0 0% 83.1%;--chart-1: 220 70% 50%;--chart-2: 160 60% 45%;--chart-3: 30 80% 55%;--chart-4: 280 65% 60%;--chart-5: 340 75% 55%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.invisible{visibility:hidden}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-3{bottom:.75rem}.bottom-\[25px\]{bottom:25px}.left-0{left:0}.left-2{left:.5rem}.left-\[50\%\]{left:50%}.right-2{right:.5rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.top-\[-12px\]{top:-12px}.top-\[2px\]{top:2px}.top-\[50\%\]{top:50%}.z-50{z-index:50}.m-2{margin:.5rem}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-auto{margin-left:auto}.mt-10{margin-top:2.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[1\.2rem\]{height:1.2rem}.h-\[1px\]{height:1px}.h-\[2px\]{height:2px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-\[300px\]{max-height:300px}.min-h-\[60px\]{min-height:60px}.w-1\/2{width:50%}.w-2{width:.5rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-4{width:1rem}.w-9{width:2.25rem}.w-\[1\.2rem\]{width:1.2rem}.w-\[1px\]{width:1px}.w-full{width:100%}.min-w-\[375px\]{min-width:375px}.min-w-\[8rem\]{min-width:8rem}.max-w-\[95\%\]{max-width:95%}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-0{--tw-rotate: 0deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-0{--tw-scale-x: 0;--tw-scale-y: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.scale-100{--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes loader{0%{left:0;width:0}50%{left:0;width:100%}to{left:100%;width:0}}.animate-loader{animation:loader .5s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.touch-none{touch-action:none}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize{resize:both}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.border{border-width:1px}.border-input{border-color:hsl(var(--input))}.border-primary\/30{border-color:hsl(var(--primary) / .3)}.border-primary\/50{border-color:hsl(var(--primary) / .5)}.bg-\[\#ebeaef\]{--tw-bg-opacity: 1;background-color:rgb(235 234 239 / var(--tw-bg-opacity, 1))}.bg-\[\#f55036\]{--tw-bg-opacity: 1;background-color:rgb(245 80 54 / var(--tw-bg-opacity, 1))}.bg-background{background-color:hsl(var(--background))}.bg-black\/80{background-color:#000c}.bg-blue-700{--tw-bg-opacity: 1;background-color:rgb(29 78 216 / var(--tw-bg-opacity, 1))}.bg-border{background-color:hsl(var(--border))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-muted{background-color:hsl(var(--muted))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-primary\/20{background-color:hsl(var(--primary) / .2)}.bg-secondary{background-color:hsl(var(--secondary))}.bg-transparent{background-color:transparent}.bg-\[url\(\'\/powered-by-groq\.png\'\)\]{background-image:url(../powered-by-groq.png)}.bg-contain{background-size:contain}.bg-center{background-position:center}.bg-no-repeat{background-repeat:no-repeat}.fill-current{fill:currentColor}.p-0{padding:0}.p-1{padding:.25rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-1{padding-bottom:.25rem}.pb-10{padding-bottom:2.5rem}.pl-8{padding-left:2rem}.pr-2{padding-right:.5rem}.pt-10{padding-top:2.5rem}.pt-3{padding-top:.75rem}.pt-4{padding-top:1rem}.text-center{text-align:center}.font-inter{font-family:var(--font-inter)}.font-montserrat{font-family:var(--font-montserrat)}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.leading-none{line-height:1}.tracking-tight{letter-spacing:-.025em}.tracking-widest{letter-spacing:.1em}.text-\[\#f55036\]{--tw-text-opacity: 1;color:rgb(245 80 54 / var(--tw-text-opacity, 1))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.underline{text-decoration-line:underline}.underline-offset-4{text-underline-offset:4px}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.duration-200{animation-duration:.2s}.running{animation-play-state:running}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-foreground::file-selector-button{color:hsl(var(--foreground))}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-background:hover{background-color:hsl(var(--background))}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-100:hover{opacity:1}.focus\:bg-accent:focus{background-color:hsl(var(--accent))}.focus\:text-accent-foreground:focus{color:hsl(var(--accent-foreground))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-1:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group.toaster .group-\[\.toaster\]\:border-border{border-color:hsl(var(--border))}.group.toast .group-\[\.toast\]\:bg-muted{background-color:hsl(var(--muted))}.group.toast .group-\[\.toast\]\:bg-primary{background-color:hsl(var(--primary))}.group.toaster .group-\[\.toaster\]\:bg-background{background-color:hsl(var(--background))}.group.toast .group-\[\.toast\]\:text-muted-foreground{color:hsl(var(--muted-foreground))}.group.toast .group-\[\.toast\]\:text-primary-foreground{color:hsl(var(--primary-foreground))}.group.toaster .group-\[\.toaster\]\:text-foreground{color:hsl(var(--foreground))}.group.toaster .group-\[\.toaster\]\:shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:hsl(var(--muted-foreground))}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}.dark\:-rotate-90:is(.dark *){--tw-rotate: -90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:rotate-0:is(.dark *){--tw-rotate: 0deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:scale-0:is(.dark *){--tw-scale-x: 0;--tw-scale-y: 0;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:scale-100:is(.dark *){--tw-scale-x: 1;--tw-scale-y: 1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.dark\:bg-\[\#2b2a2f\]:is(.dark *){--tw-bg-opacity: 1;background-color:rgb(43 42 47 / var(--tw-bg-opacity, 1))}@media (min-width: 640px){.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media (min-width: 768px){.md\:text-sm{font-size:.875rem;line-height:1.25rem}}.\[\&\>p\]\:text-sm>p{font-size:.875rem;line-height:1.25rem}.\[\&\>p\]\:text-muted-foreground>p{color:hsl(var(--muted-foreground))}.\[\&\>svg\]\:size-4>svg{width:1rem;height:1rem}.\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:1rem;height:1rem}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0} 2 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Groq Summarizer 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /dist/powered-by-groq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jose-mdz/groq-chrome-ext/edf6c041f7ad72973f13d677f2eccb67d90616c4/dist/powered-by-groq.png -------------------------------------------------------------------------------- /icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jose-mdz/groq-chrome-ext/edf6c041f7ad72973f13d677f2eccb67d90616c4/icons/128.png -------------------------------------------------------------------------------- /icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jose-mdz/groq-chrome-ext/edf6c041f7ad72973f13d677f2eccb67d90616c4/icons/16.png -------------------------------------------------------------------------------- /icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jose-mdz/groq-chrome-ext/edf6c041f7ad72973f13d677f2eccb67d90616c4/icons/32.png -------------------------------------------------------------------------------- /icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jose-mdz/groq-chrome-ext/edf6c041f7ad72973f13d677f2eccb67d90616c4/icons/48.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Groq Summarizer 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Groq Summary", 4 | "version": "1.1", 5 | "description": "Uses Groq to summarize or interact with the content of the current page or selected text.", 6 | "permissions": ["activeTab", "scripting", "storage"], 7 | "action": { 8 | "default_title": "Groq Summary", 9 | "default_popup": "dist/index.html", 10 | "default_icon": { 11 | "16": "icons/16.png", 12 | "32": "icons/32.png", 13 | "48": "icons/48.png", 14 | "128": "icons/128.png" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "groq-summary-extension", 3 | "type": "module", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "vite --mode development", 7 | "build": "vite build" 8 | }, 9 | "dependencies": { 10 | "@radix-ui/react-dialog": "^1.1.2", 11 | "@radix-ui/react-dropdown-menu": "^2.1.2", 12 | "@radix-ui/react-separator": "^1.1.0", 13 | "@radix-ui/react-slider": "^1.2.2", 14 | "@radix-ui/react-slot": "^1.1.0", 15 | "@vitejs/plugin-react": "^4.3.4", 16 | "class-variance-authority": "^0.7.1", 17 | "clsx": "^2.1.1", 18 | "groq-sdk": "^0.9.0", 19 | "lucide-react": "^0.468.0", 20 | "next-themes": "^0.4.4", 21 | "react": "^19.0.0", 22 | "react-dom": "^19.0.0", 23 | "react-markdown": "^9.0.1", 24 | "react-syntax-highlighter": "^15.6.1", 25 | "remark-gfm": "^4.0.0", 26 | "sonner": "^1.7.1", 27 | "swr": "^2.2.5", 28 | "tailwind-merge": "^2.5.5", 29 | "tailwindcss-animate": "^1.0.7", 30 | "zod": "^3.24.1" 31 | }, 32 | "devDependencies": { 33 | "@biomejs/biome": "1.9.4", 34 | "@types/node": "^22.10.1", 35 | "@types/react": "^19.0.1", 36 | "@types/react-dom": "^19.0.2", 37 | "@types/react-syntax-highlighter": "^15.5.13", 38 | "autoprefixer": "^10.4.20", 39 | "chrome-types": "^0.1.324", 40 | "postcss": "^8.4.49", 41 | "tailwindcss": "^3.4.16", 42 | "typescript": "^5.7.2", 43 | "vite": "^6.0.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/powered-by-groq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jose-mdz/groq-chrome-ext/edf6c041f7ad72973f13d677f2eccb67d90616c4/public/powered-by-groq.png -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from "./providers/theme-provider"; 2 | import { MainLayout } from "./components/layout/main-layout"; 3 | import { SettingsProvider } from "./providers/settings-provider"; 4 | import { AppNavProvider } from "./providers/app-nav"; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | // Not strictly required for this simple example, but useful if we need background tasks. 2 | // Currently empty, but the presence of a background file is often a good starting point. 3 | chrome.runtime.onInstalled.addListener(() => { 4 | console.log("Summarizer installed."); 5 | }); 6 | -------------------------------------------------------------------------------- /src/components/layout/chat/chat-component.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input"; 2 | import { useChat } from "@/providers/chat-provider"; 3 | import { ChatMessage } from "./chat-message"; 4 | import { Button } from "@/components/ui/button"; 5 | import { SendHorizontal } from "lucide-react"; 6 | import { useEffect } from "react"; 7 | import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 8 | import { toast } from "sonner"; 9 | 10 | export function ChatComponent() { 11 | const { 12 | messages, 13 | addUserMessage, 14 | composingMessage, 15 | setComposingMessage, 16 | isLoading, 17 | clearMessages, 18 | } = useChat(); 19 | 20 | const { copyToClipboard } = useCopyToClipboard({}); 21 | 22 | const handleSubmit = (e: React.FormEvent) => { 23 | e.preventDefault(); 24 | addUserMessage(composingMessage); 25 | setComposingMessage(""); 26 | }; 27 | 28 | useEffect(() => { 29 | if (messages.length > 0) { 30 | window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }); 31 | } 32 | }, [messages]); 33 | 34 | const copyConversation = () => { 35 | copyToClipboard(JSON.stringify(messages, null, 2)); 36 | toast.success("Copied conversation to clipboard"); 37 | }; 38 | 39 | return ( 40 |
41 | {messages.length > 0 && ( 42 |
43 | {messages 44 | .filter((m) => m.role !== "system") 45 | .map((message, index) => ( 46 | 51 | ))} 52 |
53 | )} 54 |
55 | setComposingMessage(e.target.value)} 61 | /> 62 | 69 |
70 | {messages.length > 0 && ( 71 |
72 | 79 | 86 |
87 | )} 88 |
89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /src/components/layout/chat/chat-layout.tsx: -------------------------------------------------------------------------------- 1 | import { ChatComponent } from "@/components/layout/chat/chat-component"; 2 | 3 | export function ChatLayout() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/layout/chat/chat-message.tsx: -------------------------------------------------------------------------------- 1 | import { ContentBlock } from "@/components/shelf/content-block"; 2 | import { cn } from "@/lib/utils"; 3 | import type { ChatCompletionMessage } from "@/models"; 4 | 5 | export function ChatMessage({ 6 | message, 7 | className, 8 | }: { 9 | message: ChatCompletionMessage; 10 | className?: string; 11 | }) { 12 | const { role, content } = message; 13 | return ( 14 |
21 |
22 | {role === "user" ? "you" : role} 23 |
24 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/layout/enter-key/enter-key-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "@/components/ui/separator"; 2 | import { CircleHelp } from "lucide-react"; 3 | import { KeyInputForm } from "./key-input-form"; 4 | 5 | export function EnterKeyLayout() { 6 | return ( 7 |
8 |

Welcome to Groq Summarizer

9 |
10 |
This extension requires a Groq API Key:
11 | 12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |

Get a free key

20 |

21 | You can get a free Groq API key from the{" "} 22 | 28 | Groq developer console website. 29 | 30 | . 31 |

32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/layout/enter-key/key-input-form.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Input } from "@/components/ui/input"; 3 | import { useGroqKeyTester } from "@/hooks/use-groq-key-tester"; 4 | import { useSettings } from "@/providers/settings-provider"; 5 | import { ArrowRight, Loader2 } from "lucide-react"; 6 | import { useEffect, useState } from "react"; 7 | import { toast } from "sonner"; 8 | 9 | export function KeyInputForm() { 10 | const { apiKey, setApiKey } = useSettings(); 11 | const { isTesting, testSuccess, testKey } = useGroqKeyTester(); 12 | const [text, setText] = useState(apiKey); 13 | 14 | const handleSubmit = (e: React.FormEvent) => { 15 | e.preventDefault(); 16 | testKey(text); 17 | }; 18 | 19 | useEffect(() => { 20 | if (testSuccess === false) { 21 | } else if (testSuccess === true) { 22 | setApiKey(text); 23 | toast.success("API Key saved"); 24 | } 25 | }, [testSuccess, text, setApiKey]); 26 | 27 | return ( 28 |
29 |
30 | setText(e.target.value)} 34 | placeholder="Groq API key" 35 | /> 36 | 43 |
44 | {testSuccess === false && !isTesting && ( 45 |
46 | The key is invalid. Pease try another one. 47 |
48 | )} 49 |
50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/layout/main-layout.tsx: -------------------------------------------------------------------------------- 1 | import { EnterKeyLayout } from "./enter-key/enter-key-layout"; 2 | import { PoweredByGroq } from "../shelf/powered-by-groq"; 3 | import { PromptLayout } from "./prompt-layout"; 4 | import { SettingsLayout } from "./settings/settings-layout"; 5 | import { useAppNav } from "@/providers/app-nav"; 6 | import { Toaster } from "sonner"; 7 | 8 | export function MainLayout() { 9 | const { currentView } = useAppNav(); 10 | return ( 11 |
12 | {currentView === "key" ? ( 13 | 14 | ) : currentView === "settings" ? ( 15 | 16 | ) : ( 17 | 18 | )} 19 | 20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/layout/prompt-layout.tsx: -------------------------------------------------------------------------------- 1 | import { SummaryProvider } from "@/providers/summary-provider"; 2 | import { ChatLayout } from "./chat/chat-layout"; 3 | import { SummaryLayout } from "./summary/summary-layout"; 4 | import { ChatProvider } from "@/providers/chat-provider"; 5 | 6 | export function PromptLayout() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/layout/settings/delete-key-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { useSettings } from "@/providers/settings-provider"; 12 | 13 | export function DeleteKeyButton() { 14 | const { setApiKey } = useSettings(); 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | Delete API Key 24 | 25 | 26 | Are you sure you want to delete the API key? 27 | 28 | 29 | 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/layout/settings/models-combo.tsx: -------------------------------------------------------------------------------- 1 | import { useGroqModels } from "@/hooks/use-groq-models"; 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuItem, 6 | DropdownMenuTrigger, 7 | } from "@/components/ui/dropdown-menu"; 8 | import { Button } from "@/components/ui/button"; 9 | import { ChevronDown, Loader2 } from "lucide-react"; 10 | import { useSettings } from "@/providers/settings-provider"; 11 | import { useEffect } from "react"; 12 | 13 | export function ModelsCombo() { 14 | const { models = [], isLoading } = useGroqModels(); 15 | const { currentModel, setCurrentModel } = useSettings(); 16 | 17 | // If the current model is not in the list of models, set the current model to the first model in the list 18 | useEffect(() => { 19 | if ( 20 | models.length > 0 && 21 | !models.filter((model) => model.id === currentModel).length 22 | ) { 23 | setCurrentModel(models[0].id); 24 | } 25 | }, [models, currentModel, setCurrentModel]); 26 | 27 | return ( 28 | 29 | 30 | 38 | 39 | 40 | {models.map((model) => ( 41 | setCurrentModel(model.id)} 44 | > 45 | {model.id} 46 | 47 | ))} 48 | 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/layout/settings/reset-settings-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogDescription, 6 | DialogFooter, 7 | DialogHeader, 8 | DialogTitle, 9 | DialogTrigger, 10 | } from "@/components/ui/dialog"; 11 | import { useAppNav } from "@/providers/app-nav"; 12 | import { useSettings } from "@/providers/settings-provider"; 13 | import { useState } from "react"; 14 | 15 | export function ResetAllSettingsButton() { 16 | const { resetAllSettings } = useSettings(); 17 | const { closeSettings } = useAppNav(); 18 | const [open, setOpen] = useState(false); 19 | 20 | const handleReset = () => { 21 | resetAllSettings(); 22 | closeSettings(); 23 | setOpen(false); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | 30 | 31 | 32 | 33 | Reset All Settings 34 | 35 | 36 | Are you sure you want to reset all settings? 37 | 38 | 39 | 42 | 43 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/layout/settings/settings-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Separator } from "@/components/ui/separator"; 3 | import { useSettings } from "@/providers/settings-provider"; 4 | import { ModelsCombo } from "./models-combo"; 5 | import { X } from "lucide-react"; 6 | import { useAppNav } from "@/providers/app-nav"; 7 | import { SummaryPrompt } from "./summary-prompt"; 8 | import { DeleteKeyButton } from "./delete-key-button"; 9 | import { ResetAllSettingsButton } from "./reset-settings-button"; 10 | import { TruncateSlider } from "./truncate-slider"; 11 | 12 | export function SettingsLayout() { 13 | const { closeSettings } = useAppNav(); 14 | return ( 15 |
16 |
17 |

Settings

18 | 21 |
22 | 23 | 24 | 25 | 26 |

27 | You can customize the prompt used to summarize the text. This is 28 | useful if you want to change the way the summary is generated. 29 |

30 | 31 |
32 | 33 |

34 | Text is truncated to stay within model limits while still providing 35 | meaningful summaries. You can adjust the word limit to balance between 36 | summary completeness and processing speed. 37 |

38 | 39 |
40 | 41 |

If you want to use a different API key, delete the current key.

42 | 43 |
44 | 45 |

46 | Reset all settings to their default values. This will delete all 47 | custom settings and reset the extension to its initial state. 48 |

49 | 50 |
51 |
52 | ); 53 | } 54 | 55 | function SettingsSection({ 56 | children, 57 | title, 58 | noSeparator, 59 | }: { 60 | children: React.ReactNode; 61 | title: string; 62 | noSeparator?: boolean; 63 | }) { 64 | return ( 65 |
66 | {!noSeparator && } 67 |

{title}

68 |
69 | {children} 70 |
71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/components/layout/settings/summary-prompt.tsx: -------------------------------------------------------------------------------- 1 | import TextareaAutosize from "@/components/shelf/textarea-autosize"; 2 | import { useSettings } from "@/providers/settings-provider"; 3 | 4 | export function SummaryPrompt() { 5 | const { summarizePrompt, setSummarizePrompt } = useSettings(); 6 | return ( 7 |
8 | setSummarizePrompt(e.target.value)} 11 | /> 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /src/components/layout/settings/truncate-slider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from "@/components/ui/slider"; 2 | import { useGroqModels } from "@/hooks/use-groq-models"; 3 | import { formatNumber } from "@/lib/utils"; 4 | import { useSettings } from "@/providers/settings-provider"; 5 | 6 | declare global { 7 | namespace Groq { 8 | namespace Models { 9 | interface Model { 10 | context_window: number; 11 | } 12 | } 13 | } 14 | } 15 | 16 | export function TruncateSlider() { 17 | const { currentModel, chunkWordLimit, setChunkWordLimit } = useSettings(); 18 | const { models = [], getContextWindow } = useGroqModels(); 19 | 20 | if (models.length === 0) return null; 21 | 22 | const model = models.find((model) => model.id === currentModel); 23 | 24 | if (!model) return null; 25 | 26 | const contextWindow = getContextWindow(currentModel); 27 | 28 | return ( 29 | <> 30 |
31 | {formatNumber(Number.parseInt(chunkWordLimit))} words 32 |
33 | setChunkWordLimit(value[0].toString())} 38 | /> 39 |
40 |
41 |
Model:
42 |
{currentModel}
43 |
44 |
45 |
Context window:
46 |
{formatNumber(contextWindow)} tokens
47 |
48 |
49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/layout/summary/summary-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { cn, formatNumber, formatTime } from "@/lib/utils"; 3 | import { Zap, PanelTop, TextSelect, Lightbulb, RefreshCcw } from "lucide-react"; 4 | import { useSummary } from "@/providers/summary-provider"; 5 | import { ContentBlock } from "@/components/shelf/content-block"; 6 | 7 | export function SummaryLayout() { 8 | const { 9 | isLoading, 10 | summary, 11 | pageTokens, 12 | selectionTokens, 13 | pageText, 14 | selectionText, 15 | summarySource, 16 | setSummarySource, 17 | clearSummary, 18 | retrySummary, 19 | usage, 20 | } = useSummary(); 21 | 22 | return ( 23 |
24 |

25 | 26 | Summarize 27 |
28 | with groq 29 |
30 |

31 |
32 |
37 |
38 |
39 |
40 | 51 | {selectionText && ( 52 | 63 | )} 64 |
65 | {pageTokens && ( 66 |
67 |
68 | {formatNumber(pageTokens)} word{pageTokens > 1 ? "s" : ""} 69 |
70 | {selectionText && ( 71 |
72 | {formatNumber(selectionTokens)} word 73 | {selectionTokens > 1 ? "s" : ""} 74 |
75 | )} 76 |
77 | )} 78 |
79 | 80 | {!summary && !selectionText && ( 81 |
82 |
83 | 84 | 85 | You can also select text to 86 | have an option to summarize the selected text. 87 | 88 |
89 |
90 | )} 91 | {summary && ( 92 | <> 93 | 94 |
95 | 103 | {usage && ( 104 |
105 |
{formatTime(usage.total_time || 0)}
106 | 107 |
108 | {formatNumber( 109 | usage.completion_tokens / (usage.completion_time || 1), 110 | )}{" "} 111 | T/s 112 |
113 |
114 | )} 115 | 123 |
124 | 125 | )} 126 |
127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /src/components/shelf/code-block.tsx: -------------------------------------------------------------------------------- 1 | import { Prism } from "react-syntax-highlighter"; 2 | import { vscDarkPlus } from "react-syntax-highlighter/dist/cjs/styles/prism"; 3 | 4 | export function CodeBlock({ content }: { content: string }) { 5 | return ( 6 | 7 | {content} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/components/shelf/content-block.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { useState } from "react"; 3 | import { Button } from "@/components/ui/button"; 4 | import { AlignLeft, Copy, Check, ALargeSmall } from "lucide-react"; 5 | import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"; 6 | import { MarkdownBlock } from "./markdown-block"; 7 | export function ContentBlock({ 8 | content, 9 | noControls, 10 | className, 11 | }: { 12 | content: string; 13 | className?: string; 14 | noControls?: boolean; 15 | }) { 16 | const [isPlainText, setIsPlainText] = useState(false); 17 | const { isCopied, copyToClipboard } = useCopyToClipboard({}); 18 | return ( 19 |
20 | {!noControls && ( 21 |
22 |
23 | 31 | 39 |
40 |
41 | 48 |
49 |
50 | )} 51 |
57 | {isPlainText ? ( 58 |
{content}
59 | ) : ( 60 | {content} 61 | )} 62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/shelf/markdown-block.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from "react-markdown"; 2 | import remarkGfm from "remark-gfm"; 3 | import { CodeBlock } from "./code-block"; 4 | 5 | export function MarkdownBlock({ 6 | children, 7 | }: { children: string | null | undefined }) { 8 | return ( 9 | ( 13 | 14 | ), 15 | h1: ({ children }) => ( 16 |

{children}

17 | ), 18 | h2: ({ children }) =>

{children}

, 19 | h3: ({ children }) =>

{children}

, 20 | p: ({ children }) =>
{children}
, 21 | ul: ({ children }) =>
    {children}
, 22 | ol: ({ children }) => ( 23 |
    {children}
24 | ), 25 | li: ({ children }) =>
  • {children}
  • , 26 | }} 27 | > 28 | {children} 29 |
    30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/shelf/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | DropdownMenu, 5 | DropdownMenuContent, 6 | DropdownMenuItem, 7 | DropdownMenuTrigger, 8 | } from "@/components/ui/dropdown-menu"; 9 | import { useTheme } from "@/providers/theme-provider"; 10 | 11 | export function ModeToggle() { 12 | const { setTheme } = useTheme(); 13 | 14 | return ( 15 | 16 | 17 | 26 | 27 | 28 | setTheme("light")}> 29 | Light 30 | 31 | setTheme("dark")}> 32 | Dark 33 | 34 | setTheme("system")}> 35 | System 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/components/shelf/powered-by-groq.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Settings } from "lucide-react"; 3 | import { useAppNav } from "@/providers/app-nav"; 4 | 5 | export function PoweredByGroq() { 6 | const { currentView, openSettings } = useAppNav(); 7 | return ( 8 |
    9 | {currentView === "summary" && ( 10 | 18 | )} 19 |
    20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/components/shelf/textarea-autosize.tsx: -------------------------------------------------------------------------------- 1 | import { Textarea, type TextareaProps } from "@/components/ui/textarea"; 2 | import { useWindowResize } from "@/hooks/use-window-resize"; 3 | import React, { useCallback, useEffect, useRef } from "react"; 4 | 5 | const TextareaAutosize = React.forwardRef( 6 | ({ className, value, autoFocus, ...props }, ref) => { 7 | const textareaRef = useRef(null); 8 | 9 | const adjustHeight = useCallback(() => { 10 | const textarea = textareaRef.current; 11 | if (textarea) { 12 | textarea.style.height = "auto"; 13 | if (String(value)) { 14 | textarea.style.height = `${textarea.scrollHeight}px`; 15 | } 16 | } 17 | }, [value]); 18 | 19 | useWindowResize(adjustHeight); 20 | 21 | useEffect(() => { 22 | if (value) { 23 | adjustHeight(); 24 | } 25 | }, [value, adjustHeight]); 26 | 27 | useEffect(() => { 28 | if (autoFocus) { 29 | textareaRef.current?.focus(); 30 | } 31 | }, [autoFocus]); 32 | 33 | return ( 34 |