├── .env.sample ├── .gitignore ├── LICENSE ├── README.md ├── client ├── extension │ ├── build.ts │ ├── package.json │ ├── src │ │ ├── cursor.tsx │ │ ├── icons │ │ │ ├── icon128.png │ │ │ ├── icon16.png │ │ │ ├── icon32.png │ │ │ └── icon48.png │ │ ├── lib │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── Button │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ │ ├── Header │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ │ ├── Logo │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ │ ├── Message │ │ │ │ │ │ ├── input.tsx │ │ │ │ │ │ ├── messages.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ │ ├── Navigation │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── styles.ts │ │ │ │ │ └── Workflow │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── firebaseClient │ │ │ │ │ └── index.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── layout │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── styles.ts │ │ │ │ ├── pages │ │ │ │ │ ├── createWorkflow.tsx │ │ │ │ │ ├── login.tsx │ │ │ │ │ ├── messages.tsx │ │ │ │ │ ├── setup.tsx │ │ │ │ │ └── workflows.tsx │ │ │ │ ├── store │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── messageSlice.ts │ │ │ │ │ ├── pageSlice.ts │ │ │ │ │ ├── userSlice.ts │ │ │ │ │ └── workflowSlice.ts │ │ │ │ ├── styles │ │ │ │ │ ├── createWorkflow.ts │ │ │ │ │ ├── global.ts │ │ │ │ │ ├── login.ts │ │ │ │ │ ├── setup.ts │ │ │ │ │ └── workflows.ts │ │ │ │ └── utils │ │ │ │ │ ├── injectBlockId.ts │ │ │ │ │ ├── message.ts │ │ │ │ │ └── types.ts │ │ │ └── worker │ │ │ │ ├── action.ts │ │ │ │ ├── index.ts │ │ │ │ ├── login.ts │ │ │ │ ├── message.ts │ │ │ │ ├── recorder.ts │ │ │ │ ├── setup.ts │ │ │ │ ├── utils.ts │ │ │ │ └── workflows.ts │ │ ├── manifest.json │ │ ├── recorder.ts │ │ ├── run.tsx │ │ ├── ui.html │ │ ├── ui.tsx │ │ └── worker.ts │ └── tsconfig.json └── web │ ├── .gitignore │ ├── README.md │ ├── components.json │ ├── components │ ├── atoms │ │ ├── CheckboxWithLabel │ │ │ └── index.tsx │ │ ├── Icon │ │ │ ├── Confetti.tsx │ │ │ ├── Landingpage │ │ │ │ ├── AIAnswer.tsx │ │ │ │ ├── PreviewWidget.tsx │ │ │ │ ├── Semantic.tsx │ │ │ │ ├── Showcase.tsx │ │ │ │ └── Upload.tsx │ │ │ └── Star.tsx │ │ ├── Loader │ │ │ ├── DashboardLoader.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Logo │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── ModalLayout │ │ │ ├── index.tsx │ │ │ └── styles.ts │ ├── molecules │ │ ├── BottomCTA │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── DashboardHeaderInfo │ │ │ └── index.tsx │ │ ├── Features │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Footer │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Layout │ │ │ ├── LandingPageLayout.tsx │ │ │ └── LoggedInLayout.tsx │ │ ├── Modals │ │ │ ├── SignIn.tsx │ │ │ └── styles.ts │ │ ├── Showcase │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── SignInButtons │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Timer │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ └── TinyBanner │ │ │ ├── index.tsx │ │ │ └── styles.ts │ └── ui │ │ ├── button.tsx │ │ ├── checkbox.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── switch.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts │ ├── firebaseAdmin │ ├── cert_sample │ │ ├── dev_sample.json │ │ └── prod_sample.json │ └── index.ts │ ├── firebaseClient │ └── index.ts │ ├── lib │ ├── axios.ts │ └── utils.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── auth.ts │ │ ├── billing │ │ │ ├── info.ts │ │ │ ├── ltd.ts │ │ │ ├── ltdhook.ts │ │ │ └── manage.ts │ │ └── user.ts │ ├── dashboard │ │ ├── index.tsx │ │ ├── onboarding.tsx │ │ ├── stats.tsx │ │ └── workflows.tsx │ ├── index.tsx │ ├── ltdpricing.tsx │ └── privacy.tsx │ ├── postcss.config.js │ ├── prisma │ ├── index.ts │ ├── migrations │ │ ├── 20231221162954_initial │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma │ ├── public │ ├── favicon.ico │ └── logo.png │ ├── serverLib │ ├── db │ │ ├── plan.ts │ │ └── user.ts │ ├── plans │ │ └── index.ts │ └── utils │ │ ├── stripeClient.ts │ │ ├── verifyToken.ts │ │ └── withProtectApi.ts │ ├── store │ ├── index.ts │ ├── modalSlice.ts │ └── userSlice.ts │ ├── styles │ ├── dashboard.ts │ ├── globals.css │ ├── index.ts │ ├── onboarding.ts │ ├── pricing.ts │ └── privacy.ts │ ├── tailwind.config.js │ └── tsconfig.json ├── package.json └── server ├── Cargo.toml ├── package.json └── src ├── api ├── execute.rs ├── health.rs ├── mod.rs ├── user.rs └── workflow.rs ├── common ├── db │ ├── action.rs │ ├── execute.rs │ ├── mod.rs │ ├── plan.rs │ ├── stats.rs │ ├── task.rs │ ├── user.rs │ └── workflow.rs ├── dom │ ├── mod.rs │ ├── parse.rs │ └── search.rs ├── gpt │ ├── action.rs │ ├── mod.rs │ ├── prompts.rs │ └── tasks.rs ├── mod.rs └── types.rs ├── main.rs ├── schema.rs └── utils ├── config.rs ├── load_env.rs └── mod.rs /.env.sample: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_API_KEY="" 2 | NEXT_PUBLIC_AUTH_DOMAIN="" 3 | NEXT_PUBLIC_PROJECT_ID="" 4 | NEXT_PUBLIC_STORAGE_BUCKET="" 5 | NEXT_PUBLIC_MESSAGING_SENDER_ID="" 6 | NEXT_PUBLIC_APP_ID="" 7 | NEXT_PUBLIC_HOST=http://localhost:3000 # or if production then domain name ex:https://aiemploye.com 8 | NEXT_PUBLIC_GOOGLE_ANALYTICS_ID="" 9 | NEXT_PUBLIC_POSTHOG_CLIENT_KEY="" 10 | NEXT_PUBLIC_CRISP_CLIENT_KEY="" 11 | 12 | OPEN_AI_CHAT_COMPLETION_API="https://api.openai.com/v1/chat/completions" 13 | BACKEND_URL="http://localhost:8080" # or if production then domain name ex:https://api.aiemploye.com 14 | FRONTEND_URL="http://localhost:3000" # or if production then domain name ex:https://aiemploye.com 15 | 16 | OPEN_AI_API_KEY="" 17 | MEILISEARCH_API_KEY="" 18 | MEILISEARCH_URL="http://127.0.0.1:7700" 19 | DATABASE_URL="" 20 | FIREBASE_APP_ID="" 21 | BACKEND_PORT=8080 # port for api server(https://api.aiemploye.com) 22 | 23 | STRIPE_SK_KEY="" 24 | STRIPE_STARTER_PLAN_PRICE_ID="" 25 | STRIPE_PRO_PLAN_PRICE_ID="" 26 | STRIPE_PREMIUM_PLAN_PRICE_ID="" 27 | STRIPE_WEBHOOK_SECRET="" 28 | PORT=3000 # if production then port 80 29 | 30 | DEPLOYMENT_TYPE="saas" # open-source or "saas", if saas, application will record usage metrics and limit usage based on the plan -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bun.lockb 3 | package-lock.json 4 | .idea 5 | .next 6 | .DS_Store 7 | .vscode 8 | target 9 | Cargo.lock 10 | build 11 | .env.* 12 | meilisearch_data 13 | cert 14 | log_* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Install 2 | 3 | Try without Firebase authentication (temporary solution): https://github.com/vignshwarar/AI-Employe/issues/2#issuecomment-1880328518 4 | 5 | Our stack consists of Next.js, Rust, Postgres, MeiliSearch, and Firebase Auth for authentication. Please sign up for a Firebase account and create a project. 6 | 7 | In Firebase, navigate to Project settings -> Service accounts, generate a private key, and save it inside ```firebaseAdmin/cert/dev.json``` if it's for development or prod.json if it's for production. 8 | 9 | After that, make sure you install the dependencies before starting the app. 10 | 11 | 12 | 13 | - Copy the the .env.sample file to .env.production or .env.development 14 | - Fill the .env file with your credentials 15 | - Run `npm install` 16 | - Run `npm run db:deploy` 17 | - Run `npm run dev` (for development) 18 | - Run `npm run build` (for production) 19 | - Run `npm run start` (for production) 20 | 21 | Once you have run 'dev' or 'build', you will find the extension built inside the `./client/extension/build` folder. You can then load this folder as an unpacked extension in your browser. 22 | 23 | ## How it Works 24 | 25 | There are several problems with current browser agents. Here, we explain the problems and how we have solved them. 26 | 27 | ### Problem 1: Finding the Right Element 28 | 29 | There are several techniques for this, ranging from sending a shortened form of HTML to GPT-3, creating a bounding box with IDs and sending it to GPT-4-vision to take actions, or directly asking GPT-4-vision to obtain the X and Y coordinates of the element. However, none of these methods were reliable; they all led to hallucinations. 30 | 31 | To address this, we developed a new technique where we [index](https://github.com/vignshwarar/AI-Employe/blob/db530101c9fd9a0f0d7ce3eeac033e70cb172541/server/src/common/dom/search.rs#L9) the entire DOM in MeiliSearch, allowing GPT-4-vision to generate commands for which element's inner text to click, copy, or perform other actions. We then [search](https://github.com/vignshwarar/AI-Employe/blob/db530101c9fd9a0f0d7ce3eeac033e70cb172541/server/src/common/dom/search.rs#L46) the index with the generated text and retrieve the element ID to send back to the browser to take action. There are a few limitations here, but we have implemented some techniques to overcome them, such as dealing with the same text in multiple elements or clicking on an icon (we are still working on this). 32 | 33 | ### Problem 2: GPT Derailing from Workflow 34 | 35 | To prevent GPT from derailing from tasks, we use a technique that is akin to retrieval-augmented generation, but we kind of call it Actions Augmented Generation. Essentially, when a user creates a workflow, we don't record the screen, microphone, or camera, but we do record the DOM element changes for every action (clicking, typing, etc.) the user takes. We then use the workflow title, objective, and recorded actions to generate a set of tasks. Whenever we execute a task, we embed all the actions the user took on that particular domain with the prompt. This way, GPT stays on track with the task, even if the user has not provided a very brief title and objective; their actions will guide GPT to complete the task. 36 | 37 | ## Roadmap 38 | 39 | - [x] Workflows 40 | - [x] Chat with what you see 41 | - [ ] More actions support scrolling, opening links in a new tab, etc. 42 | - [ ] Loop in workflows 43 | - [ ] Clever Tab management 44 | - [ ] Share workflows 45 | - [ ] Open source models support 46 | - [ ] Community shared workflows 47 | - [ ] Cloud version of AI Employe 48 | - [ ] Control browser by text 49 | - [ ] Control browser by voice 50 | - [ ] more to come... 51 | -------------------------------------------------------------------------------- /client/extension/build.ts: -------------------------------------------------------------------------------- 1 | import * as esbuild from "esbuild"; 2 | import * as fs from "fs"; 3 | import { file, write } from "bun"; 4 | 5 | const entrypoints = [ 6 | "src/worker.ts", 7 | "src/recorder.ts", 8 | "src/ui.tsx", 9 | "src/run.tsx", 10 | ]; 11 | 12 | const commonBuildOptions = { 13 | entryPoints: entrypoints, 14 | bundle: true, 15 | outdir: "./build", 16 | format: "esm", 17 | target: "es2017", 18 | sourcemap: true, 19 | define: { 20 | "process.env.BACKEND_URL": JSON.stringify(process.env.BACKEND_URL), 21 | "process.env.FRONTEND_URL": JSON.stringify(process.env.FRONTEND_URL), 22 | "process.env.NEXT_PUBLIC_API_KEY": JSON.stringify( 23 | process.env.NEXT_PUBLIC_API_KEY 24 | ), 25 | "process.env.NEXT_PUBLIC_AUTH_DOMAIN": JSON.stringify( 26 | process.env.NEXT_PUBLIC_AUTH_DOMAIN 27 | ), 28 | "process.env.NEXT_PUBLIC_PROJECT_ID": JSON.stringify( 29 | process.env.NEXT_PUBLIC_PROJECT_ID 30 | ), 31 | "process.env.NEXT_PUBLIC_STORAGE_BUCKET": JSON.stringify( 32 | process.env.NEXT_PUBLIC_STORAGE_BUCKET 33 | ), 34 | "process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID": JSON.stringify( 35 | process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID 36 | ), 37 | "process.env.NEXT_PUBLIC_APP_ID": JSON.stringify( 38 | process.env.NEXT_PUBLIC_APP_ID 39 | ), 40 | }, 41 | }; 42 | 43 | const copyAssets = async () => { 44 | const assets = ["manifest.json", "ui.html"]; 45 | fs.mkdirSync("build", { recursive: true }); 46 | await Promise.all( 47 | assets.map((asset) => write(`build/${asset}`, file(`./src/${asset}`))) 48 | ); 49 | 50 | fs.mkdirSync("build/icons", { recursive: true }); 51 | await Promise.all( 52 | fs 53 | .readdirSync("./src/icons") 54 | .map((icon) => write(`build/icons/${icon}`, file(`./src/icons/${icon}`))) 55 | ); 56 | }; 57 | 58 | async function build(isDev: boolean) { 59 | const buildOptions = isDev 60 | ? { ...commonBuildOptions } 61 | : { ...commonBuildOptions, minify: true }; 62 | 63 | if (isDev) { 64 | const ctx = await esbuild.context(buildOptions as esbuild.BuildOptions); 65 | await ctx.watch(); 66 | } else { 67 | await esbuild.build(buildOptions as esbuild.BuildOptions); 68 | } 69 | 70 | await copyAssets(); 71 | } 72 | 73 | const isDev = process.env.ENV === "development"; 74 | build(isDev).then(() => 75 | console.log(`${isDev ? "Development build done." : "Production build done."}`) 76 | ); 77 | -------------------------------------------------------------------------------- /client/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extension", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "ENV=development dotenv -e ../../.env.development -- bun build.ts", 8 | "stage:build": "ENV=development dotenv -e ../../.env.development -- bun build.ts", 9 | "prod:build": "ENV=production dotenv -e ../../.env.production -- bun build.ts", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "bun-types": "^1.0.13", 16 | "chrome-types": "^0.1.246", 17 | "dotenv": "^16.3.1" 18 | }, 19 | "dependencies": { 20 | "@analytics/scroll-utils": "^0.1.22", 21 | "@firebase/auth": "^1.4.0", 22 | "@radix-ui/react-dropdown-menu": "^2.0.6", 23 | "@radix-ui/react-icons": "^1.3.0", 24 | "esbuild": "^0.19.6", 25 | "firebase": "^10.6.0", 26 | "framer-motion": "^10.16.12", 27 | "json-parse-even-better-errors": "^3.0.1", 28 | "normalize-url": "^8.0.0", 29 | "react": "^18.2.0", 30 | "react-dom": "^18.2.0", 31 | "sonner": "^1.2.3", 32 | "styled-components": "^6.1.1", 33 | "zustand": "^4.4.6" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/extension/src/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vignshwarar/AI-Employe/910ff98fcdc665651af0b00a27e19429822be1a1/client/extension/src/icons/icon128.png -------------------------------------------------------------------------------- /client/extension/src/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vignshwarar/AI-Employe/910ff98fcdc665651af0b00a27e19429822be1a1/client/extension/src/icons/icon16.png -------------------------------------------------------------------------------- /client/extension/src/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vignshwarar/AI-Employe/910ff98fcdc665651af0b00a27e19429822be1a1/client/extension/src/icons/icon32.png -------------------------------------------------------------------------------- /client/extension/src/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vignshwarar/AI-Employe/910ff98fcdc665651af0b00a27e19429822be1a1/client/extension/src/icons/icon48.png -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import * as S from "./styles"; 4 | 5 | interface ButtonProps { 6 | onClick?: () => void; 7 | disabled?: boolean; 8 | children?: React.ReactNode; 9 | style?: React.CSSProperties; 10 | type?: "button" | "submit" | "reset"; 11 | } 12 | 13 | const Button = ({ onClick, children, style, disabled }: ButtonProps) => ( 14 | 15 | {children} 16 | 17 | ); 18 | 19 | export default Button; 20 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Button/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { colors } from "../../styles/global"; 4 | 5 | export const Button = styled.button` 6 | width: 100%; 7 | background: ${colors.blue}; 8 | color: white; 9 | padding: 8px 16px; 10 | border-radius: 8px; 11 | border: none; 12 | font-size: 14px; 13 | cursor: pointer; 14 | transition: opacity 0.2s ease-in-out; 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | 19 | svg { 20 | margin-right: 8px; 21 | width: 16px; 22 | height: 16px; 23 | } 24 | 25 | &:hover { 26 | opacity: 0.9; 27 | } 28 | 29 | &:disabled { 30 | opacity: 0.5; 31 | cursor: not-allowed; 32 | } 33 | `; 34 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; 3 | import { HamburgerMenuIcon } from "@radix-ui/react-icons"; 4 | 5 | import { sendMessage } from "../../utils/message"; 6 | import { ActionType } from "../../utils/types"; 7 | import { useStore } from "../../store"; 8 | import { Page } from "../../store/pageSlice"; 9 | 10 | import Logo from "../Logo"; 11 | import * as S from "./styles"; 12 | 13 | const Header = () => { 14 | return ( 15 | 16 | 17 | 18 | 19 | ); 20 | }; 21 | 22 | const Dropdown = () => { 23 | const { user, setActivePage } = useStore(); 24 | 25 | if (!user) { 26 | return null; 27 | } 28 | 29 | const handleSignOut = () => { 30 | sendMessage({ 31 | actionType: ActionType.LOGOUT, 32 | }); 33 | setActivePage(Page.Login); 34 | }; 35 | 36 | return ( 37 | 38 | 39 | 42 | 43 | 44 | 45 | 46 | {user?.displayName} 47 |

