├── .gitignore ├── LICENSE.txt ├── README.md ├── api ├── dotEnv ├── index.js ├── lib │ ├── error.js │ ├── httpServer.js │ ├── promise-any.js │ └── routes.js ├── package.json └── services │ └── cohere-service.js ├── app ├── .gitignore ├── .prettierrc.cjs ├── .vscode │ ├── extensions.json │ └── launch.json ├── README.md ├── astro.config.mjs ├── package.json ├── postcss.config.js ├── public │ └── favicon.svg ├── src │ ├── components │ │ ├── Ai.jsx │ │ ├── CohereSVG.astro │ │ ├── Footer.astro │ │ ├── GithubSVG.astro │ │ ├── LinkedInSVG.astro │ │ ├── Nav.astro │ │ ├── Response.jsx │ │ └── TwitchSVG.astro │ ├── env.d.ts │ ├── hooks │ │ └── api.js │ ├── layouts │ │ └── Layout.astro │ └── pages │ │ ├── api.astro │ │ ├── clickbait.astro │ │ ├── cta.astro │ │ └── index.astro ├── tailwind.config.cjs └── tsconfig.json ├── package.json ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | dist/ 4 | .output/ 5 | # generated types 6 | .astro/ 7 | 8 | # dependencies 9 | node_modules/ 10 | 11 | # logs 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | pnpm-debug.log* 16 | 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christian Villegas 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 | [](https://persuaderai.christianvillegas.com/) 2 | 3 | # Persuader.ai 4 | 5 | ## What is this? 6 | 7 | The Persuader.ai is a tool designed to assist content creators in the creation of persuasive clickbait titles and Call-to-Action (CTA) phrases. It aims to inspire content creators and encourage the consumption of their content by potential viewers, readers, listeners, and customers. 8 | 9 | The tool can be utilized as is or integrated through its API for automated content publishing. For those seeking greater independence, the source code can be obtained from the [github repository](https://github.com/chrisvill2312/persuader-ai) for deployment on personal systems. 10 | 11 | (This tool was created as part of the applications created for the [Midudev's AI Hackaton constest](https://github.com/topics/midudev-cohere-2023)) 12 | 13 | ## Persuader.ai's API 14 | 15 | The endpoints of this API are: 16 | (Using https://persuaderai.christianvillegas.com/ as a base URL) 17 | 18 | A POST to "/api/clickbait" generate clickbait titles. 19 | A POST to "/api/cta" generate Call-to-Action (CTA) phrases. 20 | 21 | You need to give the prompt and quantity in a JSON on the body of the HTTP request on both of the endpoint, someting like this: 22 | 23 | { 24 | "input":"the result of being a bad person", 25 | "quantity": 2 26 | } 27 | 28 | 29 | As a result the server gives you JSON with an array of strigs like this: 30 | 31 | [ 32 | "This Is What Happens When You Are A Bad Person", 33 | "This Is What Happens If You Are A Bad Person" 34 | ] 35 | 36 | ## If you want to fork or download this project 37 | Don't forget to create a .env file on the API folder. It should contained: 38 | 39 | 40 | 41 | API_KEY=ofCohere'sAPI 42 | PORT=10002 // the port of the http server 43 | -------------------------------------------------------------------------------- /api/dotEnv: -------------------------------------------------------------------------------- 1 | API_KEY=ofCohere'sAPI 2 | PORT=10002 -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import httpServer from './lib/httpServer.js' 2 | import * as dotenv from 'dotenv' 3 | dotenv.config() 4 | const { PORT } = process.env 5 | 6 | httpServer.listen(PORT, () => { console.log(`Listening on port ${PORT}`) }) 7 | -------------------------------------------------------------------------------- /api/lib/error.js: -------------------------------------------------------------------------------- 1 | export default function createCustomError (type = 'error') { 2 | class CustomError extends Error { 3 | constructor (error) { 4 | super() 5 | this.name = type 6 | this.message = error 7 | } 8 | } 9 | return CustomError 10 | } 11 | -------------------------------------------------------------------------------- /api/lib/httpServer.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import morgan from 'morgan' 3 | import cors from 'cors' 4 | import router from './routes.js' 5 | 6 | const httpServer = express() 7 | 8 | httpServer.use(morgan('dev')) 9 | httpServer.use((req, res, next) => { 10 | res.set('Cache-Control', 'no-store') 11 | next() 12 | }) 13 | httpServer.use(express.json()) 14 | httpServer.use(express.urlencoded({ extended: true })) 15 | httpServer.use(cors()) 16 | httpServer.use('/', express.static('../app/dist')) 17 | httpServer.use('/', router) 18 | 19 | export default httpServer 20 | 21 | /* 22 | 23 | * I've did the for sentence because cohere could delay to much sometimes and a parallel call generally 24 | get a most quick response. Yes! i Know, is bad coding but i don't want to wait for the free version 25 | of the api, no matter if it means use more calls 26 | 27 | */ 28 | -------------------------------------------------------------------------------- /api/lib/promise-any.js: -------------------------------------------------------------------------------- 1 | // A ES6 implementation of the m0ppers's promise.any library for the missing 2 | // method any on the class Promise https://github.com/m0ppers/promise-any 3 | 4 | function reverse (promise) { 5 | return new Promise((resolve, reject) => Promise.resolve(promise).then(reject, resolve)) 6 | } 7 | 8 | export default function promiseAny (iterable) { 9 | return reverse(Promise.all([...iterable].map(reverse))) 10 | }; 11 | -------------------------------------------------------------------------------- /api/lib/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { generateClickbait, generateCTA } from '../services/cohere-service.js' 3 | 4 | const router = Router() 5 | 6 | function noNeededInfo (input, quantity) { 7 | if (!input || !quantity) return true 8 | if (input.length === 0) return true 9 | if (quantity < 1 || quantity > 5) return true 10 | return false 11 | } 12 | router.post('/api/clickbait', (req, res) => { 13 | if (noNeededInfo(req.body.input, req.body.quantity)) res.status(400).send({ error: 'missing or wrong data.' }) 14 | else { 15 | generateClickbait(req.body.input, req.body.quantity) 16 | .then((result) => res.status(200).json(result)) 17 | .catch(() => res.status(503).json(['AI Service error'])) 18 | } 19 | }) 20 | router.post('/api/cta', (req, res) => { 21 | if (noNeededInfo(req.body.input, req.body.quantity)) res.status(400).send({ error: 'missing or wrong data.' }) 22 | else { 23 | generateCTA(req.body.input, req.body.quantity) 24 | .then((result) => res.status(200).json(result)) 25 | .catch(() => res.status(503).json(['AI Service error'])) 26 | } 27 | }) 28 | 29 | export default router 30 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js", 9 | "dev": "node --watch index.js", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "lint": "eslint" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "ISC", 16 | "dependencies": { 17 | "cohere-ai": "^5.0.2", 18 | "cors": "^2.8.5", 19 | "dotenv": "^16.0.3", 20 | "express": "^4.18.2", 21 | "morgan": "^1.10.0" 22 | }, 23 | "devDependencies": { 24 | "eslint": "^8.29.0", 25 | "standard": "^17.0.0" 26 | }, 27 | "eslintConfig": { 28 | "extends": "standard" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/services/cohere-service.js: -------------------------------------------------------------------------------- 1 | import * as dotenv from 'dotenv' 2 | import cohere from 'cohere-ai' 3 | // import createCustomError from '../lib/error.js' 4 | dotenv.config() 5 | 6 | const CTAPrompt = ` 7 | this is a call to action phrase generator from given action. 8 | -- 9 | action: click the button 10 | phrase: Click this now! 11 | -- 12 | action: watch a new video 13 | phrase: Enjoy the new awesome video! 14 | -- 15 | action: enter in the stream 16 | phrase: Get in the Stream and let's have some fun! 17 | -- 18 | action: hear the podcast 19 | phrase: Let's listen to this podcast! 20 | -- 21 | action: don't wait 22 | phrase: what are you waiting for? 23 | -- 24 | action: hurry 25 | phrase: don't loose the time! 26 | -- 27 | action: {action} 28 | phrase: ` 29 | 30 | const CTAGen = { 31 | max_tokens: 10, 32 | temperature: 0.5, 33 | k: 0, 34 | p: 1, 35 | frequency_penalty: 1, 36 | presence_penalty: 0, 37 | stop_sequences: ['--'], 38 | return_likelihoods: 'NONE' 39 | } 40 | 41 | const CBPrompt = ` 42 | this is a clickbait title generator from given topic. 43 | -- 44 | topic: we show an interview with the famous actor Will Smith 45 | title: Will Smith tells us everything! 46 | -- 47 | topic: We show how make a dinner 48 | title: You’ll Never Believe this Simple Method to make a dinner! 49 | -- 50 | topic: We show 9 methods to get a job 51 | title: 9 methods to get a job That You Need to Know! 52 | -- 53 | topic: we show a best way to invest money 54 | title: This Weird Trick Increased your money outrageously! 55 | -- 56 | topic: we show the effects of don't take care of your health 57 | title: This Is What Happens if You Stop Worrying Too Much about your health 58 | -- 59 | topic: we show 7 tools to Boost a Blog Engagement 60 | title: The 7 Best Visual Content Tools to Boost your Blog’s Engagement! 61 | -- 62 | topic: we show {topic} 63 | title: ` 64 | 65 | const CBGen = { 66 | max_tokens: 20, 67 | temperature: 0.3, 68 | k: 0, 69 | p: 1, 70 | frequency_penalty: 1, 71 | presence_penalty: 0, 72 | stop_sequences: ['--'], 73 | return_likelihoods: 'NONE' 74 | } 75 | 76 | cohere.init(process.env.API_KEY, '2022-12-06') 77 | 78 | export async function generateClickbait (topic, quantity) { 79 | let input = { ...CBGen, prompt: CBPrompt.replace('{topic}', topic) } 80 | quantity && (input = { ...input, num_generations: quantity }) 81 | try { 82 | const generated = await cohere.generate(input) 83 | console.log(generated.body.generations) 84 | const response = generated.body.generations.map((result) => result.text.split('\n')[0]) 85 | console.log(response) 86 | return response 87 | } catch (error) { 88 | // const ClickbaitError = createCustomError('ClickbaitGenerationError') 89 | // return new ClickbaitError(error.message) 90 | return 'error' 91 | } 92 | } 93 | 94 | export async function generateCTA (action, quantity) { 95 | let input = { ...CTAGen, prompt: CTAPrompt.replace('{action}', action) } 96 | quantity && (input = { ...input, num_generations: quantity }) 97 | try { 98 | const generated = await cohere.generate(input) 99 | console.log(generated.body.generations) 100 | const response = generated.body.generations.map((result) => result.text.split('\n')[0]) 101 | console.log(response) 102 | return response 103 | } catch (error) { 104 | // const CTAError = createCustomError('CTAGenerationError') 105 | // return new CTAError(error.message) 106 | return 'error' 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | # generated types 5 | .astro/ 6 | 7 | # dependencies 8 | node_modules/ 9 | 10 | # logs 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | 17 | # environment variables 18 | .env 19 | .env.production 20 | 21 | # macOS-specific files 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /app/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require.resolve('prettier-plugin-astro')], 3 | overrides: [ 4 | { 5 | files: '*.astro', 6 | options: { 7 | parser: 'astro', 8 | }, 9 | }, 10 | ], 11 | }; -------------------------------------------------------------------------------- /app/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to [Astro](https://astro.build) 2 | 3 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/basics) 4 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/s/github/withastro/astro/tree/latest/examples/basics) 5 | 6 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 7 | 8 | ![basics](https://user-images.githubusercontent.com/4677417/186188965-73453154-fdec-4d6b-9c34-cb35c248ae5b.png) 9 | 10 | 11 | ## 🚀 Project Structure 12 | 13 | Inside of your Astro project, you'll see the following folders and files: 14 | 15 | ``` 16 | / 17 | ├── public/ 18 | │ └── favicon.svg 19 | ├── src/ 20 | │ ├── components/ 21 | │ │ └── Card.astro 22 | │ ├── layouts/ 23 | │ │ └── Layout.astro 24 | │ └── pages/ 25 | │ └── index.astro 26 | └── package.json 27 | ``` 28 | 29 | Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. 30 | 31 | There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. 32 | 33 | Any static assets, like images, can be placed in the `public/` directory. 34 | 35 | ## 🧞 Commands 36 | 37 | All commands are run from the root of the project, from a terminal: 38 | 39 | | Command | Action | 40 | | :--------------------- | :------------------------------------------------- | 41 | | `npm install` | Installs dependencies | 42 | | `npm run dev` | Starts local dev server at `localhost:3000` | 43 | | `npm run build` | Build your production site to `./dist/` | 44 | | `npm run preview` | Preview your build locally, before deploying | 45 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` | 46 | | `npm run astro --help` | Get help using the Astro CLI | 47 | 48 | ## 👀 Want to learn more? 49 | 50 | Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). 51 | -------------------------------------------------------------------------------- /app/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | 3 | // https://astro.build/config 4 | import react from "@astrojs/react"; 5 | 6 | // https://astro.build/config 7 | import tailwind from "@astrojs/tailwind"; 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | integrations: [react(), tailwind()] 12 | }); -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/basics", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro" 12 | }, 13 | "dependencies": { 14 | "@astrojs/react": "^2.0.0", 15 | "@astrojs/tailwind": "^3.0.0", 16 | "@types/react": "^18.0.21", 17 | "@types/react-dom": "^18.0.6", 18 | "@uiball/loaders": "^1.2.6", 19 | "astro": "^2.0.1", 20 | "axios": "^1.2.6", 21 | "react": "^18.0.0", 22 | "react-dom": "^18.0.0", 23 | "tailwindcss": "^3.0.24" 24 | }, 25 | "proxy": "http://localhost:10002", 26 | "devDependencies": { 27 | "prettier": "^2.8.3", 28 | "prettier-plugin-astro": "^0.8.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('cssnano'), 5 | ], 6 | }; -------------------------------------------------------------------------------- /app/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/components/Ai.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import useAPI, { CB, CTA } from "../hooks/api.js"; 3 | import Response from "./Response.jsx"; 4 | import { LeapFrog } from "@uiball/loaders"; 5 | 6 | function Ai(props) { 7 | const { mode } = props; 8 | const [input, quantity] = 9 | mode === CB 10 | ? ["how to learn a programing language", 1] 11 | : ["Like this video", 1]; 12 | const [userInput, AIResponse, setInput] = useAPI(input, quantity, mode); 13 | const [toSubmit, setSubmit] = useState({ clickbaitInput: "", quantity: 1 }); 14 | const inputRef = React.createRef(); 15 | const eventHandler = (e) => { 16 | let modification = {}; 17 | if (e.target.name === "quantity") { 18 | if (parseInt(e.target.value) < 1) modification = { [e.target.name]: 1 }; 19 | if (parseInt(e.target.value) > 5) modification = { [e.target.name]: 5 }; 20 | if (!modification.quantity) 21 | modification = { [e.target.name]: parseInt(e.target.value) }; 22 | } else modification = { [e.target.name]: e.target.value }; 23 | 24 | /* 25 | e.target.name === "quantity" 26 | ? parseInt(e.target.value) < 1 27 | ? { quantity: 1 } 28 | : { quantity: parseInt(e.target.value) } 29 | : { [e.target.name]: e.target.value }; 30 | PirxJ me salvo en esta linea de una quemada de cabeza (cosas de stream) */ // Para el recuerdo del stream de Twitch 31 | 32 | setSubmit((state) => ({ ...state, ...modification })); 33 | }; 34 | const runSubmit = (e) => { 35 | e.preventDefault(); 36 | setInput(toSubmit.clickbaitInput, toSubmit.quantity); 37 | }; 38 | 39 | useEffect(() => console.log(AIResponse), [AIResponse]); 40 | 41 | return ( 42 | <> 43 |
47 |

