diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 000000000..15546f4bd --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,65 @@ +name: Deploy MyST Documentation + +on: + push: + branches: [main] + paths: + - "tutorials/**" + - ".github/workflows/deploy-docs.yml" + workflow_dispatch: # Allow manual trigger + +env: + # For custom domain docs.commontools.dev, we don't need a subfolder + BASE_URL: "" + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: tutorials/package-lock.json + + - name: Install dependencies + working-directory: tutorials + run: npm ci + + - name: Build MyST site + working-directory: tutorials + run: npm run build + + - name: Copy CNAME to build directory + run: cp tutorials/CNAME tutorials/_build/html/CNAME + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "tutorials/_build/html" + + deploy: + # Only deploy from main branch + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CNAME b/CNAME new file mode 100644 index 000000000..f89f15423 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +docs.commontools.dev \ No newline at end of file diff --git a/deno.json b/deno.json index 83e8d4875..b4dd2f3c1 100644 --- a/deno.json +++ b/deno.json @@ -91,7 +91,8 @@ "packages/static/assets/", "packages/ts-transformers/test/fixtures", "packages/schema-generator/test/fixtures", - "packages/vendor-astral" + "packages/vendor-astral", + "tutorials/" ] }, "imports": { diff --git a/tutorials/.gitignore b/tutorials/.gitignore new file mode 100644 index 000000000..0197f8cd5 --- /dev/null +++ b/tutorials/.gitignore @@ -0,0 +1,7 @@ +# Node / tooling +node_modules/ +npm-debug.log* + +# Myst build output +_build/ +.cache/ diff --git a/tutorials/.nvmrc b/tutorials/.nvmrc new file mode 100644 index 000000000..3c032078a --- /dev/null +++ b/tutorials/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/tutorials/CNAME b/tutorials/CNAME new file mode 100644 index 000000000..f89f15423 --- /dev/null +++ b/tutorials/CNAME @@ -0,0 +1 @@ +docs.commontools.dev \ No newline at end of file diff --git a/tutorials/README.md b/tutorials/README.md new file mode 100644 index 000000000..5195fadc6 --- /dev/null +++ b/tutorials/README.md @@ -0,0 +1,14 @@ +# Tutorials + +Install MyST Markdown tooling by following https://mystmd.org/guide/installing. Once set up, run `myst` in this directory to serve the tutorial pages locally. + +## npm quickstart + +```sh +npm i +npm run dev +``` + +TODO: +* Add document on how Input and Output schemas work for Recipes +* Chapter on basic UI and existing ct components diff --git a/tutorials/README_BUILD.md b/tutorials/README_BUILD.md new file mode 100644 index 000000000..a7545b4df --- /dev/null +++ b/tutorials/README_BUILD.md @@ -0,0 +1,53 @@ +# Documentation Build Process + +This directory contains the MyST documentation that gets built and deployed to https://docs.commontools.dev + +## Overview + +While the main codebase uses Deno, the documentation is built using MyST (mystmd), which is a Node.js-based static site generator for scientific and technical documentation. + +## GitHub Actions Deployment + +The documentation is automatically built and deployed via GitHub Actions when: +- Changes are pushed to the `main` branch +- The changes affect files in the `tutorials/` directory +- Or when manually triggered via workflow dispatch + +The workflow is defined in `.github/workflows/deploy-docs.yml` + +## Local Development + +To work on the documentation locally, you'll need Node.js (not Deno) for MyST: + +```bash +# Navigate to tutorials directory +cd tutorials + +# Install dependencies (requires Node.js) +npm install + +# Start development server +npm run dev + +# Build static site +npm run build +``` + +## Configuration + +- **MyST Config**: `myst.yml` - Defines the site structure and settings +- **Custom Domain**: The CNAME file configures the custom domain (docs.commontools.dev) +- **GitHub Pages**: The site is deployed to GitHub Pages with the custom domain + +## Important Notes + +1. The MyST build process is completely separate from the Deno-based application code +2. The GitHub Action uses Node.js to build the documentation +3. The built HTML is deployed to GitHub Pages +4. Make sure to configure DNS records to point `docs.commontools.dev` to GitHub Pages + +## DNS Configuration Required + +For the custom domain to work, you need to configure DNS: +- Add a CNAME record pointing `docs.commontools.dev` to `.github.io` +- Or configure it according to GitHub's custom domain documentation \ No newline at end of file diff --git a/tutorials/code/state_02.tsx b/tutorials/code/state_02.tsx new file mode 100644 index 000000000..354a76773 --- /dev/null +++ b/tutorials/code/state_02.tsx @@ -0,0 +1,47 @@ +/// +import { + cell, + h, + recipe, + UI, + lift, + derive, + handler, + type Cell, +} from "commontools"; + +const calcAC = (dex: number) : number => + 20 + Math.floor((dex - 10) / 2); + +const updateName = handler< + { detail: { message: string } }, + { characterName: Cell } +>( + (event, { characterName }) => { + console.log("Updating character name to:", event.detail.message); + characterName.set(event.detail.message); + } +); + +export default recipe("state test", () => { + const characterName = cell(""); + characterName.set("Lady Ellyxir"); + const dex = cell(16); + const ac = lift(calcAC)(dex); + + return { + [UI]: ( +
+

Character name: {characterName}

