├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── jest.config.js ├── package.json ├── public ├── icon128.png ├── icon16.png ├── icon32.png ├── icon48.png ├── logo.png ├── manifest.json ├── options.html └── popup.html ├── src ├── chrome │ ├── background.ts │ ├── common.css │ ├── containers │ │ ├── CommentStyleOptions.tsx │ │ ├── ExcludedWords.tsx │ │ ├── HashtagOptions.tsx │ │ ├── ModelOptions.tsx │ │ └── Prompts.tsx │ ├── content_script.ts │ ├── options.tsx │ ├── popup.styled.ts │ └── popup.tsx ├── components │ ├── ChatGPTIcon.tsx │ ├── Checkbox │ │ ├── Checkbox.styled.ts │ │ ├── Checkbox.tsx │ │ ├── Icon.tsx │ │ └── index.ts │ ├── Container │ │ ├── Container.styled.ts │ │ ├── Container.tsx │ │ └── index.ts │ ├── ICCheck.tsx │ ├── ICSettings.tsx │ ├── ICTrash.tsx │ ├── IcClose.tsx │ ├── IcInstagram.tsx │ ├── IcLinkedIn.tsx │ ├── IcPlus.tsx │ ├── IcTwitter.tsx │ ├── Loading.tsx │ ├── Logo.tsx │ ├── PromptsList │ │ ├── PrompsForm.styled.ts │ │ ├── PrompsForm.tsx │ │ └── utils.ts │ ├── Section │ │ ├── Section.styled.ts │ │ ├── Section.tsx │ │ └── index.ts │ ├── Tab │ │ ├── Tab.styled.ts │ │ ├── Tab.tsx │ │ └── index.ts │ └── TagInput │ │ ├── TagsInput.styled.ts │ │ └── TagsInput.tsx ├── hooks │ └── useChromeStorage.tsx ├── lib │ ├── instagram.ts │ ├── linkedin.ts │ ├── styles.ts │ └── twitter.ts └── utils │ ├── announcements.ts │ ├── config.ts │ ├── constants.ts │ ├── generators.ts │ ├── options.ts │ ├── prompts.ts │ └── shared.ts ├── tsconfig.json ├── typings.d.ts ├── webpack ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js └── yarn.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chcepe/social-comments-gpt/328a774454046070ecab4502eec5b43d8551283e/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | dist/ 4 | tmp/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christian Cepe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # social-comments-gpt 2 | 3 | A chrome extension that creates engaging comments on social media, powered by OpenAI's ChatGPT. 4 | 5 | Currently supports LinkedIn and Instagram. 6 | 7 | 8 | https://user-images.githubusercontent.com/25549784/214015786-c433c3bd-e537-42b5-ae99-b1ef6fd52960.mov 9 | 10 | 11 | ## Setup 12 | 13 | ### Development 14 | 15 | ``` 16 | yarn 17 | yarn build 18 | ``` 19 | 20 | ### Build in watch mode 21 | 22 | ``` 23 | yarn watch 24 | ``` 25 | 26 | ### Load extension to chrome 27 | 28 | 1. Go to `chrome://extensions/` and click Load Unpacked 29 | 2. Load `dist` folder 30 | 31 | ### Set API key for the OpenAI API 32 | 33 | 1. Log into your OpenAI account on the [OpenAI website](https://beta.openai.com/). 34 | 2. Click on the "View API Keys" button in the top-right corner of the page. 35 | 3. Click on the "Create an API Key" button to generate a new API key. 36 | 37 | Once the API key is generated, you can copy it and set it on the social-comments-gpt options. 38 | 39 | Screenshot 2023-01-23 at 11 17 19 AM 40 | 41 | ### More Options 42 | 43 | Screenshot 2023-01-26 at 9 26 42 PM 44 | 45 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "src" 4 | ], 5 | "transform": { 6 | "^.+\\.ts$": "ts-jest" 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social-comments-gpt", 3 | "version": "1.0.0", 4 | "description": "social-comments-gpt", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "webpack --config webpack/webpack.dev.js --watch", 8 | "build": "webpack --config webpack/webpack.prod.js", 9 | "clean": "rimraf dist", 10 | "test": "npx jest", 11 | "style": "prettier --write \"src/**/*.{ts,tsx}\"" 12 | }, 13 | "author": { 14 | "name": "Christian Lou Cepe", 15 | "email": "chcepe@gmail.com", 16 | "url": "https://chcepe.github.io" 17 | }, 18 | "license": "MIT", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/chcepe/social-comments-gpt.git" 22 | }, 23 | "dependencies": { 24 | "notyf": "^3.10.0", 25 | "react": "^17.0.1", 26 | "react-dom": "^17.0.1", 27 | "react-textarea-autosize": "^8.4.0", 28 | "styled-components": "^5.3.6" 29 | }, 30 | "devDependencies": { 31 | "@types/chrome": "0.0.158", 32 | "@types/jest": "^27.0.2", 33 | "@types/react": "^17.0.0", 34 | "@types/react-dom": "^17.0.0", 35 | "@types/styled-components": "^5.1.26", 36 | "copy-webpack-plugin": "^9.0.1", 37 | "css-loader": "^6.7.3", 38 | "glob": "^7.1.6", 39 | "jest": "^27.2.1", 40 | "prettier": "^2.2.1", 41 | "rimraf": "^3.0.2 ", 42 | "style-loader": "^3.3.1", 43 | "ts-jest": "^27.0.5", 44 | "ts-loader": "^8.0.0", 45 | "typescript": "^4.4.3 ", 46 | "webpack": "^5.61.0", 47 | "webpack-cli": "^4.0.0", 48 | "webpack-merge": "^5.0.0" 49 | }, 50 | "resolutions": { 51 | "styled-components": "^5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chcepe/social-comments-gpt/328a774454046070ecab4502eec5b43d8551283e/public/icon128.png -------------------------------------------------------------------------------- /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chcepe/social-comments-gpt/328a774454046070ecab4502eec5b43d8551283e/public/icon16.png -------------------------------------------------------------------------------- /public/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chcepe/social-comments-gpt/328a774454046070ecab4502eec5b43d8551283e/public/icon32.png -------------------------------------------------------------------------------- /public/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chcepe/social-comments-gpt/328a774454046070ecab4502eec5b43d8551283e/public/icon48.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chcepe/social-comments-gpt/328a774454046070ecab4502eec5b43d8551283e/public/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | 4 | "name": "Social Comments GPT", 5 | "description": "Create engaging comments on social media, powered by ChatGPT", 6 | "version": "1.6", 7 | 8 | "icons": { 9 | "16": "icon16.png", 10 | "32": "icon32.png", 11 | "48": "icon48.png", 12 | "128": "icon128.png" 13 | }, 14 | 15 | "options_ui": { 16 | "page": "options.html", 17 | "open_in_tab": true 18 | }, 19 | 20 | "action": { 21 | "default_icon": { 22 | "16": "icon16.png", 23 | "32": "icon32.png", 24 | "48": "icon48.png", 25 | "128": "icon128.png" 26 | }, 27 | "default_popup": "popup.html" 28 | }, 29 | 30 | "content_scripts": [ 31 | { 32 | "matches": ["*://*/*"], 33 | "include_globs": [ 34 | "*://*.linkedin.com/*", 35 | "*://linkedin.com/*", 36 | "*://*.instagram.com/*", 37 | "*://instagram.com/*", 38 | "*://*.twitter.com/*", 39 | "*://twitter.com/*" 40 | ], 41 | "js": ["js/vendor.js", "js/content_script.js"] 42 | } 43 | ], 44 | 45 | "permissions": ["storage"], 46 | 47 | "host_permissions": [ 48 | "https://social-comments-gpt-site.vercel.app/", 49 | "https://social-comments-gpt.com/" 50 | ], 51 | 52 | "background": { 53 | "service_worker": "js/background.js" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Social Comments GPT 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Social Comments GPT 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/chrome/background.ts: -------------------------------------------------------------------------------- 1 | import { UNINSTALL_PAGE, WELCOME_PAGE } from "../utils/constants"; 2 | 3 | chrome.runtime.onInstalled.addListener((details) => { 4 | if (details.reason == "install") { 5 | chrome.tabs.create({ 6 | url: WELCOME_PAGE, 7 | }); 8 | } 9 | }); 10 | 11 | chrome.runtime.setUninstallURL(UNINSTALL_PAGE); 12 | -------------------------------------------------------------------------------- /src/chrome/common.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600&display=swap"); 2 | 3 | body, 4 | input, 5 | textarea { 6 | font-family: "Open Sans", sans-serif; 7 | } 8 | 9 | a { 10 | text-decoration: none; 11 | } 12 | -------------------------------------------------------------------------------- /src/chrome/containers/CommentStyleOptions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Checkbox from "../../components/Checkbox"; 4 | import Loading from "../../components/Loading"; 5 | import useChromeStorage from "../../hooks/useChromeStorage"; 6 | import { 7 | COMMENTS_STYLE_OPTS, 8 | COMMENTS_STYLE_OPT_DEFAULT, 9 | } from "../../utils/options"; 10 | 11 | const CommentStyleOptions = () => { 12 | const [selected, setSelected, { loading }] = useChromeStorage( 13 | "opt-comment-style", 14 | COMMENTS_STYLE_OPT_DEFAULT 15 | ); 16 | 17 | if (loading) return ; 18 | 19 | return ( 20 | <> 21 | { 26 | setSelected(selected[selected.length - 1]); 27 | }} 28 | /> 29 | 30 | ); 31 | }; 32 | 33 | export default CommentStyleOptions; 34 | -------------------------------------------------------------------------------- /src/chrome/containers/ExcludedWords.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Loading from "../../components/Loading"; 3 | 4 | import TagsInput from "../../components/TagInput/TagsInput"; 5 | import useChromeStorage from "../../hooks/useChromeStorage"; 6 | 7 | const ExcludedWords = () => { 8 | const [excludedWords, setExludedWords, { loading }] = useChromeStorage< 9 | string[] 10 | >("opt-excluded-words", []); 11 | 12 | if (loading) return ; 13 | 14 | return ( 15 | <> 16 | 21 | 22 | ); 23 | }; 24 | 25 | export default ExcludedWords; 26 | -------------------------------------------------------------------------------- /src/chrome/containers/HashtagOptions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Checkbox from "../../components/Checkbox"; 4 | import Loading from "../../components/Loading"; 5 | import useChromeStorage from "../../hooks/useChromeStorage"; 6 | import { HASHTAG_OPTS, HASHTAG_OPT_DEFAULT } from "../../utils/options"; 7 | 8 | const HashtagOptions = () => { 9 | const [selected, setSelected, { loading }] = useChromeStorage( 10 | "opt-hashtag-option", 11 | HASHTAG_OPT_DEFAULT 12 | ); 13 | 14 | if (loading) return ; 15 | 16 | return ( 17 | <> 18 | { 23 | setSelected(selected[selected.length - 1]); 24 | }} 25 | /> 26 | 27 | ); 28 | }; 29 | 30 | export default HashtagOptions; 31 | -------------------------------------------------------------------------------- /src/chrome/containers/ModelOptions.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import Checkbox from "../../components/Checkbox"; 4 | import Loading from "../../components/Loading"; 5 | import useChromeStorage from "../../hooks/useChromeStorage"; 6 | import { MODEL_OPTS, MODEL_OPT_DEFAULT } from "../../utils/options"; 7 | 8 | const ModelOptions = () => { 9 | const [selected, setSelected, { loading }] = useChromeStorage( 10 | "opt-model-type", 11 | MODEL_OPT_DEFAULT 12 | ); 13 | 14 | if (loading) return ; 15 | 16 | return ( 17 | <> 18 | { 23 | setSelected(selected[selected.length - 1]); 24 | }} 25 | /> 26 | 27 | ); 28 | }; 29 | 30 | export default ModelOptions; 31 | -------------------------------------------------------------------------------- /src/chrome/containers/Prompts.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import PrompsForm from "../../components/PromptsList/PrompsForm"; 4 | import Loading from "../../components/Loading"; 5 | import Section from "../../components/Section"; 6 | import { Domains } from "../../utils/constants"; 7 | import useChromeStorage from "../../hooks/useChromeStorage"; 8 | import { DEFAULT_CONFIG, StorageKeys } from "../../utils/config"; 9 | 10 | interface Props { 11 | type: Domains; 12 | } 13 | 14 | const ALL_PROMPTS: Record = { 15 | [Domains.LinkedIn]: ["LinkedIn", "opt-linkedin-prompts"], 16 | [Domains.Instagram]: ["Instagram", "opt-insta-prompts"], 17 | [Domains.Twitter]: ["Twitter", "opt-twitter-prompts"], 18 | }; 19 | 20 | const Prompts: React.FC = ({ type }) => { 21 | const [label, key] = ALL_PROMPTS[type]; 22 | const [prompts, setPrompts, { loading }] = useChromeStorage( 23 | key, 24 | DEFAULT_CONFIG[key] 25 | ); 26 | 27 | if (loading) return ; 28 | 29 | return ( 30 |
34 | 35 |
36 | ); 37 | }; 38 | 39 | export default Prompts; 40 | -------------------------------------------------------------------------------- /src/chrome/content_script.ts: -------------------------------------------------------------------------------- 1 | import { Notyf } from "notyf"; 2 | 3 | import appendStyles from "../lib/styles"; 4 | import { ALLOWED_DOMAINS, Domains } from "../utils/constants"; 5 | import { 6 | injector as linkedInInjector, 7 | handler as linkedInHandler, 8 | } from "../lib/linkedin"; 9 | import { 10 | injector as instagramInjector, 11 | handler as instagramHandler, 12 | } from "../lib/instagram"; 13 | import { 14 | injector as twitterInjector, 15 | handler as twitterHandler, 16 | } from "../lib/twitter"; 17 | import { 18 | injector as announcementInjector, 19 | handler as announcementHandler, 20 | } from "../utils/announcements"; 21 | 22 | const service: Record void, () => Promise]> = { 23 | [Domains.LinkedIn]: [linkedInInjector, linkedInHandler], 24 | [Domains.Instagram]: [instagramInjector, instagramHandler], 25 | [Domains.Twitter]: [twitterInjector, twitterHandler], 26 | }; 27 | 28 | export let notyf: Notyf | undefined; 29 | 30 | (() => { 31 | const hostname = window.location.hostname; 32 | const activeTabDomain = (hostname?.match( 33 | /^(?:.*?\.)?([a-zA-Z0-9\-_]{3,}\.(?:\w{2,8}|\w{2,4}\.\w{2,4}))$/ 34 | )?.[1] || "") as Domains; 35 | 36 | if (!ALLOWED_DOMAINS.includes(activeTabDomain)) return; 37 | 38 | const [injector, handler] = service[activeTabDomain]; 39 | 40 | notyf = new Notyf(); 41 | 42 | announcementHandler(activeTabDomain); 43 | appendStyles(); 44 | handler(); 45 | setInterval(injector, 200); 46 | setInterval(() => { 47 | announcementInjector(activeTabDomain); 48 | }, 1000); 49 | })(); 50 | -------------------------------------------------------------------------------- /src/chrome/options.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import styled from "styled-components"; 4 | 5 | import Container from "../components/Container"; 6 | import IcInstagram from "../components/IcInstagram"; 7 | import IcLinkedIn from "../components/IcLinkedIn"; 8 | import IcSettings from "../components/ICSettings"; 9 | import ICTwitter from "../components/IcTwitter"; 10 | import Logo from "../components/Logo"; 11 | import Section, { Props as SectionProps } from "../components/Section"; 12 | import Tab, { TabItem } from "../components/Tab"; 13 | import CommentStyleOptions from "./containers/CommentStyleOptions"; 14 | import HashtagOptions from "./containers/HashtagOptions"; 15 | import ExcludedWords from "./containers/ExcludedWords"; 16 | import Prompts from "./containers/Prompts"; 17 | import { Domains } from "../utils/constants"; 18 | 19 | import "./common.css"; 20 | 21 | const SECTIONS: (SectionProps & { comp: JSX.Element })[] = [ 22 | // { 23 | // title: "OpenAI Model", 24 | // desc: "Model to use for OpenAI API. text-davinci-003 produces higher quality writing.", 25 | // comp: , 26 | // }, 27 | { 28 | title: "Comment style", 29 | desc: "Whether generated comments will be professional, informal, etc.", 30 | comp: , 31 | }, 32 | { 33 | title: "Allow hashtags", 34 | desc: "Would you like to allow hashtags in generated comments?", 35 | comp: , 36 | }, 37 | { 38 | title: "Words to avoid", 39 | desc: "Words that will not be mentioned often in generated comments. It's not 100% guaranteed these words won't be mentioned.", 40 | comp: , 41 | }, 42 | ]; 43 | 44 | const TABS: TabItem[] = [ 45 | { 46 | title: "Settings", 47 | comp: ( 48 | <> 49 | {SECTIONS.map((section, i) => { 50 | const { comp, ...rest } = section; 51 | return ( 52 |
53 | {comp} 54 |
55 | ); 56 | })} 57 | 58 | ), 59 | icon: , 60 | }, 61 | { 62 | title: "Instagram Prompts", 63 | comp: , 64 | icon: , 65 | }, 66 | { 67 | title: "LinkedIn Prompts", 68 | comp: , 69 | icon: , 70 | }, 71 | { 72 | title: "Twitter Prompts", 73 | comp: , 74 | icon: , 75 | }, 76 | ]; 77 | 78 | export const Main = styled.div` 79 | margin: 24px 0; 80 | `; 81 | 82 | const Options = () => { 83 | return ( 84 | 85 | 86 | 87 | {/* Tabs */} 88 |
89 | 90 |
91 | 92 | {/* Copyright */} 93 |

94 | 95 | social-comments-gpt.com 96 | {" "} 97 | © 2022 98 |

99 | 100 | {/* Credits */} 101 |

102 | Made with ❤️ by{" "} 103 | 104 | chcepe 105 | 106 |

107 |
108 | ); 109 | }; 110 | 111 | ReactDOM.render( 112 | 113 | 114 | , 115 | document.getElementById("root") 116 | ); 117 | -------------------------------------------------------------------------------- /src/chrome/popup.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | flex-direction: column; 7 | width: 300px; 8 | height: auto; 9 | padding: 24px; 10 | position: relative; 11 | 12 | .logo { 13 | margin-bottom: 24px; 14 | height: 48px; 15 | } 16 | 17 | input { 18 | padding: 15px 20px; 19 | border-radius: 16px; 20 | border: 1px solid rgba(0, 0, 0, 0.1); 21 | width: 100%; 22 | text-align: center; 23 | margin: 8px 0; 24 | } 25 | 26 | input:focus { 27 | outline: none !important; 28 | border-color: rgba(0, 0, 0, 0.4); 29 | } 30 | 31 | p { 32 | font-size: 10px; 33 | text-align: center; 34 | } 35 | 36 | p a { 37 | font-weight: bold; 38 | text-decoration: none; 39 | } 40 | `; 41 | 42 | export const SettingsBtn = styled.div` 43 | cursor: pointer; 44 | display: flex; 45 | align-items: center; 46 | gap: 6px; 47 | padding: 8px 16px; 48 | border-radius: 16px; 49 | border: 1px solid rgba(0, 0, 0, 0.8); 50 | 51 | &:hover { 52 | color: #fff; 53 | background: linear-gradient(133.43deg, #4d0089 0%, #235f19 102.89%); 54 | 55 | svg { 56 | fill: #fff; 57 | transform: rotate(30deg); 58 | } 59 | } 60 | 61 | &, 62 | svg { 63 | transition: all 0.2s ease; 64 | } 65 | 66 | .settings-btn:hover svg { 67 | fill: #fff; 68 | transform: rotate(30deg); 69 | } 70 | `; 71 | -------------------------------------------------------------------------------- /src/chrome/popup.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | import ICSettings from "../components/ICSettings"; 5 | import Logo from "../components/Logo"; 6 | import useChromeStorage from "../hooks/useChromeStorage"; 7 | import { WELCOME_PAGE } from "../utils/constants"; 8 | import * as Styled from "./popup.styled"; 9 | import "./common.css"; 10 | 11 | const Popup = () => { 12 | const [openAIKey, setOpenAIKey, { loading }] = useChromeStorage( 13 | "social-comments-openapi-key", 14 | "" 15 | ); 16 | 17 | const handleOptions = () => { 18 | chrome.runtime.openOptionsPage(); 19 | }; 20 | 21 | return ( 22 | 23 | 24 | 25 | {/* OPENAI API Key */} 26 | 27 | { 34 | setOpenAIKey(e.target.value); 35 | }} 36 | /> 37 | 38 | {/* Help */} 39 |

40 | Have some questions? More information{" "} 41 | 42 | here. 43 | 44 |

45 | 46 | {/* Settings */} 47 | 48 | Options 49 | 50 | 51 | 52 |

53 | 54 | social-comments-gpt.com 55 | {" "} 56 | © 2022 57 |

58 |
59 | ); 60 | }; 61 | 62 | ReactDOM.render( 63 | 64 | 65 | , 66 | document.getElementById("root") 67 | ); 68 | -------------------------------------------------------------------------------- /src/components/ChatGPTIcon.tsx: -------------------------------------------------------------------------------- 1 | export default ( 2 | size: number, 3 | color: string, 4 | id?: string 5 | ) => ` 6 | 7 | 8 | `; 9 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | 3 | export const Wrapper = styled.div<{ isInline?: boolean }>` 4 | display: flex; 5 | align-items: center; 6 | flex-direction: ${({ isInline }) => (isInline ? "row" : "column")}; 7 | gap: 8px; 8 | `; 9 | 10 | export const CheckboxItem = styled.div<{ selected?: boolean }>` 11 | display: flex; 12 | align-items: center; 13 | gap: 6px; 14 | padding: 12px 16px; 15 | background: #fff; 16 | border-radius: 30px; 17 | border: 1px solid rgba(0, 0, 0, 0.3); 18 | cursor: pointer; 19 | transition: all 0.2s ease; 20 | 21 | svg { 22 | fill: rgba(0, 0, 0, 0.3); 23 | } 24 | 25 | ${({ selected }) => selected && `${selectedCSS}`} 26 | 27 | &:hover { 28 | ${({ selected }) => (selected ? `opacity: 0.9;` : `${selectedCSS}`)} 29 | } 30 | `; 31 | 32 | const selectedCSS = css` 33 | color: #fff; 34 | background: linear-gradient(133.43deg, #4d0089 0%, #235f19 102.89%); 35 | 36 | svg { 37 | fill: #fff; 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /src/components/Checkbox/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import * as Styled from "./Checkbox.styled"; 4 | import Icon, { State } from "./Icon"; 5 | 6 | export type CheckboxOption = { label: string; value: string }; 7 | 8 | interface Props { 9 | selected: string[]; 10 | options: CheckboxOption[]; 11 | onChange: (selected: string[]) => void; 12 | inline?: boolean; 13 | radio?: boolean; 14 | } 15 | 16 | const Checkbox: React.FC = ({ 17 | options, 18 | selected, 19 | onChange, 20 | inline, 21 | radio, 22 | }) => { 23 | const handleClick = (value: string) => { 24 | onChange([...selected, value].filter((v) => v)); 25 | }; 26 | 27 | return ( 28 | 29 | {options.map((option) => { 30 | let state = State.Normal; 31 | const isSelected = selected?.includes(option.value); 32 | if (isSelected) 33 | state = radio ? State.RadioSelected : State.CheckboxSelected; 34 | 35 | return ( 36 | handleClick(option.value)} 39 | selected={isSelected} 40 | > 41 | 42 | {option.label} 43 | 44 | ); 45 | })} 46 | 47 | ); 48 | }; 49 | 50 | export default Checkbox; 51 | -------------------------------------------------------------------------------- /src/components/Checkbox/Icon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export enum State { 4 | Normal, 5 | CheckboxSelected, 6 | RadioSelected, 7 | } 8 | 9 | const renderState: Record> = { 10 | [State.Normal]: ( 11 | 18 | 19 | 20 | ), 21 | [State.CheckboxSelected]: ( 22 | 29 | 30 | 31 | ), 32 | [State.RadioSelected]: ( 33 | 40 | 41 | 42 | ), 43 | }; 44 | 45 | interface Props { 46 | state: State; 47 | } 48 | 49 | export const Icon: React.FC = ({ state }) => <>{renderState[state]}; 50 | 51 | export default Icon; 52 | -------------------------------------------------------------------------------- /src/components/Checkbox/index.ts: -------------------------------------------------------------------------------- 1 | import Checkbox from "./Checkbox"; 2 | 3 | export default Checkbox; 4 | -------------------------------------------------------------------------------- /src/components/Container/Container.styled.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 48px 30px; 7 | `; 8 | -------------------------------------------------------------------------------- /src/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import * as Styled from "./Container.styled"; 4 | 5 | const Container: React.FC = ({ children }) => { 6 | return {children}; 7 | }; 8 | 9 | export default Container; 10 | -------------------------------------------------------------------------------- /src/components/Container/index.ts: -------------------------------------------------------------------------------- 1 | import Container from "./Container"; 2 | 3 | export default Container; 4 | -------------------------------------------------------------------------------- /src/components/ICCheck.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props extends React.SVGProps { 4 | isLinear?: boolean; 5 | } 6 | 7 | const ICCheck = (props: Props) => ( 8 | 16 | 20 | {props.isLinear && ( 21 | 22 | 30 | 31 | 32 | 33 | 34 | )} 35 | 36 | ); 37 | export default ICCheck; 38 | -------------------------------------------------------------------------------- /src/components/ICSettings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const IcSettings = (props: React.SVGProps) => ( 4 | 12 | 13 | 14 | ); 15 | export default IcSettings; 16 | -------------------------------------------------------------------------------- /src/components/ICTrash.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const ICTrash = (props: React.SVGProps) => ( 4 | 12 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | export default ICTrash; 27 | -------------------------------------------------------------------------------- /src/components/IcClose.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const ICClose = (props: React.SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | 19 | export default ICClose; 20 | -------------------------------------------------------------------------------- /src/components/IcInstagram.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const IcInstagram = (props: React.SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | export default IcInstagram; 19 | -------------------------------------------------------------------------------- /src/components/IcLinkedIn.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const IcLinkedIn = (props: React.SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | 19 | export default IcLinkedIn; 20 | -------------------------------------------------------------------------------- /src/components/IcPlus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface Props extends React.SVGProps { 4 | isLinear?: boolean; 5 | } 6 | 7 | const IcPlus = (props: Props) => ( 8 | 16 | 20 | {props?.isLinear && ( 21 | 22 | 30 | 31 | 32 | 33 | 34 | )} 35 | 36 | ); 37 | export default IcPlus; 38 | -------------------------------------------------------------------------------- /src/components/IcTwitter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const ICTwitter = (props: React.SVGProps) => ( 4 | 12 | 16 | 17 | ); 18 | export default ICTwitter; 19 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Loading = (props: React.SVGProps) => ( 4 | 12 | 13 | 23 | 33 | 34 | 35 | 45 | 55 | 56 | 57 | 67 | 77 | 78 | 79 | ); 80 | 81 | export default Loading; 82 | -------------------------------------------------------------------------------- /src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Logo = (props: React.SVGProps) => ( 4 | 12 | 16 | 21 | 27 | 31 | 32 | ); 33 | export default Logo; 34 | -------------------------------------------------------------------------------- /src/components/PromptsList/PrompsForm.styled.ts: -------------------------------------------------------------------------------- 1 | import styled, { css } from "styled-components"; 2 | import TextareaAutosize from "react-textarea-autosize"; 3 | 4 | export const Wrapper = styled.div``; 5 | 6 | export const InputWrapper = styled.div` 7 | width: 100%; 8 | position: relative; 9 | 10 | span { 11 | display: none; 12 | } 13 | 14 | &:focus-within span { 15 | display: block; 16 | position: absolute; 17 | top: 10px; 18 | right: 10px; 19 | opacity: 0.5; 20 | } 21 | 22 | .submit-btn { 23 | position: absolute; 24 | bottom: 15px; 25 | right: 15px; 26 | cursor: pointer; 27 | opacity: 0.6; 28 | transform: scale(1); 29 | transition: all 0.1s ease; 30 | 31 | &:hover { 32 | transform: scale(1.2); 33 | } 34 | } 35 | `; 36 | 37 | export const Input = styled(TextareaAutosize)<{ 38 | isEdit?: boolean; 39 | $error?: boolean; 40 | }>` 41 | width: 100%; 42 | height: auto; 43 | max-height: 110px; 44 | resize: none; 45 | border: 1px solid 46 | ${({ $error }) => ($error ? "#ff0000" : "rgba(0, 0, 0, 0.1)")}; 47 | border-radius: ${({ isEdit }) => (isEdit ? "0" : "16px")}; 48 | padding: 24px; 49 | box-sizing: border-box; 50 | 51 | :focus { 52 | outline: none !important; 53 | border-color: rgba(0, 0, 0, 0.4); 54 | } 55 | `; 56 | 57 | export const List = styled.div` 58 | margin: 12px 0; 59 | padding: 0 24px 0 12px; 60 | `; 61 | 62 | export const Item = styled.div` 63 | margin: 4px 0; 64 | position: relative; 65 | 66 | .delete-btn { 67 | opacity: 0.2; 68 | position: absolute; 69 | right: -24px; 70 | top: 12px; 71 | cursor: pointer; 72 | transition: all 0.1s ease; 73 | } 74 | 75 | &:hover { 76 | .delete-btn { 77 | opacity: 1; 78 | } 79 | } 80 | `; 81 | 82 | export const Error = styled.div` 83 | margin: 6px 0; 84 | color: #ff0000; 85 | 86 | span { 87 | background: rgba(0, 0, 0, 0.1); 88 | border-radius: 3px; 89 | padding: 5px 7px; 90 | color: #000; 91 | } 92 | `; 93 | -------------------------------------------------------------------------------- /src/components/PromptsList/PrompsForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import ICCheck from "../ICCheck"; 4 | import ICPlus from "../IcPlus"; 5 | import ICTrash from "../ICTrash"; 6 | 7 | import * as Styled from "./PrompsForm.styled"; 8 | import { validate } from "./utils"; 9 | 10 | interface Props { 11 | onChange: (list: string[]) => void; 12 | items: string[]; 13 | } 14 | 15 | const PrompsForm: React.FC = ({ onChange, items = [] }) => { 16 | const handleAdd = (newItem: string) => { 17 | onChange(Array.from([newItem, ...items])); 18 | }; 19 | 20 | const handleEdit = (index: number) => (value: string) => { 21 | items[index] = value; 22 | onChange(Array.from(items)); 23 | }; 24 | 25 | const handleRemove = (index: number) => () => { 26 | onChange(Array.from(items.filter((_, i) => i !== index))); 27 | }; 28 | 29 | return ( 30 | 31 |