├── .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 |
40 |
41 | ### More Options
42 |
43 |
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 |
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 |
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 | ) => `
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 |
20 | ),
21 | [State.CheckboxSelected]: (
22 |
31 | ),
32 | [State.RadioSelected]: (
33 |
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 |
36 | );
37 | export default ICCheck;
38 |
--------------------------------------------------------------------------------
/src/components/ICSettings.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const IcSettings = (props: React.SVGProps) => (
4 |
14 | );
15 | export default IcSettings;
16 |
--------------------------------------------------------------------------------
/src/components/ICTrash.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const ICTrash = (props: React.SVGProps) => (
4 |
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 |
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 |
17 | );
18 | export default IcInstagram;
19 |
--------------------------------------------------------------------------------
/src/components/IcLinkedIn.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const IcLinkedIn = (props: React.SVGProps) => (
4 |
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 |
36 | );
37 | export default IcPlus;
38 |
--------------------------------------------------------------------------------
/src/components/IcTwitter.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const ICTwitter = (props: React.SVGProps) => (
4 |
17 | );
18 | export default ICTwitter;
19 |
--------------------------------------------------------------------------------
/src/components/Loading.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | const Loading = (props: React.SVGProps) => (
4 |
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 |
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 |
32 |
33 | {items.map((item, i) => (
34 |
35 |
41 | {i !== items.length - 1 && (
42 |
43 | )}
44 |
45 | ))}
46 |
47 |
48 | );
49 | };
50 |
51 | interface TextAreaProps {
52 | onSubmit: (value: string) => void;
53 | type: "add" | "edit";
54 | value?: string;
55 | }
56 |
57 | const TextArea: React.FC = ({ value, type, onSubmit }) => {
58 | const [text, setText] = React.useState(value || "");
59 | const [error, setError] = React.useState("");
60 |
61 | const handleSubmit = () => {
62 | const error = validate(text);
63 | if (error.length) {
64 | setError(error);
65 | setTimeout(() => {
66 | setError("");
67 | }, 3000);
68 | return;
69 | }
70 | onSubmit(text);
71 | if (type === "add") setText("");
72 | };
73 |
74 | const SubmitBtn = type === "add" ? ICPlus : ICCheck;
75 |
76 | const showSubmitBtn =
77 | (type === "add" && text.length > 0) ||
78 | (type == "edit" && text.length > 0 && text !== value);
79 |
80 | return (
81 | <>
82 |
83 | {
89 | setText(e.target.value);
90 | setError("");
91 | }}
92 | isEdit={type === "edit"}
93 | $error={error.length > 0}
94 | />
95 | {text.length}
96 | {showSubmitBtn && (
97 |
98 | )}
99 |
100 |
101 | {error && }
102 | >
103 | );
104 | };
105 |
106 | export default PrompsForm;
107 |
--------------------------------------------------------------------------------
/src/components/PromptsList/utils.ts:
--------------------------------------------------------------------------------
1 | export const validate = (value: string) => {
2 | if (value.length <= 30) return "Prompt should be atleast 30 characters.";
3 |
4 | if (!value.includes("{postContent}"))
5 | return " You should include {postContent} on your prompt!";
6 |
7 | return "";
8 | };
9 |
--------------------------------------------------------------------------------
/src/components/Section/Section.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Wrapper = styled.section`
4 | width: 100%;
5 | margin: 24px 0;
6 | `;
7 |
8 | export const Head = styled.div`
9 | display: block;
10 | padding-bottom: 12px;
11 | border-bottom: 1px solid rgba(0, 0, 0, 0.2);
12 |
13 | h1 {
14 | margin: 0;
15 | }
16 |
17 | p {
18 | color: rgba(0, 0, 0, 0.7);
19 | margin: 0;
20 | }
21 | `;
22 |
23 | export const Body = styled.div`
24 | margin: 16px 0;
25 | width: 100%;
26 | `;
27 |
--------------------------------------------------------------------------------
/src/components/Section/Section.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import * as Styled from "./Section.styled";
4 |
5 | export interface Props {
6 | title: string;
7 | desc?: string;
8 | }
9 |
10 | const Section: React.FC = ({ title, desc, children }) => {
11 | return (
12 |
13 |
14 | {title}
15 | {desc && {desc}
}
16 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default Section;
23 |
--------------------------------------------------------------------------------
/src/components/Section/index.ts:
--------------------------------------------------------------------------------
1 | import Section, { Props } from "./Section";
2 |
3 | export default Section;
4 | export { Props };
5 |
--------------------------------------------------------------------------------
/src/components/Tab/Tab.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Wrapper = styled.div`
4 | width: 100%;
5 | display: flex;
6 | flex-direction: column;
7 | overflow: hidden;
8 | `;
9 |
10 | export const Tabs = styled.div`
11 | display: flex;
12 | flex-direction: row;
13 | `;
14 |
15 | export const TabItem = styled.div<{ active?: boolean }>`
16 | padding: 16px 32px;
17 | border: 1px solid rgba(0, 0, 0, ${({ active }) => (active ? "0.4" : "0.1")});
18 | cursor: pointer;
19 | position: relative;
20 |
21 | .title {
22 | display: flex;
23 | align-items: center;
24 | gap: 6px;
25 | font-weight: bold;
26 | transition: all 0.2s ease;
27 | transform: ${({ active }) =>
28 | active ? "translateY(-8%)" : "translateY(20%)"};
29 | opacity: ${({ active }) => (active ? "1" : "0.5")};
30 | }
31 |
32 | &:hover {
33 | .title {
34 | transform: translateY(-8%);
35 | opacity: 1;
36 | }
37 | }
38 |
39 | .line {
40 | width: 100%;
41 | height: 10px;
42 | position: absolute;
43 | bottom: 0;
44 | left: 0;
45 | background: linear-gradient(133.43deg, #4d0089 0%, #235f19 102.89%);
46 | }
47 |
48 | &:first-of-type {
49 | border-top-left-radius: 16px;
50 | }
51 |
52 | &:last-of-type {
53 | border-top-right-radius: 16px;
54 | }
55 | `;
56 |
57 | export const TabIcon = styled.div``;
58 |
59 | export const Body = styled.div``;
60 |
--------------------------------------------------------------------------------
/src/components/Tab/Tab.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import * as Styled from "./Tab.styled";
4 |
5 | export type TabItem = {
6 | title: string;
7 | comp: JSX.Element;
8 | icon?: JSX.Element;
9 | };
10 |
11 | interface Props {
12 | tabs: TabItem[];
13 | initialTab?: number;
14 | }
15 |
16 | const Tab: React.FC = ({ tabs, initialTab }) => {
17 | const [activeTab, setActiveTab] = React.useState(
18 | initialTab && initialTab > 0 ? initialTab : 0
19 | );
20 |
21 | const handleTabChange = (index: number) => {
22 | setActiveTab(index);
23 | };
24 |
25 | return (
26 |
27 |
28 | {tabs.map((tab, i) => {
29 | const isActive = activeTab === i;
30 |
31 | return (
32 | handleTabChange(i)}
35 | active={isActive}
36 | >
37 |
38 | {tab?.icon && {tab.icon}}
39 | {tab.title}
40 |
41 | {isActive && }
42 |
43 | );
44 | })}
45 |
46 | {tabs[activeTab].comp}
47 |
48 | );
49 | };
50 |
51 | export default Tab;
52 |
--------------------------------------------------------------------------------
/src/components/Tab/index.ts:
--------------------------------------------------------------------------------
1 | import Tab, { TabItem } from "./Tab";
2 |
3 | export default Tab;
4 | export { TabItem };
5 |
--------------------------------------------------------------------------------
/src/components/TagInput/TagsInput.styled.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const Wrapper = styled.div`
4 | display: flex;
5 | width: 100%;
6 | min-height: 48px;
7 | padding: 0 8px;
8 | border: 1px solid rgb(214, 216, 218);
9 | border-radius: 6px;
10 | box-sizing: border-box;
11 |
12 | &:focus-within {
13 | border: 1px solid #0052cc;
14 | }
15 |
16 | input {
17 | border: none;
18 | font-size: 14px;
19 | flex-grow: 1;
20 |
21 | &:focus {
22 | outline: transparent;
23 | }
24 | }
25 | `;
26 |
27 | export const Tags = styled.div`
28 | width: 100%;
29 | display: inline-flex;
30 | flex-wrap: wrap;
31 | padding: 0;
32 | gap: 4px;
33 | `;
34 |
35 | export const TagItem = styled.div`
36 | width: auto;
37 | height: 32px;
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | color: #fff;
42 | padding: 0 8px;
43 | font-size: 12px;
44 | list-style: none;
45 | border-radius: 6px;
46 | background: linear-gradient(133.43deg, #4d0089 0%, #235f19 102.89%);
47 | margin: 8px 0;
48 |
49 | svg {
50 | opacity: 0.6;
51 | margin-left: 6px;
52 | cursor: pointer;
53 | }
54 |
55 | &:hover {
56 | svg {
57 | opacity: 1;
58 | }
59 | }
60 | `;
61 |
--------------------------------------------------------------------------------
/src/components/TagInput/TagsInput.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import ICClose from "../IcClose";
3 |
4 | import * as Styled from "./TagsInput.styled";
5 |
6 | interface Props {
7 | tags: string[];
8 | onChange: (value: string[]) => void;
9 | placeholder?: string;
10 | allowDuplicates?: boolean;
11 | }
12 |
13 | const TagsInput: React.FC = ({
14 | tags,
15 | onChange,
16 | placeholder,
17 | allowDuplicates,
18 | }) => {
19 | const [inputText, setInputText] = React.useState("");
20 |
21 | const removeTags = (indexToRemove: number) => {
22 | onChange([...tags.filter((_, i) => i !== indexToRemove)]);
23 | };
24 |
25 | const addTags = (value: string) => {
26 | let newTags = [...tags, value];
27 | if (!allowDuplicates) {
28 | newTags = [...tags.filter((tag) => tag !== value), value];
29 | }
30 | onChange(newTags);
31 | setInputText("");
32 | };
33 |
34 | return (
35 |
36 |
37 | {tags.map((tag, i) => (
38 |
39 | {tag}
40 | removeTags(i)} />
41 |
42 | ))}
43 |
44 | setInputText(e.target.value)}
48 | onKeyUp={(event) =>
49 | event.key === "Enter" ? addTags(inputText) : null
50 | }
51 | placeholder={placeholder}
52 | />
53 |
54 |
55 | );
56 | };
57 |
58 | export default TagsInput;
59 |
--------------------------------------------------------------------------------
/src/hooks/useChromeStorage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { StorageKeys } from "../utils/config";
4 |
5 | export default (key: StorageKeys, defaultValue: T) => {
6 | const [loading, setLoading] = React.useState(true);
7 | const [state, setState] = React.useState(defaultValue);
8 | const isInitialized = React.useRef(false);
9 |
10 | React.useEffect(() => {
11 | chrome.storage["local"]
12 | .get(key)
13 | .then((res) => {
14 | if (key in res) {
15 | setState(res[key]);
16 | } else {
17 | setState(defaultValue);
18 | }
19 | setLoading(false);
20 | })
21 | .catch(() => {
22 | console.warn(`useChromeStorage get error: ${key}`);
23 | setState(defaultValue);
24 | })
25 | .finally(() => {
26 | isInitialized.current = true;
27 | });
28 | // eslint-disable-next-line react-hooks/exhaustive-deps
29 | }, []);
30 |
31 | React.useEffect(() => {
32 | if (!isInitialized.current) return;
33 | chrome?.storage?.local?.set({ [key]: state }).catch(() => {
34 | console.warn(`useChromeStorage set error: ${key}`);
35 | });
36 | }, [key, state]);
37 |
38 | return [state, setState, { loading }] as const;
39 | };
40 |
--------------------------------------------------------------------------------
/src/lib/instagram.ts:
--------------------------------------------------------------------------------
1 | import ChatGPTIcon from "../components/ChatGPTIcon";
2 |
3 | import { CHATGPT_BTN_ID, Domains, ERROR_MESSAGE } from "../utils/constants";
4 | import {
5 | getComment,
6 | delay,
7 | imitateKeyInput,
8 | showAPIKeyError,
9 | } from "../utils/shared";
10 | import getConfig from "../utils/config";
11 | import { notyf } from "../chrome/content_script";
12 |
13 | type PostType = "FEED" | "REELS";
14 |
15 | export const injector = () => {
16 | const isFromFeed = !window.location.pathname.includes("reels/videos/");
17 | document
18 | .querySelectorAll(
19 | isFromFeed
20 | ? `article[role="presentation"] form[method="POST"]`
21 | : `div[role="dialog"] form[method="POST"]`
22 | )
23 | .forEach((el) => {
24 | if (el.getAttribute("hasChatGPT") === "true") return;
25 | el.setAttribute("hasChatGPT", "true");
26 |
27 | const icon = el?.querySelector("svg");
28 | const iconColor = window.getComputedStyle(icon!)?.color || "#8e8e8e";
29 |
30 | if (isFromFeed) {
31 | const chatGPTBtn = createChatGPTBtn(
32 | "instafeed-chatgpt-btn",
33 | iconColor,
34 | "FEED"
35 | );
36 | const emoji = el.querySelector(
37 | "div:first-of-type button:first-of-type"
38 | );
39 | emoji?.parentElement?.setAttribute("force-flex", "true");
40 | insertAfter(emoji!, chatGPTBtn);
41 | } else {
42 | const chatGPTBtn = createChatGPTBtn(
43 | "instareels-chatgpt-btn",
44 | iconColor,
45 | "REELS"
46 | );
47 | const emojiWrapper = el.querySelector(
48 | "div:first-of-type > div:first-of-type"
49 | );
50 | emojiWrapper?.classList.add("insta-with-chatgpt");
51 | emojiWrapper?.prepend(chatGPTBtn);
52 | }
53 | });
54 | };
55 |
56 | export const handler = async () => {
57 | const handleClick = async (e: MouseEvent) => {
58 | {
59 | const target = e.target as Element;
60 | const btn = target?.closest(`#${CHATGPT_BTN_ID}`);
61 | if (!btn) return;
62 |
63 | const config = await getConfig();
64 | if (!config?.["social-comments-openapi-key"])
65 | return showAPIKeyError(Domains.Twitter);
66 |
67 | notyf?.dismissAll();
68 |
69 | const isFromFeed = !window.location.pathname.includes("reels");
70 | const wrapper = target?.closest(
71 | isFromFeed ? "article" : `div[role="dialog"]`
72 | );
73 | if (!wrapper) return;
74 |
75 | const commentInputEl = wrapper.querySelector(
76 | `form[method="POST"] textarea`
77 | ) as HTMLTextAreaElement;
78 |
79 | if (commentInputEl.value) {
80 | imitateKeyInput(commentInputEl, "");
81 | handleClick(e);
82 | return;
83 | }
84 |
85 | imitateKeyInput(commentInputEl, "");
86 |
87 | commentInputEl.setAttribute("placeholder", "ChatGPT is thinking...");
88 | commentInputEl.setAttribute("disabled", "true");
89 |
90 | btn.setAttribute("disabled", "true");
91 | btn.setAttribute("loading", "true");
92 |
93 | wrapper.querySelectorAll("div");
94 |
95 | let body = "";
96 | if (isFromFeed) {
97 | body = await getFeedContent(wrapper);
98 | } else {
99 | body = await getReelContent();
100 | }
101 |
102 | const comment = await getComment(config, Domains.Instagram, body);
103 | if (comment.length) {
104 | imitateKeyInput(commentInputEl, comment);
105 | } else {
106 | commentInputEl.setAttribute("placeholder", ERROR_MESSAGE);
107 | await delay(3000);
108 | }
109 |
110 | commentInputEl.setAttribute("placeholder", "Add a comment..");
111 | commentInputEl.removeAttribute("disabled");
112 |
113 | btn.removeAttribute("disabled");
114 | btn.removeAttribute("loading");
115 | }
116 | };
117 | document.body.addEventListener("click", handleClick);
118 | };
119 |
120 | const getInstagramContent = async (postId: string) => {
121 | const resp = await fetch(`https://www.instagram.com/p/${postId}`);
122 | if (!resp.ok) return "";
123 |
124 | const instagramResp = await resp.text();
125 |
126 | let content = decodeEntities(
127 | [
128 | ...instagramResp?.matchAll(
129 | /]+property="og:title"[^>]+content="([^"]+)" \/>/g
130 | ),
131 | ]?.[0]?.[1] || ""
132 | );
133 |
134 | content =
135 | [...content.matchAll(/[^>]+ on Instagram: "([^>]+)"/g)]?.[0]?.[1] || "";
136 |
137 | return content;
138 | };
139 |
140 | const getFeedContent = async (wrapper: Element): Promise => {
141 | const linksMatches = [...wrapper.innerHTML.matchAll(/href="\/p\/(.*?)\/"/g)];
142 | const postId =
143 | linksMatches.filter((link) => !link?.[1]?.includes("liked_by"))?.[0]?.[1] ||
144 | "";
145 |
146 | return getInstagramContent(postId);
147 | };
148 |
149 | const getReelContent = async (): Promise => {
150 | const pathName = window.location.pathname;
151 | const postId =
152 | [...pathName?.matchAll(/reels\/videos\/(.*)\//g)]?.[0]?.[1] || "";
153 |
154 | return getInstagramContent(postId);
155 | };
156 |
157 | const decodeEntities = (str: string): string => {
158 | const txt = document.createElement("textarea");
159 | txt.innerHTML = str;
160 |
161 | return txt.value;
162 | };
163 |
164 | const insertAfter = (referenceNode: Element, newNode: Element) => {
165 | referenceNode?.parentNode?.insertBefore(newNode, referenceNode.nextSibling);
166 | };
167 |
168 | const createChatGPTBtn = (
169 | className: string,
170 | color: string,
171 | postType: PostType
172 | ) => {
173 | const chatGPTBtn = document.createElement("div");
174 | chatGPTBtn.setAttribute("class", className);
175 | chatGPTBtn.setAttribute("post-type", postType);
176 | chatGPTBtn.innerHTML = ChatGPTIcon(22, color, CHATGPT_BTN_ID);
177 |
178 | return chatGPTBtn;
179 | };
180 |
--------------------------------------------------------------------------------
/src/lib/linkedin.ts:
--------------------------------------------------------------------------------
1 | import ChatGPTIcon from "../components/ChatGPTIcon";
2 |
3 | import { CHATGPT_BTN_ID, Domains, ERROR_MESSAGE } from "../utils/constants";
4 | import { getComment, delay, showAPIKeyError } from "../utils/shared";
5 | import getConfig from "../utils/config";
6 | import { notyf } from "../chrome/content_script";
7 |
8 | export const injector = () => {
9 | document
10 | .querySelectorAll(
11 | ".comments-comment-texteditor > .display-flex > .display-flex"
12 | )
13 | .forEach((el) => {
14 | if (el.getAttribute("hasChatGPT") === "true") return;
15 | el.setAttribute("hasChatGPT", "true");
16 |
17 | const chatGPTBtn = document.createElement("button");
18 | chatGPTBtn.setAttribute("type", "button");
19 | chatGPTBtn.setAttribute("id", CHATGPT_BTN_ID);
20 | chatGPTBtn.setAttribute(
21 | "class",
22 | "artdeco-button--tertiary artdeco-button artdeco-button--circle artdeco-button--muted"
23 | );
24 | chatGPTBtn.innerHTML = ChatGPTIcon(20, "#666666");
25 | el.prepend(chatGPTBtn);
26 | });
27 | };
28 |
29 | export const handler = async () => {
30 | document.body.addEventListener("click", async (e) => {
31 | const target = e.target as Element;
32 | const btn = target?.closest(`#${CHATGPT_BTN_ID}`);
33 | if (!btn) return;
34 |
35 | const config = await getConfig();
36 | if (!config["social-comments-openapi-key"])
37 | return showAPIKeyError(Domains.LinkedIn);
38 |
39 | notyf?.dismissAll();
40 |
41 | const wrapper = target?.closest(".feed-shared-update-v2");
42 | if (!wrapper) return;
43 |
44 | const commentInputEl = wrapper.querySelector(".ql-editor")!;
45 | commentInputEl.innerHTML = "";
46 |
47 | commentInputEl.setAttribute("data-placeholder", "ChatGPT is thinking...");
48 | btn.setAttribute("disabled", "true");
49 | btn.setAttribute("loading", "true");
50 |
51 | const content =
52 | (
53 | wrapper.querySelector(
54 | '.feed-shared-inline-show-more-text span[dir="ltr"]'
55 | ) as HTMLElement
56 | )?.innerText || "";
57 |
58 | const comment = await getComment(config, Domains.LinkedIn, content);
59 | if (comment.length) {
60 | commentInputEl.innerHTML = comment;
61 | } else {
62 | commentInputEl.setAttribute("data-placeholder", ERROR_MESSAGE);
63 | await delay(3000);
64 | }
65 |
66 | commentInputEl.setAttribute("data-placeholder", "Add a comment..");
67 | btn.removeAttribute("disabled");
68 | btn.removeAttribute("loading");
69 | });
70 | };
71 |
--------------------------------------------------------------------------------
/src/lib/styles.ts:
--------------------------------------------------------------------------------
1 | import { ANNOUNCEMENT_WRAPPER } from "../utils/announcements";
2 | import { CHATGPT_BTN_ID, TOAST_CLASSNAME } from "../utils/constants";
3 |
4 | export default () => {
5 | const styles = ``;
135 |
136 | document.head.insertAdjacentHTML("beforeend", styles);
137 | };
138 |
139 | const notyfCSS = `
140 | @-webkit-keyframes notyf-fadeinup{0%{opacity:0;transform:translateY(25%)}to{opacity:1;transform:translateY(0)}}@keyframes notyf-fadeinup{0%{opacity:0;transform:translateY(25%)}to{opacity:1;transform:translateY(0)}}@-webkit-keyframes notyf-fadeinleft{0%{opacity:0;transform:translateX(25%)}to{opacity:1;transform:translateX(0)}}@keyframes notyf-fadeinleft{0%{opacity:0;transform:translateX(25%)}to{opacity:1;transform:translateX(0)}}@-webkit-keyframes notyf-fadeoutright{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(25%)}}@keyframes notyf-fadeoutright{0%{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(25%)}}@-webkit-keyframes notyf-fadeoutdown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(25%)}}@keyframes notyf-fadeoutdown{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(25%)}}@-webkit-keyframes ripple{0%{transform:scale(0) translateY(-45%) translateX(13%)}to{transform:scale(1) translateY(-45%) translateX(13%)}}@keyframes ripple{0%{transform:scale(0) translateY(-45%) translateX(13%)}to{transform:scale(1) translateY(-45%) translateX(13%)}}.notyf{position:fixed;top:0;left:0;height:100%;width:100%;color:#fff;z-index:9999;display:flex;flex-direction:column;align-items:flex-end;justify-content:flex-end;pointer-events:none;box-sizing:border-box;padding:20px}.notyf__icon--error,.notyf__icon--success{height:21px;width:21px;background:#fff;border-radius:50%;display:block;margin:0 auto;position:relative}.notyf__icon--error:after,.notyf__icon--error:before{content:"";background:currentColor;display:block;position:absolute;width:3px;border-radius:3px;left:9px;height:12px;top:5px}.notyf__icon--error:after{transform:rotate(-45deg)}.notyf__icon--error:before{transform:rotate(45deg)}.notyf__icon--success:after,.notyf__icon--success:before{content:"";background:currentColor;display:block;position:absolute;width:3px;border-radius:3px}.notyf__icon--success:after{height:6px;transform:rotate(-45deg);top:9px;left:6px}.notyf__icon--success:before{height:11px;transform:rotate(45deg);top:5px;left:10px}.notyf__toast{display:block;overflow:hidden;pointer-events:auto;-webkit-animation:notyf-fadeinup .3s ease-in forwards;animation:notyf-fadeinup .3s ease-in forwards;box-shadow:0 3px 7px 0 rgba(0,0,0,.25);position:relative;padding:0 15px;border-radius:2px;max-width:500px;transform:translateY(25%);box-sizing:border-box;flex-shrink:0}.notyf__toast--disappear{transform:translateY(0);-webkit-animation:notyf-fadeoutdown .3s forwards;animation:notyf-fadeoutdown .3s forwards;-webkit-animation-delay:.25s;animation-delay:.25s}.notyf__toast--disappear .notyf__icon,.notyf__toast--disappear .notyf__message{-webkit-animation:notyf-fadeoutdown .3s forwards;animation:notyf-fadeoutdown .3s forwards;opacity:1;transform:translateY(0)}.notyf__toast--disappear .notyf__dismiss{-webkit-animation:notyf-fadeoutright .3s forwards;animation:notyf-fadeoutright .3s forwards;opacity:1;transform:translateX(0)}.notyf__toast--disappear .notyf__message{-webkit-animation-delay:.05s;animation-delay:.05s}.notyf__toast--upper{margin-bottom:20px}.notyf__toast--lower{margin-top:20px}.notyf__toast--dismissible .notyf__wrapper{padding-right:30px}.notyf__ripple{height:400px;width:400px;position:absolute;transform-origin:bottom right;right:0;top:0;border-radius:50%;transform:scale(0) translateY(-51%) translateX(13%);z-index:5;-webkit-animation:ripple .4s ease-out forwards;animation:ripple .4s ease-out forwards}.notyf__wrapper{display:flex;padding-top:17px;padding-bottom:17px;padding-right:15px;border-radius:3px;position:relative;z-index:10}.notyf__icon{width:22px;text-align:center;font-size:1.3em;opacity:0;-webkit-animation:notyf-fadeinup .3s forwards;animation:notyf-fadeinup .3s forwards;-webkit-animation-delay:.3s;animation-delay:.3s;margin-right:13px;margin-top:3px}.notyf__dismiss{position:absolute;top:0;right:0;height:100%;width:26px;margin-right:-15px;-webkit-animation:notyf-fadeinleft .3s forwards;animation:notyf-fadeinleft .3s forwards;-webkit-animation-delay:.35s;animation-delay:.35s;opacity:0}.notyf__dismiss-btn{background-color:rgba(0,0,0,.25);border:none;cursor:pointer;transition:opacity .2s ease,background-color .2s ease;outline:none;opacity:.35;height:100%;width:100%}.notyf__dismiss-btn:after,.notyf__dismiss-btn:before{content:"";background:#fff;height:12px;width:2px;border-radius:3px;position:absolute;left:calc(50% - 1px);top:calc(50% - 5px)}.notyf__dismiss-btn:after{transform:rotate(-45deg)}.notyf__dismiss-btn:before{transform:rotate(45deg)}.notyf__dismiss-btn:hover{opacity:.7;background-color:rgba(0,0,0,.15)}.notyf__dismiss-btn:active{opacity:.8}.notyf__message{vertical-align:middle;position:relative;opacity:0;-webkit-animation:notyf-fadeinup .3s forwards;animation:notyf-fadeinup .3s forwards;-webkit-animation-delay:.25s;animation-delay:.25s;line-height:1.5em}@media only screen and (max-width:480px){.notyf{padding:0}.notyf__ripple{height:600px;width:600px;-webkit-animation-duration:.5s;animation-duration:.5s}.notyf__toast{max-width:none;border-radius:0;box-shadow:0 -2px 7px 0 rgba(0,0,0,.13);width:100%}.notyf__dismiss{width:56px}}
141 | `;
142 |
--------------------------------------------------------------------------------
/src/lib/twitter.ts:
--------------------------------------------------------------------------------
1 | import ChatGPTIcon from "../components/ChatGPTIcon";
2 |
3 | import { CHATGPT_BTN_ID, Domains, ERROR_MESSAGE } from "../utils/constants";
4 | import {
5 | getComment,
6 | delay,
7 | closestSibling,
8 | showAPIKeyError,
9 | } from "../utils/shared";
10 | import getConfig from "../utils/config";
11 | import { notyf } from "../chrome/content_script";
12 |
13 | export const injector = () => {
14 | document
15 | .querySelectorAll(`[aria-label="Add photos or video"]`)
16 | .forEach((el) => {
17 | const pathname = window.location.pathname;
18 | if (pathname === "/" || pathname === "/home") return;
19 |
20 | if (el.getAttribute("hasChatGPT") === "true") return;
21 | el.setAttribute("hasChatGPT", "true");
22 |
23 | const icon = el?.querySelector("svg");
24 | const iconColor = window.getComputedStyle(icon!)?.color || "#8e8e8e";
25 |
26 | el?.insertAdjacentHTML(
27 | "beforebegin",
28 | ``
32 | );
33 | });
34 | };
35 |
36 | export const handler = async () => {
37 | document.body.addEventListener("click", async (e) => {
38 | const target = e.target as Element;
39 | const btn = target?.closest(`#${CHATGPT_BTN_ID}`);
40 | if (!btn) return;
41 |
42 | const config = await getConfig();
43 | if (!config?.["social-comments-openapi-key"])
44 | return showAPIKeyError(Domains.Twitter);
45 |
46 | notyf?.dismissAll();
47 |
48 | const commentInputWrapper = closestSibling(
49 | btn,
50 | `[class="DraftEditor-root"]`
51 | );
52 | if (!commentInputWrapper) return;
53 | setTweetText(commentInputWrapper, "ChatGPT is thinking...");
54 |
55 | btn.setAttribute("disabled", "true");
56 | btn.setAttribute("loading", "true");
57 |
58 | const content =
59 | closestSibling(btn, `[data-testid="tweetText"]`)?.textContent || "";
60 |
61 | const comment = await getComment(config, Domains.Twitter, content);
62 | if (comment.length) {
63 | setTweetText(commentInputWrapper, comment);
64 | } else {
65 | await delay(1000);
66 | setTweetText(commentInputWrapper, ERROR_MESSAGE);
67 | }
68 |
69 | btn.setAttribute("disabled", "false");
70 | btn.setAttribute("loading", "false");
71 | });
72 | };
73 |
74 | const setTweetText = async (commentInputWrapper: Element, text: string) => {
75 | const editable = commentInputWrapper?.querySelector(`[contenteditable]`);
76 | editable?.addEventListener("selectAll", () => {
77 | document.execCommand("selectAll");
78 | });
79 |
80 | if (editable?.textContent?.length) {
81 | (editable as any)?.click();
82 | editable.dispatchEvent(new CustomEvent("selectAll"));
83 | await delay(500);
84 | }
85 |
86 | const data = new DataTransfer();
87 | data.setData("text/plain", text);
88 | editable?.dispatchEvent(
89 | new ClipboardEvent("paste", {
90 | bubbles: true,
91 | clipboardData: data,
92 | cancelable: true,
93 | })
94 | );
95 | };
96 |
--------------------------------------------------------------------------------
/src/utils/announcements.ts:
--------------------------------------------------------------------------------
1 | import { ANNOUNCEMENTS_API, Domains } from "./constants";
2 | import { generateAnnouncementId } from "./generators";
3 |
4 | export type Announcement = {
5 | id: string;
6 | message: string;
7 | title: string;
8 | version: number;
9 | };
10 |
11 | export const ANNOUNCEMENT_LIST_WRAPPER = "social-comments-gpt-alerts";
12 | export const ANNOUNCEMENT_WRAPPER = "social-comments-gpt-alert";
13 |
14 | export const handler = (domain: Domains) => {
15 | document.body.addEventListener("click", async (e) => {
16 | const target = e.target as Element;
17 | const btn = target?.closest(`.${ANNOUNCEMENT_WRAPPER} .close-btn`);
18 | if (!btn) return;
19 |
20 | target?.closest(`.${ANNOUNCEMENT_WRAPPER}`)?.remove();
21 |
22 | const announcementId = generateAnnouncementId(
23 | btn.getAttribute("announcement-id") || "",
24 | domain
25 | );
26 |
27 | chrome?.storage?.local?.set({ [announcementId]: true }).catch(() => {
28 | console.warn(`useChromeStorage set error: ${announcementId}`);
29 | });
30 | });
31 | };
32 |
33 | window.announcementsLoading = false;
34 | window.allAnnouncements = [];
35 | window.filteredAnnouncements = [];
36 | window.announcementsFinishedLoading = false;
37 |
38 | const getAnnouncementsList = () =>
39 | document.querySelector(`.${ANNOUNCEMENT_LIST_WRAPPER}`);
40 |
41 | export const injector = (domain: Domains) => {
42 | (async () => {
43 | if (
44 | window.announcementsLoading ||
45 | // Finished loading without announcements
46 | (window.announcementsFinishedLoading &&
47 | !window.filteredAnnouncements.length) ||
48 | // Finished loading with announcements
49 | (window.announcementsFinishedLoading &&
50 | window.filteredAnnouncements.length &&
51 | getAnnouncementsList() !== null)
52 | )
53 | return;
54 |
55 | window.announcementsLoading = true;
56 | const manifestData = chrome.runtime.getManifest();
57 | const currentVersion = manifestData.version;
58 |
59 | const resp = await fetch(ANNOUNCEMENTS_API + `?version=${currentVersion}`);
60 |
61 | window.allAnnouncements = await resp.json();
62 | window.filteredAnnouncements = await asyncFilter(
63 | window.allAnnouncements,
64 | async (a) => {
65 | const isSeen = await isAnnouncementSeen(
66 | generateAnnouncementId(a.id, domain)
67 | );
68 | return !isSeen;
69 | }
70 | );
71 |
72 | const content = `${window.filteredAnnouncements
73 | .map(
74 | (a) =>
75 | `
76 |
77 |
${a.title}
78 |
${a.message}
79 |
`
80 | )
81 | .join("")}
`;
82 |
83 | switch (domain) {
84 | case Domains.LinkedIn:
85 | document
86 | .querySelector("main > div:first-of-type")
87 | ?.insertAdjacentHTML("afterend", content);
88 | break;
89 | case Domains.Instagram:
90 | document
91 | .querySelector(`section > div > div:first-of-type`)
92 | ?.insertAdjacentHTML("afterend", content);
93 | break;
94 | case Domains.Twitter:
95 | document
96 | .querySelector(`nav[aria-live="polite"]`)
97 | ?.insertAdjacentHTML("afterend", content);
98 | break;
99 | }
100 |
101 | window.announcementsFinishedLoading = true;
102 | window.announcementsLoading = false;
103 | })();
104 | };
105 |
106 | const isAnnouncementSeen = (announcementId: string): Promise => {
107 | return new Promise((resolve, reject) =>
108 | chrome?.storage?.local?.get([announcementId], (result) => {
109 | resolve(`${announcementId}` in result);
110 | })
111 | );
112 | };
113 |
114 | const asyncFilter = async (
115 | arr: T[],
116 | predicate: (v: T) => void
117 | ): Promise => {
118 | const results = await Promise.all(arr.map(predicate));
119 |
120 | return arr.filter((_v: any, index: any) => results[index]);
121 | };
122 |
--------------------------------------------------------------------------------
/src/utils/config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | COMMENTS_STYLE_OPT_DEFAULT,
3 | HASHTAG_OPT_DEFAULT,
4 | MODEL_OPT_DEFAULT,
5 | } from "./options";
6 | import {
7 | INSTAGRAM_PROMPTS,
8 | LINKED_IN_PROMPTS,
9 | TWITTER_PROMPTS,
10 | } from "./prompts";
11 |
12 | const OPTIONS = [
13 | "social-comments-openapi-key",
14 | "opt-model-type",
15 | "opt-comment-style",
16 | "opt-hashtag-option",
17 | "opt-excluded-words",
18 | "opt-insta-prompts",
19 | "opt-linkedin-prompts",
20 | "opt-twitter-prompts",
21 | ] as const;
22 |
23 | export type StorageKeys = (typeof OPTIONS)[number];
24 |
25 | export type Config = Record;
26 |
27 | export const DEFAULT_CONFIG: Config = {
28 | "social-comments-openapi-key": "",
29 | "opt-comment-style": COMMENTS_STYLE_OPT_DEFAULT,
30 | "opt-excluded-words": [],
31 | "opt-insta-prompts": INSTAGRAM_PROMPTS,
32 | "opt-linkedin-prompts": LINKED_IN_PROMPTS,
33 | "opt-twitter-prompts": TWITTER_PROMPTS,
34 | "opt-model-type": MODEL_OPT_DEFAULT,
35 | "opt-hashtag-option": HASHTAG_OPT_DEFAULT,
36 | };
37 |
38 | export default (): Promise =>
39 | new Promise((resolve, reject) =>
40 | chrome?.storage?.local?.get(OPTIONS, (result) => {
41 | const config = Object.keys(DEFAULT_CONFIG).reduce((a, c) => {
42 | return {
43 | ...a,
44 | // @ts-ignore
45 | [c]: result?.[c] || DEFAULT_CONFIG[c],
46 | };
47 | }, {});
48 |
49 | resolve(config as Config);
50 | })
51 | );
52 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const CHATGPT_BTN_ID = "chatgpt-btn";
2 |
3 | export const TOAST_CLASSNAME = "social-comments-toast";
4 |
5 | export const ERROR_MESSAGE =
6 | "ChatGPT failed. Follow the instructions & try again.";
7 |
8 | export enum Domains {
9 | LinkedIn = "linkedin.com",
10 | Instagram = "instagram.com",
11 | Twitter = "twitter.com",
12 | }
13 |
14 | export const ALLOWED_DOMAINS: Domains[] = [
15 | Domains.LinkedIn,
16 | Domains.Instagram,
17 | Domains.Twitter,
18 | ];
19 |
20 | export const ANNOUNCEMENTS_API =
21 | "https://social-comments-gpt-site.vercel.app/api/announcements";
22 |
23 | export const WELCOME_PAGE = "https://social-comments-gpt.com/extension/welcome";
24 | export const UNINSTALL_PAGE =
25 | "https://social-comments-gpt.com/extension/uninstall";
26 |
--------------------------------------------------------------------------------
/src/utils/generators.ts:
--------------------------------------------------------------------------------
1 | import { Config } from "./config";
2 | import { Domains } from "./constants";
3 | import { CommentsStyle } from "./options";
4 |
5 | export const createPrompt = (
6 | domain: Domains,
7 | config: Config,
8 | content: string
9 | ): string => {
10 | let prompts: string[] = [];
11 | switch (domain) {
12 | case Domains.Instagram:
13 | prompts = config["opt-insta-prompts"];
14 | break;
15 | case Domains.LinkedIn:
16 | prompts = config["opt-linkedin-prompts"];
17 | break;
18 | case Domains.Twitter:
19 | prompts = config["opt-twitter-prompts"];
20 | break;
21 | }
22 |
23 | let prompt = prompts?.[Math.floor(Math.random() * prompts.length)] || "";
24 | prompt = prompt.replace("{postContent}", content);
25 |
26 | if (
27 | config["opt-comment-style"] === CommentsStyle.ANYTHING &&
28 | !config["opt-excluded-words"].length
29 | ) {
30 | return prompt;
31 | }
32 |
33 | // Append options
34 | prompt += "\n\nAs a commentator on that post you must follow these rules:";
35 |
36 | if (config["opt-excluded-words"].length) {
37 | const words = config["opt-excluded-words"]
38 | .map((word: string) => `"${word}"`)
39 | .join(", ");
40 | prompt += ` please strictly don't mention any of the following words: ${words};`;
41 | }
42 |
43 | // Comment style option
44 | switch (config?.["opt-comment-style"]) {
45 | case CommentsStyle.PROFESSIONAL:
46 | prompt += " please respond in a very professional way.";
47 | break;
48 | case CommentsStyle.INFORMAL:
49 | prompt += " please respond in very informal way.";
50 | break;
51 | case CommentsStyle.DIRECT:
52 | prompt += " please respond in very direct way.";
53 | break;
54 | case CommentsStyle.FRIENDLY:
55 | prompt += " please respond in very friendly way.";
56 | break;
57 | case CommentsStyle.FUNNY:
58 | prompt += " please respond in a very funny way.";
59 | break;
60 | }
61 |
62 | if (prompt[prompt.length - 1] === ";") {
63 | prompt = prompt.substring(0, prompt.length - 1);
64 | prompt += ".";
65 | }
66 |
67 | return prompt;
68 | };
69 |
70 | export const generateAnnouncementId = (id: string, domain: Domains) =>
71 | `social-comments-gpt-announcement-${id}-${domain}`;
72 |
73 | type ErrorMessage = { title: string; message: string };
74 | export const generateErrorMessage = (code: number): ErrorMessage => {
75 | switch (code) {
76 | case 400:
77 | return {
78 | title: "OpenAI 400 Error: Content is too long",
79 | message: `The content of this posts seems to be too long. It exceed the limit of character allowed by OpenAI.`,
80 | };
81 | case 401:
82 | return {
83 | title: "OpenAI 401 Error: Invalid API Key",
84 | message: `Ensure that the API key you provided is correct, clear your browser cache, or generate a new one.`,
85 | };
86 | case 429:
87 | return {
88 | title: "OpenAI 429 Error: You are sending requests too quickly.",
89 | message: `Please slow down your requests. Read the Rate limit guide.`,
90 | };
91 | }
92 |
93 | return {
94 | title: "OpenAI Error: Issue on our servers.",
95 | message: `Retry your request after a brief wait and contact us if the issue persists. Check the status page.`,
96 | };
97 | };
98 |
--------------------------------------------------------------------------------
/src/utils/options.ts:
--------------------------------------------------------------------------------
1 | import { CheckboxOption } from "../components/Checkbox/Checkbox";
2 |
3 | // Model Options
4 | export enum ModelOptions {
5 | Davinci3 = "3",
6 | Davinci2 = "2",
7 | }
8 |
9 | export const MODEL_OPT_DEFAULT = ModelOptions.Davinci3;
10 | export const MODEL_OPTS: CheckboxOption[] = [
11 | { value: ModelOptions.Davinci2, label: "text-davinci-002" },
12 | { value: ModelOptions.Davinci3, label: "text-davinci-003" },
13 | ];
14 |
15 | // Comments Style Options
16 | export enum CommentsStyle {
17 | PROFESSIONAL = "professional",
18 | INFORMAL = "informal",
19 | DIRECT = "direct",
20 | FRIENDLY = "friendly",
21 | FUNNY = "funny",
22 | ANYTHING = "anything",
23 | }
24 |
25 | export const COMMENTS_STYLE_OPT_DEFAULT = CommentsStyle.ANYTHING;
26 | export const COMMENTS_STYLE_OPTS: CheckboxOption[] = [
27 | {
28 | value: CommentsStyle.ANYTHING,
29 | label: "Anything",
30 | },
31 | {
32 | value: CommentsStyle.INFORMAL,
33 | label: "Informal",
34 | },
35 | {
36 | value: CommentsStyle.PROFESSIONAL,
37 | label: "Professional",
38 | },
39 | {
40 | value: CommentsStyle.DIRECT,
41 | label: "Direct",
42 | },
43 | {
44 | value: CommentsStyle.FRIENDLY,
45 | label: "Friendly",
46 | },
47 | {
48 | value: CommentsStyle.FUNNY,
49 | label: "Funny",
50 | },
51 | ];
52 |
53 | // Hashtag Options
54 | export enum HashtagOptions {
55 | RANDOMLY = "randomly",
56 | NO = "no",
57 | }
58 |
59 | export const HASHTAG_OPT_DEFAULT = HashtagOptions.RANDOMLY;
60 | export const HASHTAG_OPTS: CheckboxOption[] = [
61 | { value: HashtagOptions.RANDOMLY, label: "Yes" },
62 | { value: HashtagOptions.NO, label: "No, please" },
63 | ];
64 |
--------------------------------------------------------------------------------
/src/utils/prompts.ts:
--------------------------------------------------------------------------------
1 | export const LINKED_IN_PROMPTS = [
2 | `I want you to act as a linkedin user that puts comments on different posts to engage other people. Here is the post "{postContent}"`,
3 | `I want you to act as a linkedin user that creates insightful comments on different posts to gain followers. Here is the content of the post "{postContent}"`,
4 | `I want you to act as a linkedin user that add comments on different posts to engage other people and have more impressions. Here is what the post about "{postContent}"`,
5 | ];
6 |
7 | export const INSTAGRAM_PROMPTS = [
8 | `I want you to act as an instagram user that puts comments on different posts to engage other people and have more followers. Here is the post "{postContent}"`,
9 | `I want you to act as an instagram influencer that creates insightful comments on different posts to gain followers and more profile views. Here is the content of the post "{postContent}"`,
10 | `I want you to act as an instagram user that add comments on different posts to engage other people and have more impressions, views, and followers. Here is what the post about "{postContent}"`,
11 | ];
12 |
13 | export const TWITTER_PROMPTS = [
14 | `I want you to act as an twitter user that replies on different tweets to engage other people and have more followers. Here is the post "{postContent}"`,
15 | `I want you to act as a twitter that creates insightful replies on different posts to gain followers and more profile views. Here is the content of the post "{postContent}"`,
16 | `I want you to act as a twitter user that add replies on different posts to engage other people and have more impressions, views, and followers. Here is what the post about "{postContent}"`,
17 | ];
18 |
--------------------------------------------------------------------------------
/src/utils/shared.ts:
--------------------------------------------------------------------------------
1 | import { notyf } from "../chrome/content_script";
2 | import { Config } from "./config";
3 | import { Domains, TOAST_CLASSNAME } from "./constants";
4 | import { HashtagOptions } from "./options";
5 | import { WELCOME_PAGE } from "./constants";
6 | import { createPrompt, generateErrorMessage } from "./generators";
7 |
8 | export const getComment = async (
9 | config: Config,
10 | domain: Domains,
11 | content: string
12 | ): Promise => {
13 | const body = {
14 | model: `text-davinci-00${config["opt-model-type"]}`,
15 | prompt: createPrompt(domain, config, content),
16 | temperature: 0,
17 | max_tokens: 3000,
18 | };
19 |
20 | const options = {
21 | method: "POST",
22 | headers: {
23 | "Content-Type": "application/json",
24 | Authorization: `Bearer ${config["social-comments-openapi-key"]}`,
25 | },
26 | body: JSON.stringify(body),
27 | };
28 |
29 | const resp = await fetch("https://api.openai.com/v1/completions", options);
30 | const chatGPTResp = await resp.json();
31 |
32 | if (!resp.ok) {
33 | const { title, message } = generateErrorMessage(resp.status);
34 | notyf?.error({
35 | duration: 0,
36 | dismissible: true,
37 | message: `${title}
${message}
See OpenAI API error guidance for more info.
`,
38 | className: `${TOAST_CLASSNAME} ${domain.replace(/([.]\w+)$/, "")}`,
39 | ripple: false,
40 | });
41 | return "";
42 | }
43 |
44 | let comment = (chatGPTResp?.["choices"]?.[0]?.["text"] || "")
45 | .replace(/^\s+|\s+$/g, "")
46 | .replace(/(^"|"$)/g, "");
47 |
48 | if (config["opt-hashtag-option"] === HashtagOptions.NO) {
49 | comment = comment.replace(/#\w+/g, "");
50 | }
51 |
52 | return comment;
53 | };
54 |
55 | export const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
56 |
57 | export const closestSibling = (
58 | element: Element,
59 | query: string
60 | ): Element | null => {
61 | const parent = element.parentElement;
62 | if (parent === null) return null;
63 | const sibling = parent.querySelector(query);
64 | if (sibling !== null) return sibling;
65 | return closestSibling(parent, query);
66 | };
67 |
68 | export const setInnerHTML = (element: Element, html: string) => {
69 | try {
70 | element.innerHTML = html;
71 | } catch {}
72 | };
73 |
74 | export const imitateKeyInput = (el: HTMLTextAreaElement, keyChar: string) => {
75 | const keyboardEventInit = {
76 | bubbles: false,
77 | cancelable: false,
78 | composed: false,
79 | key: "",
80 | code: "",
81 | location: 0,
82 | };
83 | el.dispatchEvent(new KeyboardEvent("keydown", keyboardEventInit));
84 | el.value = keyChar;
85 | el.dispatchEvent(new KeyboardEvent("keyup", keyboardEventInit));
86 | el.dispatchEvent(new Event("change", { bubbles: true }));
87 | };
88 |
89 | export const showAPIKeyError = (domain: Domains) => {
90 | notyf?.error({
91 | duration: 3000,
92 | dismissible: true,
93 | message: `API key is not set
Please set OpenAI API key in the popup.
See onboarding for more info.
`,
94 | className: `${TOAST_CLASSNAME} ${domain.replace(/([.]\w+)$/, "")}`,
95 | ripple: false,
96 | });
97 | };
98 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "commonjs",
5 | "target": "es6",
6 | "esModuleInterop": true,
7 | "sourceMap": false,
8 | "rootDir": "src",
9 | "outDir": "dist/js",
10 | "noEmitOnError": true,
11 | "jsx": "react",
12 | "typeRoots": ["node_modules/@types"]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | import { Announcement } from "./src/utils/announcements";
2 |
3 | declare module "*.module.css";
4 |
5 | declare global {
6 | interface Window {
7 | announcementsLoading: boolean;
8 | allAnnouncements: Announcement[];
9 | filteredAnnouncements: Announcement[];
10 | announcementsFinishedLoading: boolean;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/webpack/webpack.common.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 | const CopyPlugin = require("copy-webpack-plugin");
4 | const srcDir = path.join(__dirname, "..", "src");
5 |
6 | module.exports = {
7 | entry: {
8 | popup: path.join(srcDir, "chrome/popup.tsx"),
9 | options: path.join(srcDir, "chrome/options.tsx"),
10 | content_script: path.join(srcDir, "chrome/content_script.ts"),
11 | background: path.join(srcDir, "chrome/background.ts"),
12 | },
13 | output: {
14 | path: path.join(__dirname, "../dist/js"),
15 | filename: "[name].js",
16 | },
17 | optimization: {
18 | splitChunks: {
19 | name: "vendor",
20 | chunks(chunk) {
21 | return chunk.name !== "background";
22 | },
23 | },
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.tsx?$/,
29 | use: "ts-loader",
30 | exclude: /node_modules/,
31 | },
32 | {
33 | test: /\.(css)$/,
34 | use: ["style-loader", "css-loader"],
35 | },
36 | ],
37 | },
38 | resolve: {
39 | extensions: [".ts", ".tsx", ".js"],
40 | },
41 | plugins: [
42 | new CopyPlugin({
43 | patterns: [{ from: ".", to: "../", context: "public" }],
44 | options: {},
45 | }),
46 | ],
47 | };
48 |
--------------------------------------------------------------------------------
/webpack/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | devtool: 'inline-source-map',
6 | mode: 'development'
7 | });
--------------------------------------------------------------------------------
/webpack/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 |
4 | module.exports = merge(common, {
5 | mode: 'production'
6 | });
--------------------------------------------------------------------------------