├── .gitignore ├── .prettierrc.json ├── README.md ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── app_screenshot.png ├── logo.png ├── logo_black.png └── logo_black_small.png ├── src └── app │ ├── components │ ├── HeaderBar.tsx │ ├── modals │ │ ├── AlertDialogComponent.tsx │ │ ├── OpenModal.tsx │ │ └── SaveModal.tsx │ └── panels │ │ ├── Debug.tsx │ │ ├── Functions.tsx │ │ ├── Options.tsx │ │ └── chat │ │ ├── Chat.tsx │ │ └── Message.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── providers.tsx │ ├── state │ ├── actions │ │ ├── chat.tsx │ │ ├── functions.tsx │ │ ├── options.tsx │ │ └── workspace.tsx │ ├── db.tsx │ └── index.ts │ └── types.tsx ├── tailwind.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  PromptScaper 2 | 3 | A client-only OpenAI LLM Playground for building agents without writing any code. 4 | 5 | You can save (in IndexedDB) and reopen workspaces later or export and share with a coworker. 6 | 7 | The goal of this project is to enable you to explore how function calling can improve your agents _before_ investing in actually deciding what those functions need to do. You just type in the function response as text. 8 | 9 |  10 | 11 | ## Run the live version 12 | 13 | [promptscaper.com](https://www.promptscaper.com/) 14 | 15 | ## Running Locally 16 | 17 | ```bash 18 | npm i 19 | ``` 20 | 21 | Run the development server: 22 | 23 | ```bash 24 | npm run dev 25 | # or 26 | yarn dev 27 | # or 28 | pnpm dev 29 | ``` 30 | 31 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 32 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["localhost", "promptscaper.com"], 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prompt-scape", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/anatomy": "^2.2.0", 13 | "@chakra-ui/next-js": "^2.1.5", 14 | "@chakra-ui/react": "^2.8.0", 15 | "@chakra-ui/styled-system": "^2.9.1", 16 | "@emotion/react": "^11.11.1", 17 | "@emotion/styled": "^11.11.0", 18 | "@hapi/hoek": "^11.0.2", 19 | "@monaco-editor/react": "^4.5.2", 20 | "@types/lodash": "^4.14.197", 21 | "@types/node": "20.4.5", 22 | "@types/react": "18.2.17", 23 | "@types/react-dom": "18.2.7", 24 | "@types/react-syntax-highlighter": "^15.5.7", 25 | "@types/uuid": "^9.0.2", 26 | "@vercel/analytics": "^1.0.2", 27 | "autoprefixer": "10.4.14", 28 | "daisyui": "^3.5.0", 29 | "encoding": "^0.1.13", 30 | "framer-motion": "^10.15.0", 31 | "immer": "^10.0.2", 32 | "lodash": "^4.17.21", 33 | "next": "13.4.12", 34 | "openai": "^4.2.0", 35 | "postcss": "8.4.27", 36 | "react": "18.2.0", 37 | "react-contexify": "^6.0.0", 38 | "react-dom": "18.2.0", 39 | "react-github-btn": "^1.4.0", 40 | "react-icons": "^4.10.1", 41 | "react-markdown": "^8.0.7", 42 | "react-syntax-highlighter": "^15.5.0", 43 | "sharp": "^0.32.5", 44 | "tailwindcss": "3.3.3", 45 | "typescript": "5.1.6", 46 | "userflow.js": "^2.9.0", 47 | "uuid": "^9.0.0", 48 | "zundo": "2.0.0-beta.24", 49 | "zustand": "^4.3.9" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/app_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtharrison/promptscaper/630e9e55c92dcf9bd2ea811fe9ed182452f7ddc5/public/app_screenshot.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtharrison/promptscaper/630e9e55c92dcf9bd2ea811fe9ed182452f7ddc5/public/logo.png -------------------------------------------------------------------------------- /public/logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtharrison/promptscaper/630e9e55c92dcf9bd2ea811fe9ed182452f7ddc5/public/logo_black.png -------------------------------------------------------------------------------- /public/logo_black_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtharrison/promptscaper/630e9e55c92dcf9bd2ea811fe9ed182452f7ddc5/public/logo_black_small.png -------------------------------------------------------------------------------- /src/app/components/HeaderBar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Flex, 5 | Heading, 6 | Button, 7 | IconButton, 8 | Tooltip, 9 | Link, 10 | HStack, 11 | Text, 12 | Menu, 13 | MenuButton, 14 | MenuList, 15 | MenuItem, 16 | } from "@chakra-ui/react"; 17 | 18 | import GitHubButton from "react-github-btn"; 19 | 20 | import { BiChevronDown } from "react-icons/bi"; 21 | // Chat 22 | import { 23 | AiOutlineFolderOpen, 24 | AiOutlineFork, 25 | AiOutlinePlus, 26 | AiOutlinePlusSquare, 27 | AiOutlineSave, 28 | AiFillEye, 29 | AiOutlineEyeInvisible, 30 | AiFillEyeInvisible, 31 | AiFillGithub, 32 | } from "react-icons/ai"; 33 | 34 | import Image from "next/image"; 35 | import Logo from "../../../public/logo.png"; 36 | 37 | import { BiUndo, BiRedo, BiImport, BiExport } from "react-icons/bi"; 38 | 39 | import { useAppStore, useTemporalStore } from "../state"; 40 | 41 | import { OpenModal } from "./modals/OpenModal"; 42 | import { SaveModal } from "./modals/SaveModal"; 43 | import { useEffect, useState } from "react"; 44 | 45 | export function HeaderBar() { 46 | const { undo, redo, pastStates, futureStates } = useTemporalStore( 47 | (state) => state 48 | ); 49 | 50 | const { 51 | newWorkspace, 52 | showOpenModal, 53 | showSaveModal, 54 | exportWorkspace, 55 | importWorkspace, 56 | toggleFunctionsPanelVisibility, 57 | toggleConfigPanelVisibility, 58 | toggleLogsPanelVisibility, 59 | functionsVisible, 60 | configVisible, 61 | logsVisible, 62 | } = useAppStore(({ workspaceActions, workspace }) => ({ 63 | newWorkspace: workspaceActions.newWorkspace, 64 | showOpenModal: workspaceActions.showOpenModal, 65 | hideOpenModal: workspaceActions.hideOpenModal, 66 | showSaveModal: workspaceActions.showSaveModal, 67 | hideSaveModal: workspaceActions.hideSaveModal, 68 | toggleFunctionsPanelVisibility: 69 | workspaceActions.toggleFunctionsPanelVisibility, 70 | toggleConfigPanelVisibility: workspaceActions.toggleConfigPanelVisibility, 71 | toggleLogsPanelVisibility: workspaceActions.toggleLogsPanelVisibility, 72 | exportWorkspace: workspaceActions.exportWorkspace, 73 | importWorkspace: workspaceActions.importWorkspace, 74 | functionsVisible: workspace.panels.functions.visible, 75 | configVisible: workspace.panels.config.visible, 76 | logsVisible: workspace.panels.logs.visible, 77 | })); 78 | 79 | const canUndo = !!pastStates.length; 80 | const canRedo = !!futureStates.length; 81 | 82 | const [showLogo, setShowLogo] = useState(false); 83 | useEffect(() => setShowLogo(true), []); 84 | 85 | return ( 86 | 93 | 100 | 101 | {showLogo && ( 102 | 109 | )} 110 | 111 | 112 | } 117 | > 118 | Workspace 119 | 120 | 121 | }> 122 | New 123 | 124 | }> 125 | Save 126 | 127 | } 130 | > 131 | Open 132 | 133 | }> 134 | Export 135 | 136 | }> 137 | Import 138 | 139 | 140 | 141 | 142 | 143 | 144 | undo(1)} 147 | size={"sm"} 148 | icon={} 149 | aria-label="Undo" 150 | colorScheme="blackAlpha" 151 | /> 152 | 153 | 154 | redo(1)} 157 | size={"sm"} 158 | icon={} 159 | aria-label="Redo" 160 | colorScheme="blackAlpha" 161 | /> 162 | 163 | 164 | 165 | 166 | 167 | 168 | : 171 | } 172 | onClick={toggleFunctionsPanelVisibility} 173 | colorScheme="blackAlpha" 174 | size={"sm"} 175 | > 176 | Toggle Functions 177 | 178 | 179 | : } 181 | onClick={toggleConfigPanelVisibility} 182 | colorScheme="blackAlpha" 183 | size={"sm"} 184 | > 185 | Toggle Options 186 | 187 | 188 | 189 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | ); 205 | } 206 | -------------------------------------------------------------------------------- /src/app/components/modals/AlertDialogComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | AlertDialog, 5 | Button, 6 | AlertDialogOverlay, 7 | AlertDialogBody, 8 | AlertDialogContent, 9 | AlertDialogHeader, 10 | AlertDialogFooter, 11 | useDisclosure, 12 | } from "@chakra-ui/react"; 13 | 14 | import { useRef } from "react"; 15 | 16 | interface AlertDialogComponentProps { 17 | title: string; 18 | message: string; 19 | action: Function; 20 | cancel: Function; 21 | show: boolean; 22 | } 23 | 24 | export function AlertDialogComponent(props: AlertDialogComponentProps) { 25 | const { title, message, action, cancel, show } = props; 26 | const cancelRef = useRef(null); 27 | 28 | return ( 29 | <> 30 | cancel()} 35 | colorScheme="blackAlpha" 36 | > 37 | 38 | 39 | 40 | {title} 41 | 42 | 43 | {message} 44 | 45 | 46 | cancel()}> 47 | Cancel 48 | 49 | action()} ml={3}> 50 | Yes 51 | 52 | 53 | 54 | 55 | 56 | > 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/app/components/modals/OpenModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalOverlay, 8 | ModalHeader, 9 | ModalCloseButton, 10 | Button, 11 | Table, 12 | TableContainer, 13 | Tr, 14 | Tbody, 15 | Td, 16 | } from "@chakra-ui/react"; 17 | import { useAppStore } from "../../state"; 18 | import { useEffect, useRef, useState } from "react"; 19 | 20 | import { loadAll } from "../../state/db"; 21 | import { SavedWorkspace } from "../../types"; 22 | 23 | export function OpenModal() { 24 | const { workspace, workspaceActions } = useAppStore(); 25 | const [workspaces, setWorkspaces] = useState([]); 26 | 27 | const initialRef = useRef(null); 28 | const finalRef = useRef(null); 29 | 30 | useEffect(() => { 31 | const fetch = async () => { 32 | const all = await loadAll(); 33 | setWorkspaces(all as any); 34 | }; 35 | 36 | fetch(); 37 | }); 38 | 39 | return ( 40 | 48 | 49 | 50 | Open workspace 51 | 52 | 53 | 54 | 55 | 56 | {workspaces 57 | .sort((a, b) => b.ts - a.ts) 58 | .map((w, i) => ( 59 | 60 | 61 | {w.name} 62 | 63 | 64 | last saved: {new Date(w.ts).toLocaleString()} 65 | 66 | 67 | workspaceActions.open(w.id)} 69 | colorScheme="blue" 70 | size={"sm"} 71 | mr={3} 72 | > 73 | Open 74 | 75 | workspaceActions.remove(w.id)} 77 | colorScheme="red" 78 | size={"sm"} 79 | > 80 | Delete 81 | 82 | 83 | 84 | ))} 85 | 86 | 87 | 88 | 89 | 90 | 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/app/components/modals/SaveModal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalOverlay, 8 | ModalHeader, 9 | ModalCloseButton, 10 | FormControl, 11 | Flex, 12 | FormLabel, 13 | Input, 14 | ModalFooter, 15 | Button, 16 | useDisclosure, 17 | Table, 18 | TableContainer, 19 | Thead, 20 | Tr, 21 | Th, 22 | Text, 23 | Tbody, 24 | Td, 25 | useToast, 26 | Divider, 27 | } from "@chakra-ui/react"; 28 | import { useAppStore } from "../../state"; 29 | import { useRef, useState, useEffect, RefObject } from "react"; 30 | import { loadAll } from "../../state/db"; 31 | 32 | import { SavedWorkspace } from "../../types"; 33 | 34 | export function SaveModal() { 35 | const { workspace, workspaceActions } = useAppStore(); 36 | const [workspaces, setWorkspaces] = useState([]); 37 | 38 | const toast = useToast(); 39 | 40 | const initialRef = useRef(null); 41 | const finalRef = useRef(null); 42 | 43 | useEffect(() => { 44 | const fetch = async () => { 45 | const all = await loadAll(); 46 | setWorkspaces(all as any); 47 | }; 48 | 49 | fetch(); 50 | }); 51 | 52 | return ( 53 | 61 | 62 | 63 | Save workspace 64 | 65 | 66 | 67 | 68 | New Name 69 | 70 | 71 | { 73 | if (initialRef.current) { 74 | await workspaceActions.save(initialRef.current.value); 75 | toast({ 76 | position: "top", 77 | size: "lg", 78 | title: "Workspace saved", 79 | status: "success", 80 | duration: 2000, 81 | isClosable: true, 82 | }); 83 | } 84 | }} 85 | colorScheme="blue" 86 | ml={5} 87 | mt={2} 88 | w={100} 89 | > 90 | Save 91 | 92 | 93 | 94 | 95 | 96 | Existing Workspaces 97 | 98 | 99 | 100 | {workspaces 101 | .sort((a, b) => b.ts - a.ts) 102 | .map((w, i) => ( 103 | 104 | 105 | {w.name} 106 | 107 | 108 | last saved: {new Date(w.ts).toLocaleString()} 109 | 110 | 111 | { 113 | await workspaceActions.overwrite(w.id); 114 | toast({ 115 | position: "top", 116 | size: "lg", 117 | title: "Workspace overwritten", 118 | status: "success", 119 | duration: 2000, 120 | isClosable: true, 121 | }); 122 | }} 123 | colorScheme="red" 124 | size={"sm"} 125 | > 126 | Overwrite 127 | 128 | 129 | 130 | ))} 131 | 132 | 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | -------------------------------------------------------------------------------- /src/app/components/panels/Debug.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardHeader, 6 | Heading, 7 | Text, 8 | CardBody, 9 | Stack, 10 | StackDivider, 11 | Box, 12 | Flex, 13 | } from "@chakra-ui/react"; 14 | 15 | export function Debug() { 16 | return ( 17 | 26 | 27 | 28 | LLM Options 29 | 30 | 31 | } spacing="4"> 32 | 33 | 34 | 35 | 36 | Temperature 37 | 38 | 39 | View a summary of all your clients over the last month. 40 | 41 | 42 | 43 | 44 | Temperature 45 | 46 | 47 | View a summary of all your clients over the last month. 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Temperature 57 | 58 | 59 | View a summary of all your clients over the last month. 60 | 61 | 62 | 63 | 64 | Temperature 65 | 66 | 67 | View a summary of all your clients over the last month. 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | System Prompt 78 | 79 | 80 | } spacing="4"> 81 | 82 | 83 | Summary 84 | 85 | 86 | View a summary of all your clients over the last month. 87 | 88 | 89 | 90 | 91 | Overview 92 | 93 | 94 | Check out the overview of your clients. 95 | 96 | 97 | 98 | 99 | Analysis 100 | 101 | 102 | See a detailed analysis of all your business clients. 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | System Prompt 111 | 112 | 113 | } spacing="4"> 114 | 115 | 116 | Summary 117 | 118 | 119 | View a summary of all your clients over the last month. 120 | 121 | 122 | 123 | 124 | Overview 125 | 126 | 127 | Check out the overview of your clients. 128 | 129 | 130 | 131 | 132 | Analysis 133 | 134 | 135 | See a detailed analysis of all your business clients. 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | Client Report 144 | 145 | 146 | } spacing="4"> 147 | 148 | 149 | Summary 150 | 151 | 152 | View a summary of all your clients over the last month. 153 | 154 | 155 | 156 | 157 | Overview 158 | 159 | 160 | Check out the overview of your clients. 161 | 162 | 163 | 164 | 165 | Analysis 166 | 167 | 168 | See a detailed analysis of all your business clients. 169 | 170 | 171 | 172 | 173 | 174 | 175 | ); 176 | } 177 | -------------------------------------------------------------------------------- /src/app/components/panels/Functions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Card, 5 | CardHeader, 6 | Heading, 7 | Text, 8 | CardBody, 9 | Stack, 10 | StackDivider, 11 | Box, 12 | Flex, 13 | Button, 14 | } from "@chakra-ui/react"; 15 | import { useAppStore } from "../../state"; 16 | import Editor from "@monaco-editor/react"; 17 | 18 | export function Functions() { 19 | const { functions, functionsActions } = useAppStore(); 20 | 21 | return ( 22 | 32 | 33 | 34 | >_ Functions Editor 35 | 36 | 41 | Show me an example 42 | 43 | 44 | { 61 | if (newText) { 62 | functionsActions.update(newText); 63 | } 64 | }} 65 | value={functions.functions} 66 | theme="vs-dark" 67 | /> 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/app/components/panels/Options.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Heading, 5 | Text, 6 | Checkbox, 7 | Box, 8 | Flex, 9 | Select, 10 | Slider, 11 | SliderTrack, 12 | SliderFilledTrack, 13 | SliderThumb, 14 | NumberInput, 15 | NumberInputField, 16 | NumberInputStepper, 17 | NumberIncrementStepper, 18 | NumberDecrementStepper, 19 | Textarea, 20 | Input, 21 | Alert, 22 | AlertStatus, 23 | AlertIcon, 24 | } from "@chakra-ui/react"; 25 | 26 | import { useAppStore } from "../../state"; 27 | import { useEffect } from "react"; 28 | 29 | type PasswordControl = { 30 | type: "password"; 31 | name: string; 32 | value: string; 33 | onChange: (value: string) => void; 34 | }; 35 | 36 | type SelectControl = { 37 | type: "select"; 38 | name: string; 39 | options: string[]; 40 | value: string; 41 | onChange: (value: string) => void; 42 | }; 43 | 44 | type RangeControl = { 45 | type: "range"; 46 | name: string; 47 | min: number; 48 | max: number; 49 | value: number; 50 | onChange: (value: number) => void; 51 | }; 52 | 53 | type NumberControl = { 54 | type: "number"; 55 | name: string; 56 | value: number; 57 | onChange: (value: number) => void; 58 | }; 59 | 60 | type TextareaControl = { 61 | type: "textarea"; 62 | name: string; 63 | title: boolean; 64 | value: string; 65 | onChange: (value: string) => void; 66 | }; 67 | 68 | type Control = 69 | | SelectControl 70 | | RangeControl 71 | | NumberControl 72 | | TextareaControl 73 | | PasswordControl; 74 | 75 | function ControlItem({ control }: { control: Control }) { 76 | const storeApiKey = useAppStore((state) => state.options.llm.storeApiKey); 77 | const toggleStoreApiKey = useAppStore( 78 | (state) => state.optionsActions.toggleStoreApiKey 79 | ); 80 | 81 | if (control.type === "select") { 82 | return ( 83 | 84 | 85 | {control.name} 86 | 87 | control.onChange(e.target.value)} 91 | placeholder="Select option" 92 | value={control.value} 93 | > 94 | {control.options.map((o, i) => ( 95 | 96 | {o} 97 | 98 | ))} 99 | 100 | 101 | ); 102 | } 103 | 104 | if (control.type === "password") { 105 | return ( 106 | 107 | 108 | 109 | 110 | {control.name} 111 | 112 | control.onChange(e.target.value)} 119 | value={control.value} 120 | /> 121 | 122 | 123 | 124 | Cache locally? 125 | 126 | 131 | 132 | 133 | 134 | ); 135 | } 136 | 137 | if (control.type === "range") { 138 | return ( 139 | 140 | 141 | {control.name} 142 | 143 | control.onChange(e)} 151 | > 152 | 153 | 154 | 155 | 156 | 157 | 158 | {control.value} 159 | 160 | ); 161 | } 162 | 163 | if (control.type === "number") { 164 | return ( 165 | 166 | 167 | {control.name} 168 | 169 | control.onChange(parseFloat(e))} 173 | > 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | ); 182 | } 183 | 184 | if (control.type === "textarea") { 185 | return ( 186 | 187 | {control.title && ( 188 | 189 | {control.name} 190 | 191 | )} 192 | 198 | 199 | ); 200 | } 201 | } 202 | 203 | export function Options() { 204 | const { 205 | model, 206 | temperature, 207 | top_p, 208 | max_tokens, 209 | apiKey, 210 | frequency_penalty, 211 | presence_penalty, 212 | storeApiKey, 213 | } = useAppStore((state) => state.options.llm); 214 | 215 | const { 216 | updateApiKey, 217 | updateModel, 218 | updateTemperature, 219 | updateTopP, 220 | updateFrequencyPenalty, 221 | updatePresencePenalty, 222 | updateMaxTokens, 223 | } = useAppStore((state) => state.optionsActions); 224 | 225 | useEffect(() => { 226 | const cachedKey = sessionStorage.getItem("promptScaper-api-key"); 227 | console.log(cachedKey); 228 | if (cachedKey) { 229 | updateApiKey(cachedKey); 230 | } 231 | }, []); 232 | 233 | const llmControls: Control[] = [ 234 | { 235 | type: "password", 236 | name: "OpenAI API Key", 237 | value: apiKey, 238 | onChange: updateApiKey, 239 | }, 240 | { 241 | type: "select", 242 | name: "model", 243 | options: ["gpt-4", "gpt-4-32k", "gpt-3.5-turbo", "gpt-3.5-turbo-16k"], 244 | value: model, 245 | onChange: updateModel, 246 | }, 247 | { 248 | type: "range", 249 | name: "temperature", 250 | min: 0, 251 | max: 2, 252 | value: temperature, 253 | onChange: updateTemperature, 254 | }, 255 | { 256 | type: "range", 257 | name: "top_p", 258 | min: 0, 259 | max: 1, 260 | value: top_p, 261 | onChange: updateTopP, 262 | }, 263 | { 264 | type: "range", 265 | name: "frequency_penalty", 266 | min: 0, 267 | max: 1, 268 | value: frequency_penalty, 269 | onChange: updateFrequencyPenalty, 270 | }, 271 | { 272 | type: "range", 273 | name: "presence_penalty", 274 | min: 0, 275 | max: 1, 276 | value: presence_penalty, 277 | onChange: updatePresencePenalty, 278 | }, 279 | { 280 | type: "number", 281 | name: "max_tokens", 282 | value: max_tokens, 283 | onChange: updateMaxTokens, 284 | }, 285 | ]; 286 | 287 | return ( 288 | 297 | {!apiKey && ( 298 | 299 | 300 | 301 | Enter your OpenAI API Key. This will not be stored unless you 302 | choose to cache in local browser storage 303 | 304 | 305 | )} 306 | 307 | {llmControls.map((c, i) => ( 308 | 309 | ))} 310 | 311 | 312 | ); 313 | } 314 | -------------------------------------------------------------------------------- /src/app/components/panels/chat/Chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Avatar, 5 | AvatarGroup, 6 | Box, 7 | IconButton, 8 | Flex, 9 | Input, 10 | FormControl, 11 | Tooltip, 12 | Divider, 13 | Progress, 14 | } from "@chakra-ui/react"; 15 | 16 | // Chat 17 | import { AiOutlineSend, AiOutlinePlaySquare } from "react-icons/ai"; 18 | import { useAppStore, useTemporalStore } from "../../../state"; 19 | import { Message } from "./Message"; 20 | 21 | export function Chat() { 22 | const { messages, input, completionPending } = useAppStore( 23 | (state) => state.chat 24 | ); 25 | const apiKey = useAppStore((state) => state.options.llm.apiKey); 26 | const { updateInput, sendMessage, play } = useAppStore( 27 | (state) => state.chatActions 28 | ); 29 | const { pause, resume } = useTemporalStore((state) => state); 30 | 31 | const messageToSend = input.length > 0; 32 | 33 | return ( 34 | 45 | 46 | 53 | {completionPending && } 54 | {messages 55 | .slice() 56 | .reverse() 57 | .map((message, i) => ( 58 | 59 | ))} 60 | 61 | 62 | 70 | 79 | 80 | { 88 | pause(); 89 | updateInput(e.target.value); 90 | resume(); 91 | }} 92 | onKeyDown={(e) => { 93 | if (e.key === "Enter") { 94 | sendMessage(); 95 | } 96 | }} 97 | value={input} 98 | type="text" 99 | placeholder="Type a user message" 100 | /> 101 | 102 | 103 | 104 | 116 | : } 120 | aria-label="send" 121 | height={"100%"} 122 | width={"100px"} 123 | colorScheme={messageToSend ? "blue" : "green"} 124 | onClick={() => (messageToSend ? sendMessage() : play())} 125 | /> 126 | 127 | 128 | 129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /src/app/components/panels/chat/Message.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Badge, 5 | IconButton, 6 | Tooltip, 7 | Flex, 8 | Box, 9 | Heading, 10 | Text, 11 | ButtonGroup, 12 | Tag, 13 | TagLabel, 14 | Textarea, 15 | Avatar, 16 | AvatarBadge, 17 | AvatarGroup, 18 | Button, 19 | } from "@chakra-ui/react"; 20 | 21 | import _ from "lodash"; 22 | 23 | import { 24 | AiOutlineUser, 25 | AiOutlineRobot, 26 | AiOutlineLaptop, 27 | AiOutlineCode, 28 | } from "react-icons/ai"; 29 | import { RiDeleteBack2Fill } from "react-icons/ri"; 30 | import { TiDelete } from "react-icons/ti"; 31 | import { useAppStore } from "../../../state"; 32 | import { ChatMessage } from "../../../types"; 33 | import { MouseEvent, useCallback, useEffect, useRef, useState } from "react"; 34 | import ReactMarkdown from "react-markdown"; 35 | 36 | function messageColor(role: string) { 37 | if (role === "user") { 38 | return "teal"; 39 | } 40 | 41 | if (role === "function") { 42 | return "blue"; 43 | } 44 | 45 | if (role === "assistant") { 46 | return "purple"; 47 | } 48 | 49 | return "orange"; 50 | } 51 | 52 | function messageIcon(role: string) { 53 | if (role === "user") { 54 | return ; 55 | } 56 | 57 | if (role === "assistant") { 58 | return ; 59 | } 60 | 61 | if (role === "function") { 62 | return ; 63 | } 64 | 65 | return ; 66 | } 67 | 68 | function formatFunctionCall(call: any) { 69 | const name = call.name; 70 | const args = JSON.parse(call.arguments); 71 | return `\`\`\`json\n${JSON.stringify({ name, args }, null, 2)}\n\`\`\``; 72 | } 73 | 74 | function renderAvatarForMessage(message: ChatMessage) { 75 | return ( 76 | 85 | 90 | 91 | {message.function_call 92 | ? "Function Call" 93 | : message.name 94 | ? "Function Response" 95 | : _.startCase(message.role)} 96 | 97 | 98 | ); 99 | } 100 | 101 | export function Message({ 102 | message, 103 | index, 104 | }: { 105 | message: ChatMessage; 106 | index: number; 107 | }) { 108 | const { chatActions } = useAppStore(); 109 | const { pause, resume } = useAppStore.temporal.getState(); 110 | const { 111 | updateMessage, 112 | rewindToMessage, 113 | deleteMessage, 114 | submitFunctionCallResponse, 115 | } = chatActions; 116 | 117 | const [editing, setEditing] = useState(false); 118 | const [showReturnValueTextArea, setShowReturnValueTextArea] = 119 | useState(false); 120 | const [returnValue, setReturnValue] = useState(""); 121 | const [editedText, setEditedText] = useState(message.content); 122 | const textareaRef = useRef(null); 123 | 124 | useEffect(() => { 125 | setEditedText(message.content); 126 | }, [message.content]); 127 | 128 | useEffect(() => { 129 | if (editing && textareaRef.current) { 130 | const el = textareaRef.current as HTMLTextAreaElement; 131 | el.focus(); 132 | el.setSelectionRange(el.value.length, el.value.length); 133 | } 134 | }, [editing]); 135 | 136 | const maybeSetEditing = useCallback((event: MouseEvent) => { 137 | if (event.detail === 2) { 138 | pause(); 139 | setEditing(true); 140 | } 141 | }, []); 142 | 143 | const finishEditing = useCallback(() => { 144 | setEditing(false); 145 | resume(); 146 | if (message.content !== editedText) { 147 | updateMessage(message.id, editedText); 148 | } 149 | }, [editedText, message.content]); 150 | 151 | return ( 152 | 153 | 165 | 166 | {renderAvatarForMessage(message)} 167 | 168 | {editing ? ( 169 | setEditedText(e.target.value)} 176 | value={editedText} 177 | > 178 | ) : ( 179 | 180 | 188 | 189 | )} 190 | {message.function_call && index === 0 && ( 191 | <> 192 | {!showReturnValueTextArea && ( 193 | } 195 | w={"min-content"} 196 | mt={5} 197 | onClick={() => setShowReturnValueTextArea(true)} 198 | colorScheme="purple" 199 | > 200 | Set return value 201 | 202 | )} 203 | {showReturnValueTextArea && ( 204 | <> 205 | setReturnValue(e.target.value)} 211 | > 212 | 213 | { 218 | submitFunctionCallResponse(message.id, returnValue); 219 | setShowReturnValueTextArea(false); 220 | setReturnValue(""); 221 | }} 222 | > 223 | Submit 224 | 225 | setShowReturnValueTextArea(false)} 231 | > 232 | Cancel 233 | 234 | 235 | > 236 | )} 237 | > 238 | )} 239 | 240 | 250 | 251 | } 256 | aria-label="Delete message" 257 | onClick={() => deleteMessage(message.id)} 258 | /> 259 | 260 | 261 | 265 | } 269 | aria-label="xxx" 270 | size={"sm"} 271 | onClick={() => rewindToMessage(message.id)} 272 | /> 273 | 274 | 275 | 276 | ); 277 | } 278 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mtharrison/promptscaper/630e9e55c92dcf9bd2ea811fe9ed182452f7ddc5/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | body { 2 | height: 100vh; 3 | background-color: #4158D0; 4 | background-image: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%)!important; 5 | } 6 | 7 | .message-markdown > ol, ul { 8 | list-style-position: inside; 9 | } 10 | 11 | .message-markdown > p { 12 | margin: 5px 0px; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | // app/layout.tsx 2 | import "./globals.css"; 3 | import { Providers } from "./providers"; 4 | import { Analytics } from "@vercel/analytics/react"; 5 | 6 | export default function RootLayout({ 7 | children, 8 | }: { 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 | 13 | 14 | PromptScaper 15 | 16 | 17 | 18 | {children} 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AlertDialogComponent } from "./components/modals/AlertDialogComponent"; 4 | import { HeaderBar } from "./components/HeaderBar"; 5 | import { Chat } from "./components/panels/chat/Chat"; 6 | import { Functions } from "./components/panels/Functions"; 7 | import { Options } from "./components/panels/Options"; 8 | 9 | import { Flex, Text, Image } from "@chakra-ui/react"; 10 | import { useAppStore } from "./state"; 11 | 12 | import userflow from "userflow.js"; 13 | import { useEffect } from "react"; 14 | import Link from "next/link"; 15 | 16 | export default function Home() { 17 | useEffect(() => { 18 | userflow.init("ct_wfobtiv5hzhavo5h4m44qn2x5q"); 19 | userflow.identifyAnonymous({ 20 | website_lead: true, 21 | }); 22 | userflow.start("7200cc34-31dd-40d4-abe9-a3b3a382fa85"); 23 | }, []); 24 | 25 | const { dialog, panels } = useAppStore(({ workspace }) => ({ 26 | dialog: workspace.dialog, 27 | panels: workspace.panels, 28 | })); 29 | return ( 30 | 38 | 39 | 40 | 41 | 42 | 49 | {panels.functions.visible && } 50 | {panels.config.visible && } 51 | 52 | 53 | 54 | 58 | 62 | 63 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | // app/providers.tsx 2 | "use client"; 3 | 4 | import { CacheProvider } from "@chakra-ui/next-js"; 5 | import { ChakraProvider } from "@chakra-ui/react"; 6 | 7 | import { extendTheme } from "@chakra-ui/react"; 8 | import { modalAnatomy as parts } from "@chakra-ui/anatomy"; 9 | import { createMultiStyleConfigHelpers } from "@chakra-ui/styled-system"; 10 | 11 | const { definePartsStyle, defineMultiStyleConfig } = 12 | createMultiStyleConfigHelpers(parts.keys); 13 | 14 | const baseStyle = definePartsStyle({ 15 | // define the part you're going to style 16 | overlay: { 17 | bg: "blackAlpha.700", //change the background 18 | }, 19 | dialog: { 20 | mt: 100, 21 | borderRadius: "md", 22 | bg: `gray.800`, 23 | color: "white", 24 | }, 25 | }); 26 | 27 | export const modalTheme = defineMultiStyleConfig({ 28 | baseStyle, 29 | }); 30 | 31 | export const theme = extendTheme({ 32 | components: { Modal: modalTheme }, 33 | }); 34 | 35 | export function Providers({ children }: { children: React.ReactNode }) { 36 | return ( 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/state/actions/chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ChatActions, Application, ChatMessage } from "@/app/types"; 4 | import { produce } from "immer"; 5 | import { v4 } from "uuid"; 6 | import OpenAI from "openai"; 7 | import va from "@vercel/analytics"; 8 | 9 | export default ( 10 | set: ( 11 | partial: 12 | | Application 13 | | Partial 14 | | ((state: Application) => Application | Partial), 15 | replace?: boolean | undefined 16 | ) => void, 17 | get: () => Application 18 | ): ChatActions => ({ 19 | updateInput: (input: string) => 20 | set( 21 | produce((state: Application) => { 22 | state.chat.input = input; 23 | }) 24 | ), 25 | updateMessage: (msgId: string, content: string) => 26 | set( 27 | produce((state: Application) => { 28 | const msg = state.chat.messages.find(({ id }) => id === msgId); 29 | if (msg?.content) { 30 | msg.content = content; 31 | } 32 | }) 33 | ), 34 | deleteMessage: (delId: string) => 35 | set( 36 | produce((state: Application) => { 37 | state.chat.messages = state.chat.messages.filter( 38 | ({ id }) => id !== delId 39 | ); 40 | }) 41 | ), 42 | rewindToMessage: (rewId: string) => 43 | set( 44 | produce((state: Application) => { 45 | let stop = false; 46 | state.chat.messages = state.chat.messages.filter(({ id }) => { 47 | if (stop) { 48 | return false; 49 | } 50 | 51 | if (id === rewId) { 52 | stop = true; 53 | } 54 | 55 | return true; 56 | }); 57 | }) 58 | ), 59 | submitFunctionCallResponse: async (srcId: string, value: string) => { 60 | const name = get().chat.messages.find(({ id }) => srcId === id) 61 | ?.function_call.name as string; 62 | 63 | va.track("function_call_response_added"); 64 | 65 | const newMessage: ChatMessage = { 66 | id: v4(), 67 | content: value, 68 | role: "function", 69 | name, 70 | }; 71 | 72 | get().chatActions.sendMessage(newMessage); 73 | }, 74 | sendMessage: async (newMessage?: ChatMessage | null) => { 75 | if (newMessage !== null) { 76 | if (newMessage === undefined) { 77 | newMessage = { 78 | id: v4(), 79 | content: get().chat.input, 80 | role: "user", 81 | } as ChatMessage; 82 | } 83 | 84 | set(({ chat }) => ({ 85 | chat: { 86 | ...chat, 87 | messages: [...chat.messages, newMessage as ChatMessage], 88 | input: "", 89 | completionPending: true, 90 | }, 91 | })); 92 | } else { 93 | set( 94 | produce((state: Application) => { 95 | state.chat.completionPending = true; 96 | }) 97 | ); 98 | } 99 | 100 | let functions; 101 | try { 102 | functions = JSON.parse(get().functions.functions); 103 | } catch (err) { 104 | functions = undefined; 105 | } 106 | 107 | const { 108 | apiKey, 109 | model, 110 | temperature, 111 | top_p, 112 | frequency_penalty, 113 | presence_penalty, 114 | max_tokens, 115 | } = get().options.llm; 116 | 117 | const openai = new OpenAI({ 118 | apiKey, 119 | dangerouslyAllowBrowser: true, 120 | }); 121 | 122 | va.track("chat_completion_performed"); 123 | 124 | const completion = await openai.chat.completions.create({ 125 | messages: get().chat.messages.map( 126 | ({ role, content, function_call, name }) => ({ 127 | role, 128 | content, 129 | function_call, 130 | name, 131 | }) 132 | ), 133 | temperature, 134 | top_p, 135 | frequency_penalty, 136 | presence_penalty, 137 | max_tokens, 138 | functions, 139 | model, 140 | }); 141 | 142 | const resMessage = completion.choices[0].message as ChatMessage; 143 | resMessage.id = v4(); 144 | 145 | set(({ chat }) => ({ 146 | chat: { 147 | ...chat, 148 | messages: [...chat.messages, resMessage], 149 | input: "", 150 | completionPending: false, 151 | }, 152 | })); 153 | }, 154 | play: () => { 155 | get().chatActions.sendMessage(null); 156 | }, 157 | }); 158 | -------------------------------------------------------------------------------- /src/app/state/actions/functions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Application, FunctionsActions } from "@/app/types"; 4 | import { produce } from "immer"; 5 | 6 | const exampleFunction = `[ 7 | { 8 | "name": "get_current_weather", 9 | "description": "Get the current weather in a given location", 10 | "parameters": { 11 | "type": "object", 12 | "properties": { 13 | "location": { 14 | "type": "string", 15 | "description": "The city and state, e.g. San Francisco, CA" 16 | }, 17 | "unit": { 18 | "type": "string", 19 | "enum": ["celsius", "fahrenheit"] 20 | } 21 | }, 22 | "required": ["location"] 23 | } 24 | } 25 | ]`; 26 | 27 | export default ( 28 | set: ( 29 | partial: 30 | | Application 31 | | Partial 32 | | ((state: Application) => Application | Partial), 33 | replace?: boolean | undefined 34 | ) => void 35 | ): FunctionsActions => ({ 36 | update: (text: string) => { 37 | set( 38 | produce((state) => { 39 | state.functions.functions = text; 40 | }) 41 | ); 42 | }, 43 | addExample: () => 44 | set( 45 | produce((state) => { 46 | state.functions.functions = exampleFunction; 47 | }) 48 | ), 49 | }); 50 | -------------------------------------------------------------------------------- /src/app/state/actions/options.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { OptionsActions, Application } from "@/app/types"; 4 | import { produce } from "immer"; 5 | 6 | export default ( 7 | set: ( 8 | partial: 9 | | Application 10 | | Partial 11 | | ((state: Application) => Application | Partial), 12 | replace?: boolean | undefined 13 | ) => void, 14 | get: () => Application 15 | ): OptionsActions => ({ 16 | updateApiKey: (input: string) => 17 | set( 18 | produce((state: Application) => { 19 | state.options.llm.apiKey = input; 20 | if (typeof window !== "undefined" && state.options.llm.storeApiKey) { 21 | window.sessionStorage.setItem("promptScaper-api-key", input); 22 | } 23 | }) 24 | ), 25 | updateModel: (input: string) => 26 | set( 27 | produce((state: Application) => { 28 | state.options.llm.model = input; 29 | }) 30 | ), 31 | updateTemperature: (input: number) => 32 | set( 33 | produce((state: Application) => { 34 | state.options.llm.temperature = input; 35 | }) 36 | ), 37 | updateTopP: (input: number) => 38 | set( 39 | produce((state: Application) => { 40 | state.options.llm.top_p = input; 41 | }) 42 | ), 43 | updateFrequencyPenalty: (input: number) => 44 | set( 45 | produce((state: Application) => { 46 | state.options.llm.frequency_penalty = input; 47 | }) 48 | ), 49 | updatePresencePenalty: (input: number) => 50 | set( 51 | produce((state: Application) => { 52 | state.options.llm.presence_penalty = input; 53 | }) 54 | ), 55 | updateMaxTokens: (input: number) => 56 | set( 57 | produce((state: Application) => { 58 | state.options.llm.max_tokens = input; 59 | }) 60 | ), 61 | toggleStoreApiKey: () => 62 | set( 63 | produce((state: Application) => { 64 | state.options.llm.storeApiKey = !state.options.llm.storeApiKey; 65 | if (state.options.llm.storeApiKey) { 66 | const apiKey = get().options.llm.apiKey; 67 | if (typeof window !== "undefined" && state.options.llm.storeApiKey) { 68 | window.sessionStorage.setItem("promptScaper-api-key", apiKey); 69 | } 70 | } else { 71 | if (typeof window !== "undefined") { 72 | window.sessionStorage.removeItem("promptScaper-api-key"); 73 | } 74 | } 75 | }) 76 | ), 77 | }); 78 | -------------------------------------------------------------------------------- /src/app/state/actions/workspace.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Application, WorkspaceActions } from "@/app/types"; 4 | import { produce } from "immer"; 5 | import { v4 } from "uuid"; 6 | import { load, save, remove } from "../db"; 7 | import { pick, omit, defaultsDeep } from "lodash"; 8 | import { initialState } from ".."; 9 | import va from "@vercel/analytics"; 10 | 11 | const savedKeys = ["chat", "options", "workspace", "functions"]; 12 | const omitKeys = ["options.llm.apiKey", "options.llm.storeApiKey"]; 13 | 14 | export default ( 15 | set: ( 16 | partial: 17 | | Application 18 | | Partial 19 | | ((state: Application) => Application | Partial), 20 | replace?: boolean | undefined 21 | ) => void, 22 | get: () => Application 23 | ): WorkspaceActions => ({ 24 | showSaveModal: () => 25 | set( 26 | produce((state: Application) => { 27 | state.workspace.saveModal.show = true; 28 | }) 29 | ), 30 | hideSaveModal: () => 31 | set( 32 | produce((state: Application) => { 33 | state.workspace.saveModal.show = false; 34 | }) 35 | ), 36 | showOpenModal: () => 37 | set( 38 | produce((state: Application) => { 39 | state.workspace.openModal.show = true; 40 | }) 41 | ), 42 | hideOpenModal: () => 43 | set( 44 | produce((state: Application) => { 45 | state.workspace.openModal.show = false; 46 | }) 47 | ), 48 | save: async (name: string) => { 49 | va.track("saved_workspace"); 50 | 51 | get().workspaceActions.hideSaveModal(); 52 | const s = omit(pick(get(), savedKeys), omitKeys); 53 | await save({ 54 | id: v4(), 55 | state: JSON.stringify(s), 56 | name, 57 | ts: Date.now(), 58 | }); 59 | }, 60 | overwrite: async (id: string) => { 61 | get().workspaceActions.hideSaveModal(); 62 | const s = omit(pick(get(), savedKeys), omitKeys); 63 | const item = await load(id); 64 | await save({ 65 | id, 66 | state: JSON.stringify(s), 67 | name: item.name, 68 | ts: Date.now(), 69 | }); 70 | }, 71 | open: async (id: string) => { 72 | va.track("opened_workspace"); 73 | 74 | get().workspaceActions.hideOpenModal(); 75 | const item = await load(id); 76 | //@ts-ignore 77 | const savedState = JSON.parse(item.state) as Partial; 78 | set((state) => defaultsDeep(savedState, state)); 79 | }, 80 | remove: async (id: string) => { 81 | await remove(id); 82 | }, 83 | newWorkspace: () => { 84 | set(({ workspace }) => { 85 | return { 86 | workspace: { 87 | ...workspace, 88 | dialog: { 89 | show: true, 90 | title: "Create new workspace", 91 | message: 92 | "You will lose all unsaved changes, are you sure you want to continue?", 93 | action: () => { 94 | set(({ workspace }) => ({ 95 | ...initialState, 96 | workspace: { 97 | ...workspace, 98 | dialog: { 99 | ...workspace.dialog, 100 | show: false, 101 | }, 102 | }, 103 | })); 104 | }, 105 | cancel: () => { 106 | set(({ workspace }) => ({ 107 | workspace: { 108 | ...workspace, 109 | dialog: { 110 | ...workspace.dialog, 111 | show: false, 112 | }, 113 | }, 114 | })); 115 | }, 116 | }, 117 | }, 118 | }; 119 | }); 120 | }, 121 | toggleFunctionsPanelVisibility: () => 122 | set( 123 | produce((state) => { 124 | state.workspace.panels.functions.visible = 125 | !state.workspace.panels.functions.visible; 126 | }) 127 | ), 128 | toggleConfigPanelVisibility: () => 129 | set( 130 | produce((state) => { 131 | state.workspace.panels.config.visible = 132 | !state.workspace.panels.config.visible; 133 | }) 134 | ), 135 | toggleLogsPanelVisibility: () => 136 | set( 137 | produce((state) => { 138 | state.workspace.panels.logs.visible = 139 | !state.workspace.panels.logs.visible; 140 | }) 141 | ), 142 | exportWorkspace: () => { 143 | if (typeof document === "undefined") { 144 | return; 145 | } 146 | va.track("exported_workspace"); 147 | const filename = `prompscaper-${Date.now()}.json`; 148 | const s = omit(pick(get(), savedKeys), omitKeys); 149 | const json = JSON.stringify(s); 150 | const blob = new Blob([json], { type: "application/json" }); 151 | const url = URL.createObjectURL(blob); 152 | const link = document.createElement("a"); 153 | link.setAttribute("download", filename); 154 | link.href = url; 155 | document.body.appendChild(link); 156 | link.click(); 157 | document.body.removeChild(link); 158 | URL.revokeObjectURL(url); 159 | }, 160 | importWorkspace: async () => { 161 | const pickerOpts = { 162 | types: [ 163 | { 164 | description: "json", 165 | accept: { 166 | "text/*": [".json"], 167 | }, 168 | }, 169 | ], 170 | excludeAcceptAllOption: true, 171 | multiple: false, 172 | }; 173 | //@ts-ignore 174 | const handle = await window.showOpenFilePicker(pickerOpts); 175 | const file = await handle[0].getFile(); 176 | const reader = new FileReader(); 177 | 178 | va.track("imported_workspace"); 179 | 180 | reader.addEventListener( 181 | "load", 182 | () => { 183 | const savedState = JSON.parse( 184 | reader.result as string 185 | ) as Partial; 186 | set((state) => ({ ...state, ...savedState })); 187 | }, 188 | false 189 | ); 190 | 191 | if (file) { 192 | reader.readAsText(file); 193 | } 194 | }, 195 | }); 196 | -------------------------------------------------------------------------------- /src/app/state/db.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { SavedWorkspace } from "../types"; 3 | 4 | const VERSION_NUMBER = 1; 5 | 6 | export function getDb(): Promise { 7 | return new Promise((resolve, reject) => { 8 | let db; 9 | 10 | const DBOpenRequest = window.indexedDB.open("promptscaper", VERSION_NUMBER); 11 | 12 | DBOpenRequest.addEventListener("error", (err) => reject(err)); 13 | 14 | DBOpenRequest.addEventListener("success", () => { 15 | db = DBOpenRequest.result; 16 | resolve(db); 17 | }); 18 | 19 | DBOpenRequest.addEventListener( 20 | "upgradeneeded", 21 | (init: IDBVersionChangeEvent) => { 22 | //@ts-ignore 23 | db = init.target.result; 24 | 25 | // Create an objectStore for this database 26 | const objectStore = db.createObjectStore( 27 | `workspaces_${VERSION_NUMBER}`, 28 | { 29 | keyPath: "id", 30 | } 31 | ); 32 | 33 | objectStore.createIndex("name", "name", { unique: false }); 34 | objectStore.createIndex("ts", "ts", { unique: false }); 35 | objectStore.createIndex("state", "state", { unique: false }); 36 | } 37 | ); 38 | }); 39 | } 40 | 41 | export async function loadAll(): Promise { 42 | return new Promise(async (resolve, reject) => { 43 | const db: IDBDatabase = await getDb(); 44 | const objectStore = db 45 | .transaction(`workspaces_${VERSION_NUMBER}`) 46 | .objectStore(`workspaces_${VERSION_NUMBER}`); 47 | const all = objectStore.getAll(); 48 | //@ts-ignore 49 | all.onsuccess = (e) => resolve(e.target.result); 50 | all.onerror = (e) => reject(e); 51 | }); 52 | } 53 | 54 | export async function save(item: SavedWorkspace): Promise { 55 | return new Promise(async (resolve, reject) => { 56 | const db: IDBDatabase = await getDb(); 57 | const objectStore = db 58 | .transaction(`workspaces_${VERSION_NUMBER}`, "readwrite") 59 | .objectStore(`workspaces_${VERSION_NUMBER}`); 60 | const add = objectStore.put(item); 61 | //@ts-ignore 62 | add.onsuccess = (e) => resolve(); 63 | add.onerror = (e) => reject(e); 64 | }); 65 | } 66 | 67 | export async function load(id: string): Promise { 68 | return new Promise(async (resolve, reject) => { 69 | const db: IDBDatabase = await getDb(); 70 | const objectStore = db 71 | .transaction(`workspaces_${VERSION_NUMBER}`, "readwrite") 72 | .objectStore(`workspaces_${VERSION_NUMBER}`); 73 | const get = objectStore.get(id); 74 | get.onsuccess = (e) => resolve(get.result); 75 | get.onerror = (e) => reject(e); 76 | }); 77 | } 78 | 79 | export async function remove(id: string): Promise { 80 | return new Promise(async (resolve, reject) => { 81 | const db: IDBDatabase = await getDb(); 82 | const objectStore = db 83 | .transaction(`workspaces_${VERSION_NUMBER}`, "readwrite") 84 | .objectStore(`workspaces_${VERSION_NUMBER}`); 85 | const remove = objectStore.delete(id); 86 | remove.onsuccess = (e) => resolve(); 87 | remove.onerror = (e) => reject(e); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/app/state/index.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { create, useStore } from "zustand"; 4 | import { temporal, TemporalState } from "zundo"; 5 | import { ApplicationState, Application } from "../types"; 6 | import { isEqual, omit } from "lodash"; 7 | import { v4 } from "uuid"; 8 | 9 | import ChatActions from "./actions/chat"; 10 | import FunctionsActions from "./actions/functions"; 11 | import OptionsActions from "./actions/options"; 12 | import WorkspaceActions from "./actions/workspace"; 13 | import { current } from "immer"; 14 | 15 | export const initialState: ApplicationState = { 16 | functions: { 17 | functions: "", 18 | }, 19 | chat: { 20 | messages: [ 21 | { 22 | id: v4(), 23 | content: "You are a helpful assistant", 24 | role: "system", 25 | }, 26 | ], 27 | input: "", 28 | completionPending: false, 29 | }, 30 | options: { 31 | llm: { 32 | storeApiKey: false, 33 | apiKey: "", 34 | model: "gpt-3.5-turbo", 35 | temperature: 0.5, 36 | top_p: 1, 37 | frequency_penalty: 0, 38 | presence_penalty: 0, 39 | max_tokens: 1024, 40 | }, 41 | }, 42 | workspace: { 43 | dialog: { 44 | show: false, 45 | title: "", 46 | message: "", 47 | action: () => {}, 48 | cancel: () => {}, 49 | }, 50 | saveModal: { 51 | show: false, 52 | }, 53 | openModal: { 54 | show: false, 55 | }, 56 | panels: { 57 | functions: { visible: true }, 58 | config: { visible: true }, 59 | logs: { visible: false }, 60 | }, 61 | }, 62 | }; 63 | 64 | export const useAppStore = create()( 65 | temporal((set, get) => ({ 66 | ...initialState, 67 | chatActions: ChatActions(set, get), 68 | workspaceActions: WorkspaceActions(set, get), 69 | functionsActions: FunctionsActions(set), 70 | optionsActions: OptionsActions(set, get), 71 | })) 72 | ); 73 | 74 | export const useTemporalStore = ( 75 | selector: (state: TemporalState>) => T, 76 | equality?: (a: T, b: T) => boolean 77 | ) => useStore(useAppStore.temporal, selector, equality); 78 | -------------------------------------------------------------------------------- /src/app/types.tsx: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | id: string; 3 | content: string; 4 | role: "user" | "assistant" | "system" | "function"; 5 | function_call?: any; 6 | name?: string; 7 | } 8 | 9 | export interface ChatState { 10 | messages: ChatMessage[]; 11 | input: string; 12 | completionPending: boolean; 13 | } 14 | 15 | export interface FunctionsState { 16 | functions: string; 17 | } 18 | 19 | export interface WorkspaceState { 20 | dialog: { 21 | show: boolean; 22 | title: string; 23 | message: string; 24 | action: Function; 25 | cancel: Function; 26 | }; 27 | saveModal: { 28 | show: boolean; 29 | }; 30 | openModal: { 31 | show: boolean; 32 | }; 33 | panels: { 34 | functions: { visible: boolean }; 35 | config: { visible: boolean }; 36 | logs: { visible: boolean }; 37 | }; 38 | } 39 | 40 | export interface OptionsState { 41 | llm: { 42 | storeApiKey: boolean; 43 | apiKey: string; 44 | model: string; 45 | temperature: number; 46 | top_p: number; 47 | frequency_penalty: number; 48 | presence_penalty: number; 49 | max_tokens: number; 50 | }; 51 | } 52 | 53 | export interface ApplicationState { 54 | chat: ChatState; 55 | workspace: WorkspaceState; 56 | options: OptionsState; 57 | functions: FunctionsState; 58 | } 59 | 60 | // actions 61 | 62 | export interface ChatActions { 63 | updateInput: (input: string) => void; 64 | sendMessage: (message?: ChatMessage | null) => void; 65 | updateMessage: (id: string, content: string) => void; 66 | rewindToMessage: (id: string) => void; 67 | deleteMessage: (id: string) => void; 68 | submitFunctionCallResponse: (id: string, value: string) => void; 69 | play: () => void; 70 | } 71 | 72 | export interface OptionsActions { 73 | toggleStoreApiKey: () => void; 74 | updateApiKey: (input: string) => void; 75 | updateModel: (input: string) => void; 76 | updateTemperature: (input: number) => void; 77 | updateTopP: (input: number) => void; 78 | updateFrequencyPenalty: (input: number) => void; 79 | updatePresencePenalty: (input: number) => void; 80 | updateMaxTokens: (input: number) => void; 81 | } 82 | 83 | export interface WorkspaceActions { 84 | newWorkspace: () => void; 85 | showSaveModal: () => void; 86 | hideSaveModal: () => void; 87 | showOpenModal: () => void; 88 | hideOpenModal: () => void; 89 | save: (name: string) => void; 90 | overwrite: (id: string) => void; 91 | open: (id: string) => void; 92 | remove: (id: string) => void; 93 | toggleFunctionsPanelVisibility: () => void; 94 | toggleConfigPanelVisibility: () => void; 95 | toggleLogsPanelVisibility: () => void; 96 | exportWorkspace: () => void; 97 | importWorkspace: () => void; 98 | } 99 | 100 | export interface FunctionsActions { 101 | update: (text: string) => void; 102 | addExample: () => void; 103 | } 104 | 105 | export interface ApplicationActions { 106 | chatActions: ChatActions; 107 | optionsActions: OptionsActions; 108 | workspaceActions: WorkspaceActions; 109 | functionsActions: FunctionsActions; 110 | } 111 | 112 | export interface SavedWorkspace { 113 | id: string; 114 | name: string; 115 | state: string; 116 | ts: number; 117 | } 118 | 119 | export type Application = ApplicationState & ApplicationActions; 120 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 12 | "gradient-conic": 13 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 14 | }, 15 | }, 16 | }, 17 | plugins: [require("daisyui")], 18 | }; 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------