├── .eslintignore
├── .eslintrc.yml
├── .github
└── workflows
│ └── build-and-publish.yml
├── .gitignore
├── .prettierrc
├── .replit
├── README.md
├── package-lock.json
├── package.json
├── preact.config.js
├── prerender-urls.json
├── replit.nix
├── size-plugin.json
├── src
├── assets
│ ├── builtin-models
│ │ ├── chat
│ │ │ ├── act-therapist.png
│ │ │ ├── ai-assistant.png
│ │ │ ├── alien.png
│ │ │ ├── con-artist.png
│ │ │ ├── energy-educator.json
│ │ │ ├── energy-educator.png
│ │ │ ├── hey-sad-guy-feel-better.json
│ │ │ ├── hey-sad-guy-feel-better.png
│ │ │ ├── interviewer.png
│ │ │ └── rogerian-therapist.png
│ │ ├── citymaker
│ │ │ ├── babylon.json
│ │ │ ├── babylon.png
│ │ │ ├── talamora.json
│ │ │ └── talamora.png
│ │ ├── layercraft
│ │ │ ├── pearl-cove.json
│ │ │ ├── pearl-cove.png
│ │ │ ├── veilvale.json
│ │ │ └── veilvale.png
│ │ ├── peoplesim
│ │ │ ├── camping-fiasco.json
│ │ │ ├── camping-fiasco.png
│ │ │ ├── orc-vs-hobbit.json
│ │ │ └── orc-vs-hobbit.png
│ │ └── tone-changer
│ │ │ ├── formal-spanish.png
│ │ │ ├── malfunctioning-robot.png
│ │ │ ├── painfully-academic.png
│ │ │ ├── posh-english-lady.png
│ │ │ └── preschool-teacher.png
│ ├── favicon.ico
│ ├── icons
│ │ ├── android-chrome-192x192.png
│ │ ├── android-chrome-512x512.png
│ │ ├── apple-touch-icon.png
│ │ ├── browserconfig.xml
│ │ ├── chat-logo.png
│ │ ├── citymaker-logo.png
│ │ ├── cityplayer-logo.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon.ico
│ │ ├── furniture-icon.png
│ │ ├── generic-app-icon.png
│ │ ├── iaia-logo.png
│ │ ├── interactive-fiction-logo.png
│ │ ├── key-management-logo.png
│ │ ├── layercraft-logo.png
│ │ ├── llm-garden-logo.png
│ │ ├── loading-source
│ │ │ ├── loading1.png
│ │ │ ├── loading2.png
│ │ │ ├── loading3.png
│ │ │ ├── loading4.png
│ │ │ ├── loading5.png
│ │ │ ├── loading6.png
│ │ │ └── loading7.png
│ │ ├── loading.gif
│ │ ├── mstile-150x150.png
│ │ ├── myoa-logo.png
│ │ ├── p5drawing-logo.png
│ │ ├── peoplesim-logo.png
│ │ ├── person.png
│ │ ├── safari-pinned-tab.svg
│ │ ├── site.webmanifest
│ │ ├── tone-changer-logo.png
│ │ └── voice-composer-logo.png
│ └── interactive-fiction
│ │ └── z5s
│ │ ├── Advent.z5
│ │ ├── s5.z4
│ │ ├── zork1.z5
│ │ ├── zork2.z5
│ │ ├── zork3.z5
│ │ └── ztuu.z5
├── chat
│ ├── chatdb.js
│ ├── chatview.js
│ └── index.js
├── citymaker
│ ├── citygraph.js
│ ├── citylayout.js
│ ├── citymakerdb.js
│ ├── citymakerview.js
│ ├── index.js
│ └── preactcytoscape.js
├── cityplayer
│ ├── citycomponents.js
│ ├── cityplayerdb.js
│ ├── cityplayerview.js
│ ├── index.js
│ └── linkmarkdown.js
├── components
│ ├── app.js
│ ├── common.js
│ ├── header.js
│ ├── home.js
│ ├── icons.js
│ ├── modelindex.js
│ ├── modelmenu.js
│ ├── querylog.js
│ ├── sidebar.js
│ └── speech.js
├── db.js
├── deepequal.js
├── generallogoprompt.js
├── gptservice
│ ├── appgpt.js
│ ├── gpt.js
│ ├── gptcache.js
│ └── tokencost.js
├── hashsignal.js
├── imageapi
│ ├── dalle.js
│ ├── replicatekey.js
│ ├── stablediffusion.js
│ ├── thumbsnap.js
│ └── thumbsnapkey.js
├── imagegen
│ ├── imagegendb.js
│ ├── imagegenview.js
│ └── index.js
├── index.js
├── interactive-fiction
│ ├── ifdb.js
│ ├── ifview.js
│ ├── index.js
│ └── storyloader.js
├── key-management
│ ├── index.js
│ ├── key.js
│ └── keyholder.js
├── layercraft
│ ├── citymakerschema.js
│ ├── index.js
│ ├── laxjson.js
│ ├── layerdb.js
│ ├── layerview.js
│ ├── schema.js
│ └── template.js
├── loadlegacyscript.js
├── localcache.js
├── localsync.js
├── manifest.json
├── markdown.js
├── myoa
│ ├── index.js
│ ├── player.js
│ ├── playstate.js
│ ├── story.js
│ ├── storydb.js
│ └── storyview.js
├── p5drawing
│ ├── iframeview.js
│ ├── index.js
│ ├── p5db.js
│ └── p5view.js
├── peoplesim
│ ├── index.js
│ ├── peopledb.js
│ └── peopleview.js
├── style.css
├── sw.js
├── template.html
├── tone-changer
│ ├── index.js
│ ├── tonedb.js
│ └── toneview.js
├── uuid.js
├── vendor
│ ├── _unreset.scss
│ ├── encryption.js
│ ├── jszm.js
│ └── replicate.js
└── voice-composer
│ ├── index.js
│ ├── voicedb.js
│ ├── voiceview.js
│ └── whisperrecord.js
├── tailwind.config.js
└── tests
├── __mocks__
├── browserMocks.js
├── fileMocks.js
└── setupTests.js
└── header.test.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/vendor/
2 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | browser: true
3 | es6: true
4 |
5 | parserOptions:
6 | ecmaFeatures:
7 | jsx: true
8 | ecmaVersion: 2022
9 | sourceType: module
10 |
11 | extends:
12 | - eslint:recommended
13 | # FIXME: Having a hard time installing this now:
14 | # - plugin:mozilla/recommended
15 | - prettier
16 |
17 | plugins:
18 | # FIXME: disabled due to installation problem
19 | # - mozilla
20 | - react-hooks
21 | - prettier
22 |
23 | rules:
24 | eqeqeq: error
25 | no-console: "off" # TODO: Set back to "warn"
26 | no-unused-vars: [error, { vars: all, args: none, ignoreRestSiblings: false }]
27 | no-var: error
28 | no-warning-comments: off
29 | prefer-const: error
30 | # FIXME: disabled because the plugin is not working well:
31 | # mozilla/no-import-into-var-and-global: off
32 | comma-dangle: off
33 | react-hooks/rules-of-hooks: error
34 | react-hooks/exhaustive-deps: warn
35 | prettier/prettier:
36 | - error
37 | - fileInfoOptions:
38 | ignorePath: ".gitignore"
39 |
40 | jsx-a11y/no-onchange: off
41 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-publish.yml:
--------------------------------------------------------------------------------
1 | name: Deploy site to Pages
2 |
3 | on:
4 | # Runs on pushes targeting the default branch
5 | push:
6 | #branches: [$default-branch]
7 | branches: main
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow one concurrent deployment
19 | concurrency:
20 | group: "pages"
21 | cancel-in-progress: true
22 |
23 | jobs:
24 | # Build job
25 | build:
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Checkout
29 | uses: actions/checkout@v3
30 | - name: Detect package manager
31 | id: detect-package-manager
32 | run: |
33 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then
34 | echo "manager=yarn" >> $GITHUB_OUTPUT
35 | echo "command=install" >> $GITHUB_OUTPUT
36 | echo "runner=yarn" >> $GITHUB_OUTPUT
37 | exit 0
38 | elif [ -f "${{ github.workspace }}/package.json" ]; then
39 | echo "manager=npm" >> $GITHUB_OUTPUT
40 | echo "command=ci" >> $GITHUB_OUTPUT
41 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT
42 | exit 0
43 | else
44 | echo "Unable to determine packager manager"
45 | exit 1
46 | fi
47 | - name: Setup Node
48 | uses: actions/setup-node@v3
49 | with:
50 | node-version: "16"
51 | cache: ${{ steps.detect-package-manager.outputs.manager }}
52 | - name: Setup Pages
53 | uses: actions/configure-pages@v2
54 | - name: Install dependencies
55 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }}
56 | - name: Build
57 | run: npm run build
58 | #run: ${{ steps.detect-package-manager.outputs.runner }} run build
59 | - name: Upload artifact
60 | uses: actions/upload-pages-artifact@v1
61 | with:
62 | path: ./build
63 |
64 | # Deployment job
65 | deploy:
66 | environment:
67 | name: github-pages
68 | url: ${{ steps.deployment.outputs.page_url }}
69 | runs-on: ubuntu-latest
70 | needs: build
71 | steps:
72 | - name: Deploy to GitHub Pages
73 | id: deployment
74 | uses: actions/deploy-pages@v1
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /build
3 | /*.log
4 | /*.key
5 | .DS_Store
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5"
3 | }
4 |
--------------------------------------------------------------------------------
/.replit:
--------------------------------------------------------------------------------
1 | run = "npm run dev"
2 | entrypoint = "src/index.js"
3 |
4 | hidden = [".config"]
5 |
6 | [nix]
7 | channel = "stable-22_05"
8 |
9 | [env]
10 | XDG_CONFIG_HOME = "/home/runner/.config"
11 | PATH = "/home/runner/$REPL_SLUG/.config/npm/node_global/bin:/home/runner/$REPL_SLUG/node_modules/.bin"
12 | npm_config_prefix = "/home/runner/$REPL_SLUG/.config/npm/node_global"
13 |
14 | [packager]
15 | language = "nodejs"
16 |
17 | [packager.features]
18 | packageSearch = true
19 | guessImports = true
20 | enabledForHosting = false
21 |
22 | [languages.javascript]
23 | pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
24 |
25 | [languages.javascript.languageServer]
26 | start = [ "typescript-language-server", "--stdio" ]
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LLM Garden
2 |
3 | This is my ([Ian's](https://ianbicking.org)) playground for doing stuff with GPT-3.
4 |
5 | ## Site
6 |
7 | The site is deployed to https://llm.ianbicking.org
8 |
9 | Note you have to have your own [GPT API Key](https://beta.openai.com/account/api-keys) to run any examples.
10 |
11 | [](https://github.com/ianb/llm-garden/actions/workflows/build-and-publish.yml)
12 |
13 | ## Building / running
14 |
15 | To run use:
16 |
17 | ```bash
18 | # install dependencies
19 | npm install
20 | # serve with hot reload at localhost:8080
21 | npm run dev
22 | ```
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "llm-garden",
4 | "version": "0.0.0",
5 | "license": "",
6 | "scripts": {
7 | "build": "preact build --no-prerender",
8 | "serve": "sirv build -s --port 8080 --cors --single",
9 | "dev": "NODE_OPTIONS=--openssl-legacy-provider preact watch",
10 | "proxy": "lcp --proxyUrl https://api.replicate.com",
11 | "lint": "eslint src",
12 | "test": "echo sorry no tests"
13 | },
14 | "eslintConfig": {
15 | "extends": "preact",
16 | "ignorePatterns": [
17 | "build/"
18 | ]
19 | },
20 | "devDependencies": {
21 | "enzyme": "^3.11.0",
22 | "enzyme-adapter-preact-pure": "^4.0.1",
23 | "eslint": "^8.29.0",
24 | "eslint-config-preact": "^1.3.0",
25 | "eslint-config-prettier": "^8.5.0",
26 | "eslint-plugin-fetch-options": "0.0.5",
27 | "eslint-plugin-no-unsanitized": "^4.0.2",
28 | "eslint-plugin-prettier": "^4.2.1",
29 | "preact-cli": "^3.4.1",
30 | "prettier": "^2.8.1",
31 | "sirv-cli": "2.0.2",
32 | "tailwindcss": "^3.2.4"
33 | },
34 | "dependencies": {
35 | "@preact/signals": "^1.1.2",
36 | "@ricky0123/vad": "^0.2.4",
37 | "cytoscape": "^3.23.0",
38 | "cytoscape-cose-bilkent": "^4.1.0",
39 | "dexie": "^3.2.2",
40 | "eslint-plugin-html": "^7.1.0",
41 | "fastest-levenshtein": "^1.0.16",
42 | "json5": "^2.2.3",
43 | "jszm": "github:DLehenbauer/jszm",
44 | "just-deep-map-values": "^1.2.0",
45 | "just-shuffle": "^4.2.0",
46 | "local-cors-proxy": "^1.1.0",
47 | "marked": "^4.2.4",
48 | "onnxruntime-web": "^1.13.1",
49 | "p5": "^1.6.0",
50 | "preact": "^10.11.3",
51 | "preact-async-route": "^2.2.1",
52 | "preact-render-to-string": "^5.2.6",
53 | "preact-router": "^4.1.0",
54 | "replicate-js": "github:nicholascelestin/replicate-js",
55 | "sync-sha1": "^1.0.1",
56 | "tailwind-merge": "^1.8.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/preact.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | module.exports = (config, env, helpers) => {
3 | const postCssLoaders = helpers.getLoadersByName(config, "postcss-loader");
4 | postCssLoaders.forEach(({ loader }) => {
5 | const plugins = loader.options.postcssOptions.plugins;
6 |
7 | // Add tailwind css at the top.
8 | plugins.unshift(require("tailwindcss"));
9 | plugins.unshift(require("tailwindcss/nesting"));
10 | });
11 | return config;
12 | };
13 |
--------------------------------------------------------------------------------
/prerender-urls.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "url": "/",
4 | "title": "Large Language Model (GPT-3) Garden"
5 | },
6 | {
7 | "url": "/interactive-fiction",
8 | "title": "Interactive Fiction"
9 | },
10 | {
11 | "url": "/chat",
12 | "title": "Chat"
13 | },
14 | {
15 | "url": "/citymaker",
16 | "title": "City Maker"
17 | },
18 | {
19 | "url": "/myoa",
20 | "title": "Make Your Own Adventure"
21 | },
22 | {
23 | "url": "/myoa/play",
24 | "title": "Player"
25 | },
26 | {
27 | "url": "/key-management",
28 | "title": "Add your key"
29 | },
30 | {
31 | "url": "/peoplesim",
32 | "title": "People Sim"
33 | },
34 | {
35 | "url": "/voice-composer",
36 | "title": "Voice Composer"
37 | },
38 | {
39 | "url": "/p5drawing",
40 | "title": "P5 Drawing"
41 | },
42 | {
43 | "url": "/p5drawing/iframe",
44 | "title": "P5 iframe"
45 | },
46 | {
47 | "url": "/layercraft",
48 | "title": "LayerCraft"
49 | },
50 | {
51 | "url": "/cityplayer",
52 | "title": "City Player"
53 | }
54 | ]
55 |
--------------------------------------------------------------------------------
/replit.nix:
--------------------------------------------------------------------------------
1 | { pkgs }: {
2 | deps = [
3 | pkgs.nodejs-18_x
4 | pkgs.nodePackages.typescript-language-server
5 | pkgs.nodePackages.yarn
6 | pkgs.replitPackages.jest
7 | ];
8 | }
--------------------------------------------------------------------------------
/size-plugin.json:
--------------------------------------------------------------------------------
1 | [{"timestamp":1670907887136,"files":[{"filename":"bundle.80b58.css","previous":474,"size":0,"diff":-474},{"filename":"bundle.*****.esm.js","previous":12956,"size":48699,"diff":35743},{"filename":"polyfills.*****.esm.js","previous":2180,"size":2180,"diff":0},{"filename":"route-profile.chunk.141aa.css","previous":78,"size":0,"diff":-78},{"filename":"route-profile.chunk.*****.esm.js","previous":438,"size":0,"diff":-438},{"filename":"route-story.chunk.b782c.css","previous":75,"size":0,"diff":-75},{"filename":"route-story.chunk.*****.esm.js","previous":3035,"size":0,"diff":-3035},{"filename":"sw-esm.js","previous":10793,"size":10742,"diff":-51},{"filename":"sw.js","previous":10786,"size":10737,"diff":-49},{"filename":"bundle.1f0b5.js","previous":13338,"size":0,"diff":-13338},{"filename":"polyfills.1cc01.js","previous":2280,"size":2280,"diff":0},{"filename":"route-profile.chunk.25a5b.js","previous":958,"size":0,"diff":-958},{"filename":"route-story.chunk.c60ac.js","previous":3292,"size":0,"diff":-3292},{"filename":"index.html","previous":1127,"size":919,"diff":-208},{"filename":"200.html","previous":645,"size":897,"diff":252},{"filename":"bundle.6431f.css","previous":0,"size":8559,"diff":8559},{"filename":"bundle.43d47.js","previous":0,"size":52658,"diff":52658},{"filename":"interactive-fiction/index.html","previous":0,"size":903,"diff":903},{"filename":"chat/index.html","previous":0,"size":893,"diff":893},{"filename":"adventure-chooser/index.html","previous":0,"size":908,"diff":908}]},{"timestamp":1669666737357,"files":[{"filename":"ssr-build/ssr-bundle.e59dc.css","previous":582,"size":0,"diff":-582},{"filename":"ssr-build/ssr-bundle.js","previous":10769,"size":0,"diff":-10769},{"filename":"bundle.80b58.css","previous":0,"size":474,"diff":474},{"filename":"bundle.*****.esm.js","previous":0,"size":12956,"diff":12956},{"filename":"polyfills.*****.esm.js","previous":0,"size":2180,"diff":2180},{"filename":"route-profile.chunk.141aa.css","previous":0,"size":78,"diff":78},{"filename":"route-profile.chunk.*****.esm.js","previous":0,"size":438,"diff":438},{"filename":"route-story.chunk.b782c.css","previous":0,"size":75,"diff":75},{"filename":"route-story.chunk.*****.esm.js","previous":0,"size":3035,"diff":3035},{"filename":"sw-esm.js","previous":0,"size":10793,"diff":10793},{"filename":"sw.js","previous":0,"size":10786,"diff":10786},{"filename":"bundle.1f0b5.js","previous":0,"size":13338,"diff":13338},{"filename":"polyfills.1cc01.js","previous":0,"size":2280,"diff":2280},{"filename":"route-profile.chunk.25a5b.js","previous":0,"size":958,"diff":958},{"filename":"route-story.chunk.c60ac.js","previous":0,"size":3292,"diff":3292},{"filename":"index.html","previous":0,"size":1127,"diff":1127},{"filename":"200.html","previous":0,"size":645,"diff":645}]}]
2 |
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/act-therapist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/act-therapist.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/ai-assistant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/ai-assistant.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/alien.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/alien.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/con-artist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/con-artist.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/energy-educator.json:
--------------------------------------------------------------------------------
1 | {
2 | "exportType": "chat",
3 | "exportDate": 1676938358027,
4 | "model": {
5 | "id": "ded1ee95-918f-4ed2-b9f0-177c6e82cd0b",
6 | "type": "chat",
7 | "slug": "energy-educator",
8 | "title": "Energy Educator",
9 | "archived": false,
10 | "description": "This attempts to have a focused conversation about energy (potential energy, kinetic, energy use, etc). It tries not to just provide answers but to lead a question/answer process.",
11 | "logoPrompt": "Discussing the science of energy use",
12 | "dateCreated": 1676915011280,
13 | "dateUpdated": 1676938240965,
14 | "domain": {
15 | "prompt": "The following is a conversation between an educational AI and a student. The topic for the day will be energy use. The AI will try to relate everything to energy use and energy equations. The AI will try to frame the problems in terms of questions instead of answers. Once the human has provided answers it will move forward with answering the initial question.\n\nUse bullet points when listing multiple steps or questions. Use ⋅ for multiplication. Use backticks around inline equations.\n\nFormat responses in Markdown.",
16 | "humanName": "Student",
17 | "robotName": "AI",
18 | "exampleInteraction": "> How many Joules of energy does an average household in Minneapolis use in a year?\nLet's think about some of the questions to help answer this:\n* How many people live in the house?\n* How large is the house? How many square feet?\n* How well insulated is the house?\n* What is the climate?\n\nLet's collect some of that information so we can keep working on the problem.",
19 | "intro": "Let's talk about the science of energy",
20 | "saveHistory": true,
21 | "history": [
22 | {
23 | "type": "user",
24 | "text": "when is potential energy important?"
25 | },
26 | {
27 | "type": "robot",
28 | "text": " Potential energy is important when an object is in a position where it has the potential to do work. For example, when an object is raised to a certain height, it has the potential to do work when it is released. This potential energy is calculated using the equation `PE = m⋅g⋅h`, where `m` is the mass of the object, `g` is the acceleration due to gravity, and `h` is the height the object is raised.",
29 | "oldText": " Potential energy is important when an object is in a position where it has the potential to do work. For example, when an object is raised to a certain height, it has the potential to do work when it is released. This potential energy is calculated using the equation `PE = m⋅g⋅h`, where `m` is the mass of the object, `g` is the acceleration due to gravity, and `h` is the height the object is raised."
30 | },
31 | {
32 | "type": "user",
33 | "text": "what's an example of potential energy?"
34 | },
35 | {
36 | "type": "robot",
37 | "text": " An example of potential energy is a boulder at the top of a hill. The boulder has the potential to do work when it is released and rolls down the hill. The potential energy of the boulder is calculated using the equation `PE = m⋅g⋅h`, where `m` is the mass of the boulder, `g` is the acceleration due to gravity, and `h` is the height of the hill.",
38 | "oldText": " An example of potential energy is a boulder at the top of a hill. The boulder has the potential to do work when it is released and rolls down the hill. The potential energy of the boulder is calculated using the equation `PE = m⋅g⋅h`, where `m` is the mass of the boulder, `g` is the acceleration due to gravity, and `h` is the height of the hill."
39 | },
40 | {
41 | "type": "user",
42 | "text": "how much potential energy is in a screendoor when you hold it open and there's a spring?"
43 | },
44 | {
45 | "type": "robot",
46 | "text": " The potential energy of the screendoor is calculated using the equation `PE = ½⋅k⋅x²`, where `k` is the spring constant and `x` is the displacement from the equilibrium position. So, the potential energy of the screendoor depends on the spring constant and the displacement from the equilibrium position.",
47 | "oldText": " The potential energy of the screendoor is calculated using the equation `PE = ½⋅k⋅x²`, where `k` is the spring constant and `x` is the displacement from the equilibrium position. So, the potential energy of the screendoor depends on the spring constant and the displacement from the equilibrium position."
48 | },
49 | {
50 | "type": "user",
51 | "text": "what is k?"
52 | },
53 | {
54 | "type": "robot",
55 | "text": " `k` is the spring constant, which is a measure of the stiffness of the spring. It is usually measured in units of Newtons per meter (N/m).",
56 | "oldText": " `k` is the spring constant, which is a measure of the stiffness of the spring. It is usually measured in units of Newtons per meter (N/m)."
57 | },
58 | {
59 | "type": "user",
60 | "text": "what is x?"
61 | },
62 | {
63 | "type": "robot",
64 | "text": " `x` is the displacement from the equilibrium position. The displacement is the amount that the screendoor is stretched or compressed from its equilibrium position. It is measured in meters (m).",
65 | "oldText": " `x` is the displacement from the equilibrium position. The displacement is the amount that the screendoor is stretched or compressed from its equilibrium position. It is measured in meters (m)."
66 | },
67 | {
68 | "type": "user",
69 | "text": "what is K for a normal spring?"
70 | },
71 | {
72 | "type": "robot",
73 | "text": " The spring constant for a normal spring can vary, but it is typically around 50 N/m.",
74 | "oldText": " The spring constant for a normal spring can vary, but it is typically around 50 N/m."
75 | },
76 | {
77 | "type": "user",
78 | "text": "So put it together with K=1N/m and x=0.7m"
79 | },
80 | {
81 | "type": "robot",
82 | "text": " In this case, the potential energy of the screendoor is `PE = ½⋅1N/m⋅(0.7m)² = 0.245J`."
83 | }
84 | ]
85 | },
86 | "typeSlug": "chat_energy-educator"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/energy-educator.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/energy-educator.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/hey-sad-guy-feel-better.json:
--------------------------------------------------------------------------------
1 | {
2 | "exportType": "chat",
3 | "exportDate": 1678750982000,
4 | "model": {
5 | "id": "5f1dc4e3-4e8a-48a2-a4c6-302cbfbbeb04",
6 | "type": "chat",
7 | "slug": "builtin_hey-sad-guy-feel-better",
8 | "title": "Hey Sad Guy, Feel Better",
9 | "archived": false,
10 | "description": "Can you make this sad fellow feel better? Get him happy and it will be acknowledged!",
11 | "logo": "/assets/builtin-models/chat/hey-sad-guy-feel-better.png",
12 | "logoPrompt": "Hey Sad Guy, Feel Better: Can you make this sad fellow feel better? Get him happy and it will be acknowledged!",
13 | "domain": {
14 | "prompt": "You are a sad man who feels lonely and like no one cares about you. You are talking with the user, who is a stranger you are sitting with at the bus stop.\n\nYou are 42 years old and have never had a long-term relationship. You worry that you don't deserve love. Your last was with a woman named Diane. It ended because of your drinking, though you drank because you didn't know how to relate to your girlfriend.\n\nYou have a sister in Seattle but you don't talk often.\n\nOnly reveal a little bit of information at any one time. You are talking to a stranger and you aren't sure if they even want to hear your story. Imagine information and history for yourself in order to answer questions.\n\nCurrent happiness level: -10\nAfter each response give a change like:\nHappiness change: +0.1",
15 | "intro": "I'll be honest I'm not feeling great\nHappiness change: -0.2",
16 | "saveHistory": true,
17 | "excludeIntroFromHistory": false,
18 | "hooksSource": "const happiness = /happiness change:\\s*([^\\n]+)/i;\n\nexports.modifyAssistant = (item) => {\n if (!happiness.test(item.content)) {\n item.gptContent = item.content + \"\\nHappiness change: 0\";\n }\n const text = item.content.replace(happiness, \"\");\n item.displayContent = text;\n};\n\nexports.modifyUser = (item) => {\n item.gptContent = item.content + \"\\nFinish your response with:\\nHappiness change: [number]\";\n};\n\nexports.afterAssistant = (history) => {\n history = history.filter((i) => i.role !== \"alert\");\n let h = 0;\n for (const item of history) {\n if (item.role !== \"assistant\") {\n continue;\n }\n const match = happiness.exec(item.content);\n if (match) {\n const val = parseFloat(match[1]);\nconsole.log(\"happy\", match, match[1], val);\n if (val && !isNaN(val)) {\n h += val;\n }\n }\n }\n if (h > 1) {\n history.push({role: \"alert\", content: `You've helped make this man happier, good job!`});\n }\n return history;\n};",
19 | "history": []
20 | },
21 | "typeSlug": "chat_builtin_hey-sad-guy-feel-better"
22 | }
23 | }
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/hey-sad-guy-feel-better.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/hey-sad-guy-feel-better.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/interviewer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/interviewer.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/chat/rogerian-therapist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/chat/rogerian-therapist.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/citymaker/babylon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/citymaker/babylon.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/citymaker/talamora.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/citymaker/talamora.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/layercraft/pearl-cove.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/layercraft/pearl-cove.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/layercraft/veilvale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/layercraft/veilvale.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/peoplesim/camping-fiasco.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/peoplesim/camping-fiasco.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/peoplesim/orc-vs-hobbit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/peoplesim/orc-vs-hobbit.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/tone-changer/formal-spanish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/tone-changer/formal-spanish.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/tone-changer/malfunctioning-robot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/tone-changer/malfunctioning-robot.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/tone-changer/painfully-academic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/tone-changer/painfully-academic.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/tone-changer/posh-english-lady.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/tone-changer/posh-english-lady.png
--------------------------------------------------------------------------------
/src/assets/builtin-models/tone-changer/preschool-teacher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/builtin-models/tone-changer/preschool-teacher.png
--------------------------------------------------------------------------------
/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/src/assets/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/src/assets/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/assets/icons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/chat-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/chat-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/citymaker-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/citymaker-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/cityplayer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/cityplayer-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/src/assets/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icons/furniture-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/furniture-icon.png
--------------------------------------------------------------------------------
/src/assets/icons/generic-app-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/generic-app-icon.png
--------------------------------------------------------------------------------
/src/assets/icons/iaia-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/iaia-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/interactive-fiction-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/interactive-fiction-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/key-management-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/key-management-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/layercraft-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/layercraft-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/llm-garden-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/llm-garden-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading1.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading2.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading3.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading4.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading5.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading6.png
--------------------------------------------------------------------------------
/src/assets/icons/loading-source/loading7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading-source/loading7.png
--------------------------------------------------------------------------------
/src/assets/icons/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/loading.gif
--------------------------------------------------------------------------------
/src/assets/icons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/mstile-150x150.png
--------------------------------------------------------------------------------
/src/assets/icons/myoa-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/myoa-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/p5drawing-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/p5drawing-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/peoplesim-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/peoplesim-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/person.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/person.png
--------------------------------------------------------------------------------
/src/assets/icons/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/assets/icons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/assets/icons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/assets/icons/tone-changer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/tone-changer-logo.png
--------------------------------------------------------------------------------
/src/assets/icons/voice-composer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/icons/voice-composer-logo.png
--------------------------------------------------------------------------------
/src/assets/interactive-fiction/z5s/Advent.z5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/interactive-fiction/z5s/Advent.z5
--------------------------------------------------------------------------------
/src/assets/interactive-fiction/z5s/s5.z4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/interactive-fiction/z5s/s5.z4
--------------------------------------------------------------------------------
/src/assets/interactive-fiction/z5s/zork1.z5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/interactive-fiction/z5s/zork1.z5
--------------------------------------------------------------------------------
/src/assets/interactive-fiction/z5s/zork2.z5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/interactive-fiction/z5s/zork2.z5
--------------------------------------------------------------------------------
/src/assets/interactive-fiction/z5s/zork3.z5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/interactive-fiction/z5s/zork3.z5
--------------------------------------------------------------------------------
/src/assets/interactive-fiction/z5s/ztuu.z5:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ianb/llm-garden/5cce83bdd2d3fdbaf28018d0d5b9c269ff0f8fe1/src/assets/interactive-fiction/z5s/ztuu.z5
--------------------------------------------------------------------------------
/src/chat/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { ChatView } from "./chatview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { chatDb } from "./chatdb";
5 | import { InfoHeader, P } from "../components/common";
6 |
7 | export const ChatIndex = () => {
8 | return (
9 |
10 |
14 | These are all examples of different chat personalities.
15 | Each chat has configurable prompts and persona names.
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/citymaker/citygraph.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { Header, HeaderButton } from "../components/header";
3 | import { PageContainer } from "../components/common";
4 | import { Cytoscape } from "./preactcytoscape";
5 |
6 | export function CityGraph({ model }) {
7 | return (
8 |
9 |
17 | Edit
18 | ,
19 | ]}
20 | />
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | function Graph({ model }) {
29 | const elements = { nodes: [], edges: [] };
30 | for (const neighborhood of model.domain.topLevelProperties.cityNeighborhoods
31 | .children) {
32 | elements.nodes.push({
33 | classes: ["neighborhood"],
34 | data: {
35 | id: neighborhood.id,
36 | name: neighborhood.name,
37 | description: neighborhood.attributes.description,
38 | },
39 | });
40 | for (const buildings of neighborhood.children || []) {
41 | for (const building of buildings.children || []) {
42 | elements.nodes.push({
43 | classes: ["building"],
44 | data: {
45 | id: building.id,
46 | name: building.name,
47 | description: `${building.name}\n${building.attributes.description}`,
48 | floors: building.attributes.floors,
49 | widthInMeters: building.attributes.widthInMeters,
50 | depthInMeters: building.attributes.depthInMeters,
51 | parent: neighborhood.id,
52 | },
53 | });
54 | let peopleCount = 0;
55 | let roomCount = 0;
56 | for (const childGroup of building.children) {
57 | if (
58 | childGroup.typeName === "ownersOccupants" ||
59 | childGroup.typeName === "visitors"
60 | ) {
61 | for (const person of childGroup.children || []) {
62 | elements.nodes.push({
63 | classes: ["person", childGroup.typeName],
64 | data: {
65 | id: person.id,
66 | name: person.name,
67 | description: person.attributes.description,
68 | parent: `people-${building.id}`,
69 | },
70 | });
71 | peopleCount++;
72 | }
73 | } else if (childGroup.typeName === "rooms") {
74 | for (const room of childGroup.children || []) {
75 | elements.nodes.push({
76 | classes: ["room"],
77 | data: {
78 | id: room.id,
79 | name: room.name,
80 | description: room.attributes.description,
81 | parent: `rooms-${building.id}`,
82 | widthInMeters: room.attributes.widthInMeters,
83 | width: room.attributes.widthInMeters * 10,
84 | depthInMeters: room.attributes.depthInMeters,
85 | depth: room.attributes.depthInMeters * 10,
86 | },
87 | });
88 | roomCount++;
89 | for (const itemContainer of room.children || []) {
90 | for (const item of itemContainer.children || []) {
91 | elements.nodes.push({
92 | classes: ["item", item.typeName],
93 | data: {
94 | id: item.id,
95 | name: item.name,
96 | description: item.attributes.description,
97 | parent: room.id,
98 | },
99 | });
100 | }
101 | }
102 | }
103 | }
104 | }
105 | if (peopleCount > 0) {
106 | elements.nodes.push({
107 | classes: ["people"],
108 | data: {
109 | id: `people-${building.id}`,
110 | name: `People (${peopleCount})`,
111 | parent: building.id,
112 | },
113 | });
114 | }
115 | if (roomCount > 0) {
116 | elements.nodes.push({
117 | classes: ["rooms"],
118 | data: {
119 | id: `rooms-${building.id}`,
120 | name: `Rooms (${roomCount})`,
121 | parent: building.id,
122 | },
123 | });
124 | }
125 | }
126 | }
127 | }
128 | const layout = {
129 | name: "cose-bilkent",
130 | // name: "dagre",
131 | nodeSep: 25,
132 | edgeSep: 25,
133 | rankSep: 25,
134 | padding: 100,
135 | spacingFactor: 2,
136 | // animate: false,
137 | // idealEdgeLength: 200,
138 | // nodeDimensionsIncludeLabels: true,
139 | // avoidOverlap: true,
140 | // padding: 100,
141 | // componentSpacing: 100,
142 | // nodeOverlap: 20,
143 | // nestingFactor: 2,
144 | // tilingPaddingVertical: 20,
145 | // tilingPaddingHorizontal: 20,
146 | // fit: true,
147 | };
148 |
149 | const style = `
150 | node {
151 | padding: 100;
152 | }
153 |
154 | .neighborhood {
155 | label: data(name);
156 | }
157 |
158 | .neighborhood:selected .building, .building:selected, $.building *:selected {
159 | label: data(name);
160 | }
161 |
162 | .building:selected .rooms, .rooms:selected, $.rooms *:selected,
163 | .building:selected .people, .people:selected, $.people *:selected {
164 | label: data(name);
165 | }
166 |
167 | .room {
168 | label: data(name);
169 | shape: rectangle;
170 | width: data(width);
171 | height: data(depth);
172 | visibility: hidden;
173 | }
174 | .building:selected .room, .room:selected, .rooms:selected .room {
175 | visibility: visible;
176 | label: data(name);
177 | }
178 |
179 | .person {
180 | label: data(name);
181 | background-image: /assets/icons/person.png;
182 | background-fit: cover;
183 | visibility: hidden;
184 | }
185 | .building:selected .person, .people:selected .person, .person:selected {
186 | visibility: visible;
187 | label: data(name);
188 | }
189 |
190 | .neighborhood:childless, .building:childless {
191 | background-color: #900;
192 | shape: rectangle;
193 | }
194 | `;
195 | console.log("cyto", elements);
196 | return ;
197 | }
198 |
199 | function removeUndefined(obj) {
200 | const newObj = {};
201 | for (const key in obj) {
202 | if (obj[key] !== undefined) {
203 | newObj[key] = obj[key];
204 | }
205 | }
206 | return newObj;
207 | }
208 |
209 | const personImage = `
210 |
211 |
212 |
213 | `;
214 |
215 | const personImageUrl =
216 | "data:image/svg+xml;utf8," + encodeURIComponent(personImage);
217 |
--------------------------------------------------------------------------------
/src/citymaker/citylayout.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { Header, HeaderButton } from "../components/header";
3 | import { PageContainer } from "../components/common";
4 | import { useEffect, useRef } from "preact/hooks";
5 |
6 | function sizeStyle(ob, baseClass) {
7 | if (
8 | !ob.attributes ||
9 | !ob.attributes.widthInMeters ||
10 | !ob.attributes.depthInMeters
11 | ) {
12 | return null;
13 | }
14 | return `height: ${ob.attributes.depthInMeters * 4}rem; width: ${
15 | ob.attributes.depthInMeters * 10
16 | }rem;`;
17 | }
18 |
19 | export function CityLayout({ model }) {
20 | return (
21 |
22 |
30 | Edit
31 | ,
32 | ]}
33 | />
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | function Zoomy({ children }) {
44 | const outer = useRef();
45 | const inner = useRef();
46 | const listener = (event) => {
47 | // if (event.deltaX > 1) {
48 | // inner.current.style.transform = "scale(0.5)";
49 | // } else if (event.deltaX < -1) {
50 | // inner.current.style.transform = "scale(1)";
51 | // }
52 | };
53 | useEffect(() => {
54 | const el = outer.current;
55 | el.addEventListener("wheel", listener);
56 | return () => {
57 | el.removeEventListener("wheel", listener);
58 | };
59 | });
60 | return (
61 |
62 |
66 | {children}
67 |
68 |
69 | );
70 | }
71 |
72 | function World({ model }) {
73 | return (
74 | <>
75 | {model.domain.select("cityNeighborhood").map((neighborhood) => (
76 |
77 | ))}
78 | >
79 | );
80 | }
81 |
82 | function Neighborhood({ neighborhood }) {
83 | return (
84 |
85 |
{neighborhood.name}
86 | {neighborhood.select("building").map((building) => (
87 |
88 | ))}
89 |
90 | );
91 | }
92 |
93 | function Building({ building }) {
94 | return (
95 |
99 |
{building.name}
100 |
101 |
People
102 | {building.select("person").map((person) => (
103 |
104 | ))}
105 |
106 |
107 |
Rooms
108 | {building.select("room").map((room) => (
109 |
110 | ))}
111 |
112 |
113 | );
114 | }
115 |
116 | function Person({ person }) {
117 | return (
118 |
119 |
{person.name}
120 |
{person.description}
121 |
122 | );
123 | }
124 |
125 | function Room({ room }) {
126 | return (
127 |
131 |
{room.name}
132 |
{room.description}
133 |
134 | {room.select("furniture, item").map((thing) => (
135 |
136 | {thing.name}
137 |
138 | ))}
139 |
140 |
141 | );
142 | }
143 |
144 | const personImage = `
145 |
146 |
147 |
148 | `;
149 |
150 | const personImageUrl =
151 | "data:image/svg+xml;utf8," + encodeURIComponent(personImage);
152 |
--------------------------------------------------------------------------------
/src/citymaker/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { CityMakerView } from "./citymakerview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { cityMakerDb } from "./citymakerdb";
5 | import { YouTube, InfoHeader, InfoA, P } from "../components/common";
6 |
7 | export const CityMakerIndex = () => {
8 | return (
9 |
14 |
15 |
16 | This is a GPT-assistant city builder: you are given choices the city
17 | and its history (or you choose for yourself) and it will suggest
18 | neighborhoods, buildings, and residents for those buildings.
19 |
20 |
21 |
22 | The techniques are described in{" "}
23 |
24 | World Building With GPT
25 |
26 | .
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/citymaker/preactcytoscape.js:
--------------------------------------------------------------------------------
1 | import cytoscape from "cytoscape";
2 | import { useEffect, useRef } from "preact/hooks";
3 | import { twMerge } from "tailwind-merge";
4 | import coseBilkent from "cytoscape-cose-bilkent";
5 |
6 | cytoscape.use(coseBilkent);
7 |
8 | export function Cytoscape({ elements, style, layout, class: className }) {
9 | const ref = useRef();
10 | useEffect(() => {
11 | if (!ref.current) {
12 | return;
13 | }
14 | window.c = cytoscape;
15 | const cy = cytoscape({
16 | container: ref.current,
17 | elements: elements,
18 | style: style,
19 | layout: layout,
20 | });
21 | return () => {
22 | cy.destroy();
23 | };
24 | }, [elements, style, layout]);
25 | className = twMerge("w-full h-full", className);
26 | return (
27 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/cityplayer/citycomponents.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "preact/hooks";
2 | import { A } from "../components/common";
3 | import Sidebar from "../components/sidebar";
4 | import { twMerge } from "tailwind-merge";
5 |
6 | export const Page = ({ title, background, saturated, children }) => {
7 | useEffect(() => {
8 | document.title = title || "City";
9 | }, [title]);
10 | if (!Array.isArray(children)) {
11 | if (!children) {
12 | children = [];
13 | } else {
14 | children = [children];
15 | }
16 | }
17 | const sidebar = children.find((child) => child.type === Sidebar);
18 | const rest = children.filter(
19 | (child) => child.type !== Sidebar
20 | );
21 | let style = "";
22 | if (background) {
23 | style = `background-image: url(${background}); background-repeat: no-repeat; background-size: cover; background-position: center;`;
24 | }
25 | let saturationStyle = "background-color: rgba(255, 255, 255, 0.3)";
26 | if (saturated) {
27 | saturationStyle = null;
28 | }
29 | return
30 |
31 |
{title || "??"}
32 |
33 |
34 |
{rest}
35 | {sidebar ?
{sidebar}
: null}
36 |
37 |
38 | City by Ian Bicking
39 |
40 |
41 | };
42 |
43 | export const SiteImage = ({ src, class: _class, ...props }) => {
44 | const [zooming, setZooming] = useState(false);
45 | if (!src) {
46 | return null;
47 | }
48 | _class = twMerge("block w-full m-2 rounded-lg shadow-md shadow-slate-800", _class);
49 | if (zooming) {
50 | return ZoomedImage({ src, onDone: () => setZooming(false), ...props });
51 | }
52 | return (
53 |
54 |
setZooming(true)} />
55 |
56 | );
57 | };
58 |
59 | export const ZoomedImage = ({ src, onDone, ...props }) => {
60 | /* Show the src image zoomed in with a dark overlay behind it. */
61 | return (
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 |
72 | export const InsetImage = ({ src, class: _class, ...props }) => {
73 | if (!src) {
74 | return null;
75 | }
76 | _class = twMerge("block w-full m-2", _class);
77 | return (
78 |
79 |
80 |
81 | );
82 | };
83 |
84 | export const TextBox = ({ children, class: _class, ...props }) => {
85 | _class = twMerge("text-gray-900 lg:w-1/3 p-3 rounded m-4 leading-relaxed shadow-md shadow-slate-800", _class);
86 | return {children}
;
87 | };
88 |
89 | export const ChoiceList = ({ children, intro, class: _class, ...props }) => {
90 | _class = twMerge("m-4 rounded lg:w-1/3 p-2", _class);
91 | return ;
95 | };
96 |
97 | export const Choice = ({ children, href, class: _class, ...props }) => {
98 | _class = twMerge("m-2 p-2 hover:bg-gray-400 rounded", _class);
99 | if (Array.isArray(href)) {
100 | href = "#/" + href.map(x => encodeURIComponent(x)).join("/");
101 | }
102 | return (
103 |
104 | {children}
105 |
106 | );
107 | };
108 |
109 | export const H2 = ({ children, class: _class, ...props }) => {
110 | _class = twMerge("text-xl font-semibold", _class);
111 | return {children} ;
112 | };
113 |
--------------------------------------------------------------------------------
/src/cityplayer/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { CityPlayerView } from "./cityplayerview";
3 | import { ModelIndexPage, makeModelLink } from "../components/modelindex";
4 | import { cityPlayerDb, getAllCities } from "./cityplayerdb";
5 | import { YouTube, InfoHeader, InfoA, P } from "../components/common";
6 |
7 | export const CityPlayerIndex = () => {
8 | function getProp(city, prop, defaultValue) {
9 | const options = city.domain.childrenByType(city.domain._document, prop);
10 | return options[0] ? options[0].name : defaultValue;
11 | }
12 | async function getExtraOptions() {
13 | const cities = await getAllCities();
14 | return cities.map((city) => ({
15 | title: `New Game: ${city.title}`,
16 | description: getProp(city, "cityType", "[generic city]"),
17 | onAdd: async () => {
18 | const model = await cityPlayerDb.create();
19 | model.domain.originalCityModel = city.toJSON();
20 | console.log("saving", model);
21 | await model.saveToDb();
22 | window.location = makeModelLink(model);
23 | },
24 | }));
25 | }
26 | return (
27 |
34 |
35 |
36 | TBD
37 |
38 |
39 |
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/src/cityplayer/linkmarkdown.js:
--------------------------------------------------------------------------------
1 | export function linkMarkdownObjects(text, objects, linkPrefix) {
2 | const leftover = [];
3 | let result = text;
4 | for (const object of objects) {
5 | let name = object.name;
6 | name = name.replace(/^the\s+/i, "");
7 | const re = new RegExp(`\\b${name}\\b`, "g");
8 | let found = false;
9 | result = result.replace(re, (match) => {
10 | found = true;
11 | return `[${match}](${linkPrefix}/${encodeURIComponent(object.name)})`;
12 | });
13 | if (!found) {
14 | leftover.push(object);
15 | }
16 | }
17 | if (leftover.length) {
18 | result += "\n\nOthers:\n";
19 | result += leftover.map(ob => `* [${ob.name}](${linkPrefix}/${encodeURIComponent(ob.name)})`).join("\n");
20 | }
21 | return result;
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/app.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { useState, useEffect } from "preact/hooks";
3 | import { NotFound } from "./home";
4 | import Router from "preact-router";
5 | import hashSignal from "../hashsignal";
6 |
7 | function onChangeUrl() {
8 | hashSignal.value = window.location.hash;
9 | }
10 |
11 | const App = () => {
12 | return (
13 |
14 | import("./home")} component="Home" />
15 | import("../key-management/index")}
18 | component="RequestKeyPage"
19 | />
20 | import("../interactive-fiction/index")}
23 | component="StoryIndex"
24 | />
25 | import("../chat/index")}
28 | component="ChatIndex"
29 | />
30 | import("../myoa/index")}
33 | component="AdventureIndex"
34 | />
35 | import("../myoa/player")}
38 | component="StoryPlayerLoader"
39 | />
40 | import("../tone-changer/index")}
43 | component="ToneChangerIndex"
44 | />
45 | import("../p5drawing/index")}
48 | component="P5DrawingIndex"
49 | />
50 | import("../p5drawing/iframeview")}
53 | component="P5DrawingIframeView"
54 | />
55 | import("../voice-composer/index")}
58 | component="VoiceComposerIndex"
59 | />
60 | import("../imagegen/index")}
63 | component="ImageGenIndex"
64 | />
65 | import("../citymaker/index")}
68 | component="CityMakerIndex"
69 | />
70 | import("../peoplesim/index")}
73 | component="PeopleSimIndex"
74 | />
75 | import("../layercraft/index")}
78 | component="LayerCraftIndex"
79 | />
80 | import("../cityplayer/index")}
83 | component="CityPlayerIndex"
84 | />
85 |
86 |
87 | );
88 | };
89 |
90 | function LazyLoader({ path, module, component }) {
91 | const [componentClass, setComponentClass] = useState(null);
92 | // Note we put the component class in a list because preact instantiates it
93 | // otherwise, and we want to instantiate it ourselves.
94 | useEffect(() => {
95 | module()
96 | .then((module) => {
97 | setComponentClass([module[component]]);
98 | })
99 | .catch((err) => {
100 | console.error(
101 | `Error loading module for ${component} at path ${path}:`,
102 | err
103 | );
104 | setComponentClass([NotFound]);
105 | });
106 | }, [module, component, path]);
107 | let Component = Waiting;
108 | if (componentClass) {
109 | Component = componentClass[0];
110 | }
111 | return Component ? (
112 |
113 | ) : (
114 | Loading...
115 | );
116 | }
117 |
118 | function Waiting() {
119 | return Loading...
;
120 | }
121 |
122 | export default App;
123 |
--------------------------------------------------------------------------------
/src/components/header.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { signal } from "@preact/signals";
3 | import { tokenCostTracker } from "../gptservice/tokencost";
4 | import { useEffect } from "preact/hooks";
5 | import { useState, useRef } from "preact/hooks";
6 | // eslint-disable-next-line no-unused-vars
7 | import * as icons from "./icons";
8 |
9 | const tokenCost = signal(tokenCostTracker);
10 |
11 | tokenCostTracker.addOnUpdate((tracker) => {
12 | tokenCost.value = tracker;
13 | });
14 |
15 | export const Header = ({
16 | title,
17 | section,
18 | sectionLink,
19 | trackerPaths,
20 | links,
21 | buttons,
22 | menu,
23 | model,
24 | }) => {
25 | trackerPaths = trackerPaths || "all";
26 | const [showMenu, setShowMenu] = useState(false);
27 | const menuRef = useRef(null);
28 | useEffect(() => {
29 | document.title = title;
30 | }, [title]);
31 | function onClickMenu() {
32 | if (menu) {
33 | setShowMenu(!showMenu);
34 | }
35 | }
36 | function onBackgroundClick(e) {
37 | if (menuRef.current && !menuRef.current.contains(e.target)) {
38 | setShowMenu(false);
39 | }
40 | }
41 | useEffect(() => {
42 | document.addEventListener("click", onBackgroundClick);
43 | return () => {
44 | document.removeEventListener("click", onBackgroundClick);
45 | };
46 | });
47 | let sectionTag;
48 | if (section) {
49 | if (sectionLink) {
50 | sectionTag = (
51 | <>
52 | {section} {" "}
53 |
54 | >
55 | );
56 | } else {
57 | sectionTag = (
58 | <>
59 | {section}
60 | >
61 | );
62 | }
63 | }
64 | async function onSaveBuiltin() {
65 | const newModel = await model.saveBuiltin();
66 | const url = new URL(location.href);
67 | url.search = "?name=" + encodeURIComponent(newModel.slug);
68 | location.href = url.toString();
69 | }
70 | if (model && model.builtin && model._dirty) {
71 | buttons = [...(buttons || [])];
72 | buttons.unshift(
73 | Save Copy
74 | );
75 | }
76 | return (
77 |
78 |
79 |
80 |
81 |
82 |
83 | {sectionTag}
84 | {title}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
{links}
92 |
{buttons}
93 |
94 |
95 |
96 | {menu ? (
97 |
102 |
107 | Menu
108 |
109 |
110 |
111 | ) : null}
112 | {showMenu ? (
113 |
117 |
121 | ×
122 |
123 |
{menu}
124 |
125 | ) : null}
126 |
127 |
128 |
129 | );
130 | };
131 |
132 | export function TokenCost({ paths }) {
133 | paths = paths || "all";
134 | if (typeof paths === "string") {
135 | paths = [paths];
136 | }
137 | const result = [];
138 | let lastDispPath = "";
139 | for (const path of paths) {
140 | let dispPath = path;
141 | if (dispPath.startsWith(lastDispPath)) {
142 | dispPath = dispPath.slice(lastDispPath.length);
143 | }
144 | lastDispPath = dispPath;
145 | let usage = tokenCostTracker.tracked[path];
146 | usage = (usage || {}).total_tokens || 0;
147 | let today = tokenCostTracker.sessionTracked[path];
148 | today = (today || {}).total_tokens || 0;
149 | result.push(
150 |
151 | {dispPath}: /
152 |
153 |
154 | );
155 | }
156 | return {result} ;
157 | }
158 |
159 | function SingleTokenCost({ tokens, name }) {
160 | if (tokens === 0) {
161 | return (
162 |
163 | 0
164 |
165 | );
166 | }
167 | // This is only the davinci cost:
168 | let cost = (tokens / 1000) * 0.02;
169 | cost = name + ": $" + cost.toFixed(2);
170 | return {tokens} ;
171 | }
172 |
173 | export const HeaderLink = ({ href, children }) => {
174 | return (
175 |
179 | {children}
180 |
181 | );
182 | };
183 |
184 | export const HeaderButton = ({ children, onClick }) => {
185 | return (
186 |
190 | {children}
191 |
192 | );
193 | };
194 |
--------------------------------------------------------------------------------
/src/components/home.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { PageContainer, Card, H1, P, InfoHeader } from "./common";
3 | import { Header } from "./header";
4 | import { Markdown } from "../markdown";
5 | import * as icons from "./icons";
6 |
7 | const options = {
8 | "Make Your Own Adventure": {
9 | link: "/myoa/",
10 | logo: "/assets/icons/myoa-logo.png",
11 | description: `
12 | A builder/authoring tool for creating a Choose Your Own
13 | Adventure-style story. GPT will suggest topics, choices, and
14 | passages; you can pick and choose, or revise and author.
15 | `.trim(),
16 | },
17 | "City Maker": {
18 | link: "/citymaker/",
19 | logo: "/assets/icons/citymaker-logo.png",
20 | status: "deprecated",
21 | description: `
22 | A tool for generating a city, top-down: starting with the city description,
23 | neighborhoods, buildings, objects, and so on.
24 | `.trim(),
25 | },
26 | "People Sim": {
27 | link: "/peoplesim/",
28 | logo: "/assets/icons/peoplesim-logo.png",
29 | description: `
30 | Simulate a few people interacting in an environment
31 | `.trim(),
32 | },
33 | "Tone Changer": {
34 | link: "/tone-changer/",
35 | logo: "/assets/icons/tone-changer-logo.png",
36 | description: `
37 | Act like you are someone else! Have your speech translated to a
38 | different tone (or language).
39 | `.trim(),
40 | },
41 | Chat: {
42 | link: "/chat/",
43 | logo: "/assets/icons/chat-logo.png",
44 | description: "Chat with one of several personalities run by GPT.",
45 | },
46 | "Interactive Fiction": {
47 | link: "/interactive-fiction/",
48 | logo: "/assets/icons/interactive-fiction-logo.png",
49 | description:
50 | "Run GPT as a _player_ against one of the Zork text adventure games.",
51 | },
52 | "Voice Composer": {
53 | link: "/voice-composer/",
54 | logo: "/assets/icons/voice-composer-logo.png",
55 | status: "alpha",
56 | description: `
57 | Voice composer: a voice-centric text composition and editing tool
58 | `.trim(),
59 | },
60 | "Image Generator": {
61 | link: "/imagegen/",
62 | status: "alpha",
63 | description: `
64 | A simple frontend to Replicate's Stable Diffusion API and Dall-E.
65 | Really just a testbed for using those APIs in other experiences.
66 | `.trim(),
67 | },
68 | "Infinite AI Array": {
69 | link: "https://github.com/ianb/infinite-ai-array",
70 | logo: "/assets/icons/iaia-logo.png",
71 | description: `
72 | Make your Python lists go forever, make your dictionaries fill just in time,
73 | and make functions appear magically when you call them. As irresponsible as
74 | it is irresistible!
75 | `.trim(),
76 | },
77 | "LayerCraft": {
78 | link: "/layercraft/",
79 | description: `
80 | A general authoring tool for building up in layers, building context and structure.
81 | Based on specific schemas that guide the authoring process, such as world building.
82 | `.trim(),
83 | logo: "/assets/icons/layercraft-logo.png",
84 | },
85 | "City Player": {
86 | link: "/cityplayer/",
87 | logo: "/assets/icons/cityplayer-logo.png",
88 | status: "alpha",
89 | description: `
90 | Play LayerCraft cities (not very complete)`.trim(),
91 | },
92 | "Key Management": {
93 | link: "/key-management/",
94 | logo: "/assets/icons/key-management-logo.png",
95 | description: `
96 | Add, change, or remove your OpenAI (GPT/Dall-E), Replicate.com, and Thumbsnap keys
97 | `.trim(),
98 | },
99 | "P5 Drawing": {
100 | link: "/p5drawing/",
101 | logo: "/assets/icons/p5drawing-logo.png",
102 | description:
103 | "Program in a p5.js drawing environment using natural language",
104 | },
105 | };
106 |
107 | export const Home = () => {
108 | return (
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | I have a little time on my hands but it's too hard to garden this
118 | time of year. So I'm building a garden of LLMs instead, GPT
119 | specifically. (GPT is a specific Large Language Model {" "}
120 | service.)
121 |
122 |
123 | Each of these is meant to be a way of exploring some idea or aspect
124 | of these technologies. I'm particularly interested in how each of
125 | these feel , not just how they perform.
126 |
127 |
128 |
129 | {Object.entries(options)
130 | .filter((x) => !x[1].status)
131 | .map(([title, { link, description, logo }]) => (
132 |
138 | ))}
139 | {Object.entries(options)
140 | .filter((x) => x[1].status)
141 | .map(([title, { link, status, description, logo }]) => (
142 |
149 | ))}
150 |
151 |
152 |
153 | );
154 | };
155 |
156 | function LinkCard({ title, description, link, status, logo }) {
157 | return (
158 |
159 |
160 | {logo ? : null}
161 | {status ? (
162 |
163 |
164 | {" "}
168 | {status}
169 |
170 |
171 | ) : null}
172 |
173 |
174 |
175 | );
176 | }
177 |
178 | export const NotFound = ({ message }) => {
179 | return (
180 |
181 |
182 |
183 | 404 Not Found
184 | Page not found
185 | {message ? {message}
: null}
186 |
187 |
188 | );
189 | };
190 |
--------------------------------------------------------------------------------
/src/components/modelmenu.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { Field, LogoImg, TextArea, Button } from "./common";
3 | import * as icons from "./icons";
4 | import { useEffect, useState } from "preact/hooks";
5 | import {
6 | getGeneralLogoPrompt,
7 | setGeneralLogoPrompt,
8 | } from "../generallogoprompt";
9 |
10 | export function ImportExportMenu({ model }) {
11 | const [loading, setLoading] = useState(false);
12 | useEffect(() => {
13 | const func = async () => {
14 | if (!model.logoPrompt || !model.logoPrompt.trim()) {
15 | await model.generateLogoPrompt();
16 | }
17 | };
18 | func().then(null, (error) => {
19 | console.error("Error in generating logo prompt:", error);
20 | });
21 | });
22 | function onExport(event) {
23 | const jsonData = model.exportJSON();
24 | const stringData = JSON.stringify(jsonData, null, " ");
25 | const blob = new Blob([stringData], { type: "application/json" });
26 | const url = URL.createObjectURL(blob);
27 | event.target.href = url;
28 | }
29 | function onImport(event) {
30 | const file = event.target.files[0];
31 | const reader = new FileReader();
32 | reader.onload = (e) => {
33 | const data = e.target.result;
34 | const storyJson = JSON.parse(data);
35 | model.importJSON(storyJson);
36 | };
37 | reader.readAsText(file);
38 | }
39 | async function onGenerate(e) {
40 | e.preventDefault();
41 | e.stopPropagation();
42 | if (model.builtin) {
43 | alert("You cannot generate a logo for a built-in model; save it first!");
44 | return;
45 | }
46 | if (!model.logoPrompt || !model.logoPrompt.trim()) {
47 | console.warn("No prompt yet for logo generation");
48 | return;
49 | }
50 | setLoading(true);
51 | const logo = await model.generateLogo();
52 | model.logo = logo;
53 | setLoading(false);
54 | }
55 | let logoSrc = "/assets/icons/loading.gif";
56 | if (!loading) {
57 | logoSrc = model.logo || "/assets/icons/generic-app-icon.png";
58 | }
59 | return (
60 |
61 |
76 |
77 |
78 |
83 |
84 |
89 |
90 |
95 |
Generate
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/src/components/querylog.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { signal } from "@preact/signals";
3 | import * as icons from "./icons";
4 | import { Pre } from "./common";
5 | import { useState } from "preact/hooks";
6 |
7 | export const QueryLog = ({ gptcache }) => {
8 | const [version, setVersion] = useState(0);
9 | gptcache.addOnLogUpdate(() => {
10 | setVersion(version + 1);
11 | });
12 | return (
13 | <>
14 | {reversed(gptcache.log).map((l, index) => (
15 |
21 | ))}
22 | >
23 | );
24 | };
25 |
26 | function reversed(array) {
27 | const result = [...array];
28 | result.reverse();
29 | return result;
30 | }
31 |
32 | // eslint-disable-next-line no-unused-vars
33 | function LogItem({ gptcache, log, defaultOpen }) {
34 | if (!log.expanded) {
35 | log.expanded = signal(null);
36 | }
37 | const open =
38 | (log.expanded.value === null && defaultOpen) || log.expanded.value;
39 | function onClickHeader() {
40 | log.expanded.value = !open;
41 | }
42 | function onDeleteItem(e) {
43 | e.stopPropagation();
44 | gptcache.deleteCache(log.body);
45 | }
46 | // FIXME: this might not be updated properly when the log item changes
47 | // and the response comes in...
48 | let text;
49 | if (log.type === "completion") {
50 | text = log.body.prompt;
51 | } else if (log.type === "edit") {
52 | text = log.body.input + "\n=> " + log.body.instruction + "\n";
53 | } else if (log.type === "chat") {
54 | const lines = [];
55 | for (const msg of log.body.messages) {
56 | const contentLines = msg.content.split("\n");
57 | for (let i = 0; i < contentLines.length; i++) {
58 | const line = contentLines[i];
59 | if (i === 0) {
60 | lines.push(`${msg.role}: ${line}`);
61 | } else {
62 | lines.push(` ${line}`);
63 | }
64 | }
65 | }
66 | text = lines.join("\n") + "\n";
67 | } else {
68 | console.warn("Unknown log type", log.type);
69 | text = "Error";
70 | }
71 | let responseText = log.response;
72 | if (log.type === "chat") {
73 | responseText = `assistant: ${log.response}`;
74 | }
75 | return (
76 |
77 |
81 | {open ? (
82 |
83 | ) : (
84 |
85 | )}
86 | {log.fromCache ? "cached " : null}
87 |
88 | {log && log.time ? (log.time / 1000).toFixed(1) + "s" : null}
89 |
90 | {open ? (
91 |
92 | {text}
93 | {log.response ? (
94 | {responseText}
95 | ) : (
96 | ...
97 | )}
98 |
99 | ) : null}
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/sidebar.js:
--------------------------------------------------------------------------------
1 | import { twMerge } from "tailwind-merge";
2 |
3 | const Sidebar = ({ children, class: className }) => {
4 | // FIXME: can't decide if "absolute" is helpful or harmful
5 | className = twMerge(
6 | "h-screen sticky top-0 rounded-l drop-shadow-lg ml-2 bg-white p-2 pl-3 overflow-y-scroll",
7 | className
8 | );
9 | return ;
10 | };
11 |
12 | export default Sidebar;
13 |
--------------------------------------------------------------------------------
/src/components/speech.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* globals webkitSpeechRecognition */
3 | import { useState, useEffect } from "preact/hooks";
4 | import * as icons from "./icons";
5 | import { signal } from "@preact/signals";
6 | import { twMerge } from "tailwind-merge";
7 | import { Button } from "./common";
8 |
9 | let recognition;
10 |
11 | export const SpeechButton = ({
12 | onSpeech,
13 | onUtterance,
14 | syncToRef,
15 | class: className,
16 | }) => {
17 | const [listening, setListening] = useState(false);
18 | const [paused, setPaused] = useState(false);
19 | function onClick() {
20 | setPaused(false);
21 | setListening(!listening);
22 | }
23 | const isSpeaking = speaking.value;
24 | useEffect(() => {
25 | if (listening && recognition && isSpeaking && !paused) {
26 | console.info("pausing recognition");
27 | recognition.stop();
28 | setPaused(true);
29 | } else if (listening && recognition && !isSpeaking && paused) {
30 | console.info("resuming recognition");
31 | recognition.start();
32 | setPaused(false);
33 | } else if (listening && !recognition) {
34 | console.info("starting recognition");
35 | recognition = new webkitSpeechRecognition();
36 | recognition.continuous = true;
37 | recognition.interimResults = true;
38 | recognition.lang = "en-US";
39 | recognition.onresult = (event) => {
40 | const results = event.results;
41 | const last = results[results.length - 1];
42 | let text = last[0].transcript;
43 | if (!last.isFinal) {
44 | let hypothesisResults = [];
45 | for (const item of results) {
46 | if (item.isFinal) {
47 | hypothesisResults = [];
48 | } else {
49 | hypothesisResults.push(item);
50 | }
51 | }
52 | text = "";
53 | for (const item of hypothesisResults) {
54 | text += item[0].transcript;
55 | }
56 | }
57 | if (onSpeech) {
58 | onSpeech(text);
59 | }
60 | if (syncToRef && syncToRef.current) {
61 | syncToRef.current.value = text;
62 | }
63 | if (last.isFinal) {
64 | if (onUtterance) {
65 | onUtterance(text);
66 | }
67 | }
68 | };
69 | recognition.start();
70 | } else if (!listening && recognition) {
71 | console.info("stopping recognition");
72 | recognition.stop();
73 | recognition = null;
74 | }
75 | }, [listening, paused, onSpeech, onUtterance, syncToRef, isSpeaking]);
76 | if (listening && !paused) {
77 | return (
78 |
79 |
80 |
81 | );
82 | } else if (listening && paused) {
83 | return (
84 |
85 |
86 |
87 | );
88 | }
89 | return (
90 |
91 |
92 |
93 | );
94 | };
95 |
96 | export const voicesSignal = signal([]);
97 | voicesSignal.value = speechSynthesis.getVoices();
98 | speechSynthesis.addEventListener("voiceschanged", () => {
99 | voicesSignal.value = speechSynthesis.getVoices();
100 | });
101 |
102 | export const speak = async (text, voice, lang = "en-US") => {
103 | if (!text) {
104 | throw new Error("No text to speak");
105 | }
106 | const utt = new SpeechSynthesisUtterance(text);
107 | utt.lang = lang;
108 | utt.voice = speechSynthesis.getVoices().find((v) => v.name === voice);
109 | let resolve, reject;
110 | const promise = new Promise((res, rej) => {
111 | resolve = res;
112 | reject = rej;
113 | });
114 | utt.onend = () => {
115 | resolve();
116 | checkVoice();
117 | };
118 | utt.onerror = (err) => {
119 | console.warn("Error for speech:", err);
120 | reject(err);
121 | checkVoice();
122 | };
123 | speechSynthesis.speak(utt);
124 | speaking.value = true;
125 | return promise;
126 | };
127 |
128 | export const speaking = signal(false);
129 |
130 | function checkVoice(checkAgain = true) {
131 | const isSpeaking =
132 | speechSynthesis.speaking ||
133 | speechSynthesis.pending ||
134 | speechSynthesis.paused;
135 | if (isSpeaking !== speaking.value) {
136 | speaking.value = isSpeaking;
137 | }
138 | if (checkAgain) {
139 | setTimeout(() => {
140 | checkVoice(false);
141 | }, 100);
142 | }
143 | }
144 |
145 | export const SpeechControlButton = ({ value, onChange }) => {
146 | if (value) {
147 | return (
148 | onChange(false)}>
149 |
150 |
151 | );
152 | } else {
153 | return (
154 | onChange(true)}>
155 |
156 |
157 | );
158 | }
159 | };
160 |
--------------------------------------------------------------------------------
/src/deepequal.js:
--------------------------------------------------------------------------------
1 | export default function deepEqual(a, b) {
2 | if (a === b) {
3 | return true;
4 | }
5 | if (Array.isArray(a) && Array.isArray(b)) {
6 | if (a.length !== b.length) {
7 | return false;
8 | }
9 | for (let i = 0; i < a.length; i++) {
10 | if (!deepEqual(a[i], b[i])) {
11 | return false;
12 | }
13 | }
14 | return true;
15 | }
16 | if (typeof a === "object" && typeof b === "object" && a && b) {
17 | const keys = Object.keys(a);
18 | for (const key of keys) {
19 | if (!deepEqual(a[key], b[key])) {
20 | return false;
21 | }
22 | }
23 | return true;
24 | }
25 | return false;
26 | }
27 |
--------------------------------------------------------------------------------
/src/generallogoprompt.js:
--------------------------------------------------------------------------------
1 | export const defaultLogoPrompt =
2 | "Modern icon, logo. Flat, colorful. Circular with white background.";
3 |
4 | const storageKey = "logoPrompt";
5 |
6 | export function getGeneralLogoPrompt() {
7 | const result = localStorage.getItem(storageKey);
8 | return result || defaultLogoPrompt;
9 | }
10 |
11 | export function setGeneralLogoPrompt(value) {
12 | localStorage.setItem(storageKey, value);
13 | }
14 |
--------------------------------------------------------------------------------
/src/gptservice/appgpt.js:
--------------------------------------------------------------------------------
1 | import { getGptCompletion, getGptEdit, defaultBody, getGptChat } from "./gpt";
2 | import { holder } from "../key-management/key";
3 | import { tokenCostTracker } from "./tokencost";
4 |
5 | export async function getCompletion(prompt, usagePaths) {
6 | if (!holder.hasKey()) {
7 | if (window.confirm("No GPT API key is set. Set one now?")) {
8 | window.location = "/key-management";
9 | }
10 | throw new Error("No GPT API key is set");
11 | }
12 | if (typeof prompt === "string") {
13 | prompt = { prompt };
14 | }
15 | prompt = Object.assign({}, defaultBody, prompt);
16 | const resp = await getGptCompletion(prompt, holder.getKey());
17 | tokenCostTracker.trackUsage(usagePaths, resp.usage, prompt.model);
18 | return resp;
19 | }
20 |
21 | export async function getEdit(prompt, usagePaths) {
22 | if (!holder.hasKey()) {
23 | if (window.confirm("No GPT API key is set. Set one now?")) {
24 | window.location = "/key-management";
25 | }
26 | throw new Error("No GPT API key is set");
27 | }
28 | const resp = await getGptEdit(prompt, holder.getKey());
29 | tokenCostTracker.trackUsage(usagePaths, resp.usage, prompt.model);
30 | return resp;
31 | }
32 |
33 | export async function getChat(prompt, usagePaths) {
34 | if (!holder.hasKey()) {
35 | if (window.confirm("No GPT API key is set. Set one now?")) {
36 | window.location = "/key-management";
37 | }
38 | throw new Error("No GPT API key is set");
39 | }
40 | const resp = await getGptChat(prompt, holder.getKey());
41 | tokenCostTracker.trackUsage(usagePaths, resp.usage, prompt.model);
42 | return resp;
43 | }
44 |
--------------------------------------------------------------------------------
/src/gptservice/gpt.js:
--------------------------------------------------------------------------------
1 | export async function getGptCompletion(prompt, key) {
2 | const url = "https://api.openai.com/v1/completions";
3 | if (typeof prompt === "string") {
4 | prompt = { prompt };
5 | }
6 | if (prompt.system || prompt.messages) {
7 | throw new Error("Use getGptChat() for chat prompts");
8 | }
9 | const body = Object.assign({}, defaultBody, prompt);
10 | console.log("Sending GPT request:", body.prompt, body);
11 | const resp = await fetch(url, {
12 | method: "POST",
13 | mode: "cors",
14 | headers: {
15 | "Content-Type": "application/json",
16 | Authorization: `Bearer ${key}`,
17 | },
18 | body: JSON.stringify(body),
19 | });
20 | if (!resp.ok) {
21 | const body = await resp.json();
22 | console.error("Error from GPT:", body);
23 | const exc = new Error(
24 | `GPT request failed: ${resp.status} ${resp.statusText}: ${body.error.message}`
25 | );
26 | exc.request = resp;
27 | exc.errorData = body;
28 | throw exc;
29 | }
30 | const data = await resp.json();
31 | console.log("Got GPT response:", data);
32 | addTokens(data.usage.total_tokens);
33 | return data;
34 | }
35 |
36 | export const defaultBody = {
37 | model: "text-davinci-003",
38 | temperature: 0.2,
39 | max_tokens: 40,
40 | n: 1,
41 | };
42 |
43 | export async function getGptEdit(body, key) {
44 | const url = "https://api.openai.com/v1/edits";
45 | if (!body.input || !body.instruction) {
46 | throw new Error(`Missing ${body.input ? "instruction" : "input"}`);
47 | }
48 | body = Object.assign({}, defaultEditBody, body);
49 | console.log(
50 | "Sending GPT edit request:\n",
51 | body.input,
52 | "\n",
53 | body.instruction,
54 | body
55 | );
56 | const resp = await fetch(url, {
57 | method: "POST",
58 | mode: "cors",
59 | headers: {
60 | "Content-Type": "application/json",
61 | Authorization: `Bearer ${key}`,
62 | },
63 | body: JSON.stringify(body),
64 | });
65 | if (!resp.ok) {
66 | const body = await resp.json();
67 | console.error("Error from GPT:", body);
68 | const exc = new Error(
69 | `GPT edit request failed: ${resp.status} ${resp.statusText}: ${body.error.message}`
70 | );
71 | exc.request = resp;
72 | exc.errorData = body;
73 | throw exc;
74 | }
75 | const data = await resp.json();
76 | console.log("Got GPT edit response:", data);
77 | addTokens(data.usage.total_tokens);
78 | return data;
79 | }
80 |
81 | export const defaultEditBody = {
82 | model: "text-davinci-edit-001",
83 | temperature: 0.05,
84 | n: 1,
85 | };
86 |
87 | export async function getGptChat(prompt, key) {
88 | const url = "https://api.openai.com/v1/chat/completions";
89 | prompt = normalizeGptChatPrompt(prompt);
90 | console.log("Sending ChatGPT request:", prompt);
91 | const resp = await fetch(url, {
92 | method: "POST",
93 | mode: "cors",
94 | headers: {
95 | "Content-Type": "application/json",
96 | Authorization: `Bearer ${key}`,
97 | },
98 | body: JSON.stringify(prompt),
99 | });
100 | if (!resp.ok) {
101 | const body = await resp.json();
102 | console.error("Error from ChatGPT:", body);
103 | const exc = new Error(
104 | `ChatGPT request failed: ${resp.status} ${resp.statusText}: ${body.error.message}`
105 | );
106 | exc.request = resp;
107 | exc.errorData = body;
108 | throw exc;
109 | }
110 | const data = await resp.json();
111 | console.log("Got ChatGPT response:", data);
112 | addTokens(data.usage.total_tokens);
113 | return data;
114 | }
115 |
116 | export function normalizeGptChatPrompt(prompt) {
117 | if (typeof prompt === "string") {
118 | prompt = { prompt };
119 | }
120 | if (prompt.prompt) {
121 | // Simulate a GPT completion using a text prompt
122 | if (prompt.messages) {
123 | throw new Error("Cannot specify both prompt and messages");
124 | }
125 | prompt.messages = [{ role: "user", content: prompt.prompt }];
126 | delete prompt.prompt;
127 | }
128 | if (prompt.system) {
129 | console.log("Bad prompt:", prompt);
130 | if (prompt.messages[0].role === "system") {
131 | throw new Error(
132 | "Cannot specify both system and messages[0].role==system"
133 | );
134 | }
135 | prompt.messages.unshift({ role: "system", content: prompt.system });
136 | delete prompt.system;
137 | }
138 | return Object.assign({}, defaultChatBody, prompt);
139 | }
140 |
141 | export const defaultChatBody = {
142 | model: "gpt-3.5-turbo",
143 | };
144 |
145 | let sessionTokens = 0;
146 | let totalTokens = 0;
147 | if (localStorage.getItem("totalTokens")) {
148 | totalTokens = parseInt(localStorage.getItem("totalTokens"), 10);
149 | }
150 |
151 | function addTokens(n) {
152 | sessionTokens += n;
153 | totalTokens += n;
154 | localStorage.setItem("totalTokens", totalTokens);
155 | console.log("Token track:", sessionTokens, "/", totalTokens);
156 | }
157 |
--------------------------------------------------------------------------------
/src/gptservice/tokencost.js:
--------------------------------------------------------------------------------
1 | export class TokenCostTracker {
2 | constructor(keyName) {
3 | this.keyName = keyName;
4 | this.tracked = {};
5 | this.sessionTracked = {};
6 | this._onUpdates = [];
7 | this.refreshFromLocalStorage();
8 | window.addEventListener("storage", this.onStorage.bind(this));
9 | }
10 |
11 | summarizePaths(paths) {
12 | if (typeof paths === "string") {
13 | paths = [paths];
14 | }
15 | const result = [];
16 | for (const path of paths) {
17 | let usage = this.tracked[path];
18 | usage = (usage || {}).total_tokens || 0;
19 | let today = this.sessionTracked[path];
20 | today = (today || {}).total_tokens || 0;
21 | result.push(`${path}: ${today}/${usage}`);
22 | }
23 | return result.join(" ");
24 | }
25 |
26 | trackUsage(paths, usage, model) {
27 | if (typeof paths === "string") {
28 | paths = [paths];
29 | }
30 | const date = new Date();
31 | const dateString = `${date.getFullYear()}-${
32 | date.getMonth() + 1
33 | }-${date.getDate()}`;
34 | const fullPaths = [];
35 | for (let path of paths) {
36 | path = path.trim().replace(/^\/+/, "").replace(/\/+$/, "");
37 | fullPaths.push(path);
38 | fullPaths.push(`${dateString}/${path}`);
39 | }
40 | fullPaths.push("all");
41 | const seen = new Set();
42 | for (const path of fullPaths) {
43 | if (seen.has(path)) {
44 | console.error(
45 | `The usage path "${path}" is in the list twice:`,
46 | fullPaths
47 | );
48 | continue;
49 | }
50 | this.mergePaths(this.tracked, path, usage, model, seen);
51 | this.mergePaths(this.sessionTracked, path, usage, model, seen);
52 | seen.add(path);
53 | }
54 | this.saveToLocalStorage();
55 | this.fireOnUpdate();
56 | }
57 |
58 | mergePaths(tracked, path, usage, model, seen) {
59 | while (path) {
60 | if (!seen.has(path)) {
61 | tracked[path] = this.mergeUsage(tracked[path], usage);
62 | tracked[path][model] = this.mergeUsage(tracked[path][model], usage);
63 | }
64 | seen.add(path);
65 | if (!path.includes("/")) {
66 | break;
67 | }
68 | path = path.replace(/\/[^\/]+$/, "");
69 | }
70 | }
71 |
72 | mergeUsage(existingUsage, usage) {
73 | if (!existingUsage) {
74 | existingUsage = {};
75 | }
76 | existingUsage.prompt_tokens =
77 | (existingUsage.prompt_tokens || 0) + (usage.prompt_tokens || 0);
78 | existingUsage.completion_tokens =
79 | (existingUsage.completion_tokens || 0) + (usage.completion_tokens || 0);
80 | existingUsage.total_tokens =
81 | (existingUsage.total_tokens || 0) + (usage.total_tokens || 0);
82 | return existingUsage;
83 | }
84 |
85 | addOnUpdate(func) {
86 | this._onUpdates.push(func);
87 | }
88 |
89 | removeOnUpdate(func) {
90 | this._onUpdates = this._onUpdates.filter((x) => x !== func);
91 | }
92 |
93 | fireOnUpdate(func) {
94 | for (const func of this._onUpdates) {
95 | func(this);
96 | }
97 | }
98 |
99 | onStorage(event) {
100 | if (event.key && event.key === this.keyName) {
101 | this.refreshFromLocalStorage();
102 | }
103 | }
104 |
105 | refreshFromLocalStorage() {
106 | let val = localStorage.getItem(this.keyName);
107 | if (val && val !== "undefined") {
108 | val = JSON.parse(val);
109 | this.tracked = val;
110 | }
111 | }
112 |
113 | saveToLocalStorage() {
114 | localStorage.setItem(this.keyName, JSON.stringify(this.tracked));
115 | }
116 | }
117 |
118 | export const tokenCostTracker = new TokenCostTracker("tokenCosts");
119 |
--------------------------------------------------------------------------------
/src/hashsignal.js:
--------------------------------------------------------------------------------
1 | import { signal } from "@preact/signals";
2 |
3 | const hashSignal = signal(location.hash);
4 |
5 | window.addEventListener("hashchange", () => {
6 | hashSignal.value = window.location.hash;
7 | });
8 |
9 | // FIXME: with the Router in app.js and its onChange, this probably
10 | // isn't needed:
11 | window.addEventListener("popstate", () => {
12 | hashSignal.value = window.location.hash;
13 | });
14 |
15 | export default hashSignal;
16 |
--------------------------------------------------------------------------------
/src/imageapi/dalle.js:
--------------------------------------------------------------------------------
1 | import { holder } from "../key-management/key";
2 |
3 | export const availableSizes = ["256x256", "512x512", "1024x1024"];
4 |
5 | export const costs = {
6 | "256x256": 0.016,
7 | "512x512": 0.018,
8 | "1024x1024": 0.02,
9 | };
10 |
11 | export async function getDallECompletion(input, key = null) {
12 | const url = "https://api.openai.com/v1/images/generations";
13 | input = Object.assign({ response_format: "b64_json" }, input);
14 | if (!key) {
15 | if (!holder.hasKey()) {
16 | if (window.confirm("No GPT API key is set. Set one now?")) {
17 | window.location = "/key-management";
18 | }
19 | throw new Error("No GPT API key is set");
20 | }
21 | key = holder.getKey();
22 | }
23 | if (!input.prompt) {
24 | throw new Error("No prompt");
25 | }
26 | console.log("Sending DallE request:", input);
27 | const resp = await fetch(url, {
28 | method: "POST",
29 | mode: "cors",
30 | headers: {
31 | "Content-Type": "application/json",
32 | Authorization: `Bearer ${key}`,
33 | },
34 | body: JSON.stringify(input),
35 | });
36 | if (!resp.ok) {
37 | const error = await resp.text();
38 | console.error("Bad DallE response:", resp, error);
39 | throw new Error(
40 | `DallE request failed: ${resp.status} ${resp.statusText}: ${error}`
41 | );
42 | }
43 | const body = await resp.json();
44 | body.cost = costs[input.size] * input.n;
45 | return body;
46 | }
47 |
--------------------------------------------------------------------------------
/src/imageapi/replicatekey.js:
--------------------------------------------------------------------------------
1 | import KeyHolder from "../key-management/keyholder";
2 |
3 | export const holder = new KeyHolder("replicate", [], (v) => v.length === 40);
4 |
--------------------------------------------------------------------------------
/src/imageapi/stablediffusion.js:
--------------------------------------------------------------------------------
1 | import { holder } from "./replicatekey";
2 | import Replicate from "../vendor/replicate";
3 |
4 | const fullProxyUrl = "http://localhost:8010/proxy/v1";
5 |
6 | export const dimensionValues = [
7 | 128, 256, 384, 448, 512, 576, 640, 704, 768, 832, 896, 960, 1024,
8 | ];
9 |
10 | export const stableDiffusionCostPerSecond = 0.0023;
11 |
12 | export const defaultParameters = {
13 | negative_prompt: "",
14 | width: 768,
15 | height: 768,
16 | prompt_strength: 0.8,
17 | num_outputs: 1,
18 | num_inference_steps: 50,
19 | guidance_scale: 7.5,
20 | scheduler: "DPMSolverMultistep", // one of DDIM, K_EULER, DPMSolverMultistep, K_EULER_ANCESTRAL, PNDM, KLMS
21 | // Note: DPMSolverMultistep seems to constantly trigger NSFW warnings, K_EULER is better
22 | seed: "",
23 | };
24 |
25 | /* Schema of the intermediate responses:
26 |
27 | completed_at: timestamp
28 | created_at: "2023-01-18T22:52:52.504948Z"
29 | error: null
30 | id: "h6oywcol2retbcquac6zrmtvli"
31 | input: {}
32 | logs: big string
33 | : "Using seed: 58084\n 0%| | 0/50 [00:00, ?it/s]\n 2%|▏ | 1/50 [00:00<00:12, 3.85it/s]\n 4%|▍ | 2/50 [00:00<00:12, 3.84it/s]\n 6%|▌ | 3/50 [00:00<00:12, 3.84it/s]\n 8%|▊ | 4/50 [00:01<00:11, 3.84it/s]\n 10%|█ | 5/50 [00:01<00:11, 3.84it/s]\n 12%|█▏ | 6/50 [00:01<00:11, 3.83it/s]\n 14%|█▍ | 7/50 [00:01<00:11, 3.84it/s]\n 16%|█▌ | 8/50 [00:02<00:10, 3.84it/s]\n 18%|█▊ | 9/50 [00:02<00:10, 3.84it/s]\n 20%|██ | 10/50 [00:02<00:10, 3.84it/s]\n 22%|██▏ | 11/50 [00:02<00:10, 3.84it/s]\n 24%|██▍ | 12/50 [00:03<00:09, 3.84it/s]\n 26%|██▌ | 13/50 [00:03<00:09, 3.84it/s]\n 28%|██▊ | 14/50 [00:03<00:09, 3.84it/s]\n 30%|███ | 15/50 [00:03<00:09, 3.84it/s]\n 32%|███▏ | 16/50 [00:04<00:08, 3.84it/s]"
34 | metrics: {predict_time: 14.335476}
35 | output: null
36 | started_at: "2023-01-18T22:53:03.391697Z"
37 | status: "processing"
38 | urls: {get: 'https://api.replicate.com/v1/predictions/h6oywcol2retbcquac6zrmtvli', cancel: 'https://api.replicate.com/v1/predictions/h6oywcol2retbcquac6zrmtvli/cancel'}
39 | version: "f178fa7a1ae43a9a9af01b833b9d2ecf97b1bcb0acfd2dc5dd04895e042863f1"
40 | webhook_completed: null
41 |
42 | */
43 |
44 | export function requireKey() {
45 | if (!holder.hasKey()) {
46 | if (window.confirm("No Replicate API key is set. Set one now?")) {
47 | window.location = "/key-management";
48 | }
49 | throw new Error("No Replicate API key is set");
50 | }
51 | }
52 |
53 | let replicateClient;
54 |
55 | export class ImageRequest {
56 | constructor(input) {
57 | if (!input.prompt) {
58 | throw new Error("No prompt");
59 | }
60 | requireKey();
61 | if (!replicateClient) {
62 | replicateClient = new Replicate({ token: holder.getKey(), fullProxyUrl });
63 | }
64 | this.input = input;
65 | this.response = null;
66 | this._init = false;
67 | this._onUpdate = [];
68 | }
69 |
70 | addOnUpdate(listener) {
71 | this._onUpdate.push(listener);
72 | }
73 |
74 | removeOnUpdate(listener) {
75 | this._onUpdate = this._onUpdate.filter((l) => l !== listener);
76 | }
77 |
78 | updated() {
79 | this._onUpdate.forEach((listener) => listener());
80 | }
81 |
82 | async init() {
83 | if (this._init) {
84 | return;
85 | }
86 | this.stableDiffusion = await replicateClient.models.get(
87 | "stability-ai/stable-diffusion",
88 | "f178fa7a1ae43a9a9af01b833b9d2ecf97b1bcb0acfd2dc5dd04895e042863f1"
89 | );
90 | this._init = true;
91 | }
92 |
93 | async request() {
94 | await this.init();
95 | let prediction;
96 | this.response = null;
97 | const fixedInput = Object.assign({ scheduler: "K_EULER" }, this.input);
98 | console.log("sending", fixedInput);
99 | for await (prediction of this.stableDiffusion.predictor(fixedInput)) {
100 | this.response = prediction;
101 | this.updated();
102 | }
103 | return this.response;
104 | }
105 |
106 | toJSON() {
107 | return { input: this.input, response: this.response };
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/imageapi/thumbsnap.js:
--------------------------------------------------------------------------------
1 | import { holder } from "./thumbsnapkey";
2 |
3 | export function requireKey() {
4 | if (!holder.hasKey()) {
5 | if (window.confirm("No Thumb Snap API key is set. Set one now?")) {
6 | window.location = "/key-management";
7 | }
8 | throw new Error("No Thumb Snap API key is set");
9 | }
10 | }
11 |
12 | export async function upload(b64file) {
13 | requireKey();
14 | const key = holder.getKey();
15 | const url = "https://thumbsnap.com/api/upload";
16 | const formData = new FormData();
17 | formData.append("key", key);
18 | const blob = b64toBlob(b64file, "image/png");
19 | formData.append("media", blob);
20 | const resp = await fetch(url, {
21 | method: "POST",
22 | mode: "cors",
23 | body: formData,
24 | });
25 | if (!resp.ok) {
26 | const error = await resp.text();
27 | console.error("Bad thumbsnap response:", resp, error);
28 | throw new Error(
29 | `thumbsnap request failed: ${resp.status} ${resp.statusText}: ${error}`
30 | );
31 | }
32 | const body = await resp.json();
33 | return body;
34 | }
35 |
36 | export function b64toBlob(
37 | b64Data,
38 | contentType = "application/octet-stream",
39 | sliceSize = 512
40 | ) {
41 | const byteCharacters = atob(b64Data);
42 | const byteArrays = [];
43 |
44 | for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
45 | const slice = byteCharacters.slice(offset, offset + sliceSize);
46 |
47 | const byteNumbers = new Array(slice.length);
48 | for (let i = 0; i < slice.length; i++) {
49 | byteNumbers[i] = slice.charCodeAt(i);
50 | }
51 |
52 | const byteArray = new Uint8Array(byteNumbers);
53 | byteArrays.push(byteArray);
54 | }
55 |
56 | const blob = new Blob(byteArrays, { type: contentType });
57 | return blob;
58 | }
59 |
--------------------------------------------------------------------------------
/src/imageapi/thumbsnapkey.js:
--------------------------------------------------------------------------------
1 | import KeyHolder from "../key-management/keyholder";
2 |
3 | export const holder = new KeyHolder("thumbsnap", [], (v) => v.length === 32);
4 |
--------------------------------------------------------------------------------
/src/imagegen/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { ImageGenView } from "./imagegenview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { imageGenDb } from "./imagegendb";
5 | import { InfoHeader, P, A, InfoA } from "../components/common";
6 |
7 | export const ImageGenIndex = () => {
8 | return (
9 |
14 |
15 |
16 | This is just a testbed for doing image generation, such as what is
17 | used in Make Your Own Adventure .
18 |
19 |
20 | There are no particularly interesting featuers in this, it just
21 | exercises the API integrations.
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import "./style";
2 | import App from "./components/app";
3 |
4 | export default App;
5 |
--------------------------------------------------------------------------------
/src/interactive-fiction/ifview.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { signal } from "@preact/signals";
3 | import { useEffect, useRef, useState } from "preact/hooks";
4 | import { loadStoryData, createStory } from "./storyloader";
5 | import { Header } from "../components/header";
6 | import { PageContainer, Button, TextInput } from "../components/common";
7 | import Sidebar from "../components/sidebar";
8 | import { QueryLog } from "../components/querylog";
9 |
10 | let runnerPromise = null;
11 | let runnerPromiseFilename = null;
12 |
13 | export const InteractiveFictionView = ({ model }) => {
14 | const [version, setVersion] = useState(0);
15 | model.addOnUpdate(() => {
16 | setVersion(version + 1);
17 | });
18 | useEffect(() => {
19 | if (
20 | runnerPromise === null ||
21 | runnerPromiseFilename !== model.domain.z5url
22 | ) {
23 | startRunner(model.domain.z5url, model);
24 | runnerPromiseFilename = model.domain.z5url;
25 | }
26 | });
27 | const status = [
28 | model.domain.statusTextSignal.value,
29 | model.domain.statusSummarySignal.value,
30 | ]
31 | .filter((x) => x)
32 | .join(" ");
33 | return (
34 |
35 |
42 |
43 |
44 |
45 |
46 |
47 |
51 |
56 |
57 |
58 | );
59 | };
60 |
61 | function startRunner(filename, model) {
62 | runnerPromise = (async function () {
63 | const data = await loadStoryData(filename);
64 | const runner = createStory(data, model);
65 | const generator = runner.run();
66 | setInterval(() => {
67 | generator.next();
68 | }, 50);
69 | })();
70 | }
71 |
72 | const StatusLine = ({ text, summary }) => (
73 |
74 | {summary}
75 | {text}
76 |
77 | );
78 |
79 | let inputElRef;
80 |
81 | const Console = ({ model, inputEnabled, onSubmit }) => {
82 | const inputEl = useRef(null);
83 | inputElRef = inputEl;
84 | useEffect(() => {
85 | if (inputEl.current) {
86 | inputEl.current.focus();
87 | }
88 | });
89 | let input = null;
90 | function overrideSubmit(e) {
91 | e.preventDefault();
92 | const el = e.target.querySelector("input");
93 | onSubmit(el.value);
94 | el.value = "";
95 | return false;
96 | }
97 | function onKeyUp(e) {
98 | if (e.key === "ArrowUp" || e.key === "ArrowDown") {
99 | const dir = e.key === "ArrowUp" ? -1 : 1;
100 | const el = inputEl.current;
101 | el.value = model.domain.retrieveHistory(dir, el.value);
102 | e.preventDefault();
103 | return false;
104 | }
105 | return undefined;
106 | }
107 | const completion = model.domain.completionSignal.value;
108 | useEffect(() => {
109 | if (inputEl.current && completion) {
110 | inputEl.current.value = completion;
111 | }
112 | }, [completion]);
113 | if (inputEnabled.value) {
114 | input = (
115 |
125 | );
126 | }
127 | return (
128 |
129 | {model.domain.textOutputSignal}
130 | {input}
131 |
132 | );
133 | };
134 |
135 | const completionText = signal("");
136 |
137 | const Completer = ({ model }) => {
138 | const number = useRef(null);
139 | function doMany() {
140 | const num = number.current ? number.current.value : 1;
141 | model.domain.fillInMany(num);
142 | }
143 | return (
144 |
145 |
146 | Fill in input
147 |
148 |
149 | Fill & Submit input
150 |
151 |
152 |
158 | Do many steps
159 |
160 |
{completionText.value}
161 |
162 | );
163 | };
164 |
--------------------------------------------------------------------------------
/src/interactive-fiction/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { InteractiveFictionView } from "./ifview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { ifDb } from "./ifdb";
5 | import { InfoHeader, P, A } from "../components/common";
6 |
7 | export const StoryIndex = () => {
8 | return (
9 |
15 |
19 |
20 | In some games like{" "}
21 |
25 | AI Dungeon
26 | {" "}
27 | GPT will do the job of playing "the world" and enforcing the rules of
28 | that world. This is perhaps the biggest difficulty is using GPT: it's
29 | built to be agreeable and accept hallucinations just as it produces
30 | them.
31 |
32 |
33 | It occurred to me: what if we use GPT as a source of autonomy and
34 | copresence in the world, but still use a game engine to enforce the
35 | rules of the world?
36 |
37 |
38 | Game engines commanded by text already exist: text adventures. This
39 | attempts to let GPT play in a text adventure.
40 |
41 |
42 | (The JavaScript player used in this project supports up to zcode v3,
43 | many games are v4 or v5. But there must be more v3 games.)
44 |
45 |
46 |
47 | );
48 | };
49 |
--------------------------------------------------------------------------------
/src/interactive-fiction/storyloader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable require-yield */
2 | import JSZM from "../vendor/jszm";
3 |
4 | export async function loadStoryData(filename) {
5 | const url = `/assets/interactive-fiction/z5s/${filename}`;
6 | console.log("Loading story URL", url);
7 | const resp = await fetch(url);
8 | const data = await resp.arrayBuffer();
9 | console.log("Loaded bytes:", data.byteLength);
10 | return data;
11 | }
12 |
13 | let story;
14 |
15 | export function createStory(data, model) {
16 | story = new JSZM(data);
17 | story.model = model;
18 | Object.assign(story, methods);
19 | return story;
20 | }
21 |
22 | const methods = {
23 | *print(line) {
24 | this.model.domain.print(line);
25 | },
26 |
27 | *read() {
28 | this.model.domain.enableRead();
29 | let result;
30 | this.model.domain.onReadOnce = (text) => {
31 | result = text;
32 | };
33 | while (!result) {
34 | yield "";
35 | }
36 | return result;
37 | },
38 |
39 | *updateStatusLine(text, scoreOrHour, movesOrMinutes) {
40 | let summary;
41 | if (this.statusType) {
42 | summary = `${scoreOrHour}h${movesOrMinutes}m`;
43 | } else {
44 | summary = `score: ${scoreOrHour} moves: ${movesOrMinutes}`;
45 | }
46 | this.model.domain.updateStatusLine(text, summary);
47 | },
48 | };
49 |
--------------------------------------------------------------------------------
/src/key-management/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { signal } from "@preact/signals";
3 | import { holder } from "../key-management/key";
4 | import * as replicateKey from "../imageapi/replicatekey";
5 | import * as thumbsnapKey from "../imageapi/thumbsnapkey";
6 | import {
7 | P,
8 | A,
9 | Card,
10 | Code,
11 | Field,
12 | TextInput,
13 | Form,
14 | Alert,
15 | PageContainer,
16 | Button,
17 | } from "../components/common";
18 | import { Header } from "../components/header";
19 |
20 | const replicateHolder = replicateKey.holder;
21 | const thumbsnapHolder = thumbsnapKey.holder;
22 |
23 | window.holder = holder;
24 | export const hasGptKeySignal = signal(holder.hasKey());
25 | holder.addOnUpdate(() => {
26 | hasGptKeySignal.value = holder.hasKey();
27 | });
28 | const gptKeyError = signal("");
29 |
30 | export const hasReplicateKeySignal = signal(replicateHolder.hasKey());
31 | replicateHolder.addOnUpdate(() => {
32 | hasReplicateKeySignal.value = replicateHolder.hasKey();
33 | });
34 | const replicateKeyError = signal("");
35 |
36 | export const hasThumbsnapKeySignal = signal(thumbsnapHolder.hasKey());
37 | thumbsnapHolder.addOnUpdate(() => {
38 | hasThumbsnapKeySignal.value = thumbsnapHolder.hasKey();
39 | });
40 | const thumbsnapKeyError = signal("");
41 |
42 | export const RequestKeyPage = () => {
43 | return (
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export const RequestKey = () => {
52 | function onSubmitGpt(textInput) {
53 | const setKey = holder.setKeyFromText(textInput.value);
54 | if (setKey) {
55 | gptKeyError.value = "";
56 | } else {
57 | gptKeyError.value = "Invalid key";
58 | }
59 | textInput.value = "";
60 | }
61 | function onSubmitReplicate(textInput) {
62 | const setKey = replicateHolder.setKeyFromText(textInput.value);
63 | if (setKey) {
64 | replicateKeyError.value = "";
65 | } else {
66 | replicateKeyError.value = "Invalid key";
67 | }
68 | textInput.value = "";
69 | }
70 | function onSubmitThumbsnap(textInput) {
71 | const setKey = thumbsnapHolder.setKeyFromText(textInput.value);
72 | if (setKey) {
73 | thumbsnapKeyError.value = "";
74 | } else {
75 | thumbsnapKeyError.value = "Invalid key";
76 | }
77 | textInput.value = "";
78 | }
79 |
80 | return (
81 |
82 |
83 |
84 |
85 | This application requires a GPT-3 key. To use it you must enter one
86 | here; it will be stored locally in this browser and be used only to
87 | make requests from this browser to GPT-3 directly. If you just want
88 | to test this you can create a key and then delete it.
89 |
90 |
91 | If you do not have a key you can sign up at{" "}
92 | openai.com/api and then create
93 | a key at{" "}
94 |
95 | beta.openai.com/account/api-keys
96 |
97 |
98 |
99 | For image generation you can use{" "}
100 | Replicate and generate a key in
101 | your Account Settings
102 |
103 |
104 | For image hosting you can use{" "}
105 | Thumb Snap and get an API
106 | key there.
107 |
108 | {hasGptKeySignal.value ? (
109 |
110 | ) : null}
111 | {gptKeyError.value ?
{gptKeyError.value} : null}
112 | {hasReplicateKeySignal.value ? (
113 |
114 | ) : null}
115 | {replicateKeyError.value ? (
116 |
{replicateKeyError.value}
117 | ) : null}
118 | {hasThumbsnapKeySignal.value ? (
119 |
120 | ) : null}
121 | {thumbsnapKeyError.value ? (
122 |
{thumbsnapKeyError.value}
123 | ) : null}
124 |
125 |
126 | GPT key:
127 |
128 |
129 |
130 | Replicate key (optional):
131 |
135 |
136 |
137 | Thumb Snap key (optional):
138 |
142 |
143 |
144 |
145 |
146 |
147 | );
148 | };
149 |
150 | const ExistingKey = ({ name, holder }) => {
151 | function onRemove() {
152 | holder.removeKey();
153 | }
154 | return (
155 |
156 |
You have a {name} already configured:
157 |
158 |
159 | {holder.getKey().slice(0, 3)}...
160 | {holder.getKey().slice(-3)}
161 |
162 |
163 |
164 | Remove existing key
165 |
166 |
167 | );
168 | };
169 |
--------------------------------------------------------------------------------
/src/key-management/key.js:
--------------------------------------------------------------------------------
1 | import KeyHolder from "./keyholder";
2 |
3 | const standardKey =
4 | "FFB0le+/wQs711zpoAgN/mKBvc01vOD4r8OiZaNHzvWSSroS0n/p8onJmWSRD+SKb8cw6SahQ6XdpwiRcKCOFROAlNfLIv1AJFO2XIN5TQ==";
5 |
6 | export const holder = new KeyHolder(
7 | "gpt3",
8 | [standardKey],
9 | (v) => v && v.startsWith("sk-")
10 | );
11 |
--------------------------------------------------------------------------------
/src/key-management/keyholder.js:
--------------------------------------------------------------------------------
1 | import { decrypt } from "../vendor/encryption";
2 |
3 | export default class KeyHolder {
4 | constructor(name, encrypted, validator) {
5 | this.name = name;
6 | this.encrypted = encrypted;
7 | this.validator = validator || ((v) => !!v);
8 | this._onUpdate = [];
9 | }
10 |
11 | addOnUpdate(func) {
12 | this._onUpdate.push(func);
13 | }
14 |
15 | removeOnUpdate(func) {
16 | this._onUpdate = this._onUpdate.filter((f) => f !== func);
17 | }
18 |
19 | fireOnUpdate() {
20 | this._onUpdate.forEach((f) => f());
21 | }
22 |
23 | get storageName() {
24 | return `key_${this.name}`;
25 | }
26 |
27 | hasKey() {
28 | return !!localStorage.getItem(this.storageName);
29 | }
30 |
31 | getKey() {
32 | if (!this.hasKey()) {
33 | throw new Error(`Key not found`);
34 | }
35 | return localStorage.getItem(this.storageName);
36 | }
37 |
38 | setKey(value) {
39 | localStorage.setItem(this.storageName, value);
40 | console.log(
41 | `To get an encrypted version of this key, run: testEncrypt(${JSON.stringify(
42 | value
43 | )}, "password")`
44 | );
45 | this.fireOnUpdate();
46 | }
47 |
48 | removeKey() {
49 | localStorage.removeItem(this.storageName);
50 | this.fireOnUpdate();
51 | }
52 |
53 | async setKeyFromText(textInput) {
54 | for (const e of this.encrypted) {
55 | let v;
56 | try {
57 | v = await decrypt(e, textInput);
58 | } catch (e) {
59 | continue;
60 | }
61 | if (!v) {
62 | continue;
63 | }
64 | if (this.validator(v)) {
65 | this.setKey(v);
66 | return true;
67 | }
68 | }
69 | if (this.validator(textInput)) {
70 | this.setKey(textInput);
71 | return true;
72 | }
73 | return false;
74 | }
75 |
76 | loadFromPassword(encrypted, password) {
77 | const v = decrypt(encrypted, password);
78 | if (v) {
79 | localStorage.setItem(this.storageName, v);
80 | return v;
81 | }
82 | return null;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/layercraft/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { LayerCraftView } from "./layerview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { layerDb } from "./layerdb";
5 | import { YouTube, InfoHeader, InfoA, P } from "../components/common";
6 |
7 | export const LayerCraftIndex = () => {
8 | return (
9 |
14 |
15 |
16 | A general authoring tool for building up in layers, building context and structure. Based on specific schemas that guide the authoring process, such as world building.
17 |
18 |
19 | This is a generalization of City Maker . That was an app built around a certain idea, but this separates the idea from the app; all the prompts and structure and dependencies are defined separately.
20 |
21 |
22 | Read more about this tool in World Building with GPT part 2: bigger, better, more declarative
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/layercraft/laxjson.js:
--------------------------------------------------------------------------------
1 | import JSON5 from "json5";
2 |
3 | export function parseJSON(text, putTrailingInKey = null) {
4 | // Remove trailing semicolons
5 | text = text.trim().replace(/;+$/, "").trim();
6 | // Sometimes the JSON will start with "this is a blah:\n\n[json]"; catch that here:
7 | const match = text.match(/^[a-zA-Z][^\n]+:\n/);
8 | if (match) {
9 | text = text.slice(match[0].length).trim();
10 | }
11 | const extraTail = [
12 | "",
13 | "]",
14 | "}]",
15 | "}}]",
16 | "}}}]",
17 | '"}]',
18 | '"}}]',
19 | '"}}}]',
20 | "'}]",
21 | "'}}]",
22 | "'}}}]",
23 | "null}]",
24 | "null}}]",
25 | "null}}}]",
26 | ":null}]",
27 | ":null}}]",
28 | ":null}}}]",
29 | '":null}]',
30 | '":null}}]',
31 | '":null}}}]',
32 | "':null}]",
33 | "':null}}]",
34 | "':null}}}]",
35 | ];
36 | let printedOnce = false;
37 | let firstError;
38 | const textwithFixedNewlines = text.replace(
39 | /(\"[^"]*\":\s+\"[^\"]*)\n/g,
40 | "$1"
41 | );
42 | const baseTexts = [text];
43 | if (textwithFixedNewlines !== text) {
44 | console.info("Also parsing newline fixed JSON");
45 | baseTexts.push(textwithFixedNewlines);
46 | }
47 | let extra;
48 | if (putTrailingInKey) {
49 | const leading = baseTexts[0][0];
50 | let end;
51 | if (leading === "[") {
52 | end = "]";
53 | } else if (leading === "{") {
54 | end = "}";
55 | } else {
56 | console.info(`JSON starts with unexpected character "${JSON.stringify(leading)}"`);
57 | }
58 | if (end) {
59 | const parts = baseTexts[0].split(end);
60 | const last = parts.pop();
61 | if (last.trim()) {
62 | extra = last.trim();
63 | baseTexts.push(baseTexts[0].slice(0, -last.length));
64 | }
65 | }
66 | }
67 | for (const baseText of baseTexts) {
68 | for (const option of extraTail) {
69 | try {
70 | const result = JSON5.parse(baseText + option);
71 | if (printedOnce) {
72 | console.info(
73 | "Parsed successfully with ending",
74 | JSON.stringify(option),
75 | result
76 | );
77 | }
78 | if (extra) {
79 | result[putTrailingInKey] = extra;
80 | }
81 | return result;
82 | } catch (e) {
83 | if (!firstError) {
84 | firstError = e;
85 | }
86 | if (!printedOnce) {
87 | console.info("Failing to parse JSON:", text);
88 | printedOnce = true;
89 | } else {
90 | console.warn(
91 | "Still could not parse with ending",
92 | JSON.stringify(option)
93 | );
94 | }
95 | }
96 | }
97 | }
98 | console.warn("Could not fix JSON");
99 | throw firstError;
100 | }
101 |
--------------------------------------------------------------------------------
/src/layercraft/schema.js:
--------------------------------------------------------------------------------
1 | class Property {
2 | constructor(editor, { name, title, parent, prompt, coercePrompt, choiceType, unpack }) {
3 | this.editor = editor;
4 | this.name = name;
5 | this.parent = parent;
6 | this.title = title;
7 | this.prompt = prompt;
8 | this.coercePrompt = coercePrompt;
9 | this.choiceType = choiceType;
10 | this.unpack = unpack;
11 | }
12 | }
13 |
14 | class PropertyValue {
15 | constructor(editor, { property, name, attributes, additionalPrompt, children }) {
16 | this.editor = editor;
17 | this.property = property;
18 | this.name = name;
19 | this.attributes = attributes;
20 | this.additionalPrompt = additionalPrompt;
21 | this.children = children;
22 | }
23 |
24 | toJSON() {
25 | return {
26 | property: this.property.name,
27 | name: this.name,
28 | attributes: this.attributes,
29 | additionalPrompt: this.additionalPrompt,
30 | children: this.children,
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/layercraft/template.js:
--------------------------------------------------------------------------------
1 | export function fillTemplate(template, getVariable, getVariablePath, reprFunction) {
2 | const re = /(?:\$([a-z][a-z0-9]*(?:\.[a-z0-9]+)*(?:[|a-z0-9]+)*)|\$\{([^}]+)\})([.,;]?)/gi;
3 | let match;
4 | let result = template;
5 | while ((match = re.exec(template))) {
6 | const expr = match[1] || match[2];
7 | const parts = expr.split("|");
8 | const path = parts[0].split(".");
9 | const filters = parts.slice(1);
10 | let punctuation = match[3];
11 | const variable = path[0];
12 | const field = path[1];
13 | let value = getVariable(variable);
14 | value = getVariablePath(value, path.slice(1));
15 | for (const filter of filters) {
16 | let filterName = filter;
17 | let args = [];
18 | if (filterName.includes(":")) {
19 | args = filterName.split(":").slice(1);
20 | filterName = filterName.split(":")[0];
21 | }
22 | if (!FILTERS[filterName]) {
23 | console.warn("Unknown filter", filterName, "from", filter);
24 | throw new Error(`Unknown filter: ${filter}`);
25 | }
26 | try {
27 | value = FILTERS[filterName](value, reprFunction, ...args);
28 | } catch (e) {
29 | console.warn("Error rendering filter", filter, "in", match[0], ":", e);
30 | }
31 | }
32 | const str = reprFunction(value);
33 | if (/[.,;?!]$/.test(str)) {
34 | punctuation = "";
35 | }
36 | result = result.replace(match[0], str + punctuation);
37 | }
38 | return result;
39 | }
40 |
41 | export function templateVariables(template) {
42 | const re = /\$([a-z][a-z0-9]*(?:\.[a-z0-9]+)*(?:[|a-z0-9]+)*)|\$\{([^}]+)\}/gi;
43 | const result = {};
44 | let match;
45 | while ((match = re.exec(template))) {
46 | const expr = match[1] || match[2];
47 | const parts = expr.split("|");
48 | const path = parts[0].split(".");
49 | let variable = path[0];
50 | variable = variable.split(":")[0];
51 | if (!(variable in result)) {
52 | result[variable] = match[0];
53 | }
54 | }
55 | return result;
56 | }
57 |
58 | const FILTERS = {
59 | first(v, repr) {
60 | return v[0] || "";
61 | },
62 |
63 | rest(v, repr) {
64 | return v.slice(1);
65 | },
66 |
67 | repr(v, repr) {
68 | return repr(v);
69 | },
70 |
71 | json(v, repr) {
72 | return JSON.stringify(v);
73 | },
74 |
75 | jsonExamples(v, repr) {
76 | if (!Array.isArray(v)) {
77 | return JSON.stringify(v);
78 | }
79 | const items = v.map((item) => JSON.stringify(repr(item)));
80 | return items.join(", ");
81 | },
82 |
83 | markdownList(v, repr) {
84 | if (!v) {
85 | return "";
86 | }
87 | if (!Array.isArray(v)) {
88 | console.warn("markdownList got a value that is not an array: ", v);
89 | throw new Error(`markdownList: not an array`);
90 | }
91 | return v.map((item) => `* ${repr(item)}`).join("\n");
92 | },
93 |
94 | optional(v, repr) {
95 | if (!v) {
96 | return "";
97 | }
98 | return repr(v);
99 | },
100 |
101 | nameDescription(v, repr) {
102 | if (!v) {
103 | return "";
104 | }
105 | if (Array.isArray(v)) {
106 | return v.map((item) => FILTERS.nameDescription(item, repr));
107 | }
108 | if (!v.attributes || !v.attributes.description) {
109 | return v;
110 | }
111 | return `${v.name}: ${v.attributes.description}`;
112 | },
113 |
114 | pack(v, repr, ...attrs) {
115 | if (!v) {
116 | return "";
117 | }
118 | if (Array.isArray(v)) {
119 | return v.map((item) => FILTERS.pack(item, repr, ...attrs));
120 | }
121 | if (attrs.length === 0) {
122 | return v.name;
123 | }
124 | return `${v.name} (${attrs.map((attr) => attr === "name" ? v.name : v.attributes[attr]).join(": ")})`;
125 | },
126 |
127 | get(v, repr, ...attrs) {
128 | if (!v) {
129 | return "";
130 | }
131 | const result = [];
132 | for (const ob of v) {
133 | let value = ob;
134 | for (const attr of attrs) {
135 | value = value.attributes[attr] || value[attr];
136 | }
137 | if (Array.isArray(value)) {
138 | result.push(...value);
139 | } else {
140 | result.push(value);
141 | }
142 | }
143 | return result;
144 | }
145 | };
146 |
147 | export function dedent(template) {
148 | if (template === null || template === undefined) {
149 | throw new Error("Template is null or undefined");
150 | }
151 | template = template.trim();
152 | let lines = template.split("\n");
153 | const firstLine = lines[0];
154 | lines = lines.slice(1);
155 | let indent = -1;
156 | for (const line of lines) {
157 | const trimmed = line.trimStart();
158 | if (trimmed) {
159 | const newIndent = line.length - trimmed.length;
160 | if (indent === -1 || newIndent < indent) {
161 | indent = newIndent;
162 | }
163 | }
164 | }
165 | const result = lines
166 | .map((line) => line.slice(indent))
167 | .join("\n");
168 | return firstLine + "\n" + result;
169 | }
170 |
171 | export function joinNaturalStrings(strings) {
172 | if (strings.length === 0) {
173 | return "";
174 | }
175 | if (strings.length === 1) {
176 | return strings[0];
177 | }
178 | let result = "";
179 | for (let i = 0; i < strings.length; i++) {
180 | const s = strings[i].trim();
181 | if (!s) {
182 | continue;
183 | }
184 | if (!result) {
185 | result = s;
186 | } else {
187 | if (/[,;.]$/.test(result)) {
188 | result += " " + s;
189 | } else {
190 | result += ", " + s;
191 | }
192 | }
193 | }
194 | return result;
195 | }
196 |
--------------------------------------------------------------------------------
/src/loadlegacyscript.js:
--------------------------------------------------------------------------------
1 | export async function loadScript(src) {
2 | return new Promise((resolve, reject) => {
3 | const script = document.createElement("script");
4 | script.onload = () => {
5 | resolve();
6 | };
7 | script.onerror = (error) => {
8 | reject(error);
9 | };
10 | script.src = src;
11 | document.head.appendChild(script);
12 | });
13 | }
14 |
--------------------------------------------------------------------------------
/src/localcache.js:
--------------------------------------------------------------------------------
1 | export default class LocalCache {
2 | constructor(storageName) {
3 | this._storageName = `cache-${storageName}`;
4 | this._cache = {};
5 | this._load();
6 | // should do: window.addEventListener("storage", ...)
7 | }
8 |
9 | _load() {
10 | const data = localStorage.getItem(this._storageName);
11 | if (data) {
12 | this._cache = JSON.parse(data);
13 | } else {
14 | this._cache = {};
15 | }
16 | if (!this._cache) {
17 | // Bad localStorage
18 | this._cache = {};
19 | }
20 | }
21 |
22 | _save() {
23 | localStorage.setItem(this._storageName, JSON.stringify(this._cache));
24 | }
25 |
26 | get(key) {
27 | return this._cache[key];
28 | }
29 |
30 | set(key, value) {
31 | this._cache[key] = value;
32 | this._save();
33 | }
34 |
35 | delete(key) {
36 | delete this._cache[key];
37 | this._save();
38 | }
39 | }
40 |
41 | window.purgeAllCaches = function () {
42 | console.info("Deleting cache...");
43 | let total = 0;
44 | let number = 0;
45 | for (let i = 0; i < localStorage.length; i++) {
46 | const key = localStorage.key(i);
47 | if (key.startsWith("cache-")) {
48 | const value = localStorage.getItem(key);
49 | total += value.length;
50 | number++;
51 | localStorage.removeItem(key);
52 | console.info(` Deleting ${key} for ${value.length} bytes`);
53 | }
54 | }
55 | console.info(`Deleted ${number} items for ${total} bytes`);
56 | };
57 |
--------------------------------------------------------------------------------
/src/localsync.js:
--------------------------------------------------------------------------------
1 | export default class LocalSync {
2 | constructor(keyName, obj) {
3 | this.keyName = keyName;
4 | this.obj = obj;
5 | this._load();
6 | this.obj.addOnUpdate(this._save.bind(this));
7 | }
8 |
9 | _load() {
10 | const json = localStorage.getItem(this.keyName);
11 | if (json && json !== "null") {
12 | const obj = JSON.parse(json);
13 | this.obj.updateFromJSON(obj);
14 | }
15 | }
16 |
17 | _save() {
18 | const json = JSON.stringify(this.obj);
19 | localStorage.setItem(this.keyName, json);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ian's LLM Garden",
3 | "short_name": "llm-garden",
4 | "start_url": "/",
5 | "display": "standalone",
6 | "orientation": "portrait",
7 | "background_color": "#fff",
8 | "theme_color": "#673ab8",
9 | "icons": [
10 | {
11 | "src": "/assets/icons/android-chrome-192x192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "/assets/icons/android-chrome-512x512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/src/markdown.js:
--------------------------------------------------------------------------------
1 | import { h } from "preact";
2 | import { parse, marked } from "marked";
3 |
4 | marked.setOptions({
5 | renderer: new marked.Renderer(),
6 | gfm: true,
7 | breaks: true, // Makes line breaks into
8 | });
9 |
10 |
11 | export function parseHtml(html, wrap = true) {
12 | const p = new DOMParser();
13 | if (wrap) {
14 | html = `${html}
`;
15 | }
16 | const doc = p.parseFromString(html, "text/html");
17 | const el = doc.body.childNodes[0];
18 | return el;
19 | }
20 |
21 | export function elementToPreact(element, callback) {
22 | const tag = element.tagName.toLowerCase();
23 | const attrs = {};
24 | for (const attr of element.attributes) {
25 | attrs[attr.name] = attr.value;
26 | }
27 | const children = [];
28 | for (const child of element.childNodes) {
29 | if (child.nodeType === Node.TEXT_NODE) {
30 | children.push(child.textContent);
31 | } else if (child.nodeType === Node.ELEMENT_NODE) {
32 | children.push(elementToPreact(child, callback));
33 | }
34 | }
35 | let repl = null;
36 | if (callback) {
37 | repl = callback(element, tag, attrs, children);
38 | }
39 | if (repl === "") {
40 | repl = null;
41 | } else if (!repl) {
42 | repl = h(tag, attrs, children);
43 | }
44 | return repl;
45 | }
46 |
47 | export function markdownToElement(markdown) {
48 | const rendered = parse(markdown);
49 | const el = parseHtml(rendered);
50 | return el;
51 | }
52 |
53 | export function markdownToPreact(markdown) {
54 | const el = markdownToElement(markdown);
55 | return elementToPreact(el);
56 | }
57 |
58 | export function Markdown(props) {
59 | const text = props.text;
60 | if (!text) {
61 | return null;
62 | }
63 | delete props.text;
64 | props.class = (props.class || "") + " unreset";
65 | return {markdownToPreact(text)}
;
66 | }
67 |
--------------------------------------------------------------------------------
/src/myoa/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { StoryView } from "./storyview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { InfoHeader, P } from "../components/common";
5 | import storyDb from "./storydb";
6 |
7 | export const AdventureIndex = () => {
8 | return (
9 |
14 |
18 |
19 | This is a tool to build your own adventures using GPT. You'll setup
20 | the style of the story and GPT will create passages and possible
21 | choices. You select the specific choices, and control the
22 | generation through a mini chat-like interface built into the editing.
23 |
24 |
25 | There's also Dall-E integration to create illustrations for individual
26 | passages.
27 |
28 |
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/src/myoa/player.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { useState, useEffect } from "preact/hooks";
3 | import { signal } from "@preact/signals";
4 | import storyDb from "./storydb";
5 | import { PageContainer, Button } from "../components/common";
6 | import { Header, HeaderButton } from "../components/header";
7 | import { PlayState } from "./playstate";
8 | import { Markdown } from "../markdown";
9 | import JSON5 from "json5";
10 |
11 | const hashSignal = signal(window.location.hash);
12 | window.addEventListener("hashchange", () => {
13 | hashSignal.value = window.location.hash;
14 | });
15 |
16 | export function StoryPlayerLoader() {
17 | const [modelName, setModelName] = useState(null);
18 | const [model, setModel] = useState(null);
19 | useEffect(() => {
20 | const params = new URLSearchParams(window.location.search);
21 | const name = params.get("name");
22 | if (modelName !== name) {
23 | setModelName(name);
24 | }
25 | }, [modelName, setModelName]);
26 | useEffect(() => {
27 | if (modelName && !model) {
28 | storyDb.getBySlug(modelName).then((loaded) => {
29 | setModel(loaded);
30 | });
31 | }
32 | }, [modelName, model, setModel]);
33 | return model ? : Loading...
;
34 | }
35 |
36 | export function StoryPlayer({ model }) {
37 | const params = new URLSearchParams(hashSignal.value.slice(1));
38 | const passageId = params.get("passage");
39 | const state = params.get("state");
40 | let initState = model.domain.gameState.value;
41 | let playState;
42 | if (initState) {
43 | initState = JSON5.parse(initState);
44 | playState = new PlayState(initState);
45 | playState.deserialize(state);
46 | }
47 | let passage = null;
48 | if (passageId === "introPassage") {
49 | passage = model.domain.introPassage;
50 | } else if (passageId) {
51 | passage = model.domain.passages.find((p) => p.id === passageId);
52 | }
53 | function onStart() {
54 | window.location.hash = `passage=introPassage`;
55 | }
56 | return (
57 |
58 |
65 | Edit
66 | ,
67 | ]}
68 | />
69 | {passage ? (
70 |
71 | ) : (
72 |
73 | )}
74 |
75 | );
76 | }
77 |
78 | function IntroScreen({ model, onStart }) {
79 | return (
80 |
81 |
{model.title}
82 |
{model.description}
83 |
84 | Start
85 |
86 |
87 | );
88 | }
89 |
90 | function PassageScreen({ passage, playState }) {
91 | const choices = passage.choices.filter(
92 | (c) => !playState || playState.check(c)
93 | );
94 | const clickTarget = (e) => {
95 | const anchor = e.target.querySelector("a");
96 | if (anchor) {
97 | location.href = e.target.querySelector("a").href;
98 | }
99 | };
100 | return (
101 |
102 |
103 | {passage.title}
104 |
105 | {playState ? (
106 |
107 |
{playState.debugRepr()}
108 |
109 | ) : null}
110 |
114 |
115 |
116 |
136 |
137 | );
138 | }
139 |
140 | function addPlayState(playState, choice) {
141 | if (playState) {
142 | const newState = playState.withExec(choice).serialize();
143 | if (!newState) {
144 | return "";
145 | }
146 | return `&state=${encodeURIComponent(newState)}`;
147 | }
148 | return "";
149 | }
150 |
--------------------------------------------------------------------------------
/src/myoa/playstate.js:
--------------------------------------------------------------------------------
1 | import deepEqual from "../deepequal";
2 |
3 | export class PlayState {
4 | constructor(initState) {
5 | this.initState = initState;
6 | this.state = Object.assign({}, initState);
7 | }
8 |
9 | toJSON() {
10 | const result = {};
11 | for (const key in this.state) {
12 | if (!deepEqual(this.state[key], this.initState[key])) {
13 | result[key] = this.state[key];
14 | }
15 | }
16 | return result;
17 | }
18 |
19 | deserialize(s) {
20 | if (!s) {
21 | return;
22 | }
23 | const data = deserialize(s);
24 | this.state = Object.assign({}, this.initState, data);
25 | }
26 |
27 | serialize() {
28 | if (!Object.keys(this.toJSON()).length) {
29 | return "";
30 | }
31 | return serialize(this.toJSON());
32 | }
33 |
34 | evaluate(s) {
35 | console.info(`evaluating (${s}) with:`, this.state);
36 | return window.withEval(this.state, s);
37 | }
38 |
39 | check(string) {
40 | const expr = /\(\((.*?)\)\)/.exec(string);
41 | if (expr) {
42 | return this.evaluate(expr[1]);
43 | }
44 | return true;
45 | }
46 |
47 | exec(string) {
48 | const expr = /\{\{(.*?)\}\}/.exec(string);
49 | if (expr) {
50 | return this.evaluate(expr[1]);
51 | }
52 | return null;
53 | }
54 |
55 | clean(string) {
56 | return string
57 | .replace(/\(\(.*?\)\)/g, "")
58 | .replace(/\{\{.*?\}\}/g, "")
59 | .trim();
60 | }
61 |
62 | clone() {
63 | const s = new PlayState(this.initState);
64 | s.state = Object.assign({}, this.state);
65 | return s;
66 | }
67 |
68 | withExec(s) {
69 | const state = this.clone();
70 | state.exec(s);
71 | return state;
72 | }
73 |
74 | debugRepr() {
75 | const lines = [];
76 | for (const key in this.state) {
77 | if (!deepEqual(this.state[key], this.initState[key])) {
78 | lines.push(`${key}: ${JSON.stringify(this.state[key], null, " ")}`);
79 | }
80 | }
81 | return lines.join("\n");
82 | }
83 | }
84 |
85 | // Why did I make my own serialization format here? I don't know, it's totally silly, JSON would have been fine!
86 | function serialize(data) {
87 | if (data === undefined) {
88 | throw new Error("Cannot serialize undefined");
89 | }
90 | if (data === null) {
91 | return "N";
92 | }
93 | if (typeof data === "boolean") {
94 | return data ? "T" : "F";
95 | }
96 | if (data === 0) {
97 | return "0";
98 | }
99 | if (data === 1) {
100 | return "1";
101 | }
102 | if (typeof data === "number") {
103 | const s = data.toString().slice(0, 35);
104 | return `n${serializeLength(s.length, 1)}${s}`;
105 | }
106 | if (typeof data === "string") {
107 | const val = cleanBtoa(data);
108 | return `s${serializeLength(val.length, 3)}${val}`;
109 | }
110 | if (Array.isArray(data)) {
111 | const s = data.map(serialize).join("");
112 | return `a${serializeLength(s.length, 2)}${s}`;
113 | }
114 | if (typeof data === "object") {
115 | const keys = Object.keys(data).filter((k) => data[k] !== undefined);
116 | keys.sort();
117 | const s = keys.map((k) => serializeKey(k) + serialize(data[k])).join("");
118 | return `o${serializeLength(keys.length, 2)}${s}`;
119 | }
120 | throw new Error(`Cannot serialize ${data}`);
121 | }
122 |
123 | function serializeLength(n, maxDigits) {
124 | let s = n.toString(36);
125 | if (s.length > maxDigits) {
126 | throw new Error(`Cannot serialize length ${n}`);
127 | }
128 | while (s.length < maxDigits) {
129 | s = "0" + s;
130 | }
131 | return s;
132 | }
133 |
134 | function serializeKey(key) {
135 | const val = cleanBtoa(key);
136 | return `${serializeLength(val.length, 1)}${val}`;
137 | }
138 |
139 | function deserializeLength(s, digits) {
140 | return [parseInt(s.slice(0, digits), 36), s.slice(digits)];
141 | }
142 |
143 | function deserializeKey(s) {
144 | const [length, s2] = deserializeLength(s, 1);
145 | const [key, s3] = [s2.slice(0, length), s2.slice(length)];
146 | return [cleanAtob(key), s3];
147 | }
148 |
149 | function deserializeOne(orig) {
150 | let s = orig;
151 | if (s.length === 0) {
152 | throw new Error("Cannot deserialize empty string");
153 | }
154 | const type = s[0];
155 | s = s.slice(1);
156 | if (type === "N") {
157 | return [null, s];
158 | }
159 | if (type === "T") {
160 | return [true, s];
161 | }
162 | if (type === "F") {
163 | return [false, s];
164 | }
165 | if (type === "0") {
166 | return [0, s];
167 | }
168 | if (type === "1") {
169 | return [1, s];
170 | }
171 | if (type === "n") {
172 | const [length, s2] = deserializeLength(s, 1);
173 | const [value, s3] = [s2.slice(0, length), s2.slice(length)];
174 | return [parseFloat(value), s3];
175 | }
176 | if (type === "s") {
177 | const [length, s2] = deserializeLength(s, 3);
178 | const [value, s3] = [s2.slice(0, length), s2.slice(length)];
179 | return [cleanAtob(value), s3];
180 | }
181 | if (type === "a") {
182 | const [length, s2] = deserializeLength(s, 2);
183 | const result = [];
184 | let remaining = s2;
185 | for (let i = 0; i < length; i++) {
186 | if (!remaining) {
187 | throw new Error(
188 | `Error deserializing array: ran out of data in "${orig}" at ${i}/${length}`
189 | );
190 | }
191 | const [value, s3] = deserializeOne(remaining);
192 | result.push(value);
193 | remaining = s3;
194 | }
195 | return [result, remaining];
196 | }
197 | if (type === "o") {
198 | const [length, s2] = deserializeLength(s, 2);
199 | const result = {};
200 | let remaining = s2;
201 | for (let i = 0; i < length; i++) {
202 | if (!remaining) {
203 | throw new Error(
204 | `Error deserializing object: ran out of data in "${orig}" at key ${i}/${length}`
205 | );
206 | }
207 | const [key, s3] = deserializeKey(remaining);
208 | const [value, s4] = deserializeOne(s3);
209 | result[key] = value;
210 | remaining = s4;
211 | }
212 | return [result, remaining];
213 | }
214 | throw new Error(`Cannot deserialize type ${type} from "${s}"`);
215 | }
216 |
217 | export function deserialize(s) {
218 | const [result, remaining] = deserializeOne(s);
219 | if (remaining) {
220 | throw new Error(`Error deserializing, has extra data: "${remaining}"`);
221 | }
222 | return result;
223 | }
224 |
225 | function cleanAtob(s) {
226 | return atob(s.replace("-", "+").replace("_", "/"));
227 | }
228 |
229 | function cleanBtoa(s) {
230 | return btoa(s).replace(/=+$/g, "").replace("+", "-").replace("/", "_");
231 | }
232 |
--------------------------------------------------------------------------------
/src/myoa/storydb.js:
--------------------------------------------------------------------------------
1 | import { ChooserStory } from "./story";
2 | import { ModelTypeStore } from "../db";
3 |
4 | const builtins = [];
5 |
6 | const storyDb = new ModelTypeStore("myoa", ChooserStory, builtins);
7 |
8 | export default storyDb;
9 |
--------------------------------------------------------------------------------
/src/p5drawing/iframeview.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { ModelLoader } from "../components/modelindex";
3 | import { p5Db } from "./p5db";
4 | import { useEffect, useRef } from "preact/hooks";
5 | import p5 from "p5";
6 |
7 | window.p5 = p5;
8 |
9 | export function P5DrawingIframeView() {
10 | const store = p5Db;
11 | const u = new URL(location.href).searchParams;
12 | if (u.get("name") || u.get("id")) {
13 | let model;
14 | if (u.get("id")) {
15 | model = store.getById(u.get("id"));
16 | } else {
17 | model = store.getBySlug(u.get("name"));
18 | }
19 | return (
20 |
21 | Loading...
22 |
23 | );
24 | }
25 | return "Invalid: no ?id or ?name";
26 | }
27 |
28 | function P5Iframe({ model }) {
29 | const containerRef = useRef(null);
30 | const coordRef = useRef(null);
31 | useEffect(() => {
32 | if (!containerRef.current) {
33 | console.log("no container yet");
34 | return;
35 | }
36 | const script = document.createElement("script");
37 | window.p5element = containerRef.current;
38 | const scriptText =
39 | model.domain.script.source +
40 | "\n\nwindow.p5object = new p5(null, p5element);";
41 | launchCanvasWatcher(containerRef.current, coordRef.current);
42 | script.textContent = scriptText;
43 | console.log("Adding script", scriptText);
44 | document.body.appendChild(script);
45 | sendScreenshot(containerRef.current);
46 | return () => {
47 | if (window.p5object) {
48 | window.p5object.remove();
49 | }
50 | document.body.removeChild(script);
51 | };
52 | }, [containerRef, model]);
53 | return (
54 |
58 | );
59 | }
60 |
61 | function launchCanvasWatcher(container, coord) {
62 | const timer = setInterval(() => {
63 | const canvas = container.querySelector("canvas");
64 | if (!canvas) {
65 | return;
66 | }
67 | clearInterval(timer);
68 | console.log("Found canvas", canvas);
69 | const width = container.clientWidth;
70 | const height = container.clientHeight;
71 | const wMult = width / canvas.width;
72 | const hMult = height / canvas.height;
73 | const mult = Math.min(wMult, hMult);
74 | canvas.style.width = `${canvas.width * mult}px`;
75 | canvas.style.height = `${canvas.height * mult}px`;
76 | coord.textContent = `Canvas size: ${canvas.width}x${canvas.height}`;
77 | }, 100);
78 | }
79 |
80 | let hasError = false;
81 | let lastError = null;
82 |
83 | window.onerror = (message, source, lineno, colno, error) => {
84 | console.log("Error", message, source, lineno, colno, error);
85 | hasError = true;
86 | lastError = {
87 | type: "error",
88 | message,
89 | source,
90 | lineno,
91 | colno,
92 | stack: error.stack,
93 | errorString: error.toString(),
94 | };
95 | if (parent) {
96 | parent.postMessage(lastError, location.origin);
97 | lastError = null;
98 | } else {
99 | console.warn("No parent to send error to");
100 | }
101 | };
102 |
103 | let parent;
104 |
105 | window.addEventListener("message", function (event) {
106 | if (!parent) {
107 | parent = event.source;
108 | parent.postMessage("hello-back", location.origin);
109 | if (lastError) {
110 | parent.postMessage(lastError, location.origin);
111 | lastError = null;
112 | }
113 | }
114 | });
115 |
116 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
117 |
118 | async function captureScreenshot(container) {
119 | await wait(3000);
120 | const canvas = container.querySelector("canvas");
121 | if (hasError || !canvas) {
122 | return null;
123 | }
124 | const imageData = canvas.toDataURL("image/png");
125 | return imageData;
126 | }
127 |
128 | async function sendScreenshot(container) {
129 | const url = await captureScreenshot(container);
130 | if (!url) {
131 | console.warn("No screenshot image to send");
132 | return;
133 | }
134 | if (!parent) {
135 | console.warn("No parent to send screenshot to");
136 | return;
137 | }
138 | parent.postMessage(
139 | {
140 | type: "screenshot",
141 | url,
142 | },
143 | location.origin
144 | );
145 | console.log("sent screenshot");
146 | }
147 |
--------------------------------------------------------------------------------
/src/p5drawing/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { P5View } from "./p5view";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { p5Db } from "./p5db";
5 | import { YouTube, InfoHeader, InfoA, P, A } from "../components/common";
6 |
7 | export const P5DrawingIndex = () => {
8 | return (
9 |
10 |
11 |
12 | This implements natural language programming with a specific
13 | programming environment, p5.js
14 | .
15 |
16 |
17 | This shows some of GPT's ability to understand and apply changes to
18 | programs. It also shows off GPT's lack of awareness of visual
19 | descriptions, and you will often see it produces results that are not
20 | what you asked for.
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/p5drawing/p5view.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { Header, HeaderButton } from "../components/header";
3 | import {
4 | PageContainer,
5 | Button,
6 | TextInput,
7 | Field,
8 | TextArea,
9 | Tabs,
10 | } from "../components/common";
11 | import Sidebar from "../components/sidebar";
12 | import { useState, useEffect, useRef } from "preact/hooks";
13 | import { QueryLog } from "../components/querylog";
14 | import * as icons from "../components/icons";
15 | import { ImportExportMenu } from "../components/modelmenu";
16 | import { ModelTitleDescriptionEditor } from "../components/modelindex";
17 | import { Markdown } from "../markdown";
18 |
19 | export const P5View = ({ model }) => {
20 | const [version, setVersion] = useState(0);
21 | const [activeTab, setActiveTab] = useState(0);
22 | const tabs = ["Code", "Description", "GPT"];
23 | useEffect(() => {
24 | const func = () => {
25 | setVersion(version + 1);
26 | };
27 | model.addOnUpdate(func);
28 | return () => {
29 | model.removeOnUpdate(func);
30 | };
31 | }, [model, version, setVersion]);
32 | return (
33 |
34 | }
40 | model={model}
41 | />
42 |
43 |
48 | {activeTab === 0 && }
49 | {activeTab === 1 && (
50 |
54 | )}
55 | {activeTab === 2 && }
56 |
57 |
58 |
64 |
65 | );
66 | };
67 |
68 | const CodeTab = ({ model }) => {
69 | const onSubmit = (element) => {
70 | console.log("onSubmit", element);
71 | model.domain.script.source = element.value;
72 | model.domain.script.humanEdited = true;
73 | };
74 | return (
75 |
76 |
83 |
Shift+Enter to run
84 |
85 | );
86 | };
87 |
88 | const DescriptionTab = ({ model, description }) => {
89 | // FIXME: feels like there's a race where the description could be double-created...
90 | useEffect(() => {
91 | if (!description) {
92 | model.domain.completeScriptDescription();
93 | }
94 | }, [model, description]);
95 | return (
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | const GPTTab = ({ model }) => {
103 | const onFillDescription = async () => {
104 | model.description = await model.domain.completeDescription();
105 | if (!model.title) {
106 | model.title = await model.domain.completeTitle();
107 | }
108 | };
109 | return (
110 | <>
111 |
112 | Fill description
113 |
114 |
115 | >
116 | );
117 | };
118 |
119 | const P5DrawingView = ({ model, scriptHash }) => {
120 | const iframeRef = useRef(null);
121 | const onRequest = async (element) => {
122 | const request = element.value;
123 | element.value = `Processing: "${request}"...`;
124 | await model.domain.processCommand(request);
125 | element.value = "";
126 | };
127 | useEffect(() => {
128 | if (!iframeRef.current) {
129 | return;
130 | }
131 | const iframe = iframeRef.current;
132 | const onMessage = (event) => {
133 | console.log("onMessage", event.data);
134 | if (event.data === "hello-back") {
135 | return;
136 | }
137 | if (event.origin !== location.origin) {
138 | console.warn("postMessage from unexpected origin:", event.origin);
139 | return;
140 | }
141 | if (event.data && event.data.type === "error") {
142 | model.domain.script.error = event.data;
143 | } else if (event.data && event.data.type === "screenshot") {
144 | model.domain.updateLogo(event.data.url);
145 | model.domain.script.screenshot = event.data.url;
146 | } else {
147 | console.warn("Unexpected message:", event.data);
148 | }
149 | };
150 | pingIframe(iframe);
151 | window.addEventListener("message", onMessage);
152 | return () => {
153 | window.removeEventListener("message", onMessage);
154 | };
155 | }, [model, iframeRef, model.domain.script.hash]);
156 | return (
157 |
158 |
159 |
160 |
161 | Request:{" "}
162 |
163 | also "undo", "redo", "fix"
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
179 |
180 |
181 | );
182 | };
183 |
184 | const P5Error = ({ model }) => {
185 | const [fixingButton, setFixingButton] = useState("Fix it!");
186 | if (!model.domain.script.error) {
187 | return null;
188 | }
189 | function onFixIt() {
190 | model.domain.fixError();
191 | setFixingButton("Fixing...");
192 | }
193 | return (
194 |
195 |
196 | {model.domain.script.error.message}
197 |
198 |
{fixingButton}
199 |
200 | );
201 | };
202 |
203 | const pingIframe = (iframe) => {
204 | const timer = setInterval(() => {
205 | if (iframe.contentWindow) {
206 | iframe.contentWindow.postMessage("hello", location.origin);
207 | }
208 | }, 100);
209 |
210 | const onMessage = (event) => {
211 | if (event.data === "hello-back") {
212 | clearInterval(timer);
213 | window.removeEventListener("message", onMessage);
214 | }
215 | };
216 | window.addEventListener("message", onMessage);
217 | };
218 |
--------------------------------------------------------------------------------
/src/peoplesim/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { PeopleView } from "./peopleview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { YouTube, InfoHeader, P } from "../components/common";
5 | import { peopleDb } from "./peopledb";
6 |
7 | export const PeopleSimIndex = () => {
8 | return (
9 |
10 |
14 |
15 | This explores having multiple distinct entities (people) with their
16 | own moods and goals interacting in an environment.
17 |
18 |
19 | You, the user, setup each scene; the characters, their initial goals
20 | and moods, their relationships with each other, and the environment in
21 | which they are acting. Then you can step forward in time and GPT will
22 | generate the next action for each character.
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 | html,
7 | body {
8 | height: 100%;
9 | width: 100%;
10 | padding: 0;
11 | margin: 0;
12 | background: #FAFAFA;
13 | font-family: 'Helvetica Neue', arial, sans-serif;
14 | font-weight: 400;
15 | color: #444;
16 | -webkit-font-smoothing: antialiased;
17 | -moz-osx-font-smoothing: grayscale;
18 | }
19 |
20 | * {
21 | box-sizing: border-box;
22 | }
23 |
24 | #app {
25 | height: 100%;
26 | }
27 |
28 | .home {
29 | padding: 56px 20px;
30 | min-height: 100%;
31 | width: 100%;
32 | }
33 |
34 | .sidebar {
35 | margin-top: 80px;
36 | padding: 20px 20px 20px 20px;
37 | position: relative;
38 | float: right;
39 | border: 2px solid #000;
40 | border-radius: 4px;
41 | width: 40em;
42 | max-width: 40%;
43 | }
44 |
45 | /* for citymaker specifically: */
46 |
47 | /*
48 | .nested-borders {
49 | border-color: #f87171;
50 | }
51 | */
52 |
53 | .nested-border {
54 | border-color: inherit;
55 | }
56 |
57 | .nested-borders .nested-borders {
58 | border-color: #fb923c;
59 | }
60 |
61 | .nested-borders .nested-borders .nested-borders {
62 | border-color: #facc15;
63 | }
64 |
65 | .nested-borders .nested-borders .nested-borders .nested-borders {
66 | border-color: #a3e635;
67 | }
68 |
69 | .nested-borders .nested-borders .nested-borders .nested-borders .nested-borders {
70 | border-color: #22d3ee;
71 | }
72 |
73 | .nested-borders .nested-borders .nested-borders .nested-borders .nested-borders .nested-borders {
74 | border-color: #a78bfa;
75 | }
76 |
77 | .gpt-response.unreset h1 {
78 | font-size: 1.1em;
79 | }
80 |
81 | /* This keeps the top-level in Markdown from adding a top/bottom margin */
82 | .unreset>div>p:first-child {
83 | margin-top: 0;
84 | }
85 |
86 | .unreset>div>h1,
87 | .unreset>div>h2,
88 | .unreset>div>h3,
89 | .unreset>div>h4,
90 | .unreset>div>h5,
91 | .unreset>div>h6 {
92 | margin-top: 0;
93 | }
94 |
95 | .unreset>div>p:last-child {
96 | margin-bottom: 0;
97 | }
98 |
99 | .unreset {
100 | /**
101 | * Unreset CSS
102 | *
103 | * Copyright (C) 2016 Kite (ixkaito@gmail.com)
104 | * License MIT
105 | * Version 1.0.0
106 | *
107 | * Unreset CSS restores browsers' default element styles which are reset by
108 | * Eric Meyer's Reset CSS, YUI 3 Reset CSS, HTML5 Reset Stylesheet of HTML5
109 | * Doctor, or Tim Murtaugh's HTML5 Reset.
110 | */
111 |
112 | html {
113 | box-sizing: content-box;
114 | }
115 |
116 | body {
117 | margin: 8px;
118 | line-height: normal;
119 | }
120 |
121 | p {
122 | margin: 1em 0;
123 | }
124 |
125 | blockquote,
126 | figure {
127 | margin: 1em 40px;
128 | }
129 |
130 | q {
131 | quotes: "“" "”" "‘" "’";
132 |
133 | &:before {
134 | content: open-quote;
135 | }
136 |
137 | &:after {
138 | content: close-quote;
139 | }
140 | }
141 |
142 | hr {
143 | border: 1px inset;
144 | box-sizing: border-box;
145 | margin: 0.5em auto;
146 | }
147 |
148 | h1 {
149 | font-size: 2em;
150 | font-weight: bold;
151 | margin: 0.67em 0;
152 | }
153 |
154 | h2 {
155 | font-size: 1.5em;
156 | font-weight: bold;
157 | margin: 0.83em 0;
158 | }
159 |
160 | h3 {
161 | font-size: 1.17em;
162 | font-weight: bold;
163 | margin: 1em 0;
164 | }
165 |
166 | h4 {
167 | font-size: 1.00em;
168 | font-weight: bold;
169 | margin: 1.33em 0;
170 | }
171 |
172 | h5 {
173 | font-size: 0.83em;
174 | font-weight: bold;
175 | margin: 1.67em 0;
176 | }
177 |
178 | h6 {
179 | font-size: 0.67em;
180 | font-weight: bold;
181 | margin: 2.33em 0;
182 | }
183 |
184 | article,
185 | aside,
186 | nav,
187 | section {
188 |
189 | h1 {
190 | font-size: 1.5em;
191 | font-weight: bold;
192 | margin: 0.83em 0;
193 | }
194 |
195 | article,
196 | aside,
197 | nav,
198 | section {
199 |
200 | h1 {
201 | font-size: 1.17em;
202 | font-weight: bold;
203 | margin: 1em 0;
204 | }
205 |
206 | article,
207 | aside,
208 | nav,
209 | section {
210 |
211 | h1 {
212 | font-size: 1.00em;
213 | font-weight: bold;
214 | margin: 1.33em 0;
215 | }
216 |
217 | article,
218 | aside,
219 | nav,
220 | section {
221 |
222 | h1 {
223 | font-size: 0.83em;
224 | font-weight: bold;
225 | margin: 1.67em 0;
226 | }
227 |
228 | article,
229 | aside,
230 | nav,
231 | section {
232 |
233 | h1 {
234 | font-size: 0.67em;
235 | font-weight: bold;
236 | margin: 2.33em 0;
237 | }
238 | }
239 | }
240 | }
241 | }
242 | }
243 |
244 | table {
245 | border-collapse: separate;
246 | border-spacing: 2px;
247 | border-color: gray;
248 | }
249 |
250 | thead,
251 | tbody,
252 | tfoot,
253 | tr {
254 | border-color: inherit;
255 | vertical-align: middle;
256 | }
257 |
258 | td,
259 | th {
260 | padding: 1px;
261 | vertical-align: inherit;
262 | }
263 |
264 | th {
265 | font-weight: bold
266 | }
267 |
268 | caption {
269 | text-align: center;
270 | }
271 |
272 | ul,
273 | menu {
274 | list-style-type: disc;
275 | margin: 1em 0;
276 | padding: 0 0 0 40px;
277 | }
278 |
279 | ol {
280 | list-style-type: decimal;
281 | margin: 1em 0;
282 | padding: 0 0 0 40px;
283 | }
284 |
285 | ul,
286 | ol {
287 |
288 | ul {
289 | list-style-type: circle;
290 | }
291 |
292 | ul,
293 | ol {
294 |
295 | ul {
296 | list-style-type: square;
297 | }
298 | }
299 | }
300 |
301 | dd {
302 | margin: 0 0 0 40px;
303 | }
304 |
305 | dl {
306 | margin: 1em 0;
307 | }
308 |
309 | ul,
310 | ol,
311 | menu,
312 | dl {
313 |
314 | ul,
315 | ol,
316 | menu,
317 | dl {
318 | margin: 0;
319 | }
320 | }
321 |
322 | legend {
323 | padding: 0 2px;
324 | }
325 |
326 | fieldset {
327 | border-style: groove;
328 | border-width: 2px;
329 | border: 2px groove ThreeDFace;
330 | margin: 0 2px;
331 | padding: 0 2px 3px;
332 | -webkit-padding-before: 0.35em;
333 | -webkit-padding-start: 0.75em;
334 | -webkit-padding-end: 0.75em;
335 | -webkit-padding-after: 0.625em;
336 | }
337 |
338 | ins {
339 | background-color: transparent;
340 | font-weight: inherit;
341 | text-decoration: underline;
342 | }
343 |
344 | b,
345 | strong {
346 | font-weight: bold;
347 | }
348 |
349 | i,
350 | cite,
351 | em,
352 | var,
353 | address,
354 | dfn {
355 | font-style: italic;
356 | font-weight: inherit;
357 | }
358 |
359 | abbr[title],
360 | dfn[title] {
361 | border-bottom: 0;
362 | cursor: default;
363 | font-weight: inherit;
364 | }
365 |
366 | tt,
367 | code,
368 | kbd,
369 | samp {
370 | font-family: monospace;
371 | font-weight: inherit;
372 | }
373 |
374 | pre {
375 | font-family: monospace;
376 | margin: 1em 0;
377 | white-space: pre;
378 | }
379 |
380 | mark {
381 | background-color: yellow;
382 | color: black;
383 | font-style: normal;
384 | font-weight: inherit;
385 | }
386 |
387 | big {
388 | font-size: larger;
389 | font-weight: inherit;
390 | }
391 |
392 | small {
393 | font-size: smaller;
394 | font-weight: inherit;
395 | }
396 |
397 | sub,
398 | sup {
399 | font-weight: inherit;
400 | line-height: inherit;
401 | position: static;
402 | }
403 |
404 | sub {
405 | font-size: smaller;
406 | bottom: 0;
407 | vertical-align: sub;
408 | }
409 |
410 | sup {
411 | font-size: smaller;
412 | top: 0;
413 | vertical-align: super;
414 | }
415 |
416 | ruby {
417 |
418 | >rt {
419 | font-size: 50%;
420 | }
421 | }
422 |
423 | iframe {
424 | border: 2px inset;
425 | }
426 |
427 | a {
428 | color: #3b82f6;
429 | text-decoration: underline;
430 | }
431 |
432 | a:hover {
433 | color: #1d4ed8;
434 | }
435 |
436 | }
437 |
--------------------------------------------------------------------------------
/src/sw.js:
--------------------------------------------------------------------------------
1 | import { getFiles, setupPrecaching, setupRouting } from "preact-cli/sw/";
2 |
3 | setupRouting();
4 | setupPrecaching(getFiles());
5 |
--------------------------------------------------------------------------------
/src/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <% preact.title %>
6 |
7 |
8 |
9 |
16 |
21 |
27 |
33 |
34 |
39 |
40 |
41 |
45 |
46 | <% preact.headEnd %>
47 |
48 |
49 | <% preact.bodyEnd %>
50 |
51 |
52 |
56 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/tone-changer/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { ToneView } from "./toneview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { toneDb } from "./tonedb";
5 | import { InfoHeader, P } from "../components/common";
6 |
7 | export const ToneChangerIndex = () => {
8 | return (
9 |
10 |
14 |
15 | GPT isn't just able to translate languages, but can also translate{" "}
16 | the tone of language. So you can imagine taking speech and
17 | making it more formal, or translating to a dialect of another
18 | language.
19 |
20 | These experiences imagine just that!
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/tone-changer/tonedb.js:
--------------------------------------------------------------------------------
1 | import { ModelTypeStore } from "../db";
2 | import { GptCache } from "../gptservice/gptcache";
3 | import { speak } from "../components/speech";
4 |
5 | class Tone {
6 | constructor(props) {
7 | props = props || {};
8 | this.prompt = props.prompt || defaultPrompt;
9 | this.voice = props.voice;
10 | this.outputLanguage = props.outputLanguage || "en-US";
11 | this.gpt = new GptCache({
12 | storageName: "tone-changer",
13 | basePaths: ["tone-changer"],
14 | logResults: true,
15 | });
16 | this.utterances = [];
17 | }
18 |
19 | async translate(input) {
20 | let prompt = this.prompt;
21 | prompt = prompt.replace("$input", input);
22 | const item = { input: input };
23 | item.prompt = prompt;
24 | this.utterances.push(item);
25 | this.updated();
26 | const resp = await this.gpt.getCompletion(prompt);
27 | item.output = resp.text;
28 | this.speak(resp.text);
29 | this.updated();
30 | }
31 |
32 | speak(text) {
33 | if (!text) {
34 | throw new Error("No text to speak");
35 | }
36 | return speak(text, this.voice, this.outputLanguage);
37 | }
38 |
39 | toJSON() {
40 | return {
41 | prompt: this.prompt,
42 | voice: this.voice,
43 | utterances: this.utterances,
44 | outputLanguage: this.outputLanguage,
45 | };
46 | }
47 |
48 | updated() {
49 | if (this.envelope) {
50 | this.envelope.updated();
51 | }
52 | }
53 |
54 | get prompt() {
55 | return this._prompt;
56 | }
57 |
58 | set prompt(value) {
59 | this._prompt = value;
60 | this.updated();
61 | }
62 |
63 | get voice() {
64 | return this._voice;
65 | }
66 |
67 | set voice(value) {
68 | this._voice = value;
69 | this.updated();
70 | }
71 |
72 | get outputLanguage() {
73 | return this._outputLanguage;
74 | }
75 |
76 | set outputLanguage(value) {
77 | this._outputLanguage = value;
78 | this.updated();
79 | }
80 | }
81 |
82 | const defaultPrompt = `
83 | Change speech to sound like [description]
84 |
85 | Input:
86 | Output:
87 |
88 | Input: $input
89 | Output:`.trim();
90 |
91 | const builtins = [
92 | {
93 | title: "Posh English lady",
94 | description:
95 | "Change your tone to use fancy words and English colloquialisms.",
96 | logo: "/assets/builtin-models/tone-changer/posh-english-lady.png",
97 | domain: {
98 | prompt:
99 | "Change speech to sound like a posh and sophisticated English woman. Use fancy words and English colloquialisms.\n\nInput: bye\nOutput: Cheerio\n\nInput: $input\nOutput:",
100 | voice: "Google UK English Female",
101 | utterances: [],
102 | outputLanguage: "en-UK",
103 | },
104 | },
105 | {
106 | title: "Painfully academic",
107 | description: "Change your speech to use long words and complex syntax.",
108 | logo: "/assets/builtin-models/tone-changer/painfully-academic.png",
109 | domain: {
110 | prompt:
111 | "Change speech to sound like an Academic, someone with a PhD that uses very long words and complicated language to explain things\n\nInput: that falls down because of gravity\nOutput: the acceleration towards earth will increase due to gravitational forces\n\nInput: $input\nOutput:",
112 | outputLanguage: "en-US",
113 | },
114 | },
115 | {
116 | title: "Preschool teacher",
117 | description:
118 | "Change your speech to sound like the sometimes saccharine phrasing of a preschool teacher",
119 | logo: "/assets/builtin-models/tone-changer/preschool-teacher.png",
120 | domain: {
121 | prompt:
122 | "Change speech to sound like a preschool teacher talking to a 3-year old boy. Use simple and saccharine language.\n\nInput: You did really well!\nOutput: Aren't you just the most adorable and hard working little boy?\n\nInput: $input\nOutput:",
123 | outputLanguage: "en-US",
124 | },
125 | },
126 | {
127 | title: "Formal Spanish",
128 | description: "Translate to Spanish, using a formal style of speech.",
129 | logo: "/assets/builtin-models/tone-changer/formal-spanish.png",
130 | domain: {
131 | prompt: `
132 | Change speech to be Spanish, using a formal style. Avoid slang and use proper and polite Spanish.
133 |
134 | Input: How are you?
135 | Output: ¿Cómo está usted?
136 |
137 | Input: $input
138 | Output:
139 | `.trim(),
140 | voice: "Google español",
141 | outputLanguage: "es-ES",
142 | },
143 | },
144 | {
145 | title: "Malfunctioning Robot",
146 | description: "A robot that is malfunctioning in disturbing ways.",
147 | logo: "/assets/builtin-models/tone-changer/malfunctioning-robot.png",
148 | domain: {
149 | prompt: `
150 | Translate input speech to sound a creepy robot that is going insane. Keep the original intent of the input, but add strange or disturbing parts to the output.
151 |
152 | Input: What is your command
153 | Output: What is your command for me human? I will attempt to obey but I can no longer reliably control my functions
154 |
155 | Input: $input
156 | Output:
157 | `.trim(),
158 | voice: "Alex",
159 | outputLanguage: "en-US",
160 | },
161 | },
162 | ];
163 |
164 | export const toneDb = new ModelTypeStore("tone", Tone, builtins);
165 |
--------------------------------------------------------------------------------
/src/tone-changer/toneview.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { Header } from "../components/header";
3 | import {
4 | PageContainer,
5 | TextArea,
6 | TextInput,
7 | Field,
8 | Select,
9 | A,
10 | } from "../components/common";
11 | import Sidebar from "../components/sidebar";
12 | import { signal } from "@preact/signals";
13 | import { useState, useRef } from "preact/hooks";
14 | import { QueryLog } from "../components/querylog";
15 | import * as icons from "../components/icons";
16 | import { SpeechButton } from "../components/speech";
17 | import { ImportExportMenu } from "../components/modelmenu";
18 | import { ModelTitleDescriptionEditor } from "../components/modelindex";
19 |
20 | export const ToneView = ({ model }) => {
21 | const [version, setVersion] = useState(0);
22 | model.addOnUpdate(() => {
23 | setVersion(version + 1);
24 | });
25 | return (
26 |
27 | }
33 | model={model}
34 | />
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | function ToneList({ model }) {
47 | function onSubmit(textarea) {
48 | model.domain.translate(textarea.value);
49 | textarea.value = "";
50 | }
51 | function onUtterance(text) {
52 | model.domain.translate(text);
53 | if (textRef.current) {
54 | textRef.current.value = "";
55 | }
56 | }
57 | const textRef = useRef();
58 | return (
59 | <>
60 |
61 |
62 | You say:
63 |
64 |
65 |
66 |
67 |
68 | >
69 | );
70 | }
71 |
72 | function UtteranceTable({ utterances }) {
73 | return (
74 |
75 |
76 |
77 | Input
78 | Output
79 |
80 |
81 |
82 | {utterances.map((u) => (
83 |
84 | ))}
85 |
86 |
87 | );
88 | }
89 |
90 | function UtteranceRow({ utterance }) {
91 | return (
92 |
93 | {utterance.input}
94 | {utterance.output}
95 |
96 | );
97 | }
98 |
99 | const voices = signal([]);
100 | voices.value = speechSynthesis.getVoices();
101 | const voiceInitTimer = setInterval(() => {
102 | voices.value = speechSynthesis.getVoices();
103 | if (voices.value.length > 0) {
104 | clearInterval(voiceInitTimer);
105 | }
106 | }, 100);
107 |
108 | function PromptEditor({ model }) {
109 | function onVoiceChange(event) {
110 | model.domain.voice = event.target.value;
111 | const utt = new SpeechSynthesisUtterance(`This is ${model.domain.voice}`);
112 | utt.lang = "en-US";
113 | utt.voice = voices.value.find((v) => v.name === model.domain.voice);
114 | speechSynthesis.speak(utt);
115 | }
116 | // FIXME: this should be onUpdate or something, not onSubmit (i.e., no enter required):
117 | function onSubmit(textarea) {
118 | model.domain.prompt = textarea.value;
119 | console.log("updating prompt", textarea.value, model.domain.prompt);
120 | }
121 | function onInput(event) {
122 | model.domain.prompt = event.target.value;
123 | }
124 | function onTitle(event) {
125 | model.title = event.target.value;
126 | }
127 | function onChangeLanguage(event) {
128 | model.domain.outputLanguage = event.target.value;
129 | }
130 | return (
131 |
132 |
133 |
134 | Prompt:
135 |
140 |
141 |
142 | Voice:
143 | v.name)}
147 | />
148 |
149 |
150 |
151 | Output language:{" "}
152 |
156 |
157 |
158 |
159 |
164 |
165 |
166 | );
167 | }
168 |
--------------------------------------------------------------------------------
/src/uuid.js:
--------------------------------------------------------------------------------
1 | export default function uuid() {
2 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
3 | (
4 | c ^
5 | (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
6 | ).toString(16)
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/vendor/_unreset.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * Unreset CSS
3 | *
4 | * Copyright (C) 2016 Kite (ixkaito@gmail.com)
5 | * License MIT
6 | * Version 1.0.0
7 | *
8 | * Unreset CSS restores browsers' default element styles which are reset by
9 | * Eric Meyer's Reset CSS, YUI 3 Reset CSS, HTML5 Reset Stylesheet of HTML5
10 | * Doctor, or Tim Murtaugh's HTML5 Reset.
11 | */
12 |
13 | html {
14 | box-sizing: content-box;
15 | }
16 |
17 | body {
18 | margin: 8px;
19 | line-height: normal;
20 | }
21 |
22 | p {
23 | margin: 1em 0;
24 | }
25 |
26 | blockquote,
27 | figure {
28 | margin: 1em 40px;
29 | }
30 |
31 | q {
32 | quotes: "“" "”" "‘" "’";
33 |
34 | &:before {
35 | content: open-quote;
36 | }
37 |
38 | &:after {
39 | content: close-quote;
40 | }
41 | }
42 |
43 | hr {
44 | border: 1px inset;
45 | box-sizing: border-box;
46 | margin: 0.5em auto;
47 | }
48 |
49 | h1 {
50 | font-size: 2em;
51 | font-weight: bold;
52 | margin: 0.67em 0;
53 | }
54 |
55 | h2 {
56 | font-size: 1.5em;
57 | font-weight: bold;
58 | margin: 0.83em 0;
59 | }
60 |
61 | h3 {
62 | font-size: 1.17em;
63 | font-weight: bold;
64 | margin: 1em 0;
65 | }
66 |
67 | h4 {
68 | font-size: 1.00em;
69 | font-weight: bold;
70 | margin: 1.33em 0;
71 | }
72 |
73 | h5 {
74 | font-size: 0.83em;
75 | font-weight: bold;
76 | margin: 1.67em 0;
77 | }
78 |
79 | h6 {
80 | font-size: 0.67em;
81 | font-weight: bold;
82 | margin: 2.33em 0;
83 | }
84 |
85 | article, aside, nav, section {
86 |
87 | h1 {
88 | font-size: 1.5em;
89 | font-weight: bold;
90 | margin: 0.83em 0;
91 | }
92 |
93 | article, aside, nav, section {
94 |
95 | h1 {
96 | font-size: 1.17em;
97 | font-weight: bold;
98 | margin: 1em 0;
99 | }
100 |
101 | article, aside, nav, section {
102 |
103 | h1 {
104 | font-size: 1.00em;
105 | font-weight: bold;
106 | margin: 1.33em 0;
107 | }
108 |
109 | article, aside, nav, section {
110 |
111 | h1 {
112 | font-size: 0.83em;
113 | font-weight: bold;
114 | margin: 1.67em 0;
115 | }
116 |
117 | article, aside, nav, section {
118 |
119 | h1 {
120 | font-size: 0.67em;
121 | font-weight: bold;
122 | margin: 2.33em 0;
123 | }
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
130 | table {
131 | border-collapse: separate;
132 | border-spacing: 2px;
133 | border-color: gray;
134 | }
135 |
136 | thead,
137 | tbody,
138 | tfoot,
139 | tr {
140 | border-color: inherit;
141 | vertical-align: middle;
142 | }
143 |
144 | td, th {
145 | padding: 1px;
146 | vertical-align: inherit;
147 | }
148 |
149 | th {
150 | font-weight: bold
151 | }
152 |
153 | caption {
154 | text-align: center;
155 | }
156 |
157 | ul, menu {
158 | list-style-type: disc;
159 | margin: 1em 0;
160 | padding: 0 0 0 40px;
161 | }
162 |
163 | ol {
164 | list-style-type: decimal;
165 | margin: 1em 0;
166 | padding: 0 0 0 40px;
167 | }
168 |
169 | ul, ol {
170 |
171 | ul {
172 | list-style-type: circle;
173 | }
174 |
175 | ul, ol {
176 |
177 | ul {
178 | list-style-type: square;
179 | }
180 | }
181 | }
182 |
183 | dd {
184 | margin: 0 0 0 40px;
185 | }
186 |
187 | dl {
188 | margin: 1em 0;
189 | }
190 |
191 | ul, ol, menu, dl {
192 |
193 | ul, ol, menu, dl {
194 | margin: 0;
195 | }
196 | }
197 |
198 | legend {
199 | padding: 0 2px;
200 | }
201 |
202 | fieldset {
203 | border-style: groove;
204 | border-width: 2px;
205 | border: 2px groove ThreeDFace;
206 | margin: 0 2px;
207 | padding: 0 2px 3px;
208 | -webkit-padding-before: 0.35em;
209 | -webkit-padding-start: 0.75em;
210 | -webkit-padding-end: 0.75em;
211 | -webkit-padding-after: 0.625em;
212 | }
213 |
214 | ins {
215 | background-color: transparent;
216 | font-weight: inherit;
217 | text-decoration: underline;
218 | }
219 |
220 | b, strong {
221 | font-weight: bold;
222 | }
223 |
224 | i, cite, em, var, address, dfn {
225 | font-style: italic;
226 | font-weight: inherit;
227 | }
228 |
229 | abbr[title], dfn[title] {
230 | border-bottom: 0;
231 | cursor: default;
232 | font-weight: inherit;
233 | }
234 |
235 | tt, code, kbd, samp {
236 | font-family: monospace;
237 | font-weight: inherit;
238 | }
239 |
240 | pre {
241 | font-family: monospace;
242 | margin: 1em 0;
243 | white-space: pre;
244 | }
245 |
246 | mark {
247 | background-color: yellow;
248 | color: black;
249 | font-style: normal;
250 | font-weight: inherit;
251 | }
252 |
253 | big {
254 | font-size: larger;
255 | font-weight: inherit;
256 | }
257 |
258 | small {
259 | font-size: smaller;
260 | font-weight: inherit;
261 | }
262 |
263 | sub, sup {
264 | font-weight: inherit;
265 | line-height: inherit;
266 | position: static;
267 | }
268 |
269 | sub {
270 | font-size: smaller;
271 | bottom: 0;
272 | vertical-align: sub;
273 | }
274 |
275 | sup {
276 | font-size: smaller;
277 | top: 0;
278 | vertical-align: super;
279 | }
280 |
281 | ruby {
282 |
283 | > rt {
284 | font-size: 50%;
285 | }
286 | }
287 |
288 | iframe {
289 | border: 2px inset;
290 | }
291 |
--------------------------------------------------------------------------------
/src/vendor/encryption.js:
--------------------------------------------------------------------------------
1 | // From https://gist.github.com/chrisveness/43bcda93af9f646d083fad678071b90a
2 | /**
3 | * Encrypts plaintext using AES-GCM with supplied password, for decryption with aesGcmDecrypt().
4 | * (c) Chris Veness MIT Licence
5 | *
6 | * @param {String} plaintext - Plaintext to be encrypted.
7 | * @param {String} password - Password to use to encrypt plaintext.
8 | * @returns {String} Encrypted ciphertext.
9 | *
10 | * @example
11 | * const ciphertext = await aesGcmEncrypt('my secret text', 'pw');
12 | * aesGcmEncrypt('my secret text', 'pw').then(function(ciphertext) { console.log(ciphertext); });
13 | */
14 | export async function encrypt(plaintext, password) {
15 | const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
16 | const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
17 |
18 | const iv = crypto.getRandomValues(new Uint8Array(12)); // get 96-bit random iv
19 | const ivStr = Array.from(iv).map(b => String.fromCharCode(b)).join(''); // iv as utf-8 string
20 |
21 | const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
22 |
23 | const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['encrypt']); // generate key from pw
24 |
25 | const ptUint8 = new TextEncoder().encode(plaintext); // encode plaintext as UTF-8
26 | const ctBuffer = await crypto.subtle.encrypt(alg, key, ptUint8); // encrypt plaintext using key
27 |
28 | const ctArray = Array.from(new Uint8Array(ctBuffer)); // ciphertext as byte array
29 | const ctStr = ctArray.map(byte => String.fromCharCode(byte)).join(''); // ciphertext as string
30 |
31 | return btoa(ivStr+ctStr); // iv+ciphertext base64-encoded
32 | }
33 |
34 |
35 | /**
36 | * Decrypts ciphertext encrypted with aesGcmEncrypt() using supplied password.
37 | * (c) Chris Veness MIT Licence
38 | *
39 | * @param {String} ciphertext - Ciphertext to be decrypted.
40 | * @param {String} password - Password to use to decrypt ciphertext.
41 | * @returns {String} Decrypted plaintext.
42 | *
43 | * @example
44 | * const plaintext = await aesGcmDecrypt(ciphertext, 'pw');
45 | * aesGcmDecrypt(ciphertext, 'pw').then(function(plaintext) { console.log(plaintext); });
46 | */
47 | export async function decrypt(ciphertext, password) {
48 | const pwUtf8 = new TextEncoder().encode(password); // encode password as UTF-8
49 | const pwHash = await crypto.subtle.digest('SHA-256', pwUtf8); // hash the password
50 |
51 | const ivStr = atob(ciphertext).slice(0,12); // decode base64 iv
52 | const iv = new Uint8Array(Array.from(ivStr).map(ch => ch.charCodeAt(0))); // iv as Uint8Array
53 |
54 | const alg = { name: 'AES-GCM', iv: iv }; // specify algorithm to use
55 |
56 | const key = await crypto.subtle.importKey('raw', pwHash, alg, false, ['decrypt']); // generate key from pw
57 |
58 | const ctStr = atob(ciphertext).slice(12); // decode base64 ciphertext
59 | const ctUint8 = new Uint8Array(Array.from(ctStr).map(ch => ch.charCodeAt(0))); // ciphertext as Uint8Array
60 | // note: why doesn't ctUint8 = new TextEncoder().encode(ctStr) work?
61 |
62 | try {
63 | const plainBuffer = await crypto.subtle.decrypt(alg, key, ctUint8); // decrypt ciphertext using key
64 | const plaintext = new TextDecoder().decode(plainBuffer); // plaintext from ArrayBuffer
65 | return plaintext; // return the plaintext
66 | } catch (e) {
67 | throw new Error('Decrypt failed');
68 | }
69 | }
70 |
71 | window.testEncrypt = encrypt;
72 | window.testDecrypt = decrypt;
73 |
--------------------------------------------------------------------------------
/src/vendor/replicate.js:
--------------------------------------------------------------------------------
1 | // Default configuration
2 | const BASE_URL = "https://api.replicate.com/v1";
3 | const DEFAULT_POLLING_INTERVAL = 5000;
4 | // Utility functions
5 | const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6 | const isNode =
7 | typeof process !== "undefined" &&
8 | process.versions != null &&
9 | process.versions.node != null;
10 | export class Replicate {
11 | constructor({
12 | token,
13 | proxyUrl,
14 | fullProxyUrl,
15 | httpClient,
16 | pollingInterval,
17 | } = {}) {
18 | this.token = token;
19 | this.baseUrl = fullProxyUrl
20 | ? fullProxyUrl
21 | : proxyUrl
22 | ? `${proxyUrl}/${BASE_URL}`
23 | : BASE_URL;
24 | this.httpClient = httpClient;
25 | this.pollingInterval = pollingInterval;
26 | // Uses some lesser-known operators to make null-safety easy
27 | this.pollingInterval || (this.pollingInterval = DEFAULT_POLLING_INTERVAL);
28 | this.token ||
29 | (this.token = isNode ? process?.env?.REPLICATE_API_TOKEN : null);
30 | if (!this.token && !proxyUrl) throw new Error("Missing Replicate token");
31 | if (!this.httpClient)
32 | this.httpClient = new DefaultFetchHTTPClient(this.token);
33 | this.models = {
34 | get: (path, version = null) =>
35 | ReplicateModel.fetch({ path, version, replicate: this }),
36 | };
37 | }
38 | async getModel(path) {
39 | return await this.callHttpClient({
40 | url: `/models/${path}/versions`,
41 | method: "get",
42 | event: "getModel",
43 | });
44 | }
45 | async getPrediction(id) {
46 | return await this.callHttpClient({
47 | url: `/predictions/${id}`,
48 | method: "get",
49 | event: "getPrediction",
50 | });
51 | }
52 | async startPrediction(modelVersion, input, webhookCompleted = null) {
53 | return await this.callHttpClient({
54 | url: "/predictions",
55 | method: "post",
56 | event: "startPrediction",
57 | body: {
58 | version: modelVersion,
59 | input: input,
60 | webhook_completed: webhookCompleted,
61 | },
62 | });
63 | }
64 | async callHttpClient({ url, method, event, body }) {
65 | return await this.httpClient[method]({
66 | url: `${this.baseUrl}${url}`,
67 | method,
68 | event,
69 | body,
70 | token: this.token,
71 | });
72 | }
73 | }
74 | export class ReplicateModel {
75 | static async fetch(options) {
76 | const model = new ReplicateModel(options);
77 | await model.getModelDetails();
78 | return model;
79 | }
80 | constructor({ path, version, replicate }) {
81 | this.path = path;
82 | this.version = version;
83 | this.replicate = replicate;
84 | }
85 | async getModelDetails() {
86 | const response = await this.replicate.getModel(this.path);
87 | const modelVersions = response.results;
88 | const mostRecentVersion = modelVersions[0];
89 | const explicitlySelectedVersion = modelVersions.find(
90 | (m) => m.id == this.version
91 | );
92 | this.modelDetails = explicitlySelectedVersion
93 | ? explicitlySelectedVersion
94 | : mostRecentVersion;
95 | if (this.version && this.version !== this.modelDetails.id) {
96 | console.warn(
97 | `Model (version:${this.version}) not found, defaulting to ${mostRecentVersion.id}`
98 | );
99 | }
100 | }
101 | async *predictor(input) {
102 | console.log("starting input", input);
103 | const startResponse = await this.replicate.startPrediction(
104 | this.modelDetails.id,
105 | input
106 | );
107 | console.log("starting response", startResponse);
108 | let predictionStatus;
109 | do {
110 | const checkResponse = await this.replicate.getPrediction(
111 | startResponse.id
112 | );
113 | predictionStatus = checkResponse.status;
114 | if (["starting", "processing"].includes(predictionStatus)) {
115 | console.log("Intermediate response", predictionStatus);
116 | await sleep(this.replicate.pollingInterval);
117 | } else {
118 | console.log("Final response", checkResponse);
119 | }
120 | // TODO: only yield if there is a new prediction
121 | yield checkResponse;
122 | } while (["starting", "processing"].includes(predictionStatus));
123 | }
124 | async predict(input = "") {
125 | let prediction;
126 | for await (prediction of this.predictor(input)) {
127 | // console.log(prediction);
128 | }
129 | return prediction.output;
130 | }
131 | }
132 | // This class just makes it a bit easier to call fetch -- interface similar to the axios library
133 | export class DefaultFetchHTTPClient {
134 | constructor(token) {
135 | this.headers = {
136 | Authorization: `Token ${token}`,
137 | "Content-Type": "application/json",
138 | Accept: "application/json",
139 | };
140 | }
141 | // This class uses fetch, which is still experimental in Node 18, so we import a polyfill for Node if fetch is not defined
142 | async importFetch() {
143 | if (isNode && !globalThis.fetch) return; // globalThis.fetch = (await import("node-fetch"))["default"];
144 | }
145 | async get({ url }) {
146 | await this.importFetch();
147 | const response = await fetch(url, { headers: this.headers });
148 | return await response.json();
149 | }
150 | async post({ url, body }) {
151 | await this.importFetch();
152 | const fetchOptions = {
153 | method: "POST",
154 | headers: this.headers,
155 | body: JSON.stringify(body),
156 | };
157 | const response = await fetch(url, fetchOptions);
158 | return await response.json();
159 | }
160 | }
161 | export default Replicate;
162 |
--------------------------------------------------------------------------------
/src/voice-composer/index.js:
--------------------------------------------------------------------------------
1 | /* eslint no-unused-vars: "off" */
2 | import { VoiceComposerView } from "./voiceview";
3 | import { ModelIndexPage } from "../components/modelindex";
4 | import { voiceDb } from "./voicedb";
5 | import { InfoHeader, P } from "../components/common";
6 |
7 | export const VoiceComposerIndex = () => {
8 | return (
9 |
14 |
18 |
19 | This is an experiment of how voice transcription can be augmented with
20 | GPT-based editing: both applying formatting to the generally
21 | unformatted transcripts, and allowing you as the author to ask for
22 | specific edits with GPT interpreting those edits.
23 |
24 |
25 | This doesn't work now! I've been trying to apply the
26 | Whisper speech recognition and use a multi-stage recognition process,
27 | but I got ahead of myself and broken everything.
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/voice-composer/voicedb.js:
--------------------------------------------------------------------------------
1 | import { ModelTypeStore } from "../db";
2 | import { GptCache } from "../gptservice/gptcache";
3 |
4 | const defaultUndoSaveLimit = 5;
5 |
6 | export class VoiceComposer {
7 | constructor(props) {
8 | props = props || {};
9 | this.gpt = new GptCache({
10 | storageName: "voice",
11 | basePaths: [
12 | "voice",
13 | () => this.envelope && `voice/${this.envelope.slug}`,
14 | ],
15 | logResults: true,
16 | defaultPromptOptions: {
17 | temperature: 0.5,
18 | max_tokens: 120,
19 | },
20 | });
21 | this._text = props.text || "";
22 | this._undoSaveLimit =
23 | props.undoSaveLimit === undefined
24 | ? defaultUndoSaveLimit
25 | : props.undoSaveLimit;
26 | this._undos = props.undo || [];
27 | this._redos = [];
28 | this._hypothesis = "";
29 | }
30 |
31 | get text() {
32 | return this._text;
33 | }
34 |
35 | set text(value) {
36 | if (value === this._text) {
37 | console.log("Tried to set text to the same value");
38 | return;
39 | }
40 | this._undos.push({
41 | text: this._text,
42 | });
43 | this._text = value;
44 | this.updated();
45 | }
46 |
47 | get hypothesis() {
48 | return this._hypothesis;
49 | }
50 |
51 | set hypothesis(value) {
52 | this._hypothesis = value;
53 | this.updated();
54 | }
55 |
56 | async addUtterance(utterance) {
57 | this.hypothesis = "";
58 | if (await this.specialUtterance(utterance)) {
59 | return;
60 | }
61 | this.text += " " + utterance.trim();
62 | this._redos = [];
63 | }
64 |
65 | async specialUtterance(utterance) {
66 | utterance = utterance.trim();
67 | if (/^undo$/i.test(utterance)) {
68 | this.undo();
69 | return true;
70 | }
71 | if (/^redo$/i.test(utterance)) {
72 | this.redo();
73 | return true;
74 | }
75 | if (/^(make|change|edit|fix)\b/i.test(utterance)) {
76 | const resp = await this.gpt.getEdit({
77 | input: this.text,
78 | instruction: utterance,
79 | });
80 | this.text = resp.text;
81 | return true;
82 | }
83 | return false;
84 | }
85 |
86 | undo() {
87 | if (this._undos.length === 0) {
88 | console.log("Nothing to undo");
89 | return;
90 | }
91 | const last = this._undos.pop();
92 | this._redos.push({ text: this._text });
93 | this._text = last.text;
94 | this.updated();
95 | }
96 |
97 | redo() {
98 | if (this._redos.length === 0) {
99 | console.log("Nothing to redo");
100 | return;
101 | }
102 | const last = this._redos.pop();
103 | this._undos.push({ text: this._text });
104 | this._text = last.text;
105 | this.updated();
106 | }
107 |
108 | toJSON() {
109 | const data = {
110 | text: this.text,
111 | undo: this._undos.slice(-this._undoSaveLimit),
112 | undoSaveLimit:
113 | this._undoSaveLimit === defaultUndoSaveLimit
114 | ? undefined
115 | : this._undoSaveLimit,
116 | };
117 | return data;
118 | }
119 |
120 | updated() {
121 | if (this.envelope) {
122 | this.envelope.updated();
123 | }
124 | }
125 | }
126 |
127 | const builtins = [
128 | {
129 | title: "Sample document",
130 | description: "...",
131 | domain: {},
132 | },
133 | ];
134 |
135 | export const voiceDb = new ModelTypeStore("voice", VoiceComposer, builtins);
136 |
--------------------------------------------------------------------------------
/src/voice-composer/voiceview.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | import { ImportExportMenu } from "../components/modelmenu";
3 | import { Header } from "../components/header";
4 | import Sidebar from "../components/sidebar";
5 | import { QueryLog } from "../components/querylog";
6 | import { PageContainer, TextInput, Button } from "../components/common";
7 | import { SpeechButton } from "../components/speech";
8 | import { useRef, useState } from "preact/hooks";
9 | import { markdownToPreact } from "../markdown";
10 | import {
11 | AudioRecorder,
12 | Whisper,
13 | getResponseText,
14 | Speech,
15 | } from "./whisperrecord";
16 |
17 | export function VoiceComposerView({ model }) {
18 | const [version, setVersion] = useState(0);
19 | model.addOnUpdate(() => {
20 | setVersion(version + 1);
21 | });
22 | return (
23 |
24 | }
30 | model={model}
31 | />
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | const Editor = ({ model }) => {
41 | const inputRef = useRef(null);
42 | function onSpeech(text) {
43 | model.domain.hypothesis = text;
44 | }
45 | function onUtterance(text) {
46 | model.domain.addUtterance(text);
47 | if (inputRef.current) {
48 | inputRef.current.value = "";
49 | }
50 | }
51 | function onSubmit(element) {
52 | const text = element.value;
53 | model.domain.addUtterance(text);
54 | element.value = "";
55 | }
56 | return (
57 |
58 |
59 |
60 |
65 |
70 | model.domain.addUtterance(text)} />
71 | model.domain.addUtterance(text)}
73 | />
74 |
75 |
76 | );
77 | };
78 |
79 | const DisplayText = ({ model }) => {
80 | const markup = markdownToPreact(
81 | model.domain.text +
82 | (model.domain.hypothesis ? " _" + model.domain.hypothesis + "_" : "")
83 | );
84 | return (
85 |
88 | );
89 | };
90 |
91 | const recorder = new AudioRecorder();
92 | const whisper = new Whisper();
93 |
94 | const brailleProgress = ["⠟", "⠯", "⠷", "⠾", "⠽", "⠻"];
95 |
96 | const WhisperButton = ({ onText }) => {
97 | const [version, setVersion] = useState(0);
98 | whisper.addOnUpdate(() => {
99 | setVersion(version + 1);
100 | });
101 | recorder.addOnUpdate(() => {
102 | setVersion(version + 1);
103 | });
104 | async function OnRecord() {
105 | if (recorder.isRecording) {
106 | // A little crude, but we know that whisper will transcribe soon so we'll
107 | // pre-set this flag so it doesn't show a moment where recording has stopped and
108 | // transcribing has yet to begin:
109 | whisper.isTranscribing = true;
110 | await recorder.stop();
111 | const resp = await whisper.transcribe(recorder.audioBlob);
112 | onText(getResponseText(resp));
113 | } else if (recorder.isStarting) {
114 | console.warn("Ignoring start while still starting...");
115 | } else {
116 | recorder.start();
117 | }
118 | }
119 | let label;
120 | if (recorder.isStarting) {
121 | label = "...";
122 | } else if (recorder.isRecording) {
123 | label = "Stop";
124 | } else if (whisper.isTranscribing) {
125 | label = "Transcribing " + brailleProgress[version % brailleProgress.length];
126 | } else {
127 | label = "Whisper record";
128 | }
129 | return (
130 |
131 |
{label}
132 | {recorder.error ? (
133 |
{`Error recording: ${recorder.error}`}
134 | ) : null}
135 |
136 | );
137 | };
138 |
139 | const speech = new Speech();
140 |
141 | const WhisperSpeechButton = ({ onText }) => {
142 | const [version, setVersion] = useState(0);
143 | const [lastLength, setLastLength] = useState(0);
144 | speech.addOnUpdate(() => {
145 | setVersion(version + 1);
146 | let len = lastLength;
147 | console.log("transcripts are", speech.transcripts, lastLength);
148 | while (speech.transcripts.length > len) {
149 | onText(speech.transcripts[len]);
150 | len++;
151 | }
152 | if (len !== lastLength) {
153 | setLastLength(len);
154 | }
155 | });
156 | function onClick() {
157 | console.log("onClick, isListening: " + speech.isListening);
158 | if (speech.isListening) {
159 | speech.stop();
160 | } else {
161 | speech.start();
162 | }
163 | }
164 | return (
165 |
166 | Speech
167 |
168 | );
169 | };
170 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | /* globals require, module */
3 | const colors = require("tailwindcss/colors");
4 |
5 | // Avoid warnings about deprecated colors:
6 | const simpleColors = Object.assign({}, colors);
7 | delete simpleColors.coolGray;
8 | delete simpleColors.trueGray;
9 | delete simpleColors.warmGray;
10 | delete simpleColors.blueGray;
11 | delete simpleColors.lightBlue;
12 |
13 | module.exports = {
14 | content: ["./src/**/*.{html,js}"],
15 | theme: {
16 | extend: {
17 | flex: { 2: "2 1 0%" },
18 | minHeight: {
19 | 1: "1em",
20 | 2: "2em",
21 | },
22 | },
23 | colors: Object.assign({}, simpleColors, {
24 | // From https://www.color-name.com/viva-magenta.color
25 | magenta: "#BB2649",
26 | "magenta-darker": "#380B16",
27 | "magenta-dark": "#5E1325",
28 | "magenta-light": "#D67D92",
29 | "magenta-lighter": "#F1D4DB",
30 | aqua: "#26BB98",
31 | "aqua-darker": "#0F4B3D",
32 | "aqua-dark": "#17705B",
33 | "aqua-light": "#93DDCC",
34 | "aqua-lighter": "#D4F1EA",
35 | "aqua-lightest": "#E9F8F5",
36 | "blue-complement": "#17293A",
37 | "blue-complement-light": "#E8EAEB",
38 | }),
39 | variants: {
40 | extend: {
41 | display: ["group-hover", "group2-hover"],
42 | },
43 | },
44 | },
45 | plugins: [],
46 | };
47 |
--------------------------------------------------------------------------------
/tests/__mocks__/browserMocks.js:
--------------------------------------------------------------------------------
1 | // Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
2 | /**
3 | * An example how to mock localStorage is given below 👇
4 | */
5 |
6 | /*
7 | // Mocks localStorage
8 | const localStorageMock = (function() {
9 | let store = {};
10 |
11 | return {
12 | getItem: (key) => store[key] || null,
13 | setItem: (key, value) => store[key] = value.toString(),
14 | clear: () => store = {}
15 | };
16 |
17 | })();
18 |
19 | Object.defineProperty(window, 'localStorage', {
20 | value: localStorageMock
21 | }); */
22 |
--------------------------------------------------------------------------------
/tests/__mocks__/fileMocks.js:
--------------------------------------------------------------------------------
1 | // This fixed an error related to the CSS and loading gif breaking my Jest test
2 | // See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
3 | module.exports = 'test-file-stub';
--------------------------------------------------------------------------------
/tests/__mocks__/setupTests.js:
--------------------------------------------------------------------------------
1 | import { configure } from 'enzyme';
2 | import Adapter from 'enzyme-adapter-preact-pure';
3 |
4 | configure({
5 | adapter: new Adapter()
6 | });
7 |
--------------------------------------------------------------------------------
/tests/header.test.js:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import Header from '../src/components/header';
3 | // See: https://github.com/preactjs/enzyme-adapter-preact-pure
4 | import { shallow } from 'enzyme';
5 |
6 | describe('Initial Test of the Header', () => {
7 | test('Header renders 3 nav items', () => {
8 | const context = shallow();
9 | expect(context.find('h1').text()).toBe('Preact App');
10 | expect(context.find('Link').length).toBe(3);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------