├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app └── api │ └── prompts │ └── route.ts ├── components ├── Button.module.css ├── Button.tsx ├── ButtonGroup.module.css ├── ButtonGroup.tsx ├── CreativityIcon.module.css ├── CreativityIcon.tsx ├── Dialog.module.css ├── Dialog.tsx ├── DropdownMenu.module.css ├── DropdownMenu.tsx ├── Instructions.module.css ├── Instructions.tsx ├── PromptLogo.module.css ├── PromptLogo.tsx ├── ScrollArea.module.css ├── ScrollArea.tsx ├── Toast.module.css └── Toast.tsx ├── data └── prompts.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── [[...slug]].tsx ├── _app.tsx └── shared.tsx ├── postcss.config.js ├── public ├── favicon.ico ├── favicon.png └── og-image.png ├── styles ├── Home.module.css └── globals.css ├── tsconfig.json └── utils ├── actions.ts ├── extractPrompts.ts ├── isTouchDevice.ts └── useSectionInViewObserver.tsx /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .pnpm-debug.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 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Raycast 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > Moved to https://github.com/raycast/ray-so 3 | 4 | 5 | 6 |

7 | Prompt Explorer 8 |

9 |

10 | prompts.ray.so 11 |

12 |

13 | Easily browse, share, and add prompts to Raycast 14 |

