├── LICENSE ├── README.md ├── step-0 ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── convex │ ├── README.md │ ├── _generated │ │ ├── api.d.ts │ │ ├── api.js │ │ ├── dataModel.d.ts │ │ ├── server.d.ts │ │ └── server.js │ └── tsconfig.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ └── main.jsx ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.js ├── step-1 ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── convex │ ├── README.md │ ├── _generated │ │ ├── api.d.ts │ │ ├── api.js │ │ ├── dataModel.d.ts │ │ ├── server.d.ts │ │ └── server.js │ ├── chapters.ts │ ├── schema.ts │ └── tsconfig.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ └── main.jsx ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.js ├── step-2 ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── convex │ ├── README.md │ ├── _generated │ │ ├── api.d.ts │ │ ├── api.js │ │ ├── dataModel.d.ts │ │ ├── server.d.ts │ │ └── server.js │ ├── ai.ts │ ├── chapters.ts │ ├── schema.ts │ └── tsconfig.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── index.css │ └── main.jsx ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.js └── step-3 ├── .eslintrc.cjs ├── .gitignore ├── .prettierignore ├── .prettierrc.cjs ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── ai.ts ├── chapters.ts ├── schema.ts └── tsconfig.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── public └── vite.svg ├── src ├── App.css ├── App.tsx ├── index.css └── main.jsx ├── tailwind.config.cjs ├── tsconfig.json └── vite.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Convex 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 | # AI Storybook 2 | 3 | An example project showing how to make a full-stack app that integrates elements of generative AI. 4 | 5 | ![](https://i.ytimg.com/vi/4DEFIEHbC_s/maxresdefault.jpg) 6 | 7 | These project feature the following platforms and frameworks: 8 | 9 | - [Replicate](https://replicate.com) 10 | - [OpenAI](https://openai.com) 11 | - [LangChain.js](https://github.com/hwchase17/langchainjs) 12 | - [Convex](https://convex.dev) 13 | 14 | ## Video 15 | 16 | This project was coded on a live stream. The video, which introduces the concepts gradually and explains how the project was built, is found on YouTube here: 17 | 18 | [https://www.youtube.com/watch?v=4DEFIEHbC_s](https://www.youtube.com/watch?v=4DEFIEHbC_s) 19 | 20 | ## Steps 21 | 22 | The project is broken up an initial state, and then three steps/stages of development that are completed during the video. The end result of each step is a separate subdirectly in this repository. 23 | 24 | - Step 0 (The initial state): A simple, frontend-only React app that shows a caurosel view with each cell representing a page in a children's book. The left side of the cell is the _text_ of the page of the book, which can be edited by clicking the text and typing in the resultant textarea. The right side of the cell is reserved for an eventual AI-generated image to accompany the page contents. For now this will show a simple spinner. This basic React app will be more of less unchanged as we develop the rest of the project on the backend. The page state is driven by an in-app `useState` React state variable, but that will soon change. 25 | - Step 1: A full-stack app, where we've swapped out the React state for a [Convex](https://convex.dev) backend. We'll replace `useState` with `useQuery`, and do the other plumbing necessary to make our app state stored in the backend. 26 | - Step 2: Initial generative AI. We'll create a background task that fetches an image to represent the illustration for each page from a [Replicate](https://replicate.com) model using the LangChain.js library. The prompt for any given page will just be the page contents. 27 | - Step 3: Richer, prompt-tuned generative AI. We'll introduce using "chaining" from LangChain.js, and we'll utilize it to both improve our prompts, and to connect ChatGPT to our app to further refine our storybook illustrations. 28 | 29 | ## Running each step of the project 30 | 31 | For step-0, which is only a react app: 32 | 33 | cd step-0 34 | npm i 35 | npm run dev 36 | 37 | For step-1, a full stack app: 38 | 39 | cd step-1 40 | npm i 41 | npm run dev 42 | 43 | For step-2, a full stack app using Replicate: 44 | 45 | # Get a replicate.com API key and put it in your Convex backend's environment 46 | # named REPLICATE_API_KEY. 47 | cd step-2 48 | npm i 49 | npm run dev 50 | 51 | For step-3, a full stack app using Replicate and OpenAI: 52 | 53 | # Get a replicate.com API key and put it in your Convex backend's environment 54 | # named REPLICATE_API_KEY. 55 | # Get a platform.openai.com API key and put it in your Convex backend's environment 56 | # named OPENAI_API_KEY. 57 | cd step-3 58 | npm i 59 | npm run dev 60 | -------------------------------------------------------------------------------- /step-0/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | settings: { react: { version: "18.2" } }, 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": "warn", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /step-0/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.env 26 | convex.json 27 | -------------------------------------------------------------------------------- /step-0/.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | package-lock.json -------------------------------------------------------------------------------- /step-0/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | proseWrap: "always", 3 | arrowParens: "avoid", 4 | }; 5 | -------------------------------------------------------------------------------- /step-0/convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /step-0/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | 18 | /** 19 | * A utility for referencing Convex functions in your app's API. 20 | * 21 | * Usage: 22 | * ```js 23 | * const myFunctionReference = api.myModule.myFunction; 24 | * ``` 25 | */ 26 | declare const fullApi: ApiFromModules<{}>; 27 | export declare const api: FilterApi< 28 | typeof fullApi, 29 | FunctionReference 30 | >; 31 | export declare const internal: FilterApi< 32 | typeof fullApi, 33 | FunctionReference 34 | >; 35 | -------------------------------------------------------------------------------- /step-0/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /step-0/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { AnyDataModel } from "convex/server"; 13 | import type { GenericId } from "convex/values"; 14 | 15 | /** 16 | * No `schema.ts` file found! 17 | * 18 | * This generated code has permissive types like `Doc = any` because 19 | * Convex doesn't know your schema. If you'd like more type safety, see 20 | * https://docs.convex.dev/using/schemas for instructions on how to add a 21 | * schema file. 22 | * 23 | * After you change a schema, rerun codegen with `npx convex dev`. 24 | */ 25 | 26 | /** 27 | * The names of all of your Convex tables. 28 | */ 29 | export type TableNames = string; 30 | 31 | /** 32 | * The type of a document stored in Convex. 33 | */ 34 | export type Doc = any; 35 | 36 | /** 37 | * An identifier for a document in Convex. 38 | * 39 | * Convex documents are uniquely identified by their `Id`, which is accessible 40 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 41 | * 42 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 43 | * 44 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 45 | * strings when type checking. 46 | */ 47 | export type Id = 48 | GenericId; 49 | 50 | /** 51 | * A type describing your Convex data model. 52 | * 53 | * This type includes information about what tables you have, the type of 54 | * documents stored in those tables, and the indexes defined on them. 55 | * 56 | * This type is used to parameterize methods like `queryGeneric` and 57 | * `mutationGeneric` to make them type-safe. 58 | */ 59 | export type DataModel = AnyDataModel; 60 | -------------------------------------------------------------------------------- /step-0/convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | ActionCtx as GenericActionCtx, 18 | MutationCtx as GenericMutationCtx, 19 | QueryCtx as GenericQueryCtx, 20 | DatabaseReader as GenericDatabaseReader, 21 | DatabaseWriter as GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder<"public">; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder<"internal">; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /step-0/convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /step-0/convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /step-0/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AI Storybook 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /step-0/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-storybook", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "format": "npx prettier --write .", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "convex": "^1.0.2", 15 | "daisyui": "^2.51.6", 16 | "langchain": "^0.0.88", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "replicate": "^0.9.3", 20 | "typeorm": "^0.3.16", 21 | "typescript": "^5.0.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20.2.3", 25 | "@types/react": "^18.0.28", 26 | "@types/react-dom": "^18.0.11", 27 | "@vitejs/plugin-react": "^4.0.0", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.38.0", 30 | "eslint-plugin-react": "^7.32.2", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-react-refresh": "^0.3.4", 33 | "postcss": "^8.4.23", 34 | "prettier": "^2.8.8", 35 | "tailwindcss": "^3.3.2", 36 | "vite": "^4.3.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /step-0/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /step-0/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /step-0/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .lds-ring { 6 | display: inline-block; 7 | position: relative; 8 | width: 80px; 9 | height: 80px; 10 | } 11 | .lds-ring div { 12 | box-sizing: border-box; 13 | display: block; 14 | position: absolute; 15 | width: 64px; 16 | height: 64px; 17 | margin: 8px; 18 | border: 8px solid #fff; 19 | border-radius: 50%; 20 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 21 | border-color: #999 transparent transparent transparent; 22 | } 23 | .lds-ring div:nth-child(1) { 24 | animation-delay: -0.45s; 25 | } 26 | .lds-ring div:nth-child(2) { 27 | animation-delay: -0.3s; 28 | } 29 | .lds-ring div:nth-child(3) { 30 | animation-delay: -0.15s; 31 | } 32 | @keyframes lds-ring { 33 | 0% { 34 | transform: rotate(0deg); 35 | } 36 | 100% { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-0/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useRef, useState } from "react"; 2 | import "./App.css"; 3 | 4 | function App() { 5 | const [count, setCount] = useState(0); 6 | 7 | return ( 8 | <> 9 |
10 |
11 |
12 | 📖 Collaborative AI Storybook 13 |
14 | 15 |
16 |
17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | 23 | type Page = { 24 | content: string; 25 | image: { 26 | url: string; 27 | prompt: string; 28 | }; 29 | }; 30 | 31 | type BookData = { 32 | pages: Page[]; 33 | addPage: () => void; 34 | updatePage: (pageNumber: number, content: string) => void; 35 | setEditState: (editState: EditState | null) => void; 36 | editState: EditState | null; 37 | }; 38 | 39 | type EditState = { 40 | page: number; 41 | content: string; 42 | }; 43 | 44 | const BookContext = React.createContext(null as null | BookData); 45 | 46 | const PictureBook = () => { 47 | //const updateChapter = useMutation("chapters:updateChapterContents"); 48 | //const [pages, _version] = useQuery("chapters:getBookState") ?? [undefined, 0]; 49 | const [pages, setPages] = useState([ 50 | { 51 | content: "this is page one", 52 | image: null, 53 | }, 54 | { 55 | content: "this is page two", 56 | image: null, 57 | }, 58 | ]); 59 | const updateChapter = async ({ pageNumber, content }) => { 60 | let localPages = [...pages]; 61 | if (pageNumber === localPages.length) { 62 | localPages.push({ content: content, image: null }); 63 | } else { 64 | localPages[pageNumber].content = content; 65 | } 66 | setPages(localPages); 67 | }; 68 | 69 | const addPage = () => { 70 | (async () => { 71 | await updateChapter({ pageNumber: pages!.length, content: "" }); 72 | })(); 73 | }; 74 | useEffect(() => { 75 | if (pages !== undefined && pages.length === 0) { 76 | addPage(); 77 | } 78 | }, [addPage, pages]); 79 | const updatePage = (pageNumber, content) => { 80 | (async () => { 81 | await updateChapter({ pageNumber, content }); 82 | })(); 83 | }; 84 | const [editState, setEditState] = useState(null as null | EditState); 85 | return ( 86 |
87 | {pages && ( 88 | 91 |
92 | {pages.map((_p, idx) => ( 93 |
94 | 95 |
96 | ))} 97 |
98 |
99 | )} 100 |
101 | ); 102 | }; 103 | 104 | const Page = ({ 105 | pageNumber, 106 | isLast, 107 | }: { 108 | pageNumber: number; 109 | isLast: boolean; 110 | }) => { 111 | const book = useContext(BookContext)!; 112 | return ( 113 |
114 | } 116 | right={} 117 | pageNumber={pageNumber} 118 | isLast={isLast} 119 | /> 120 |
121 | ); 122 | }; 123 | 124 | const Divider = ({ left, right, pageNumber, isLast }) => { 125 | const book = useContext(BookContext)!; 126 | return ( 127 | <> 128 |
129 |
{left}
130 |
131 |
132 | {right} 133 |
134 |
135 |
136 | Page {pageNumber + 1} of {book.pages.length} 137 | {isLast && ( 138 | 147 | )} 148 |
149 | 150 | ); 151 | }; 152 | 153 | const EditArea = ({ pageNumber }: { pageNumber: number }) => { 154 | const book = useContext(BookContext)!; 155 | const storedContent = book.pages[pageNumber].content; 156 | const makeMeEditable = () => { 157 | if (book.editState === null) { 158 | book.setEditState({ 159 | page: pageNumber, 160 | content: storedContent, 161 | }); 162 | } 163 | }; 164 | const disableEdit = () => { 165 | book.setEditState(null); 166 | }; 167 | const disableDebouncer = useRef(debounce(disableEdit, 5000)); 168 | const editPage = content => { 169 | book.updatePage(pageNumber, content); 170 | }; 171 | const updateDebouncer = useRef(debounce(editPage, 500)); 172 | const checkForEnter = e => { 173 | if (e.keyCode === 13) { 174 | disableDebouncer.current.cancel(); 175 | disableEdit(); 176 | updateDebouncer.current.cancel(); 177 | editPage(e.target.value.replace(/\n/, "")); 178 | e.preventDefault(); 179 | } 180 | }; 181 | if (book.editState?.page === pageNumber) { 182 | disableDebouncer.current.call(); 183 | return ( 184 | 193 | ); 194 | } else { 195 | return ( 196 |

{ 199 | makeMeEditable(); 200 | }} 201 | > 202 | {storedContent} 203 |

204 | ); 205 | } 206 | }; 207 | 208 | const Illustration = ({ pageNumber }: { pageNumber: number }) => { 209 | const book = useContext(BookContext)!; 210 | const ourEntry = book.pages[pageNumber]; 211 | if (ourEntry.image === null) { 212 | return ( 213 | <> 214 |
215 | 216 |
217 | 218 | 219 | ); 220 | } else { 221 | return ( 222 | <> 223 |
224 | 229 |
230 | 231 | 232 | ); 233 | } 234 | }; 235 | 236 | const Regenerate = ({ pageNumber }) => { 237 | // const regenerate = useMutation("chapters:regenerateImage"); 238 | const regenerate = ({ pageNumber }) => alert("not implemented"); 239 | return ( 240 | 250 | ); 251 | }; 252 | 253 | function debounce(func, timeout = 300) { 254 | let timer; 255 | return { 256 | call: (...args) => { 257 | clearTimeout(timer); 258 | timer = setTimeout(() => { 259 | func.apply(this, args); 260 | }, timeout); 261 | }, 262 | cancel: () => { 263 | clearTimeout(timer); 264 | }, 265 | }; 266 | } 267 | 268 | const Spinner = () => ( 269 |
270 |
271 |
272 |
273 |
274 |
275 | ); 276 | -------------------------------------------------------------------------------- /step-0/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/ai-storybook/cfae40a1179dc298d3ad97f680fe10ddf963fe01/step-0/src/index.css -------------------------------------------------------------------------------- /step-0/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | //import { ConvexProvider, ConvexReactClient } from "convex/react"; 6 | 7 | //const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /step-0/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{html,js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | }; 9 | -------------------------------------------------------------------------------- /step-0/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* These settings are not required by Convex and can be modified. */ 4 | "allowJs": true, 5 | "jsx": "react-jsx", 6 | "target": "ESNext", 7 | "lib": ["ES2021", "dom"], 8 | "forceConsistentCasingInFileNames": true, 9 | "allowSyntheticDefaultImports": true, 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "isolatedModules": true, 13 | "noEmit": true 14 | }, 15 | "include": ["./src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /step-0/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /step-1/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | settings: { react: { version: "18.2" } }, 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": "warn", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /step-1/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.env 26 | convex.json 27 | -------------------------------------------------------------------------------- /step-1/.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | package-lock.json -------------------------------------------------------------------------------- /step-1/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | proseWrap: "always", 3 | arrowParens: "avoid", 4 | }; 5 | -------------------------------------------------------------------------------- /step-1/convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /step-1/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as chapters from "../chapters"; 18 | 19 | /** 20 | * A utility for referencing Convex functions in your app's API. 21 | * 22 | * Usage: 23 | * ```js 24 | * const myFunctionReference = api.myModule.myFunction; 25 | * ``` 26 | */ 27 | declare const fullApi: ApiFromModules<{ 28 | chapters: typeof chapters; 29 | }>; 30 | export declare const api: FilterApi< 31 | typeof fullApi, 32 | FunctionReference 33 | >; 34 | export declare const internal: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | >; 38 | -------------------------------------------------------------------------------- /step-1/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /step-1/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { DataModelFromSchemaDefinition } from "convex/server"; 13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server"; 14 | import type { GenericId } from "convex/values"; 15 | import schema from "../schema"; 16 | 17 | /** 18 | * The names of all of your Convex tables. 19 | */ 20 | export type TableNames = TableNamesInDataModel; 21 | 22 | /** 23 | * The type of a document stored in Convex. 24 | * 25 | * @typeParam TableName - A string literal type of the table name (like "users"). 26 | */ 27 | export type Doc = DocumentByName< 28 | DataModel, 29 | TableName 30 | >; 31 | 32 | /** 33 | * An identifier for a document in Convex. 34 | * 35 | * Convex documents are uniquely identified by their `Id`, which is accessible 36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 37 | * 38 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 39 | * 40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 41 | * strings when type checking. 42 | * 43 | * @typeParam TableName - A string literal type of the table name (like "users"). 44 | */ 45 | export type Id = GenericId; 46 | 47 | /** 48 | * A type describing your Convex data model. 49 | * 50 | * This type includes information about what tables you have, the type of 51 | * documents stored in those tables, and the indexes defined on them. 52 | * 53 | * This type is used to parameterize methods like `queryGeneric` and 54 | * `mutationGeneric` to make them type-safe. 55 | */ 56 | export type DataModel = DataModelFromSchemaDefinition; 57 | -------------------------------------------------------------------------------- /step-1/convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | ActionCtx as GenericActionCtx, 18 | MutationCtx as GenericMutationCtx, 19 | QueryCtx as GenericQueryCtx, 20 | DatabaseReader as GenericDatabaseReader, 21 | DatabaseWriter as GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder<"public">; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder<"internal">; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /step-1/convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /step-1/convex/chapters.ts: -------------------------------------------------------------------------------- 1 | import { Doc } from "./_generated/dataModel"; 2 | import { mutation, query } from "./_generated/server"; 3 | 4 | export const updateChapterContents = mutation( 5 | async ( 6 | { db }, 7 | { pageNumber, content }: { pageNumber: number; content: string } 8 | ) => { 9 | let existing = await db 10 | .query("chapters") 11 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 12 | .first(); 13 | if (existing !== null) { 14 | await db.patch(existing._id, { 15 | content, 16 | }); 17 | } else { 18 | await db.insert("chapters", { 19 | pageNumber, 20 | content, 21 | image: null, 22 | }); 23 | } 24 | } 25 | ); 26 | 27 | export const getBookState = query( 28 | async ({ db }): Promise[]> => { 29 | const pages = await db 30 | .query("chapters") 31 | .withIndex("by_pageNumber") 32 | .collect(); 33 | return pages; 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /step-1/convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | chapters: defineTable({ 6 | pageNumber: v.number(), 7 | content: v.string(), 8 | image: v.null(), 9 | }).index("by_pageNumber", ["pageNumber"]), 10 | }); 11 | -------------------------------------------------------------------------------- /step-1/convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /step-1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AI Storybook 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /step-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-storybook", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "npm-run-all dev:init --parallel dev:backend dev:frontend", 8 | "build": "tsc && vite build", 9 | "dev:backend": "convex dev", 10 | "dev:frontend": "vite --open --clearScreen false", 11 | "dev:init": "convex dev --once" 12 | }, 13 | "dependencies": { 14 | "convex": "^1.0.2", 15 | "daisyui": "^2.51.6", 16 | "langchain": "^0.0.88", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "replicate": "^0.9.3", 20 | "typeorm": "^0.3.16", 21 | "typescript": "^5.0.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20.2.3", 25 | "@types/react": "^18.0.28", 26 | "@types/react-dom": "^18.0.11", 27 | "@vitejs/plugin-react": "^4.0.0", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.38.0", 30 | "eslint-plugin-react": "^7.32.2", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-react-refresh": "^0.3.4", 33 | "npm-run-all": "^4.1.5", 34 | "postcss": "^8.4.23", 35 | "prettier": "^2.8.8", 36 | "tailwindcss": "^3.3.2", 37 | "vite": "^4.3.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-1/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /step-1/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /step-1/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .lds-ring { 6 | display: inline-block; 7 | position: relative; 8 | width: 80px; 9 | height: 80px; 10 | } 11 | .lds-ring div { 12 | box-sizing: border-box; 13 | display: block; 14 | position: absolute; 15 | width: 64px; 16 | height: 64px; 17 | margin: 8px; 18 | border: 8px solid #fff; 19 | border-radius: 50%; 20 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 21 | border-color: #999 transparent transparent transparent; 22 | } 23 | .lds-ring div:nth-child(1) { 24 | animation-delay: -0.45s; 25 | } 26 | .lds-ring div:nth-child(2) { 27 | animation-delay: -0.3s; 28 | } 29 | .lds-ring div:nth-child(3) { 30 | animation-delay: -0.15s; 31 | } 32 | @keyframes lds-ring { 33 | 0% { 34 | transform: rotate(0deg); 35 | } 36 | 100% { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-1/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useRef, useState } from "react"; 2 | import { Doc } from "../convex/_generated/dataModel"; 3 | import { api } from "../convex/_generated/api"; 4 | import { useMutation, useQuery } from "convex/react"; 5 | import "./App.css"; 6 | 7 | function App() { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | <> 12 |
13 |
14 |
15 | 📖 Collaborative AI Storybook 16 |
17 | 18 |
19 |
20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | 26 | type BookData = { 27 | pages: Doc<"chapters">[]; 28 | addPage: () => void; 29 | updatePage: (pageNumber: number, content: string) => void; 30 | setEditState: (editState: EditState | null) => void; 31 | editState: EditState | null; 32 | }; 33 | 34 | type EditState = { 35 | page: number; 36 | content: string; 37 | }; 38 | 39 | const BookContext = React.createContext(null as null | BookData); 40 | 41 | const PictureBook = () => { 42 | const pages = useQuery(api.chapters.getBookState); 43 | const updateChapter = useMutation(api.chapters.updateChapterContents); 44 | 45 | const addPage = () => { 46 | (async () => { 47 | await updateChapter({ pageNumber: pages!.length, content: "" }); 48 | })(); 49 | }; 50 | useEffect(() => { 51 | if (pages !== undefined && pages.length === 0) { 52 | addPage(); 53 | } 54 | }, [addPage, pages]); 55 | const updatePage = (pageNumber, content) => { 56 | (async () => { 57 | await updateChapter({ pageNumber, content }); 58 | })(); 59 | }; 60 | const [editState, setEditState] = useState(null as null | EditState); 61 | return ( 62 |
63 | {pages && ( 64 | 67 |
68 | {pages.map((_p, idx) => ( 69 |
70 | 71 |
72 | ))} 73 |
74 |
75 | )} 76 |
77 | ); 78 | }; 79 | 80 | const Page = ({ 81 | pageNumber, 82 | isLast, 83 | }: { 84 | pageNumber: number; 85 | isLast: boolean; 86 | }) => { 87 | const book = useContext(BookContext)!; 88 | return ( 89 |
90 | } 92 | right={} 93 | pageNumber={pageNumber} 94 | isLast={isLast} 95 | /> 96 |
97 | ); 98 | }; 99 | 100 | const Divider = ({ left, right, pageNumber, isLast }) => { 101 | const book = useContext(BookContext)!; 102 | return ( 103 | <> 104 |
105 |
{left}
106 |
107 |
108 | {right} 109 |
110 |
111 |
112 | Page {pageNumber + 1} of {book.pages.length} 113 | {isLast && ( 114 | 123 | )} 124 |
125 | 126 | ); 127 | }; 128 | 129 | const EditArea = ({ pageNumber }: { pageNumber: number }) => { 130 | const book = useContext(BookContext)!; 131 | const storedContent = book.pages[pageNumber].content; 132 | const makeMeEditable = () => { 133 | if (book.editState === null) { 134 | book.setEditState({ 135 | page: pageNumber, 136 | content: storedContent, 137 | }); 138 | } 139 | }; 140 | const disableEdit = () => { 141 | book.setEditState(null); 142 | }; 143 | const disableDebouncer = useRef(debounce(disableEdit, 5000)); 144 | const editPage = content => { 145 | book.updatePage(pageNumber, content); 146 | }; 147 | const updateDebouncer = useRef(debounce(editPage, 500)); 148 | const checkForEnter = e => { 149 | if (e.keyCode === 13) { 150 | disableDebouncer.current.cancel(); 151 | disableEdit(); 152 | updateDebouncer.current.cancel(); 153 | editPage(e.target.value.replace(/\n/, "")); 154 | e.preventDefault(); 155 | } 156 | }; 157 | if (book.editState?.page === pageNumber) { 158 | disableDebouncer.current.call(); 159 | return ( 160 | 169 | ); 170 | } else { 171 | return ( 172 |

{ 175 | makeMeEditable(); 176 | }} 177 | > 178 | {storedContent} 179 |

180 | ); 181 | } 182 | }; 183 | 184 | const Illustration = ({ pageNumber }: { pageNumber: number }) => { 185 | const book = useContext(BookContext)!; 186 | const ourEntry = book.pages[pageNumber]!; 187 | if (ourEntry.image === null) { 188 | return ( 189 | <> 190 |
191 | 192 |
193 | 194 | 195 | ); 196 | } else { 197 | return ( 198 | <> 199 |
200 | 205 |
206 | 207 | 208 | ); 209 | } 210 | }; 211 | 212 | const Regenerate = ({ pageNumber }) => { 213 | // const regenerate = useMutation("chapters:regenerateImage"); 214 | const regenerate = ({ pageNumber }) => alert("not implemented"); 215 | return ( 216 | 226 | ); 227 | }; 228 | 229 | function debounce(func, timeout = 300) { 230 | let timer; 231 | return { 232 | call: (...args) => { 233 | clearTimeout(timer); 234 | timer = setTimeout(() => { 235 | func.apply(this, args); 236 | }, timeout); 237 | }, 238 | cancel: () => { 239 | clearTimeout(timer); 240 | }, 241 | }; 242 | } 243 | 244 | const Spinner = () => ( 245 |
246 |
247 |
248 |
249 |
250 |
251 | ); 252 | -------------------------------------------------------------------------------- /step-1/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/ai-storybook/cfae40a1179dc298d3ad97f680fe10ddf963fe01/step-1/src/index.css -------------------------------------------------------------------------------- /step-1/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 6 | 7 | const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /step-1/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{html,js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | }; 9 | -------------------------------------------------------------------------------- /step-1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* These settings are not required by Convex and can be modified. */ 4 | "allowJs": true, 5 | "jsx": "react-jsx", 6 | "target": "ESNext", 7 | "lib": ["ES2021", "dom"], 8 | "forceConsistentCasingInFileNames": true, 9 | "allowSyntheticDefaultImports": true, 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "isolatedModules": true, 13 | "noEmit": true 14 | }, 15 | "include": ["./src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /step-1/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /step-2/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | settings: { react: { version: "18.2" } }, 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": "warn", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /step-2/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.env 26 | convex.json 27 | -------------------------------------------------------------------------------- /step-2/.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | package-lock.json -------------------------------------------------------------------------------- /step-2/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | proseWrap: "always", 3 | arrowParens: "avoid", 4 | }; 5 | -------------------------------------------------------------------------------- /step-2/convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /step-2/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as ai from "../ai"; 18 | import type * as chapters from "../chapters"; 19 | 20 | /** 21 | * A utility for referencing Convex functions in your app's API. 22 | * 23 | * Usage: 24 | * ```js 25 | * const myFunctionReference = api.myModule.myFunction; 26 | * ``` 27 | */ 28 | declare const fullApi: ApiFromModules<{ 29 | ai: typeof ai; 30 | chapters: typeof chapters; 31 | }>; 32 | export declare const api: FilterApi< 33 | typeof fullApi, 34 | FunctionReference 35 | >; 36 | export declare const internal: FilterApi< 37 | typeof fullApi, 38 | FunctionReference 39 | >; 40 | -------------------------------------------------------------------------------- /step-2/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /step-2/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { DataModelFromSchemaDefinition } from "convex/server"; 13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server"; 14 | import type { GenericId } from "convex/values"; 15 | import schema from "../schema"; 16 | 17 | /** 18 | * The names of all of your Convex tables. 19 | */ 20 | export type TableNames = TableNamesInDataModel; 21 | 22 | /** 23 | * The type of a document stored in Convex. 24 | * 25 | * @typeParam TableName - A string literal type of the table name (like "users"). 26 | */ 27 | export type Doc = DocumentByName< 28 | DataModel, 29 | TableName 30 | >; 31 | 32 | /** 33 | * An identifier for a document in Convex. 34 | * 35 | * Convex documents are uniquely identified by their `Id`, which is accessible 36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 37 | * 38 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 39 | * 40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 41 | * strings when type checking. 42 | * 43 | * @typeParam TableName - A string literal type of the table name (like "users"). 44 | */ 45 | export type Id = GenericId; 46 | 47 | /** 48 | * A type describing your Convex data model. 49 | * 50 | * This type includes information about what tables you have, the type of 51 | * documents stored in those tables, and the indexes defined on them. 52 | * 53 | * This type is used to parameterize methods like `queryGeneric` and 54 | * `mutationGeneric` to make them type-safe. 55 | */ 56 | export type DataModel = DataModelFromSchemaDefinition; 57 | -------------------------------------------------------------------------------- /step-2/convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | ActionCtx as GenericActionCtx, 18 | MutationCtx as GenericMutationCtx, 19 | QueryCtx as GenericQueryCtx, 20 | DatabaseReader as GenericDatabaseReader, 21 | DatabaseWriter as GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder<"public">; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder<"internal">; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /step-2/convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /step-2/convex/ai.ts: -------------------------------------------------------------------------------- 1 | "use node"; 2 | import { internal } from "./_generated/api"; 3 | import { Doc } from "./_generated/dataModel"; 4 | import { action } from "./_generated/server"; 5 | import { Replicate } from "langchain/llms/replicate"; 6 | 7 | export const populatePageImage = action( 8 | async ( 9 | { runQuery, runMutation }, 10 | { pageNumber, version }: { pageNumber: number; version: number } 11 | ) => { 12 | console.log( 13 | `Hello from populatePageImage for page ${pageNumber} at book version ${version}` 14 | ); 15 | const [currentVersion, book] = await runQuery( 16 | internal.chapters.getBookStateWithVersion 17 | ); 18 | if (currentVersion !== version) { 19 | console.log("Outdated! Exiting."); 20 | return; 21 | } 22 | if (book[pageNumber].content.trim() === "") { 23 | return; 24 | } 25 | const [imageUrl, prompt] = await getPageImage(book, pageNumber); 26 | console.log(`Got a result! ${imageUrl}, ${prompt}`); 27 | await runMutation(internal.chapters.updateChapterImage, { 28 | pageNumber, 29 | version, 30 | imageUrl, 31 | prompt, 32 | }); 33 | } 34 | ); 35 | 36 | async function getPageImage( 37 | book: Doc<"chapters">[], 38 | pageNumber: number 39 | ): Promise<[string, string]> { 40 | const prompt = book[pageNumber].content; 41 | const imageModel = new Replicate({ 42 | model: 43 | "ai-forever/kandinsky-2:601eea49d49003e6ea75a11527209c4f510a93e2112c969d548fbb45b9c4f19f", 44 | apiKey: process.env.REPLICATE_API_KEY, 45 | input: { image_dimensions: "512x512" }, 46 | }); 47 | const response = await imageModel.call(prompt); 48 | return [response, prompt]; 49 | } 50 | -------------------------------------------------------------------------------- /step-2/convex/chapters.ts: -------------------------------------------------------------------------------- 1 | import { api } from "./_generated/api"; 2 | import { Doc } from "./_generated/dataModel"; 3 | import { 4 | DatabaseWriter, 5 | internalMutation, 6 | internalQuery, 7 | mutation, 8 | query, 9 | } from "./_generated/server"; 10 | 11 | async function getOrCreateVersion(db: DatabaseWriter): Promise> { 12 | const versionDoc = await db.query("version").first(); 13 | if (versionDoc !== null) { 14 | return versionDoc; 15 | } 16 | const id = await db.insert("version", { 17 | version: 0, 18 | }); 19 | return (await db.get(id))!; 20 | } 21 | 22 | async function bumpVersion(db: DatabaseWriter): Promise { 23 | const versionDoc = await getOrCreateVersion(db); 24 | const newVersion = versionDoc.version + 1; 25 | await db.patch(versionDoc._id, { 26 | version: newVersion, 27 | }); 28 | return newVersion; 29 | } 30 | 31 | export const updateChapterContents = mutation( 32 | async ( 33 | { db, scheduler }, 34 | { pageNumber, content }: { pageNumber: number; content: string } 35 | ) => { 36 | let existing = await db 37 | .query("chapters") 38 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 39 | .first(); 40 | if (existing !== null) { 41 | await db.patch(existing._id, { 42 | content, 43 | image: null, 44 | }); 45 | } else { 46 | await db.insert("chapters", { 47 | pageNumber, 48 | content, 49 | image: null, 50 | }); 51 | } 52 | const version = await bumpVersion(db); 53 | const pages = await db.query("chapters").collect(); 54 | for (let i = 0; i < pages.length; i++) { 55 | await scheduler.runAfter(5000, api.ai.populatePageImage, { 56 | pageNumber: i, 57 | version, 58 | }); 59 | } 60 | } 61 | ); 62 | 63 | export const getBookState = query( 64 | async ({ db }): Promise[]> => { 65 | const pages = await db 66 | .query("chapters") 67 | .withIndex("by_pageNumber") 68 | .collect(); 69 | return pages; 70 | } 71 | ); 72 | 73 | export const getBookStateWithVersion = internalQuery( 74 | async ({ db }): Promise<[number, Doc<"chapters">[]]> => { 75 | const pages = await db 76 | .query("chapters") 77 | .withIndex("by_pageNumber") 78 | .collect(); 79 | const versionDoc = await db.query("version").first(); 80 | return [versionDoc?.version ?? 0, pages]; 81 | } 82 | ); 83 | 84 | export const updateChapterImage = internalMutation( 85 | async ( 86 | { db }, 87 | { 88 | pageNumber, 89 | version, 90 | imageUrl, 91 | prompt, 92 | }: { 93 | pageNumber: number; 94 | version: number; 95 | imageUrl: string; 96 | prompt: string; 97 | } 98 | ) => { 99 | const versionDoc = await getOrCreateVersion(db); 100 | if (version === versionDoc.version) { 101 | // It's still the same version of the book. Let's go! 102 | const existing = await db 103 | .query("chapters") 104 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 105 | .first(); 106 | await db.patch(existing!._id, { 107 | image: { 108 | url: imageUrl, 109 | prompt, 110 | }, 111 | }); 112 | } else { 113 | console.log( 114 | "Not updating database. AI action was for outdated book version" 115 | ); 116 | } 117 | } 118 | ); 119 | 120 | export const regenerateImageForPage = mutation( 121 | async ({ db, scheduler }, { pageNumber }: { pageNumber: number }) => { 122 | const existing = await db 123 | .query("chapters") 124 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 125 | .first(); 126 | if (existing !== null) { 127 | await db.patch(existing._id, { 128 | image: null, 129 | }); 130 | const versionDoc = await getOrCreateVersion(db); 131 | const version = versionDoc.version; 132 | await scheduler.runAfter(0, api.ai.populatePageImage, { 133 | pageNumber, 134 | version, 135 | }); 136 | } 137 | } 138 | ); 139 | -------------------------------------------------------------------------------- /step-2/convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | chapters: defineTable({ 6 | pageNumber: v.number(), 7 | content: v.string(), 8 | image: v.union( 9 | v.null(), 10 | v.object({ 11 | url: v.string(), 12 | prompt: v.string(), 13 | }) 14 | ), 15 | }).index("by_pageNumber", ["pageNumber"]), 16 | version: defineTable({ 17 | version: v.number(), 18 | }), 19 | }); 20 | -------------------------------------------------------------------------------- /step-2/convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /step-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AI Storybook 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /step-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-storybook", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "npm-run-all dev:init --parallel dev:backend dev:frontend", 8 | "build": "tsc && vite build", 9 | "dev:backend": "convex dev", 10 | "dev:frontend": "vite --open --clearScreen false", 11 | "dev:init": "convex dev --once" 12 | }, 13 | "dependencies": { 14 | "convex": "^1.0.2", 15 | "daisyui": "^2.51.6", 16 | "langchain": "^0.0.88", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "replicate": "^0.9.3", 20 | "typeorm": "^0.3.16", 21 | "typescript": "^5.0.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20.2.3", 25 | "@types/react": "^18.0.28", 26 | "@types/react-dom": "^18.0.11", 27 | "@vitejs/plugin-react": "^4.0.0", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.38.0", 30 | "eslint-plugin-react": "^7.32.2", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-react-refresh": "^0.3.4", 33 | "npm-run-all": "^4.1.5", 34 | "postcss": "^8.4.23", 35 | "prettier": "^2.8.8", 36 | "tailwindcss": "^3.3.2", 37 | "vite": "^4.3.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-2/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /step-2/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /step-2/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .lds-ring { 6 | display: inline-block; 7 | position: relative; 8 | width: 80px; 9 | height: 80px; 10 | } 11 | .lds-ring div { 12 | box-sizing: border-box; 13 | display: block; 14 | position: absolute; 15 | width: 64px; 16 | height: 64px; 17 | margin: 8px; 18 | border: 8px solid #fff; 19 | border-radius: 50%; 20 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 21 | border-color: #999 transparent transparent transparent; 22 | } 23 | .lds-ring div:nth-child(1) { 24 | animation-delay: -0.45s; 25 | } 26 | .lds-ring div:nth-child(2) { 27 | animation-delay: -0.3s; 28 | } 29 | .lds-ring div:nth-child(3) { 30 | animation-delay: -0.15s; 31 | } 32 | @keyframes lds-ring { 33 | 0% { 34 | transform: rotate(0deg); 35 | } 36 | 100% { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-2/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "../convex/_generated/api"; 2 | import { useMutation, useQuery } from "convex/react"; 3 | import React, { 4 | useCallback, 5 | useContext, 6 | useEffect, 7 | useRef, 8 | useState, 9 | } from "react"; 10 | import { Doc } from "../convex/_generated/dataModel"; 11 | import "./App.css"; 12 | 13 | function App() { 14 | const [count, setCount] = useState(0); 15 | 16 | return ( 17 | <> 18 |
19 |
20 |
21 | 📖 Collaborative AI Storybook 22 |
23 | 24 |
25 |
26 | 27 | ); 28 | } 29 | 30 | export default App; 31 | 32 | type BookData = { 33 | pages: Doc<"chapters">[]; 34 | addPage: () => void; 35 | updatePage: (pageNumber: number, content: string) => void; 36 | setEditState: (editState: EditState | null) => void; 37 | editState: EditState | null; 38 | }; 39 | 40 | type EditState = { 41 | page: number; 42 | content: string; 43 | }; 44 | 45 | const BookContext = React.createContext(null as null | BookData); 46 | 47 | const PictureBook = () => { 48 | const pages = useQuery(api.chapters.getBookState); 49 | const updateChapter = useMutation(api.chapters.updateChapterContents); 50 | 51 | const addPage = () => { 52 | (async () => { 53 | await updateChapter({ pageNumber: pages!.length, content: "" }); 54 | })(); 55 | }; 56 | useEffect(() => { 57 | if (pages !== undefined && pages.length === 0) { 58 | addPage(); 59 | } 60 | }, [addPage, pages]); 61 | const updatePage = useCallback( 62 | (pageNumber: number, content: string) => { 63 | (async () => { 64 | await updateChapter({ pageNumber, content }); 65 | })(); 66 | }, 67 | [updateChapter] 68 | ); 69 | const [editState, setEditState] = useState(null as null | EditState); 70 | return ( 71 |
72 | {pages && ( 73 | 76 |
77 | {pages.map((_p, idx) => ( 78 |
79 | 80 |
81 | ))} 82 |
83 |
84 | )} 85 |
86 | ); 87 | }; 88 | 89 | const Page = ({ 90 | pageNumber, 91 | isLast, 92 | }: { 93 | pageNumber: number; 94 | isLast: boolean; 95 | }) => { 96 | const book = useContext(BookContext)!; 97 | return ( 98 |
99 | } 101 | right={} 102 | pageNumber={pageNumber} 103 | isLast={isLast} 104 | /> 105 |
106 | ); 107 | }; 108 | 109 | const Divider = ({ left, right, pageNumber, isLast }) => { 110 | const book = useContext(BookContext)!; 111 | return ( 112 | <> 113 |
114 |
{left}
115 |
116 |
117 | {right} 118 |
119 |
120 |
121 | Page {pageNumber + 1} of {book.pages.length} 122 | {isLast && ( 123 | 132 | )} 133 |
134 | 135 | ); 136 | }; 137 | 138 | const EditArea = ({ pageNumber }: { pageNumber: number }) => { 139 | const book = useContext(BookContext)!; 140 | const storedContent = book.pages[pageNumber].content; 141 | const makeMeEditable = () => { 142 | if (book.editState === null) { 143 | book.setEditState({ 144 | page: pageNumber, 145 | content: storedContent, 146 | }); 147 | } 148 | }; 149 | const disableEdit = () => { 150 | book.setEditState(null); 151 | }; 152 | const disableDebouncer = useRef(debounce(disableEdit, 5000)); 153 | const editPage = content => { 154 | book.updatePage(pageNumber, content); 155 | }; 156 | const updateDebouncer = useRef(debounce(editPage, 500)); 157 | const checkForEnter = e => { 158 | if (e.keyCode === 13) { 159 | disableDebouncer.current.cancel(); 160 | disableEdit(); 161 | updateDebouncer.current.cancel(); 162 | editPage(e.target.value.replace(/\n/, "")); 163 | e.preventDefault(); 164 | } 165 | }; 166 | if (book.editState?.page === pageNumber) { 167 | disableDebouncer.current.call(); 168 | return ( 169 | 178 | ); 179 | } else { 180 | return ( 181 |

{ 184 | makeMeEditable(); 185 | }} 186 | > 187 | {storedContent} 188 |

189 | ); 190 | } 191 | }; 192 | 193 | const Illustration = ({ pageNumber }: { pageNumber: number }) => { 194 | const book = useContext(BookContext)!; 195 | const ourEntry = book.pages[pageNumber]!; 196 | if (ourEntry.image === null) { 197 | return ( 198 | <> 199 |
200 | 201 |
202 | 203 | 204 | ); 205 | } else { 206 | return ( 207 | <> 208 |
209 | 214 |
215 | 216 | 217 | ); 218 | } 219 | }; 220 | 221 | const Regenerate = ({ pageNumber }) => { 222 | const backendRegenerate = useMutation(api.chapters.regenerateImageForPage); 223 | const regenerate = ({ pageNumber }) => { 224 | (async () => { 225 | await backendRegenerate({ pageNumber }); 226 | })(); 227 | }; 228 | return ( 229 | 235 | ); 236 | }; 237 | 238 | function debounce(func, timeout = 300) { 239 | let timer; 240 | return { 241 | call: (...args) => { 242 | clearTimeout(timer); 243 | timer = setTimeout(() => { 244 | func.apply(this, args); 245 | }, timeout); 246 | }, 247 | cancel: () => { 248 | clearTimeout(timer); 249 | }, 250 | }; 251 | } 252 | 253 | const Spinner = () => ( 254 |
255 |
256 |
257 |
258 |
259 |
260 | ); 261 | -------------------------------------------------------------------------------- /step-2/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/ai-storybook/cfae40a1179dc298d3ad97f680fe10ddf963fe01/step-2/src/index.css -------------------------------------------------------------------------------- /step-2/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 6 | 7 | const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /step-2/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{html,js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | }; 9 | -------------------------------------------------------------------------------- /step-2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* These settings are not required by Convex and can be modified. */ 4 | "allowJs": true, 5 | "jsx": "react-jsx", 6 | "target": "ESNext", 7 | "lib": ["ES2021", "dom"], 8 | "forceConsistentCasingInFileNames": true, 9 | "allowSyntheticDefaultImports": true, 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "isolatedModules": true, 13 | "noEmit": true 14 | }, 15 | "include": ["./src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /step-2/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | -------------------------------------------------------------------------------- /step-3/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 10 | settings: { react: { version: "18.2" } }, 11 | plugins: ["react-refresh"], 12 | rules: { 13 | "react-refresh/only-export-components": "warn", 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /step-3/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.env 26 | convex.json 27 | -------------------------------------------------------------------------------- /step-3/.prettierignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | package-lock.json -------------------------------------------------------------------------------- /step-3/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | proseWrap: "always", 3 | arrowParens: "avoid", 4 | }; 5 | -------------------------------------------------------------------------------- /step-3/convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. See 4 | https://docs.convex.dev/using/writing-convex-functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | hander: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | hander: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /step-3/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as ai from "../ai"; 18 | import type * as chapters from "../chapters"; 19 | 20 | /** 21 | * A utility for referencing Convex functions in your app's API. 22 | * 23 | * Usage: 24 | * ```js 25 | * const myFunctionReference = api.myModule.myFunction; 26 | * ``` 27 | */ 28 | declare const fullApi: ApiFromModules<{ 29 | ai: typeof ai; 30 | chapters: typeof chapters; 31 | }>; 32 | export declare const api: FilterApi< 33 | typeof fullApi, 34 | FunctionReference 35 | >; 36 | export declare const internal: FilterApi< 37 | typeof fullApi, 38 | FunctionReference 39 | >; 40 | -------------------------------------------------------------------------------- /step-3/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /step-3/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { DataModelFromSchemaDefinition } from "convex/server"; 13 | import type { DocumentByName, TableNamesInDataModel } from "convex/server"; 14 | import type { GenericId } from "convex/values"; 15 | import schema from "../schema"; 16 | 17 | /** 18 | * The names of all of your Convex tables. 19 | */ 20 | export type TableNames = TableNamesInDataModel; 21 | 22 | /** 23 | * The type of a document stored in Convex. 24 | * 25 | * @typeParam TableName - A string literal type of the table name (like "users"). 26 | */ 27 | export type Doc = DocumentByName< 28 | DataModel, 29 | TableName 30 | >; 31 | 32 | /** 33 | * An identifier for a document in Convex. 34 | * 35 | * Convex documents are uniquely identified by their `Id`, which is accessible 36 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 37 | * 38 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 39 | * 40 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 41 | * strings when type checking. 42 | * 43 | * @typeParam TableName - A string literal type of the table name (like "users"). 44 | */ 45 | export type Id = GenericId; 46 | 47 | /** 48 | * A type describing your Convex data model. 49 | * 50 | * This type includes information about what tables you have, the type of 51 | * documents stored in those tables, and the indexes defined on them. 52 | * 53 | * This type is used to parameterize methods like `queryGeneric` and 54 | * `mutationGeneric` to make them type-safe. 55 | */ 56 | export type DataModel = DataModelFromSchemaDefinition; 57 | -------------------------------------------------------------------------------- /step-3/convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | ActionCtx as GenericActionCtx, 18 | MutationCtx as GenericMutationCtx, 19 | QueryCtx as GenericQueryCtx, 20 | DatabaseReader as GenericDatabaseReader, 21 | DatabaseWriter as GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder<"public">; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder<"internal">; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /step-3/convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.0.2. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /step-3/convex/ai.ts: -------------------------------------------------------------------------------- 1 | "use node"; 2 | import { internal } from "./_generated/api"; 3 | 4 | import { LLMChain, PromptTemplate } from "langchain"; 5 | import { Doc } from "./_generated/dataModel"; 6 | import { action } from "./_generated/server"; 7 | import { Replicate } from "langchain/llms/replicate"; 8 | import { ChatOpenAI } from "langchain/chat_models/openai"; 9 | import { 10 | ChatPromptTemplate, 11 | HumanMessagePromptTemplate, 12 | SystemMessagePromptTemplate, 13 | } from "langchain/prompts"; 14 | import { SequentialChain } from "langchain/chains"; 15 | import { v } from "convex/values"; 16 | 17 | export const populatePageImage = action({ 18 | args: { pageNumber: v.number(), version: v.number() }, 19 | handler: async (ctx, { pageNumber, version }) => { 20 | console.log( 21 | `Hello from populatePageImage for page ${pageNumber} at book version ${version}` 22 | ); 23 | const [currentVersion, book] = await ctx.runQuery( 24 | internal.chapters.getBookStateWithVersion 25 | ); 26 | if (currentVersion !== version) { 27 | console.log("Outdated! Exiting."); 28 | return; 29 | } 30 | if (book[pageNumber].content.trim() === "") { 31 | return; 32 | } 33 | const [prompt, imageUrl] = await getPageImage(book, pageNumber); 34 | console.log(`Got a result! ${imageUrl}, ${prompt}`); 35 | await ctx.runMutation(internal.chapters.updateChapterImage, { 36 | pageNumber, 37 | version, 38 | imageUrl, 39 | prompt, 40 | }); 41 | }, 42 | }); 43 | 44 | async function getPageImage( 45 | book: Doc<"chapters">[], 46 | pageNumber: number 47 | ): Promise<[string, string]> { 48 | const imageChain = getImageChain(); 49 | const summaryChain = getSummarizeChain(); 50 | const overallChain = new SequentialChain({ 51 | chains: [summaryChain, imageChain], 52 | inputVariables: ["bookParagraphs", "numPages"], 53 | outputVariables: ["imageUrl", "pageDescription"], 54 | }); 55 | 56 | const subPages = book.slice(0, pageNumber + 1); 57 | const bookParagraphs = subPages.map(p => p.content).join("\n\n"); 58 | const numPages = subPages.length; 59 | const response = await overallChain.call({ bookParagraphs, numPages }); 60 | return [response.pageDescription, response.imageUrl]; 61 | } 62 | 63 | function getImageChain(): LLMChain { 64 | const promptTemplate = new PromptTemplate({ 65 | template: 66 | "{pageDescription} in the style of a children's book illustration", 67 | inputVariables: ["pageDescription"], 68 | }); 69 | const imageModel = new Replicate({ 70 | model: 71 | "ai-forever/kandinsky-2:601eea49d49003e6ea75a11527209c4f510a93e2112c969d548fbb45b9c4f19f", 72 | apiKey: process.env.REPLICATE_API_KEY, 73 | input: { image_dimensions: "512x512" }, 74 | }); 75 | const imageChain = new LLMChain({ 76 | prompt: promptTemplate, 77 | llm: imageModel, 78 | outputKey: "imageUrl", 79 | }); 80 | return imageChain; 81 | } 82 | 83 | function getSummarizeChain(): LLMChain { 84 | const promptTemplate = ChatPromptTemplate.fromPromptMessages([ 85 | SystemMessagePromptTemplate.fromTemplate( 86 | `You are speaking to an eight year old. Provide your answer in 87 | simple language using at most one to two sentences.` 88 | ), 89 | HumanMessagePromptTemplate.fromTemplate( 90 | ` 91 | I'm going to tell you a story. Each paragraph is a page in a children's book. 92 | There are {numPages} paragraphs in total, representing {numPages} pages. 93 | 94 | Here is that story: 95 | 96 | {bookParagraphs} 97 | 98 | Can you please provide a brief description of the scene that occurs on 99 | on page {numPages} so that an illustrator can draw it? This illustrator 100 | has not read the book and so they do not have any context on the scenes, 101 | plot, or characters. Please include details about the appearance of the 102 | characters in this scene so that the illustrator can accurately 103 | represent them. 104 | ` 105 | ), 106 | ]); 107 | const summaryModel = new ChatOpenAI({ 108 | openAIApiKey: process.env.OPENAI_API_KEY, 109 | }); 110 | const summaryChain = new LLMChain({ 111 | prompt: promptTemplate, 112 | llm: summaryModel, 113 | outputKey: "pageDescription", 114 | }); 115 | return summaryChain; 116 | } 117 | -------------------------------------------------------------------------------- /step-3/convex/chapters.ts: -------------------------------------------------------------------------------- 1 | import { v } from "convex/values"; 2 | import { api } from "./_generated/api"; 3 | import { Doc } from "./_generated/dataModel"; 4 | import { 5 | DatabaseWriter, 6 | internalMutation, 7 | internalQuery, 8 | mutation, 9 | query, 10 | } from "./_generated/server"; 11 | 12 | async function getOrCreateVersion(db: DatabaseWriter): Promise> { 13 | const versionDoc = await db.query("version").first(); 14 | if (versionDoc !== null) { 15 | return versionDoc; 16 | } 17 | const id = await db.insert("version", { 18 | version: 0, 19 | }); 20 | return (await db.get(id))!; 21 | } 22 | 23 | async function bumpVersion(db: DatabaseWriter): Promise { 24 | const versionDoc = await getOrCreateVersion(db); 25 | const newVersion = versionDoc!.version + 1; 26 | await db.patch(versionDoc!._id, { 27 | version: newVersion, 28 | }); 29 | return newVersion; 30 | } 31 | 32 | export const updateChapterContents = mutation({ 33 | args: { pageNumber: v.number(), content: v.string() }, 34 | handler: async (ctx, { pageNumber, content }) => { 35 | let existing = await ctx.db 36 | .query("chapters") 37 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 38 | .first(); 39 | if (existing !== null) { 40 | await ctx.db.patch(existing._id, { 41 | content, 42 | }); 43 | } else { 44 | await ctx.db.insert("chapters", { 45 | pageNumber, 46 | content, 47 | image: null, 48 | }); 49 | } 50 | const version = await bumpVersion(ctx.db); 51 | const pages = await ctx.db.query("chapters").collect(); 52 | for (let i = 0; i < pages.length; i++) { 53 | await ctx.scheduler.runAfter(5000, api.ai.populatePageImage, { 54 | pageNumber: i, 55 | version, 56 | }); 57 | await ctx.db.patch(pages[i]._id, { 58 | image: null, 59 | }); 60 | } 61 | }, 62 | }); 63 | 64 | export const getBookState = query({ 65 | args: {}, 66 | handler: async (ctx): Promise[]> => { 67 | const pages = await ctx.db 68 | .query("chapters") 69 | .withIndex("by_pageNumber") 70 | .collect(); 71 | return pages; 72 | }, 73 | }); 74 | 75 | export const getBookStateWithVersion = internalQuery({ 76 | handler: async (ctx): Promise<[number, Doc<"chapters">[]]> => { 77 | const pages = await ctx.db 78 | .query("chapters") 79 | .withIndex("by_pageNumber") 80 | .collect(); 81 | const versionDoc = await ctx.db.query("version").first(); 82 | return [versionDoc?.version ?? 0, pages]; 83 | }, 84 | }); 85 | 86 | export const updateChapterImage = internalMutation({ 87 | handler: async ( 88 | ctx, 89 | { 90 | pageNumber, 91 | version, 92 | imageUrl, 93 | prompt, 94 | }: { 95 | pageNumber: number; 96 | version: number; 97 | imageUrl: string; 98 | prompt: string; 99 | } 100 | ) => { 101 | const versionDoc = await getOrCreateVersion(ctx.db); 102 | if (version === versionDoc!.version) { 103 | // It's still the same version of the book. Let's go! 104 | const existing = await ctx.db 105 | .query("chapters") 106 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 107 | .first(); 108 | await ctx.db.patch(existing!._id, { 109 | image: { 110 | url: imageUrl, 111 | prompt, 112 | }, 113 | }); 114 | } else { 115 | console.log( 116 | "Not updating database. AI action was for outdated book version" 117 | ); 118 | } 119 | }, 120 | }); 121 | 122 | export const regenerateImageForPage = mutation({ 123 | args: { pageNumber: v.number() }, 124 | handler: async (ctx, { pageNumber }) => { 125 | const existing = await ctx.db 126 | .query("chapters") 127 | .withIndex("by_pageNumber", q => q.eq("pageNumber", pageNumber)) 128 | .first(); 129 | if (existing !== null) { 130 | await ctx.db.patch(existing._id, { 131 | image: null, 132 | }); 133 | const versionDoc = await getOrCreateVersion(ctx.db); 134 | const version = versionDoc!.version; 135 | await ctx.scheduler.runAfter(0, api.ai.populatePageImage, { 136 | pageNumber, 137 | version, 138 | }); 139 | } 140 | }, 141 | }); 142 | -------------------------------------------------------------------------------- /step-3/convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | chapters: defineTable({ 6 | pageNumber: v.number(), 7 | content: v.string(), 8 | image: v.union( 9 | v.null(), 10 | v.object({ 11 | url: v.string(), 12 | prompt: v.string(), 13 | }) 14 | ), 15 | }).index("by_pageNumber", ["pageNumber"]), 16 | version: defineTable({ 17 | version: v.number(), 18 | }), 19 | }); 20 | -------------------------------------------------------------------------------- /step-3/convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "noEmit": true 20 | }, 21 | "include": ["./**/*"], 22 | "exclude": ["./_generated"] 23 | } 24 | -------------------------------------------------------------------------------- /step-3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AI Storybook 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /step-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-storybook", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "npm-run-all dev:init --parallel dev:backend dev:frontend", 8 | "build": "tsc && vite build", 9 | "dev:backend": "convex dev", 10 | "dev:frontend": "vite --open --clearScreen false", 11 | "dev:init": "convex dev --once" 12 | }, 13 | "dependencies": { 14 | "convex": "^1.0.2", 15 | "daisyui": "^2.51.6", 16 | "langchain": "^0.0.88", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "replicate": "^0.9.3", 20 | "typeorm": "^0.3.16", 21 | "typescript": "^5.0.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20.2.3", 25 | "@types/react": "^18.0.28", 26 | "@types/react-dom": "^18.0.11", 27 | "@vitejs/plugin-react": "^4.0.0", 28 | "autoprefixer": "^10.4.14", 29 | "eslint": "^8.38.0", 30 | "eslint-plugin-react": "^7.32.2", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-react-refresh": "^0.3.4", 33 | "npm-run-all": "^4.1.5", 34 | "postcss": "^8.4.23", 35 | "prettier": "^2.8.8", 36 | "tailwindcss": "^3.3.2", 37 | "vite": "^4.3.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-3/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /step-3/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /step-3/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .lds-ring { 6 | display: inline-block; 7 | position: relative; 8 | width: 80px; 9 | height: 80px; 10 | } 11 | .lds-ring div { 12 | box-sizing: border-box; 13 | display: block; 14 | position: absolute; 15 | width: 64px; 16 | height: 64px; 17 | margin: 8px; 18 | border: 8px solid #fff; 19 | border-radius: 50%; 20 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 21 | border-color: #999 transparent transparent transparent; 22 | } 23 | .lds-ring div:nth-child(1) { 24 | animation-delay: -0.45s; 25 | } 26 | .lds-ring div:nth-child(2) { 27 | animation-delay: -0.3s; 28 | } 29 | .lds-ring div:nth-child(3) { 30 | animation-delay: -0.15s; 31 | } 32 | @keyframes lds-ring { 33 | 0% { 34 | transform: rotate(0deg); 35 | } 36 | 100% { 37 | transform: rotate(360deg); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /step-3/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "../convex/_generated/api"; 2 | import { useMutation, useQuery } from "convex/react"; 3 | import React, { useContext, useEffect, useRef, useState } from "react"; 4 | import { Doc } from "../convex/_generated/dataModel"; 5 | import "./App.css"; 6 | 7 | function App() { 8 | const [count, setCount] = useState(0); 9 | 10 | return ( 11 | <> 12 |
13 |
14 |
15 | 📖 Collaborative AI Storybook 16 |
17 | 18 |
19 |
20 | 21 | ); 22 | } 23 | 24 | export default App; 25 | 26 | type BookData = { 27 | pages: Doc<"chapters">[]; 28 | addPage: () => void; 29 | updatePage: (pageNumber: number, content: string) => void; 30 | setEditState: (editState: EditState | null) => void; 31 | editState: EditState | null; 32 | }; 33 | 34 | type EditState = { 35 | page: number; 36 | content: string; 37 | }; 38 | 39 | const BookContext = React.createContext(null as null | BookData); 40 | 41 | const PictureBook = () => { 42 | const pages = useQuery(api.chapters.getBookState); 43 | const updateChapter = useMutation(api.chapters.updateChapterContents); 44 | 45 | const addPage = () => { 46 | (async () => { 47 | await updateChapter({ pageNumber: pages!.length, content: "" }); 48 | })(); 49 | }; 50 | useEffect(() => { 51 | if (pages !== undefined && pages.length === 0) { 52 | addPage(); 53 | } 54 | }, [addPage, pages]); 55 | const updatePage = (pageNumber, content) => { 56 | (async () => { 57 | await updateChapter({ pageNumber, content }); 58 | })(); 59 | }; 60 | const [editState, setEditState] = useState(null as null | EditState); 61 | return ( 62 |
63 | {pages && ( 64 | 67 |
68 | {pages.map((_p, idx) => ( 69 |
70 | 71 |
72 | ))} 73 |
74 |
75 | )} 76 |
77 | ); 78 | }; 79 | 80 | const Page = ({ 81 | pageNumber, 82 | isLast, 83 | }: { 84 | pageNumber: number; 85 | isLast: boolean; 86 | }) => { 87 | const book = useContext(BookContext)!; 88 | return ( 89 |
90 | } 92 | right={} 93 | pageNumber={pageNumber} 94 | isLast={isLast} 95 | /> 96 |
97 | ); 98 | }; 99 | 100 | const Divider = ({ left, right, pageNumber, isLast }) => { 101 | const book = useContext(BookContext)!; 102 | return ( 103 | <> 104 |
105 |
{left}
106 |
107 |
108 | {right} 109 |
110 |
111 |
112 | Page {pageNumber + 1} of {book.pages.length} 113 | {isLast && ( 114 | 123 | )} 124 |
125 | 126 | ); 127 | }; 128 | 129 | const EditArea = ({ pageNumber }: { pageNumber: number }) => { 130 | const book = useContext(BookContext)!; 131 | const storedContent = book.pages[pageNumber].content; 132 | const makeMeEditable = () => { 133 | if (book.editState === null) { 134 | book.setEditState({ 135 | page: pageNumber, 136 | content: storedContent, 137 | }); 138 | } 139 | }; 140 | const disableEdit = () => { 141 | book.setEditState(null); 142 | }; 143 | const disableDebouncer = useRef(debounce(disableEdit, 5000)); 144 | const editPage = content => { 145 | book.updatePage(pageNumber, content); 146 | }; 147 | const updateDebouncer = useRef(debounce(editPage, 500)); 148 | const checkForEnter = e => { 149 | if (e.keyCode === 13) { 150 | disableDebouncer.current.cancel(); 151 | disableEdit(); 152 | updateDebouncer.current.cancel(); 153 | editPage(e.target.value.replace(/\n/, "")); 154 | e.preventDefault(); 155 | } 156 | }; 157 | if (book.editState?.page === pageNumber) { 158 | disableDebouncer.current.call(); 159 | return ( 160 | 169 | ); 170 | } else { 171 | return ( 172 |

{ 175 | makeMeEditable(); 176 | }} 177 | > 178 | {storedContent} 179 |

180 | ); 181 | } 182 | }; 183 | 184 | const Illustration = ({ pageNumber }: { pageNumber: number }) => { 185 | const book = useContext(BookContext)!; 186 | const ourEntry = book.pages[pageNumber]!; 187 | if (ourEntry.image === null) { 188 | return ( 189 | <> 190 |
191 | 192 |
193 | 194 | 195 | ); 196 | } else { 197 | return ( 198 | <> 199 |
200 | 205 |
206 | 207 | 208 | ); 209 | } 210 | }; 211 | 212 | const Regenerate = ({ pageNumber }) => { 213 | const backendRegenerate = useMutation(api.chapters.regenerateImageForPage); 214 | const regenerate = ({ pageNumber }) => { 215 | (async () => { 216 | await backendRegenerate({ pageNumber }); 217 | })(); 218 | }; 219 | return ( 220 | 230 | ); 231 | }; 232 | 233 | function debounce(func, timeout = 300) { 234 | let timer; 235 | return { 236 | call: (...args) => { 237 | clearTimeout(timer); 238 | timer = setTimeout(() => { 239 | func.apply(this, args); 240 | }, timeout); 241 | }, 242 | cancel: () => { 243 | clearTimeout(timer); 244 | }, 245 | }; 246 | } 247 | 248 | const Spinner = () => ( 249 |
250 |
251 |
252 |
253 |
254 |
255 | ); 256 | -------------------------------------------------------------------------------- /step-3/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/get-convex/ai-storybook/cfae40a1179dc298d3ad97f680fe10ddf963fe01/step-3/src/index.css -------------------------------------------------------------------------------- /step-3/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App.tsx"; 4 | import "./index.css"; 5 | import { ConvexProvider, ConvexReactClient } from "convex/react"; 6 | 7 | const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL); 8 | 9 | ReactDOM.createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /step-3/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{html,js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [require("daisyui")], 8 | }; 9 | -------------------------------------------------------------------------------- /step-3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* These settings are not required by Convex and can be modified. */ 4 | "allowJs": true, 5 | "jsx": "react-jsx", 6 | "target": "ESNext", 7 | "lib": ["ES2021", "dom"], 8 | "forceConsistentCasingInFileNames": true, 9 | "allowSyntheticDefaultImports": true, 10 | "module": "ESNext", 11 | "moduleResolution": "Node", 12 | "isolatedModules": true, 13 | "noEmit": true 14 | }, 15 | "include": ["./src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /step-3/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------