{user?.email}

48 |
49 | setActivePage(Page.SetupKey)} 51 | className="DropdownMenuItem" 52 | > 53 | Set OpenAI API Key 54 | 55 | 59 | Logout 60 | 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Header; 67 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Header/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Header = styled.header` 4 | height: 60px; 5 | padding: 0 20px; 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | 10 | .IconButton { 11 | border-radius: 50%; 12 | width: 25px; 13 | height: 25px; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | background-color: #fff; 18 | cursor: pointer; 19 | border: 1px solid #e0e0e0; 20 | svg { 21 | fill: white; 22 | path { 23 | fill: #000; 24 | } 25 | } 26 | } 27 | 28 | .DropdownMenuContent { 29 | background-color: #fff; 30 | margin-right: 20px; 31 | margin-top: 4px; 32 | border-radius: 12px; 33 | padding: 4px; 34 | box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1); 35 | z-index: 100; 36 | } 37 | 38 | .DropdownMenuItem { 39 | padding: 6px; 40 | cursor: pointer; 41 | background-color: transparent; 42 | font-size: 12px; 43 | border-radius: 8px; 44 | 45 | p { 46 | font-size: 10px; 47 | color: #b2b2b2; 48 | } 49 | 50 | &:hover { 51 | background-color: #f3f3f3; 52 | outline: none; 53 | } 54 | 55 | &:focus { 56 | background-color: #f3f3f3; 57 | outline: none; 58 | } 59 | } 60 | 61 | .DropdownMenuSeparator { 62 | background-color: #e0e0e0; 63 | height: 1px; 64 | margin: 4px 0; 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import * as S from "./styles"; 4 | 5 | const Logo = () => { 6 | return ( 7 | 8 | AI Employe 9 | 10 | ); 11 | }; 12 | 13 | export default Logo; 14 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Logo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Logo = styled.a` 4 | font-weight: 500; 5 | color: black; 6 | text-decoration: none; 7 | font-size: 20px; 8 | 9 | span { 10 | background: linear-gradient( 11 | 90deg, 12 | #ff5b81 0%, 13 | #ff9b63 29.39%, 14 | #fb4646 67.71%, 15 | #fb46f8 100% 16 | ); 17 | background-clip: text; 18 | -webkit-background-clip: text; 19 | -webkit-text-fill-color: transparent; 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Message/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { ArrowUpIcon } from "@radix-ui/react-icons"; 3 | import { toast } from "sonner"; 4 | 5 | import { useStore } from "../../store"; 6 | import { Page } from "../../store/pageSlice"; 7 | import { Message } from "../../store/messageSlice"; 8 | import { sendMessage } from "../../utils/message"; 9 | import { ActionType } from "../../utils/types"; 10 | import * as S from "./styles"; 11 | 12 | const MessageInput = () => { 13 | const { 14 | setBlurContent, 15 | setActivePage, 16 | addMessage, 17 | setShowTyping, 18 | messages, 19 | editingWorkflow, 20 | } = useStore(); 21 | const [message, setMessage] = useState(""); 22 | 23 | const setInput = (e: React.ChangeEvent) => { 24 | setMessage(e.target.value); 25 | if (e.target.value != "") { 26 | setBlurContent(true); 27 | } else { 28 | setBlurContent(false); 29 | } 30 | }; 31 | 32 | const handleSubmit = (e: React.FormEvent) => { 33 | e.preventDefault(); 34 | 35 | if (editingWorkflow) { 36 | toast.error("You can't send messages while a workflow is running."); 37 | return; 38 | } 39 | 40 | if (message === "") { 41 | return; 42 | } 43 | let payloadBackend: Message[] = []; 44 | if (messages.length > 0) { 45 | payloadBackend = [ 46 | ...messages, 47 | { 48 | content: message, 49 | role: "user", 50 | }, 51 | ]; 52 | } else { 53 | payloadBackend = [ 54 | { 55 | content: message, 56 | role: "user", 57 | }, 58 | ]; 59 | } 60 | 61 | const payload = { 62 | content: message, 63 | role: "user", 64 | }; 65 | addMessage(payload as Message); 66 | sendMessage({ 67 | actionType: ActionType.NEW_MESSAGE, 68 | payload: payloadBackend, 69 | }); 70 | setActivePage(Page.Messages); 71 | setShowTyping(true); 72 | setMessage(""); 73 | }; 74 | 75 | return ( 76 | 77 | 78 | 83 | 84 | 85 | 86 | 87 | 88 | ); 89 | }; 90 | 91 | export default MessageInput; 92 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Message/messages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from "react"; 2 | 3 | import { useStore } from "../../store"; 4 | 5 | import * as S from "./styles"; 6 | 7 | const Messages = () => { 8 | const { messages } = useStore(); 9 | const ref = useRef(null); 10 | 11 | useEffect(() => { 12 | console.log("messages", messages); 13 | if (ref.current) { 14 | ref.current.scrollIntoView({ 15 | block: "end", 16 | inline: "nearest", 17 | }); 18 | } 19 | }, [messages]); 20 | 21 | return ( 22 | 23 | 24 | {messages.map((message, index) => ( 25 | 26 | 27 | {message.role === "user" ? ( 28 | You 29 | ) : ( 30 | AI Employe 31 | )} 32 | 33 | {message.content} 34 | 35 | ))} 36 | 37 | 38 | 39 | AI Employe 40 | 41 | Typing... 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default Messages; 50 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Message/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { colors } from "../../styles/global"; 4 | 5 | export const MessageContainer = styled.div` 6 | width: 100%; 7 | padding: 8px 20px 20px 20px; 8 | box-sizing: border-box; 9 | `; 10 | 11 | export const MessageInputForm = styled.form` 12 | width: 100%; 13 | position: relative; 14 | `; 15 | 16 | export const MessageInput = styled.input` 17 | width: 100%; 18 | box-sizing: border-box; 19 | padding: 10px 12px; 20 | font-size: 14px; 21 | border-radius: 12px; 22 | border: 2px solid white; 23 | background: white; 24 | transition: border 0.2s ease-in-out; 25 | 26 | &:focus { 27 | outline: none; 28 | border: 2px solid ${colors.blue}; 29 | } 30 | 31 | &::placeholder { 32 | color: #bfbfbf; 33 | } 34 | 35 | &:hover { 36 | box-shadow: 0px 8px 20px 0px rgba(0, 0, 0, 0.09); 37 | } 38 | `; 39 | 40 | export const SendButton = styled.button` 41 | position: absolute; 42 | right: 7px; 43 | top: 50%; 44 | transform: translateY(-50%); 45 | background: ${colors.blue}; 46 | color: white; 47 | border: none; 48 | border-radius: 10px; 49 | width: 32px; 50 | height: 32px; 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | `; 55 | 56 | export const MessageWrapper = styled.div` 57 | width: 100%; 58 | height: 100%; 59 | overflow-y: auto; 60 | padding: 20px 20px 0px 20px; 61 | box-sizing: border-box; 62 | height: 80vh; 63 | display: flex; 64 | flex-direction: column; 65 | `; 66 | 67 | export const MessageList = styled.div` 68 | margin-top: auto; 69 | display: flex; 70 | flex-direction: column; 71 | align-items: flex-start; 72 | overflow-y: scroll; 73 | scrollbar-width: none; /* Firefox */ 74 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 75 | 76 | &::-webkit-scrollbar { 77 | width: 0; 78 | height: 0; 79 | } 80 | `; 81 | 82 | export const Message = styled.div` 83 | margin-bottom: 8px; 84 | `; 85 | 86 | export const Role = styled.div``; 87 | 88 | export const MessageContent = styled.p` 89 | color: #3d3d3d; 90 | font-size: 15px; 91 | word-break: break-word; 92 | `; 93 | 94 | export const DummyDivToScroll = styled.div` 95 | height: 2px; 96 | width: 50%; 97 | box-sizing: border-box; 98 | `; 99 | 100 | export const AI = styled.p` 101 | margin-bottom: 4px; 102 | font-weight: 500; 103 | background: linear-gradient( 104 | 90deg, 105 | #ff5b81 0%, 106 | #ff9b63 29.39%, 107 | #fb4646 67.71%, 108 | #fb46f8 100% 109 | ); 110 | background-clip: text; 111 | -webkit-background-clip: text; 112 | -webkit-text-fill-color: transparent; 113 | font-size: 13px; 114 | `; 115 | 116 | export const You = styled.p` 117 | margin-bottom: 2px; 118 | color: #808080; 119 | font-weight: 500; 120 | font-size: 13px; 121 | `; 122 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { useStore } from "../../store"; 4 | import { Page } from "../../store/pageSlice"; 5 | import * as S from "./styles"; 6 | 7 | const Navigation = () => { 8 | const { setActivePage, setBlurContent, clearMessages, setEditingWorkflow } = 9 | useStore(); 10 | 11 | const changePage = (page: Page) => { 12 | setActivePage(page); 13 | setBlurContent(false); 14 | clearMessages(); 15 | setEditingWorkflow(null); 16 | }; 17 | 18 | return ( 19 | 20 | 21 | changePage(Page.CreateWorkflow)}> 22 | Create workflow 23 | 24 | changePage(Page.YourWorkflows)}> 25 | Your workflow 26 | 27 | changePage(Page.Community)}> 28 | Community 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default Navigation; 36 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Navigation/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { colors } from "../../styles/global"; 3 | 4 | export const NavigationContainer = styled.nav` 5 | padding: 0 20px; 6 | `; 7 | 8 | export const NavigationList = styled.ul` 9 | display: flex; 10 | margin: 0; 11 | padding: 0; 12 | width: 100%; 13 | overflow-x: scroll; 14 | scrollbar-width: none; 15 | -ms-overflow-style: none; 16 | 17 | &::-webkit-scrollbar { 18 | width: 0; 19 | height: 0; 20 | } 21 | `; 22 | 23 | export const NavigationItem = styled.li` 24 | list-style: none; 25 | margin-right: 8px; 26 | padding: 2px 8px; 27 | border-radius: 16px; 28 | border: 1px solid ${colors.blue}; 29 | background: ${colors.blueBackground}; 30 | font-size: 12px; 31 | color: ${colors.blue}; 32 | cursor: pointer; 33 | transition: background 0.2s ease-in-out; 34 | white-space: nowrap; 35 | 36 | &:hover { 37 | background: ${colors.blueBackgroundHover}; 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/components/Workflow/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | PlayIcon, 4 | StopIcon, 5 | CheckCircledIcon, 6 | Pencil1Icon, 7 | } from "@radix-ui/react-icons"; 8 | import { toast } from "sonner"; 9 | 10 | import { Workflow } from "../../store/workflowSlice"; 11 | import { Page } from "../../store/pageSlice"; 12 | import { useStore } from "../../store"; 13 | import Button from "../Button"; 14 | import { sendMessage } from "../../utils/message"; 15 | import { ActionType } from "../../utils/types"; 16 | 17 | import * as S from "../../styles/workflows"; 18 | import { colors } from "../../styles/global"; 19 | 20 | interface WorkflowProps { 21 | data: Workflow; 22 | key?: number; 23 | } 24 | 25 | const Workflow = ({ data }: WorkflowProps) => { 26 | const { 27 | setActivePage, 28 | setWorkflow, 29 | clearMessages, 30 | setShowTyping, 31 | workflow: currentWorkflow, 32 | setEditingWorkflow, 33 | } = useStore(); 34 | 35 | const isCurrentWorkflowRunning = currentWorkflow?.id === data.id; 36 | const isCurrentWorkflowDone = currentWorkflow?.done; 37 | 38 | const cleanUpAndClose = () => { 39 | clearMessages(); 40 | setWorkflow(null); 41 | setActivePage(Page.YourWorkflows); 42 | setShowTyping(false); 43 | }; 44 | 45 | const startWorkflow = async (workflow) => { 46 | if (currentWorkflow) { 47 | sendMessage({ 48 | actionType: ActionType.STOP_WORKFLOW, 49 | payload: { workflowId: currentWorkflow.id }, 50 | }); 51 | cleanUpAndClose(); 52 | toast.success("Workflow stopped successfully"); 53 | return; 54 | } 55 | 56 | clearMessages(); 57 | setShowTyping(true); 58 | setWorkflow(workflow); 59 | setActivePage(Page.Messages); 60 | sendMessage({ 61 | actionType: ActionType.INITIATE_WORKFLOW, 62 | payload: { workflowId: workflow.id }, 63 | }); 64 | }; 65 | 66 | const handleEditWorkflow = () => { 67 | setEditingWorkflow(data); 68 | setActivePage(Page.CreateWorkflow); 69 | }; 70 | 71 | const renderWorkflowStatus = () => { 72 | if (isCurrentWorkflowDone) { 73 | return ( 74 | 84 | ); 85 | } 86 | return ( 87 | 97 | ); 98 | }; 99 | 100 | return ( 101 | 102 | handleEditWorkflow()} 104 | className="edit" 105 | > 106 | 107 | 108 |

{data.title}

109 |

{data.objective}

110 | 111 | handleEditWorkflow()}> 112 | 113 |

{data.tasks && data.tasks.length} tasks

114 |
115 |
116 | {renderWorkflowStatus()} 117 |
118 | ); 119 | }; 120 | 121 | export default Workflow; 122 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/firebaseClient/index.ts: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | 3 | const firebaseConfig = { 4 | apiKey: process.env.NEXT_PUBLIC_API_KEY, 5 | authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN, 6 | projectId: process.env.NEXT_PUBLIC_PROJECT_ID, 7 | storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET, 8 | messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_SENDER_ID, 9 | appId: process.env.NEXT_PUBLIC_APP_ID, 10 | }; 11 | 12 | const app = initializeApp(firebaseConfig); 13 | export default app; 14 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/index.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import React, { useEffect } from "react"; 5 | import { toast } from "sonner"; 6 | 7 | import { GlobalStyle } from "./styles/global"; 8 | import { Page } from "./store/pageSlice"; 9 | import { useStore } from "./store"; 10 | import Layout from "./layout"; 11 | import Login from "./pages/login"; 12 | import CreateWorkflow from "./pages/createWorkflow"; 13 | import SetupKey from "./pages/setup"; 14 | import Workflows from "./pages/workflows"; 15 | import Messages from "./pages/messages"; 16 | import { sendMessage } from "./utils/message"; 17 | import { ActionType, MessageContent } from "./utils/types"; 18 | import { Message } from "./store/messageSlice"; 19 | 20 | const pageComponents = { 21 | [Page.Login]: () => , 22 | [Page.CreateWorkflow]: () => , 23 | [Page.YourWorkflows]: () => , 24 | [Page.Messages]: () => , 25 | [Page.SetupKey]: () => , 26 | [Page.Community]: () =>
Community shared workflows
, 27 | [Page.Loading]: () =>
Loading...
, 28 | default: () =>
Home
, 29 | }; 30 | 31 | export const App = () => { 32 | const { 33 | activePage, 34 | setUser, 35 | setLoading, 36 | setActivePage, 37 | addMessage, 38 | setShowTyping, 39 | setCurrentWorkflowDone, 40 | } = useStore(); 41 | const isLoginPage = activePage === Page.Login; 42 | 43 | const renderInitialPage = async (payload) => { 44 | if (isLoginPage && payload) { 45 | const hasOpenAIKey = await sendMessage({ 46 | actionType: ActionType.GET_OPENAI_KEY, 47 | }); 48 | 49 | if (!hasOpenAIKey) { 50 | setActivePage(Page.SetupKey); 51 | return; 52 | } 53 | 54 | setActivePage(Page.YourWorkflows); 55 | } 56 | }; 57 | 58 | const listenForMessages = () => { 59 | chrome.runtime.onMessage.addListener( 60 | // @ts-ignore 61 | (message: MessageContent, sender, sendResponse) => { 62 | const { actionType, payload } = message; 63 | if (actionType == ActionType.AUTH_STATE_CHANGE) { 64 | setUser(payload); 65 | setLoading(false); 66 | renderInitialPage(payload); 67 | return; 68 | } 69 | 70 | if (actionType == ActionType.AI_RESPONSE_FOR_COMMON_ACTION) { 71 | addMessage(payload as Message); 72 | setShowTyping(false); 73 | 74 | if (payload?.status?.error) { 75 | toast.success(payload?.status?.error); 76 | } 77 | 78 | return; 79 | } 80 | 81 | if (actionType == ActionType.AI_RESPONSE_FOR_WORKFLOW_ACTION) { 82 | addMessage(payload as Message); 83 | return; 84 | } 85 | 86 | if (actionType == ActionType.WORKFLOW_DONE) { 87 | setShowTyping(false); 88 | setCurrentWorkflowDone(); 89 | toast.success("Workflow completed successfully 🎉"); 90 | return; 91 | } 92 | 93 | if (actionType == ActionType.SHOW_MESSAGE_SIDEBAR) { 94 | toast.success(payload); 95 | return; 96 | } 97 | } 98 | ); 99 | }; 100 | 101 | useEffect(() => { 102 | listenForMessages(); 103 | sendMessage({ 104 | actionType: ActionType.CLIENT_READY, 105 | }); 106 | }, []); 107 | 108 | const renderPageComponent = () => { 109 | const Component = pageComponents[activePage] || pageComponents.default; 110 | return ; 111 | }; 112 | 113 | return ( 114 | <> 115 | 116 | {renderPageComponent()} 117 | 118 | ); 119 | }; 120 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Toaster } from "sonner"; 3 | 4 | import { Page } from "../store/pageSlice"; 5 | import Header from "../components/Header"; 6 | import Navigation from "../components/Navigation"; 7 | import MessageInput from "../components/Message/input"; 8 | import { useStore } from "../store"; 9 | 10 | import * as S from "./styles"; 11 | 12 | interface LayoutProps { 13 | children?: React.ReactNode; 14 | } 15 | 16 | const Layout = ({ children }: LayoutProps) => { 17 | const { blurContent, activePage, loading } = useStore(); 18 | const isMessagesPage = activePage === Page.Messages; 19 | const isLoginPage = activePage === Page.Login; 20 | 21 | if (loading) { 22 | return
Loading...
; 23 | } 24 | 25 | return ( 26 | <> 27 | 28 |
29 | {!isLoginPage && } 30 | 35 | {children} 36 | 37 | {!isLoginPage && } 38 | 39 | ); 40 | }; 41 | 42 | export default Layout; 43 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/layout/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const LayoutWrapper = styled.div` 4 | transition: all 0.2s ease-in-out; 5 | `; 6 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Logo from "../components/Logo"; 4 | import Button from "../components/Button"; 5 | import { sendMessage } from "../utils/message"; 6 | import { ActionType } from "../utils/types"; 7 | import * as S from "../styles/login"; 8 | 9 | const Login = () => { 10 | const handleSignIn = () => { 11 | sendMessage({ 12 | actionType: ActionType.LOGIN, 13 | }); 14 | }; 15 | 16 | return ( 17 | 18 | 19 | 20 |

Hey, please sign in to get started.

21 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default Setup; 67 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/pages/workflows.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | 3 | import { sendMessage } from "../utils/message"; 4 | import { ActionType } from "../utils/types"; 5 | import { useStore } from "../store"; 6 | import { Page } from "../store/pageSlice"; 7 | import * as S from "../styles/workflows"; 8 | import Workflow from "../components/Workflow"; 9 | 10 | const Workflows = () => { 11 | const { setActivePage } = useStore(); 12 | const [workflowsLoading, setWorkflowsLoading] = useState(true); 13 | const [workflows, setWorkflows] = useState([]); 14 | 15 | useEffect(() => { 16 | async function fetchWorkflows() { 17 | try { 18 | const response = await sendMessage({ 19 | actionType: ActionType.GET_WORKFLOWS, 20 | }); 21 | setWorkflows(response.payload); 22 | setWorkflowsLoading(false); 23 | if (response.payload.length === 0) { 24 | setActivePage(Page.CreateWorkflow); 25 | } 26 | } catch (error) { 27 | console.error("Error fetching workflows:", error); 28 | } 29 | } 30 | 31 | fetchWorkflows(); 32 | }, []); 33 | 34 | if (workflows.length === 0 && !workflowsLoading) { 35 | setActivePage(Page.CreateWorkflow); 36 | } 37 | 38 | return ( 39 | 40 | Your workflows 41 | {workflowsLoading && } 42 | {workflows.map((data) => ( 43 | 44 | ))} 45 | 46 | ); 47 | }; 48 | 49 | export default Workflows; 50 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/store/index.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | import { PageSlice, createPageSlice } from "./pageSlice"; 4 | import { MessageSlice, createMessageSlice } from "./messageSlice"; 5 | import { UserSlice, createUserSlice } from "./userSlice"; 6 | import { WorkflowSlice, createWorkflowSlice } from "./workflowSlice"; 7 | 8 | type Store = PageSlice & MessageSlice & UserSlice & WorkflowSlice; 9 | 10 | export const useStore = create((...args) => ({ 11 | ...createPageSlice(...args), 12 | ...createMessageSlice(...args), 13 | ...createUserSlice(...args), 14 | ...createWorkflowSlice(...args), 15 | })); 16 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/store/messageSlice.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | 3 | import { Workflow } from "./workflowSlice"; 4 | 5 | export interface AIAction { 6 | action_type: string; 7 | search_term_to_find_this_element: null; 8 | value: string; 9 | node_id: null; 10 | thought_process?: string; 11 | } 12 | 13 | export type Content = AIAction[] | string; 14 | 15 | export interface Message { 16 | role: "user" | "assistant"; 17 | content: Content; 18 | } 19 | 20 | export type MessageSlice = { 21 | messages: Message[]; 22 | addMessage: (message: Message) => void; 23 | showTyping: boolean; 24 | setShowTyping: (showTyping: boolean) => void; 25 | workflow: Workflow | null; 26 | setWorkflow: (workflow: Workflow) => void; 27 | clearMessages: () => void; 28 | setCurrentWorkflowDone: () => void; 29 | }; 30 | 31 | export const createMessageSlice: StateCreator = (set) => ({ 32 | messages: [], 33 | addMessage: (message: Message) => 34 | set((state) => ({ messages: [...state.messages, message] })), 35 | showTyping: false, 36 | setShowTyping: (showTyping: boolean) => set({ showTyping }), 37 | workflow: null, 38 | setWorkflow: (workflow: Workflow) => set({ workflow }), 39 | clearMessages: () => set({ messages: [] }), 40 | setCurrentWorkflowDone: () => 41 | set((state) => ({ 42 | workflow: { 43 | ...state.workflow, 44 | done: true, 45 | }, 46 | })), 47 | }); 48 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/store/pageSlice.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | 3 | export enum Page { 4 | Login = "Login", 5 | CreateWorkflow = "CreateWorkflow", 6 | YourWorkflows = "YourWorkflows", 7 | Messages = "Messages", 8 | Community = "Community", 9 | Loading = "Loading", 10 | SetupKey = "SetupKey", 11 | } 12 | 13 | export type PageSlice = { 14 | activePage: Page; 15 | setActivePage: (page: Page) => void; 16 | blurContent: boolean; 17 | setBlurContent: (blur: boolean) => void; 18 | }; 19 | 20 | export const createPageSlice: StateCreator = (set) => ({ 21 | activePage: Page.Login, 22 | setActivePage: (page: Page) => set({ activePage: page }), 23 | blurContent: false, 24 | setBlurContent: (blur: boolean) => set({ blurContent: blur }), 25 | }); 26 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/store/userSlice.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@firebase/auth"; 2 | import { StateCreator } from "zustand"; 3 | 4 | export type UserSlice = { 5 | loading: boolean; 6 | user: User | null; 7 | setUser: (user: User | null) => void; 8 | setLoading: (loading: boolean) => void; 9 | }; 10 | 11 | export const createUserSlice: StateCreator = (set) => ({ 12 | loading: true, 13 | user: null, 14 | setUser: (user) => set({ user }), 15 | setLoading: (loading) => set({ loading }), 16 | }); 17 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/store/workflowSlice.ts: -------------------------------------------------------------------------------- 1 | import { StateCreator } from "zustand"; 2 | 3 | export interface Task { 4 | task: string; 5 | task_number: number; 6 | } 7 | 8 | export interface Workflow { 9 | id: string; 10 | userId: string; 11 | objective: string; 12 | createdAt: string; 13 | title: string; 14 | tasks: Task[]; 15 | done?: boolean; 16 | } 17 | 18 | export type WorkflowSlice = { 19 | editingWorkflow: Workflow | null; 20 | setEditingWorkflow: (workflow: Workflow) => void; 21 | updateTitle: (title: string) => void; 22 | updateObjective: (objective: string) => void; 23 | setUpdatedTasks: (tasks: Task[]) => void; 24 | }; 25 | 26 | export const createWorkflowSlice: StateCreator = (set) => ({ 27 | editingWorkflow: null, 28 | setEditingWorkflow: (workflow: Workflow) => 29 | set({ editingWorkflow: workflow }), 30 | updateTitle: (title: string) => 31 | set((state) => ({ 32 | editingWorkflow: { 33 | ...state.editingWorkflow, 34 | title, 35 | }, 36 | })), 37 | updateObjective: (objective: string) => 38 | set((state) => ({ 39 | editingWorkflow: { 40 | ...state.editingWorkflow, 41 | objective, 42 | }, 43 | })), 44 | setUpdatedTasks: (tasks: Task[]) => 45 | set((state) => ({ 46 | editingWorkflow: { 47 | ...state.editingWorkflow, 48 | tasks, 49 | }, 50 | })), 51 | }); 52 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/styles/createWorkflow.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { colors } from "../styles/global"; 4 | 5 | export const CreateWorkflowContainer = styled.div` 6 | height: 80vh; 7 | display: flex; 8 | width: 100%; 9 | justify-content: center; 10 | align-items: center; 11 | flex-direction: column; 12 | `; 13 | 14 | export const CreateWorkflowContentWrapper = styled.div` 15 | width: 80%; 16 | h2 { 17 | font-size: 18px; 18 | font-weight: 400; 19 | color: black; 20 | } 21 | p { 22 | color: #444444; 23 | font-family: Poppins; 24 | font-size: 14px; 25 | font-weight: 400; 26 | margin-bottom: 16px; 27 | } 28 | `; 29 | 30 | export const CreateWorkflowTextarea = styled.textarea` 31 | width: 100%; 32 | box-sizing: border-box; 33 | min-height: 110px; 34 | border-radius: 12px; 35 | padding: 10px 12px; 36 | font-size: 14px; 37 | border: 2px solid white; 38 | background: white; 39 | transition: border 0.2s ease-in-out; 40 | 41 | &:focus { 42 | outline: none; 43 | border: 2px solid ${colors.blue}; 44 | } 45 | `; 46 | 47 | export const CreateWorkflowTitle = styled.input` 48 | width: 100%; 49 | box-sizing: border-box; 50 | border-radius: 12px; 51 | padding: 10px 12px; 52 | font-size: 14px; 53 | border: 2px solid white; 54 | background: white; 55 | transition: border 0.2s ease-in-out; 56 | margin-bottom: 8px; 57 | 58 | &:focus { 59 | outline: none; 60 | border: 2px solid ${colors.blue}; 61 | } 62 | `; 63 | 64 | export const BackButton = styled.button` 65 | display: flex; 66 | align-items: center; 67 | border: none; 68 | border-radius: 8px; 69 | background: rgba(0, 0, 0, 0.05); 70 | font-size: 10px; 71 | margin-bottom: 8px; 72 | cursor: pointer; 73 | 74 | svg { 75 | margin-right: 4px; 76 | width: 10px; 77 | height: 10px; 78 | } 79 | 80 | &:hover { 81 | background: rgba(0, 0, 0, 0.1); 82 | } 83 | `; 84 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/styles/global.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | export const colors = { 4 | background: "#ECECEC", 5 | blueBackground: "rgba(29, 133, 255, 0.09)", 6 | blueBackgroundHover: "rgba(29, 133, 255, 0.2)", 7 | blue: "#1d85ff", 8 | grey: "#E5E8E8", 9 | lightGrey: "#F2F4F4", 10 | strongGrey: "#707070", 11 | }; 12 | 13 | export const GlobalStyle = createGlobalStyle` 14 | html, body, div, span, applet, object, iframe, 15 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 16 | a, abbr, acronym, address, big, cite, code, 17 | del, dfn, em, img, ins, kbd, q, s, samp, 18 | small, strike, strong, sub, sup, tt, var, 19 | b, u, i, center, 20 | dl, dt, dd, ol, ul, li, 21 | fieldset, form, label, legend, 22 | table, caption, tbody, tfoot, thead, tr, th, td, 23 | article, aside, canvas, details, embed, 24 | figure, figcaption, footer, header, hgroup, 25 | menu, nav, output, ruby, section, summary, 26 | time, mark, audio, video { 27 | margin: 0; 28 | padding: 0; 29 | border: 0; 30 | font-size: 100%; 31 | font: inherit; 32 | vertical-align: baseline; 33 | } 34 | /* HTML5 display-role reset for older browsers */ 35 | article, aside, details, figcaption, figure, 36 | footer, header, hgroup, menu, nav, section { 37 | display: block; 38 | } 39 | body { 40 | background: ${colors.background}; 41 | padding:0; 42 | margin:0; 43 | overflow: hidden; 44 | font-family: 'Poppins', sans-serif; 45 | } 46 | button, input, select, textarea { 47 | font-family: 'Poppins', sans-serif; 48 | } 49 | ol, ul { 50 | list-style: none; 51 | } 52 | blockquote, q { 53 | quotes: none; 54 | } 55 | blockquote:before, blockquote:after, 56 | q:before, q:after { 57 | content: ''; 58 | content: none; 59 | } 60 | table { 61 | border-collapse: collapse; 62 | border-spacing: 0; 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/styles/login.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const LoginContainer = styled.div` 4 | height: 80vh; 5 | display: flex; 6 | width: 100%; 7 | justify-content: center; 8 | align-items: center; 9 | flex-direction: column; 10 | `; 11 | 12 | export const LoginContentWrapper = styled.div` 13 | width: 80%; 14 | h2 { 15 | font-size: 24px; 16 | font-weight: 400; 17 | color: black; 18 | margin-bottom: 16px; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/styles/setup.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const SetupKeyContainer = styled.div` 4 | height: 80vh; 5 | display: flex; 6 | width: 85%; 7 | margin: 0 auto; 8 | flex-direction: column; 9 | justify-content: center; 10 | 11 | h2 { 12 | font-size: 18px; 13 | font-weight: 400; 14 | color: black; 15 | } 16 | p { 17 | color: #444444; 18 | font-family: Poppins; 19 | font-size: 14px; 20 | font-weight: 400; 21 | margin-bottom: 8px; 22 | } 23 | a { 24 | color: #444444; 25 | font-family: Poppins; 26 | font-size: 14px; 27 | font-weight: 400; 28 | margin-bottom: 8px; 29 | text-decoration: underline; 30 | } 31 | `; 32 | 33 | export const Form = styled.form``; 34 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/styles/workflows.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from "styled-components"; 2 | import { colors } from "./global"; 3 | 4 | export const WorkflowContainer = styled.div` 5 | height: 80vh; 6 | padding: 20px 20px 0 20px; 7 | overflow-y: scroll; 8 | `; 9 | 10 | export const WorkflowContentWrapper = styled.div` 11 | width: 100%; 12 | background: white; 13 | margin-bottom: 8px; 14 | border-radius: 12px; 15 | padding: 10px 12px; 16 | box-sizing: border-box; 17 | overflow: hidden; 18 | height: 180px; 19 | display: flex; 20 | flex-direction: column; 21 | min-height: 150px; 22 | position: relative; 23 | 24 | h3 { 25 | font-size: 15px; 26 | font-weight: 400; 27 | color: black; 28 | } 29 | p { 30 | display: -webkit-box; 31 | -webkit-line-clamp: 2; 32 | -webkit-box-orient: vertical; 33 | overflow: hidden; 34 | font-size: 12px; 35 | color: #7d7d7d; 36 | margin-bottom: 8px; 37 | } 38 | 39 | &:hover .edit { 40 | display: flex; 41 | } 42 | `; 43 | 44 | const shimmer = keyframes` 45 | 0% { 46 | background-position: -468px 0; 47 | } 48 | 100% { 49 | background-position: 468px 0; 50 | } 51 | `; 52 | 53 | export const WorkflowContentPlaceholder = styled.div` 54 | height: 150px; 55 | background: #f6f7f8; 56 | background: linear-gradient(to right, #e7e7e7 8%, #dddddd 28%, #e7e7e7 33%); 57 | background-size: 800px 104px; 58 | position: relative; 59 | animation: ${shimmer} 1.5s infinite linear; 60 | border-radius: 12px; 61 | `; 62 | 63 | export const WorkflowTitle = styled.h2` 64 | font-size: 18px; 65 | font-weight: 400; 66 | color: black; 67 | margin-bottom: 8px; 68 | `; 69 | 70 | export const WorkflowOptions = styled.div` 71 | display: flex; 72 | align-items: center; 73 | `; 74 | 75 | export const EditWorkflowButton = styled.button` 76 | width: 25px; 77 | height: 25px; 78 | align-items: center; 79 | justify-content: center; 80 | position: absolute; 81 | right: 10px; 82 | border-radius: 50%; 83 | border: none; 84 | background-color: transparent; 85 | outline: none; 86 | cursor: pointer; 87 | display: none; 88 | 89 | svg { 90 | path { 91 | fill: ${colors.strongGrey}; 92 | } 93 | } 94 | 95 | &:hover { 96 | background: ${colors.lightGrey}; 97 | } 98 | `; 99 | 100 | export const TasksTag = styled.div` 101 | display: flex; 102 | background: ${colors.lightGrey}; 103 | align-items: center; 104 | border-radius: 8px; 105 | padding: 4px 8px; 106 | cursor: pointer; 107 | border: none; 108 | outline: none; 109 | width: fit-content; 110 | font-size: 12px; 111 | 112 | p { 113 | margin-left: 4px; 114 | margin-bottom: 0; 115 | color: ${colors.strongGrey}; 116 | } 117 | 118 | svg { 119 | path { 120 | fill: ${colors.strongGrey}; 121 | } 122 | } 123 | 124 | &:hover { 125 | opacity: 0.8; 126 | } 127 | `; 128 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/utils/message.ts: -------------------------------------------------------------------------------- 1 | import { MessageContent } from "./types"; 2 | 3 | export const sendMessage = async (messageContent: MessageContent) => { 4 | return await chrome.runtime.sendMessage(messageContent); 5 | }; 6 | -------------------------------------------------------------------------------- /client/extension/src/lib/ui/utils/types.ts: -------------------------------------------------------------------------------- 1 | export enum ActionType { 2 | LOGIN = "LOGIN", 3 | LOGOUT = "LOGOUT", 4 | AUTH_STATE_CHANGE = "AUTH_STATE_CHANGE", 5 | CLIENT_READY = "CLIENT_READY", 6 | START_RECORDING = "START_RECORDING", 7 | STOP_RECORDING = "STOP_RECORDING", 8 | RECORD_SCRIPT_STATUS = "RECORD_SCRIPT_STATUS", 9 | RECORD_SCRIPT_ACTIVE = "RECORD_SCRIPT_ACTIVE", 10 | RUN_SCRIPT_STATUS = "RUN_SCRIPT_STATUS", 11 | RUN_SCRIPT_ACTIVE = "RUN_SCRIPT_ACTIVE", 12 | RECORD_ACTION = "RECORD_ACTION", 13 | CREATE_WORKFLOW = "CREATE_WORKFLOW", 14 | UPDATE_WORKFLOW = "UPDATE_WORKFLOW", 15 | SETUP_OPENAI_KEY = "SETUP_OPENAI_KEY", 16 | GET_OPENAI_KEY = "GET_OPENAI_KEY", 17 | CREATE_WORKFLOW_SUCCESS = "CREATE_WORKFLOW_SUCCESS", 18 | GET_WORKFLOWS = "GET_WORKFLOWS", 19 | NEW_MESSAGE = "NEW_MESSAGE", 20 | AI_RESPONSE_FOR_COMMON_ACTION = "AI_RESPONSE_FOR_COMMON_ACTION", 21 | AI_RESPONSE_FOR_WORKFLOW_ACTION = "AI_RESPONSE_FOR_WORKFLOW_ACTION", 22 | WORKFLOW_DONE = "WORKFLOW_DONE", 23 | TAKE_SCREENSHOT = "TAKE_SCREENSHOT", 24 | INITIATE_WORKFLOW = "INITIATE_WORKFLOW", 25 | START_WORKFLOW = "START_WORKFLOW", 26 | STOP_WORKFLOW = "STOP_WORKFLOW", 27 | EXECUTE_ACTION = "EXECUTE_ACTION", 28 | CAPTURE_SCREENSHOT = "CAPTURE_SCREENSHOT", 29 | GET_HTML_WITH_NODE_IDS = "GET_HTML_WITH_NODE_IDS", 30 | ANIMATE_CURSOR = "ANIMATE_CURSOR", 31 | SHOW_MESSAGE_SIDEBAR = "SHOW_MESSAGE_SIDEBAR", 32 | } 33 | 34 | export interface MessageContent { 35 | actionType: ActionType; 36 | payload?: any; 37 | } 38 | -------------------------------------------------------------------------------- /client/extension/src/lib/worker/index.ts: -------------------------------------------------------------------------------- 1 | import login, { listenAuthStateChange, handleLogout } from "./login"; 2 | import { ActionType, MessageContent } from "../ui/utils/types"; 3 | import record, { 4 | stopRecording, 5 | recordAction, 6 | createWorkflow, 7 | updateWorkflow, 8 | } from "./recorder"; 9 | import { getWorkflows, startWorkflow, stopWorkflow } from "./workflows"; 10 | import handleNewMessage from "./message"; 11 | import { storeOpenAIKey, getOpenAIKey } from "./setup"; 12 | 13 | export const init = () => { 14 | chrome.runtime.onMessage.addListener( 15 | // @ts-ignore 16 | async (message, sender, sendResponse) => { 17 | const { actionType, payload } = message as MessageContent; 18 | console.log("message received", message, sender, sendResponse); 19 | switch (actionType) { 20 | case ActionType.CLIENT_READY: 21 | listenAuthStateChange(); 22 | break; 23 | case ActionType.START_RECORDING: 24 | record(); 25 | break; 26 | case ActionType.STOP_RECORDING: 27 | stopRecording(); 28 | break; 29 | case ActionType.NEW_MESSAGE: 30 | handleNewMessage(payload); 31 | break; 32 | case ActionType.LOGIN: 33 | login(); 34 | break; 35 | case ActionType.RECORD_ACTION: 36 | recordAction(payload, sender); 37 | break; 38 | case ActionType.LOGOUT: 39 | handleLogout(); 40 | break; 41 | case ActionType.INITIATE_WORKFLOW: 42 | startWorkflow(payload); 43 | break; 44 | case ActionType.STOP_WORKFLOW: 45 | stopWorkflow(); 46 | break; 47 | default: 48 | break; 49 | } 50 | } 51 | ); 52 | 53 | // sendResponse is not working if it inside the switch statement 54 | // but it works if it is outside the switch statement, i am doing something wrong 55 | // TODO: clean this up 56 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 57 | const { actionType, payload } = message as MessageContent; 58 | if (actionType === ActionType.CREATE_WORKFLOW) { 59 | createWorkflow(payload).then((payload) => { 60 | // @ts-ignore 61 | sendResponse({ 62 | actionType: ActionType.CREATE_WORKFLOW_SUCCESS, 63 | payload, 64 | }); 65 | }); 66 | return true; 67 | } 68 | 69 | if (actionType === ActionType.UPDATE_WORKFLOW) { 70 | updateWorkflow(payload).then((payload) => { 71 | // @ts-ignore 72 | sendResponse({ 73 | actionType: ActionType.UPDATE_WORKFLOW, 74 | payload, 75 | }); 76 | }); 77 | return true; 78 | } 79 | 80 | if (actionType === ActionType.GET_WORKFLOWS) { 81 | getWorkflows().then((payload) => { 82 | // @ts-ignore 83 | sendResponse({ 84 | actionType: ActionType.GET_WORKFLOWS, 85 | payload, 86 | }); 87 | }); 88 | return true; 89 | } 90 | 91 | if (actionType === ActionType.SETUP_OPENAI_KEY) { 92 | storeOpenAIKey(payload).then((payload) => { 93 | // @ts-ignore 94 | sendResponse({ 95 | actionType: ActionType.SETUP_OPENAI_KEY, 96 | payload, 97 | }); 98 | }); 99 | return true; 100 | } 101 | 102 | if (actionType === ActionType.GET_OPENAI_KEY) { 103 | getOpenAIKey().then((payload) => { 104 | // @ts-ignore 105 | sendResponse({ 106 | actionType: ActionType.GET_OPENAI_KEY, 107 | payload, 108 | }); 109 | }); 110 | return true; 111 | } 112 | }); 113 | 114 | chrome.action.onClicked.addListener(function (tab) { 115 | const queryOptions = { active: true, lastFocusedWindow: true }; 116 | chrome.tabs.query(queryOptions, (tab) => { 117 | chrome.sidePanel.open({ tabId: tab[0].id }); 118 | }); 119 | }); 120 | }; 121 | -------------------------------------------------------------------------------- /client/extension/src/lib/worker/login.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getAuth, 3 | signInWithCustomToken, 4 | onAuthStateChanged, 5 | } from "@firebase/auth"; 6 | 7 | import firebaseClient from "../ui/firebaseClient"; 8 | import { sendMessage } from "../ui/utils/message"; 9 | import { ActionType } from "../ui/utils/types"; 10 | 11 | export const auth = getAuth(firebaseClient); 12 | 13 | export const getUserToken = async () => { 14 | return auth.currentUser.getIdToken(); 15 | }; 16 | 17 | const handleLogin = async () => { 18 | await chrome.tabs.create({ 19 | url: `${process.env.FRONTEND_URL}/?signInFromExtension=true`, 20 | }); 21 | listenForUpdates(); 22 | }; 23 | 24 | export const handleLogout = async () => { 25 | await auth.signOut(); 26 | }; 27 | 28 | const isRelevantTab = (tab) => 29 | tab.status === "complete" && 30 | tab.url && 31 | new URL(tab.url).origin === process.env.FRONTEND_URL; 32 | 33 | const removeEventListeners = () => { 34 | chrome.tabs.onUpdated.removeListener(handleTabUpdatedEvent); 35 | chrome.tabs.onCreated.removeListener(handleTabCreatedEvent); 36 | }; 37 | 38 | const getTokenAndSignIn = async (tab) => { 39 | const url = new URL(tab.url); 40 | const token = url.searchParams.get("extensionToken"); 41 | if (token) { 42 | await signIn(token); 43 | try { 44 | chrome.tabs.remove(tab.id); 45 | removeEventListeners(); 46 | } catch (error) { 47 | console.error("Tab already closed.", error); 48 | } 49 | } 50 | }; 51 | 52 | const handleTabUpdatedEvent = async (tabId, changeInfo, tab) => { 53 | tab = tab || tabId; 54 | if (isRelevantTab(tab)) { 55 | await getTokenAndSignIn(tab); 56 | } 57 | }; 58 | 59 | const handleTabCreatedEvent = async (tab) => { 60 | if (isRelevantTab(tab)) { 61 | await getTokenAndSignIn(tab); 62 | } 63 | }; 64 | 65 | const listenForUpdates = () => { 66 | chrome.tabs.onUpdated.addListener(handleTabUpdatedEvent); 67 | chrome.tabs.onCreated.addListener(handleTabCreatedEvent); 68 | }; 69 | 70 | export const listenAuthStateChange = () => { 71 | onAuthStateChanged(auth, async (user) => { 72 | sendMessage({ actionType: ActionType.AUTH_STATE_CHANGE, payload: user }); 73 | }); 74 | }; 75 | 76 | const signIn = async (token) => { 77 | await signInWithCustomToken(auth, token); 78 | }; 79 | 80 | export default handleLogin; 81 | -------------------------------------------------------------------------------- /client/extension/src/lib/worker/message.ts: -------------------------------------------------------------------------------- 1 | import { Message } from "../ui/store/messageSlice"; 2 | import { sendMessage } from "../ui/utils/message"; 3 | import { ActionType } from "../ui/utils/types"; 4 | import { getOpenAIKey } from "./setup"; 5 | import { queryActiveTab, fetchClient } from "./utils"; 6 | 7 | const handleNewMessage = async (messages: Message[]) => { 8 | try { 9 | const tab = await queryActiveTab(); 10 | const html = await getActivePageHtml(tab.id); 11 | const screenshot = await getActivePageScreenshot(); 12 | const response = await getAction({ 13 | html, 14 | screenshot, 15 | url: tab.url, 16 | messages, 17 | }); 18 | 19 | const aiMessage = { 20 | role: "assistant", 21 | content: response.action.actions, 22 | }; 23 | sendMessage({ 24 | actionType: ActionType.AI_RESPONSE_FOR_COMMON_ACTION, 25 | payload: aiMessage, 26 | }); 27 | } catch (error) { 28 | console.log({ error }); 29 | } 30 | }; 31 | 32 | const getActivePageHtml = async (tabId) => { 33 | try { 34 | const rawData = await chrome.scripting.executeScript({ 35 | target: { tabId }, 36 | func: () => { 37 | return document.documentElement.innerHTML; 38 | }, 39 | }); 40 | 41 | if (rawData.length > 0) { 42 | return rawData[0].result; 43 | } 44 | 45 | return null; 46 | } catch (error) { 47 | console.log({ error }); 48 | return null; 49 | } 50 | }; 51 | export const getActivePageScreenshot = async (): Promise => { 52 | try { 53 | const dataUrl = await chrome.tabs.captureVisibleTab(); 54 | return dataUrl; 55 | } catch (error) { 56 | console.log({ error }); 57 | return null; 58 | } 59 | }; 60 | 61 | export const getPageScreenshotByTabId = async (tabId) => { 62 | try { 63 | const dataUrl = await chrome.tabs.captureVisibleTab(tabId); 64 | return dataUrl; 65 | } catch (error) { 66 | console.log({ error }); 67 | return null; 68 | } 69 | }; 70 | 71 | const getAction = async ({ 72 | html, 73 | screenshot, 74 | url, 75 | messages, 76 | }: { 77 | html?: string; 78 | screenshot?: string; 79 | url?: string; 80 | messages?: Message[]; 81 | }) => { 82 | let openAIKey = await getOpenAIKey(); 83 | let host; 84 | if (url) { 85 | const urlObj = new URL(url); 86 | host = urlObj.host; 87 | } 88 | 89 | let convertMessageContentToString = messages.map((message) => { 90 | if (typeof message.content === "string") { 91 | return message; 92 | } 93 | 94 | return { 95 | ...message, 96 | content: JSON.stringify(message.content), 97 | }; 98 | }); 99 | 100 | const payload = { 101 | html, 102 | screenshot, 103 | url, 104 | host, 105 | messages: convertMessageContentToString, 106 | openai_api_key: openAIKey, 107 | }; 108 | 109 | const response = await fetchClient("/execute", { 110 | method: "POST", 111 | headers: { 112 | "Content-Type": "application/json", 113 | }, 114 | body: JSON.stringify(payload), 115 | }); 116 | 117 | return await response.json(); 118 | }; 119 | 120 | export default handleNewMessage; 121 | -------------------------------------------------------------------------------- /client/extension/src/lib/worker/setup.ts: -------------------------------------------------------------------------------- 1 | export const storeOpenAIKey = async (key) => { 2 | await chrome.storage.local.set({ openAIKey: key }); 3 | }; 4 | 5 | export const getOpenAIKey = async () => { 6 | const result = await chrome.storage.local.get(["openAIKey"]); 7 | return result.openAIKey || null; 8 | }; 9 | -------------------------------------------------------------------------------- /client/extension/src/lib/worker/utils.ts: -------------------------------------------------------------------------------- 1 | import { getUserToken } from "./login"; 2 | 3 | export const setLocalStorage = async (key, value) => { 4 | await chrome.storage.local.set({ [key]: value }); 5 | }; 6 | 7 | export const getLocalStorage = async (key) => { 8 | const result = await chrome.storage.local.get([key]); 9 | return result[key]; 10 | }; 11 | 12 | export const queryActiveTab = async () => { 13 | const queryOptions = { active: true, lastFocusedWindow: true }; 14 | const [tab] = await chrome.tabs.query(queryOptions); 15 | return tab; 16 | }; 17 | 18 | export const isChromeUrl = (url) => url && url.startsWith("chrome://"); 19 | 20 | export const fetchClient = async (path, options) => { 21 | const token = await getUserToken(); 22 | const baseUrl = process.env.BACKEND_URL; 23 | const url = `${baseUrl}${path}`; 24 | 25 | const response = await fetch(url, { 26 | ...options, 27 | headers: { 28 | ...options.headers, 29 | Authorization: `Bearer ${token}`, 30 | }, 31 | }); 32 | return response; 33 | }; 34 | -------------------------------------------------------------------------------- /client/extension/src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "AI Employe", 4 | "description": "First reliable AI browser automation powered by GPT-4 Vision.", 5 | "version": "1.0", 6 | "background": { 7 | "service_worker": "worker.js" 8 | }, 9 | "host_permissions": [""], 10 | "action": {}, 11 | "icons": { 12 | "16": "icons/icon16.png", 13 | "32": "icons/icon32.png", 14 | "48": "icons/icon48.png", 15 | "128": "icons/icon128.png" 16 | }, 17 | "permissions": [ 18 | "contextMenus", 19 | "tabs", 20 | "activeTab", 21 | "scripting", 22 | "sidePanel", 23 | "storage", 24 | "debugger" 25 | ], 26 | "side_panel": { 27 | "default_path": "ui.html" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/extension/src/recorder.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from "./lib/ui/utils/types"; 2 | import { sendMessage } from "./lib/ui/utils/message"; 3 | import { executeBlockInjectionAndAssignment } from "./lib/ui/utils/injectBlockId"; 4 | import { recalculateBlockPositions } from "./lib/ui/utils/injectBlockId"; 5 | 6 | import { onScrollChange } from "@analytics/scroll-utils"; 7 | 8 | // @ts-ignore 9 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 10 | console.log("message received", message, sender, sendResponse); 11 | const { actionType, payload } = message; 12 | switch (actionType) { 13 | case ActionType.RECORD_SCRIPT_STATUS: 14 | // @ts-ignore 15 | sendResponse({ 16 | actionType: ActionType.RECORD_SCRIPT_STATUS, 17 | payload: ActionType.RECORD_SCRIPT_ACTIVE, 18 | }); 19 | break; 20 | default: 21 | break; 22 | } 23 | }); 24 | 25 | const injectRecordStatus = () => { 26 | const button = document.createElement("button"); 27 | button.id = "record-aie"; 28 | button.innerHTML = "Recording..."; 29 | button.style.position = "fixed"; 30 | button.style.bottom = "10px"; 31 | button.style.left = "10px"; 32 | button.style.zIndex = "9999"; 33 | button.style.padding = "10px 16px"; 34 | button.style.border = "none"; 35 | button.style.borderRadius = "50px"; 36 | button.style.backgroundColor = "red"; 37 | button.style.color = "white"; 38 | document.body.appendChild(button); 39 | }; 40 | 41 | const divWithExceptions = ["div[role=button]", "div[contenteditable=true]"]; 42 | 43 | const eventsToListen = ["click", "dblclick", "input", "focus", "keydown"]; 44 | 45 | const listenInteractions = () => { 46 | eventsToListen.forEach((event) => { 47 | document.addEventListener(event, handleInteraction); 48 | }); 49 | 50 | const scrollChangeHandlers = Array.from({ length: 100 }, (_, i) => ({ 51 | [1 * (i + 1)]: (scrollDepth, maxScroll) => { 52 | const payload = { 53 | actionType: `scroll_${scrollDepth.direction}`, 54 | value: `${scrollDepth.trigger}%`, 55 | timestamp: new Date().toISOString(), 56 | url: window.location.href, 57 | }; 58 | 59 | sendMessage({ 60 | actionType: ActionType.RECORD_ACTION, 61 | payload, 62 | }); 63 | }, 64 | })).reduce((a, b) => ({ ...a, ...b }), {}); 65 | 66 | onScrollChange(scrollChangeHandlers); 67 | }; 68 | 69 | const handleInteraction = async (event) => { 70 | recalculateBlockPositions(); 71 | let { target, type } = event; 72 | const { tagName, id, className } = target; 73 | const tag = target.tagName.toLowerCase(); 74 | const uniqueId = `${tagName}-${id}-${className}`; 75 | const isDivWithException = divWithExceptions.some((selector) => 76 | target.matches(selector) 77 | ); 78 | const blockIds = target.getAttribute("aie-block-ids"); 79 | 80 | let value; 81 | if (isDivWithException) { 82 | value = target.innerText; 83 | } else { 84 | value = target.value; 85 | } 86 | 87 | const payload = { 88 | tag, 89 | uniqueId, 90 | actionType: type, 91 | timestamp: new Date().toISOString(), 92 | value, 93 | url: window.location.href, 94 | innerText: target.innerText, 95 | placeholder: target.placeholder, 96 | blockIds, 97 | }; 98 | 99 | sendMessage({ 100 | actionType: ActionType.RECORD_ACTION, 101 | payload, 102 | }); 103 | }; 104 | 105 | const sendInitialVisitActions = () => { 106 | sendMessage({ 107 | actionType: ActionType.RECORD_ACTION, 108 | payload: { 109 | actionType: "visit", 110 | timestamp: new Date().toISOString(), 111 | url: window.location.href, 112 | }, 113 | }); 114 | }; 115 | 116 | const init = () => { 117 | injectRecordStatus(); 118 | sendInitialVisitActions(); 119 | listenInteractions(); 120 | executeBlockInjectionAndAssignment(); 121 | }; 122 | 123 | const existingButton = document.getElementById("record-aie"); 124 | if (!existingButton) { 125 | init(); 126 | } 127 | -------------------------------------------------------------------------------- /client/extension/src/ui.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | Hello, world! 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /client/extension/src/ui.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import { App } from "./lib/ui"; 5 | 6 | const root = ReactDOM.createRoot( 7 | document.getElementById("root") as HTMLElement 8 | ); 9 | 10 | root.render(); 11 | -------------------------------------------------------------------------------- /client/extension/src/worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { init } from "./lib/worker"; 4 | 5 | init(); 6 | -------------------------------------------------------------------------------- /client/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["bun-types", "chrome-types"], 4 | "lib": ["es5", "es6", "dom"], 5 | "jsx": "react" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /client/web/.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /client/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 41 | -------------------------------------------------------------------------------- /client/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/web/components/atoms/CheckboxWithLabel/index.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@/components/ui/checkbox"; 2 | 3 | interface CheckboxWithLabelProps { 4 | text: string; 5 | htmlFor: string; 6 | disabled?: boolean; 7 | selected?: boolean; 8 | theme?: "light" | "dark"; 9 | onCheckedChange?: (checked: boolean) => void; 10 | } 11 | 12 | export function CheckboxWithLabel({ 13 | text, 14 | htmlFor, 15 | disabled, 16 | selected, 17 | onCheckedChange, 18 | theme = "dark", 19 | }: CheckboxWithLabelProps): JSX.Element { 20 | return ( 21 |
28 | 35 | 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /client/web/components/atoms/Icon/Star.tsx: -------------------------------------------------------------------------------- 1 | const StarIcon = () => ( 2 | 9 | 14 | 21 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | export default StarIcon; 44 | -------------------------------------------------------------------------------- /client/web/components/atoms/Loader/DashboardLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as S from "./styles"; 2 | 3 | export default function DashboardLoader() { 4 | return ( 5 | 11 | hello 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/web/components/atoms/Loader/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | import * as S from "./styles"; 4 | 5 | export default function Home() { 6 | return ( 7 | 8 | 9 | AIEmploye 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/web/components/atoms/Loader/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const LoaderWrapper = styled.div` 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | width: 100%; 8 | height: calc(100vh - 64px); 9 | `; 10 | 11 | export const Loader = styled.div` 12 | width: 30px; 13 | height: 30px; 14 | border: 1px solid rgba(161, 161, 170, 0.5); 15 | border-bottom-color: transparent; 16 | border-radius: 50%; 17 | display: inline-block; 18 | box-sizing: border-box; 19 | animation: rotation 1s linear infinite; 20 | 21 | @keyframes rotation { 22 | 0% { 23 | transform: rotate(0deg); 24 | } 25 | 100% { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | `; 30 | -------------------------------------------------------------------------------- /client/web/components/atoms/Logo/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import * as S from "./styles"; 4 | 5 | export default function Logo() { 6 | return ( 7 | 8 | AIEmployee 9 |

AI Employe

10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/web/components/atoms/Logo/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const LogoLink = styled.a` 4 | display: flex; 5 | align-items: center; 6 | text-decoration: none; 7 | 8 | p { 9 | margin-left: 4px; 10 | color: white; 11 | font-weight: 500; 12 | } 13 | 14 | img { 15 | width: 50px; 16 | } 17 | `; 18 | -------------------------------------------------------------------------------- /client/web/components/atoms/ModalLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import { X } from "lucide-react"; 3 | 4 | import { useStore } from "@/store"; 5 | 6 | import * as S from "./styles"; 7 | 8 | interface ModalProps { 9 | children: React.ReactNode; 10 | } 11 | 12 | export default function Modal({ children }: ModalProps) { 13 | const { setSignInModal } = useStore(); 14 | const escPressed = useKeyPress("Escape"); 15 | 16 | const closeModal = useCallback(() => { 17 | setSignInModal(false); 18 | }, [setSignInModal]); 19 | 20 | useEffect(() => { 21 | if (escPressed) { 22 | closeModal(); 23 | } 24 | }, [escPressed, closeModal]); 25 | 26 | return ( 27 | closeModal()} 31 | > 32 | e.stopPropagation()} 36 | > 37 | closeModal()}> 38 | 39 | 40 | {children} 41 | 42 | 43 | ); 44 | } 45 | 46 | export const useKeyPress = (targetKey: string) => { 47 | const [keyPressed, setKeyPressed] = useState(false); 48 | 49 | useEffect(() => { 50 | const downHandler = ({ key }: KeyboardEvent) => { 51 | if (key === targetKey) setKeyPressed(true); 52 | }; 53 | 54 | const upHandler = ({ key }: KeyboardEvent) => { 55 | if (key === targetKey) setKeyPressed(false); 56 | }; 57 | 58 | window.addEventListener("keydown", downHandler); 59 | window.addEventListener("keyup", upHandler); 60 | 61 | return () => { 62 | window.removeEventListener("keydown", downHandler); 63 | window.removeEventListener("keyup", upHandler); 64 | }; 65 | }, [targetKey]); 66 | 67 | return keyPressed; 68 | }; 69 | -------------------------------------------------------------------------------- /client/web/components/atoms/ModalLayout/styles.ts: -------------------------------------------------------------------------------- 1 | import { motion } from "framer-motion"; 2 | import styled from "@emotion/styled"; 3 | 4 | export const ModalBackground = styled(motion.div)` 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | z-index: 100; 9 | width: 100%; 10 | height: 100%; 11 | backdrop-filter: blur(4px); 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | `; 16 | 17 | export const ModalContainer = styled(motion.div)` 18 | width: 400px; 19 | background-color: black; 20 | border-radius: 16px; 21 | border: 1px solid rgb(38, 38, 38); 22 | padding: 24px; 23 | position: relative; 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | 29 | @media (max-width: 768px) { 30 | width: 100%; 31 | height: 100%; 32 | border-radius: 0; 33 | } 34 | `; 35 | 36 | export const CloseBtn = styled.button` 37 | background-color: rgba(255, 255, 255, 0.1); 38 | width: 30px; 39 | height: 30px; 40 | border-radius: 50%; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | position: absolute; 45 | right: 16px; 46 | top: 16px; 47 | transition: all 0.2s ease-in-out; 48 | 49 | svg { 50 | path { 51 | stroke: rgba(255, 255, 255, 0.5); 52 | } 53 | } 54 | 55 | &:hover { 56 | background-color: rgba(255, 255, 255, 0.2); 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /client/web/components/molecules/BottomCTA/index.tsx: -------------------------------------------------------------------------------- 1 | import TinyBanner from "../TinyBanner"; 2 | import { SectionTitle } from "@/styles"; 3 | 4 | import * as S from "./styles"; 5 | import Timer from "../Timer"; 6 | import SignInButtons from "../SignInButtons"; 7 | 8 | const BottomCTA = () => { 9 | return ( 10 | 11 | 12 | 13 | {/* Save double the time and money with 14 |
AI Employee Buy now. */} 15 | Get Your AI Employe Now and
16 | Reclaim Your Week! 17 |
18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | export default BottomCTA; 25 | -------------------------------------------------------------------------------- /client/web/components/molecules/BottomCTA/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const BottomCtaWrapper = styled.div` 4 | margin-top: 48px; 5 | display: flex; 6 | justify-content: center; 7 | flex-direction: column; 8 | align-items: center; 9 | `; 10 | -------------------------------------------------------------------------------- /client/web/components/molecules/DashboardHeaderInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2 } from "lucide-react"; 2 | 3 | import * as DashboardStyles from "@/styles/dashboard"; 4 | import { Button } from "@/components/ui/button"; 5 | 6 | interface DashboardHeaderInfoProps { 7 | title: string; 8 | description: string; 9 | buttonText?: string; 10 | onclick?: () => void; 11 | loading?: boolean; 12 | buttonIcon?: React.ReactNode; 13 | } 14 | 15 | const DashboardHeaderInfo = ({ 16 | title, 17 | description, 18 | buttonText, 19 | onclick, 20 | loading, 21 | buttonIcon, 22 | }: DashboardHeaderInfoProps) => { 23 | return ( 24 | 25 | 26 | {title} 27 | 28 | {description} 29 | 30 | 31 | {buttonText && ( 32 | 37 | )} 38 | 39 | ); 40 | }; 41 | 42 | export default DashboardHeaderInfo; 43 | -------------------------------------------------------------------------------- /client/web/components/molecules/Features/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { motion } from "framer-motion"; 3 | 4 | const FEATURE_BREAKPOINT = 850; 5 | 6 | export const FeatureContainer = styled.div` 7 | position: relative; 8 | width: 80%; 9 | 10 | @media (max-width: 1550px) { 11 | width: 100%; 12 | } 13 | `; 14 | 15 | export const FeatureTitle = styled.h3` 16 | font-size: 24px; 17 | font-size: 32px; 18 | color: #787878; 19 | margin-top: 56px; 20 | margin-bottom: 24px; 21 | text-align: center; 22 | span { 23 | color: #fff; 24 | } 25 | `; 26 | 27 | export const FeatureWrapper = styled(motion.div)` 28 | width: 100%; 29 | min-height: 420px; 30 | border-radius: 20px; 31 | border: 1px solid rgba(120, 120, 120, 0.3); 32 | box-sizing: border-box; 33 | overflow: hidden; 34 | display: flex; 35 | margin-bottom: 24px; 36 | 37 | @media (max-width: ${FEATURE_BREAKPOINT}px) { 38 | flex-direction: column; 39 | min-height: 480px; 40 | } 41 | `; 42 | 43 | export const FeatLeft = styled.div` 44 | width: 50%; 45 | padding: 28px; 46 | 47 | @media (max-width: ${FEATURE_BREAKPOINT}px) { 48 | width: 100%; 49 | } 50 | `; 51 | 52 | export const FeatTitle = styled.p` 53 | color: #fff; 54 | font-size: 28px; 55 | `; 56 | 57 | export const FeatDescription = styled.p` 58 | color: #868686; 59 | font-size: 16px; 60 | font-weight: 300; 61 | margin-top: 4px; 62 | `; 63 | 64 | export const FeatRight = styled(motion.div)` 65 | width: 50%; 66 | position: relative; 67 | 68 | @media (max-width: ${FEATURE_BREAKPOINT}px) { 69 | width: 100%; 70 | } 71 | `; 72 | 73 | export const FeatBackground = styled.div` 74 | filter: blur(123px); 75 | width: 100%; 76 | height: 100%; 77 | 78 | @media (max-width: ${FEATURE_BREAKPOINT}px) { 79 | height: 400px; 80 | position: absolute; 81 | } 82 | `; 83 | 84 | export const FeatIconWrapper = styled.div` 85 | position: absolute; 86 | top: 15%; 87 | width: 100%; 88 | 89 | @media (max-width: ${FEATURE_BREAKPOINT}px) { 90 | padding: 0 24px; 91 | svg { 92 | width: 100%; 93 | height: auto; 94 | } 95 | } 96 | `; 97 | 98 | export const WidgetIconWrapper = styled.div` 99 | position: absolute; 100 | bottom: 5%; 101 | right: 5%; 102 | 103 | @media (max-width: ${FEATURE_BREAKPOINT}px) { 104 | z-index: 1; 105 | width: 100%; 106 | right: -60%; 107 | bottom: -230px; 108 | } 109 | 110 | @media (max-width: 500px) { 111 | /* Make it center */ 112 | right: -35%; 113 | bottom: -150px; 114 | } 115 | `; 116 | -------------------------------------------------------------------------------- /client/web/components/molecules/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { NavLinks } from "../Layout/LandingPageLayout"; 2 | 3 | import * as S from "./styles"; 4 | 5 | const Footer = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Footer; 14 | -------------------------------------------------------------------------------- /client/web/components/molecules/Footer/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const FooterContainer = styled.div``; 4 | -------------------------------------------------------------------------------- /client/web/components/molecules/Modals/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | export const SignInContainer = styled.div` 4 | width: 100%; 5 | `; 6 | 7 | export const ModalTitle = styled.h2` 8 | color: white; 9 | font-size: 18px; 10 | font-weight: 400; 11 | `; 12 | 13 | export const ModalDescription = styled.p``; 14 | 15 | export const InputContainer = styled.div``; 16 | 17 | export const SignInModalIcon = styled.div` 18 | width: 70px; 19 | height: 70px; 20 | background-color: rgba(255, 255, 255, 0.05); 21 | border-radius: 50%; 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | margin-bottom: 28px; 26 | 27 | svg { 28 | path, 29 | line, 30 | polyline { 31 | stroke: rgba(255, 255, 255, 0.3); 32 | } 33 | } 34 | `; 35 | -------------------------------------------------------------------------------- /client/web/components/molecules/Showcase/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { PlayIcon } from "lucide-react"; 3 | 4 | import * as S from "./styles"; 5 | 6 | const videoMeta = [ 7 | { 8 | title: "Automate logging your budget from email to your expense tracker", 9 | id: 0, 10 | url: "https://www.loom.com/embed/f8dbe36b7e824e8c9b5e96772826de03?sid=649c36e1-0743-4235-a2ca-cdf9a78d61df", 11 | }, 12 | { 13 | title: 14 | "Automate log details from the PDF receipt into your expense tracker", 15 | id: 1, 16 | url: "https://www.loom.com/embed/2caf488bbb76411993f9a7cdfeb80cd7?sid=d80800de-7997-4303-8f7f-8e163203fddf", 17 | }, 18 | { 19 | title: "Comparison: Adept.ai vs AIEmploye", 20 | id: 2, 21 | url: "https://www.loom.com/embed/27d1f8983572429a8a08efdb2c336fe8?sid=3eeaa778-4350-412c-8042-58805a63ad13", 22 | }, 23 | { 24 | title: "How to create a workflow", 25 | id: 3, 26 | url: "https://www.loom.com/embed/8327dfa677ec44869ab0d83235763998?sid=a676cdba-48e8-4f31-8cc2-5adf47989ae5", 27 | }, 28 | ]; 29 | 30 | const Showcase = () => { 31 | const [selectedVideoId, setSelectedVideoId] = useState(0); 32 | return ( 33 | 34 | 35 | {videoMeta.map((meta) => ( 36 | setSelectedVideoId(meta.id)} 38 | key={meta.id} 39 | {...meta} 40 | selected={selectedVideoId === meta.id} 41 | /> 42 | ))} 43 | 44 |
54 |