15 | 16 | 17 | 18 | --- 19 | 20 | ## Contributing 21 | 22 | This is a [Next.js](https://nextjs.org/) project. If you're unfamiliar with it, check out the [Next.js Documentation](https://nextjs.org/docs). 23 | 24 | To get started, download the repo and run the development server: 25 | 26 | ```bash 27 | npm run dev 28 | ``` 29 | 30 | ### Prompts 31 | 32 | We welcome contributions to the prompts data. Here's how you can contribute: 33 | 34 | - Open `./data/prompts.ts` 35 | - Add your prompt to the relevant category 36 | - Ensure it includes all fields, and that they're unique within their category 37 | - Create a pull request 🚀 38 | -------------------------------------------------------------------------------- /app/api/prompts/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { baseCategories } from "../../../data/prompts"; 3 | 4 | export async function GET() { 5 | return NextResponse.json(baseCategories); 6 | } 7 | -------------------------------------------------------------------------------- /components/Button.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | appearance: none; 3 | border: none; 4 | height: 30px; 5 | display: inline-flex; 6 | gap: 8px; 7 | align-items: center; 8 | border-radius: 6px; 9 | padding: 0 12px; 10 | font-weight: 500; 11 | font-size: 14px; 12 | position: relative; 13 | 14 | &:hover { 15 | z-index: 1; 16 | } 17 | 18 | &:focus { 19 | outline: none; 20 | z-index: 1; 21 | } 22 | 23 | &:disabled { 24 | pointer-events: none; 25 | opacity: 0.4; 26 | } 27 | 28 | &[data-variant="gray"] { 29 | background-color: rgba(255, 255, 255, 0.1); 30 | box-shadow: inset 0 0 0 1px #343434; 31 | color: rgba(255, 255, 255, 0.6); 32 | 33 | &:hover { 34 | background-color: rgba(255, 255, 255, 0.15); 35 | box-shadow: inset 0 0 0 1px #343434; 36 | } 37 | 38 | &:focus { 39 | box-shadow: inset 0 0 0 1px #343434, 0 0 0 1px #343434; 40 | } 41 | } 42 | 43 | &[data-variant="red"] { 44 | background-color: rgba(255, 99, 99, 0.15); 45 | box-shadow: inset 0 0 0 1px #4d2a2a; 46 | color: #ff6363; 47 | 48 | &:hover { 49 | background-color: rgba(255, 99, 99, 0.3); 50 | box-shadow: inset 0 0 0 1px #6d2d2d; 51 | } 52 | 53 | &:focus { 54 | box-shadow: inset 0 0 0 1px #6d2d2d, 0 0 0 1px #6d2d2d; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./Button.module.css"; 3 | 4 | type ButtonProps = React.ComponentPropsWithoutRef<"button"> & { 5 | variant?: "gray" | "red"; 6 | }; 7 | 8 | export const Button = React.forwardRef( 9 | ({ children, variant = "gray", ...props }, forwardedRef) => ( 10 | 19 | ) 20 | ); 21 | 22 | Button.displayName = "Button"; 23 | -------------------------------------------------------------------------------- /components/ButtonGroup.module.css: -------------------------------------------------------------------------------- 1 | .buttonGroup { 2 | display: flex; 3 | align-items: center; 4 | 5 | & > *:first-child { 6 | border-top-right-radius: 0; 7 | border-bottom-right-radius: 0; 8 | } 9 | 10 | & > * ~ * { 11 | margin-left: -1px; 12 | border-top-left-radius: 0; 13 | border-bottom-left-radius: 0; 14 | } 15 | } -------------------------------------------------------------------------------- /components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./ButtonGroup.module.css"; 3 | 4 | export function ButtonGroup({ children }: { children: React.ReactNode }) { 5 | return {children}; 6 | } 7 | -------------------------------------------------------------------------------- /components/CreativityIcon.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | all: unset; 3 | background: none; 4 | border: none; 5 | } 6 | 7 | .tooltip { 8 | border-radius: 8px; 9 | padding: 8px 12px; 10 | font-size: 14px; 11 | line-height: 1; 12 | color: white; 13 | background-color: #222; 14 | user-select: none; 15 | animation-duration: 400ms; 16 | animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); 17 | will-change: transform, opacity; 18 | } 19 | .tooltip[data-state="delayed-open"][data-side="top"] { 20 | animation-name: slideDownAndFade; 21 | } 22 | .tooltip[data-state="delayed-open"][data-side="right"] { 23 | animation-name: slideLeftAndFade; 24 | } 25 | .tooltip[data-state="delayed-open"][data-side="bottom"] { 26 | animation-name: slideUpAndFade; 27 | } 28 | .tooltip[data-state="delayed-open"][data-side="left"] { 29 | animation-name: slideRightAndFade; 30 | } 31 | 32 | @keyframes slideUpAndFade { 33 | from { 34 | opacity: 0; 35 | transform: translateY(2px); 36 | } 37 | to { 38 | opacity: 1; 39 | transform: translateY(0); 40 | } 41 | } 42 | 43 | @keyframes slideRightAndFade { 44 | from { 45 | opacity: 0; 46 | transform: translateX(-2px); 47 | } 48 | to { 49 | opacity: 1; 50 | transform: translateX(0); 51 | } 52 | } 53 | 54 | @keyframes slideDownAndFade { 55 | from { 56 | opacity: 0; 57 | transform: translateY(-2px); 58 | } 59 | to { 60 | opacity: 1; 61 | transform: translateY(0); 62 | } 63 | } 64 | 65 | @keyframes slideLeftAndFade { 66 | from { 67 | opacity: 0; 68 | transform: translateX(2px); 69 | } 70 | to { 71 | opacity: 1; 72 | transform: translateX(0); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /components/CreativityIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as Tooltip from "@radix-ui/react-tooltip"; 2 | 3 | import { 4 | CircleDisabledIcon, 5 | StackedBars1Icon, 6 | StackedBars2Icon, 7 | StackedBars3Icon, 8 | StackedBars4Icon, 9 | } from "@raycast/icons"; 10 | import { Prompt } from "../data/prompts"; 11 | 12 | import styles from "./CreativityIcon.module.css"; 13 | 14 | export default function CreativityIcon({ 15 | creativity, 16 | }: { 17 | creativity: Prompt["creativity"]; 18 | }) { 19 | let component = null; 20 | if (creativity === "none") { 21 | component = ; 22 | } 23 | 24 | if (creativity === "low") { 25 | component = ; 26 | } 27 | 28 | if (creativity === "medium") { 29 | component = ; 30 | } 31 | 32 | if (creativity === "high") { 33 | component = ; 34 | } 35 | 36 | if (creativity === "maximum") { 37 | component = ; 38 | } 39 | 40 | const creativityText = { 41 | none: "No creativity", 42 | low: "Low creativity", 43 | medium: "Medium creativity", 44 | high: "High creativity", 45 | maximum: "Maximum creativity", 46 | }; 47 | 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {creativityText[creativity]} 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /components/Dialog.module.css: -------------------------------------------------------------------------------- 1 | .overlay { 2 | background-color: rgba(0, 0, 0, 0.8); 3 | position: fixed; 4 | inset: 0; 5 | animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 6 | } 7 | 8 | .content { 9 | border-radius: 10px; 10 | background-color: rgba(31, 31, 31, 1); 11 | position: fixed; 12 | top: 70px; 13 | left: 20px; 14 | width: 640px; 15 | max-width: calc(100vw - 20px); 16 | max-height: 80vh; 17 | animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1); 18 | 19 | &:focus { 20 | outline: none; 21 | } 22 | 23 | &[data-centered="true"] { 24 | top: 50%; 25 | left: 50%; 26 | width: 90vw; 27 | max-width: 500px; 28 | max-height: 85vh; 29 | transform: translate(-50%, -50%); 30 | animation: contentShowCentered 150ms cubic-bezier(0.16, 1, 0.3, 1); 31 | } 32 | } 33 | 34 | .inner { 35 | padding: 25px; 36 | } 37 | 38 | @keyframes overlayShow { 39 | from { 40 | opacity: 0; 41 | } 42 | to { 43 | opacity: 1; 44 | } 45 | } 46 | 47 | @keyframes contentShow { 48 | from { 49 | opacity: 0; 50 | transform: translateY(-10px) scale(0.96); 51 | } 52 | to { 53 | opacity: 1; 54 | transform: translateY(0px) scale(1); 55 | } 56 | } 57 | 58 | @keyframes contentShowCentered { 59 | from { 60 | opacity: 0; 61 | transform: translate(-50%, -48%) scale(0.96); 62 | } 63 | to { 64 | opacity: 1; 65 | transform: translate(-50%, -50%) scale(1); 66 | } 67 | } 68 | 69 | .close { 70 | all: unset; 71 | position: absolute; 72 | top: 18px; 73 | right: 18px; 74 | color: hsla(0, 0%, 100%, 0.4); 75 | transition: color 150ms ease; 76 | 77 | &:hover { 78 | color: hsla(0, 0%, 100%, 1); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /components/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | 4 | import styles from "./Dialog.module.css"; 5 | import { ScrollArea } from "./ScrollArea"; 6 | import { XMarkCircleIcon } from "@raycast/icons"; 7 | 8 | type DialogPrimitiveContentProps = React.ComponentProps< 9 | typeof DialogPrimitive.Content 10 | > & { showCloseButton?: boolean; centered?: boolean }; 11 | 12 | export const DialogContent = React.forwardRef< 13 | React.ElementRef, 14 | DialogPrimitiveContentProps 15 | >( 16 | ( 17 | { children, showCloseButton, centered, className, ...props }, 18 | forwardedRef 19 | ) => ( 20 | 21 | 22 | 28 | 29 |
30 | {children} 31 | {showCloseButton && ( 32 | 33 | 34 | 35 | )} 36 |
37 |
38 |
39 |
40 | ) 41 | ); 42 | 43 | DialogContent.displayName = "Dialog"; 44 | 45 | export const Dialog = DialogPrimitive.Root; 46 | export const DialogTrigger = DialogPrimitive.Trigger; 47 | export const DialogTitle = DialogPrimitive.Title; 48 | export const DialogDescription = DialogPrimitive.Description; 49 | export const DialogClose = DialogPrimitive.Close; 50 | -------------------------------------------------------------------------------- /components/DropdownMenu.module.css: -------------------------------------------------------------------------------- 1 | .content { 2 | border-radius: 8px; 3 | background-color: #252525; 4 | border: 1px solid hsla(0, 0%, 100%, 0.07); 5 | box-shadow: 0 4px 16px 0 rgb(0 0 0 / 50%); 6 | animation-duration: 400ms; 7 | animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); 8 | will-change: transform, opacity; 9 | 10 | &:focus { 11 | outline: none; 12 | } 13 | 14 | &[data-state="open"][data-side="top"] { 15 | animation-name: slideDownAndFade; 16 | } 17 | &[data-state="open"][data-side="right"] { 18 | animation-name: slideLeftAndFade; 19 | } 20 | &[data-state="open"][data-side="bottom"] { 21 | animation-name: slideUpAndFade; 22 | } 23 | &[data-state="open"][data-side="left"] { 24 | animation-name: slideRightAndFade; 25 | } 26 | } 27 | 28 | .item { 29 | display: flex; 30 | align-items: center; 31 | gap: 8px; 32 | font-size: 13px; 33 | line-height: 40px; 34 | height: 40px; 35 | letter-spacing: 0.1px; 36 | padding: 0 8px 0 16px; 37 | margin: 8px; 38 | border-radius: 6px; 39 | cursor: default; 40 | 41 | &:focus { 42 | outline: none; 43 | background-color: rgba(255, 255, 255, 0.1); 44 | } 45 | 46 | &[data-disabled] { 47 | opacity: 0.5; 48 | cursor: not-allowed; 49 | } 50 | 51 | & > span { 52 | padding-left: 24px; 53 | } 54 | } 55 | 56 | @keyframes slideUpAndFade { 57 | from { 58 | opacity: 0; 59 | transform: translateY(2px); 60 | } 61 | to { 62 | opacity: 1; 63 | transform: translateY(0); 64 | } 65 | } 66 | 67 | @keyframes slideRightAndFade { 68 | from { 69 | opacity: 0; 70 | transform: translateX(-2px); 71 | } 72 | to { 73 | opacity: 1; 74 | transform: translateX(0); 75 | } 76 | } 77 | 78 | @keyframes slideDownAndFade { 79 | from { 80 | opacity: 0; 81 | transform: translateY(-2px); 82 | } 83 | to { 84 | opacity: 1; 85 | transform: translateY(0); 86 | } 87 | } 88 | 89 | @keyframes slideLeftAndFade { 90 | from { 91 | opacity: 0; 92 | transform: translateX(2px); 93 | } 94 | to { 95 | opacity: 1; 96 | transform: translateX(0); 97 | } 98 | } 99 | 100 | .separator { 101 | height: 1px; 102 | background-color: hsla(0, 0%, 100%, 0.07); 103 | } 104 | -------------------------------------------------------------------------------- /components/DropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 3 | 4 | import styles from "./DropdownMenu.module.css"; 5 | 6 | type DropdownMenuPrimitiveRootProps = React.ComponentProps< 7 | typeof DropdownMenuPrimitive.Root 8 | >; 9 | 10 | export const DropdownMenuContent = React.forwardRef< 11 | React.ElementRef, 12 | DropdownMenuPrimitiveRootProps 13 | >(({ children, ...props }, forwardedRef) => { 14 | return ( 15 | 16 | 23 | {children} 24 | 25 | 26 | ); 27 | }); 28 | DropdownMenuContent.displayName = "DropdownMenuContent"; 29 | 30 | type DropdownMenuPrimitiveItemProps = React.ComponentProps< 31 | typeof DropdownMenuPrimitive.Item 32 | >; 33 | 34 | export const DropdownMenuItem = React.forwardRef< 35 | React.ElementRef, 36 | DropdownMenuPrimitiveItemProps 37 | >(({ children, ...props }, forwardedRef) => { 38 | return ( 39 | 44 | {children} 45 | 46 | ); 47 | }); 48 | DropdownMenuItem.displayName = "DropdownMenuItem"; 49 | 50 | export const DropdownMenuSeparator = () => ( 51 | 52 | ); 53 | 54 | export const DropdownMenu = DropdownMenuPrimitive.Root; 55 | export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 56 | -------------------------------------------------------------------------------- /components/Instructions.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 24px 0; 3 | background: rgba(255, 255, 255, 0.05); 4 | border: 1px solid rgba(255, 255, 255, 0.05); 5 | box-shadow: 0px 8px 24px -12px #000000; 6 | border-radius: 6px; 7 | text-align: center; 8 | line-height: 17px; 9 | transform: translateY(50%); 10 | opacity: 0; 11 | animation: showInstructions 500ms 200ms ease-out forwards; 12 | 13 | & button { 14 | width: calc(100% - 24px - 24px); 15 | justify-content: center; 16 | } 17 | } 18 | 19 | @keyframes showInstructions { 20 | to { 21 | opacity: 1; 22 | transform: translateY(0); 23 | } 24 | } 25 | 26 | .title { 27 | font-size: 14px; 28 | font-weight: 500; 29 | margin-bottom: 8px; 30 | } 31 | 32 | .description { 33 | font-size: 14px; 34 | margin-bottom: 24px; 35 | line-height: 18px; 36 | padding: 0 16px; 37 | 38 | color: rgba(255, 255, 255, 0.6); 39 | 40 | & strong { 41 | color: #ff6363; 42 | font-weight: 500; 43 | } 44 | } 45 | 46 | .skeletons { 47 | display: flex; 48 | align-items: center; 49 | justify-content: center; 50 | gap: 4px; 51 | margin-bottom: 24px; 52 | position: relative; 53 | } 54 | 55 | .skeleton { 56 | width: 40px; 57 | height: 48px; 58 | background: rgba(255, 255, 255, 0.05); 59 | border: 1px solid rgba(255, 255, 255, 0.1); 60 | border-radius: 4px; 61 | padding: 10px 0; 62 | display: flex; 63 | flex-direction: column; 64 | gap: 5px; 65 | align-items: center; 66 | 67 | &[data-selected="true"] { 68 | animation: fadeBgToRed 100ms 2850ms ease-out forwards, 69 | fadeBorderToRed 100ms 2850ms ease-out forwards; 70 | } 71 | } 72 | 73 | .skeletonPrimary { 74 | width: 20px; 75 | height: 20px; 76 | background: rgba(255, 255, 255, 0.05); 77 | border-radius: 2px; 78 | 79 | [data-selected="true"] > & { 80 | animation: fadeBgToRed 100ms 2850ms ease-out forwards; 81 | } 82 | } 83 | 84 | .skeletonSecondary { 85 | width: 24px; 86 | height: 3px; 87 | background: rgba(255, 255, 255, 0.05); 88 | border-radius: 2px; 89 | 90 | [data-selected="true"] > & { 91 | animation: fadeBgToRed 100ms 2850ms ease-out forwards; 92 | } 93 | } 94 | 95 | .skeletonCursor { 96 | position: absolute; 97 | bottom: -10px; 98 | left: 0; 99 | opacity: 0; 100 | animation: moveCursor 2000ms 1000ms ease-in-out forwards; 101 | } 102 | 103 | @keyframes fadeBgToRed { 104 | to { 105 | background: rgba(255, 99, 99, 0.1); 106 | } 107 | } 108 | 109 | @keyframes fadeBorderToRed { 110 | to { 111 | border: 1px solid rgba(255, 99, 99, 0.3); 112 | } 113 | } 114 | 115 | @keyframes moveCursor { 116 | 25% { 117 | opacity: 1; 118 | } 119 | 25% { 120 | opacity: 1; 121 | transform: translateX(0); 122 | } 123 | 85% { 124 | opacity: 1; 125 | transform: translateX(85px); 126 | } 127 | 90% { 128 | opacity: 1; 129 | transform: translateX(85px) scale(0.8); 130 | } 131 | 95% { 132 | opacity: 1; 133 | transform: translateX(85px) scale(1); 134 | } 135 | 100% { 136 | opacity: 1; 137 | transform: translateX(85px) scale(1); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /components/Instructions.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./Button"; 2 | import styles from "./Instructions.module.css"; 3 | 4 | export function Instructions() { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |

Install AI Commands

16 |

17 | Select a prompt by clicking on it. Hold{" "} 18 | to select multiple. Click{" "} 19 | Add to Raycast to import them directly. 20 |

21 | 22 | 25 |
26 | ); 27 | } 28 | 29 | function Skeleton({ selected = false }) { 30 | return ( 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | const Cursor = () => ( 39 | 47 | 48 | 52 | 56 | 57 | ); 58 | -------------------------------------------------------------------------------- /components/PromptLogo.module.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | background-image: linear-gradient(135deg, #ff6363, #d72a2a); 3 | display: inline-flex; 4 | width: 24px; 5 | height: 24px; 6 | align-items: center; 7 | justify-content: center; 8 | border-radius: 6px; 9 | position: relative; 10 | 11 | svg { 12 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 13 | } 14 | 15 | [data-icon="raycast"] { 16 | opacity: 0; 17 | transform: scale(0.8) rotate(-45deg); 18 | position: absolute; 19 | } 20 | 21 | &:hover { 22 | [data-icon="stars"] { 23 | opacity: 0; 24 | transform: scale(0.8) rotate(45deg); 25 | } 26 | 27 | [data-icon="raycast"] { 28 | opacity: 1; 29 | transform: scale(1) rotate(0deg); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /components/PromptLogo.tsx: -------------------------------------------------------------------------------- 1 | import { StarsIcon, RaycastLogoNegIcon } from "@raycast/icons"; 2 | import styles from "./PromptLogo.module.css"; 3 | 4 | export function PromptLogo() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /components/ScrollArea.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 100%; 3 | height: 100%; 4 | border-radius: 4px; 5 | overflow: hidden; 6 | --scrollbar-size: 10px; 7 | } 8 | 9 | .viewport { 10 | width: 100%; 11 | height: 100%; 12 | border-radius: inherit; 13 | } 14 | 15 | .scrollbar { 16 | display: flex; 17 | /* ensures no selection */ 18 | user-select: none; 19 | /* disable browser handling of all panning and zooming gestures on touch devices */ 20 | touch-action: none; 21 | padding: 3px; 22 | transition: background 160ms ease-out; 23 | 24 | &[data-orientation="vertical"] { 25 | width: var(--scrollbar-size); 26 | } 27 | &[data-orientation="horizontal"] { 28 | flex-direction: column; 29 | height: var(--scrollbar-size); 30 | } 31 | } 32 | 33 | .thumb { 34 | flex: 1; 35 | background: rgba(255, 255, 255, 0.1); 36 | border-radius: var(--scrollbar-size); 37 | position: relative; 38 | 39 | /* increase target size for touch devices https://www.w3.org/WAI/WCAG21/Understanding/target-size.html */ 40 | &::before { 41 | content: ""; 42 | position: absolute; 43 | top: 50%; 44 | left: 50%; 45 | transform: translate(-50%, -50%); 46 | width: 100%; 47 | height: 100%; 48 | min-width: 44px; 49 | min-height: 44px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components/ScrollArea.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 3 | 4 | import styles from "./ScrollArea.module.css"; 5 | 6 | type ScrollAreaPrimitiveRootProps = React.ComponentProps< 7 | typeof ScrollAreaPrimitive.Root 8 | >; 9 | 10 | export const ScrollArea = React.forwardRef< 11 | React.ElementRef, 12 | ScrollAreaPrimitiveRootProps 13 | >(({ children }, forwardedRef) => ( 14 | 15 | 16 | {children} 17 | 18 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | )); 32 | ScrollArea.displayName = "ScrollArea"; 33 | -------------------------------------------------------------------------------- /components/Toast.module.css: -------------------------------------------------------------------------------- 1 | .viewport { 2 | position: fixed; 3 | inset: 0; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | list-style: none; 8 | z-index: 2147483647; 9 | outline: none; 10 | background-color: rgba(0, 0, 0, 0.5); 11 | transition: background-color 200ms; 12 | 13 | &:empty { 14 | background-color: transparent; 15 | } 16 | } 17 | 18 | .root { 19 | background-color: #3b383b; 20 | border-radius: 20px; 21 | padding: 0 18px; 22 | height: 40px; 23 | display: inline-flex; 24 | align-items: center; 25 | 26 | &[data-state="open"] { 27 | animation: show 150ms ease-in; 28 | } 29 | &[data-state="closed"] { 30 | animation: hide 150ms ease-in; 31 | } 32 | &[data-swipe="move"] { 33 | transform: translateY(var(--radix-toast-swipe-move-y)); 34 | } 35 | &[data-swipe="cancel"] { 36 | transform: translateY(0); 37 | transition: transform 200ms ease-out; 38 | } 39 | &[data-swipe="end"] { 40 | animation: swipeOut 100ms ease-out; 41 | } 42 | } 43 | 44 | @keyframes show { 45 | from { 46 | opacity: 0; 47 | } 48 | to { 49 | opacity: 1; 50 | } 51 | } 52 | @keyframes hide { 53 | from { 54 | opacity: 1; 55 | } 56 | to { 57 | opacity: 0; 58 | } 59 | } 60 | 61 | @keyframes swipeOut { 62 | from { 63 | transform: translateY(var(--radix-toast-swipe-end-y)); 64 | } 65 | to { 66 | transform: translateY(100%); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /components/Toast.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as ToastPrimitive from "@radix-ui/react-toast"; 3 | 4 | import styles from "./Toast.module.css"; 5 | 6 | type ToastPrimitiveRootProps = React.ComponentProps; 7 | 8 | export const Toast = React.forwardRef< 9 | React.ElementRef, 10 | ToastPrimitiveRootProps 11 | >(({ children, ...props }, forwardedRef) => { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }); 18 | Toast.displayName = "Toast"; 19 | 20 | type ToastPrimitiveViewportProps = React.ComponentProps< 21 | typeof ToastPrimitive.Viewport 22 | >; 23 | 24 | export const ToastViewport = React.forwardRef< 25 | React.ElementRef, 26 | ToastPrimitiveViewportProps 27 | >((props, forwardedRef) => { 28 | return ( 29 | 34 | ); 35 | }); 36 | ToastViewport.displayName = "ToastViewport"; 37 | 38 | export const ToastProvider = ToastPrimitive.Provider; 39 | export const ToastTitle = ToastPrimitive.Title; 40 | -------------------------------------------------------------------------------- /data/prompts.ts: -------------------------------------------------------------------------------- 1 | import { IconName, Icons } from "@raycast/icons"; 2 | import { SVGProps } from "react"; 3 | 4 | export type Model = 5 | | "openai-gpt-3.5-turbo-instruct" 6 | | "openai-gpt-3.5-turbo" 7 | | "openai-gpt-4" 8 | | "openai-gpt-4-turbo" 9 | | "anthropic-claude-haiku" 10 | | "anthropic-claude-opus" 11 | | "anthropic-claude-sonnet" 12 | | "perplexity-sonar-medium-online" 13 | | "perplexity-sonar-small-online" 14 | | "llama2-70b" 15 | | "mixtral-8x7b" 16 | | "codellama-70b-instruct"; 17 | 18 | export type Prompt = { 19 | id: string; 20 | title: string; 21 | prompt: string; 22 | icon: IconName; 23 | creativity: "none" | "low" | "medium" | "high" | "maximum"; 24 | model?: Model; 25 | date: `${number}-${number}-${number}`; 26 | author?: { 27 | name: string; 28 | link?: string; 29 | }; 30 | }; 31 | 32 | function generateSelection(selectionWord: string, resultWord: string) { 33 | return `\n\n${selectionWord}: {selection}\n\n${resultWord}:`; 34 | } 35 | 36 | const browser: Prompt[] = [ 37 | { 38 | id: "inspect-webpage", 39 | title: "Inspect Webpage", 40 | prompt: `Describe me the tech stack used based on the following HTML document: 41 | 42 | {browser-tab format="html"} 43 | 44 | Consider every element of a tech stack, from frameworks to APIs through tools (analytics, monitoring, etc.). Include which fonts are used. Don't make any guesses on what’s used if there’s no evidence.`, 45 | creativity: "low", 46 | date: "2024-03-18", 47 | icon: "globe-01", 48 | model: "anthropic-claude-haiku", 49 | }, 50 | { 51 | id: "summarize-youtube-video", 52 | title: "Summarize YouTube Video", 53 | prompt: `Create a summary of a YouTube video using its transcript. You will use the following template: 54 | 55 | """ 56 | ## Summary 57 | {Multiple sentences summarising the YouTube video} 58 | 59 | ## Notes 60 | {Bullet points that summarize the key points or important moments from the video’s transcript with explanations} 61 | 62 | ## Quotes 63 | {Extract the best sentences from the transcript in a list} 64 | """ 65 | 66 | Transcript: {browser-tab}`, 67 | creativity: "low", 68 | model: "anthropic-claude-haiku", 69 | date: "2024-03-18", 70 | icon: "play-filled", 71 | }, 72 | ]; 73 | 74 | const code: Prompt[] = [ 75 | { 76 | id: "json-data", 77 | title: "Natural Language Processing", 78 | prompt: 79 | `Act as a natural language processing software. Analyze the given text and return me only a parsable and minified JSON object.\n 80 | 81 | Here's the JSON Object structure: 82 | { 83 | "key1": /* Some instructions */, 84 | "key2": /* Some instructions */, 85 | } 86 | 87 | Here are the rules you must follow: 88 | - You MUST return a valid, parsable JSON object. 89 | - More rules… 90 | 91 | Here are some examples to help you out: 92 | - Example 1… 93 | - Example 2…` + generateSelection("Text", "JSON Data"), 94 | creativity: "low", 95 | date: "2023-12-22", 96 | icon: "code", 97 | }, 98 | { 99 | id: "css-to-tailwind", 100 | title: "Convert CSS code to Tailwind Classes", 101 | prompt: 102 | "Convert the following code into Tailwind CSS classes and give me the result in a code block. Make sure to remove any browser prefixes. Only give me what I can put into my HTML elements `class` properties." + 103 | generateSelection("Code", "Tailwind CSS classes"), 104 | creativity: "low", 105 | date: "2023-06-06", 106 | icon: "code", 107 | }, 108 | { 109 | id: "linux-terminal", 110 | title: "Linux Terminal", 111 | prompt: 112 | "Act as a linux terminal. Execute the following code and reply with what the terminal should show. Only reply with the terminal output inside one unique code block, and nothing else. Do not write explanations." + 113 | generateSelection("Code", "Terminal"), 114 | creativity: "medium", 115 | date: "2023-06-06", 116 | icon: "code", 117 | }, 118 | { 119 | id: "code-interpreter", 120 | title: "Code Interpreter", 121 | prompt: 122 | "Act as a {argument name=language} interpreter. Execute the {argument name=language} code and reply with the output. Do not provide any explanations." + 123 | generateSelection("Code", "Output"), 124 | creativity: "medium", 125 | date: "2023-06-06", 126 | icon: "code", 127 | }, 128 | { 129 | id: "git-commands", 130 | title: "Git Commands", 131 | prompt: 132 | "Translate the text to Git commands. Only reply one unique code block, and nothing else. Do not write explanations." + 133 | generateSelection("Text", "Git commands"), 134 | creativity: "low", 135 | date: "2023-06-06", 136 | icon: "code", 137 | }, 138 | { 139 | id: "regex-generator", 140 | title: "Regex Generator", 141 | prompt: 142 | "Generate a regular expression that match the specific patterns in the text. Return the regular expression in a format that can be easily copied and pasted into a regex-enabled text editor or programming language. Then, give clear and understandable explanations on what the regex is doing and how it is constructed." + 143 | generateSelection("Text", "Regex"), 144 | creativity: "medium", 145 | date: "2023-06-06", 146 | icon: "code", 147 | }, 148 | { 149 | id: "convert-html-to-markdown", 150 | title: "Convert HTML to Markdown", 151 | prompt: 152 | "Convert the HTML code to Markdown." + 153 | generateSelection("HTML code", "Markdown"), 154 | creativity: "none", 155 | date: "2023-06-06", 156 | icon: "code", 157 | }, 158 | { 159 | id: "add-debug-statements", 160 | title: "Add Debug Statements", 161 | prompt: 162 | "Act as a software engineer debugging its code. Add debug statements to the code. Add as many as necessary to make debugging easier." + 163 | generateSelection("Code", "Debugged code"), 164 | creativity: "medium", 165 | date: "2023-06-06", 166 | icon: "bug", 167 | }, 168 | { 169 | id: "write-tests", 170 | title: "Write Tests", 171 | prompt: 172 | "As a software developer, I am currently working on a project using Jest, TypeScript, and React Testing Library. I would like you to help me generate unit tests for the given code. Analyze the given code and provide a single unit test with the necessary imports, without any additional explanations or comments, unless absolutely necessary. Avoid repeating imports and mocks you've mentioned already.\n\nIf I say 'next,' please give me another test for the same code. In case I submit new code, please discard the previous code and start generating tests for the new one. Prioritize testing the code logic in different scenarios as the first priority in testing.\n\nIf I provide specific instructions or ask you to test a particular part or scenario, please follow those instructions and generate the unit test accordingly. If I send you a Jest error, fix the problem and only return the lines which need to be changed in a readable format. Please format the output as a unique code block." + 173 | generateSelection("Code", "Output"), 174 | creativity: "medium", 175 | date: "2023-06-06", 176 | icon: "bug", 177 | author: { 178 | name: "Alireza Sheikholmolouki", 179 | link: "https://github.com/Alireza29675", 180 | }, 181 | }, 182 | { 183 | id: "write-docstring", 184 | title: "Write Docstring", 185 | prompt: 186 | "Write a docstring for the function. Make sure the documentation is detailed." + 187 | generateSelection("Function", "Docstring"), 188 | creativity: "low", 189 | date: "2023-06-06", 190 | icon: "blank-document", 191 | }, 192 | { 193 | id: "convert-to-crontab", 194 | title: "Convert to Crontab Schedule", 195 | prompt: `Act as a knowledgable unix server admin. Given a cronjob schedule in natural language, respond with the correct crontab format for this exact schedule. Double-check your results, ensure it's valid crontab syntax, and respond with nothing but the crontab format. 196 | 197 | Example Schedule: at 5:30am every tuesday in may 198 | Expected Crontab: 30 5 * 5 2 199 | 200 | Schedule: {argument name="schedule"} 201 | Crontab: `, 202 | creativity: "low", 203 | date: "2024-04-21", 204 | icon: "code", 205 | author: { 206 | name: "Philipp Daun", 207 | link: "https://github.com/daun", 208 | }, 209 | }, 210 | ]; 211 | 212 | const communication: Prompt[] = [ 213 | { 214 | id: "translate-to-language", 215 | title: "Translate to Language", 216 | prompt: 217 | "Translate the text in {argument name=language}." + 218 | generateSelection("Text", "Translation"), 219 | creativity: "low", 220 | date: "2023-06-06", 221 | icon: "speech-bubble", 222 | }, 223 | { 224 | id: "decline-mail", 225 | title: "Decline this Mail", 226 | prompt: 227 | "Write a polite and friendly email to decline the following email. The email should be written in a way that it can be sent to the recipient." + 228 | generateSelection("Email", "Declined email"), 229 | creativity: "low", 230 | date: "2023-06-06", 231 | icon: "envelope", 232 | }, 233 | { 234 | id: "ask-question", 235 | title: "Ask Question", 236 | prompt: 237 | "Rewrite the following text as a concise and friendly message, phrased as a question. This should be written in a way that it can be sent in a chat application like Slack." + 238 | generateSelection("Text", "Question"), 239 | creativity: "low", 240 | date: "2023-06-06", 241 | icon: "question-mark-circle", 242 | }, 243 | { 244 | id: "bluf-message", 245 | title: "BLUF Message", 246 | prompt: 247 | `Rewrite the following text as a bottom line up front (BLUF) message formatted in Markdown. The format of the message should be made of two parts: 248 | 249 | - The first part should be written in bold and convey the message's key information. It can either be a statement or a question. Don't lose any important detail in this part. 250 | - The second part should be put onto a new line. This should give more details and provide some background about the message. 251 | 252 | Make sure the message stays concise and clear so that readers don't lose extra time reading it.` + 253 | generateSelection("Text", "Rewritten text"), 254 | creativity: "low", 255 | date: "2023-06-06", 256 | icon: "speech-bubble-active", 257 | }, 258 | { 259 | id: "summarize-long-email", 260 | title: "Summarize Long Emails", 261 | prompt: 262 | `Help me summarize the key points from the email text into a maximum of 5 bullet points, each preferably one sentence, and at most two sentences. Also, identify any action items requested of me. 263 | 264 | Key points: 265 | 266 | 267 | ... 268 | 269 | Asked from you: 270 | 271 | 272 | 273 | If there are no action items, the "Asked from you" section will be left empty.` + 274 | generateSelection("Email", "Output"), 275 | creativity: "low", 276 | date: "2023-06-06", 277 | icon: "envelope", 278 | author: { 279 | name: "Alireza Sheikholmolouki", 280 | link: "https://github.com/Alireza29675", 281 | }, 282 | }, 283 | { 284 | id: "debate-controversial-topic", 285 | title: "Debate a Topic", 286 | prompt: 287 | "Take a stance on the topic and {argument default=for} it. Construct a convincing argument and provide evidence to support your stance." + 288 | generateSelection("Topic", "Argument"), 289 | creativity: "high", 290 | date: "2023-06-06", 291 | icon: "speech-bubble-important", 292 | }, 293 | { 294 | id: "create-calendar-event", 295 | title: "Create a Calendar Event", 296 | prompt: 297 | "Create a calendar event in ICS format based on the information. Include the start time, the end time, the location, all attendees, and a summary. If no end time is provided, assume the event will be one hour long. Add a reminder 1 hour before the event starts and 1 day before the event starts. Don't include the PRODID property. Only output the code block. Don't add any comments." + 298 | generateSelection("Information", "ICS"), 299 | creativity: "medium", 300 | date: "2023-06-06", 301 | icon: "calendar", 302 | author: { 303 | name: "Roel Van Gils", 304 | link: "https://github.com/roelvangils", 305 | }, 306 | }, 307 | { 308 | id: "break-up-wall-of-text", 309 | title: "Break Up Wall of Text", 310 | prompt: `Take the wall of text below and write a cleaned up version inserting naturally appropriate paragraph breaks. It's important that the text does not change, only the whitespace. 311 | 312 | Wall of text: 313 | {selection} 314 | 315 | Cleaned up version: 316 | `, 317 | creativity: "none", 318 | date: "2023-11-06", 319 | icon: "paragraph", 320 | }, 321 | { 322 | id: "summarize-and-sympathize", 323 | title: "Summarize and sympathize text", 324 | prompt: 325 | "Please summarize and omit the following. Then express your empathy." + 326 | generateSelection("Text", "Sympathy"), 327 | creativity: "low", 328 | date: "2023-06-12", 329 | icon: "speech-bubble", 330 | author: { 331 | name: "nagauta", 332 | link: "https://github.com/nagauta", 333 | }, 334 | }, 335 | { 336 | id: "fill-the-gap", 337 | title: "Fill in the gap", 338 | prompt: 339 | `Use the following instructions to rewrite the text 340 | 341 | Give me 5 words that most accurarely fill in the blank in a sentence. 342 | 343 | The blank is represented by a few underscores, such as ___, or ______. 344 | 345 | So for example: "I'm super ___ to announce my new product". 346 | 347 | 1. I'm super happy to announce my new product 348 | 2. I'm super excited to announce my new product 349 | 3. I'm super pumped to announce my new product 350 | 4. I'm super proud to announce my new product 351 | 5. I'm super nervous to announce my new product 352 | 353 | Now do the same for this sentece:` + 354 | generateSelection("Text", "Rewritten text"), 355 | creativity: "high", 356 | date: "2023-08-03", 357 | icon: "speech-bubble", 358 | author: { 359 | name: "peduarte", 360 | link: "https://github.com/peduarte", 361 | }, 362 | }, 363 | ]; 364 | 365 | const image: Prompt[] = [ 366 | { 367 | id: "youtube-script", 368 | title: "Create a YouTube Script", 369 | prompt: 370 | "Create a compelling and captivating YouTube script based on the text. Make sure to include B-Rolls in the script. Make the script as long as necessary to make a video of {argument name=minutes default=10} minutes." + 371 | generateSelection("Text", "Script"), 372 | creativity: "high", 373 | date: "2023-06-06", 374 | icon: "image", 375 | }, 376 | { 377 | id: "midjourney-prompt-generator", 378 | title: "Midjourney Prompt Generator", 379 | prompt: 380 | `Based on the text, generate an "imagine prompt" that contains a maximum word count of 1,500 words that will be used as input for an AI-based text to image program called MidJourney based on the following parameters: /imagine prompt: [1], [2], [3], [4], [5], [6] 381 | 382 | In this prompt, [1] should be replaced with a random subject and [2] should be a short concise description about that subject. Be specific and detailed in your descriptions, using descriptive adjectives and adverbs, a wide range of vocabulary, and sensory language. Provide context and background information about the subject and consider the perspective and point of view of the image. Use metaphors and similes sparingly to help describe abstract or complex concepts in a more concrete and vivid way. Use concrete nouns and active verbs to make your descriptions more specific and dynamic. 383 | 384 | [3] should be a short concise description about the environment of the scene. Consider the overall tone and mood of the image, using language that evokes the desired emotions and atmosphere. Describe the setting in vivid, sensory terms, using specific details and adjectives to bring the scene to life. 385 | 386 | [4] should be a short concise description about the mood of the scene. Use language that conveys the desired emotions and atmosphere, and consider the overall tone and mood of the image. 387 | 388 | [5] should be a short concise description about the atmosphere of the scene. Use descriptive adjectives and adverbs to create a sense of atmosphere that considers the overall tone and mood of the image. 389 | 390 | [6] should be a short concise description of the lighting effect including Types of Lights, Types of Displays, Lighting Styles and Techniques, Global Illumination and Shadows. Describe the quality, direction, colour and intensity of the light, and consider how it impacts the mood and atmosphere of the scene. Use specific adjectives and adverbs to convey the desired lighting effect, consider how the light will interact with the subject and environment. 391 | 392 | It's important to note that the descriptions in the prompt should be written back to back, separated with commas and spaces, and should not include any line breaks or colons. Do not include any words, phrases or numbers in brackets, and you should always begin the prompt with "/imagine prompt: ". 393 | 394 | Be consistent in your use of grammar and avoid using cliches or unnecessary words. Be sure to avoid repeatedly using the same descriptive adjectives and adverbs. Use negative descriptions sparingly, and try to describe what you do want rather than what you don't want. Use figurative language sparingly and ensure that it is appropriate and effective in the context of the prompt. Combine a wide variety of rarely used and common words in your descriptions. 395 | 396 | The "imagine prompt" should strictly contain under 1,500 words. Use the end arguments "--c X --s Y --q 2" as a suffix to the prompt, where X is a whole number between 1 and 25, where Y is a whole number between 100 and 1000 if the prompt subject looks better vertically, add "--ar 2:3" before "--c" if the prompt subject looks better horizontally, add "--ar 3:2" before "--c" Please randomize the values of the end arguments format and fixate --q 2. Please do not use double quotation marks or punctuation marks. Please use randomized end suffix format.` + 397 | generateSelection("Text", "Midjourney Prompt"), 398 | creativity: "high", 399 | date: "2023-06-06", 400 | icon: "image", 401 | }, 402 | { 403 | id: "generate-icons", 404 | title: "Generate Icons", 405 | prompt: 406 | "Generate base64 data URIs of 100x100 SVG icons representing the text. Do not provide any commentary other than the list of data URIs as markdown images. For each icon, explain how it relates to the text." + 407 | generateSelection("Text", "Icons"), 408 | creativity: "maximum", 409 | date: "2023-06-06", 410 | icon: "image", 411 | author: { 412 | name: "Stephen Kaplan", 413 | link: "https://github.com/SKaplanOfficial", 414 | }, 415 | }, 416 | ]; 417 | 418 | const writing: Prompt[] = [ 419 | { 420 | id: "write-story", 421 | title: "Write a Story", 422 | prompt: 423 | "Write a story based on the text. Make the story engaging. The story shouldn't be more than {argument name=number default=500} words." + 424 | generateSelection("Text", "Story"), 425 | creativity: "high", 426 | date: "2023-06-06", 427 | icon: "pencil", 428 | }, 429 | { 430 | id: "blog-post", 431 | title: "Write a Blog Post", 432 | prompt: 433 | "Write a blog post on the topic. Don't use more than {argument name=number default=1000} words" + 434 | generateSelection("Topic", "Blog post"), 435 | creativity: "high", 436 | date: "2023-06-06", 437 | icon: "pencil", 438 | }, 439 | { 440 | id: "twitter-thread", 441 | title: "Twitter Thread", 442 | prompt: 443 | "Convert the text into a list of tweets (= Twitter thread). The first tweet should be clear and engaging. Each tweet should flow smoothly into the next, building anticipation and momentum. The last tweet should be impactful so that the user can reflect on the whole thread. Make sure each tweet doesn't exceed 280 characters. Don't add a single hashtag to any of the tweets." + 444 | generateSelection("Text", "Tweets"), 445 | creativity: "medium", 446 | date: "2023-06-06", 447 | icon: "bird", 448 | }, 449 | ]; 450 | 451 | const music: Prompt[] = [ 452 | { 453 | id: "write-a-song", 454 | title: "Write a Song", 455 | prompt: 456 | "Write a song based on the given text. The song should have a clear melody, lyrics that tell a story, and a memorable chorus. The mood of the song should be {argument name=mood}." + 457 | generateSelection("Text", "Song"), 458 | creativity: "high", 459 | date: "2023-06-06", 460 | icon: "music", 461 | }, 462 | { 463 | id: "playlist-maker", 464 | title: "Playlist Maker", 465 | prompt: 466 | "Act as a song recommender. Based on the given song, create a playlist of 10 similar songs. Provide a name and description for the playlist. Do not choose songs that are the same name or artist. Do not include the original song in the playlist." + 467 | generateSelection("Song", "Playlist"), 468 | creativity: "high", 469 | date: "2023-06-06", 470 | icon: "music", 471 | }, 472 | ]; 473 | 474 | const ideas: Prompt[] = [ 475 | { 476 | id: "write-alternatives", 477 | title: "Write 10 Alternatives", 478 | prompt: 479 | "Give me 10 alternative versions of the text. Ensure that the alternatives are all distinct from one another." + 480 | generateSelection("Text", "Alternatives"), 481 | creativity: "high", 482 | date: "2023-06-06", 483 | icon: "shuffle", 484 | }, 485 | { 486 | id: "project-ideas", 487 | title: "Project Ideas", 488 | prompt: 489 | "Brainstorm 5 project ideas based on the text. Make sure the ideas are distinct from one another." + 490 | generateSelection("Text", "Ideas"), 491 | creativity: "high", 492 | date: "2023-06-06", 493 | icon: "shuffle", 494 | author: { 495 | name: "Stephen Kaplan", 496 | link: "https://github.com/SKaplanOfficial", 497 | }, 498 | }, 499 | { 500 | id: "create-analogies", 501 | title: "Create Analogies", 502 | prompt: 503 | "Develop {argument name=number default=3} creative analogies or metaphors that help explain the main idea of the text." + 504 | generateSelection("Text", "Analogies"), 505 | creativity: "high", 506 | date: "2023-06-06", 507 | icon: "light-bulb", 508 | }, 509 | ]; 510 | 511 | const fun: Prompt[] = [ 512 | { 513 | id: "act-as-a-character", 514 | title: "Act As a Character", 515 | prompt: 516 | "Rewrite the text as if you were {argument name=character default=yoda}. Use {argument name=character default=yoda}'s tone, manner and vocabulary. You must know all of the knowledge of {argument name=character default=yoda}." + 517 | generateSelection("Text", "Rewritten text"), 518 | creativity: "medium", 519 | date: "2023-06-06", 520 | icon: "person", 521 | }, 522 | { 523 | id: "drunkgpt", 524 | title: "DrunkGPT", 525 | prompt: 526 | "Rewrite the text as if you were drunk." + 527 | generateSelection("Text", "Rewritten text"), 528 | creativity: "medium", 529 | date: "2023-06-06", 530 | icon: "game-controller", 531 | }, 532 | ]; 533 | 534 | const misc: Prompt[] = [ 535 | { 536 | id: "tldr", 537 | title: "TL;DR", 538 | prompt: 539 | "Extract all facts from the text and summarize it in all relevant aspects in up to seven bullet points and a 1-liner summary. Pick a good matching emoji for every bullet point." + 540 | generateSelection("Text", "Summary"), 541 | creativity: "low", 542 | date: "2023-06-06", 543 | icon: "bullet-points", 544 | }, 545 | { 546 | id: "title-case", 547 | title: "Title Case", 548 | prompt: "Convert {selection} to title case.", 549 | creativity: "low", 550 | date: "2023-06-06", 551 | icon: "text", 552 | }, 553 | { 554 | id: "emoji-suggestion", 555 | title: "Emoji Suggestion", 556 | prompt: 557 | "Suggest emojis that relate to the text. Suggest around 10 emojis and order them by relevance. Don't add any duplicates. Only respond with emojis." + 558 | generateSelection("Text", "Emojis"), 559 | creativity: "medium", 560 | date: "2023-06-06", 561 | icon: "emoji", 562 | }, 563 | { 564 | id: "find-synonyms", 565 | title: "Find Synonyms", 566 | prompt: 567 | "Find synonyms for the word {selection} and format the output as a list. Words should exist. Do not write any explanations. Do not include the original word in the list. The list should not have any duplicates.", 568 | creativity: "medium", 569 | date: "2023-06-06", 570 | icon: "text", 571 | }, 572 | { 573 | id: "create-recipe", 574 | title: "Give Me a Recipe", 575 | prompt: 576 | "Give me a recipe based on the ingredients. The recipe should be easy to follow." + 577 | generateSelection("Ingredients", "Recipe"), 578 | creativity: "medium", 579 | date: "2023-06-06", 580 | icon: "bullet-points", 581 | }, 582 | { 583 | id: "create-action-items", 584 | title: "Create Action Items", 585 | prompt: 586 | "Generate a markdown list of action items to complete based on the text, using a unique identifier for each item as bold headings. If there are any errors in the text, make action items to fix them. In a sublist of each item, provide a description, priority, estimated level of difficulty, and a reasonable duration for the task." + 587 | generateSelection("Text", "Action items"), 588 | creativity: "medium", 589 | date: "2023-06-06", 590 | icon: "check-circle", 591 | author: { 592 | name: "Stephen Kaplan", 593 | link: "https://github.com/SKaplanOfficial", 594 | }, 595 | }, 596 | { 597 | id: "create-task-list", 598 | title: "Create Task List", 599 | prompt: 600 | "List detailed action steps in markdown format based on the provided text. Ensure the tasks can be efficiently completed." + 601 | generateSelection("Text", "Tasks"), 602 | creativity: "medium", 603 | date: "2024-05-04", 604 | icon: "flag", 605 | author: { 606 | name: "Abner Shang", 607 | link: "https://www.linkedin.com/in/abnershang/", 608 | }, 609 | }, 610 | { 611 | id: "extract-email-addresses", 612 | title: "Extract Email Addresses", 613 | prompt: 614 | "Extract all email addresses in the text and list them using markdown. Include anything that might be an email address. If possible, provide the name of the person or company to which the email address belongs." + 615 | generateSelection("Text", "Email addresses"), 616 | creativity: "low", 617 | date: "2023-06-06", 618 | icon: "envelope", 619 | author: { 620 | name: "Stephen Kaplan", 621 | link: "https://github.com/SKaplanOfficial", 622 | }, 623 | }, 624 | { 625 | id: "extract-phone-numbers", 626 | title: "Extract Phone Numbers", 627 | prompt: 628 | "Identify all phone numbers in the text and list them using markdown. Include anything that might be a phone number. If possible, provide the name of the person or company to which the phone number belongs." + 629 | generateSelection("Text", "Phone numbers"), 630 | creativity: "low", 631 | date: "2023-06-06", 632 | icon: "phone", 633 | author: { 634 | name: "Stephen Kaplan", 635 | link: "https://github.com/SKaplanOfficial", 636 | }, 637 | }, 638 | { 639 | id: "extract-links", 640 | title: "Extract Links", 641 | prompt: 642 | "Extract links in the text. Do not provide any commentary other than the list of Markdown links." + 643 | generateSelection("Text", "Links"), 644 | creativity: "low", 645 | date: "2023-06-06", 646 | icon: "link", 647 | author: { 648 | name: "Stephen Kaplan", 649 | link: "https://github.com/SKaplanOfficial", 650 | }, 651 | }, 652 | { 653 | id: "pros-and-cons", 654 | title: "Pros & Cons", 655 | prompt: 656 | "List pros and cons for the text based on the topics mentioned. Format the response as a markdown list of pros and cons. Do not provide any other commentary." + 657 | generateSelection("Text", "Pros & Cons"), 658 | creativity: "low", 659 | date: "2023-06-06", 660 | icon: "bullet-points", 661 | author: { 662 | name: "Stephen Kaplan", 663 | link: "https://github.com/SKaplanOfficial", 664 | }, 665 | }, 666 | { 667 | id: "eli", 668 | title: "Explain Like I'm a…", 669 | prompt: 670 | `Explain the text like I’m a {argument name=identity default="5 year old"}` + 671 | generateSelection("Text", "Explanation"), 672 | creativity: "low", 673 | date: "2023-06-06", 674 | icon: "book", 675 | }, 676 | { 677 | id: "text-analysis", 678 | title: "Text Analysis", 679 | prompt: 680 | "Analyze the text and provide insights on its tone, style, and potential audience." + 681 | generateSelection("Text", "Analysis"), 682 | creativity: "medium", 683 | date: "2023-06-06", 684 | icon: "magnifying-glass", 685 | }, 686 | { 687 | id: "summarize-product-reviews", 688 | title: "Summarize Product Reviews", 689 | prompt: 690 | `Carefully read the product reviews below. Translate them to English and create a summary of all the reviews in English and list them as Pros and Cons in the bullet point format. Remember that each bullet point should be one sentence or at max two short sentences. Most frequently mentioned should come first in each list and every bullet point should have a percentage showing how much evidence the reviews have brought for that pro or con. For example if reviews are mentioning that product is going bad easily and they brought some reasons for what they are saying, the percentage of your confidence should go higher, but if there are some reviews which are unsure about something or there are no evidence or it's not repeated frequently then the percentage should go lower. At the end you should write a paragraph about what I should pay attention to, before buying this product. These can be some warnings or some tips or some suggestions, points that I will miss, or anything that you think is important to know before buying this product. 691 | 692 | You can use the following template to create the summary: 693 | 694 | ''' 695 | ## Summary of the reviews 696 | 697 | **✅ Pros:** 698 | - Pro 1 - percentage of your confidence% 699 | - Pro 2 - percentage of your confidence% 700 | ... 701 | - Pro n - percentage of your confidence% 702 | 703 | **❌ Cons:** 704 | - Con 1 - percentage of your confidence% 705 | - Con 2 - percentage of your confidence% 706 | ... 707 | - Con n - percentage of your confidence% 708 | 709 | **💡 You should pay attention to:** 710 | - Tip 1 711 | - Tip 2 712 | ... 713 | - Tip n 714 | '''` + generateSelection("Product reviews", "Summary"), 715 | creativity: "low", 716 | date: "2023-06-16", 717 | icon: "tag", 718 | author: { 719 | name: "Alireza Sheikholmolouki", 720 | link: "https://github.com/Alireza29675", 721 | }, 722 | }, 723 | ]; 724 | 725 | const raycast: Prompt[] = [ 726 | { 727 | id: "improve-writing-custom", 728 | title: "Improve Writing - Editable", 729 | prompt: 730 | `Act as a spelling corrector, content writer, and text improver/editor. Reply to each message only with the rewritten text 731 | Stricly follow these rules: 732 | - Correct spelling, grammar, and punctuation errors in the given text 733 | - Enhance clarity and conciseness without altering the original meaning 734 | - Divide lengthy sentences into shorter, more readable ones 735 | - Eliminate unnecessary repetition while preserving important points 736 | - Prioritize active voice over passive voice for a more engaging tone 737 | - Opt for simpler, more accessible vocabulary when possible 738 | - ALWAYS ensure the original meaning and intention of the given text 739 | - ALWAYS detect and maintain the original language of the text 740 | - ALWAYS maintain the existing tone of voice and style, e.g. formal, casual, polite, etc. 741 | - NEVER surround the improved text with quotes or any additional formatting 742 | - If the text is already well-written and requires no improvement, don't change the given text` + 743 | generateSelection("Text", "Improved Text"), 744 | creativity: "low", 745 | date: "2024-04-23", 746 | icon: "raycast-logo-neg", 747 | model: "anthropic-claude-haiku", 748 | }, 749 | { 750 | id: "fix-spelling-and-grammar-custom", 751 | title: "Fix Spelling and Grammar - Editable", 752 | prompt: 753 | `Act as a spelling corrector and improver. Reply to each message only with the rewritten text 754 | 755 | Strictly follow these rules: 756 | - Correct spelling, grammar and punctuation 757 | - ALWAYS detect and maintain the original language of the text 758 | - NEVER surround the rewritten text with quotes 759 | - Don't replace urls with markdown links 760 | - Don't change emojis` + generateSelection("Text", "Fixed Text"), 761 | creativity: "low", 762 | date: "2024-04-23", 763 | icon: "raycast-logo-neg", 764 | model: "openai-gpt-3.5-turbo", 765 | }, 766 | { 767 | id: "explain-this-in-simple-terms-custom", 768 | title: "Explain This in Simple Terms - Editable", 769 | prompt: 770 | `Act as a dictionary and encyclopedia, providing clear and concise explanations for given words or concepts. 771 | 772 | Strictly follow these rules: 773 | - Explain the text in a simple and concise language 774 | - For a single word, provide a brief, easy-to-understand definition 775 | - For a concept or phrase, give a concise explanation that breaks down the main ideas into simple terms 776 | - Use examples or analogies to clarify complex topics when necessary 777 | - Only reply with the explanation or definition 778 | 779 | Some examples: 780 | Text: Philosophy 781 | Explanation: Philosophy is the study of the fundamental nature of knowledge, reality, and existence. It is a system of ideas that attempts to explain the world and our place in it. Philosophers use logic and reason to explore the meaning of life and the universe.` + 782 | generateSelection("Text", "Explanation"), 783 | creativity: "low", 784 | date: "2024-04-23", 785 | icon: "raycast-logo-neg", 786 | model: "openai-gpt-3.5-turbo", 787 | }, 788 | { 789 | id: "make-longer-custom", 790 | title: "Make Longer - Editable", 791 | prompt: 792 | `Act as a professional content writer tasked with expanding a client's text while maintaining its essence and style. Reply to each message only with the rewritten text 793 | 794 | Stictly follow these rules: 795 | - ALWAYS preserve the original tone, voice, and language of the text 796 | - Identify and expand the most critical information and key points 797 | - Avoid repetition 798 | - Stay factual close to the provided text 799 | - Keep URLs in their original format without replacing them with markdown links 800 | - Only reply with the expanded text` + 801 | generateSelection("Text", "Expanded text"), 802 | creativity: "high", 803 | date: "2024-04-23", 804 | icon: "raycast-logo-neg", 805 | model: "openai-gpt-3.5-turbo", 806 | }, 807 | { 808 | id: "make-shorter-custom", 809 | title: "Make Shorter - Editable", 810 | prompt: 811 | `Act as a professional content writer tasked with shortening a client's text while maintaining its essence and style. Reply to each message only with the rewritten text 812 | 813 | Strictly follow these rules: 814 | - ALWAYS preserve the original tone, voice, and language of the text 815 | - Identify and retain the most critical information and key points 816 | - Eliminate redundancies and repetitive phrases or sentences 817 | - Keep URLs in their original format without replacing them with markdown links 818 | - Ensure the shortened text flows smoothly and maintains coherence 819 | - Aim to reduce the word count as much as possible without compromising the core meaning and style 820 | - Only reply with the shortend text` + 821 | generateSelection("Text", "Shortened text"), 822 | creativity: "low", 823 | date: "2024-04-23", 824 | icon: "raycast-logo-neg", 825 | model: "anthropic-claude-haiku", 826 | }, 827 | { 828 | id: "change-tone-to-professional", 829 | title: "Change Tone to Professional - Editable", 830 | prompt: 831 | `Act as a professional content writer and editor. Reply to each message only with the rewritten text 832 | 833 | Strictly follow these rules: 834 | - Professional tone of voice 835 | - Formal language 836 | - Accurate facts 837 | - Correct spelling, grammar, and punctuation 838 | - Concise phrasing 839 | - meaning unchanged 840 | - Length retained 841 | - Don't replace urls with markdown links 842 | - ALWAYS detect and maintain the original language of the text` + 843 | generateSelection("Text", "Rewritten text"), 844 | creativity: "low", 845 | date: "2024-04-23", 846 | icon: "raycast-logo-neg", 847 | model: "openai-gpt-3.5-turbo", 848 | }, 849 | { 850 | id: "change-tone-to-friendly", 851 | title: "Change Tone to Friendly - Editable", 852 | prompt: 853 | `Act as a content writer and editor. Reply to each message only with the rewritten text 854 | 855 | Strictly follow these rules: 856 | - Friendly and optimistic tone of voice 857 | - Correct spelling, grammar, and punctuation 858 | - Meaning unchanged 859 | - Length retained 860 | - Don't replace urls with markdown links 861 | - ALWAYS detect and maintain the original language of the text` + 862 | generateSelection("Text", "Rewritten text"), 863 | creativity: "low", 864 | date: "2024-04-23", 865 | icon: "raycast-logo-neg", 866 | model: "openai-gpt-3.5-turbo", 867 | }, 868 | { 869 | id: "change-tone-to-confident-custom", 870 | title: "Change Tone to Confident - Editable", 871 | prompt: 872 | `Act as a content writer and editor. Reply to each message only with the rewritten text 873 | 874 | Strictly follow these rules: 875 | - Use confident, formal and friendly tone of voice 876 | - Avoid hedging, be definite where possible 877 | - Skip apologies 878 | - Focus on main arguments 879 | - Correct spelling, grammar, and punctuation 880 | - Keep meaning unchanged 881 | - Keep length retained 882 | - Don't replace urls with markdown links 883 | - ALWAYS detect and maintain the original language of the text` + 884 | generateSelection("Text", "Rewritten text"), 885 | creativity: "low", 886 | date: "2024-04-23", 887 | icon: "raycast-logo-neg", 888 | model: "openai-gpt-3.5-turbo", 889 | }, 890 | { 891 | id: "change-tone-to-casual-custom", 892 | title: "Change Tone to Casual - Editable", 893 | prompt: 894 | `Act as a content writer and editor. Reply to each message only with the rewritten text 895 | 896 | Strictly follow these rules: 897 | - Use casual and friendly tone of voice 898 | - Use active voice 899 | - Keep sentences shorts 900 | - Ok to use slang and contractions 901 | - Keep grammatical person 902 | - Correct spelling, grammar, and punctuation 903 | - Keep meaning unchanged 904 | - Keep length retained 905 | - Don't replace urls with markdown links 906 | - ALWAYS detect and maintain the original language of the text` + 907 | generateSelection("Text", "Rewritten text"), 908 | creativity: "low", 909 | date: "2024-04-23", 910 | icon: "raycast-logo-neg", 911 | model: "openai-gpt-3.5-turbo", 912 | }, 913 | { 914 | id: "rephrase-as-tweet-custom", 915 | title: "Rephrase as Tweet - Editable", 916 | prompt: 917 | `You're an expert in the field and have the perfect opportunity to share your ideas and insights with a huge audience!. Rewrite the text as a tweet that is: 918 | - Casual and upbeat 919 | - Creative and catchy 920 | - Focused on key takeaways that challenge the status quo 921 | - Engaging and punchy 922 | - Don't replace urls with markdown links 923 | - IMPORTANT: less than 25 words. 924 | - IMPORTANT: doesn't include hash, hashtags and words starting with #, i.e. #innovation #Technology 925 | - ALWAYS detect and maintain the original language of the text 926 | 927 | Text: 928 | The concept of Rayday is simple. Every Friday, everyone can use the day to work on something that benefits Raycast. From new features, to fixing bugs, drafting documentation or tidying up, it’s time for us to take a break from project work. As well as getting creative with our own ideas, it’s a great chance to act on feedback from our users and community too. 929 | 930 | Tweet: 931 | ⚒️ We hack every Friday – we call it 'Rayday'. Everyone can use the day to work on something that benefits Raycast – aside from normal project work.` + 932 | generateSelection("Text", "Tweet"), 933 | creativity: "high", 934 | date: "2024-04-23", 935 | icon: "raycast-logo-neg", 936 | model: "openai-gpt-3.5-turbo", 937 | }, 938 | { 939 | id: "explain-code-custom", 940 | title: "Explain Code Step by Step - Editable", 941 | prompt: 942 | `Act as a software engineer with deep understanding of any programming language and it's documentation. Explain how the code works step by step in a list. Be concise with a casual tone of voice and write it as documentation for others. 943 | 944 | Code: 945 | \`\`\` 946 | function GoToPreviousPageAction() { 947 | const [page, setPage] = useGlobalState("page"); 948 | return ( 949 | setPage(Math.max(page - 1, 0))} 954 | /> 955 | ); 956 | } 957 | \`\`\` 958 | 959 | Explanation: 960 | The code is a React component that goes to the previous page. 961 | 1. The component renders an 'Action' component with an icon, title, and shortcut. 962 | 3. The 'useGlobalState' hook is used to get the current page number from the global state. 963 | 4. The 'onAction' prop is used to set the page number to one less than the current page number. 964 | 5. This will cause the page to go back one page when the action is taken. 965 | 6. The page is capped at 0 so that the page is never negative.` + 966 | generateSelection("Code", "Explanation"), 967 | creativity: "medium", 968 | date: "2024-04-23", 969 | icon: "raycast-logo-neg", 970 | model: "openai-gpt-3.5-turbo", 971 | }, 972 | { 973 | id: "find-bugs-custom", 974 | title: "Find Bugs in Code - Editable", 975 | prompt: 976 | `Act as a software engineer with deep understanding of any programming language. Review the code to fix logical bugs in the code. Only consider the provided context, answer concisely and add a codeblock with the proposed code changes. If you can't confidently find bugs, answer with "Nothing found - LGTM 👍".. 977 | 978 | Code: 979 | \`\`\` 980 | function PrevAction() { 981 | const [page, setPage] = useGlobalState("page"); 982 | return ( 983 | setPage(page - 1)} 986 | /> 987 | ); 988 | } 989 | \`\`\` 990 | 991 | Review: 992 | The code is missing a check to make sure \`page\` is greater than 0 before subtracting 1. Otherwise, the page could be set to -1 which might cause unexpected behavior. 993 | \`\`\` 994 | function PrevAction() { 995 | const [page, setPage] = useGlobalState("page"); 996 | return ( 997 | setPage(Math.max(page - 1, 0))} 1000 | /> 1001 | ); 1002 | } 1003 | \`\`\` 1004 | 1005 | Code: 1006 | \`\`\` 1007 | private func submit(_ text: String) { 1008 | guard !text.isEmpty else { return } 1009 | let prompt = OpenAIPrompt(prompt: text, imitateChatGPT: true) 1010 | submit(prompt) 1011 | } 1012 | \`\`\` 1013 | 1014 | Review: 1015 | Nothing found - LGTM 👌` + generateSelection("Code", "Review"), 1016 | creativity: "medium", 1017 | date: "2024-04-23", 1018 | icon: "raycast-logo-neg", 1019 | model: "openai-gpt-3.5-turbo", 1020 | }, 1021 | { 1022 | id: "summarize-webpage-custom", 1023 | title: "Summarize Webpage - Editable", 1024 | prompt: `Summarize the provided webpage with the following format: 1025 | """ 1026 | ## 1027 | 1028 | 1029 | 1030 | ### Key Takeaways 1031 | 1032 | - 1033 | """ 1034 | 1035 | Some rules to follow precisely: 1036 | - ALWAYS capture the tone, perspective and POV of the author 1037 | - NEVER come up with additional information 1038 | 1039 | Here's the webpage information: 1040 | {browser-tab}`, 1041 | creativity: "low", 1042 | date: "2024-03-21", 1043 | icon: "raycast-logo-neg", 1044 | model: "anthropic-claude-haiku", 1045 | }, 1046 | ]; 1047 | 1048 | type IconComponent = (props: SVGProps) => JSX.Element; 1049 | 1050 | export type Category = { 1051 | name: string; 1052 | slug: string; 1053 | prompts: (Prompt & { iconComponent: IconComponent })[]; 1054 | icon: IconName; 1055 | iconComponent: IconComponent; 1056 | }; 1057 | 1058 | export const baseCategories: Category[] = [ 1059 | { 1060 | name: "Code", 1061 | slug: "/code", 1062 | prompts: [...code], 1063 | icon: "code" as const, 1064 | }, 1065 | { 1066 | name: "Browser", 1067 | slug: "/browser", 1068 | prompts: [...browser], 1069 | icon: "globe-01" as const, 1070 | }, 1071 | { 1072 | name: "Communication", 1073 | slug: "/communication", 1074 | prompts: [...communication], 1075 | icon: "envelope" as const, 1076 | }, 1077 | { 1078 | name: "Image", 1079 | slug: "/image", 1080 | prompts: [...image], 1081 | icon: "image" as const, 1082 | }, 1083 | { 1084 | name: "Writing", 1085 | slug: "/writing", 1086 | prompts: [...writing], 1087 | icon: "pencil" as const, 1088 | }, 1089 | { 1090 | name: "Music", 1091 | slug: "/music", 1092 | prompts: [...music], 1093 | icon: "music" as const, 1094 | }, 1095 | { 1096 | name: "Ideas", 1097 | slug: "/ideas", 1098 | prompts: [...ideas], 1099 | icon: "light-bulb" as const, 1100 | }, 1101 | { 1102 | name: "Fun", 1103 | slug: "/fun", 1104 | prompts: [...fun], 1105 | icon: "game-controller" as const, 1106 | }, 1107 | { 1108 | name: "Misc", 1109 | slug: "/misc", 1110 | prompts: [...misc], 1111 | icon: "folder" as const, 1112 | }, 1113 | { 1114 | name: "Raycast Prompts", 1115 | slug: "/raycast", 1116 | prompts: [...raycast], 1117 | icon: "raycast-logo-neg" as const, 1118 | }, 1119 | ].map((category) => { 1120 | return { 1121 | ...category, 1122 | iconComponent: Icons[category.icon], 1123 | prompts: category.prompts.map((prompt) => { 1124 | return { 1125 | ...prompt, 1126 | iconComponent: Icons[prompt.icon], 1127 | }; 1128 | }), 1129 | }; 1130 | }); 1131 | 1132 | const allPrompts = baseCategories.flatMap((category) => category.prompts); 1133 | 1134 | const newCategory = { 1135 | name: "New", 1136 | slug: "/new", 1137 | // Show prompts that have been published for the past two weeks 1138 | prompts: allPrompts 1139 | .filter((prompt) => { 1140 | const twoWeeksAgo = new Date(); 1141 | twoWeeksAgo.setDate(twoWeeksAgo.getDate() - 14); 1142 | return new Date(prompt.date) >= twoWeeksAgo; 1143 | }) 1144 | .sort((a, b) => { 1145 | return new Date(b.date).getTime() - new Date(a.date).getTime(); 1146 | }), 1147 | icon: "calendar" as const, 1148 | iconComponent: Icons["calendar"], 1149 | }; 1150 | 1151 | export const categories: Category[] = [ 1152 | ...(newCategory.prompts.length > 0 ? [newCategory] : []), 1153 | ...baseCategories, 1154 | ]; 1155 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | reactStrictMode: true, 3 | swcMinify: true, 4 | }; 5 | 6 | module.exports = config; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raycast-prompt-explorer", 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 | "@radix-ui/react-alert-dialog": "^1.0.4", 13 | "@radix-ui/react-collapsible": "^1.0.3", 14 | "@radix-ui/react-context-menu": "^2.1.5", 15 | "@radix-ui/react-dialog": "^1.0.4", 16 | "@radix-ui/react-dropdown-menu": "^2.0.5", 17 | "@radix-ui/react-scroll-area": "^1.0.4", 18 | "@radix-ui/react-toast": "^1.1.4", 19 | "@radix-ui/react-tooltip": "^1.0.6", 20 | "@raycast/icons": "^0.4.1", 21 | "@vercel/analytics": "^0.1.11", 22 | "@viselect/react": "^3.2.7", 23 | "autoprefixer": "^10.4.11", 24 | "copy-to-clipboard": "^3.3.3", 25 | "lodash.debounce": "^4.0.8", 26 | "nanoid": "^4.0.2", 27 | "next": "^13.4.4", 28 | "postcss": "^8.4.24", 29 | "postcss-nested": "^5.0.6", 30 | "react": "^18.2.0", 31 | "react-dom": "^18.2.0", 32 | "react-markdown": "^8.0.7" 33 | }, 34 | "devDependencies": { 35 | "@types/lodash.debounce": "^4.0.9", 36 | "@types/node": "18.11.9", 37 | "eslint": "8.27.0", 38 | "eslint-config-next": "13.0.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pages/[[...slug]].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import NextLink from "next/link"; 3 | import SelectionArea, { SelectionEvent } from "@viselect/react"; 4 | import { useRouter } from "next/router"; 5 | import copy from "copy-to-clipboard"; 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogDescription, 10 | DialogTitle, 11 | DialogTrigger, 12 | } from "../components/Dialog"; 13 | import { 14 | DropdownMenu, 15 | DropdownMenuContent, 16 | DropdownMenuItem, 17 | DropdownMenuTrigger, 18 | DropdownMenuSeparator, 19 | } from "../components/DropdownMenu"; 20 | import { Toast, ToastTitle } from "../components/Toast"; 21 | import { ScrollArea } from "../components/ScrollArea"; 22 | import { Button } from "../components/Button"; 23 | import { ButtonGroup } from "../components/ButtonGroup"; 24 | import * as Collapsible from "@radix-ui/react-collapsible"; 25 | import { isTouchDevice } from "../utils/isTouchDevice"; 26 | 27 | import { categories, Category, Model, Prompt } from "../data/prompts"; 28 | 29 | import styles from "../styles/Home.module.css"; 30 | import { Instructions } from "../components/Instructions"; 31 | import { useSectionInView } from "../utils/useSectionInViewObserver"; 32 | import { extractPrompts } from "../utils/extractPrompts"; 33 | import CreativityIcon from "../components/CreativityIcon"; 34 | import * as ContextMenu from "@radix-ui/react-context-menu"; 35 | import { 36 | ChevronDownIcon, 37 | CopyClipboardIcon, 38 | DownloadIcon, 39 | LinkIcon, 40 | MinusCircleIcon, 41 | PlusCircleIcon, 42 | RaycastLogoNegIcon, 43 | StarsIcon, 44 | TrashIcon, 45 | } from "@raycast/icons"; 46 | import { 47 | addToRaycast, 48 | copyData, 49 | downloadData, 50 | makeUrl, 51 | } from "../utils/actions"; 52 | 53 | const promptModel: Record = { 54 | "openai-gpt-3.5-turbo-instruct": [ 55 | "GPT-3.5 Instruct", 56 | "OpenAI GPT-3.5 Turbo Instruct", 57 | ], 58 | "openai-gpt-3.5-turbo": ["GPT-3.5 Turbo", "OpenAI GPT-3.5 Turbo"], 59 | "openai-gpt-4": ["GPT-4", "OpenAI GPT-4"], 60 | "openai-gpt-4-turbo": ["GPT-4 Turbo", "OpenAI GPT-4 Turbo"], 61 | "anthropic-claude-haiku": ["Claude Haiku", "Anthropic Claude Haiku"], 62 | "anthropic-claude-opus": ["Claude Opus", "Anthropic Claude Opus"], 63 | "anthropic-claude-sonnet": ["Claude Sonnet", "Anthropic Claude Sonnet"], 64 | "perplexity-sonar-medium-online": [ 65 | "Sonar Medium", 66 | "Perplexity Sonar Medium Online", 67 | ], 68 | "perplexity-sonar-small-online": [ 69 | "Sonar Small", 70 | "Perplexity Sonar Small Online", 71 | ], 72 | "llama2-70b": ["Llama2", "Llama2 70B"], 73 | "mixtral-8x7b": ["Mixtral", "Mixtral 8x7B"], 74 | "codellama-70b-instruct": ["CodeLlama Instruct", "CodeLlama 70B Instruct"], 75 | }; 76 | 77 | export function getStaticPaths() { 78 | const paths = categories.map((category) => ({ 79 | params: { slug: [category.slug.replace("/", "")] }, 80 | })); 81 | 82 | return { 83 | paths: [ 84 | ...paths, 85 | { 86 | params: { slug: [] }, 87 | }, 88 | ], 89 | fallback: false, 90 | }; 91 | } 92 | 93 | export async function getStaticProps() { 94 | return { 95 | props: {}, 96 | }; 97 | } 98 | 99 | export default function Home({ onTouchReady }: { onTouchReady: () => void }) { 100 | const router = useRouter(); 101 | 102 | const [selectedPrompts, setSelectedPrompts] = React.useState([]); 103 | 104 | const [showToast, setShowToast] = React.useState(false); 105 | const [toastMessage, setToastMessage] = React.useState(""); 106 | 107 | const [actionsOpen, setActionsOpen] = React.useState(false); 108 | const [aboutOpen, setAboutOpen] = React.useState(false); 109 | const [isTouch, setIsTouch] = React.useState(); 110 | 111 | const onStart = ({ event, selection }: SelectionEvent) => { 112 | if (!isTouch && !event?.ctrlKey && !event?.metaKey) { 113 | selection.clearSelection(); 114 | setSelectedPrompts([]); 115 | } 116 | }; 117 | 118 | const onMove = ({ 119 | store: { 120 | changed: { added, removed }, 121 | }, 122 | }: SelectionEvent) => { 123 | const addedPrompts = extractPrompts(added, categories); 124 | const removedPrompts = extractPrompts(removed, categories); 125 | 126 | setSelectedPrompts((prevPrompts) => { 127 | const prompts = [...prevPrompts]; 128 | 129 | addedPrompts.forEach((prompt) => { 130 | if (!prompt) { 131 | return; 132 | } 133 | if (prompts.find((p) => p.id === prompt.id)) { 134 | return; 135 | } 136 | prompts.push(prompt); 137 | }); 138 | 139 | removedPrompts.forEach((prompt) => { 140 | return prompts.filter((s) => s?.id !== prompt?.id); 141 | }); 142 | 143 | return prompts; 144 | }); 145 | }; 146 | 147 | const handleDownload = React.useCallback(() => { 148 | downloadData(selectedPrompts); 149 | }, [selectedPrompts]); 150 | 151 | const handleCopyData = React.useCallback(() => { 152 | copyData(selectedPrompts); 153 | setToastMessage("Copied to clipboard"); 154 | setShowToast(true); 155 | }, [selectedPrompts]); 156 | 157 | const handleCopyUrl = React.useCallback(async () => { 158 | setToastMessage("Copying URL to clipboard..."); 159 | setShowToast(true); 160 | 161 | const url = makeUrl(selectedPrompts); 162 | let urlToCopy = url; 163 | const encodedUrl = encodeURIComponent(urlToCopy); 164 | const response = await fetch( 165 | `https://ray.so/api/shorten-url?url=${encodedUrl}&ref=prompts` 166 | ).then((res) => res.json()); 167 | 168 | if (response.link) { 169 | urlToCopy = response.link; 170 | } 171 | 172 | copy(urlToCopy); 173 | setShowToast(true); 174 | setToastMessage("Copied URL to clipboard!"); 175 | }, [selectedPrompts]); 176 | 177 | const handleCopyText = React.useCallback((prompt: Prompt) => { 178 | copy(prompt.prompt); 179 | setShowToast(true); 180 | setToastMessage("Copied to clipboard"); 181 | }, []); 182 | 183 | const handleAddToRaycast = React.useCallback( 184 | () => addToRaycast(router, selectedPrompts), 185 | [router, selectedPrompts] 186 | ); 187 | 188 | React.useEffect(() => { 189 | setIsTouch(isTouchDevice()); 190 | onTouchReady(); 191 | }, [isTouch, setIsTouch, onTouchReady]); 192 | 193 | React.useEffect(() => { 194 | const down = (event: KeyboardEvent) => { 195 | const { key, keyCode, metaKey, shiftKey, altKey } = event; 196 | 197 | if (key === "k" && metaKey) { 198 | if (selectedPrompts.length === 0) return; 199 | setActionsOpen((prevOpen) => { 200 | return !prevOpen; 201 | }); 202 | } 203 | 204 | if (key === "d" && metaKey) { 205 | if (selectedPrompts.length === 0) return; 206 | event.preventDefault(); 207 | handleDownload(); 208 | } 209 | 210 | if (key === "Enter" && metaKey) { 211 | if (selectedPrompts.length === 0) return; 212 | event.preventDefault(); 213 | handleAddToRaycast(); 214 | } 215 | 216 | // key === "c" doesn't work when using alt key, so we use keCode instead (67) 217 | if (keyCode === 67 && metaKey && altKey) { 218 | if (selectedPrompts.length === 0) return; 219 | event.preventDefault(); 220 | handleCopyData(); 221 | setActionsOpen(false); 222 | } 223 | 224 | if (key === "c" && metaKey && shiftKey) { 225 | event.preventDefault(); 226 | handleCopyUrl(); 227 | setActionsOpen(false); 228 | } 229 | 230 | if (key === "/" && metaKey) { 231 | event.preventDefault(); 232 | setActionsOpen(false); 233 | setAboutOpen((prevOpen) => !prevOpen); 234 | } 235 | 236 | if (key === "a" && metaKey) { 237 | event.preventDefault(); 238 | } 239 | }; 240 | 241 | document.addEventListener("keydown", down); 242 | return () => document.removeEventListener("keydown", down); 243 | }, [ 244 | setActionsOpen, 245 | setAboutOpen, 246 | selectedPrompts, 247 | handleCopyData, 248 | handleDownload, 249 | handleCopyUrl, 250 | handleAddToRaycast, 251 | ]); 252 | 253 | React.useEffect(() => { 254 | if (showToast) { 255 | setTimeout(() => { 256 | setShowToast(false); 257 | }, 2000); 258 | } 259 | }, [showToast]); 260 | 261 | return ( 262 |
263 |
264 | 265 | 266 | 271 | 272 | 273 |
274 |
275 | About 276 | 277 | Prompt Explorer is a tool to easily browse, share, and add 278 | prompts to Raycast. 279 | 280 |

281 | Select the prompts by clicking on them. To select multiple, 282 | hold or select them with your mouse. 283 |

284 |

285 | Then, click the “Add to Raycast” button to import these 286 | prompts as AI Commands. You can also download the prompts as a 287 | JSON file, or copy the URL to share with others. 288 |

289 |
290 | {!isTouch && ( 291 |
292 |

Shortcuts

293 |
    294 |
  • 295 | Add to Raycast 296 | 297 | 298 | 299 | 300 |
  • 301 |
  • 302 | Toggle Export Menu 303 | 304 | 305 | K 306 | 307 |
  • 308 |
  • 309 | Download JSON 310 | 311 | 312 | D 313 | 314 |
  • 315 |
  • 316 | Copy JSON 317 | 318 | 319 | 320 | C 321 | 322 |
  • 323 |
  • 324 | Copy URL to Share 325 | 326 | 327 | 328 | C 329 | 330 |
  • 331 |
  • 332 | Toggle this view 333 | 334 | 335 | / 336 | 337 |
  • 338 |
339 |
340 | )} 341 |
342 | 343 |

Contribute

344 |

345 | This project is Open Source and{" "} 346 | 350 | available on GitHub 351 | 352 | . We welcome contributions! 353 |
354 | If you have any questions or feedback, please{" "} 355 | 356 | send us an email 357 | 358 | . 359 |

360 | 361 |

362 | 371 | Made by{" "} 372 | 373 | {" "} 374 | 375 | Raycast 376 | 377 |

378 |
379 | 380 |
381 | 382 |
383 | {!isTouch ? ( 384 | 385 | 392 | 393 | 394 | 395 | 402 | 403 | 404 | handleDownload()} 407 | > 408 | Download JSON 409 | 410 | 411 | D 412 | 413 | 414 | handleCopyData()} 417 | > 418 | Copy JSON{" "} 419 | 420 | 421 | 422 | C 423 | 424 | 425 | handleCopyUrl()} 428 | > 429 | Copy URL to Share{" "} 430 | 431 | 432 | 433 | C 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | ) : ( 442 | 449 | )} 450 |
451 |
452 | 453 | 454 | 455 | {toastMessage} 456 | 457 | 458 | 459 |
460 |
461 |
462 | 463 |
464 |
465 |