+ +
  • DEX: {dex}
  • +
  • DEX Modifier: {Math.floor((dex - 10) / 2)}
  • +
  • AC: {ac}
  • +
    + ), + }; +}); diff --git a/tutorials/code/state_03.tsx b/tutorials/code/state_03.tsx new file mode 100644 index 000000000..da8cc1568 --- /dev/null +++ b/tutorials/code/state_03.tsx @@ -0,0 +1,65 @@ +/// +import { + cell, + h, + recipe, + UI, + lift, + derive, + handler, + type Cell, +} from "commontools"; + +const calcAC = (dex: number) : number => + 20 + Math.floor((dex - 10) / 2); + +const updateName = handler< + { detail: { message: string } }, + { characterName: Cell } +>( + (event, { characterName }) => { + characterName.set(event.detail.message); + } +); + +const rollD6 = () => Math.floor(Math.random() * 6) + 1; + +const rollDex = handler< + unknown, + Cell +>( + (_, dex) => { + // Roll 3d6 for new DEX value + const roll = rollD6() + rollD6() + rollD6(); + dex.set(roll); + } +); + +export default recipe("state test", () => { + const characterName = cell(""); + characterName.set("Lady Ellyxir"); + const dex = cell(16); + const ac = lift(calcAC)(dex); + + return { + [UI]: ( +
    +

    Character name: {characterName}

    + +
  • + DEX: {dex} + {" "} + + Roll + +
  • +
  • DEX Modifier: {Math.floor((dex - 10) / 2)}
  • +
  • AC: {ac}
  • +
    + ), + }; +}); diff --git a/tutorials/common_ui.md b/tutorials/common_ui.md new file mode 100644 index 000000000..4710bf5bd --- /dev/null +++ b/tutorials/common_ui.md @@ -0,0 +1,365 @@ +--- +title: Common UI +short_title: Common UI +description: Introduction to Common UI +subject: Tutorial +authors: + - name: Ben Follington + email: ben@common.tools +keywords: commontools, UI +abstract: | + Common UI is a collection of web components (prefixed with ct-) exposed for building patterns. +--- +# Common UI + +The philosophy of Common UI is inspired by Swift UI, the 'default' configuration should 'just work' if you use the correct building blocks together. + +:::{note} +Swift UI operates on a [reactive binding model](https://developer.apple.com/tutorials/swiftui-concepts/driving-changes-in-your-ui-with-state-and-bindings) with [FRP elements](https://developer.apple.com/documentation/combine), making it a short-leap from our needs (as compared with general Web UI). + +![](./images/managing-user-interface-state~dark@2x.png) + +Swift developers are encouraged to use the defaults as much as possible. By doing less specification you [maintain the dynamic ability to adapt to the user's preferences and environment]( https://developer.apple.com/tutorials/swiftui-concepts/maintaining-the-adaptable-sizes-of-built-in-views). This means you 'know less' about what you'll be drawing than you may have come to expect from an abstraction like Tailwind. The **composition** of components is emphasised over granular control. +::: + + +Our `ui` package is a web component library implemented in `lit` that interoperates with the Common Tools runtime to produce a Swift UI-like abstraction, this means our components are divided into layers: + + +# System Components + +## ct-theme + +Applies a set of theme variables to the entire subtree. Not all components respect the theme yet, but many do. See `packages/ui/src/v2/components/theme-context.ts` + +```{code-block} typescript +const localTheme = { + accentColor: cell("#3b82f6"), + fontFace: cell("system-ui, -apple-system, sans-serif"), + borderRadius: cell("0.5rem"), +}; + +// later... + +return { + [NAME]: "Themed Charm", + [UI]: ( + + {/* all components in subtree are themed */} + + ) +} +``` + +Can be nested and overriden further down the subtree. + +## ct-render + +Used to render a `Cell` that has a `[UI]` property into the DOM. Usually not required inside a pattern, used in the app shell itself. + +```{code-block} html + +``` + +## ct-keybind (beta) + +Register keyboard shortcuts with a handler. These registrations are mediated by `packages/shell/src/lib/keyboard-router.ts` in the shell to prevent conflicts with system shortcuts. + +```{code-block} html + +``` + +# Layout Components + +Layout components do not provide any content themselves, they are used to arrange other components. We draw quite directly from the [Swift UI Layout Fundamentals](https://developer.apple.com/documentation/swiftui/layout-fundamentals). + +## ct-screen + +Designed to represent content that could fill the entire screen or a panel / content area. This will expand to fill the available space. It offers two optional slots: `header` and `footer`. + +When to use: your `
    ` or `
    ` is not growing to full the available space. Typically appears _once_ at the root of a pattern's `[UI]` tree: + +```{figure} ./images/diagrams/ct-screen.svg +:name: layout-example +``` + +```{code-block} html + + + Hello + + +
    ...
    +
    ...
    + +
    + World +
    +
    +``` + +Inspired by this [Swift UI convention](https://scottsmithdev.com/screen-vs-view-in-swiftui). A `Screen` is just a `View` but it represents the kind of view that MIGHT fill a screen on some device. + +## ct-toolbar + +Stack several actions into a horizontal bar, typically at the top of ``. + +```{figure} ./images/diagrams/ct-toolbar.svg +:name: layout-example +``` + +```{code-block} html + + + A + B + + +``` + +## Stacks are all you need + +... almost. Just the [horizontal and vertical stacks](https://developer.apple.com/tutorials/swiftui-concepts/organizing-and-aligning-content-with-stacks) if you control the [spacing and alignment](https://developer.apple.com/tutorials/swiftui-concepts/adjusting-the-space-between-views). + +## ct-vstack + +Stack items vertically, this is a layer over the [CSS flexbox API](https://flexbox.malven.co/). You can permuate `gap`, `align`, `justify` and `reverse` attributes to control the behavior. + +When to use: any time you need to stack items vertically. + +```{figure} ./images/diagrams/ct-vstack-1.svg +:name: layout-example +``` + +```{code-block} html + +
    A
    +
    B
    +
    C
    +
    +``` + +## ct-hstack + +Stack items horizontally, this is a layer over the [CSS flexbox API](https://flexbox.malven.co/). You can permuate `gap`, `align`, `justify` and `reverse` attributes to control the behavior. + +When to use: toolbars, column layouts, grouping icons and buttons and text together. + +```{figure} ./images/diagrams/ct-hstack.svg +:name: layout-example +``` + +```{code-block} html + +
    A
    +
    B
    +
    C
    +
    +``` + +## ct-zstack + +Currently missing, would allow similar control for layering items on top of one another. [Swift UI ZStack](https://developer.apple.com/documentation/swiftui/zstack). + +## ct-vscroll + +Wrap tall vertical content in a scrollable container with control over autoscroll and scrollbar appearance. Inspired by [SwiftUI ScrollView](https://developer.apple.com/documentation/swiftui/scrollview). + +```{code-block} html + + +

    Long content...

    +
    +
    +``` + +In practice we often use a specific set of properties if dealing with a "chat view" that scrolls: + +```{code-block} html + +``` + +Here `flex` will force the `vscroll` to expand without a fixed height. `snapToBottom` will automatically scroll to the bottom when new content is added. + +## ct-autolayout + +Will attempt to lay out the children provided as best it can. Provides two slots for `left` and `right` sidebars (that can be toggled open/shut). On a wide view, items stack horizontally, on a medium view thet stack vertically and on mobile it converts to a tabbed view. + +```{figure} ./images/diagrams/ct-autolayout-wide.svg +:name: layout-example +``` + +```{figure} ./images/diagrams/ct-autolayout-mid.svg +:name: layout-example +``` + +```{figure} ./images/diagrams/ct-autolayout-narrow.svg +:name: layout-example +``` + +```{code-block} html + + +
    +

    Header Section

    +
    + + + + + + + + + + +
    +

    Main Content Area

    +

    This is the main content with sidebars

    + Main Button +
    + +
    +

    Second Content Area

    +

    This is the second content with sidebars

    + Second Button +
    + + + +
    + + +
    +

    Footer Section

    +
    +
    +``` + +## ct-grid (stale) + +`ct-grid` has not been used in production and likely doesn't work, but the intention is to wrap the [CSS Grid API](https://grid.malven.co/) and blend in ideas from [Swift UI Grid](https://developer.apple.com/documentation/swiftui/grid). + +## ct-spacer (missing) + +[Swift UI Spacer](https://developer.apple.com/documentation/swiftui/spacer) + +## Composed Layouts + +You can mix-and-match the above components to achieve practically any (standard) layout. + +```{code-block} html + + + hello + + + + + question + + + + + + + question + + + + + + + + ... + + + + + +
      +
    • Imagine this was long
    • +
    +
    +
    +
    +``` + +# Visual Components + +- typesetting: `ct-label`, `ct-heading` + - gap: `ct-text` for themed paragraph usecase (`p` works) + -

    , + +- gap: `ct-icon` (and `ct-label` has an optional in-built icon) + - gap: icon set? + +- visual: `ct-kdb`, `ct-separator`, `ct-table`, `ct-tool-call` + - gap: `ct-img` or `ct-media` + +# Input Components + +- input: `ct-button`, `ct-select`, `ct-input`, `ct-textarea`, `ct-checkbox`, `ct-tags` + - gap: `ct-search` which has an autocomplete menu + - gap: `ct-file-picker` + - redundant: common-send-message, ct-message-input (?) + - this is JUST a button and an input + - the "right" way is: + - ```{code-block} html + + + Submit + + ``` + + - ```{code-block} typescript + const EnterToSubmit = recipe(({ myHandler }) => { + return { + [UI]: + + Submit + + } + }) + + + ``` + +# Interactive / Complex Components + +- interactive: `ct-collapsible`, `ct-list-item`, `ct-tab-list`, `ct-canvas` + - `type ListItem = { title: string }` + - `const items: ListItem[]` + - Consider using `[NAME]`? + - gap: re-orderable list + - ```{code-block} html + + {items.map((i: Opaque<{ name: string }>) => {i.name})} + + ``` + - `editable` only applies to the `title` property of each list item + +- complex/integrated (cell interop): `ct-code-editor`, `ct-outliner`, `ct-list` + - gap: editable table rows + +## Chat Components + +- chat: `ct-chat`, `ct-prompt-input`, `ct-chat-message`, `ct-tool-call`, `ct-tools-chip` + +# Unused/Unproven Components + +- stale: `ct-aspect-ratio`, `ct-draggable`, `ct-form`, `ct-grid`, `ct-hgroup`, `ct-input-otp`, `ct-message-input`, `ct-progress`, `ct-radio`, `ct-radio-group`, `ct-slider`, `ct-switch`, `ct-tile`, `ct-toggle`, `ct-toggle-group`, `ct-vgroup` +- superfluous: `ct-resizeable-handle`, `ct-resizable-panel`, `ct-resizeable-panel-group`, `ct-scroll-area`, `ct-tabs`/`ct-tab-list`/`ct-tab-panel` diff --git a/tutorials/cts.md b/tutorials/cts.md new file mode 100644 index 000000000..c16108f6e --- /dev/null +++ b/tutorials/cts.md @@ -0,0 +1,15 @@ +--- +title: Common Type System +short_title: Common Type System +description: How to use the Common Type System to simplify types +subject: Tutorial +keywords: commontools, recipes, cts, types +abstract: | + The CTS (Common Type System) helps automatically convert types and common + coding patterns for you. This leads to more readable and succint code. + In this section, we will go over the basic features offered by CTS. +--- +# Common Type System + +Needs to be written + diff --git a/tutorials/images/diagrams/ct-autolayout-mid.svg b/tutorials/images/diagrams/ct-autolayout-mid.svg new file mode 100644 index 000000000..2d69c0c29 --- /dev/null +++ b/tutorials/images/diagrams/ct-autolayout-mid.svg @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/diagrams/ct-autolayout-narrow.svg b/tutorials/images/diagrams/ct-autolayout-narrow.svg new file mode 100644 index 000000000..0714b6356 --- /dev/null +++ b/tutorials/images/diagrams/ct-autolayout-narrow.svg @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/diagrams/ct-autolayout-wide.svg b/tutorials/images/diagrams/ct-autolayout-wide.svg new file mode 100644 index 000000000..335868e8a --- /dev/null +++ b/tutorials/images/diagrams/ct-autolayout-wide.svg @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/diagrams/ct-hstack.svg b/tutorials/images/diagrams/ct-hstack.svg new file mode 100644 index 000000000..1ca6f6df6 --- /dev/null +++ b/tutorials/images/diagrams/ct-hstack.svg @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/diagrams/ct-screen.svg b/tutorials/images/diagrams/ct-screen.svg new file mode 100644 index 000000000..5f2715116 --- /dev/null +++ b/tutorials/images/diagrams/ct-screen.svg @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/diagrams/ct-toolbar.svg b/tutorials/images/diagrams/ct-toolbar.svg new file mode 100644 index 000000000..6410e137d --- /dev/null +++ b/tutorials/images/diagrams/ct-toolbar.svg @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/diagrams/ct-vstack-1.svg b/tutorials/images/diagrams/ct-vstack-1.svg new file mode 100644 index 000000000..297b4507b --- /dev/null +++ b/tutorials/images/diagrams/ct-vstack-1.svg @@ -0,0 +1,333 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tutorials/images/llm_final.png b/tutorials/images/llm_final.png new file mode 100755 index 000000000..d7a2b66d8 Binary files /dev/null and b/tutorials/images/llm_final.png differ diff --git a/tutorials/images/llm_handler.png b/tutorials/images/llm_handler.png new file mode 100755 index 000000000..06679aa7c Binary files /dev/null and b/tutorials/images/llm_handler.png differ diff --git a/tutorials/images/llmv1.png b/tutorials/images/llmv1.png new file mode 100755 index 000000000..3c4899bb4 Binary files /dev/null and b/tutorials/images/llmv1.png differ diff --git a/tutorials/images/managing-user-interface-state~dark@2x.png b/tutorials/images/managing-user-interface-state~dark@2x.png new file mode 100644 index 000000000..702f0ba8e Binary files /dev/null and b/tutorials/images/managing-user-interface-state~dark@2x.png differ diff --git a/tutorials/images/state_ac.png b/tutorials/images/state_ac.png new file mode 100755 index 000000000..2ba42194c Binary files /dev/null and b/tutorials/images/state_ac.png differ diff --git a/tutorials/images/state_charname.png b/tutorials/images/state_charname.png new file mode 100755 index 000000000..c4469274a Binary files /dev/null and b/tutorials/images/state_charname.png differ diff --git a/tutorials/images/state_dex_button.png b/tutorials/images/state_dex_button.png new file mode 100755 index 000000000..4d398838a Binary files /dev/null and b/tutorials/images/state_dex_button.png differ diff --git a/tutorials/images/state_dex_mod.png b/tutorials/images/state_dex_mod.png new file mode 100755 index 000000000..f2ac14747 Binary files /dev/null and b/tutorials/images/state_dex_mod.png differ diff --git a/tutorials/images/state_name_change.png b/tutorials/images/state_name_change.png new file mode 100755 index 000000000..caa108a52 Binary files /dev/null and b/tutorials/images/state_name_change.png differ diff --git a/tutorials/index.md b/tutorials/index.md new file mode 100644 index 000000000..06f0f2631 --- /dev/null +++ b/tutorials/index.md @@ -0,0 +1,38 @@ +--- +title: Guide to the Common Tools Runtime +subject: Tutorial +keywords: commontools +--- + +# Welcome to the Common Tools Runtime Guide + +This guide will help you get started with the Common Tools runtime, from installation through advanced features. + +It is still a work in progress. + +## Contents + +- **{doc}`install-ct`** - Get the runtime up and running +- **{doc}`llm-builtin`** - A quick tour +- **{doc}`state`** - Managing application state +- **{doc}`state_modify`** - How to modify state with user input + +## TODO items +* more complex state types + * cell "object" make stats as an object + * cell array, make inventory as an array + * map() builtin +* pass reference to toggle an item (constant time) +* remove item via cell reference (O(n) via cell equality check) +* How to derive from two state inputs +* How to read a value +* recipe input and output - schemas +* cell creation via Default +* derive +* other builtins + * ifelse + * fetchdata + * navigateTo + * compileAndRun + * llmDialog +* sorting shopping list (filter by key such as aisle) diff --git a/tutorials/install-ct.md b/tutorials/install-ct.md new file mode 100644 index 000000000..297b512d6 --- /dev/null +++ b/tutorials/install-ct.md @@ -0,0 +1,143 @@ +--- +title: Installing Common Tools +short_title: Installing Common Tools +description: How to install the Common Tools runtime +subject: Tutorial +subtitle: How to install the Common Tools runtime +authors: + - name: Ellyse Cedeno + email: ellyse@common.tools +keywords: commontools, install +abstract: | + In this section, we install the code and servers needed to run the Common Tools runtime locally. +--- +## Install Common Tools + +Getting the basic Common Tools runtime up and running locally consists of 4 steps +1. Install Deno +1. Get the code +1. Configure any AI or extra services you want to run locally +1. Run the servers (Toolshed and Shell) + +We'll go over each of these steps. + +## Install Deno + +You can visit [Deno's website](https://docs.deno.com/runtime/getting_started/installation/) for more information about how to install Deno on your system. + +::::{tab-set} +:::{tab-item} Mac +:sync: tab1 +**Shell** +```bash +curl -fsSL https://deno.land/install.sh | sh +``` + +**Homebrew** + +```bash +brew install deno +``` + +**MacPorts** + +```bash +sudo port install deno +``` +::: +:::{tab-item} Linux +:sync: tab2 +**Shell** +```bash +curl -fsSL https://deno.land/install.sh | sh +``` + +**npm** + +```bash +npm install -g deno +``` + +**Nix** + +```bash +nix-shell -p deno +``` +::: +:::{tab-item} Windows +:sync: tab3 +You really expected us to have docs for programming on Windows? 😂 + +[WSL](https://learn.microsoft.com/en-us/windows/wsl/install) might be a good option. +:::: + +## Getting the Code +All the source code you need is available at +[https://github.com/commontoolsinc/labs.git](https://github.com/commontoolsinc/labs.git) + +``` +$ git clone https://github.com/commontoolsinc/labs.git +$ cd labs +``` + +## Configuration +If you plan on running any of the API services that Toolshed supplies, set the +`API_URL` environment variable. +``` +$ export API_URL="http://localhost:8000" +``` + +If you plan to run LLM calls, you will need to give Toolshed your API keys for the LLM services. +See `./packages/toolshed/env.ts` for a list of LLMs supported and their associated environment variables. +The current default LLM is Claude, therefore setting the Anthropic key is really the only +requirement. +``` +$ export CTTS_AI_LLM_ANTHROPIC_API_KEY= +``` + +## Run the servers +You'll need to run two servers (at the same time). The first one is the backend server, Toolshed. The following command will run Toolshed on its default port 8000. Note: the previous exported environment variables are important only for Toolshed. So make sure they are set in this shell instance. +``` +$ cd ./packages/toolshed +$ deno task dev +``` + +Next, is the front-end server, Shell. The following command will run Shell on its default port 5173. +``` +$ cd ./packages/shell +$ deno task dev-local +``` + +Now the servers should be running and you can navigate to [http://localhost:5173/](http://localhost:5173/) to see a not-too-exciting-yet charm. + +(deploy_charms)= +## How to deploy charms +To deploy your first charm, you will run the `ct` CLI tool. +You'll need to create an identity for yourself. Run the following command at the project root: +``` +$ deno task ct id new > my.key +``` +This will create a key for you. We'll refer to this in the next command which actually deploys a charm. We'll deploy the `counter.tsx` charm. You can explore other charms in the same directory. + +``` +$ deno task ct charm new --identity my.key --url http://localhost:8000/test_space ./packages/patterns/counter.tsx +Task ct ROOT=$(pwd) && cd $INIT_CWD && deno run --allow-net --allow-ffi --allow-read --allow-write --allow-env "$ROOT/packages/cli/mod.ts" "charm" "new" "--identity" "my.key" "--url" "http://localhost:8000/test_space" "./packages/patterns/counter.tsx" +Warning experimentalDecorators compiler option is deprecated and may be removed at any time +baedreihr5yyujte22cd7oogtqldt4miifj356zj7ivgk4eom264ldsu5pm +``` + +Notice the last line from the deploy output. This is the charm ID that you just deployed. We will use it to navigate to this charm. +Here is the URL; replace with + the value from your command output: `http://localhost:5173/test_space/` + +Notice the format. Port 5173 is the port number that the Shell process is listening on. +`test_space` is the SPACE that you are deploying to. You can think of it as a namespace for permissions. +Any SPACE that doesn't exist already is dynamically created when you visit it. +Lastly, we see the charm ID that was created when you deployed the charm. + +:::{admonition} Don't forget! +You will need to run the `deno task ct charm new` command each time you want to deploy a new charm. + +You will also need to keep the Toolshed and Shell servers running in order to run the deploy command and also to visit the charm on your browser. +::: + diff --git a/tutorials/llm-builtin.md b/tutorials/llm-builtin.md new file mode 100644 index 000000000..e6976cb3a --- /dev/null +++ b/tutorials/llm-builtin.md @@ -0,0 +1,303 @@ +--- +title: Using the LLM built-in within a Recipe +short_title: LLM built-in +description: A tutorial on llm built-in +subject: Tutorial +subtitle: A gentle tutorial into using the llm() function in a recipe +authors: + - name: Ellyse Cedeno + email: ellyse@common.tools +keywords: commontools, recipes, llm, builtins +abstract: | + In this section, we will create recipes to make LLM calls. We'll iterate on them to make new features, making sure you understand the changes each step of the way. +--- +(skeleton_recipe)= +## Skeleton Recipe +Let's first create a skeleton recipe. We'll need the basic imports. These are so common that you should generally just copy and paste them. + +```{code-block} typescript +:label: imports +:linenos: true +:emphasize-lines: 1 +:caption: Imports for Recipe +/// +import { + BuiltInLLMContent, + Cell, + cell, + Default, + derive, + h, + handler, + ifElse, + llm, + NAME, + recipe, + UI, +} from "commontools"; +``` +Notice line 1 begins with `/// { + return { + [NAME]: "MyLLM Test", + [UI]: ( +

    +

    MyLLM Test

    +
    + ), + }; +}); +``` + +Note that we are using `export` on line 1, this is because a single recipe source file may have multiple recipes in it. When the system is building a charm from the source file, it needs the main entry point, which will be the recipe that you export. + +The [NAME] symbol is used as a property in the created charm. It is used for displaying the charm and also often for debugging. + +The [UI] property has all the information to render the charm in the browser. It contains a mix of JSX and interpolated function calls using `{some_function()}` syntax. The results of these functions are inserted into display for rendering. We'll learn more about what works in the UI property in future sections. + +When you deploy and browse to your charm, you should see something that looks like this: + +![](./images/llmv1.png) +**Figure**: Placeholder Recipe + +## Add User Input +Now that we have a working deployed charm, let's continue to iterate and add a user input form. +This will eventually serve as the user's input to the LLM call. + +We'll update our `recipe` function to create a `Cell` to hold the value of the user input, add the JSX component that gets user text input, and also display the user input. +A `Cell` is like a variable in programming languages. Cells can store values and can be displayed in a recipe's [UI]. They are also persistent and automatically saved to the datastore. + +Here's our updated Recipe: +```{code-block} typescript +:label: llm_code_03 +:linenos: true +:emphasize-lines: 2, 11, 15 +:caption: Adding the Input JSX Component +export default recipe("LLM Test", () => { + const userMessage = cell(undefined); + + return { + [NAME]: "MyLLM Test", + [UI]: ( +
    +

    My LLM Test

    +
    User Message: {userMessage}
    +
    + +
    +
    + ), + userMessage, + }; +}); +``` +On line 2, we have a call to cell(). This will create a Cell with the default value passed in. +We'll use this cell to store the text the user types in our user input component. + +On line 11, we've added the `` component. Note that regular HTML forms are not allowed in the recipe UI. These restrictions are there for data privacy and security reasons. +The `placeholder` property (line 13) shows faded text in the input form as its default value. The `onmessagesend` property (line 15) is called when the user submits their message (presses enter or clicks on the submit button). The value for `onmessagesend` is the function that gets executed to handle the event. We'll define that next. The parameters you send must be wrapped in an object. Example: `{ userMessage }`. Additional parameters would be comma separated. + + +Before this code will actually work, we'll need to define the textInputHandler function. This should be at the same level as the `recipe` function. + +```{code-block} typescript +:label: llm_code_04 +:linenos: true +:emphasize-lines: +:caption: Adding the Event Handler + +const textInputHandler = handler< + { detail: { message: string } }, + { userMessage: Cell } +>(({ detail: { message } }, { userMessage }) => { + userMessage.set(message); +}); +``` + +We first imported the function `handler` from {ref}`imports`. It takes in type parameters for the Event it will handle and the Args it will be passed (`handler`). The shape of `Event` is based on the component you use. In our case, we used the component `common-send-message`. It takes in an event with the shape `{detail: {message: string}}`, you see this reflected on line 2. The shape of `Args` depends on what we are passing into the handler. We've set this in {ref}`llm_code_03` line 15, where we pass in `userMessage`, which is a Cell. We therefore set the Args type as `{ userMessage: Cell }` (line 3). + +Now that we've told `handler` what to expect as types, we can send the actual parameters on line 4. Here we pretty much repeat the type information with the actual variables. + +Our function body is just a single line (line 5). It simply sets the value of the `userMessage` cell to the user input. Cells have set() and get() functions. There are other functions which we'll discover in other chapters. + +After deploying your charm, you should see something like this: + +![](./images/llm_handler.png) +**Figure**: Recipe with Handler + +(calling_llm)= +## Calling the LLM! + +The next step is the real pay-off. We'll finally call the LLM. We'll add the built-in call right after the userMessage definition in the recipe. The actual location doesn't matter as long as it's in the `recipe`'s body. +```{code-block} typescript +:label: llm_builtin +:linenos: true +:emphasize-lines: +:caption: Adding the LLM built-in call +export default recipe("LLM Test", () => { + const userMessage = cell(undefined); + + const llmResponse = llm({ + system: + "You are a helpful assistant. Answer questions clearly and concisely.", + messages: derive(userMessage, (msg) => + msg ? [{ role: "user", content: msg, }] : []), }); +``` + +Lines 4-8 are new. We imported the `llm` function on line 10 in our {ref}`imports`. +We define a variable `llmResponse` to hold the return value of the llm call. +The parameter to `llm` is an object of the shape `{system: string, messages: BuiltInLLMMessage[]}`. The `BuiltInLLMParams` type looks like this, but really you only need to worry about `system` and `messages` for now: +``` +export interface BuiltInLLMParams { + messages?: BuiltInLLMMessage[]; + model?: string; + system?: string; + stop?: string; + maxTokens?: number; + mode?: "json"; + tools?: Record; +} +``` +You can see on line 5-8 that our shape is +`{system: string, messages: Array<{role: "user", content: msg}>}`. So what's the `derive` all about here? +This gets a bit technical. The parameters to a built-in like `llm()` are +passed as an `Opaque` object. Opaque objects mirror the expected type, and +any property you pass in as an `OpaqueRef` (such as `Cell`, `derive`) +stays reactive. Plain values still stay static. We want the +`messages` property to be reactive so it updates whenever `userMessage` +changes. The easiest way to do that here is to wrap the transformation in +`derive`, which returns an `OpaqueRef`. + +We are calling `derive(userMessage, (msg) => function_body())` on line 7. This means that the reactive node depends on `userMessage`; it will be called each time userMessage is updated. `userMessage` is passed in as the first arg to the anonymous function here, as `msg`. + +Looking at the function body on line 8, we see it first checks if `msg` is defined. This is important because `derive` will be called initially with an `undefined` value. We do not want to pass that along, so we do an explicit check here. If it is defined, we then return a well-formed message array, with just one message. This looks like `[ {role: "user", content: msg} ]`. Otherwise, we return `[]`, the empty list. + +Notice that we do not have to explicitly call the llm() function each time we get an input. +This is handled for us by the reactive system we just talked about. Specifically, the `llm()` built-in will re-execute every time `userMessages` is updated. We can then use the variable `llmResponse` to view the response, which we'll do right now. + +We'll add a new section to the recipe [UI] to display the current value of the `llmResponse`. Luckily, this is very straightforward: +```{code-block} html +:label: llm_response +:linenos: true +:emphasize-lines: +:caption: Display llmResponse +
    + llmResponse: + {llmResponse.result ? JSON.stringify(llmResponse.result) : ""} +
    +``` +`llmResponse` is typed as `Opaque`. The important thing to know about this is that it has a `result?` property (the ? means it is optional). This is where the result of the llm call is stored. +Because `result` is optional, we guard with a ternary before stringifying so that we don't render `undefined` while the LLM is still working. +The result can be either a direct string or an array of parts. We call JSON.stringify() because +this just makes it easier to do a bunch of work to display it. It won't be pretty, but you'll get +content you're looking for. + +:::{dropdown} Detailed explanation +:animate: fade-in + +The AST Transformer (enabled via `/// `) rewrites that ternary expression into `{ifElse(llmResponse.result, derive(llmResponse.result, _v1 => JSON.stringify(_v1)), "")}`. You'll still need to import `ifElse` (even though you never call it yourself) alongside the existing `derive` import for the generated code to type-check. +::: + +If you deploy and run it, you should be able to enter a message into the input form, then wait a few seconds and see a response from our friendly LLM. Here is what it looks like for me: + +![](./images/llm_final.png) +**Figure**: Final Recipe + +## The Final Flow +Et voilĂ  ! Hopefully that worked for you. The full source code is listed at the end of this section. + +Here is the flow control at a high level. +When the system first loads, it executes the body of the recipe() function, which creates the `userMessage` cell, and `llmResponse` which holds the result of calling `llm()`. + +Technically, the `llm()` built-in is called once with the undefined userMessage upon its initialization. + +The charm then renders the code in the [UI] section and the system sets the reactive node to display the llmResponse with the conditional expression we wrote (`{llmResponse.result ? ... : ""}`) and the user's message with `{userMessage}`. +These initially don't show anything since the values are undefined. + +The user types a prompt into the `` component which triggers the `textInputHandler()`. The handler gets passed in the event, which contains the user's message (as a normal js object), and also the `userMessage` which is a Cell. The handler sets the cell's value with the event message. + +The `userMessage` has been updated now and therefore kicks off the reactive system. +We re-render the portion of the UI that contains `User Message: {userMessage}` since the cell contained within the braces has changed. +The `llm()` built-in notices that its object (the messages property) has been updated and runs again. +In a few seconds, it gets a response back from the LLM. +This sets `llmResponse.result`, which triggers the generated `ifElse(derive(...))` wrapper behind that conditional expression. +And finally we see the `llmResponse: ...` in the [UI]. + +There's a lot more to discover with the llm() function call (such as sending a list of user and agent messages for history or even tool use) and even more to learn about the Common Tools runtime system. + + +```{code-block} typescript +:label: llm_full_code +:linenos: true +:emphasize-lines: +:caption: Full Code +/// +import { + BuiltInLLMContent, + Cell, + cell, + Default, + derive, + h, + handler, + ifElse, + llm, + NAME, + recipe, + UI, +} from "commontools"; + +const textInputHandler = handler< + { detail: { message: string } }, + { userMessage: Cell } +>(({ detail: { message } }, { userMessage }) => { + userMessage.set(message); +}); + +export default recipe("LLM Test", () => { + const userMessage = cell(undefined); + + const llmResponse = llm({ + system: + "You are a helpful assistant. Answer questions clearly and concisely.", + messages: derive(userMessage, (msg) => + msg ? [{ role: "user", content: msg, }] : []), }); + + return { + [NAME]: "MyLLM Test", + [UI]: ( +
    +

    MyLLM Test

    +
    User Message: {userMessage}
    +
    + llmResponse: + {llmResponse.result ? JSON.stringify(llmResponse.result) : ""} +
    +
    + +
    +
    + ), + }; +}); +``` diff --git a/tutorials/myst.yml b/tutorials/myst.yml new file mode 100644 index 000000000..1fdf8f3bc --- /dev/null +++ b/tutorials/myst.yml @@ -0,0 +1,28 @@ +# See docs at: https://mystmd.org/guide/frontmatter +version: 1 +project: + id: 9ec3a2ef-fa55-4e77-8593-8dc1fc1e51ed + title: Guide to the Common Tools Runtime + description: Documentation and tutorials for the Common Tools runtime and framework + keywords: [commontools, runtime, tutorials, documentation] + github: https://github.com/commontoolsinc/labs/ + # To autogenerate a Table of Contents, run "myst init --write-toc" + toc: + # Auto-generated by `myst init --write-toc` + - file: index.md + - file: install-ct.md + - file: llm-builtin.md + - file: state.md + - file: state_modify.md + - file: common_ui.md + - file: cts.md + +site: + template: book-theme + title: Common Tools Documentation + parts: + title: Common Tools Documentation + options: + # favicon: favicon.ico + # logo: site_logo.png + base_url: https://docs.commontools.dev diff --git a/tutorials/netlify.toml b/tutorials/netlify.toml new file mode 100644 index 000000000..65181ee79 --- /dev/null +++ b/tutorials/netlify.toml @@ -0,0 +1,6 @@ +[build] + command = "npm ci && npm run build" + publish = "_build/html" + +[build.environment] + NODE_VERSION = "18" diff --git a/tutorials/package-lock.json b/tutorials/package-lock.json new file mode 100644 index 000000000..79312b238 --- /dev/null +++ b/tutorials/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "common-tools-guide", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "common-tools-guide", + "version": "0.0.1", + "devDependencies": { + "mystmd": "^1.0.0" + } + }, + "node_modules/mystmd": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mystmd/-/mystmd-1.6.1.tgz", + "integrity": "sha512-jrqXTYtKAfy3wtnXwoPSEFO9POHTe2QYI16lZAhNw1FvfWkdTmpDys8vNfXDsTWFRmRdKeDg30MdebDcnhSKxQ==", + "dev": true, + "license": "MIT", + "bin": { + "myst": "dist/myst.cjs" + } + } + } +} diff --git a/tutorials/package.json b/tutorials/package.json new file mode 100644 index 000000000..7bda2ce67 --- /dev/null +++ b/tutorials/package.json @@ -0,0 +1,13 @@ +{ + "name": "common-tools-guide", + "private": true, + "version": "0.0.1", + "scripts": { + "build": "myst build --html", + "clean": "myst clean --cache || true", + "dev": "myst start" + }, + "devDependencies": { + "mystmd": "^1.0.0" + } +} diff --git a/tutorials/state.md b/tutorials/state.md new file mode 100644 index 000000000..bbbaf09d2 --- /dev/null +++ b/tutorials/state.md @@ -0,0 +1,230 @@ +--- +title: Working with State +short_title: State +description: How state is handled in the Common Tools runtime +subject: Tutorial +authors: + - name: Ellyse Cedeno + email: ellyse@common.tools +keywords: commontools, state, Cell, database +abstract: | + In this section, we discover how state is handled in the runtime, how persistence is related, and discuss common patterns to use. +--- +## State and Cells + +We'll be learning how to handle state within the Common Tools runtime. +The most direct way to store state is via `Cells`. +Cells store and access data. We can set data in a cell via the set() function. We can also retrieve data via the get() function, we'll demonstrate that in a later section. +There are many ways to create cells and we'll get to all of them, but for now, we'll start with the `cell()` function available in Recipes. +We've already used this in {ref}`calling_llm` + +Creating a cell is quite easy! Let's create the beginnings of a +character sheet, one we might use playing a table top role playing game. Don't worry if you don't get the reference, it should be easy to follow. + +```{code-block} typescript +export default recipe("state test", () => { + const characterName = cell(""); +} +``` +Here, we have created a cell with a type argument of `string`, +and its initial value is the empty string. + +Let's now set `characterName` to something a bit more interesting. + +```{code-block} typescript +export default recipe("state test", () => { + const characterName = cell(""); + characterName.set("Lady Ellyxir"); +} +``` +We can now display the cell within the `[UI]` section of the recipe: +```{code-block} typescript +/// +import { + cell, + h, + recipe, + UI, +} from "commontools"; + +export default recipe("state test", () => { + const characterName = cell(""); + characterName.set("Lady Ellyxir"); + return { + [UI]: ( +
    +

    Character name: {characterName}

    +
    + ), + }; +}); +``` +The `{characterName}` snippet creates a reactive node behind the scenes. This means the rendered character name is updated whenever the cell changes. + +We can now deploy the code. See the section {ref}`deploy_charms` for how to do this. + +It'll look a bit like this: +![](./images/state_charname.png) +**Figure**: Character Name set + +## Deriving from Cells + +We often have computed states which are derived from existing states. + +A concrete example of derived state is AC (Armor Class). +Its value is affected by another state value, Dexterity. +We'll build out this example. + +First, let's create the Dexterity attribute. Not surprisingly, we'll use a `Cell` to store this data. We'll also display it in the `[UI]` section. + +```{code-block} typescript +:label: state_display_dex +:linenos: true +:emphasize-lines: 2,3,8 +:caption: Displaying DEX + const characterName = cell(""); + const dex = cell(16); + ... + [UI]: ( +
    +

    Character name: {characterName}

    +
  • DEX: {dex}
  • +
    + ... +``` +The highlighted lines are the ones we added. It should be pretty self-explanatory. We create the dex cell to store Dexterity and we display on line 8. + +If all we want to do is display the derived calculation, we can simply put it between the `{}` symbols. Let's display our dexterity modifier. We'll use this later to calculate our Armor Class. + +```{code-block} typescript +:label: state_display_dex_modifier +:linenos: true +:emphasize-lines: 5 +:caption: Displaying DEX Modifier + [UI]: ( +
    +

    Character name: {characterName}

    +
  • DEX: {dex}
  • +
  • DEX Modifier: {Math.floor((dex - 10) / 2)}
  • +
    + ... +``` + +It should look a bit like this: +![](./images/state_dex_mod.png) + +Let's now calculate Armor Class. +It's defined as `10 + Dexterity modifier`. We *could* do that same thing +and just display it inline, however, this gets complicated to read +after a while. Instead we'll introduce `lift()` which lets you +create a reactive state based on inputs such as cells. + +## Lift + +A lift takes a regular function and allows it to be used with +reactive nodes. For example, here's a regular TypeScript function to +calculate armor class: +```{code-block} typescript +:label: state_ac +:linenos: false +:emphasize-lines: + const calcAC = (dex: number) : number => + 10 + Math.floor((dex - 10) / 2); +``` + +We can't just pass our `dex` variable into this function since `dex` isn't a regular `number` (it's a `Cell`). This is the magic of `lift`. It takes in the regular function and returns a new function that can take in matching reactive components as parameters. + +We create the lifted function with the following code in the recipe body: +```{code-block} typescript +:label: state_ac_lift +:linenos: false +:emphasize-lines: + const ac = lift(calcAC)(dex); +``` +We can now use the reference to the lift `ac` just like we'd use any cell reference. + +:::{dropdown} Detailed explanation +:animate: fade-in + +`lift` returns a function that matches the passed in function's +argument list but accepts reactive component versions of each parameter instead. + +In our code above, we immediately call this function with the parameter `dex`. +The return value is a reactive component this will be updated anytime +the input is updated (in our case, the `dex Cell`). + +We can instead defer calling this `lift`'d function or even call it repeatedly. Each new call will result in a new reactive component that is tracked independently of the others. +::: + +Here, we add it to the `[UI]` on line 5: +```{code-block} typescript +:label: state_ac_lift_display +:linenos: true +:emphasize-lines: 5 +
    +

    Character name: {characterName}

    +
  • DEX: {dex}
  • +
  • DEX Modifier: {Math.floor((dex - 10) / 2)}
  • +
  • AC: {ac}
  • +
    +``` + +Note: we must import `lift` and `derive`. We'll need `derive` because of some behind-the-scenes code transformation, but we will not be using it directly in this section. (See {doc}`cts` for more information). + +Here's what the full Recipe looks like: +```{code-block} typescript +:label: state_code_full +:linenos: true +:emphasize-lines: 7,8,14,15,16,17,23,24 +:caption: Full State Code +/// +import { + cell, + h, + recipe, + UI, + lift, + derive, +} from "commontools"; + +export default recipe("state test", () => { + const characterName = cell(""); + characterName.set("Lady Ellyxir"); + const dex = cell(16); + const calcAC = (dex: number) : number => + 10 + Math.floor((dex - 10) / 2); + const ac = lift(calcAC)(dex); + return { + [UI]: ( +
    +

    Character name: {characterName}

    +
  • DEX: {dex}
  • +
  • DEX Modifier: {Math.floor((dex - 10) / 2)}
  • +
  • AC: {ac}
  • +
    + ), + }; +}); +``` + +It should render something similar to this: +![](./images/state_ac.png) +**Figure:** Display derived `ac` for Armor Class + +We've demonstrated the following state-related concepts: +* How to create a simple `Cell` +* Set its value +* Display the cell in `[UI]` +* Create UI that calculates a derived value (DEX Modifier) +* Create a lifted function from regular TypeScript +* Use the lifted function as a reactive component in `[UI]` + +In the next section, we will learn how user input can +modify state. + +### Credits +We used the Open Source SRD 5.1 for character sheet information. +See [SRD 5.1](https://www.dndbeyond.com/srd). +It is licensed under +Creative Commons Attribution 4.0 International (“CC-BY-4.0”) + diff --git a/tutorials/state_modify.md b/tutorials/state_modify.md new file mode 100644 index 000000000..8c5ec15ee --- /dev/null +++ b/tutorials/state_modify.md @@ -0,0 +1,205 @@ +--- +title: Modifying State +short_title: Modifying State +description: Using user input to modify state +subject: Tutorial +authors: + - name: Ellyse Cedeno + email: ellyse@common.tools +keywords: commontools, state, Cell, database +abstract: | + In this section, we will add on to the simple state we created in the last + section. We will use user input to modify the existing state. +--- +# Modifying State + +## Introduction + +In the last section, we learned how to create state via `Cell`s and also +how to create derived states. +We used building a fantasy game character sheet as an example. +We'll continue with that to learn how to modify state. + +## Handling User Input + +Let's start with changing our character's name. +We'll need to add a text input field in the `[UI]` section. +We can't just use regular HTML components. +The Common Tools runtime has its own JSX components to +make sure data is protected and not accessed by +other scripts. + +```{code-block} typescript +:label: state_send_message_placeholder +:linenos: false +:emphasize-lines: + +``` + +If you deploy this update, you'll see an input field, but nothing happens +when you enter data. As the comments indicate, we +need to fill out code for the onmessagesend JSX event listener. + +This is when we learn about `handler`. +A `handler` is a Common Tools runtime component that, like its name +suggests, handles events. +The JSX event listener (such as `onmessagesend` in our code) will call +our handler to handle the event emitted by the JSX component. + +## Understanding Handlers + +Handlers in Common Tools have a specific signature: + +```{code-block} typescript +handler(handlerFunction) +``` + +The `handler` function takes: +- Two **type** parameters: + - `EventType`: defines the event data structure + - `ArgsType`: defines the arguments/context that you want to pass to the handler +- One **argument**: we pass in a function which receives: + - `event` (matches EventType) as its first parameter + - `args` (matches ArgsType) as its second parameter + +:::{dropdown} Detailed explanation +:animate: fade-in + +The `handler` function returns a factory that you call with your actual arguments to create the event handler. This factory pattern allows the handler to bind specific values from your recipe while still receiving events from the UI components. +::: + +We'll start by writing our handler which takes the event emitted by the +`` component. This component emits a CustomEvent with the structure `{detail: {message: string}}`, +where `message` contains the text the user entered. +The handler will also take in the +`characterName` cell. It will simply set the cell with the new name +from the event. + +### Creating the Handler + +```{code-block} typescript +:label: state_handler_updatename +:linenos: false +:emphasize-lines: +const updateName = handler< + { detail: { message: string } }, + { characterName: Cell } +>( + (event, { characterName }) => { + console.log("Updating character name to:", event.detail.message); + characterName.set(event.detail.message); + } +); +``` +Note that `characterName` was passed in as a `Cell`. We created it via the +`cell()` function, which returns us a `Cell`. It's important to +mark reactive components as `Cell` so that we can call methods such +as `set()` on them. + +Now we can attach this handler to our input component: + +```{code-block} typescript +:label: state_handler_attach +:linenos: false +:emphasize-lines: + +``` + +If you deploy this code, you should see something like: +![](./images/state_name_change.png) +**Figure:** Updating your character's name + +:::{dropdown} View complete code +:animate: fade-in + +```{literalinclude} ./code/state_02.tsx +:language: typescript +``` +::: + +Et voilà ! We've fully implemented modifying state through user input. + +:::{admonition} Important! +Notice that if you reload the page, or even load the same URL on a different +browser, you'll see the saved data for your character's name. +This is because cells are persistent by default. +::: + +:::{dropdown} Detailed explanation +:animate: fade-in + +Each cell is created with a `cause` that uniquely identifies it. +We carefully construct the `cause` so that it remains the same +each time a recipe is run, but also unique from other cells created. +This leads to automatic persistence when using the Common Tools +runtime. +::: + +## Adding Buttons + +We'll create a button to roll "dice" for the character's Dexterity +value. This will update the existing value. + +First let's create the handler for the click event. We +don't need details on the event itself, so we mark it as `unknown`. + +```{code-block} typescript +:label: state_rollDex_handler +:linenos: false +:emphasize-lines: +const rollD6 = () => Math.floor(Math.random() * 6) + 1; + +const rollDex = handler< + unknown, + Cell +>( + (_, dex) => { + // Roll 3d6 for new DEX value + const roll = rollD6() + rollD6() + rollD6(); + dex.set(roll); + } +); +``` + +This handler simulates rolling three six-sided dice (3d6) and sets the DEX value to the result. + +Next, we'll add a button beside DEX in the UI and attach our handler: + +```{code-block} typescript +:label: state_button_with_handler +:linenos: false +:emphasize-lines: +
  • + DEX: {dex} + {" "} + + Roll + +
  • +``` +Note the `{" "}` between the DEX value and button - this adds just a little padding before the button. + +When we click on the button, the elements that depend on the value of that cell are also updated. This means the DEX, DEX Modifier, and AC values are all updated. + +You should see something like the following once you click on the Roll button: +![](./images/state_dex_button.png) + +:::{dropdown} View complete code +:animate: fade-in + +```{literalinclude} ./code/state_03.tsx +:language: typescript +``` +::: + +So far, we've been using `Cell` to store primitive data types. +In the next section, we'll move on to objects and arrays.