{`${ 48 | mode === CTA ? "CTA" : "Clickbait" 49 | } generator`}

50 |
51 | 56 |
57 | 60 | 68 |
69 |
70 | 73 | 81 |
82 | 88 |
89 |
90 |
91 | {Array.isArray(AIResponse) ? ( 92 | AIResponse.length > 0 ? ( 93 | AIResponse.map((response, i) => ( 94 | 95 | )) 96 | ) : ( 97 |

101 | { 102 | `Creating the best ${mode === CTA ? "CTA" : "Clickbait"}${ 103 | toSubmit.quantity > 1 ? "s" : "" 104 | } for you` /* UX inspirited from GermanL3t */ 105 | } 106 |

107 | 108 |
109 |

110 | ) 111 | ) : ( 112 |

115 | Sorry but at this moment the provider of the service isn't responding 😢, try again in a moment 116 |

117 | )} 118 |
119 | 120 | ); 121 | } 122 | 123 | export default Ai; 124 | -------------------------------------------------------------------------------- /app/src/components/CohereSVG.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { size, className = "" } = Astro.props; 3 | --- 4 | 5 | 15 | -------------------------------------------------------------------------------- /app/src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TwitchSVG from "./TwitchSVG.astro"; 3 | import LinkedInSVG from "./LinkedInSVG.astro"; 4 | import GithubSVG from "./GithubSVG.astro"; 5 | import CohereSVG from "./CohereSVG.astro"; 6 | const size = "1.15rem"; 7 | const color = ""; 8 | --- 9 | 10 | 82 | -------------------------------------------------------------------------------- /app/src/components/GithubSVG.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { size, className = "" } = Astro.props; 3 | --- 4 | 5 | 16 | -------------------------------------------------------------------------------- /app/src/components/LinkedInSVG.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { size, className = "" } = Astro.props; 3 | --- 4 | 5 | 19 | -------------------------------------------------------------------------------- /app/src/components/Nav.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const btnStyle = 3 | "flex py-2 px-4 my-1 mx-2 border rounded border-amber-600 hover:border-amber-400 hover:scale-105 hover:bg-indigo-700 active:scale-95 active:border-amber-200 active:bg-indigo-500 sm:text-xl transition-all"; 4 | // 5 | --- 6 | 7 | 8 |