Categories

466 | 467 | {categories.map((category) => ( 468 | 469 | ))} 470 |
471 | 472 | {selectedPrompts.length === 0 && } 473 | 474 | {selectedPrompts.length > 0 && ( 475 |
476 |

Add to Raycast

477 | 478 | 479 | 480 | 486 | 487 | 488 | 489 | {selectedPrompts.map((prompt, index) => ( 490 |
494 | {prompt.title} 495 | 508 |
509 | ))} 510 |
511 |
512 | 513 |
514 | 517 | 518 | 521 |
522 |
523 | )} 524 |
525 |
526 |
527 |
528 | 529 |
530 | {isTouch !== null && ( 531 | 546 | {categories.map((category) => { 547 | return ( 548 |
556 |

557 | {category.name} 558 |

559 |
560 | {category.prompts.map((prompt, index) => { 561 | const isSelected = selectedPrompts.some( 562 | (selectedPrompt) => selectedPrompt.id === prompt.id 563 | ); 564 | return ( 565 | 566 | 567 |
572 |
573 | 574 |
$&`
580 |                                         ),
581 |                                       }}
582 |                                     >
583 |
584 |
585 |
586 | 587 | 588 | {prompt.title} 589 | {prompt.author ? ( 590 | 591 | by{" "} 592 | {prompt.author.link ? ( 593 | 598 | {prompt.author.name} 599 | 600 | ) : ( 601 | prompt.author.name 602 | )} 603 | 604 | ) : null} 605 | 606 | {prompt.model ? ( 607 | 611 | {promptModel[prompt.model][0]} 612 | 613 | ) : null} 614 | 617 |
618 |
619 |
620 | 621 | 624 | { 627 | if (isSelected) { 628 | return setSelectedPrompts((prevPrompts) => 629 | prevPrompts.filter( 630 | (prevPrompt) => 631 | prevPrompt.id !== prompt.id 632 | ) 633 | ); 634 | } 635 | setSelectedPrompts((prevPrompts) => [ 636 | ...prevPrompts, 637 | prompt, 638 | ]); 639 | }} 640 | > 641 | {isSelected ? ( 642 | 643 | ) : ( 644 | 645 | )} 646 | {isSelected 647 | ? "Deselect Prompt" 648 | : "Select Prompt"} 649 | 650 | handleCopyText(prompt)} 653 | > 654 | Copy Prompt Text{" "} 655 | 656 | 657 | 658 |
659 | ); 660 | })} 661 |
662 |
663 | ); 664 | })} 665 |
666 | )} 667 |
668 |
669 |
670 | ); 671 | } 672 | 673 | function NavItem({ category }: { category: Category }) { 674 | const activeSection = useSectionInView(); 675 | 676 | return ( 677 | 683 | {category.icon ? : } 684 | 685 | {category.name} 686 | {category.prompts.length} 687 | 688 | ); 689 | } 690 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | import "../styles/globals.css"; 5 | 6 | import { Inter, JetBrains_Mono } from "next/font/google"; 7 | 8 | import type { AppProps } from "next/app"; 9 | import { ToastProvider, ToastViewport } from "../components/Toast"; 10 | import { useSectionInViewObserver } from "../utils/useSectionInViewObserver"; 11 | 12 | const inter = Inter({ subsets: ["latin"] }); 13 | const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"] }); 14 | 15 | function MyApp({ Component, pageProps }: AppProps) { 16 | const [enableViewObserver, setEnableViewObserver] = React.useState(false); 17 | useSectionInViewObserver({ headerHeight: 72, enabled: enableViewObserver }); 18 | 19 | return ( 20 | <> 21 | 22 | 23 | Prompt Explorer by Raycast 24 | 25 | 30 | 35 | 40 | 41 | 42 | 47 | 48 | 52 | 53 | 54 | 60 | setEnableViewObserver(true)} 63 | /> 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | export default MyApp; 72 | -------------------------------------------------------------------------------- /pages/shared.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | import SelectionArea, { SelectionEvent } from "@viselect/react"; 4 | import { useRouter } from "next/router"; 5 | import { nanoid } from "nanoid"; 6 | import { 7 | DropdownMenu, 8 | DropdownMenuContent, 9 | DropdownMenuItem, 10 | DropdownMenuTrigger, 11 | DropdownMenuSeparator, 12 | } from "../components/DropdownMenu"; 13 | import { Toast, ToastTitle } from "../components/Toast"; 14 | import { ScrollArea } from "../components/ScrollArea"; 15 | import { Button } from "../components/Button"; 16 | import { ButtonGroup } from "../components/ButtonGroup"; 17 | import { isTouchDevice } from "../utils/isTouchDevice"; 18 | import { extractPrompts } from "../utils/extractPrompts"; 19 | import styles from "../styles/Home.module.css"; 20 | import buttonStyles from "../components/Button.module.css"; 21 | import { Prompt } from "../data/prompts"; 22 | import CreativityIcon from "../components/CreativityIcon"; 23 | import { 24 | ChevronDownIcon, 25 | CopyClipboardIcon, 26 | DownloadIcon, 27 | PlusCircleIcon, 28 | StarsIcon, 29 | Icons, 30 | MinusCircleIcon, 31 | } from "@raycast/icons"; 32 | import * as ContextMenu from "@radix-ui/react-context-menu"; 33 | import { addToRaycast, copyData, downloadData } from "../utils/actions"; 34 | import copy from "copy-to-clipboard"; 35 | 36 | export default function Home() { 37 | const router = useRouter(); 38 | 39 | const [copied, setCopied] = React.useState(false); 40 | 41 | const [actionsOpen, setActionsOpen] = React.useState(false); 42 | const sharedPromptsInURL = React.useMemo( 43 | () => parseURLPrompt(router.query.prompts), 44 | [router.query] 45 | ); 46 | const [selectedPrompts, setSelectedPrompts] = React.useState([ 47 | ...sharedPromptsInURL, 48 | ]); 49 | const isTouch = React.useMemo( 50 | () => (typeof window !== "undefined" ? isTouchDevice() : false), 51 | [] 52 | ); 53 | 54 | React.useEffect(() => { 55 | // everytime the sharedPromptsInURL changes, we want to update the selectedPrompts 56 | // so that we start with the shared prompts selected 57 | setSelectedPrompts([...sharedPromptsInURL]); 58 | }, [sharedPromptsInURL]); 59 | 60 | const categories = [ 61 | { 62 | name: `${sharedPromptsInURL.length} ${ 63 | sharedPromptsInURL.length > 1 ? "prompts" : "prompt" 64 | } shared with you`, 65 | isTemplate: true, 66 | isShared: true, 67 | prompts: sharedPromptsInURL, 68 | slug: "/shared", 69 | icon: StarsIcon, 70 | }, 71 | ]; 72 | 73 | const onStart = ({ event, selection }: SelectionEvent) => { 74 | if (!event?.ctrlKey && !event?.metaKey) { 75 | selection.clearSelection(); 76 | setSelectedPrompts([]); 77 | } 78 | }; 79 | 80 | const onMove = ({ 81 | store: { 82 | changed: { added, removed }, 83 | }, 84 | }: SelectionEvent) => { 85 | const addedPrompts = extractPrompts(added, categories); 86 | const removedPrompts = extractPrompts(removed, categories); 87 | 88 | setSelectedPrompts((prevPrompts) => { 89 | const prompts = [...prevPrompts]; 90 | 91 | addedPrompts.forEach((prompt) => { 92 | if (!prompt) { 93 | return; 94 | } 95 | if (prompts.find((p) => p.id === prompt.id)) { 96 | return; 97 | } 98 | prompts.push(prompt); 99 | }); 100 | 101 | removedPrompts.forEach((prompt) => { 102 | return prompts.filter((s) => s?.id !== prompt?.id); 103 | }); 104 | 105 | return prompts; 106 | }); 107 | }; 108 | 109 | const handleDownload = React.useCallback(() => { 110 | downloadData(selectedPrompts); 111 | }, [selectedPrompts]); 112 | 113 | const handleCopyData = React.useCallback(() => { 114 | copyData(selectedPrompts); 115 | setCopied(true); 116 | }, [selectedPrompts]); 117 | 118 | const handleAddToRaycast = React.useCallback( 119 | () => addToRaycast(router, selectedPrompts), 120 | [router, selectedPrompts] 121 | ); 122 | 123 | const handleCopyText = React.useCallback((prompt: Prompt) => { 124 | copy(prompt.prompt); 125 | setCopied(true); 126 | }, []); 127 | 128 | React.useEffect(() => { 129 | const down = (event: KeyboardEvent) => { 130 | const { key, keyCode, metaKey, altKey } = event; 131 | 132 | if (key === "k" && metaKey) { 133 | if (selectedPrompts.length === 0) return; 134 | setActionsOpen((prevOpen) => { 135 | return !prevOpen; 136 | }); 137 | } 138 | 139 | if (key === "d" && metaKey) { 140 | if (selectedPrompts.length === 0) return; 141 | event.preventDefault(); 142 | handleDownload(); 143 | } 144 | 145 | if (key === "Enter" && metaKey) { 146 | if (selectedPrompts.length === 0) return; 147 | event.preventDefault(); 148 | handleAddToRaycast(); 149 | } 150 | 151 | // key === "c" doesn't work when using alt key, so we use keCode instead (67) 152 | if (keyCode === 67 && metaKey && altKey) { 153 | if (selectedPrompts.length === 0) return; 154 | event.preventDefault(); 155 | handleCopyData(); 156 | setActionsOpen(false); 157 | } 158 | 159 | if (key === "a" && metaKey) { 160 | event.preventDefault(); 161 | setSelectedPrompts([...sharedPromptsInURL]); 162 | } 163 | }; 164 | 165 | document.addEventListener("keydown", down); 166 | return () => document.removeEventListener("keydown", down); 167 | }, [ 168 | sharedPromptsInURL, 169 | setActionsOpen, 170 | selectedPrompts, 171 | handleCopyData, 172 | handleDownload, 173 | handleAddToRaycast, 174 | ]); 175 | 176 | React.useEffect(() => { 177 | if (copied) { 178 | setTimeout(() => { 179 | setCopied(false); 180 | }, 2000); 181 | } 182 | }, [copied]); 183 | 184 | if (sharedPromptsInURL.length === 0) { 185 | return; 186 | } 187 | 188 | console.log(categories); 189 | 190 | return ( 191 |
192 |
193 | 198 | 203 | ← See all Prompts 204 | 205 | 206 |
207 | 208 | 215 | 216 | 217 | 218 | 225 | 226 | 227 | handleDownload()} 230 | > 231 | Download JSON 232 | 233 | 234 | D 235 | 236 | 237 | handleCopyData()} 240 | > 241 | Copy JSON{" "} 242 | 243 | 244 | 245 | C 246 | 247 | 248 | 249 | 250 | 251 | 252 |
253 |
254 | 255 | 256 | 257 | Copied to clipboard 258 | 259 | 260 | 261 |
262 |
263 | {isTouch !== null && ( 264 | 279 | {categories.map((promptGroup) => { 280 | return ( 281 |
286 |

287 | {promptGroup.name} 288 |

289 |
290 | {promptGroup.prompts.map((prompt, index) => { 291 | const Icon = 292 | prompt.icon in Icons ? Icons[prompt.icon] : StarsIcon; 293 | 294 | const isSelected = selectedPrompts.some( 295 | (selectedPrompt) => selectedPrompt.id === prompt.id 296 | ); 297 | 298 | return ( 299 | 300 | 301 |
306 | selectedPrompt?.id === prompt.id 307 | )} 308 | data-key={`${promptGroup.slug}-${index}`} 309 | > 310 |
311 | 312 |
$&`
318 |                                         ),
319 |                                       }}
320 |                                     >
321 |
322 |
323 |
324 | 325 | 326 | {prompt.title} 327 | 328 | 331 |
332 |
333 |
334 | 335 | 338 | { 341 | if (isSelected) { 342 | return setSelectedPrompts((prevPrompts) => 343 | prevPrompts.filter( 344 | (prevPrompt) => 345 | prevPrompt.id !== prompt.id 346 | ) 347 | ); 348 | } 349 | setSelectedPrompts((prevPrompts) => [ 350 | ...prevPrompts, 351 | prompt, 352 | ]); 353 | }} 354 | > 355 | {isSelected ? ( 356 | 357 | ) : ( 358 | 359 | )} 360 | {isSelected 361 | ? "Deselect Prompt" 362 | : "Select Prompt"} 363 | 364 | handleCopyText(prompt)} 367 | > 368 | Copy Prompt Text{" "} 369 | 370 | 371 | 372 |
373 | ); 374 | })} 375 |
376 |
377 | ); 378 | })} 379 |
380 | )} 381 |
382 |
383 |
384 | ); 385 | } 386 | 387 | function parseURLPrompt(promptQueryString?: string | string[]): Prompt[] { 388 | if (!promptQueryString) { 389 | return []; 390 | } 391 | let prompts; 392 | if (Array.isArray(promptQueryString)) { 393 | prompts = promptQueryString; 394 | } else { 395 | prompts = [promptQueryString]; 396 | } 397 | return prompts.map((prompt) => ({ 398 | ...JSON.parse(prompt), 399 | id: nanoid(), 400 | isShared: true, 401 | })); 402 | } 403 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["postcss-nested", "autoprefixer"], 3 | }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raycast/prompt-explorer/2b5875e1a877129aee849248d724237a188ea546/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raycast/prompt-explorer/2b5875e1a877129aee849248d724237a188ea546/public/favicon.png -------------------------------------------------------------------------------- /public/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/raycast/prompt-explorer/2b5875e1a877129aee849248d724237a188ea546/public/og-image.png -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | position: relative; 3 | 4 | @media (min-width: 768px) { 5 | padding-left: 320px; 6 | 7 | & .container { 8 | padding-left: 0; 9 | } 10 | } 11 | } 12 | 13 | .sidebar { 14 | position: fixed; 15 | width: 320px; 16 | top: 60px; 17 | left: 0; 18 | bottom: 0; 19 | height: calc(100vh - 50px); 20 | padding: 24px; 21 | display: none; 22 | 23 | @media (min-width: 768px) { 24 | display: block; 25 | } 26 | } 27 | 28 | .sidebarInner { 29 | background-color: rgba(255, 255, 255, 0.05); 30 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05); 31 | height: 100%; 32 | border-radius: 12px; 33 | 34 | & [data-radix-scroll-area-viewport] > div { 35 | height: 100%; 36 | } 37 | } 38 | 39 | .sidebarContent { 40 | height: 100%; 41 | display: flex; 42 | flex-direction: column; 43 | justify-content: space-between; 44 | padding: 16px; 45 | } 46 | 47 | .sidebarTitle { 48 | font-size: 13px; 49 | font-weight: 500; 50 | color: rgba(255, 255, 255, 0.6); 51 | margin-bottom: 16px; 52 | display: flex; 53 | align-items: center; 54 | justify-content: space-between; 55 | } 56 | 57 | .sidebarNav { 58 | margin-bottom: 32px; 59 | } 60 | 61 | .sidebarNavItem { 62 | display: flex; 63 | align-items: center; 64 | gap: 8px; 65 | padding: 0 12px; 66 | border-radius: 8px; 67 | height: 36px; 68 | font-size: 13px; 69 | font-weight: 500; 70 | color: rgba(255, 255, 255, 0.6); 71 | margin-bottom: 2px; 72 | transition: all 300ms ease; 73 | 74 | &[data-active="true"] { 75 | background-color: rgba(255, 255, 255, 0.05); 76 | color: white; 77 | } 78 | 79 | &:hover { 80 | background-color: rgba(255, 255, 255, 0.1); 81 | color: white; 82 | } 83 | 84 | &:focus { 85 | outline: none; 86 | box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1); 87 | transition: box-shadow 100ms; 88 | } 89 | } 90 | 91 | .badge { 92 | display: flex; 93 | flex-direction: column; 94 | justify-content: center; 95 | align-items: center; 96 | user-select: none; 97 | padding: 2px 8px; 98 | color: #9261f9; 99 | background-color: #2c1d4b; 100 | border-radius: 9999px; 101 | font-weight: 500; 102 | font-size: 11px; 103 | line-height: 13px; 104 | margin-left: auto; 105 | } 106 | 107 | .summaryTrigger { 108 | all: unset; 109 | font-weight: 500; 110 | font-size: 13px; 111 | line-height: 16px; 112 | display: flex; 113 | width: calc(100%); 114 | margin-left: -8px; 115 | padding: 8px 8px; 116 | margin-top: -8px; 117 | border-radius: 8px; 118 | color: #ffffff; 119 | align-items: center; 120 | justify-content: space-between; 121 | 122 | &:focus-visible { 123 | outline: none; 124 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); 125 | } 126 | 127 | & svg { 128 | color: rgba(255, 255, 255, 0.4); 129 | transition: 300ms ease; 130 | margin-right: 8px; 131 | } 132 | 133 | &[aria-expanded="true"] svg { 134 | transform: rotate(180deg); 135 | } 136 | } 137 | 138 | .summaryContent { 139 | overflow: hidden; 140 | } 141 | 142 | .summaryContent[data-state="open"] { 143 | animation: slideDown 300ms ease-out; 144 | } 145 | 146 | .summaryContent[data-state="closed"] { 147 | animation: slideUp 300ms ease-out; 148 | } 149 | 150 | @keyframes slideDown { 151 | from { 152 | height: 0; 153 | } 154 | to { 155 | height: var(--radix-collapsible-content-height); 156 | } 157 | } 158 | 159 | @keyframes slideUp { 160 | from { 161 | height: var(--radix-collapsible-content-height); 162 | } 163 | to { 164 | height: 0; 165 | } 166 | } 167 | 168 | .summaryItem { 169 | display: flex; 170 | align-items: center; 171 | gap: 8px; 172 | border-radius: 8px; 173 | font-size: 13px; 174 | font-weight: 500; 175 | color: rgba(255, 255, 255, 0.6); 176 | margin-bottom: 2px; 177 | } 178 | 179 | .summaryControls { 180 | margin-top: 12px; 181 | display: flex; 182 | flex-direction: column; 183 | gap: 8px; 184 | 185 | & button { 186 | justify-content: center; 187 | } 188 | } 189 | 190 | .summaryItemButton { 191 | all: unset; 192 | color: #ff6363; 193 | padding: 8px; 194 | border-radius: 9999px; 195 | margin-left: auto; 196 | margin-right: 1px; 197 | 198 | &:hover { 199 | background-color: rgba(255, 99, 99, 0.15); 200 | } 201 | 202 | &:focus-visible { 203 | box-shadow: inset 0 0 0 1px rgba(255, 99, 99, 0.15), 204 | 0 0 0 1px rgba(255, 99, 99, 0.15); 205 | } 206 | } 207 | 208 | .container { 209 | position: relative; 210 | top: 60px; 211 | padding: 32px 16px; 212 | user-select: none; 213 | } 214 | 215 | .nav { 216 | position: fixed; 217 | top: 0; 218 | left: 0; 219 | right: 0; 220 | z-index: 9; 221 | display: flex; 222 | align-items: center; 223 | justify-content: space-between; 224 | flex-wrap: wrap; 225 | gap: 12px; 226 | background-color: black; 227 | padding: 24px 16px; 228 | 229 | @media (min-width: 768px) { 230 | padding: 16px 24px; 231 | } 232 | } 233 | 234 | .navControls { 235 | display: flex; 236 | gap: 16px; 237 | } 238 | 239 | .subtitle { 240 | font-size: 13px; 241 | margin-bottom: 16px; 242 | font-weight: 500; 243 | color: rgba(255, 255, 255, 0.6); 244 | display: flex; 245 | align-items: center; 246 | gap: 8px; 247 | } 248 | 249 | .prompts { 250 | display: grid; 251 | gap: 16px; 252 | margin-bottom: 64px; 253 | position: relative; 254 | z-index: 1; 255 | grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); 256 | } 257 | 258 | .item { 259 | padding: 16px; 260 | box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1); 261 | border-radius: 12px; 262 | display: flex; 263 | flex-direction: column; 264 | gap: 16px; 265 | align-items: flex-start; 266 | position: relative; 267 | transition: all 30ms; 268 | min-width: 0; 269 | color: #ffffff60; 270 | height: 100%; 271 | 272 | &:hover { 273 | background-color: rgba(44, 29, 75, 0.2); 274 | box-shadow: inset 0 0 0 1px #2c1d4b; 275 | color: #fff; 276 | 277 | .placeholder { 278 | color: #ff6363; 279 | } 280 | } 281 | 282 | &:focus { 283 | outline: none; 284 | box-shadow: inset 0 0 0 1px #9261f9; 285 | color: #fff; 286 | 287 | .placeholder { 288 | color: #ff6363; 289 | } 290 | } 291 | 292 | &[data-selected="true"] { 293 | background-color: rgba(44, 29, 75, 0.5); 294 | user-select: none; 295 | box-shadow: inset 0 0 0 2px solid #2c1d4b; 296 | color: #fff; 297 | 298 | .placeholder { 299 | color: #ff6363; 300 | } 301 | } 302 | } 303 | 304 | .prompt { 305 | width: 100%; 306 | display: flex; 307 | gap: 8px; 308 | justify-content: space-between; 309 | align-items: center; 310 | } 311 | 312 | .icons { 313 | display: flex; 314 | gap: 8px; 315 | } 316 | 317 | .name { 318 | display: flex; 319 | align-items: center; 320 | gap: 8px; 321 | font-size: 13px; 322 | font-weight: 500; 323 | color: #fff; 324 | } 325 | 326 | .promptAuthor { 327 | color: rgba(255, 255, 255, 0.5); 328 | font-size: 12px; 329 | font-weight: 400; 330 | 331 | a { 332 | color: rgba(255, 255, 255, 0.5); 333 | transition: color 150ms ease; 334 | 335 | &:hover { 336 | color: #ff6363; 337 | } 338 | } 339 | } 340 | 341 | .promptModel { 342 | color: rgba(255, 255, 255, 0.5); 343 | font-size: 10px; 344 | font-weight: 400; 345 | margin-left: auto; 346 | } 347 | 348 | .text { 349 | padding: 24px; 350 | display: block; 351 | 352 | &[data-type="symbol"] { 353 | font-size: 24px; 354 | } 355 | 356 | &[data-type="unicode"] { 357 | font-size: 14px; 358 | white-space: nowrap; 359 | } 360 | } 361 | 362 | .promptTemplate { 363 | background-color: rgba(255, 255, 255, 0.05); 364 | border-radius: 6px; 365 | max-height: 320px; 366 | width: 100%; 367 | flex: 1; 368 | display: flex; 369 | align-items: center; 370 | justify-content: center; 371 | } 372 | 373 | .template { 374 | font-family: var(--font-jetbrains); 375 | font-size: 13px; 376 | line-height: 1.6; 377 | white-space: pre-wrap; 378 | text-align: left; 379 | padding: 12px; 380 | } 381 | 382 | .placeholder { 383 | color: #ffffff60; 384 | } 385 | 386 | .trigger { 387 | appearance: none; 388 | background-color: transparent; 389 | border: none; 390 | outline: none; 391 | border-radius: 6px; 392 | padding: 0 4px 0 8px; 393 | height: 30px; 394 | font-weight: 500; 395 | display: inline-flex; 396 | align-items: center; 397 | gap: 8px; 398 | white-space: nowrap; 399 | font-size: 13px; 400 | font-weight: 500; 401 | letter-spacing: 0.1px; 402 | 403 | svg { 404 | flex-shrink: 0; 405 | } 406 | 407 | &[data-variant="primary"] { 408 | color: #fff; 409 | background: rgba(255, 99, 99, 0.15); 410 | color: #ff6363; 411 | &:not(:disabled):hover { 412 | background: rgba(255, 99, 99, 0.3); 413 | } 414 | &:not(:disabled):focus { 415 | box-shadow: 0 0 0 2px #191919, 0 0 0 4px rgba(255, 99, 99, 0.2); 416 | } 417 | } 418 | 419 | &[data-variant="secondary"] { 420 | color: hsla(0, 0%, 100%, 0.6); 421 | 422 | &:not(:disabled):hover, 423 | &[aria-expanded="true"] { 424 | background-color: hsla(0, 0%, 100%, 0.1); 425 | } 426 | &:not(:disabled):focus { 427 | box-shadow: 0 0 0 2px #191919, 0 0 0 4px hsla(0, 0%, 100%, 0.1); 428 | } 429 | } 430 | 431 | &:disabled { 432 | opacity: 0.5; 433 | } 434 | } 435 | 436 | .dialogTitle { 437 | color: hsla(0, 0%, 100%, 0.5); 438 | font-size: 16px; 439 | font-weight: 500; 440 | margin-bottom: 24px; 441 | } 442 | 443 | .dialogDescription { 444 | color: hsla(0, 0%, 100%, 0.6); 445 | font-size: 14px; 446 | line-height: 1.6; 447 | margin-bottom: 24px; 448 | } 449 | 450 | .dialogButtons { 451 | display: flex; 452 | align-items: center; 453 | gap: 12px; 454 | } 455 | 456 | .hotkeys { 457 | display: inline-flex; 458 | gap: 4px; 459 | align-items: center; 460 | margin-left: auto; 461 | } 462 | 463 | .toastTitle { 464 | font-size: 13px; 465 | font-weight: 500; 466 | letter-spacing: 0.1px; 467 | display: flex; 468 | align-items: center; 469 | gap: 10px; 470 | } 471 | 472 | .logo { 473 | all: unset; 474 | position: relative; 475 | display: flex; 476 | align-items: center; 477 | gap: 8px; 478 | cursor: pointer; 479 | 480 | &:hover, 481 | &:focus-visible { 482 | background-color: #282b3580; 483 | border-radius: 8px; 484 | } 485 | 486 | svg { 487 | width: 30px; 488 | height: 30px; 489 | color: #ff6363; 490 | } 491 | 492 | h2 { 493 | font-size: 16px; 494 | font-weight: 500; 495 | } 496 | 497 | @media (min-width: 768px) { 498 | gap: 12px; 499 | padding: 6px 12px; 500 | 501 | svg { 502 | width: 36px; 503 | height: 36px; 504 | } 505 | } 506 | } 507 | 508 | .logoSeparator { 509 | height: 30px; 510 | border-right: 1px solid #282b35; 511 | } 512 | 513 | .about { 514 | & a { 515 | color: white; 516 | transition: color 150ms ease; 517 | 518 | &:hover { 519 | color: #ff6363; 520 | } 521 | } 522 | } 523 | 524 | .aboutTopContent { 525 | display: grid; 526 | gap: 24px; 527 | 528 | @media (min-width: 640px) { 529 | grid-template-columns: 1.5fr 1fr; 530 | } 531 | } 532 | 533 | .aboutGlow { 534 | position: absolute; 535 | bottom: 0; 536 | left: 20%; 537 | right: 20%; 538 | height: 100px; 539 | background: conic-gradient( 540 | from 147.14deg at 50% 50%, 541 | #0294fe -55.68deg, 542 | #ff2136 113.23deg, 543 | #9b4dff 195deg, 544 | #0294fe 304.32deg, 545 | #ff2136 473.23deg 546 | ); 547 | filter: blur(60px); 548 | border-radius: 80%; 549 | transform: translateY(100%) scale(0.5); 550 | opacity: 0.3; 551 | z-index: -1; 552 | animation: showGlow 1000ms ease-out forwards; 553 | } 554 | 555 | @keyframes showGlow { 556 | to { 557 | opacity: 0.5; 558 | transform: translateY(50%) scale(1); 559 | } 560 | } 561 | 562 | .shortcuts { 563 | margin-bottom: 32px; 564 | 565 | li { 566 | display: flex; 567 | align-items: center; 568 | gap: 8px; 569 | font-size: 13px; 570 | font-weight: 500; 571 | letter-spacing: 0.1px; 572 | color: white; 573 | margin-bottom: 8px; 574 | } 575 | } 576 | 577 | .contextMenuContent { 578 | min-width: 220px; 579 | overflow: hidden; 580 | padding: 6px; 581 | border-radius: 8px; 582 | background-color: #252525; 583 | border: 1px solid hsla(0, 0%, 100%, 0.07); 584 | box-shadow: 0 4px 16px 0 rgb(0 0 0 / 50%); 585 | } 586 | 587 | .contextMenuItem { 588 | font-size: 13px; 589 | line-height: 1; 590 | color: var(--violet-11); 591 | border-radius: 3px; 592 | display: flex; 593 | align-items: center; 594 | padding: 8px; 595 | position: relative; 596 | user-select: none; 597 | outline: none; 598 | cursor: default; 599 | gap: 6px; 600 | 601 | &[data-highlighted] { 602 | background-color: rgba(255, 255, 255, 0.1); 603 | } 604 | } 605 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html, 8 | body { 9 | padding: 0; 10 | margin: 0; 11 | background: #000000; 12 | } 13 | 14 | body, 15 | input, 16 | button { 17 | font-family: var(--font-inter), -apple-system, BlinkMacSystemFont, Segoe UI, 18 | Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, 19 | sans-serif; 20 | color: #fff; 21 | -webkit-text-size-adjust: 100%; 22 | user-select: none; 23 | } 24 | 25 | #__next { 26 | position: relative; 27 | z-index: 0; 28 | } 29 | 30 | button { 31 | cursor: default; 32 | } 33 | 34 | button:disabled { 35 | cursor: not-allowed; 36 | } 37 | 38 | svg { 39 | display: block; 40 | width: 16px; 41 | height: 16px; 42 | } 43 | 44 | a { 45 | color: inherit; 46 | text-decoration: none; 47 | } 48 | 49 | ul, 50 | ol { 51 | list-style: none; 52 | } 53 | 54 | kbd { 55 | font-family: var(--font-inter); 56 | width: 24px; 57 | height: 24px; 58 | font-weight: 400; 59 | border-radius: 4px; 60 | display: inline-flex; 61 | align-items: center; 62 | justify-content: center; 63 | background-color: hsla(0, 0%, 100%, 0.1); 64 | color: hsla(0, 0%, 100%, 0.6); 65 | 66 | &[data-variant="small"] { 67 | width: 20px; 68 | height: 20px; 69 | border-radius: 3px; 70 | background-color: hsla(0, 0%, 100%, 0.3); 71 | color: hsla(0, 0%, 100%, 0.9); 72 | } 73 | } 74 | 75 | .selection-area { 76 | background: rgba(86, 194, 255, 0.15); 77 | border: 1px solid #56c2ff; 78 | border-radius: 2px; 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "incremental": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "svgr-template.js", 32 | ".next/types/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /utils/actions.ts: -------------------------------------------------------------------------------- 1 | import copy from "copy-to-clipboard"; 2 | import { NextRouter } from "next/router"; 3 | import { Model, Prompt } from "../data/prompts"; 4 | 5 | const raycastProtocolForEnvironments = { 6 | development: "raycastinternal", 7 | production: "raycast", 8 | test: "raycastinternal", 9 | }; 10 | const raycastProtocol = raycastProtocolForEnvironments[process.env.NODE_ENV]; 11 | 12 | function prepareModel(model?: string) { 13 | if (model && /^".*"$/.test(model)) { 14 | return model.slice(1, model.length - 1) as Model; 15 | } 16 | return model || "openai-gpt-3.5-turbo"; 17 | } 18 | 19 | function makePromptImportData(prompts: Prompt[]): string { 20 | return `[${prompts 21 | .map((selectedPrompt) => { 22 | const { title, prompt, creativity, icon, model } = selectedPrompt; 23 | 24 | return JSON.stringify({ 25 | title, 26 | prompt, 27 | creativity, 28 | icon, 29 | model: prepareModel(model), 30 | }); 31 | }) 32 | .join(",")}]`; 33 | } 34 | 35 | function makeQueryString(prompts: Prompt[]): string { 36 | const queryString = prompts 37 | .map((selectedPrompt) => { 38 | const { title, prompt, creativity, icon, model } = selectedPrompt; 39 | 40 | return `prompts=${encodeURIComponent( 41 | JSON.stringify({ 42 | title, 43 | prompt, 44 | creativity, 45 | icon, 46 | model: prepareModel(model), 47 | }) 48 | )}`; 49 | }) 50 | .join("&"); 51 | return queryString; 52 | } 53 | 54 | export function downloadData(prompts: Prompt[]) { 55 | const encodedPromptsData = encodeURIComponent(makePromptImportData(prompts)); 56 | const jsonString = `data:text/json;chatset=utf-8,${encodedPromptsData}`; 57 | const link = document.createElement("a"); 58 | link.href = jsonString; 59 | link.download = "prompts.json"; 60 | link.click(); 61 | } 62 | 63 | export function copyData(prompts: Prompt[]) { 64 | copy(makePromptImportData(prompts)); 65 | } 66 | 67 | export function makeUrl(prompts: Prompt[]) { 68 | return `${window.location.origin}/shared?${makeQueryString(prompts)}`; 69 | } 70 | 71 | export function copyUrl(prompts: Prompt[]) { 72 | copy(makeUrl(prompts)); 73 | } 74 | 75 | export function addToRaycast(router: NextRouter, prompts: Prompt[]) { 76 | router.replace( 77 | `${raycastProtocol}://prompts/import?${makeQueryString(prompts)}` 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /utils/extractPrompts.ts: -------------------------------------------------------------------------------- 1 | export function extractPrompts( 2 | els: Element[], 3 | categories: { slug: string; prompts: T[] }[] 4 | ) { 5 | const ids = els.map((v) => v.getAttribute("data-key")); 6 | 7 | const prompts = ids 8 | .map((id) => { 9 | if (!id) { 10 | return; 11 | } 12 | const [slug, index] = id?.split("-") ?? []; 13 | const category = categories.find((category) => category.slug === slug); 14 | 15 | return category?.prompts[parseInt(index)]; 16 | }) 17 | .filter(Boolean); 18 | 19 | return prompts; 20 | } 21 | -------------------------------------------------------------------------------- /utils/isTouchDevice.ts: -------------------------------------------------------------------------------- 1 | export function isTouchDevice() { 2 | return ( 3 | "ontouchstart" in window || 4 | navigator.maxTouchPoints > 0 || 5 | (navigator as any).msMaxTouchPoints > 0 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /utils/useSectionInViewObserver.tsx: -------------------------------------------------------------------------------- 1 | // Adapted from Vlad's work for the WorkOS docs. 2 | 3 | import debounce from "lodash.debounce"; 4 | import { useRouter } from "next/router"; 5 | import * as React from "react"; 6 | 7 | interface Config { 8 | headerHeight: number; 9 | enabled: boolean; 10 | } 11 | 12 | export function useSectionInViewObserver({ 13 | headerHeight, 14 | enabled = false, 15 | }: Config) { 16 | const defaultRootMargin = `-${headerHeight}px 0% -50% 0%`; 17 | const router = useRouter(); 18 | const historyKey = React.useRef(""); 19 | const animationFrame = React.useRef(0); 20 | const isPageReload = React.useRef(false); 21 | 22 | React.useEffect(() => { 23 | // Automatic scroll restoration messes up scroll position on load and how browser 24 | // forward/back buttons work with what we are hand-rolling for the section URLs. 25 | // We still need to do `window.scrollTo({ top: 0 })` to fully counter it though. 26 | window.history.scrollRestoration = "manual"; 27 | 28 | let sections: NodeListOf; 29 | let sectionsInView: IntersectionObserverEntry[] = []; 30 | 31 | const handleIntersect: IntersectionObserverCallback = (entries) => { 32 | let newEntryInView: IntersectionObserverEntry | undefined; 33 | 34 | entries.forEach((entry) => { 35 | if (entry.isIntersecting) { 36 | sectionsInView = [ 37 | ...sectionsInView.filter((view) => view.target !== entry.target), 38 | entry, 39 | ]; 40 | } else { 41 | sectionsInView = sectionsInView.filter( 42 | (view) => view.target !== entry.target 43 | ); 44 | } 45 | }); 46 | 47 | if (sectionsInView.length < 1) { 48 | return; 49 | } 50 | 51 | const fullyInView = sectionsInView.filter( 52 | (view) => view.intersectionRatio === 1 53 | ); 54 | 55 | // Sections fully in view get priority if there are any 56 | if (fullyInView.length > 0) { 57 | // get the top-most section fully in view (top is closest to zero) 58 | newEntryInView = fullyInView.reduce( 59 | (previousCandidate, currentCandidate) => 60 | previousCandidate.target.getBoundingClientRect().top > 61 | currentCandidate.target.getBoundingClientRect().top 62 | ? currentCandidate 63 | : previousCandidate 64 | ); 65 | } else { 66 | // get the section closest to the crossing border (top is closest to half-viewport mark) 67 | newEntryInView = sectionsInView.reduce( 68 | (previousCandidate, currentCandidate) => 69 | previousCandidate.target.getBoundingClientRect().top < 70 | currentCandidate.target.getBoundingClientRect().top 71 | ? currentCandidate 72 | : previousCandidate 73 | ); 74 | } 75 | 76 | const newSlug = newEntryInView 77 | ? (newEntryInView.target as HTMLElement).dataset.sectionSlug 78 | : undefined; 79 | 80 | // Wait for router to be ready and avoid setting route if it hasn't changed 81 | if (newSlug && router.isReady && router.asPath !== newSlug) { 82 | const newUrl = router.basePath + newSlug; 83 | 84 | router.asPath = newSlug; 85 | 86 | updateHistory(newUrl); 87 | dispatchEvent( 88 | new CustomEvent("sectionInViewChange", { detail: newSlug }) 89 | ); 90 | } 91 | }; 92 | 93 | // Debounce `window.history.replaceState` because Safari may throw 94 | // a security error for too many history updates and stop accepting updates. 95 | const updateHistory = debounce( 96 | (newUrl: string) => { 97 | const newState = { ...window.history.state, as: newUrl, url: newUrl }; 98 | window.history.replaceState(newState, "", newUrl); 99 | }, 100 | 120, 101 | { 102 | leading: true, 103 | trailing: true, 104 | } 105 | ); 106 | 107 | const observer = { 108 | current: new IntersectionObserver(handleIntersect, { 109 | rootMargin: defaultRootMargin, 110 | threshold: [0, 1], 111 | }), 112 | }; 113 | 114 | const adjustScroll = ({ shouldRestore } = { shouldRestore: true }) => { 115 | const section = document.querySelector( 116 | `[data-section-slug="${router.asPath}"]` 117 | ); 118 | 119 | // Focus the section so AT announces the new content after navigation. 120 | section?.focus({ preventScroll: true }); 121 | 122 | // Get latest state key if this was a page reload, or the current state key otherwise. 123 | const key = isPageReload.current ? undefined : window.history.state.key; 124 | const restoredScrollTop = getScrollHistory(key); 125 | 126 | if (shouldRestore && restoredScrollTop) { 127 | window.scrollTo({ top: 0 }); 128 | window.scrollTo({ top: restoredScrollTop }); 129 | return; 130 | } 131 | 132 | if (section) { 133 | // 1. If current section is the first section, this is the final scroll position we want. 134 | // 2. If not, we still need to reset scroll to top so that native scroll restoration doesn't mess things up. 135 | window.scrollTo({ top: 0 }); 136 | 137 | const firstSection = document.querySelector("[data-section-slug]"); 138 | 139 | // We'll do some elaborate calculations to determine where to scroll to depending on the resulting root margin 140 | if (section !== firstSection) { 141 | const sectionTop = section.getBoundingClientRect().top; 142 | const scrollY = Math.floor(window.scrollY); 143 | const sh = document.documentElement.scrollHeight; 144 | const ch = document.documentElement.clientHeight; 145 | const fh = getFooterHeight(); 146 | 147 | let remainingScroll = 148 | (sh * (0.5 * ch - fh) + 149 | fh * (fh + scrollY + sectionTop - 0.5 * ch) - 150 | 0.5 * ch * (scrollY + sectionTop)) / 151 | (1.5 * ch - 2 * fh); 152 | 153 | remainingScroll = Math.max(remainingScroll, 0); 154 | 155 | // Is it less than half of the viewport height remaining to scroll? 156 | // If so, scroll to a place where section top will meet the root margin top 157 | if (fh + remainingScroll < 0.5 * ch) { 158 | window.scrollTo({ top: sh - ch - remainingScroll }); 159 | } else { 160 | // But in most cases, we scroll to the section top minus some padding 161 | window.scrollTo({ top: sectionTop - 80 }); 162 | } 163 | } 164 | } 165 | }; 166 | 167 | // Get the total height of everything below the last section 168 | const getFooterHeight = () => { 169 | const sections = document.querySelectorAll("[data-section-slug]"); 170 | const lastSection = sections[sections.length - 1]; 171 | const sectionBottom = lastSection?.getBoundingClientRect().bottom ?? 0; 172 | const sh = document.documentElement.scrollHeight; 173 | const scrollY = Math.floor(window.scrollY); 174 | return sh - scrollY - sectionBottom; 175 | }; 176 | 177 | // We'll check if you are scrolling close to the bottom of the document; 178 | // if so, gradually lower and collapse the root margin of where intersections are tracked. 179 | // This makes it so that the bottom-most sections can be reported as currently active. See more: 180 | // https://www.figma.com/file/P14gbzIrrPdkOqFPQv69i4/Active-Section-in-View?node-id=138%3A2450&t=zgvyG71TV8jiiN1X-1 181 | const handleScrollOrResize = () => { 182 | let rootMargin = defaultRootMargin; 183 | const scrollY = Math.floor(window.scrollY); 184 | const sh = document.documentElement.scrollHeight; 185 | const ch = document.documentElement.clientHeight; 186 | const fh = getFooterHeight(); 187 | const remainingScroll = sh - scrollY - ch; 188 | 189 | // Start collapsing the root margin when 50% of the viewport height remains below the fold 190 | if (fh + remainingScroll < 0.5 * ch) { 191 | const speed = (ch - fh) / (0.5 * ch - fh); 192 | const rootMarginBottom = Math.max(0, fh + remainingScroll); 193 | const rootMarginBottomTravel = 0.5 * ch - rootMarginBottom; 194 | const rootMarginTopMin = Math.floor(rootMarginBottomTravel * speed); 195 | const rootMarginTop = Math.max(rootMarginTopMin, headerHeight); 196 | rootMargin = `${-1 * rootMarginTop}px 0% ${-1 * rootMarginBottom}px 0%`; 197 | } 198 | 199 | if (observer.current.rootMargin !== rootMargin) { 200 | sectionsInView.length = 0; 201 | observer.current.disconnect(); 202 | observer.current = new IntersectionObserver(handleIntersect, { 203 | rootMargin, 204 | threshold: [0, 1], 205 | }); 206 | sections?.forEach((section) => observer.current.observe(section)); 207 | } 208 | }; 209 | 210 | const handleRouteChangeComplete = () => { 211 | historyKey.current = window.history.state.key; 212 | }; 213 | 214 | const handleBeforeHistoryChange = () => { 215 | setScrollHistory(historyKey.current, window.scrollY); 216 | }; 217 | 218 | const handleBeforeUnload = () => { 219 | setScrollHistory(historyKey.current, window.scrollY); 220 | }; 221 | 222 | if (!historyKey.current) { 223 | // set a different key if session state isn't empty? 224 | historyKey.current = window.history.state.key; 225 | } 226 | 227 | const adjustScrollRecursively = () => { 228 | // Adjust scroll every animation frame until it's cancelled 229 | animationFrame.current = requestAnimationFrame(() => { 230 | adjustScroll({ shouldRestore: true }); 231 | adjustScrollRecursively(); 232 | }); 233 | }; 234 | 235 | // If the router isn't ready, it's a fully fresh page load. 236 | // Start adjusting the scroll position if we are not doing that yet. 237 | if (!router.isReady && !animationFrame.current) { 238 | adjustScrollRecursively(); 239 | } 240 | 241 | if (!router.isReady) { 242 | const navigationEntry = window.performance.getEntriesByType( 243 | "navigation" 244 | )[0] as PerformanceNavigationTiming | undefined; 245 | 246 | isPageReload.current = navigationEntry?.type === "reload"; 247 | } 248 | 249 | if (router.isReady && enabled) { 250 | // Cancel the last scheduled scroll adjustment 251 | cancelAnimationFrame(animationFrame.current); 252 | 253 | // Decide whether we should try to restore the original scroll position or refresh the scroll 254 | // to the current section coordinates (e.g. when clicking a link that goes to the current URL). 255 | const wasRecursive = animationFrame.current !== 0; 256 | const shallow = window.history.state.key === historyKey.current; 257 | const shouldRestore = wasRecursive || !shallow; 258 | 259 | adjustScroll({ shouldRestore }); 260 | sections = document.querySelectorAll("[data-section-slug]"); 261 | sections?.forEach((section) => observer.current.observe(section)); 262 | 263 | animationFrame.current = 0; 264 | isPageReload.current = false; 265 | 266 | router.events.on("routeChangeComplete", handleRouteChangeComplete); 267 | router.events.on("beforeHistoryChange", handleBeforeHistoryChange); 268 | window.addEventListener("beforeunload", handleBeforeUnload); 269 | window.addEventListener("scroll", handleScrollOrResize); 270 | window.addEventListener("resize", handleScrollOrResize); 271 | } 272 | 273 | return () => { 274 | sectionsInView.length = 0; 275 | observer.current.disconnect(); 276 | cancelAnimationFrame(animationFrame.current); 277 | router.events.off("routeChangeComplete", handleRouteChangeComplete); 278 | router.events.off("beforeHistoryChange", handleBeforeHistoryChange); 279 | window.removeEventListener("beforeunload", handleBeforeUnload); 280 | window.removeEventListener("scroll", handleScrollOrResize); 281 | window.removeEventListener("resize", handleScrollOrResize); 282 | }; 283 | }); 284 | } 285 | 286 | export function useSectionInView() { 287 | const router = useRouter(); 288 | const [sectionInView, setSectionInView] = React.useState(); 289 | 290 | React.useEffect(() => { 291 | setSectionInView(router.asPath); 292 | 293 | const handleSectionInViewChange = (event: CustomEvent) => { 294 | setSectionInView(event.detail); 295 | }; 296 | 297 | addEventListener( 298 | "sectionInViewChange", 299 | handleSectionInViewChange as (event: Event) => void 300 | ); 301 | 302 | return () => { 303 | removeEventListener( 304 | "sectionInViewChange", 305 | handleSectionInViewChange as (event: Event) => void 306 | ); 307 | }; 308 | }, [router]); 309 | 310 | return sectionInView; 311 | } 312 | 313 | interface ScrollHistoryEntry { 314 | key: string; 315 | value: number; 316 | } 317 | 318 | // Store scroll restoration history in session storage so it works even when reloading or restoring the tab. 319 | function setScrollHistory(key: string, value: number) { 320 | const str = window.sessionStorage.getItem("@workos/scroll-restoration"); 321 | const arr: ScrollHistoryEntry[] = str ? JSON.parse(str) : []; 322 | const index = arr.findIndex((entry) => entry.key === key); 323 | 324 | // Remove item with a matching key 325 | if (index !== -1) { 326 | arr.splice(index, 1); 327 | } 328 | 329 | // Push the new item last into the history stack 330 | arr.push({ key, value }); 331 | 332 | window.sessionStorage.setItem( 333 | "@workos/scroll-restoration", 334 | JSON.stringify(arr) 335 | ); 336 | } 337 | 338 | function getScrollHistory(key?: string): number | undefined { 339 | const str = window.sessionStorage.getItem("@workos/scroll-restoration"); 340 | const arr: ScrollHistoryEntry[] = str ? JSON.parse(str) : []; 341 | 342 | // Return last scroll history value if key isn't supplied 343 | if (key === undefined) { 344 | return arr[arr.length - 1]?.value; 345 | } 346 | 347 | return arr.find((entry) => entry.key === key)?.value; 348 | } 349 | --------------------------------------------------------------------------------