├── .babelrc ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── assets │ └── img │ │ ├── bubble-arrow.png │ │ ├── fox-transparent.png │ │ ├── fox.png │ │ ├── icon-128.png │ │ ├── icon-34.png │ │ ├── spinner-orange.svg │ │ ├── spinner.svg │ │ ├── ss1.png │ │ └── ss2.png ├── components │ ├── common │ │ ├── Checkbox.js │ │ ├── Error.jsx │ │ ├── GlobalError.jsx │ │ ├── HelpBar.jsx │ │ ├── Input.jsx │ │ ├── Loading.jsx │ │ └── Pills.jsx │ ├── fox │ │ └── FoxSays.jsx │ ├── newscrape │ │ └── NewScrape.jsx │ ├── openai │ │ └── OpenAiKeyEntry.jsx │ ├── pagination │ │ └── Pagination.jsx │ ├── perpage │ │ └── PerPage.jsx │ ├── prompt │ │ └── InputPrompt.jsx │ ├── report │ │ └── Report.jsx │ ├── scrape │ │ ├── Results.jsx │ │ ├── Scrape.css │ │ ├── Scrape.jsx │ │ ├── ScrapeStep.jsx │ │ ├── UrlsStep.jsx │ │ └── shared.js │ └── share │ │ └── Share.jsx ├── containers │ └── Greetings │ │ └── Greetings.jsx ├── lib │ ├── ai.mjs │ ├── browser.mjs │ ├── cache.mjs │ ├── constants.mjs │ ├── controller.mjs │ ├── csv.mjs │ ├── errors.mjs │ ├── gather.mjs │ ├── gen.mjs │ ├── job.mjs │ ├── navigation.mjs │ ├── report.mjs │ ├── scrape.mjs │ ├── share.mjs │ ├── store.mjs │ ├── templates.mjs │ └── util.mjs ├── manifest.json ├── pages │ ├── Background │ │ └── index.js │ ├── Content │ │ ├── content.styles.css │ │ ├── index.js │ │ └── modules │ │ │ └── print.js │ ├── Devtools │ │ ├── index.html │ │ └── index.js │ ├── Newtab │ │ ├── Newtab.css │ │ ├── Newtab.scss │ │ ├── index.css │ │ ├── index.html │ │ └── index.jsx │ ├── Options │ │ ├── Options.css │ │ ├── Options.tsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.jsx │ ├── Panel │ │ ├── Panel.css │ │ ├── Panel.jsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.jsx │ └── Popup │ │ ├── Popup.css │ │ ├── Popup.jsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.jsx └── state │ ├── errors.js │ ├── gather.js │ ├── jobs.js │ ├── navigation.js │ ├── openai.js │ ├── storage.js │ └── util.js ├── test ├── data │ ├── amazonsoap.1.gather.json │ ├── amazonsoap.1.mjs │ ├── ebay.1.gather.json │ ├── ebay.1.mjs │ ├── etsy.1.gather.json │ ├── etsy.1.mjs │ ├── hackernews-comments.1.scrape.json │ ├── hackernews-comments.2.scrape.json │ ├── hackernews-comments.3.scrape.json │ ├── hackernews.1.gather.json │ ├── linkedin.1.gather.json │ ├── linkedin.1.mjs │ ├── reddit.1.gather.json │ ├── redfin.1.gather.json │ ├── redfin.1.mjs │ ├── wayfair.1.gather.json │ └── zillow.1.gather.json ├── testAiGather.mjs └── testAiScrape.mjs ├── tsconfig.json ├── utils ├── build.js ├── env.js └── webserver.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | // "@babel/preset-env" 4 | "@babel/preset-react" 5 | // "react-app" 6 | ], 7 | "plugins": [ 8 | // "@babel/plugin-proposal-class-properties", 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SENTRY_AUTH_TOKEN= 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/lib/cache.mjs 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "globals": { 4 | "chrome": "readonly" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # zip 13 | /zip 14 | 15 | # misc 16 | .DS_Store 17 | .env.production 18 | .env.dev 19 | 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .history 25 | 26 | # secrets 27 | secrets.*.js 28 | 29 | Makefile 30 | 31 | # Sentry Config File 32 | .env.sentry-build-plugin 33 | 34 | .plasmo -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "requirePragma": false, 5 | "arrowParens": "always" 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Hi! I'm FetchFox 4 | 5 | FetchFox is an AI powered web scraper. It takes the raw text of a website, and uses AI to extract data the user is looking for. It runs as a Chrome Extension, and the user describes the desired data in plain English. 6 | 7 | You can use FetchFox to quickly gather data like building a list of leads, assembling research data, or scoping out a market segment. 8 | 9 | By scraping raw text with AI, FetchFox lets you circumvent anti-scraping measures on sites like LinkedIn and Facebook. Even the the complicated HTML structures are possible to parse with FetchFox. 10 | 11 | # Get the extension 12 | 13 | You can get the extension for free in the [Chrome Extension Store](https://chromewebstore.google.com/detail/fetchfox/meieeikgpmlhmfjmjgciiclgmbcocfnk?authuser=0&hl=en). 14 | 15 | # Contributing 16 | 17 | Contributions are welcome! Please be aware the extension is under active constuction. [Join us on Discord](https://discord.gg/mM54bwdu59) for more info. 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fetchfox", 3 | "version": "1.0.50", 4 | "description": "FetchFox lets you scrape any site for any data", 5 | "license": "none", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/fetchfox/fetchfox.git" 9 | }, 10 | "scripts": { 11 | "build": "node utils/build.js", 12 | "build-dev": "NODE_ENV=dev node utils/build.js", 13 | "start": "node utils/webserver.js", 14 | "prettier": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'" 15 | }, 16 | "dependencies": { 17 | "@fortawesome/react-fontawesome": "^0.2.0", 18 | "@plasmohq/storage": "^1.12.0", 19 | "@sentry/react": "^8.32.0", 20 | "@sentry/webpack-plugin": "^2.22.4", 21 | "dotenv": "^16.4.5", 22 | "export-to-csv": "^1.3.0", 23 | "js-sha256": "^0.11.0", 24 | "json5": "^2.2.3", 25 | "jsonic": "^1.0.1", 26 | "openai": "^4.29.2", 27 | "openai-tokens": "^2.3.3", 28 | "pdfjs-dist": "^4.6.82", 29 | "radash": "^12.1.0", 30 | "react": "^18.2.0", 31 | "react-copy-to-clipboard": "^5.1.0", 32 | "react-dom": "^18.2.0", 33 | "react-expanding-textarea": "^2.3.6", 34 | "react-icons": "^5.0.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.20.12", 38 | "@babel/plugin-proposal-class-properties": "^7.18.6", 39 | "@babel/preset-env": "^7.20.2", 40 | "@babel/preset-react": "^7.18.6", 41 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", 42 | "@types/chrome": "^0.0.202", 43 | "@types/react": "^18.0.26", 44 | "@types/react-dom": "^18.0.10", 45 | "babel-eslint": "^10.1.0", 46 | "babel-loader": "^9.1.2", 47 | "babel-preset-react-app": "^10.0.1", 48 | "clean-webpack-plugin": "^4.0.0", 49 | "copy-webpack-plugin": "^11.0.0", 50 | "css-loader": "^6.7.3", 51 | "eslint": "^8.31.0", 52 | "eslint-config-react-app": "^7.0.1", 53 | "eslint-plugin-flowtype": "^8.0.3", 54 | "eslint-plugin-import": "^2.27.4", 55 | "eslint-plugin-jsx-a11y": "^6.7.1", 56 | "eslint-plugin-react": "^7.32.0", 57 | "eslint-plugin-react-hooks": "^4.6.0", 58 | "file-loader": "^6.2.0", 59 | "fs-extra": "^11.1.0", 60 | "html-loader": "^4.2.0", 61 | "html-webpack-plugin": "^5.5.0", 62 | "mocha": "^10.4.0", 63 | "prettier": "^2.8.3", 64 | "react-refresh": "^0.14.0", 65 | "react-refresh-typescript": "^2.0.7", 66 | "sass": "^1.57.1", 67 | "sass-loader": "^13.2.0", 68 | "source-map-loader": "^3.0.1", 69 | "style-loader": "^3.3.1", 70 | "terser-webpack-plugin": "^5.3.6", 71 | "ts-loader": "^9.4.2", 72 | "type-fest": "^3.5.2", 73 | "typescript": "^4.9.4", 74 | "webpack": "^5.75.0", 75 | "webpack-cli": "^4.10.0", 76 | "webpack-dev-server": "^4.11.1", 77 | "zip-webpack-plugin": "^4.0.1" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/assets/img/bubble-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/bubble-arrow.png -------------------------------------------------------------------------------- /src/assets/img/fox-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/fox-transparent.png -------------------------------------------------------------------------------- /src/assets/img/fox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/fox.png -------------------------------------------------------------------------------- /src/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/icon-128.png -------------------------------------------------------------------------------- /src/assets/img/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/icon-34.png -------------------------------------------------------------------------------- /src/assets/img/spinner-orange.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/img/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/img/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/ss1.png -------------------------------------------------------------------------------- /src/assets/img/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fetchfox/fetchfox-extension/698f8969568fc2a88a8abc10a368e37c842aa332/src/assets/img/ss2.png -------------------------------------------------------------------------------- /src/components/common/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { IoIosCheckmark } from "react-icons/io"; 3 | import { 4 | IoCheckmarkCircle, 5 | IoEllipseOutline, 6 | } from 'react-icons/io5'; 7 | import { mainColor } from '../../lib/constants.mjs'; 8 | 9 | 10 | export const Checkbox = ({ size, checked, disabled, onClick, children }) => { 11 | size ||= 18; 12 | return ( 13 |
23 |
24 | {checked &&
30 | 36 |
} 37 | {!checked &&
42 |
} 43 |
44 |
{children}
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/common/Error.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export const Error = ({ message }) => { 4 | return message ?
{message}
: null; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/common/GlobalError.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { useGlobalError } from '../../state/errors'; 3 | import { clearGlobalError } from '../../lib/errors.mjs'; 4 | import { bgColor, errorColor } from '../../lib/constants.mjs'; 5 | import { useQuota } from '../../state/openai'; 6 | import { 7 | IoMdCloseCircle, 8 | } from 'react-icons/io'; 9 | import { 10 | RiErrorWarningFill, 11 | } from 'react-icons/ri'; 12 | 13 | export const GlobalError = () => { 14 | const globalError = useGlobalError(); 15 | 16 | let errorNode; 17 | if (globalError) { 18 | errorNode = ( 19 |
31 |
32 | {globalError.message} 33 |
34 |
35 | clearGlobalError()} /> 39 |
40 |
41 | ); 42 | } 43 | return ( 44 |
52 | {errorNode} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/common/HelpBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { useGlobalError } from '../../state/errors'; 3 | import { clearGlobalError } from '../../lib/errors.mjs'; 4 | import { bgColor, errorColor, discordUrl } from '../../lib/constants.mjs'; 5 | import { Report } from '../report/Report'; 6 | 7 | export const HelpBar = () => { 8 | return ( 9 |
23 |
24 | FetchFox is new. You can DM the devs on Discord.
25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/common/Input.jsx: -------------------------------------------------------------------------------- 1 | //https://stackoverflow.com/questions/46000544/react-controlled-input-cursor-jumps 2 | import React, { useEffect, useRef, useState } from 'react'; 3 | 4 | export const Input = (props) => { 5 | const { value, onChange, ...rest } = props; 6 | const [cursor, setCursor] = useState(null); 7 | const ref = useRef(null); 8 | 9 | useEffect(() => { 10 | const input = ref.current; 11 | if (input) input.setSelectionRange(cursor, cursor); 12 | }, [ref, cursor, value]); 13 | 14 | const handleChange = (e) => { 15 | setCursor(e.target.selectionStart); 16 | onChange && onChange(e); 17 | }; 18 | 19 | return ; 20 | }; 21 | -------------------------------------------------------------------------------- /src/components/common/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import spinner from '../../assets/img/spinner.svg'; 3 | 4 | export const Loading = ({ width, size }) => { 5 | return ( 6 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/common/Pills.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Pills = ({ value, onChange, children }) => { 4 | const nodes = children.map((child) => { 5 | const isActive = value === child.key; 6 | return ( 7 |
onChange(child.key)} 10 | key={child.key} 11 | > 12 | {child} 13 |
14 | ); 15 | }); 16 | 17 | return ( 18 |
19 | {nodes} 20 |
21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/fox/FoxSays.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import fox from '../../assets/img/fox-transparent.png'; 4 | import arrow from '../../assets/img/bubble-arrow.png'; 5 | 6 | export const FoxSays = ({ message, small }) => { 7 | const size = small ? 32 : 100; 8 | return ( 9 |
14 |
15 | 16 |
17 |
18 |
26 | {message} 27 |
28 | 35 |
36 |
37 | ); 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/components/newscrape/NewScrape.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { IoIosArrowDroprightCircle } from 'react-icons/io'; 3 | import { FaShareFromSquare } from 'react-icons/fa6'; 4 | import { shareResults } from '../../lib/share.mjs'; 5 | import { bgColor } from '../../lib/constants.mjs'; 6 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 7 | import { getJobColumn } from '../../lib/util.mjs'; 8 | import { LuCopy, LuCopyCheck } from 'react-icons/lu'; 9 | import { Loading } from '../common/Loading'; 10 | import { InputPrompt } from '../prompt/InputPrompt'; 11 | import { genJobFromUrls } from '../../lib/gen.mjs'; 12 | import { 13 | getKey, 14 | setKey, 15 | saveJob, 16 | setActiveJob, 17 | } from '../../lib/store.mjs'; 18 | 19 | const NewScrapeModal = ({ job, header, onSubmit, onClose }) => { 20 | const [prompt, setPrompt] = useState(''); 21 | const [loading, setLoading] = useState(); 22 | 23 | const urls = getJobColumn(job, header) 24 | 25 | let body; 26 | if (false) { 27 | body =
; 28 | } else { 29 | body = ( 30 |
31 |
Create a new scrape from the URLs below
32 |
41 | {urls.map(x =>
{x}
)} 42 |
43 | 44 | { setLoading(true); onSubmit(e, prompt, urls)}} 46 | onChange={(e) => setPrompt(e.target.value)} 47 | prompt={prompt} 48 | loading={loading} 49 | /> 50 |
51 | ); 52 | } 53 | 54 | return ( 55 |
70 | 71 |
e.stopPropagation()} 79 | > 80 | {body} 81 |
82 |
83 | ); 84 | } 85 | 86 | export const NewScrape = ({ job, header }) => { 87 | const [show, setShow] = useState(); 88 | 89 | const handleSubmit = async (e, prompt, urls) => { 90 | e.preventDefault(); 91 | const job = await genJobFromUrls(prompt, urls); 92 | console.log('new scrape job -->', job); 93 | await saveJob(job); 94 | await setActiveJob(job.id); 95 | await setKey('scrapeStep', 'inner'); 96 | window.scrollTo(0, 0); 97 | } 98 | 99 | return ( 100 |
101 | {show && setShow(false)} 106 | />} 107 | 108 | 116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/components/openai/OpenAiKeyEntry.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useOpenAiKey, useOpenAiModels, useQuota } from '../../state/openai'; 3 | import { setKey } from '../../lib/store.mjs'; 4 | import { Error } from '../common/Error'; 5 | import { FoxSays } from '../fox/FoxSays'; 6 | import { 7 | IoIosCheckmarkCircle, 8 | IoIosCircle, 9 | } from 'react-icons/io'; 10 | import { 11 | FiCircle, 12 | FiCheckCircle, 13 | } from 'react-icons/fi'; 14 | import { 15 | RiErrorWarningFill, 16 | } from 'react-icons/ri'; 17 | import { mainColor } from '../../lib/constants.mjs'; 18 | 19 | const QuotaError = () => { 20 | const quota = useQuota(); 21 | 22 | if (quota.ok) return null; 23 | 24 | return ( 25 |
26 |
41 |
You are out of OpenAI credits
42 |

43 | You won't be able to use FetchFox until you add more credits. You can do so by visiting OpenAI's billing dashboard: 44 |

45 |
46 | Visit OpenAI Billing Page 47 |
48 |

Alternatively, you can switch to the FetchFox server below.

49 |
50 |
51 | ); 52 | } 53 | 54 | export const OpenAiKeyEntry = ({ onDone, doneText }) => { 55 | const { key: openAiKey, plan: openAiPlan } = useOpenAiKey(); 56 | const models = useOpenAiModels(); 57 | const [apiKey, setApiKey] = useState(); 58 | const [modelName, setModelName] = useState(models.model); 59 | const [error, setError] = useState(); 60 | const [success, setSuccess] = useState(false); 61 | const [disabled, setDisabled] = useState(true); 62 | const quota = useQuota(); 63 | 64 | useEffect(() => { 65 | setDisabled( 66 | !quota?.ok || 67 | (!openAiPlan || (openAiPlan == 'openai' && !openAiKey))); 68 | }, [openAiKey, openAiPlan, quota?.ok]); 69 | 70 | useEffect(() => { 71 | setApiKey(openAiKey); 72 | }, [openAiKey]); 73 | 74 | useEffect(() => { 75 | setModelName(models.model); 76 | }, [models.model]); 77 | 78 | const handleOpenAi = (e) => { 79 | e.preventDefault(); 80 | setError(null); 81 | setKey('openAiPlan', 'openai'); 82 | setKey('openAiKey', apiKey); 83 | setKey('model', modelName); 84 | setTimeout(() => setSuccess(false), 3000); 85 | } 86 | 87 | const handleFree = (e) => { 88 | e.preventDefault(); 89 | setError(null); 90 | setKey('openAiPlan', 'free'); 91 | setTimeout(() => setSuccess(false), 3000); 92 | } 93 | 94 | const handleChange = (e) => { 95 | setError(null); 96 | setApiKey(e.target.value); 97 | setKey('openAiKey', e.target.value); 98 | } 99 | 100 | const handleChangeModel = (e) => { 101 | setModelName(e.target.value); 102 | setKey('model', e.target.value); 103 | } 104 | 105 | const handleDone = () => { 106 | if (openAiPlan == 'openai' && !openAiKey) { 107 | setError('Enter your API key'); 108 | return; 109 | } 110 | onDone(); 111 | } 112 | 113 | const wrapperStyle = { 114 | borderRadius: 4, 115 | padding: 20, 116 | background: '#fff0', 117 | margin: '20px 0', 118 | border: '2px solid transparent', 119 | }; 120 | 121 | const wrapperActiveStyle = { 122 | border: '2px solid #fffa', 123 | background: '#fff2', 124 | }; 125 | 126 | const titleStyle = { 127 | fontSize: 18, 128 | opacity: 0.8, 129 | fontVariantCaps: 'small-caps', 130 | }; 131 | 132 | const subtitleStyle = { 133 | fontSize: 24, 134 | fontWeight: 'bold', 135 | }; 136 | 137 | const pickModelNode = ( 138 |
139 |
Pick a model
140 | 148 |
149 | ) 150 | 151 | return ( 152 |
153 | 154 | 155 | 156 | 157 |
159 |
160 |
161 |
162 |
Free Forever
163 |
Bring Your Own Key
164 |
165 |

166 | Got ChatGPT? Enter your OpenAI API key below, and your use of FetchFox will be completely free forever. You will only pay for your own OpenAI usage. 167 |

168 |

169 | You can find your OpenAI API key on your API Keys setting page: 170 |

171 | 181 | 182 |
Enter your OpenAI key
183 |
184 | 191 | 192 | {models.available.length > 0 && pickModelNode} 193 | 194 |
195 | 208 |
209 |
210 |
211 |
212 |
213 | 214 |
216 |
217 |
218 |
219 |
Free... For Now
220 |
Use Our Server
221 |
222 |

223 | Don't have ChatGPT, or don't want to find your key? 224 |

225 |

226 | For now, we're letting people try the extension with our own AI backend. This costs us money and we're poor, so expect this option to go away soon! 😅 227 |

228 | 229 |
230 | 243 |
244 |
245 |
246 | 247 | {!disabled &&
255 | 263 |
} 264 |
265 |
266 | ); 267 | } 268 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Loading } from '../common/Loading'; 3 | import { Checkbox } from '../common/Checkbox'; 4 | import { usePagination } from '../../state/gather'; 5 | import { useActiveJob } from '../../state/jobs'; 6 | import { useActivePage } from '../../state/navigation'; 7 | 8 | export const Pagination = ({ onChange, follow, count }) => { 9 | const activePage = useActivePage(); 10 | const pagination = usePagination(activePage); 11 | 12 | const update = (f, c) => { 13 | const pages = pagination.links?.pages.filter(x => f && x.pageNumber <= c); 14 | onChange({ 15 | count: c, 16 | follow: f, 17 | links: pages, 18 | }); 19 | } 20 | 21 | const size = 12; 22 | const style = { 23 | display: 'flex', 24 | alignItems: 'center', 25 | gap: 4, 26 | fontStyle: 'italic', 27 | fontSize: size, 28 | height: 30, 29 | }; 30 | 31 | const loadingNode = ( 32 |
33 | Checking for pagination 34 |
35 | ); 36 | 37 | let paginationInfo; 38 | let paginationOptions; 39 | if (pagination.didInit && !pagination.loading) { 40 | const num = (pagination.links?.pages || []).length; 41 | 42 | if (num <= 1) { 43 | paginationInfo =
No pagination found
; 44 | } else { 45 | paginationInfo = ( 46 |
47 | update(!follow, count)} 51 | > 52 | Follow pagination 53 | 54 |
55 | ); 56 | 57 | const optionNodes = pagination.links.pages.map(link => ( 58 | 59 | )); 60 | 61 | paginationOptions = ( 62 |
63 | 70 |
71 | ); 72 | } 73 | } 74 | 75 | return ( 76 |
77 | {pagination.loading && loadingNode} 78 |
79 | {paginationInfo} 80 | {follow && paginationOptions} 81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/components/perpage/PerPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import { Loading } from '../common/Loading'; 3 | import { Checkbox } from '../common/Checkbox'; 4 | import { usePagination } from '../../state/gather'; 5 | import { useActiveJob } from '../../state/jobs'; 6 | import { useActivePage } from '../../state/navigation'; 7 | import { Pills } from '../common/Pills'; 8 | 9 | export const PerPage = ({ perPage, onChange }) => { 10 | return ( 11 |
12 |

How many items per page?

13 |
14 | 15 |
One
16 |
Multiple
17 |
Let AI Guess
18 |
19 |
20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/components/prompt/InputPrompt.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import Textarea from 'react-expanding-textarea'; 3 | import { Loading } from '../common/Loading'; 4 | import { FaArrowRight } from 'react-icons/fa'; 5 | 6 | export const InputPrompt = ({ onSubmit, onChange, prompt, loading, disabled }) => { 7 | const timeoutRef = useRef(null); 8 | const handleSubmit = (e) => { 9 | if (loading) return; 10 | if (disabled) return; 11 | onSubmit(e); 12 | }; 13 | 14 | const handleKeyDown = (e) => { 15 | if (e.key == 'Enter' && !e.shiftKey) { 16 | e.preventDefault(); 17 | handleSubmit(e); 18 | } else { 19 | console.log('handleKeyDown call onchange', e.target.value); 20 | onChange(e); 21 | } 22 | } 23 | 24 | return ( 25 |
26 |
31 |
35 | 52 |
53 | 54 |
55 |