11 | Persuader.ai 12 |

13 |
14 | 21 | -------------------------------------------------------------------------------- /app/src/components/Response.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | function Response({ response, i }) { 4 | const [copyStatus, setCopyStatus] = useState({ 5 | copied: false, 6 | achievedCopy: false, 7 | }); 8 | const copiedEffect = (success) => { 9 | setCopyStatus({ copied: true, achievedCopy: success }); 10 | setTimeout(() => { 11 | setCopyStatus((state) => { 12 | return { ...state, copied: false }; 13 | }); 14 | }, 2000); 15 | }; 16 | 17 | const copier = () => { 18 | navigator.clipboard 19 | .writeText(response) 20 | .then(() => { 21 | copiedEffect(true); 22 | }) 23 | .catch(() => { 24 | copiedEffect(false); 25 | }); 26 | }; 27 | 28 | return ( 29 |

35 | {!copyStatus.copied 36 | ? response 37 | : copyStatus.achievedCopy 38 | ? "Copied to the clipboard!" 39 | : "Error copying to the clipboard 😭"} 40 |

41 | ); 42 | } 43 | 44 | export default Response; 45 | -------------------------------------------------------------------------------- /app/src/components/TwitchSVG.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { size, className = "" } = Astro.props; 3 | --- 4 | 5 | 13 | 14 | 15 | 17 | 18 | 19 | twitch 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/src/hooks/api.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import axios from "axios" 3 | 4 | export const [CB, CTA] = ['clickbait', 'cta'] 5 | 6 | export default function useAPI(input = "how to learn a programming language", quantity = 1, mode = CB) { 7 | const [userInput, setUserInput] = useState({ input, quantity }) 8 | const [AIResponse, setAIResponse] = useState([]) 9 | useEffect(() => { 10 | setAIResponse([]) 11 | const config = { 12 | url: `http://localhost:10002/api/${mode}`, // to be replaced with `${document.location.origin}/api/${mode}` on build 13 | method: 'POST', 14 | data: { 15 | input: userInput.input, 16 | quantity: userInput.quantity 17 | } 18 | } 19 | axios(config) 20 | .then(response => { 21 | setAIResponse(response.data) 22 | }) 23 | .catch( 24 | error => console.error(error) 25 | ) 26 | }, [userInput]) 27 | const setInput = (input, quantity) => { quantity ? setUserInput({ input, quantity }) : setUserInput((state) => ({ ...state, input })) } 28 | return [userInput, AIResponse, setInput] 29 | } -------------------------------------------------------------------------------- /app/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { title } = Astro.props; 3 | import Nav from "../components/Nav.astro"; 4 | import Footer from "../components/Footer.astro"; 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {title} 16 | 17 | 18 |