├── public ├── _redirects ├── favicon.ico ├── manifest.json └── index.html ├── src ├── assets │ └── images │ │ └── logo.png ├── components │ ├── common │ │ ├── LoadingSpinner.js │ │ ├── ThemeContext.js │ │ ├── ThemeToggle.js │ │ ├── CopyToClipboard.js │ │ └── Navigation.js │ ├── generators │ │ ├── GeneratorCard.js │ │ ├── Generators.js │ │ └── Generator.js │ ├── api │ │ └── OpenAIAPI.js │ └── pages │ │ └── Home.js ├── index.css ├── index.js ├── hooks │ └── useTypingEffect.js ├── App.js ├── App.css └── data │ └── generatorList.js ├── .gitignore ├── api ├── package.json ├── index.js └── package-lock.json ├── package.json ├── .github └── workflows │ └── production-cd.yml └── README.md /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ash1eygrace/ai-content/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ash1eygrace/ai-content/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "AshAI", 3 | "name": "AshAI: AI Generated Web Copy ", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/common/LoadingSpinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spinner } from 'react-bootstrap'; 3 | 4 | const LoadingSpinner = () => ( 5 |
6 | 7 | Loading... 8 | 9 |
10 | ); 11 | 12 | export default LoadingSpinner; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/components/common/ThemeContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState } from 'react'; 2 | 3 | export const ThemeContext = createContext(); 4 | 5 | export const ThemeProvider = ({ children }) => { 6 | const [isDarkMode, setIsDarkMode] = useState(false); 7 | const toggleTheme = () => setIsDarkMode(!isDarkMode); 8 | 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node index.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.0.3", 17 | "express": "^4.18.2", 18 | "node-fetch": "^3.3.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { ThemeProvider } from './components/common/ThemeContext'; 6 | import '@fortawesome/fontawesome-free/css/all.css'; 7 | 8 | const container = document.getElementById('root'); 9 | 10 | createRoot(container).render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/common/ThemeToggle.js: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { ThemeContext } from './ThemeContext'; 3 | 4 | const ThemeToggle = () => { 5 | const { isDarkMode, toggleTheme } = useContext(ThemeContext); 6 | 7 | return ( 8 | 15 | ); 16 | }; 17 | 18 | export default ThemeToggle; 19 | -------------------------------------------------------------------------------- /src/components/generators/GeneratorCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card, Col, Button } from 'react-bootstrap'; 3 | 4 | const GeneratorCard = ({ generator }) => ( 5 | 6 | 7 | 8 |

{generator.title}

9 |
10 | 11 | 12 | {generator.description} 13 | 14 | 15 | 16 |
17 | 18 | ); 19 | 20 | export default GeneratorCard; 21 | -------------------------------------------------------------------------------- /src/hooks/useTypingEffect.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | 3 | const useTypingEffect = (text, typingSpeed) => { 4 | const [displayText, setDisplayText] = useState(''); 5 | 6 | useEffect(() => { 7 | setDisplayText(''); 8 | let currentIndex = 0; 9 | const interval = setInterval(() => { 10 | if (currentIndex < text.length) { 11 | setDisplayText((prevText) => prevText + text.charAt(currentIndex)); 12 | currentIndex++; 13 | } else { 14 | clearInterval(interval); 15 | } 16 | }, typingSpeed); 17 | 18 | return () => clearInterval(interval); 19 | }, [text, typingSpeed]); 20 | 21 | return displayText; 22 | }; 23 | 24 | export default useTypingEffect; 25 | -------------------------------------------------------------------------------- /src/components/generators/Generators.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container, Row } from 'react-bootstrap'; 3 | import { generatorList } from '../../data/generatorList'; 4 | import GeneratorCard from './GeneratorCard'; 5 | 6 | function Generators() { 7 | return ( 8 |
9 |

Content Generators

10 | 11 | 12 | {generatorList.map((generator) => ( 13 | 14 | ))} 15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default Generators; 22 | -------------------------------------------------------------------------------- /src/components/common/CopyToClipboard.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | import { FiClipboard } from 'react-icons/fi'; 4 | 5 | const CopyToClipboard = ({ text }) => { 6 | const [isCopied, setIsCopied] = useState(false); 7 | 8 | const copyText = () => { 9 | navigator.clipboard.writeText(text) 10 | .then(() => { 11 | setIsCopied(true); 12 | setTimeout(() => { 13 | setIsCopied(false); 14 | }, 2000); 15 | }); 16 | }; 17 | 18 | return ( 19 | 22 | ); 23 | }; 24 | 25 | export default CopyToClipboard; 26 | -------------------------------------------------------------------------------- /src/components/common/Navigation.js: -------------------------------------------------------------------------------- 1 | import { Nav, Navbar, Container } from 'react-bootstrap'; 2 | import React from 'react'; 3 | import ThemeToggle from './ThemeToggle'; 4 | 5 | const Navigation = React.memo(() => ( 6 | 23 | )); 24 | 25 | export default Navigation; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copyai", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@fortawesome/fontawesome-free": "^6.4.0", 7 | "bootstrap": "^5.2.3", 8 | "react": "^18.2.0", 9 | "react-bootstrap": "^2.7.2", 10 | "react-dom": "^18.2.0", 11 | "react-icons": "^4.8.0", 12 | "react-router": "^6.10.0", 13 | "react-router-dom": "^6.10.0", 14 | "react-scripts": "^5.0.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app" 24 | ] 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.2%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | }, 38 | "proxy": "http://localhost:6001/" 39 | } 40 | -------------------------------------------------------------------------------- /src/components/api/OpenAIAPI.js: -------------------------------------------------------------------------------- 1 | export async function callAPI(prompt, options) { 2 | try { 3 | const response = await fetch(`/api/ai-content`, { 4 | method: "POST", 5 | mode: "cors", 6 | cache: "no-cache", 7 | credentials: "same-origin", 8 | headers: { 9 | "Content-Type": "application/json", 10 | }, 11 | 12 | body: JSON.stringify({ 13 | prompt: prompt, 14 | temperature: options.temperature, 15 | max_tokens: options.max_tokens, 16 | top_p: options.top_p, 17 | frequency_penalty: options.frequency_penalty, 18 | presence_penalty: options.presence_penalty, 19 | }), 20 | }); 21 | 22 | const data = await response.json(); 23 | return data.output.choices[0].text; 24 | } catch (error) { 25 | console.log(error); 26 | return { 27 | error: true, 28 | message: 29 | "Sorry, there was an error with your request. Please make sure your API Key is valid and try again later. If the issue persists, please try again later.", 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import fetch from "node-fetch"; 2 | import express from "express"; 3 | import cors from "cors"; 4 | import dotenv from "dotenv"; 5 | dotenv.config(); 6 | 7 | const app = express(); 8 | 9 | app.use(cors()); 10 | 11 | app.use(express.json()); 12 | 13 | app.post("/api/ai-content", async (req, res) => { 14 | const { 15 | prompt, 16 | temperature, 17 | max_tokens, 18 | top_p, 19 | frequency_penalty, 20 | presence_penalty, 21 | } = req.body; 22 | 23 | const openAPICall = await fetch(`https://api.openai.com/v1/completions`, { 24 | method: "POST", 25 | headers: { 26 | "Content-Type": "application/json", 27 | Authorization: `Bearer ${process.env.API_KEY}`, 28 | }, 29 | body: JSON.stringify({ 30 | model: "text-davinci-002", 31 | prompt: prompt, 32 | temperature: temperature, 33 | max_tokens: max_tokens, 34 | top_p: top_p, 35 | frequency_penalty: frequency_penalty, 36 | presence_penalty: presence_penalty, 37 | }), 38 | }); 39 | 40 | const content = await openAPICall.json(); 41 | res.status(201).json({ output: content }); 42 | }); 43 | 44 | const port = process.env.PORT || 6001; 45 | 46 | app.listen(port, () => { 47 | console.log(`Server running on port ${port}`); 48 | }); 49 | -------------------------------------------------------------------------------- /.github/workflows/production-cd.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Production 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "README.md" 9 | - ".gitignore" 10 | - ".prettierrc" 11 | - ".eslintrc.json" 12 | - "package.json" 13 | - "package-lock.json" 14 | - "jsconfig.json" 15 | - ".github/**" 16 | - ".screenshots/**" 17 | 18 | permissions: write-all 19 | 20 | jobs: 21 | deploy: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | 27 | - name: Deploy to production 28 | uses: appleboy/ssh-action@master 29 | with: 30 | host: ${{ secrets.IP_ADDRESS }} 31 | username: ${{ secrets.USER }} 32 | key: ${{ secrets.SSH_PRIVATE_KEY }} 33 | script: | 34 | cd ${{ secrets.PRODUCTION_PATH }} 35 | git checkout main 36 | git pull 37 | npm i 38 | npm run build 39 | cd ${{ secrets.PRODUCTION_API_PATH }} 40 | npm i 41 | pm2 restart ${{ secrets.PRODUCTION_NAME }} 42 | 43 | - name: Publish Summary 44 | if: success() 45 | run: echo -e "Production Deployment Successful!" >> $GITHUB_STEP_SUMMARY 46 | -------------------------------------------------------------------------------- /src/components/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Container, Row } from 'react-bootstrap' 3 | import Logo from '../../assets/images/logo.png' 4 | import { generatorList } from '../../data/generatorList'; 5 | import GeneratorCard from '../generators/GeneratorCard'; 6 | 7 | const Home = () => { 8 | const featuredGenerators = generatorList.filter((generator) => generator.featured); 9 | 10 | return ( 11 |
12 | 13 | Illustration of woman with long brown hair and purple back ground 14 |

Hey, I’m AshAI.

15 |

I'm a sales and content virtual assistant. You'll never have to think about what to say again. With a click of a button I'll write high quality copy for your products, company bio, or blog intro paragraphs. I was trained via OpenAI and achieved my GPT-3 degree.

16 |

What I can do for you:

17 |
18 | 19 | 20 | {featuredGenerators.map((generator) => ( 21 | 22 | ))} 23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default Home; -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import 'react-bootstrap/dist/react-bootstrap.min.js'; 3 | import React, { useEffect, useState } from 'react'; 4 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 5 | 6 | import Navigation from './components/common/Navigation'; 7 | import Home from './components/pages/Home'; 8 | import Generators from './components/generators/Generators'; 9 | import Generator from './components/generators/Generator'; 10 | 11 | import { generatorList } from './data/generatorList'; 12 | import { ThemeContext } from './components/common/ThemeContext'; 13 | 14 | function App() { 15 | const [isDarkMode, setIsDarkMode] = useState(() => { 16 | const storedTheme = localStorage.getItem('theme'); 17 | return storedTheme !== null ? JSON.parse(storedTheme) : true; 18 | }); 19 | const toggleTheme = () => setIsDarkMode(!isDarkMode); 20 | 21 | useEffect(() => { 22 | localStorage.setItem('theme', JSON.stringify(isDarkMode)); 23 | }, [isDarkMode]); 24 | 25 | return ( 26 | 27 | 28 |
29 | 30 | 31 | } /> 32 | } /> 33 | {generatorList.map((generator) => ( 34 | } 38 | /> 39 | ))} 40 | 41 |
42 |
43 |
44 | ); 45 | } 46 | 47 | export default App; 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 15 | 16 | 20 | 21 | 30 | AshAI 31 | 32 | 33 | 34 |
35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary-color: #8484c3; 3 | --secondary-color: #5cb0a2; 4 | --text-color: #242325; 5 | --background-color: #F8F7F9; 6 | --btn-hover-color: #dc7684; 7 | --white-color: #F8F7F9; 8 | 9 | /* Dark mode variables */ 10 | --dark-primary-color: #1E2028; 11 | --dark-secondary-color: #5cb0a2; 12 | --dark-text-color: #b3b6bd; 13 | --dark-background-color: #2A2D34; 14 | --dark-btn-hover-color: #dc7684; 15 | } 16 | 17 | .dark-mode { 18 | --primary-color: var(--dark-primary-color); 19 | --secondary-color: var(--dark-secondary-color); 20 | --text-color: var(--dark-text-color); 21 | --background-color: var(--dark-background-color); 22 | --btn-hover-color: var(--dark-btn-hover-color); 23 | } 24 | 25 | @media (max-width: 768px) { 26 | .col-6 { 27 | flex: 0 0 100%; 28 | max-width: 100%; 29 | } 30 | .btn-primary { 31 | margin-bottom: 10%; 32 | } 33 | } 34 | body { 35 | background: var(--background-color); 36 | font-family: 'Open Sans', sans-serif; 37 | } 38 | h1 { 39 | font-size: 2.3rem; 40 | text-align: center; 41 | padding-bottom: 5%; 42 | } 43 | h2 { 44 | font-size: 1.37rem; 45 | } 46 | p { 47 | font-size: 1.2rem; 48 | } 49 | #main { 50 | background-color: var(--background-color); 51 | padding: 6%; 52 | min-height: 100vh; 53 | color: var(--text-color); 54 | height:auto 55 | } 56 | /* Nav*/ 57 | #navigation { 58 | background-color: var(--primary-color); 59 | padding: 1%; 60 | } 61 | .navbar-light .navbar-nav .nav-link, 62 | .navbar-nav a.navbar-brand { 63 | color: var(--white-color); 64 | } 65 | /*Home Page*/ 66 | #hero { 67 | padding-bottom: 3%; 68 | text-align: center; 69 | } 70 | #hero h1 { 71 | padding-top: 2%; 72 | padding-bottom: 2%; 73 | } 74 | #hero p { 75 | padding-left: 14%; 76 | padding-right: 14%; 77 | padding-bottom: 8%; 78 | } 79 | img.logo { 80 | max-width: 204px; 81 | border-radius: 9999px; 82 | } 83 | /*Generator*/ 84 | #pageDescription { 85 | padding-bottom: 10%; 86 | } 87 | textarea { 88 | resize: none; 89 | } 90 | label.form-label { 91 | font-weight: bold; 92 | } 93 | .pre-wrap { 94 | white-space: pre-wrap; 95 | } 96 | .copy-button { 97 | color: var(--white-color); 98 | background-color: var(--secondary-color); 99 | border: none; 100 | position: absolute; 101 | bottom: 10px; 102 | right: 10px; 103 | } 104 | .response-container { 105 | margin-bottom: 60px; 106 | } 107 | /* Buttons */ 108 | .btn:hover { 109 | box-shadow: 1px 1px 2px rgba(74, 48, 89, 0.4); 110 | cursor: pointer; 111 | color: var(--white-color); 112 | background-color: var(--btn-hover-color); 113 | } 114 | .btn-primary { 115 | padding-top: 4%; 116 | padding-bottom: 4%; 117 | background-color: var(--secondary-color); 118 | border: none; 119 | float: right; 120 | min-width: 100%; 121 | } 122 | /*Cards*/ 123 | .card-header { 124 | padding-top: 4%; 125 | padding-bottom: 4%; 126 | background-color: var(--primary-color); 127 | color: var(--white-color); 128 | } 129 | .card { 130 | background-color: var(--background-color); 131 | box-shadow: 1px 1px 5px rgba(0, 0, 0, .22); 132 | min-height: 79vh; 133 | min-height: 100%; 134 | border-radius: 0.25rem; 135 | } 136 | .card-body { 137 | background-color: var(--background-color); 138 | } 139 | .col-sm-6 { 140 | margin-bottom: 20px; 141 | } 142 | .col-sm-6 .card p { 143 | margin-top: 35px; 144 | margin-bottom: 45px; 145 | } 146 | 147 | .theme-toggle { 148 | background-color: var(--background-color); 149 | width: 40px; 150 | height: 40px; 151 | border: none; 152 | font-size: 24px; 153 | padding: 10x 10px; 154 | color: var(--text-color); 155 | border-radius: 5px; 156 | transition: color 0.2s; 157 | } 158 | button:focus { 159 | outline: none; 160 | } 161 | 162 | .theme-toggle .fas.fa-sun { 163 | color: #F4D35E; 164 | } 165 | -------------------------------------------------------------------------------- /src/data/generatorList.js: -------------------------------------------------------------------------------- 1 | export const generatorList = [ 2 | { 3 | id: 1, 4 | title: 'Blog Post Ideas', 5 | description: 'Stuck in the idea phase? Generate 5 ideas', 6 | link: 'blog-ideas', 7 | featured: true, 8 | prompt: 'Brainstorm a list of 5 blog post ideas for ', 9 | description2: 'Enter your your content topic to generate a list of 5 blog post ideas.', 10 | formLabel: 'Topic:', 11 | formName: 'topic', 12 | placeholder: 'e.g. Finance, Working With AI, etc.', 13 | temperature: 0.2, 14 | max_tokens: 200, 15 | top_p: 1, 16 | frequency_penalty: 1, 17 | presence_penalty: 1, 18 | }, 19 | { 20 | id: 2, 21 | title: 'Product Description', 22 | description: 'Generate an awesome Product Description', 23 | link: 'product-description', 24 | featured: false, 25 | prompt: 'Write a persuasive and exciting product description for: ', 26 | description2: 'Enter your your product and a few keywords to include in your output. Click Submit and get a product description generated for you by AI. Think less and sell more.', 27 | formLabel: 'Product Name & Purpose:', 28 | formName: 'productName', 29 | placeholder: 'e.g. ScoobySnacks will make your dog chill out', 30 | temperature: 0.6, 31 | max_tokens: 150, 32 | top_p: 1, 33 | frequency_penalty: 1, 34 | presence_penalty: 1, 35 | }, 36 | { 37 | id: 3, 38 | title: 'Company Bio', 39 | description: 'Generate an awesome Company Bio', 40 | link: 'company-bio', 41 | featured: true, 42 | prompt: 'Write a persuasive and exciting company bio for: ', 43 | description2: 'Enter your company name and a few keywords that you\'d like to include in your output. Click Submit and get a company bio generated for you by AI. Think less and sell more.', 44 | formLabel: 'Company Name & Purpose:', 45 | formName: 'companyName', 46 | placeholder: 'e.g. Dunder Mifflin the best paper company ', 47 | temperature: 0.6, 48 | max_tokens: 150, 49 | top_p: 1, 50 | frequency_penalty: 1, 51 | presence_penalty: 1, 52 | }, 53 | { 54 | id: 4, 55 | title: 'SEO Blog Intro', 56 | description: 'Generate an awesome SEO blog into paragraph', 57 | link: 'seo-blog-intro', 58 | featured: true, 59 | prompt: 'Write an uplifting and positive Blog intro paragraph with SEO keywords for the blog title: ', 60 | description2: 'Enter your blog posts title and SEO keywords that you\'d like to include in your output. Click Submit and get a blog post introduction paragraph generated for you by AI. Think less and publish more.', 61 | formLabel: 'Blog post title:', 62 | formName: 'blogTitle', 63 | placeholder: 'e.g. How to Make a Commit with Git', 64 | temperature: 0.6, 65 | max_tokens: 150, 66 | top_p: 1, 67 | frequency_penalty: 1, 68 | presence_penalty: 1, 69 | }, 70 | { 71 | id: 5, 72 | title: 'LinkedIn Job Description', 73 | description: 'Generate a knowledgeable LinkedIn Job Description', 74 | link: 'linkedin-job-description', 75 | featured: false, 76 | prompt: 'Write an informative and knowledgeable LinkedIn job description in the first person past tense for the job title: ', 77 | description2: 'Enter your your job title, click submit, and get a job description generated for you by AI.', 78 | formLabel: 'Job Title:', 79 | formName: 'jobTitle', 80 | placeholder: 'e.g. Software Engineer specializing in AI', 81 | temperature: 0.6, 82 | max_tokens: 150, 83 | top_p: 1, 84 | frequency_penalty: 1, 85 | presence_penalty: 1, 86 | }, 87 | { 88 | id: 6, 89 | title: 'TL;DR', 90 | description: 'Summarize verbose text into a TL;DR.', 91 | link: 'tldr', 92 | featured: true, 93 | prompt: 'Write a TL;DR for the following text: ', 94 | description2: 'Enter your your verbose text, click submit, and get a TL;DR generated for you by AI.', 95 | formLabel: 'Paragraph:', 96 | formName: 'longParagraph', 97 | placeholder: 'paste your text here', 98 | temperature: 0.6, 99 | max_tokens: 150, 100 | top_p: 1, 101 | frequency_penalty: 1, 102 | presence_penalty: 1, 103 | } 104 | ]; 105 | -------------------------------------------------------------------------------- /src/components/generators/Generator.js: -------------------------------------------------------------------------------- 1 | import React, { useReducer } from 'react' 2 | import { Container, Form, Button, Card, Row, Col } from 'react-bootstrap' 3 | import { callAPI } from '../api/OpenAIAPI.js'; 4 | 5 | import CopyToClipboard from '../common/CopyToClipboard.js'; 6 | import LoadingSpinner from '../common/LoadingSpinner.js'; 7 | import useTypingEffect from '../../hooks/useTypingEffect.js'; 8 | 9 | 10 | const initialState = { 11 | heading: 'AI Generated Response:', 12 | response: '', 13 | errorMessage: '', 14 | dataLoaded: false, 15 | isFormSubmitted: false, 16 | }; 17 | 18 | function reducer(state, action) { 19 | switch (action.type) { 20 | case 'SET_HEADING': 21 | return { ...state, heading: action.payload }; 22 | case 'SET_RESPONSE': 23 | return { ...state, response: action.payload }; 24 | case 'SET_ERROR_MESSAGE': 25 | return { ...state, errorMessage: action.payload }; 26 | case 'SET_DATA_LOADED': 27 | return { ...state, dataLoaded: action.payload }; 28 | case 'SET_IS_FORM_SUBMITTED': 29 | return { ...state, isFormSubmitted: action.payload }; 30 | default: 31 | return state; 32 | } 33 | } 34 | 35 | const Generator = ({ generatorData }) => { 36 | const [state, dispatch] = useReducer(reducer, initialState); 37 | 38 | const typingSpeed = 50; 39 | const responseWithTypingEffect = useTypingEffect(state.response, typingSpeed); 40 | 41 | const onFormSubmit = async (e) => { 42 | e.preventDefault(); 43 | 44 | const formData = new FormData(e.target); 45 | const prompt = `${generatorData.prompt}${formData.get(generatorData.formName)}`; 46 | 47 | dispatch({ type: 'SET_HEADING', payload: `Thinking about your ${generatorData.title} now...` }); 48 | dispatch({ type: 'SET_RESPONSE', payload: '' }); 49 | dispatch({ type: 'SET_ERROR_MESSAGE', payload: '' }); 50 | dispatch({ type: 'SET_IS_FORM_SUBMITTED', payload: true }); 51 | dispatch({ type: 'SET_DATA_LOADED', payload: false }); 52 | 53 | try { 54 | const data = await callAPI(prompt, { 55 | temperature: generatorData.temperature, 56 | max_tokens: generatorData.max_tokens, 57 | top_p: generatorData.top_p, 58 | frequency_penalty: generatorData.frequency_penalty, 59 | presence_penalty: generatorData.presence_penalty, 60 | }); 61 | 62 | if (data.error) { 63 | dispatch({ type: 'SET_ERROR_MESSAGE', payload: data.message }); 64 | } else { 65 | dispatch({ type: 'SET_HEADING', payload: `Your AI Generated ${generatorData.title}:` }); 66 | dispatch({ type: 'SET_RESPONSE', payload: data }); 67 | } 68 | } catch (error) { 69 | console.error("Error:", error); 70 | } finally { 71 | dispatch({ type: 'SET_DATA_LOADED', payload: true }); 72 | } 73 | }; 74 | 75 | 76 | const { title, description2, formLabel, formName, placeholder } = generatorData; 77 | 78 | return ( 79 |
80 | 81 | 82 | 83 |

{title}

84 |

{description2}

85 |
86 | 87 | {formLabel} 88 | 89 | 90 | 93 |
94 | 95 | 96 | 97 | 98 | 99 |

{state.heading}

100 |
101 | 102 | { 103 | state.errorMessage ? 104 | ( 105 |

{state.errorMessage}

106 | ) 107 | : state.dataLoaded && state.response ? 108 | ( 109 |
110 | 111 | {responseWithTypingEffect} 112 | 113 | {state.response && } 114 |
115 | ) 116 | : !state.isFormSubmitted ? 117 | ( 118 | 119 | The Response from the AI for your {title} will show here. 120 | 121 | ) 122 | : 123 | ( 124 | 125 | )} 126 |
127 |
128 | 129 |
130 |
131 |
132 | ); 133 | }; 134 | 135 | export default Generator; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ai-content 2 | 3 | ## Purpose of this App 🤖 4 | 5 | To generate website, product copy, and brianstorm ideas with OpenAI's Generative Pre-trained Transformer 3 (GPT-3) an autoregressive language model that uses deep learning to produce human-like text. 6 | 7 | ![theme croped](https://user-images.githubusercontent.com/29527450/229385033-67441a4b-77e5-48a5-a570-f27620d62892.jpg) 8 | 9 | 10 | ## Getting Started 🛠️: 11 | 12 | ### Tools Used: 13 | 14 | - [Node.js](https://nodejs.org/) 15 | - [npm](https://npmjs.com) 16 | - [React](https://reactjs.org) 17 | - [React Bootstrap](https://react-bootstrap.github.io) 18 | - [Font Awesome](https://fontawesome.com/docs/web/use-with/react/) 19 | - [OpenAI's GPT-3 autoregressive language model](https://openai.com/) 20 | 21 | ### Development: 22 | 23 | This repository is set up to run on a VPS with an express backend, to run it locally do the following: 24 | 25 | 1. Fork and clone this repo. 26 | 2. In Terminal, open the directory ai-content. 27 | 3. Check that Node.js is installed by opening Terminal and typing `node -v` if you don’t see a version [Install Node.js](https://nodejs.org/en/) 28 | 4. In the `ai-content` directory type `npm install` to install any missing dependences. 29 | 5. cd into the `ai-content/api` directory and type `npm install` to install any missing dependences. 30 | 6. Obtain your [OpenAI API key](https://openai.com/api/) via Log In > Personal > API keys > Create and Copy your Secret Key. 31 | 7. In the `ai-content/api` directory add a `.env` file with an API key variable and the port for the backend: 32 | ``` 33 | API_KEY=abc123yourapikey 34 | PORT=6001 35 | ``` 36 | 8. In Terminal cd into the `ai-content/api` directory and `npm start` 37 | 9. finally, in Terminal cd in the ai-content directory and `npm start` 38 | 39 | ## Structure: 40 | 41 | ``` 42 | ai-content 43 | ├── api 44 | │ ├── node_modules 45 | │ ├── .env // Your API key goes here 46 | │ ├── index.js 47 | │ ├── package-lock.json 48 | │ └── package.json 49 | ├── node_modules 50 | ├── public 51 | │ ├── _redirects 52 | │ ├── favicon.ico 53 | │ ├── index.html 54 | │ └── manifest.json 55 | ├── src 56 | │ ├── assets 57 | │ │ └── images 58 | │ │ └── logo.png 59 | │ ├── components 60 | │ │ ├── api 61 | │ │ │ └── OpenAIAPI.js 62 | │ │ ├── common 63 | │ │ │ ├── CopyToClipboard.js 64 | │ │ │ ├── LoadingSpinner.js 65 | │ │ │ ├── ThemeContext.js 66 | │ │ │ ├── ThemeToggle.js 67 | │ │ │ ├── LoadingSpinner.js 68 | │ │ │ └── Navigation.js 69 | │ │ ├── generators 70 | │ │ │ ├── Generator.js 71 | │ │ │ ├── GeneratorCard.js 72 | │ │ │ └── Generators.js 73 | │ │ └── pages 74 | │ │ └── Home.js 75 | │ ├── data 76 | │ │ └── generatorList.js 77 | │ ├── hooks 78 | │ │ └── useTypingEffect.js 79 | │ ├── App.css 80 | │ ├── App.js 81 | │ ├── index.css 82 | │ └── index.js 83 | ├── .gitignore 84 | ├── package.json 85 | └── README.md 86 | 87 | ``` 88 | 89 | ## Current app features ✨ 90 | 91 | - Navigation 92 | - Theme toggle for dark and light modes 93 | - Blog Post Ideas Generator 94 | - Product Description Generator 95 | - Company Bio Generator 96 | - SEO Blog Intro Paragraph Generator 97 | - LinkedIn Job Description Generator 98 | - TL;DR Text SummarizerGenerator 99 | 100 | https://user-images.githubusercontent.com/29527450/229373842-d9e8182f-a0e7-4136-a040-b736db8e34fc.mov 101 | 102 | 103 | ## Examples: 104 | 105 | ### Themes ✨ 106 | 107 | - The app features two themes: a dark theme and a light theme for your preference. 108 | - Switching between themes is easy - just click the toggle button in the navigation bar. The moon ☽ icon indicates the dark theme, while the sun ☀️ icon represents the light theme. 109 | - Your chosen theme will be saved in local storage, so you won't have to select it again as you navigate around the app. 110 | 111 | https://user-images.githubusercontent.com/29527450/229374056-fbd30eb7-d640-4187-8a18-cb2939c16b77.mov 112 | 113 | 114 | ### Generators ♺ 115 | 116 | - The app dynamically displays each generator from an array of data via routes, making it easy to add new generators in one data file. 117 | - Each generator prompts the user for input via a form, based on the generator. 118 | - When the user submits their prompt, their prompt is concatenated with a predefined context string for the generator that's defined in the data to produce the best response from GPT. 119 | - While waiting for a response, state changes to show a Loading spinner, and the card header title indicates that the app is thinking. 120 | - Once OpenAI sends a response, state changes to show a new card header title, the body of the response from OpenAI, and a copy to clipboard button for easy copying. 121 | - The copy to clipboard button changes to a "copied" state and back to the clipboard copy button after a few seconds, making it easy to copy the response multiple times if necessary. 122 | 123 | #### Product Description Generator 🛒 124 | 125 | https://user-images.githubusercontent.com/29527450/229372828-b4b30c1f-be9e-494c-a108-628b52ec6220.mov 126 | 127 | #### Company Bio Generator 🛒 128 | 129 | https://user-images.githubusercontent.com/29527450/229372925-72b03169-a992-4dd7-8a4a-33f512395005.mov 130 | -------------------------------------------------------------------------------- /api/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "api", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cors": "^2.8.5", 13 | "dotenv": "^16.0.3", 14 | "express": "^4.18.2", 15 | "node-fetch": "^3.3.1" 16 | } 17 | }, 18 | "node_modules/accepts": { 19 | "version": "1.3.8", 20 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 21 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 22 | "dependencies": { 23 | "mime-types": "~2.1.34", 24 | "negotiator": "0.6.3" 25 | }, 26 | "engines": { 27 | "node": ">= 0.6" 28 | } 29 | }, 30 | "node_modules/array-flatten": { 31 | "version": "1.1.1", 32 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 33 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 34 | }, 35 | "node_modules/body-parser": { 36 | "version": "1.20.1", 37 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", 38 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", 39 | "dependencies": { 40 | "bytes": "3.1.2", 41 | "content-type": "~1.0.4", 42 | "debug": "2.6.9", 43 | "depd": "2.0.0", 44 | "destroy": "1.2.0", 45 | "http-errors": "2.0.0", 46 | "iconv-lite": "0.4.24", 47 | "on-finished": "2.4.1", 48 | "qs": "6.11.0", 49 | "raw-body": "2.5.1", 50 | "type-is": "~1.6.18", 51 | "unpipe": "1.0.0" 52 | }, 53 | "engines": { 54 | "node": ">= 0.8", 55 | "npm": "1.2.8000 || >= 1.4.16" 56 | } 57 | }, 58 | "node_modules/bytes": { 59 | "version": "3.1.2", 60 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 61 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 62 | "engines": { 63 | "node": ">= 0.8" 64 | } 65 | }, 66 | "node_modules/call-bind": { 67 | "version": "1.0.2", 68 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 69 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 70 | "dependencies": { 71 | "function-bind": "^1.1.1", 72 | "get-intrinsic": "^1.0.2" 73 | }, 74 | "funding": { 75 | "url": "https://github.com/sponsors/ljharb" 76 | } 77 | }, 78 | "node_modules/content-disposition": { 79 | "version": "0.5.4", 80 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 81 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 82 | "dependencies": { 83 | "safe-buffer": "5.2.1" 84 | }, 85 | "engines": { 86 | "node": ">= 0.6" 87 | } 88 | }, 89 | "node_modules/content-type": { 90 | "version": "1.0.5", 91 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 92 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 93 | "engines": { 94 | "node": ">= 0.6" 95 | } 96 | }, 97 | "node_modules/cookie": { 98 | "version": "0.5.0", 99 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 100 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 101 | "engines": { 102 | "node": ">= 0.6" 103 | } 104 | }, 105 | "node_modules/cookie-signature": { 106 | "version": "1.0.6", 107 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 108 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 109 | }, 110 | "node_modules/cors": { 111 | "version": "2.8.5", 112 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 113 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 114 | "dependencies": { 115 | "object-assign": "^4", 116 | "vary": "^1" 117 | }, 118 | "engines": { 119 | "node": ">= 0.10" 120 | } 121 | }, 122 | "node_modules/data-uri-to-buffer": { 123 | "version": "4.0.1", 124 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 125 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 126 | "engines": { 127 | "node": ">= 12" 128 | } 129 | }, 130 | "node_modules/debug": { 131 | "version": "2.6.9", 132 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 133 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 134 | "dependencies": { 135 | "ms": "2.0.0" 136 | } 137 | }, 138 | "node_modules/depd": { 139 | "version": "2.0.0", 140 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 141 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 142 | "engines": { 143 | "node": ">= 0.8" 144 | } 145 | }, 146 | "node_modules/destroy": { 147 | "version": "1.2.0", 148 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 149 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 150 | "engines": { 151 | "node": ">= 0.8", 152 | "npm": "1.2.8000 || >= 1.4.16" 153 | } 154 | }, 155 | "node_modules/dotenv": { 156 | "version": "16.0.3", 157 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", 158 | "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", 159 | "engines": { 160 | "node": ">=12" 161 | } 162 | }, 163 | "node_modules/ee-first": { 164 | "version": "1.1.1", 165 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 166 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 167 | }, 168 | "node_modules/encodeurl": { 169 | "version": "1.0.2", 170 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 171 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 172 | "engines": { 173 | "node": ">= 0.8" 174 | } 175 | }, 176 | "node_modules/escape-html": { 177 | "version": "1.0.3", 178 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 179 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 180 | }, 181 | "node_modules/etag": { 182 | "version": "1.8.1", 183 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 184 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 185 | "engines": { 186 | "node": ">= 0.6" 187 | } 188 | }, 189 | "node_modules/express": { 190 | "version": "4.18.2", 191 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", 192 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", 193 | "dependencies": { 194 | "accepts": "~1.3.8", 195 | "array-flatten": "1.1.1", 196 | "body-parser": "1.20.1", 197 | "content-disposition": "0.5.4", 198 | "content-type": "~1.0.4", 199 | "cookie": "0.5.0", 200 | "cookie-signature": "1.0.6", 201 | "debug": "2.6.9", 202 | "depd": "2.0.0", 203 | "encodeurl": "~1.0.2", 204 | "escape-html": "~1.0.3", 205 | "etag": "~1.8.1", 206 | "finalhandler": "1.2.0", 207 | "fresh": "0.5.2", 208 | "http-errors": "2.0.0", 209 | "merge-descriptors": "1.0.1", 210 | "methods": "~1.1.2", 211 | "on-finished": "2.4.1", 212 | "parseurl": "~1.3.3", 213 | "path-to-regexp": "0.1.7", 214 | "proxy-addr": "~2.0.7", 215 | "qs": "6.11.0", 216 | "range-parser": "~1.2.1", 217 | "safe-buffer": "5.2.1", 218 | "send": "0.18.0", 219 | "serve-static": "1.15.0", 220 | "setprototypeof": "1.2.0", 221 | "statuses": "2.0.1", 222 | "type-is": "~1.6.18", 223 | "utils-merge": "1.0.1", 224 | "vary": "~1.1.2" 225 | }, 226 | "engines": { 227 | "node": ">= 0.10.0" 228 | } 229 | }, 230 | "node_modules/fetch-blob": { 231 | "version": "3.2.0", 232 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 233 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 234 | "funding": [ 235 | { 236 | "type": "github", 237 | "url": "https://github.com/sponsors/jimmywarting" 238 | }, 239 | { 240 | "type": "paypal", 241 | "url": "https://paypal.me/jimmywarting" 242 | } 243 | ], 244 | "dependencies": { 245 | "node-domexception": "^1.0.0", 246 | "web-streams-polyfill": "^3.0.3" 247 | }, 248 | "engines": { 249 | "node": "^12.20 || >= 14.13" 250 | } 251 | }, 252 | "node_modules/finalhandler": { 253 | "version": "1.2.0", 254 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 255 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 256 | "dependencies": { 257 | "debug": "2.6.9", 258 | "encodeurl": "~1.0.2", 259 | "escape-html": "~1.0.3", 260 | "on-finished": "2.4.1", 261 | "parseurl": "~1.3.3", 262 | "statuses": "2.0.1", 263 | "unpipe": "~1.0.0" 264 | }, 265 | "engines": { 266 | "node": ">= 0.8" 267 | } 268 | }, 269 | "node_modules/formdata-polyfill": { 270 | "version": "4.0.10", 271 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 272 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 273 | "dependencies": { 274 | "fetch-blob": "^3.1.2" 275 | }, 276 | "engines": { 277 | "node": ">=12.20.0" 278 | } 279 | }, 280 | "node_modules/forwarded": { 281 | "version": "0.2.0", 282 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 283 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 284 | "engines": { 285 | "node": ">= 0.6" 286 | } 287 | }, 288 | "node_modules/fresh": { 289 | "version": "0.5.2", 290 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 291 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 292 | "engines": { 293 | "node": ">= 0.6" 294 | } 295 | }, 296 | "node_modules/function-bind": { 297 | "version": "1.1.1", 298 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 299 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 300 | }, 301 | "node_modules/get-intrinsic": { 302 | "version": "1.2.0", 303 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", 304 | "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", 305 | "dependencies": { 306 | "function-bind": "^1.1.1", 307 | "has": "^1.0.3", 308 | "has-symbols": "^1.0.3" 309 | }, 310 | "funding": { 311 | "url": "https://github.com/sponsors/ljharb" 312 | } 313 | }, 314 | "node_modules/has": { 315 | "version": "1.0.3", 316 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 317 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 318 | "dependencies": { 319 | "function-bind": "^1.1.1" 320 | }, 321 | "engines": { 322 | "node": ">= 0.4.0" 323 | } 324 | }, 325 | "node_modules/has-symbols": { 326 | "version": "1.0.3", 327 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 328 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 329 | "engines": { 330 | "node": ">= 0.4" 331 | }, 332 | "funding": { 333 | "url": "https://github.com/sponsors/ljharb" 334 | } 335 | }, 336 | "node_modules/http-errors": { 337 | "version": "2.0.0", 338 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 339 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 340 | "dependencies": { 341 | "depd": "2.0.0", 342 | "inherits": "2.0.4", 343 | "setprototypeof": "1.2.0", 344 | "statuses": "2.0.1", 345 | "toidentifier": "1.0.1" 346 | }, 347 | "engines": { 348 | "node": ">= 0.8" 349 | } 350 | }, 351 | "node_modules/iconv-lite": { 352 | "version": "0.4.24", 353 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 354 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 355 | "dependencies": { 356 | "safer-buffer": ">= 2.1.2 < 3" 357 | }, 358 | "engines": { 359 | "node": ">=0.10.0" 360 | } 361 | }, 362 | "node_modules/inherits": { 363 | "version": "2.0.4", 364 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 365 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 366 | }, 367 | "node_modules/ipaddr.js": { 368 | "version": "1.9.1", 369 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 370 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 371 | "engines": { 372 | "node": ">= 0.10" 373 | } 374 | }, 375 | "node_modules/media-typer": { 376 | "version": "0.3.0", 377 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 378 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 379 | "engines": { 380 | "node": ">= 0.6" 381 | } 382 | }, 383 | "node_modules/merge-descriptors": { 384 | "version": "1.0.1", 385 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 386 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 387 | }, 388 | "node_modules/methods": { 389 | "version": "1.1.2", 390 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 391 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 392 | "engines": { 393 | "node": ">= 0.6" 394 | } 395 | }, 396 | "node_modules/mime": { 397 | "version": "1.6.0", 398 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 399 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 400 | "bin": { 401 | "mime": "cli.js" 402 | }, 403 | "engines": { 404 | "node": ">=4" 405 | } 406 | }, 407 | "node_modules/mime-db": { 408 | "version": "1.52.0", 409 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 410 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 411 | "engines": { 412 | "node": ">= 0.6" 413 | } 414 | }, 415 | "node_modules/mime-types": { 416 | "version": "2.1.35", 417 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 418 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 419 | "dependencies": { 420 | "mime-db": "1.52.0" 421 | }, 422 | "engines": { 423 | "node": ">= 0.6" 424 | } 425 | }, 426 | "node_modules/ms": { 427 | "version": "2.0.0", 428 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 429 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 430 | }, 431 | "node_modules/negotiator": { 432 | "version": "0.6.3", 433 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 434 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 435 | "engines": { 436 | "node": ">= 0.6" 437 | } 438 | }, 439 | "node_modules/node-domexception": { 440 | "version": "1.0.0", 441 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 442 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 443 | "funding": [ 444 | { 445 | "type": "github", 446 | "url": "https://github.com/sponsors/jimmywarting" 447 | }, 448 | { 449 | "type": "github", 450 | "url": "https://paypal.me/jimmywarting" 451 | } 452 | ], 453 | "engines": { 454 | "node": ">=10.5.0" 455 | } 456 | }, 457 | "node_modules/node-fetch": { 458 | "version": "3.3.1", 459 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.1.tgz", 460 | "integrity": "sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==", 461 | "dependencies": { 462 | "data-uri-to-buffer": "^4.0.0", 463 | "fetch-blob": "^3.1.4", 464 | "formdata-polyfill": "^4.0.10" 465 | }, 466 | "engines": { 467 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 468 | }, 469 | "funding": { 470 | "type": "opencollective", 471 | "url": "https://opencollective.com/node-fetch" 472 | } 473 | }, 474 | "node_modules/object-assign": { 475 | "version": "4.1.1", 476 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 477 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 478 | "engines": { 479 | "node": ">=0.10.0" 480 | } 481 | }, 482 | "node_modules/object-inspect": { 483 | "version": "1.12.3", 484 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 485 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", 486 | "funding": { 487 | "url": "https://github.com/sponsors/ljharb" 488 | } 489 | }, 490 | "node_modules/on-finished": { 491 | "version": "2.4.1", 492 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 493 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 494 | "dependencies": { 495 | "ee-first": "1.1.1" 496 | }, 497 | "engines": { 498 | "node": ">= 0.8" 499 | } 500 | }, 501 | "node_modules/parseurl": { 502 | "version": "1.3.3", 503 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 504 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 505 | "engines": { 506 | "node": ">= 0.8" 507 | } 508 | }, 509 | "node_modules/path-to-regexp": { 510 | "version": "0.1.7", 511 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 512 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 513 | }, 514 | "node_modules/proxy-addr": { 515 | "version": "2.0.7", 516 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 517 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 518 | "dependencies": { 519 | "forwarded": "0.2.0", 520 | "ipaddr.js": "1.9.1" 521 | }, 522 | "engines": { 523 | "node": ">= 0.10" 524 | } 525 | }, 526 | "node_modules/qs": { 527 | "version": "6.11.0", 528 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 529 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 530 | "dependencies": { 531 | "side-channel": "^1.0.4" 532 | }, 533 | "engines": { 534 | "node": ">=0.6" 535 | }, 536 | "funding": { 537 | "url": "https://github.com/sponsors/ljharb" 538 | } 539 | }, 540 | "node_modules/range-parser": { 541 | "version": "1.2.1", 542 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 543 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 544 | "engines": { 545 | "node": ">= 0.6" 546 | } 547 | }, 548 | "node_modules/raw-body": { 549 | "version": "2.5.1", 550 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 551 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 552 | "dependencies": { 553 | "bytes": "3.1.2", 554 | "http-errors": "2.0.0", 555 | "iconv-lite": "0.4.24", 556 | "unpipe": "1.0.0" 557 | }, 558 | "engines": { 559 | "node": ">= 0.8" 560 | } 561 | }, 562 | "node_modules/safe-buffer": { 563 | "version": "5.2.1", 564 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 565 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 566 | "funding": [ 567 | { 568 | "type": "github", 569 | "url": "https://github.com/sponsors/feross" 570 | }, 571 | { 572 | "type": "patreon", 573 | "url": "https://www.patreon.com/feross" 574 | }, 575 | { 576 | "type": "consulting", 577 | "url": "https://feross.org/support" 578 | } 579 | ] 580 | }, 581 | "node_modules/safer-buffer": { 582 | "version": "2.1.2", 583 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 584 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 585 | }, 586 | "node_modules/send": { 587 | "version": "0.18.0", 588 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 589 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 590 | "dependencies": { 591 | "debug": "2.6.9", 592 | "depd": "2.0.0", 593 | "destroy": "1.2.0", 594 | "encodeurl": "~1.0.2", 595 | "escape-html": "~1.0.3", 596 | "etag": "~1.8.1", 597 | "fresh": "0.5.2", 598 | "http-errors": "2.0.0", 599 | "mime": "1.6.0", 600 | "ms": "2.1.3", 601 | "on-finished": "2.4.1", 602 | "range-parser": "~1.2.1", 603 | "statuses": "2.0.1" 604 | }, 605 | "engines": { 606 | "node": ">= 0.8.0" 607 | } 608 | }, 609 | "node_modules/send/node_modules/ms": { 610 | "version": "2.1.3", 611 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 612 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 613 | }, 614 | "node_modules/serve-static": { 615 | "version": "1.15.0", 616 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 617 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 618 | "dependencies": { 619 | "encodeurl": "~1.0.2", 620 | "escape-html": "~1.0.3", 621 | "parseurl": "~1.3.3", 622 | "send": "0.18.0" 623 | }, 624 | "engines": { 625 | "node": ">= 0.8.0" 626 | } 627 | }, 628 | "node_modules/setprototypeof": { 629 | "version": "1.2.0", 630 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 631 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 632 | }, 633 | "node_modules/side-channel": { 634 | "version": "1.0.4", 635 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 636 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 637 | "dependencies": { 638 | "call-bind": "^1.0.0", 639 | "get-intrinsic": "^1.0.2", 640 | "object-inspect": "^1.9.0" 641 | }, 642 | "funding": { 643 | "url": "https://github.com/sponsors/ljharb" 644 | } 645 | }, 646 | "node_modules/statuses": { 647 | "version": "2.0.1", 648 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 649 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 650 | "engines": { 651 | "node": ">= 0.8" 652 | } 653 | }, 654 | "node_modules/toidentifier": { 655 | "version": "1.0.1", 656 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 657 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 658 | "engines": { 659 | "node": ">=0.6" 660 | } 661 | }, 662 | "node_modules/type-is": { 663 | "version": "1.6.18", 664 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 665 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 666 | "dependencies": { 667 | "media-typer": "0.3.0", 668 | "mime-types": "~2.1.24" 669 | }, 670 | "engines": { 671 | "node": ">= 0.6" 672 | } 673 | }, 674 | "node_modules/unpipe": { 675 | "version": "1.0.0", 676 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 677 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 678 | "engines": { 679 | "node": ">= 0.8" 680 | } 681 | }, 682 | "node_modules/utils-merge": { 683 | "version": "1.0.1", 684 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 685 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 686 | "engines": { 687 | "node": ">= 0.4.0" 688 | } 689 | }, 690 | "node_modules/vary": { 691 | "version": "1.1.2", 692 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 693 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 694 | "engines": { 695 | "node": ">= 0.8" 696 | } 697 | }, 698 | "node_modules/web-streams-polyfill": { 699 | "version": "3.2.1", 700 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", 701 | "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", 702 | "engines": { 703 | "node": ">= 8" 704 | } 705 | } 706 | } 707 | } 708 | --------------------------------------------------------------------------------