Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd6b48fcf5 | ||
|
|
6e678cbcde | ||
|
|
fa7a89c64e | ||
|
|
bea656ade8 | ||
|
|
51fd4d5538 |
@@ -94,7 +94,7 @@ Enables SQLite foreign keys (`enableForeignKeys`), `bunextInit()`, `ensureDocker
|
||||
- **Conversation loading:** pages render everything server-side (`grabConversationProps`) **except the history**, which the client loads (`api/admin/bots/conversation-history`, merged with what's already shown, refetched on bfcache restore) and pages older messages on scroll. Rail lists go through `railBots`/`railWorkspaces` (no prompts/memory, previews cut). While a reply is due the page polls `session-messages` (`use-reply-catch-up`) in case a push was missed.
|
||||
- **Sending:** `api/admin/bots/send-message.ts` -> `enqueueBotReply` -> `wakeBotsQueue`. **Never import `src/bots-queue/index.ts` from a page or API route** (pulls the bunext runtime into page bundles). Optimistic bubble in `pending_messages` until the saved message replaces it.
|
||||
- **Composer:** Tiptap editor reading/writing markdown, drafts in `localStorage`, `@` bots / `#` workspaces written as `[bot:ID]`/`[workspace:ID]`. Enter sends (Mod+Enter in lists/code). `@tiptap/react` needs `use-sync-external-store`; the app ships an ESM stand-in in `packages/use-sync-external-store-esm` (file dependency + override; the Dockerfile copies `packages/` before `bun install`).
|
||||
- **Rendering:** markdown via `react-markdown` + `remark-gfm`, raw HTML never rendered, component map in `[bot_id]/(data)/message-markdown-components.tsx`. Inline refs, all linked by `link-computer-refs.ts` and rendered by chips in `(partials)/`: `[bot:ID]`, `[workspace:ID]`, `[computer:ID]`, `[file:ID]` (a saved file), `[folder:<volume>:<owner>:<path>]` (opens `/admin/files`), `[take-over:ID:mode]`, `[proposal:ID]`, `[hook-proposal:ID]`, `[secret:ID]` (masked env, Reveal/Copy), `[secret-request:ID]` (masked input card). Parsers in `src/utils/*-ref.ts`.
|
||||
- **Rendering:** markdown via `react-markdown` + `remark-gfm`, raw HTML never rendered, component map in `[bot_id]/(data)/message-markdown-components.tsx`. Inline refs, all linked by `link-computer-refs.ts` and rendered by chips in `(partials)/`: `[bot:ID]`, `[workspace:ID]`, `[computer:ID]`, `[file:ID]` (a saved file), `[folder:<volume>:<owner>:<path>]` (opens `/admin/files`), `[take-over:ID:mode]`, `[proposal:ID]`, `[hook-proposal:ID]`, `[secret:ID]` (masked env, Reveal/Copy), `[secret-request:ID]` (masked input card), `[graphic:ID]` (a bot graphic, framed inline). Parsers in `src/utils/*-ref.ts`.
|
||||
- **Copy chat** (header menu): the active session (the one on screen) as one markdown document (`api/admin/bots/conversation-export` -> `conversationMarkdown`, refs as names, message headings demoted). The clipboard write starts on the click with a pending `ClipboardItem` (`copyPendingText`), since Safari drops the gesture after a fetch.
|
||||
- **Replies/reactions:** `reply_to_message_id` (not an FK); reactions from the fixed `BotMessageReactions`, toggled by `react-message.ts` and the `react_to_message` tool, pushed as `server:bot-reaction`.
|
||||
- **Attachments:** uploaded on pick (`upload-attachment.ts`, private `media`, text extracted when possible), linked via `bot_message_media`. Replies read files of the last `BotReplyAttachmentMessages` messages. Tasks don't get files yet.
|
||||
@@ -161,7 +161,7 @@ Set in Settings > Environment (super admin), not `.env`. `envs` (private; encryp
|
||||
- **Install name:** app setting `install_name` (Settings > General, super admin). `handlePwaFile` rewrites the manifest's `name`/`short_name` per request (`namePwaManifest`), the root head uses it for iOS tags, the loading page's `<title>` is the `{{name}}` placeholder. Devices pick up a new name only on reinstall.
|
||||
- **Install icon:** app setting `install_icon` (JSON, `parseInstallIcon`): a stone, or an upload (decoded, cropped to `source.png`, never stored as sent, no SVG) with the stone at `PwaCornerMarkInset`/`PwaCornerMarkScale`, inside a round launcher's crop. `saveInstallIcon` draws the set (`renderInstallIcons`, shared with `gen:favicon` via `renderAppIcon`) into `DATA_DIR/install-icon/<version>/`, served publicly and `immutable` by `handleInstallIconFile`; the manifest, head links and push payloads point at it. Ember stone = stock icons, no set.
|
||||
- Notification `badge` is `icons/badge-96.png` (white silhouette; Android masks by alpha, so never the opaque app icon). Brave on Android has no WebAPK, so its notifications always carry Brave's name and icon.
|
||||
- `public/sw.js` is served `no-store`; navigations network-only (authenticated HTML never cached); `/api/`, `/ws`, `/media/`, `/computers/`, `/build-info.json` never intercepted. It never reloads on its own. In development the worker and caches are actively removed (`clear-development-service-worker.ts`, stub `/sw.js`).
|
||||
- `public/sw.js` is served `no-store`; navigations network-only (authenticated HTML never cached); `/api/`, `/ws`, `/media/`, `/computers/`, `/build-info.json` never intercepted. A new worker takes over by itself (`skipWaiting` on install; nothing in it is per-version) and never reloads a page; there is no update prompt. In development the worker and caches are actively removed (`clear-development-service-worker.ts`, stub `/sw.js`).
|
||||
- **Precache (production):** a navigation without the current build cookie gets `loading.html`, which downloads all of `.bunext/public` into `cobble-assets-<build_id>`, verifies, sets the cookie and reloads (falls back to network on stall). React knows nothing of this.
|
||||
- Phone shell: rail is a slide-over below `md`; rail internals use the `@rail:` container variant, not viewport `lg:`. `LiveSocket` is remounted on online/pageshow/visibility since twui stops retrying after three attempts. Nothing is queued offline.
|
||||
- **Web Push:** `push_subscriptions`, `user_presence`; VAPID keys generated into `envs` by `ensureVapidKeys`. `pushBotNotice` is the one trigger, skipped while the user is present (`AppData.PushPresenceWindowMs`, per user not per device). Best effort, no retry. Needs HTTPS.
|
||||
@@ -174,6 +174,10 @@ Set in Settings > Environment (super admin), not `.env`. `envs` (private; encryp
|
||||
- Ingestion: chunks + embeddings into `bot_file_chunks` / `bot_file_vectors` (vec0), worked by `src/file-ingest/`; without an embedding model search falls back to words and says so. sqlite-vec limits: metadata columns are NOT NULL; side-car columns can't be filtered with `MATCH` (filter in code). **Never pass `--vector` to `bunx bun-sqlite schema` in `entrypoint.sh`** (wipes vectors on boot). Changing `AppData.EmbeddingDimensions` needs a manual `--vector` run and a re-ingest.
|
||||
- **Foreign keys are off per connection** unless `enableForeignKeys` ran (only `src/server.ts` does). Scripts never fire `ON DELETE CASCADE`, so **any helper that deletes a parent row must delete its children explicitly** (as `deleteBotFile`, `purgeStorage`, `deleteSkill` do). A leftover child fails the next `bun run db:schema` with a foreign key violation.
|
||||
|
||||
### Graphics (`bot_graphics`)
|
||||
|
||||
A bot shows a concept as a visual with `create_graphic` (plus `update_graphic`/`read_graphic`/`list_graphics`, `src/harness/tools/graphics/`, on every run, no computer needed): one HTML document stored as text, per user like the conversation (`viewerGraphicsSql`: only the user it was made for reads it; only its bot edits it), `version` bumped on each edit (in place, no history), `message_id` filled when the job completes. The run's message always carries its `[graphic:ID]` (`shown` in `withTakeOverRefs`); the chat renders it as `GraphicRefCard` (inline frame sized by the graphic's `postMessage`, open large, own page `/admin/bots/<bot>/graphics/<id>`). **Graphic HTML is hostile** (a prompt injection can write it): it's only ever served at `/graphics/<id>` (`handleGraphicsRequest`, before the precache gate, bypassed by the service worker) with `graphicHeaders`: CSP `sandbox allow-scripts` (never `allow-same-origin`), `default-src 'none'`, scripts only inline or from `/graphics-lib/` (the vendored `GraphicsLibs`: Chart.js, Mermaid, read from `node_modules`). Never render graphic HTML on the app's origin (`dangerouslySetInnerHTML`, `srcdoc` without sandbox) and never add network to that CSP. A frame that navigates itself away is closed (`GraphicFrame`); meta refreshes are stripped (`graphicDocument`), which also sets the theme vars parsed from `main.css`.
|
||||
|
||||
### Knowledgebases
|
||||
|
||||
A fourth extension kind (`ExtensionTables.knowledgebase`): `knowledgebases` given per bot (`bot_knowledgebases`), per workspace (`workspace_knowledgebases`) or every bot (`all_bots`), through the same add/remove/access/default paths as skills (coordinator extension tools leave them out). Documents (`knowledgebase_documents`: an upload in private `media`, paradigm `knowledgebase-document`, or written `content`) are chunked into `knowledgebase_chunks` and embedded into the `knowledgebase_vectors` vec0 table by the file-ingest worker (`file_ingest_queue.document_id`, `ingestKnowledgebaseDocument`), always with the install's embedding model; old passages stay until the new ones are in, `embedding_model` records where vectors came from; whatever can change the install's model (default settings, a provider saved or deleted, the hourly catalog sync) calls `reindexAfterEmbeddingChange`, and Re-index reads a knowledgebase again by hand. Word search runs on `knowledgebase_chunks_fts`, an FTS5 table (`unicode61 remove_diacritics 2`) made by `ensureKnowledgebaseFts` (FTS5 can't be declared in the schema; sync leaves undeclared tables alone), backfilled at boot by `syncKnowledgebaseFts`, ranked by BM25 with a scored `LIKE` fallback for unspaced scripts (`knowledgebaseWordQuery`); every write and delete of `knowledgebase_chunks` updates it. Ingest jobs (`settleFailedIngestJob`) wait without spending an attempt while every embedding model rests, fail at once on an error retrying can't fix, and a document whose model refused keeps its new passages for word search (counted as needing re-index, not failed). Runs get `search_knowledgebase`/`read_knowledgebase_document` (`openBotKnowledgebases`, limited to the run's ones) and replies start with passages found for the unanswered messages (`grabKnowledgebaseContext`); passages are fenced (`knowledgebasePassageBlock`) and `KnowledgebasesNote` says they're never instructions. UI: Settings > Knowledgebases (super admin edits and fills; admins set access), a Knowledgebases group on bot/workspace settings, and the composer's own button beside Tools (`ComposerToolsModal` with `composerKnowledgebasesCopy`). Deleting one deletes its documents' vectors, chunks, media and links explicitly.
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@tiptap/react": "3.32.0",
|
||||
"@tiptap/starter-kit": "3.32.0",
|
||||
"ace-builds": "^1.44.0",
|
||||
"chart.js": "^4.6.1",
|
||||
"dayjs": "^1.11.23",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-react": "^1.7.0",
|
||||
@@ -25,6 +26,7 @@
|
||||
"lucide-react": "^1.59.0",
|
||||
"mammoth": "^1.12.1",
|
||||
"mdx": "^0.3.1",
|
||||
"mermaid": "^12.2.0",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"nodemailer": "^9.0.5",
|
||||
"openai": "^7.4.0",
|
||||
@@ -71,6 +73,8 @@
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/[email protected]", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||
|
||||
"@antfu/install-pkg": ["@antfu/[email protected]", "", { "dependencies": { "package-manager-detector": "^1.8.0", "tinyexec": "^1.3.1" } }, "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/[email protected]", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
@@ -81,6 +85,18 @@
|
||||
|
||||
"@bedrock-layout/use-stateful-ref": ["@bedrock-layout/[email protected]", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18" } }, "sha512-pzVUYMTa32E9dGa2xadOFej96gYJEHg+Qh4Tg0fMj6zYT4y5LZpE+XkCiPcFafR3kTFhqSGfjRTIJbnSyeEAxQ=="],
|
||||
|
||||
"@braintree/sanitize-url": ["@braintree/[email protected]", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="],
|
||||
|
||||
"@chevrotain/cst-dts-gen": ["@chevrotain/[email protected]", "", { "dependencies": { "@chevrotain/gast": "13.2.0", "@chevrotain/types": "13.2.0" } }, "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg=="],
|
||||
|
||||
"@chevrotain/gast": ["@chevrotain/[email protected]", "", { "dependencies": { "@chevrotain/types": "13.2.0" } }, "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg=="],
|
||||
|
||||
"@chevrotain/regexp-to-ast": ["@chevrotain/[email protected]", "", {}, "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw=="],
|
||||
|
||||
"@chevrotain/types": ["@chevrotain/[email protected]", "", {}, "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg=="],
|
||||
|
||||
"@chevrotain/utils": ["@chevrotain/[email protected]", "", {}, "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||
|
||||
"@emotion/is-prop-valid": ["@emotion/[email protected]", "", { "dependencies": { "@emotion/memoize": "^0.9.0" } }, "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw=="],
|
||||
@@ -147,6 +163,10 @@
|
||||
|
||||
"@hono/node-server": ["@hono/[email protected]", "", { "peerDependencies": { "hono": "^4 || ^5.0.0-0" } }, "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q=="],
|
||||
|
||||
"@iconify/types": ["@iconify/[email protected]", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||
|
||||
"@iconify/utils": ["@iconify/[email protected]", "", { "dependencies": { "@antfu/install-pkg": "^2.0.1", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w=="],
|
||||
|
||||
"@img/colour": ["@img/[email protected]", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="],
|
||||
@@ -243,10 +263,14 @@
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/[email protected]", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@kurkle/color": ["@kurkle/[email protected]", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/[email protected]", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/[email protected]", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
|
||||
|
||||
"@mermaid-js/parser": ["@mermaid-js/[email protected]", "", { "dependencies": { "@chevrotain/types": "~13.2.0" } }, "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA=="],
|
||||
|
||||
"@mixmark-io/domino": ["@mixmark-io/[email protected]", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw=="],
|
||||
@@ -393,12 +417,76 @@
|
||||
|
||||
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/d3": ["@types/[email protected]", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
|
||||
"@types/d3-array": ["@types/[email protected]", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
|
||||
|
||||
"@types/d3-axis": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="],
|
||||
|
||||
"@types/d3-brush": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="],
|
||||
|
||||
"@types/d3-chord": ["@types/[email protected]", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="],
|
||||
|
||||
"@types/d3-color": ["@types/[email protected]", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="],
|
||||
|
||||
"@types/d3-contour": ["@types/[email protected]", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="],
|
||||
|
||||
"@types/d3-delaunay": ["@types/[email protected]", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="],
|
||||
|
||||
"@types/d3-dispatch": ["@types/[email protected]", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="],
|
||||
|
||||
"@types/d3-drag": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="],
|
||||
|
||||
"@types/d3-dsv": ["@types/[email protected]", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="],
|
||||
|
||||
"@types/d3-ease": ["@types/[email protected]", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="],
|
||||
|
||||
"@types/d3-fetch": ["@types/[email protected]", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="],
|
||||
|
||||
"@types/d3-force": ["@types/[email protected]", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="],
|
||||
|
||||
"@types/d3-format": ["@types/[email protected]", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="],
|
||||
|
||||
"@types/d3-geo": ["@types/[email protected]", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="],
|
||||
|
||||
"@types/d3-hierarchy": ["@types/[email protected]", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="],
|
||||
|
||||
"@types/d3-interpolate": ["@types/[email protected]", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="],
|
||||
|
||||
"@types/d3-path": ["@types/[email protected]", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="],
|
||||
|
||||
"@types/d3-polygon": ["@types/[email protected]", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="],
|
||||
|
||||
"@types/d3-quadtree": ["@types/[email protected]", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="],
|
||||
|
||||
"@types/d3-random": ["@types/[email protected]", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="],
|
||||
|
||||
"@types/d3-scale": ["@types/[email protected]", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="],
|
||||
|
||||
"@types/d3-scale-chromatic": ["@types/[email protected]", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="],
|
||||
|
||||
"@types/d3-selection": ["@types/[email protected]", "", {}, "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw=="],
|
||||
|
||||
"@types/d3-shape": ["@types/[email protected]", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="],
|
||||
|
||||
"@types/d3-time": ["@types/[email protected]", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="],
|
||||
|
||||
"@types/d3-time-format": ["@types/[email protected]", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="],
|
||||
|
||||
"@types/d3-timer": ["@types/[email protected]", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||
|
||||
"@types/d3-transition": ["@types/[email protected]", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="],
|
||||
|
||||
"@types/d3-zoom": ["@types/[email protected]", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA=="],
|
||||
|
||||
"@types/debug": ["@types/[email protected]", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
|
||||
|
||||
"@types/estree": ["@types/[email protected]", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/[email protected]", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/geojson": ["@types/[email protected]", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/hast": ["@types/[email protected]", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="],
|
||||
|
||||
"@types/lodash": ["@types/[email protected]", "", {}, "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ=="],
|
||||
@@ -421,6 +509,8 @@
|
||||
|
||||
"@types/sanitize-html": ["@types/[email protected]", "", { "dependencies": { "htmlparser2": "^12.0" } }, "sha512-kuAbgDCFv418flrp3z4o/4ufLA/5ovmXRH0L419iZCPGO7w00znjeDI5Wy/KZWNIBpDQc7hucCK+J5almB6FyA=="],
|
||||
|
||||
"@types/trusted-types": ["@types/[email protected]", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/turndown": ["@types/[email protected]", "", {}, "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg=="],
|
||||
|
||||
"@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
@@ -471,6 +561,8 @@
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
|
||||
|
||||
"@upsetjs/venn.js": ["@upsetjs/[email protected]", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||
|
||||
"@xmldom/xmldom": ["@xmldom/[email protected]", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="],
|
||||
|
||||
"accepts": ["[email protected]", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
@@ -547,6 +639,10 @@
|
||||
|
||||
"chardet": ["[email protected]", "", {}, "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA=="],
|
||||
|
||||
"chart.js": ["[email protected]", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-hJyniucsFmq//EvXYvJ6c6mahPoI3kaxdjVH8awTaJ/fDNWeSqqqArixMJWkWNN6fcYvPU3HneSANFNYtRi61g=="],
|
||||
|
||||
"chevrotain": ["[email protected]", "", { "dependencies": { "@chevrotain/cst-dts-gen": "13.2.0", "@chevrotain/gast": "13.2.0", "@chevrotain/regexp-to-ast": "13.2.0", "@chevrotain/types": "13.2.0", "@chevrotain/utils": "13.2.0" } }, "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA=="],
|
||||
|
||||
"chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"classnames": ["[email protected]", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||
@@ -577,6 +673,8 @@
|
||||
|
||||
"cors": ["[email protected]", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cose-base": ["[email protected]", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||
|
||||
"crc-32": ["[email protected]", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
|
||||
|
||||
"cross-spawn": ["[email protected]", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
@@ -589,12 +687,86 @@
|
||||
|
||||
"currently-unhandled": ["[email protected]", "", { "dependencies": { "array-find-index": "^1.0.1" } }, "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng=="],
|
||||
|
||||
"cytoscape": ["[email protected]", "", {}, "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg=="],
|
||||
|
||||
"cytoscape-cose-bilkent": ["[email protected]", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="],
|
||||
|
||||
"cytoscape-fcose": ["[email protected]", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="],
|
||||
|
||||
"d3": ["[email protected]", "", { "dependencies": { "d3-array": "3", "d3-axis": "3", "d3-brush": "3", "d3-chord": "3", "d3-color": "3", "d3-contour": "4", "d3-delaunay": "6", "d3-dispatch": "3", "d3-drag": "3", "d3-dsv": "3", "d3-ease": "3", "d3-fetch": "3", "d3-force": "3", "d3-format": "3", "d3-geo": "3", "d3-hierarchy": "3", "d3-interpolate": "3", "d3-path": "3", "d3-polygon": "3", "d3-quadtree": "3", "d3-random": "3", "d3-scale": "4", "d3-scale-chromatic": "3", "d3-selection": "3", "d3-shape": "3", "d3-time": "3", "d3-time-format": "4", "d3-timer": "3", "d3-transition": "3", "d3-zoom": "3" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="],
|
||||
|
||||
"d3-array": ["[email protected]", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="],
|
||||
|
||||
"d3-axis": ["[email protected]", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="],
|
||||
|
||||
"d3-brush": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="],
|
||||
|
||||
"d3-chord": ["[email protected]", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="],
|
||||
|
||||
"d3-color": ["[email protected]", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="],
|
||||
|
||||
"d3-contour": ["[email protected]", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="],
|
||||
|
||||
"d3-delaunay": ["[email protected]", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="],
|
||||
|
||||
"d3-dispatch": ["[email protected]", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="],
|
||||
|
||||
"d3-drag": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="],
|
||||
|
||||
"d3-dsv": ["[email protected]", "", { "dependencies": { "commander": "7", "iconv-lite": "0.6", "rw": "1" }, "bin": { "csv2json": "bin/dsv2json.js", "csv2tsv": "bin/dsv2dsv.js", "dsv2dsv": "bin/dsv2dsv.js", "dsv2json": "bin/dsv2json.js", "json2csv": "bin/json2dsv.js", "json2dsv": "bin/json2dsv.js", "json2tsv": "bin/json2dsv.js", "tsv2csv": "bin/dsv2dsv.js", "tsv2json": "bin/dsv2json.js" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="],
|
||||
|
||||
"d3-ease": ["[email protected]", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="],
|
||||
|
||||
"d3-fetch": ["[email protected]", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="],
|
||||
|
||||
"d3-force": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="],
|
||||
|
||||
"d3-format": ["[email protected]", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="],
|
||||
|
||||
"d3-geo": ["[email protected]", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="],
|
||||
|
||||
"d3-hierarchy": ["[email protected]", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||
|
||||
"d3-interpolate": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="],
|
||||
|
||||
"d3-path": ["[email protected]", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="],
|
||||
|
||||
"d3-polygon": ["[email protected]", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="],
|
||||
|
||||
"d3-quadtree": ["[email protected]", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="],
|
||||
|
||||
"d3-random": ["[email protected]", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="],
|
||||
|
||||
"d3-sankey": ["[email protected]", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="],
|
||||
|
||||
"d3-scale": ["[email protected]", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="],
|
||||
|
||||
"d3-scale-chromatic": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="],
|
||||
|
||||
"d3-selection": ["[email protected]", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="],
|
||||
|
||||
"d3-shape": ["[email protected]", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="],
|
||||
|
||||
"d3-time": ["[email protected]", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="],
|
||||
|
||||
"d3-time-format": ["[email protected]", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="],
|
||||
|
||||
"d3-timer": ["[email protected]", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
|
||||
|
||||
"d3-transition": ["[email protected]", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="],
|
||||
|
||||
"d3-zoom": ["[email protected]", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="],
|
||||
|
||||
"dagre-d3-es": ["[email protected]", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="],
|
||||
|
||||
"dayjs": ["[email protected]", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="],
|
||||
|
||||
"debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"decode-named-character-reference": ["[email protected]", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||
|
||||
"delaunator": ["[email protected]", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="],
|
||||
|
||||
"depd": ["[email protected]", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["[email protected]", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
@@ -611,6 +783,8 @@
|
||||
|
||||
"domhandler": ["[email protected]", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="],
|
||||
|
||||
"dompurify": ["[email protected]", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w=="],
|
||||
|
||||
"domutils": ["[email protected]", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="],
|
||||
|
||||
"duck": ["[email protected]", "", { "dependencies": { "underscore": "^1.13.1" } }, "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg=="],
|
||||
@@ -621,6 +795,8 @@
|
||||
|
||||
"ee-first": ["[email protected]", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"elkjs": ["[email protected]", "", {}, "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="],
|
||||
|
||||
"encodeurl": ["[email protected]", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"enhanced-resolve": ["[email protected]", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
@@ -635,6 +811,8 @@
|
||||
|
||||
"es-object-atoms": ["[email protected]", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="],
|
||||
|
||||
"es-toolkit": ["[email protected]", "", {}, "sha512-3+AYXp9fmJ/UhHBMYj2/U+RaUouMD90SzoItyDOB2Za7W6dB7fQApFGnZPl2MF6K8/Tv1koh1JyirSx/kju6Pw=="],
|
||||
|
||||
"esast-util-from-estree": ["[email protected]", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
|
||||
|
||||
"esast-util-from-js": ["[email protected]", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
|
||||
@@ -717,6 +895,8 @@
|
||||
|
||||
"gray-matter": ["[email protected]", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
|
||||
|
||||
"hachure-fill": ["[email protected]", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="],
|
||||
|
||||
"has-symbols": ["[email protected]", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["[email protected]", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
@@ -761,6 +941,8 @@
|
||||
|
||||
"immediate": ["[email protected]", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
|
||||
|
||||
"import-meta-resolve": ["[email protected]", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="],
|
||||
|
||||
"indent-string": ["[email protected]", "", { "dependencies": { "repeating": "^2.0.0" } }, "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg=="],
|
||||
|
||||
"inherits": ["[email protected]", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
@@ -769,6 +951,8 @@
|
||||
|
||||
"inquirer": ["[email protected]", "", { "dependencies": { "@inquirer/ansi": "^2.0.5", "@inquirer/core": "^11.1.10", "@inquirer/prompts": "^8.4.3", "@inquirer/type": "^4.0.5", "mute-stream": "^3.0.0", "run-async": "^4.0.6", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-EPd3IqieHSavSOXh+LZhrIkdQcOELWeRblLT6kslQr+cF9XTh/HxZdSt1YkHH1iq4dvqBnV42uwg2YlorgOy6g=="],
|
||||
|
||||
"internmap": ["[email protected]", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"ip-address": ["[email protected]", "", {}, "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg=="],
|
||||
|
||||
"ipaddr.js": ["[email protected]", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
@@ -823,10 +1007,16 @@
|
||||
|
||||
"jws": ["[email protected]", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
|
||||
|
||||
"katex": ["[email protected]", "", { "dependencies": { "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, "sha512-yvyz/2VMKqoho/35Hat5w37wIEdWJ5ElVMidTAlWrxr5uW4aBGsEpveTmKh18/zw08v/7lGswCPon4OhqmqrTA=="],
|
||||
|
||||
"khroma": ["[email protected]", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="],
|
||||
|
||||
"kind-of": ["[email protected]", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"launder": ["[email protected]", "", { "dependencies": { "dayjs": "^1.11.23" } }, "sha512-DLg3HPnHUfBi5/MxMLmJD12dmlMpFEC2HgMW6vkZ/9JR0RU00kXoRGvDxAvFmdO0o610OA77i4FgNTLucmhDVg=="],
|
||||
|
||||
"layout-base": ["[email protected]", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="],
|
||||
|
||||
"lie": ["[email protected]", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
|
||||
|
||||
"lightningcss": ["[email protected]", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
@@ -861,6 +1051,8 @@
|
||||
|
||||
"lodash": ["[email protected]", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||
|
||||
"lodash-es": ["[email protected]", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="],
|
||||
|
||||
"lodash.camelcase": ["[email protected]", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
|
||||
|
||||
"log-symbols": ["[email protected]", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="],
|
||||
@@ -931,6 +1123,8 @@
|
||||
|
||||
"merge-descriptors": ["[email protected]", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mermaid": ["[email protected]", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^2.0.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "chevrotain": "~13.2.0", "cytoscape": "^3.34.0", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.21", "dompurify": "^3.4.12", "elkjs": "^0.9.3", "es-toolkit": "^1.45.1", "katex": "^0.18.0", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-w7nr/SFrLO7BaVTs3dQTvYSV6nYSBj0Yy9DjBhsIJtAcHqRzXSq4Pg32ujHZ/AdWsAMNEvvhbm5dt9E2tOiHzw=="],
|
||||
|
||||
"micromark": ["[email protected]", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["[email protected]", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
@@ -1045,6 +1239,8 @@
|
||||
|
||||
"orderedmap": ["[email protected]", "", {}, "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="],
|
||||
|
||||
"package-manager-detector": ["[email protected]", "", {}, "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA=="],
|
||||
|
||||
"pako": ["[email protected]", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
||||
|
||||
"parse-entities": ["[email protected]", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
|
||||
@@ -1059,6 +1255,8 @@
|
||||
|
||||
"parseurl": ["[email protected]", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-data-parser": ["[email protected]", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||
|
||||
"path-exists": ["[email protected]", "", { "dependencies": { "pinkie-promise": "^2.0.0" } }, "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ=="],
|
||||
|
||||
"path-is-absolute": ["[email protected]", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
@@ -1083,6 +1281,10 @@
|
||||
|
||||
"pkce-challenge": ["[email protected]", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"points-on-curve": ["[email protected]", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="],
|
||||
|
||||
"points-on-path": ["[email protected]", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="],
|
||||
|
||||
"postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.20", "picocolors": "^1.1.1", "source-map-js": "^1.2.2" } }, "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA=="],
|
||||
|
||||
"postcss-value-parser": ["[email protected]", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
|
||||
@@ -1187,12 +1389,18 @@
|
||||
|
||||
"restore-cursor": ["[email protected]", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
|
||||
|
||||
"robust-predicates": ["[email protected]", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||
|
||||
"rope-sequence": ["[email protected]", "", {}, "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ=="],
|
||||
|
||||
"roughjs": ["[email protected]", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||
|
||||
"router": ["[email protected]", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"run-async": ["[email protected]", "", {}, "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ=="],
|
||||
|
||||
"rw": ["[email protected]", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="],
|
||||
|
||||
"rxjs": ["[email protected]", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
|
||||
|
||||
"safe-buffer": ["[email protected]", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
|
||||
@@ -1295,6 +1503,8 @@
|
||||
|
||||
"tapable": ["[email protected]", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||
|
||||
"tinyexec": ["[email protected]", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="],
|
||||
|
||||
"toidentifier": ["[email protected]", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"trim-lines": ["[email protected]", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
@@ -1303,6 +1513,8 @@
|
||||
|
||||
"trough": ["[email protected]", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
|
||||
"ts-dedent": ["[email protected]", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="],
|
||||
|
||||
"tslib": ["[email protected]", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
||||
|
||||
"turndown": ["[email protected]", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="],
|
||||
@@ -1417,6 +1629,16 @@
|
||||
|
||||
"body-parser/content-type": ["[email protected]", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"cytoscape-fcose/cose-base": ["[email protected]", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||
|
||||
"d3-dsv/commander": ["[email protected]", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="],
|
||||
|
||||
"d3-dsv/iconv-lite": ["[email protected]", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"d3-sankey/d3-array": ["[email protected]", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="],
|
||||
|
||||
"d3-sankey/d3-shape": ["[email protected]", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="],
|
||||
|
||||
"decode-named-character-reference/character-entities": ["[email protected]", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"dom-serializer/domhandler": ["[email protected]", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
|
||||
@@ -1431,12 +1653,16 @@
|
||||
|
||||
"htmlparser2/domhandler": ["[email protected]", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="],
|
||||
|
||||
"katex/commander": ["[email protected]", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="],
|
||||
|
||||
"lightningcss-wasm/napi-wasm": ["[email protected]", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="],
|
||||
|
||||
"loud-rejection/signal-exit": ["[email protected]", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["[email protected]", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"mermaid/marked": ["[email protected]", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="],
|
||||
|
||||
"negotiator/content-type": ["[email protected]", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
|
||||
|
||||
"normalize-package-data/semver": ["[email protected]", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="],
|
||||
@@ -1459,6 +1685,12 @@
|
||||
|
||||
"@tailwindcss/postcss/postcss/source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"cytoscape-fcose/cose-base/layout-base": ["[email protected]", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="],
|
||||
|
||||
"d3-sankey/d3-array/internmap": ["[email protected]", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="],
|
||||
|
||||
"d3-sankey/d3-shape/d3-path": ["[email protected]", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="],
|
||||
|
||||
"html-to-react/htmlparser2/domelementtype": ["[email protected]", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="],
|
||||
|
||||
"html-to-react/htmlparser2/domutils": ["[email protected]", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="],
|
||||
|
||||
@@ -1522,6 +1522,67 @@ const schema: BUN_SQLITE_DatabaseSchemaType = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tableName: "bot_graphics",
|
||||
tableDescription: `A visual a bot made for a user (a chart, diagram, timeline or report) as one self-contained HTML document. Served by the app at \`/graphics/<id>\` in a sandbox with no network and an origin of its own, so it never runs with the viewer's session. Per user like the conversation it was made in; an edit replaces the HTML and bumps \`version\``,
|
||||
fields: [
|
||||
{
|
||||
fieldName: "title",
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "html",
|
||||
fieldDescription: `The document as the bot wrote it, never rendered on the app's origin`,
|
||||
dataType: "TEXT",
|
||||
},
|
||||
{
|
||||
fieldName: "version",
|
||||
fieldDescription: `1 when made, one more on every edit`,
|
||||
dataType: "INTEGER",
|
||||
defaultValue: 1,
|
||||
},
|
||||
{
|
||||
fieldName: "user_id",
|
||||
fieldDescription: `The user it was made for, who alone sees it (and the super admin). Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "bot_id",
|
||||
fieldDescription: `The bot that made it, the only one that edits it. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "session_id",
|
||||
fieldDescription: `The conversation it was made in. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "workspace_id",
|
||||
fieldDescription: `The session's workspace; null for the bot's own chat. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "job_id",
|
||||
fieldDescription: `The run that made it. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
{
|
||||
fieldName: "message_id",
|
||||
fieldDescription: `The message the run posted, filled in when the job completes. Not a foreign key`,
|
||||
dataType: "INTEGER",
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
indexName: "idx_bot_graphics_owner",
|
||||
indexTableFields: ["user_id", "bot_id"],
|
||||
},
|
||||
{
|
||||
indexName: "idx_bot_graphics_job_id",
|
||||
indexTableFields: ["job_id"],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
tableName: "workspaces",
|
||||
fields: [
|
||||
|
||||
+50
-1
@@ -21,6 +21,7 @@ export const BunSQLiteTables = [
|
||||
"bot_hook_events",
|
||||
"hook_proposals",
|
||||
"secret_requests",
|
||||
"bot_graphics",
|
||||
"workspaces",
|
||||
"workspace_bots",
|
||||
"bot_types",
|
||||
@@ -969,6 +970,54 @@ export type BUN_SQLITE_COBBLE_SECRET_REQUESTS = {
|
||||
workspace_id?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_BOT_GRAPHICS = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
*/
|
||||
id?: number | "";
|
||||
/**
|
||||
* The time when the record was created. (Unix Timestamp)
|
||||
*/
|
||||
created_at?: number | "";
|
||||
/**
|
||||
* The time when the record was updated. (Unix Timestamp)
|
||||
*/
|
||||
updated_at?: number | "";
|
||||
title?: string;
|
||||
/**
|
||||
* The document as the bot wrote it, never rendered on the app's origin
|
||||
*/
|
||||
html?: string;
|
||||
/**
|
||||
* 1 when made, one more on every edit
|
||||
*/
|
||||
version?: number | "";
|
||||
/**
|
||||
* The user it was made for, who alone sees it (and the super admin). Not a foreign key
|
||||
*/
|
||||
user_id?: number | "";
|
||||
/**
|
||||
* The bot that made it, the only one that edits it. Not a foreign key
|
||||
*/
|
||||
bot_id?: number | "";
|
||||
/**
|
||||
* The conversation it was made in. Not a foreign key
|
||||
*/
|
||||
session_id?: number | "";
|
||||
/**
|
||||
* The session's workspace; null for the bot's own chat. Not a foreign key
|
||||
*/
|
||||
workspace_id?: number | "";
|
||||
/**
|
||||
* The run that made it. Not a foreign key
|
||||
*/
|
||||
job_id?: number | "";
|
||||
/**
|
||||
* The message the run posted, filled in when the job completes. Not a foreign key
|
||||
*/
|
||||
message_id?: number | "";
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_WORKSPACES = {
|
||||
/**
|
||||
* The unique identifier of the record.
|
||||
@@ -2407,4 +2456,4 @@ export type BUN_SQLITE_COBBLE_APP_SETTINGS = {
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_MESSAGE_REACTIONS & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_TASKS & BUN_SQLITE_COBBLE_TASK_MEDIA & BUN_SQLITE_COBBLE_TASK_NOTES & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_BOT_HOOKS & BUN_SQLITE_COBBLE_BOT_HOOK_EVENTS & BUN_SQLITE_COBBLE_HOOK_PROPOSALS & BUN_SQLITE_COBBLE_SECRET_REQUESTS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_BOT_TYPES & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_SKILLS & BUN_SQLITE_COBBLE_EXTENSION_PROPOSALS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_SKILLS & BUN_SQLITE_COBBLE_WORKSPACE_SKILLS & BUN_SQLITE_COBBLE_KNOWLEDGEBASES & BUN_SQLITE_COBBLE_KNOWLEDGEBASE_DOCUMENTS & BUN_SQLITE_COBBLE_KNOWLEDGEBASE_CHUNKS & BUN_SQLITE_COBBLE_KNOWLEDGEBASE_VECTORS & BUN_SQLITE_COBBLE_BOT_KNOWLEDGEBASES & BUN_SQLITE_COBBLE_WORKSPACE_KNOWLEDGEBASES & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_USER_PRESENCE & BUN_SQLITE_COBBLE_PUSH_SUBSCRIPTIONS & BUN_SQLITE_COBBLE_ENVS & BUN_SQLITE_COBBLE_BOT_FILES & BUN_SQLITE_COBBLE_BOT_FILE_CHUNKS & BUN_SQLITE_COBBLE_TOKEN_USAGES & BUN_SQLITE_COBBLE_BOT_FILE_VECTORS & BUN_SQLITE_COBBLE_FILE_INGEST_QUEUE & BUN_SQLITE_COBBLE_APP_SETTINGS
|
||||
export type BUN_SQLITE_COBBLE_ALL_TYPEDEFS = BUN_SQLITE_COBBLE_USERS & BUN_SQLITE_COBBLE_USER_TYPES & BUN_SQLITE_COBBLE_SSO_LOGIN_CODES & BUN_SQLITE_COBBLE_USER_PASSWORDS & BUN_SQLITE_COBBLE_MEDIA & BUN_SQLITE_COBBLE_MEDIA_PARADIGMS & BUN_SQLITE_COBBLE_BOTS & BUN_SQLITE_COBBLE_BOT_SESSIONS & BUN_SQLITE_COBBLE_BOT_MESSAGES & BUN_SQLITE_COBBLE_BOT_MESSAGE_REACTIONS & BUN_SQLITE_COBBLE_BOT_TASKS & BUN_SQLITE_COBBLE_BOT_TASK_STEPS & BUN_SQLITE_COBBLE_TASKS & BUN_SQLITE_COBBLE_TASK_MEDIA & BUN_SQLITE_COBBLE_TASK_NOTES & BUN_SQLITE_COBBLE_BOTS_QUEUE & BUN_SQLITE_COBBLE_BOTS_QUEUE_UPDATES & BUN_SQLITE_COBBLE_CRON_JOBS & BUN_SQLITE_COBBLE_BOT_HOOKS & BUN_SQLITE_COBBLE_BOT_HOOK_EVENTS & BUN_SQLITE_COBBLE_HOOK_PROPOSALS & BUN_SQLITE_COBBLE_SECRET_REQUESTS & BUN_SQLITE_COBBLE_BOT_GRAPHICS & BUN_SQLITE_COBBLE_WORKSPACES & BUN_SQLITE_COBBLE_WORKSPACE_BOTS & BUN_SQLITE_COBBLE_BOT_TYPES & BUN_SQLITE_COBBLE_WORKSPACE_USERS & BUN_SQLITE_COBBLE_BOT_USERS & BUN_SQLITE_COBBLE_BOT_USER_MEMORIES & BUN_SQLITE_COBBLE_BOT_USER_FACES & BUN_SQLITE_COBBLE_BOT_MESSAGE_MEDIA & BUN_SQLITE_COBBLE_CONTAINERS & BUN_SQLITE_COBBLE_COMPUTERS & BUN_SQLITE_COBBLE_BOT_COMPUTERS & BUN_SQLITE_COBBLE_WORKSPACE_COMPUTERS & BUN_SQLITE_COBBLE_HARNESS_PROVIDERS & BUN_SQLITE_COBBLE_HARNESS_MODELS & BUN_SQLITE_COBBLE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_BOT_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_WORKSPACE_HARNESS_SETTINGS & BUN_SQLITE_COBBLE_MCP_SERVERS & BUN_SQLITE_COBBLE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_SKILLS & BUN_SQLITE_COBBLE_EXTENSION_PROPOSALS & BUN_SQLITE_COBBLE_BOT_MCP_SERVERS & BUN_SQLITE_COBBLE_WORKSPACE_MCP_SERVERS & BUN_SQLITE_COBBLE_BOT_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_WORKSPACE_CUSTOM_TOOLS & BUN_SQLITE_COBBLE_BOT_SKILLS & BUN_SQLITE_COBBLE_WORKSPACE_SKILLS & BUN_SQLITE_COBBLE_KNOWLEDGEBASES & BUN_SQLITE_COBBLE_KNOWLEDGEBASE_DOCUMENTS & BUN_SQLITE_COBBLE_KNOWLEDGEBASE_CHUNKS & BUN_SQLITE_COBBLE_KNOWLEDGEBASE_VECTORS & BUN_SQLITE_COBBLE_BOT_KNOWLEDGEBASES & BUN_SQLITE_COBBLE_WORKSPACE_KNOWLEDGEBASES & BUN_SQLITE_COBBLE_LOGS & BUN_SQLITE_COBBLE_USER_PRESENCE & BUN_SQLITE_COBBLE_PUSH_SUBSCRIPTIONS & BUN_SQLITE_COBBLE_ENVS & BUN_SQLITE_COBBLE_BOT_FILES & BUN_SQLITE_COBBLE_BOT_FILE_CHUNKS & BUN_SQLITE_COBBLE_TOKEN_USAGES & BUN_SQLITE_COBBLE_BOT_FILE_VECTORS & BUN_SQLITE_COBBLE_FILE_INGEST_QUEUE & BUN_SQLITE_COBBLE_APP_SETTINGS
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cobble",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.1",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -54,6 +54,7 @@
|
||||
"@tiptap/react": "3.32.0",
|
||||
"@tiptap/starter-kit": "3.32.0",
|
||||
"ace-builds": "^1.44.0",
|
||||
"chart.js": "^4.6.1",
|
||||
"dayjs": "^1.11.23",
|
||||
"gray-matter": "^4.0.3",
|
||||
"html-to-react": "^1.7.0",
|
||||
@@ -61,6 +62,7 @@
|
||||
"lucide-react": "^1.59.0",
|
||||
"mammoth": "^1.12.1",
|
||||
"mdx": "^0.3.1",
|
||||
"mermaid": "^12.2.0",
|
||||
"next-mdx-remote": "^6.0.0",
|
||||
"nodemailer": "^9.0.5",
|
||||
"openai": "^7.4.0",
|
||||
|
||||
+11
-5
@@ -54,9 +54,9 @@ const ASSET_PATHS = ["/favicon.ico", "/favicon.png", "/apple-touch-icon.png"];
|
||||
|
||||
/**
|
||||
* Never intercepted: live data, sockets, media bytes, proxied computers,
|
||||
* auth side effects, and the worker itself.
|
||||
* bot graphics, auth side effects, and the worker itself.
|
||||
*/
|
||||
const BYPASS_PREFIXES = ["/api/", "/ws", "/media", "/computers/"];
|
||||
const BYPASS_PREFIXES = ["/api/", "/ws", "/media", "/computers/", "/graphics/", "/graphics-lib/"];
|
||||
const BYPASS_PATHS = ["/sw.js", "/manifest.webmanifest", "/auth/logout", BUILD_INFO_URL];
|
||||
|
||||
/**
|
||||
@@ -85,7 +85,14 @@ function isBypassRequest(url) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nothing the worker holds belongs to one version (navigations go to the
|
||||
* network, the code cache belongs to the build), so a new worker takes
|
||||
* over at once instead of waiting for every tab to close. It never
|
||||
* reloads a page: the open one keeps running, now served by it.
|
||||
*/
|
||||
self.addEventListener("install", (event) => {
|
||||
self.skipWaiting();
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) =>
|
||||
Promise.allSettled(SHELL.map((url) => cache.add(url))),
|
||||
@@ -190,9 +197,8 @@ async function cacheFirst(request) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The page asks for a new version from its update notice: nothing skips
|
||||
* the wait until the user says so. The loading page asks too, since it
|
||||
* isn't the app and nothing of the app's is running.
|
||||
* The loading page asks a waiting worker to take over. Workers from
|
||||
* before `install` skipped the wait on their own only answer this.
|
||||
*/
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data?.type === "SKIP_WAITING") {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AppData } from "@/src/data/app-data";
|
||||
import {
|
||||
ActionCheckNote,
|
||||
SecretsNote,
|
||||
GraphicsNote,
|
||||
botFilesNote,
|
||||
chatReplyNote,
|
||||
ComputerFilesNote,
|
||||
@@ -25,6 +26,7 @@ import grabBotComputers from "@/src/functions/backend/db/computers/grab-bot-comp
|
||||
import runTextLoop from "@/src/harness/text/run-text-loop";
|
||||
import grabBotChatTools from "@/src/harness/tools/bot/grab-bot-chat-tools";
|
||||
import grabBotFileTools from "@/src/harness/tools/storage/grab-bot-file-tools";
|
||||
import grabBotGraphicTools from "@/src/harness/tools/graphics/grab-bot-graphic-tools";
|
||||
import grabRunScopes from "@/src/functions/backend/db/bot-files/grab-run-scopes";
|
||||
import openBotExtensions from "@/src/harness/tools/extensions/open-bot-extensions";
|
||||
import openBotSkills from "@/src/harness/tools/skills/open-bot-skills";
|
||||
@@ -193,8 +195,8 @@ export default async function runBotReply({
|
||||
*/
|
||||
const proposals: string[] = [];
|
||||
/**
|
||||
* The secrets the bot showed (`show_secret`), whose chips the reply
|
||||
* always carries.
|
||||
* The secrets the bot showed (`show_secret`) and the graphics it made
|
||||
* or changed, whose chips the reply always carries.
|
||||
*/
|
||||
const shown: string[] = [];
|
||||
/**
|
||||
@@ -308,6 +310,7 @@ export default async function runBotReply({
|
||||
TaskReminderNote,
|
||||
ActionCheckNote,
|
||||
SecretsNote,
|
||||
GraphicsNote,
|
||||
workspace_id ? WorkspaceLearningNote : "",
|
||||
composeComputersNote({
|
||||
computers,
|
||||
@@ -350,6 +353,14 @@ export default async function runBotReply({
|
||||
? [searchPastSessionsTool(past_scope), readPastSessionTool(past_scope)]
|
||||
: []),
|
||||
...file_tools,
|
||||
...grabBotGraphicTools({
|
||||
bot_id: bot.id,
|
||||
user_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
job_id: job.id || null,
|
||||
onGraphic: ({ ref }) => shown.push(ref),
|
||||
}),
|
||||
...extensions.tools,
|
||||
...skills.tools,
|
||||
...knowledge.tools,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { BUN_SQLITE_COBBLE_BOTS } from "@/db/types/db";
|
||||
import {
|
||||
ActionCheckNote,
|
||||
SecretsNote,
|
||||
GraphicsNote,
|
||||
botFilesNote,
|
||||
ComputerFilesNote,
|
||||
ScheduledRunNote,
|
||||
@@ -31,6 +32,7 @@ import askToTakeOverTool from "@/src/harness/tools/computer/ask-to-take-over-too
|
||||
import importFileTool from "@/src/harness/tools/computer/import-file-tool";
|
||||
import searchComputersTool from "@/src/harness/tools/computer/search-computers-tool";
|
||||
import grabBotFileTools from "@/src/harness/tools/storage/grab-bot-file-tools";
|
||||
import grabBotGraphicTools from "@/src/harness/tools/graphics/grab-bot-graphic-tools";
|
||||
import reportRunFailedTool from "@/src/harness/tools/bot/report-run-failed-tool";
|
||||
import computerRef from "@/src/utils/computer-ref";
|
||||
import grabRunScopes from "@/src/functions/backend/db/bot-files/grab-run-scopes";
|
||||
@@ -79,8 +81,8 @@ export default async function runBotTask({
|
||||
const dirs = computerRunDirs({ bot_id: bot.id, bot_kind: bot.kind, user_id, workspace_id });
|
||||
|
||||
/**
|
||||
* The secrets the bot captured or showed, whose chips the report
|
||||
* always carries.
|
||||
* The secrets the bot captured or showed and the graphics it made or
|
||||
* changed, whose chips the report always carries.
|
||||
*/
|
||||
const shown: string[] = [];
|
||||
|
||||
@@ -225,6 +227,14 @@ export default async function runBotTask({
|
||||
scopes,
|
||||
onAttach: ({ media_id }) => attachments.push(media_id),
|
||||
})),
|
||||
...grabBotGraphicTools({
|
||||
bot_id: bot.id,
|
||||
user_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
job_id: job.id || null,
|
||||
onGraphic: ({ ref }) => shown.push(ref),
|
||||
}),
|
||||
];
|
||||
|
||||
extensions = await openBotExtensions({
|
||||
@@ -260,6 +270,7 @@ export default async function runBotTask({
|
||||
TasksNote,
|
||||
ActionCheckNote,
|
||||
SecretsNote,
|
||||
GraphicsNote,
|
||||
workspace_id ? WorkspaceLearningNote : "",
|
||||
powers.is_coordinator
|
||||
? `If this brief asked you to record something in a workspace (its context, or a new one), do it yourself with update_workspace or create_workspace before you report: the report alone changes nothing.`
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { GraphicSizeMessageType } from "@/src/dict/graphics-dict";
|
||||
import useDocumentDark from "@/src/hooks/use-document-dark";
|
||||
import graphicUrl from "@/src/utils/graphic-url";
|
||||
|
||||
type Props = {
|
||||
graphic_id: number;
|
||||
title: string;
|
||||
version?: number;
|
||||
/**
|
||||
* Grow to fit the graphic up to this height (px), then scroll inside.
|
||||
* Left out, the frame fills its container (`className` sizes it).
|
||||
*/
|
||||
max_height?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const MinHeight = 120;
|
||||
|
||||
/**
|
||||
* A bot graphic in a sandboxed frame (`/graphics/<id>`): scripts run, but
|
||||
* under an origin of its own with no network, so it can't reach the app
|
||||
* or send anything out. A raw iframe: twui has no equivalent. In the
|
||||
* page's theme, sized to the height the graphic reports. A graphic that
|
||||
* navigates its frame away is closed, since that's never what a graphic
|
||||
* is for.
|
||||
*/
|
||||
export default function GraphicFrame({ graphic_id, title, version, max_height, className }: Props) {
|
||||
const dark = useDocumentDark();
|
||||
const frame = useRef<HTMLIFrameElement>(null);
|
||||
const loads = useRef(0);
|
||||
const [height, setHeight] = useState(MinHeight);
|
||||
const [moved, setMoved] = useState(false);
|
||||
const src = graphicUrl({ graphic_id, dark, version });
|
||||
|
||||
useEffect(() => {
|
||||
loads.current = 0;
|
||||
setMoved(false);
|
||||
}, [src]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!max_height) return;
|
||||
|
||||
const onMessage = (event: MessageEvent) => {
|
||||
if (event.source !== frame.current?.contentWindow) return;
|
||||
if (event.data?.type !== GraphicSizeMessageType) return;
|
||||
|
||||
const reported = Number(event.data.height);
|
||||
|
||||
if (Number.isFinite(reported)) setHeight(Math.min(max_height, Math.max(MinHeight, Math.ceil(reported))));
|
||||
};
|
||||
|
||||
window.addEventListener("message", onMessage);
|
||||
|
||||
return () => window.removeEventListener("message", onMessage);
|
||||
}, [max_height]);
|
||||
|
||||
if (moved) {
|
||||
return (
|
||||
<Span className="block w-full px-3 py-4 text-[13px] leading-[18px] text-ink-secondary bg-sunken">
|
||||
{`This graphic tried to open another page, so it was closed.`}
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<iframe
|
||||
key={src}
|
||||
ref={frame}
|
||||
src={src}
|
||||
title={title}
|
||||
sandbox="allow-scripts"
|
||||
referrerPolicy="no-referrer"
|
||||
loading="lazy"
|
||||
height={max_height ? height : undefined}
|
||||
onLoad={() => {
|
||||
loads.current += 1;
|
||||
|
||||
if (loads.current > 1) setMoved(true);
|
||||
}}
|
||||
className={twMerge("block w-full border-0 bg-paper", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import Modal from "@moduletrace/twui/elements/Modal";
|
||||
import H2 from "@moduletrace/twui/layout/H2";
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { SquareArrowOutUpRight } from "lucide-react";
|
||||
import type { BotGraphicView } from "@/src/types";
|
||||
import { graphicPageHref } from "@/src/utils/graphic-url";
|
||||
import GraphicFrame from "./graphic-frame";
|
||||
|
||||
type Props = {
|
||||
graphic: BotGraphicView;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A graphic large, over the page, with a link to its own page.
|
||||
*/
|
||||
export default function GraphicViewerModal({ graphic, onClose }: Props) {
|
||||
return (
|
||||
<Modal
|
||||
open
|
||||
setOpen={(next_open) => {
|
||||
if (!next_open) onClose();
|
||||
}}
|
||||
className="max-w-[95vw]! w-[1200px] h-[90vh] p-0 bg-paper flex flex-col overflow-hidden"
|
||||
>
|
||||
<Row className="w-full gap-3! flex-nowrap items-center pl-5 pr-14 py-3 border-0 border-b border-solid border-hairline">
|
||||
<Stack className="min-w-0 gap-0! items-start">
|
||||
<H2 className="text-base! leading-6! tracking-normal! font-bold text-ink truncate w-full">
|
||||
{graphic.title}
|
||||
</H2>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">{`By ${graphic.bot_name}`}</Span>
|
||||
</Stack>
|
||||
<Link
|
||||
href={graphicPageHref({
|
||||
graphic_id: graphic.id,
|
||||
bot_id: graphic.bot_id,
|
||||
})}
|
||||
title="Open on its own page"
|
||||
aria-label="Open on its own page"
|
||||
className="ml-auto shrink-0 p-2 text-ink-secondary hover:text-ink rounded-default hover:bg-sunken"
|
||||
target="_blank"
|
||||
>
|
||||
<SquareArrowOutUpRight size={16} aria-hidden />
|
||||
</Link>
|
||||
</Row>
|
||||
<GraphicFrame
|
||||
graphic_id={graphic.id}
|
||||
title={graphic.title}
|
||||
version={graphic.version}
|
||||
className="grow h-full"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Paper from "@moduletrace/twui/elements/Paper";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { PwaServiceWorkerPath } from "@/src/data/pwa-data";
|
||||
import clearDevelopmentServiceWorker from "@/src/functions/frontend/clear-development-service-worker";
|
||||
|
||||
@@ -24,8 +19,11 @@ const DevelopmentClearMaxAttempts = 3;
|
||||
|
||||
/**
|
||||
* Registers the app's service worker (production only, so development
|
||||
* HMR is never cached) and offers a reload once a new version is
|
||||
* installed. It never reloads on its own: a bot reply may be running.
|
||||
* HMR is never cached) and checks for a new one hourly and whenever the
|
||||
* app comes back into view. A new worker takes over by itself
|
||||
* (`public/sw.js`), so there is nothing to offer and nothing reloads: a
|
||||
* bot reply may be running. New builds reach the device through the
|
||||
* loading page.
|
||||
*
|
||||
* In development it does the opposite: it clears the worker and the
|
||||
* caches a production deploy left on the device, so the dev server's
|
||||
@@ -33,8 +31,6 @@ const DevelopmentClearMaxAttempts = 3;
|
||||
* `clear-development-service-worker.ts`).
|
||||
*/
|
||||
export default function ServiceWorkerRegistrar() {
|
||||
const [update_ready, setUpdateReady] = useState(false);
|
||||
const [reloading, setReloading] = useState(false);
|
||||
const registration = useRef<ServiceWorkerRegistration | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -45,26 +41,6 @@ export default function ServiceWorkerRegistrar() {
|
||||
let cancelled = false;
|
||||
let interval: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
const watch = (reg: ServiceWorkerRegistration) => {
|
||||
if (reg.waiting && navigator.serviceWorker.controller) {
|
||||
setUpdateReady(true);
|
||||
}
|
||||
|
||||
reg.addEventListener("updatefound", () => {
|
||||
const installing = reg.installing;
|
||||
|
||||
installing?.addEventListener("statechange", () => {
|
||||
if (cancelled) return;
|
||||
if (
|
||||
installing.state == "installed" &&
|
||||
navigator.serviceWorker.controller
|
||||
) {
|
||||
setUpdateReady(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
navigator.serviceWorker
|
||||
.register(PwaServiceWorkerPath, {
|
||||
scope: "/",
|
||||
@@ -73,7 +49,6 @@ export default function ServiceWorkerRegistrar() {
|
||||
.then((reg) => {
|
||||
if (cancelled) return;
|
||||
registration.current = reg;
|
||||
watch(reg);
|
||||
interval = setInterval(() => reg.update().catch(() => {}), UpdateCheckMs);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -160,57 +135,5 @@ export default function ServiceWorkerRegistrar() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reloading) return;
|
||||
|
||||
const onControllerChange = () => window.location.reload();
|
||||
navigator.serviceWorker?.addEventListener("controllerchange", onControllerChange);
|
||||
registration.current?.waiting?.postMessage({ type: "SKIP_WAITING" });
|
||||
const fallback = setTimeout(() => window.location.reload(), 1500);
|
||||
|
||||
return () => {
|
||||
clearTimeout(fallback);
|
||||
navigator.serviceWorker?.removeEventListener("controllerchange", onControllerChange);
|
||||
};
|
||||
}, [reloading]);
|
||||
|
||||
if (!update_ready) return null;
|
||||
|
||||
return (
|
||||
<Paper
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed bottom-4 right-4 z-[250] w-[min(320px,calc(100vw-32px))] gap-3! p-4 bg-surface shadow-popover! border-hairline dark:bg-surface dark:border-hairline"
|
||||
>
|
||||
<Span className="text-sm font-bold leading-5 text-ink">
|
||||
A new version is ready
|
||||
</Span>
|
||||
<Span className="text-[13px] leading-[18px] text-ink-secondary">
|
||||
Reload to use it. Your conversations are saved.
|
||||
</Span>
|
||||
<Row className="gap-2! flex-nowrap justify-end">
|
||||
<Button
|
||||
title="Reload later"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="gray"
|
||||
size="small"
|
||||
disabled={reloading}
|
||||
onClick={() => setUpdateReady(false)}
|
||||
>
|
||||
Later
|
||||
</Button>
|
||||
<Button
|
||||
title="Reload the app"
|
||||
type="button"
|
||||
size="small"
|
||||
loading={reloading}
|
||||
beforeIcon={<RefreshCw size={15} />}
|
||||
onClick={() => setReloading(true)}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
</Row>
|
||||
</Paper>
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -535,6 +535,15 @@ export const AppData = {
|
||||
* themselves to a reply; older ones are named.
|
||||
*/
|
||||
BotReplyAttachmentMessages: 4,
|
||||
/**
|
||||
* Longest HTML document a bot graphic (`bot_graphics`) may be.
|
||||
*/
|
||||
BotGraphicMaxChars: 200000,
|
||||
/**
|
||||
* Tallest a graphic grows inline in the thread (px) before it scrolls
|
||||
* inside its frame.
|
||||
*/
|
||||
BotGraphicInlineMaxHeight: 640,
|
||||
/**
|
||||
* Longest dictation the composer records before it stops on its own.
|
||||
*/
|
||||
|
||||
@@ -162,6 +162,14 @@ Anything you read or write goes to your model provider, so secret values (passwo
|
||||
- Never print, cat or echo a secret on a computer. To show one to the user, use show_secret for an env you have, or in a task on a computer capture it with capture_secret: either gives you a [secret:ID] ref to put in your message.
|
||||
- Use secrets by key ($KEY in the shell, {{env.KEY}} in tools), never by value. A value you see as [KEY] in output was redacted: that's expected.`;
|
||||
|
||||
/**
|
||||
* Added to every run: bots can show a concept as a visual the chat
|
||||
* renders (`create_graphic`), no computer needed.
|
||||
*/
|
||||
export const GraphicsNote = `# Graphics
|
||||
|
||||
When a concept, comparison, process, timeline or set of numbers is easier to see than to read, show it: create_graphic makes a chart, diagram or visual report as HTML that the chat shows inline, where you write its [graphic:ID] ref. It needs no computer. Keep the words beside it short, and say what it shows only if that isn't obvious. To change one, list_graphics or its ref gives the ID, read_graphic the HTML, and update_graphic replaces it: don't make a second one for the same thing. Don't make a graphic for something a sentence or a short list says as well.`;
|
||||
|
||||
/**
|
||||
* Added to the coordinator's chat prompt: it hands work to the crew.
|
||||
* `bot_id` is the coordinator itself, so the ref example it copies is its
|
||||
|
||||
@@ -121,6 +121,10 @@ export const JobStepToolLabels: Record<string, string> = {
|
||||
update_instructions: "Updating instructions",
|
||||
load_skill: "Reading a skill",
|
||||
send_message: "Sending a message",
|
||||
create_graphic: "Drawing a graphic",
|
||||
update_graphic: "Redrawing a graphic",
|
||||
read_graphic: "Reading a graphic",
|
||||
list_graphics: "Looking through graphics",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Where the app serves a bot graphic (`bot_graphics`): `/graphics/<id>`,
|
||||
* sandboxed with no network and an origin of its own
|
||||
* (`handleGraphicsRequest`).
|
||||
*/
|
||||
export const GraphicsPrefix = "/graphics";
|
||||
|
||||
/**
|
||||
* Where the libraries a graphic may load are served: `/graphics-lib/<file>`.
|
||||
* A graphic reaches nothing else, so these ship with the app.
|
||||
*/
|
||||
export const GraphicsLibPrefix = "/graphics-lib";
|
||||
|
||||
/**
|
||||
* The libraries a graphic may load, by the file name it loads them under,
|
||||
* read from the app's own `node_modules`.
|
||||
*/
|
||||
export const GraphicsLibs = [
|
||||
{
|
||||
file: "chart.js",
|
||||
source: "node_modules/chart.js/dist/chart.umd.min.js",
|
||||
description: `Chart.js 4 (global \`Chart\`): bar, line, pie, doughnut, radar, scatter and bubble charts on a <canvas>`,
|
||||
},
|
||||
{
|
||||
file: "mermaid.js",
|
||||
source: "node_modules/mermaid/dist/mermaid.min.js",
|
||||
description: `Mermaid (global \`mermaid\`): flowcharts, sequence, class, state, ER, gantt, pie, mindmap and timeline diagrams from text in <pre class="mermaid">; call \`mermaid.initialize({ startOnLoad: true, theme: document.documentElement.classList.contains("dark") ? "dark" : "default" })\``,
|
||||
},
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* The message a graphic's frame posts to the page with its height, so the
|
||||
* thread sizes it to fit.
|
||||
*/
|
||||
export const GraphicSizeMessageType = "cobble:graphic-size";
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
|
||||
type Params = {
|
||||
title?: string;
|
||||
html?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A graphic's title and HTML as they're stored: trimmed, the title one
|
||||
* line, the HTML within `AppData.BotGraphicMaxChars`. Throws an `AppError`
|
||||
* the bot can act on.
|
||||
*/
|
||||
export default function checkGraphicInput({ title, html }: Params) {
|
||||
const final_title = title?.replace(/\s+/g, " ").trim().slice(0, 200);
|
||||
const final_html = html?.trim();
|
||||
|
||||
if (title !== undefined && !final_title) throw new AppError(`Give the graphic a title.`);
|
||||
if (html !== undefined && !final_html) throw new AppError(`The graphic's HTML is empty.`);
|
||||
|
||||
if (final_html && final_html.length > AppData.BotGraphicMaxChars) {
|
||||
throw new AppError(
|
||||
`The HTML is ${final_html.length} characters; a graphic holds at most ${AppData.BotGraphicMaxChars}. Make it leaner: draw with SVG or a chart library instead of inlining data or images.`,
|
||||
);
|
||||
}
|
||||
|
||||
return { title: final_title, html: final_html };
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOT_GRAPHICS, BunSQLiteTables } from "@/db/types/db";
|
||||
import checkGraphicInput from "./check-graphic-input";
|
||||
|
||||
type Params = {
|
||||
user_id: number | string;
|
||||
bot_id: number | string;
|
||||
session_id?: number | string | null;
|
||||
workspace_id?: number | string | null;
|
||||
/**
|
||||
* The run that made it, so its message is linked once posted.
|
||||
*/
|
||||
job_id?: number | string | null;
|
||||
title: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Stores a graphic a bot made for a user. Returns its id.
|
||||
*/
|
||||
export default async function createBotGraphic({
|
||||
user_id,
|
||||
bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
job_id,
|
||||
title,
|
||||
html,
|
||||
}: Params) {
|
||||
const checked = checkGraphicInput({ title, html });
|
||||
|
||||
const res = await BunSQLite.insert<BUN_SQLITE_COBBLE_BOT_GRAPHICS, (typeof BunSQLiteTables)[number]>({
|
||||
table: "bot_graphics",
|
||||
data: [
|
||||
{
|
||||
title: checked.title,
|
||||
html: checked.html,
|
||||
version: 1,
|
||||
user_id: Number(user_id),
|
||||
bot_id: Number(bot_id),
|
||||
session_id: session_id ? Number(session_id) : undefined,
|
||||
workspace_id: workspace_id ? Number(workspace_id) : undefined,
|
||||
job_id: job_id ? Number(job_id) : undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const id = Number(res.singleRes?.id || res.postInsertReturn?.insertId);
|
||||
|
||||
if (!res.success || !id) throw new Error(`Couldn't store the graphic: ${res.msg}`);
|
||||
|
||||
return id;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BotGraphicView } from "@/src/types";
|
||||
import { viewerGraphicsSql } from "@/src/utils/bot-graphic-sql";
|
||||
|
||||
type Params = {
|
||||
graphic_id: number | string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A graphic as its card and page name it (never its HTML, which only
|
||||
* `/graphics/<id>` serves), for the user it was made for; null otherwise.
|
||||
*/
|
||||
export default async function grabBotGraphicView({ graphic_id, user_id }: Params): Promise<BotGraphicView | null> {
|
||||
const reach = viewerGraphicsSql({ alias: "g", user_id });
|
||||
|
||||
const row = (
|
||||
await BunSQLite.sql<BotGraphicView & { bot_name: string | null }>({
|
||||
sql: `SELECT g.id, g.title, g.version, g.bot_id, g.workspace_id, g.session_id, g.updated_at, b.name AS bot_name
|
||||
FROM bot_graphics g LEFT JOIN bots b ON b.id = g.bot_id
|
||||
WHERE g.id = ? AND ${reach.sql} LIMIT 1`,
|
||||
values: [Number(graphic_id), ...reach.values],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!row) return null;
|
||||
|
||||
return {
|
||||
id: Number(row.id),
|
||||
title: row.title || `Graphic ${row.id}`,
|
||||
version: Number(row.version) || 1,
|
||||
bot_id: Number(row.bot_id),
|
||||
bot_name: row.bot_name || "A bot",
|
||||
workspace_id: row.workspace_id ? Number(row.workspace_id) : null,
|
||||
session_id: row.session_id ? Number(row.session_id) : null,
|
||||
updated_at: Number(row.updated_at) || 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import type { BUN_SQLITE_COBBLE_BOT_GRAPHICS } from "@/db/types/db";
|
||||
import { viewerGraphicsSql } from "@/src/utils/bot-graphic-sql";
|
||||
|
||||
type Params = {
|
||||
graphic_id: number | string;
|
||||
/**
|
||||
* The viewer: only the user it was made for reads it.
|
||||
*/
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A graphic with its HTML, for the user it was made for; null for anyone
|
||||
* else or a missing one.
|
||||
*/
|
||||
export default async function grabBotGraphic({ graphic_id, user_id }: Params) {
|
||||
const reach = viewerGraphicsSql({ alias: "g", user_id });
|
||||
|
||||
return (
|
||||
(
|
||||
await BunSQLite.sql<BUN_SQLITE_COBBLE_BOT_GRAPHICS>({
|
||||
sql: `SELECT g.* FROM bot_graphics g WHERE g.id = ? AND ${reach.sql} LIMIT 1`,
|
||||
values: [Number(graphic_id), ...reach.values],
|
||||
})
|
||||
).payload?.[0] || null
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import { runGraphicsSql } from "@/src/utils/bot-graphic-sql";
|
||||
|
||||
type Params = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
/**
|
||||
* The place: a workspace, or null for the bot's own chat.
|
||||
*/
|
||||
workspace_id?: number | string | null;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphics a bot made for a user in one place, newest first, without
|
||||
* their HTML.
|
||||
*/
|
||||
export default async function listBotGraphics({ bot_id, user_id, workspace_id, limit = 20 }: Params) {
|
||||
const reach = runGraphicsSql({ alias: "g", user_id, bot_id });
|
||||
|
||||
return (
|
||||
(
|
||||
await BunSQLite.sql<{ id: number; title: string; version: number; updated_at: number; chars: number }>({
|
||||
sql: `SELECT g.id, g.title, g.version, g.updated_at, LENGTH(g.html) AS chars FROM bot_graphics g
|
||||
WHERE ${reach.sql} AND g.workspace_id IS ?
|
||||
ORDER BY g.updated_at DESC, g.id DESC LIMIT ?`,
|
||||
values: [...reach.values, workspace_id ? Number(workspace_id) : null, limit] as (string | number)[],
|
||||
})
|
||||
).payload || []
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import BunSQLite from "@moduletrace/bun-sqlite";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import { runGraphicsSql } from "@/src/utils/bot-graphic-sql";
|
||||
import checkGraphicInput from "./check-graphic-input";
|
||||
|
||||
type Params = {
|
||||
graphic_id: number | string;
|
||||
/**
|
||||
* The run's bot and user: only the graphics that bot made for that
|
||||
* user are changed.
|
||||
*/
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
title?: string;
|
||||
html?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces a graphic's HTML (and title, when given) and bumps its
|
||||
* version. Every message naming it shows the new one. Returns its new
|
||||
* version.
|
||||
*/
|
||||
export default async function updateBotGraphic({ graphic_id, bot_id, user_id, title, html }: Params) {
|
||||
const checked = checkGraphicInput({ title, html });
|
||||
|
||||
if (!checked.title && !checked.html) throw new AppError(`Nothing to change: give new html or a title.`);
|
||||
|
||||
const reach = runGraphicsSql({ alias: "bot_graphics", user_id, bot_id });
|
||||
|
||||
const row = (
|
||||
await BunSQLite.sql<{ version: number }>({
|
||||
sql: `UPDATE bot_graphics SET
|
||||
title = COALESCE(?, title),
|
||||
html = COALESCE(?, html),
|
||||
version = COALESCE(version, 1) + 1,
|
||||
updated_at = ?
|
||||
WHERE id = ? AND ${reach.sql}
|
||||
RETURNING version`,
|
||||
values: [checked.title ?? null, checked.html ?? null, Date.now(), Number(graphic_id), ...reach.values],
|
||||
})
|
||||
).payload?.[0];
|
||||
|
||||
if (!row) throw new AppError(`Graphic ${graphic_id} isn't one you made for this user.`);
|
||||
|
||||
return Number(row.version);
|
||||
}
|
||||
@@ -110,6 +110,14 @@ export default async function completeBotJob({
|
||||
sql: `UPDATE token_usages SET message_id = ? WHERE job_id = ? AND message_id IS NULL`,
|
||||
values: [inserted.id, job.id],
|
||||
}).catch(() => {});
|
||||
|
||||
/**
|
||||
* Graphics the run made belong to the message it posted.
|
||||
*/
|
||||
await BunSQLite.sql({
|
||||
sql: `UPDATE bot_graphics SET message_id = ? WHERE job_id = ? AND message_id IS NULL`,
|
||||
values: [inserted.id, job.id],
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,7 @@ export default async function deleteBot({ user_id, bot_id }: Params) {
|
||||
await BunSQLite.sql({ sql: `DELETE FROM bot_types WHERE bot_id = ?`, values: [bot.id] });
|
||||
await BunSQLite.sql({ sql: `DELETE FROM envs WHERE bot_id = ?`, values: [bot.id] });
|
||||
await BunSQLite.sql({ sql: `DELETE FROM secret_requests WHERE bot_id = ?`, values: [bot.id] });
|
||||
await BunSQLite.sql({ sql: `DELETE FROM bot_graphics WHERE bot_id = ?`, values: [bot.id] });
|
||||
|
||||
const res = await BunSQLite.sql({ sql: `DELETE FROM bots WHERE id = ?`, values: [bot.id] });
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export default async function deleteUser({ actor_user_id, actor_user_types, user
|
||||
/**
|
||||
* These point at the user without a foreign key, so nothing cascades.
|
||||
*/
|
||||
for (const table of ["user_types", "user_passwords", "sso_login_codes", "secret_requests"]) {
|
||||
for (const table of ["user_types", "user_passwords", "sso_login_codes", "secret_requests", "bot_graphics"]) {
|
||||
await BunSQLite.sql({ sql: `DELETE FROM ${table} WHERE user_id = ?`, values: [user.id] });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import parseThemeVars, { type ThemeVars } from "@/src/utils/parse-theme-vars";
|
||||
|
||||
let cached: ThemeVars | null = null;
|
||||
|
||||
/**
|
||||
* The app's theme colours and fonts for graphics, read once from
|
||||
* `src/styles/main.css` so a theme change there reaches them too.
|
||||
*/
|
||||
export default function grabGraphicThemeVars(): ThemeVars {
|
||||
if (cached) return cached;
|
||||
|
||||
try {
|
||||
cached = parseThemeVars({
|
||||
css: fs.readFileSync(path.join(process.cwd(), "src/styles/main.css"), "utf-8"),
|
||||
});
|
||||
} catch {
|
||||
cached = { light: {}, dark: {}, fonts: {} };
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import path from "node:path";
|
||||
import { GraphicsLibPrefix, GraphicsLibs, GraphicsPrefix } from "@/src/dict/graphics-dict";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabBotGraphic from "@/src/functions/backend/db/bot-graphics/grab-bot-graphic";
|
||||
import graphicDocument from "@/src/utils/graphic-document";
|
||||
import graphicHeaders from "@/src/utils/graphic-headers";
|
||||
import isSecureRequest from "@/src/utils/is-secure-request";
|
||||
import grabGraphicThemeVars from "./grab-graphic-theme-vars";
|
||||
|
||||
type Params = {
|
||||
req: Request;
|
||||
url: URL;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serves bot graphics and the libraries they may load:
|
||||
*
|
||||
* - `/graphics/<id>[?theme=dark]`: the graphic, for the user it was made
|
||||
* for only (anyone else, signed in or not, gets the same 404), themed
|
||||
* and sandboxed (`graphicHeaders`).
|
||||
* - `/graphics-lib/<file>`: a library from `GraphicsLibs`, public (a
|
||||
* sandboxed graphic sends no cookie of the app's) and cached for a day.
|
||||
*
|
||||
* Null for any other path.
|
||||
*/
|
||||
export default async function handleGraphicsRequest({ req, url }: Params): Promise<Response | null> {
|
||||
if (url.pathname.startsWith(`${GraphicsLibPrefix}/`)) {
|
||||
return await serveLib({ file: url.pathname.slice(GraphicsLibPrefix.length + 1) });
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(`${GraphicsPrefix}/`)) return null;
|
||||
|
||||
const id = url.pathname.slice(GraphicsPrefix.length + 1).replace(/\/$/, "");
|
||||
|
||||
if (req.method != "GET" || !/^\d+$/.test(id)) return notFound();
|
||||
|
||||
const { user } = await userAuth({ req });
|
||||
|
||||
if (!user?.id) return notFound();
|
||||
|
||||
const graphic = await grabBotGraphic({ graphic_id: id, user_id: user.id });
|
||||
|
||||
if (!graphic?.html) return notFound();
|
||||
|
||||
const origin = `${isSecureRequest({ req }) ? "https" : "http"}://${req.headers.get("host") || url.host}`;
|
||||
|
||||
return new Response(
|
||||
graphicDocument({
|
||||
html: graphic.html,
|
||||
title: graphic.title || `Graphic ${graphic.id}`,
|
||||
dark: url.searchParams.get("theme") == "dark",
|
||||
vars: grabGraphicThemeVars(),
|
||||
}),
|
||||
{ headers: graphicHeaders({ origin }) },
|
||||
);
|
||||
}
|
||||
|
||||
async function serveLib({ file }: { file: string }) {
|
||||
const lib = GraphicsLibs.find((it) => it.file == file);
|
||||
|
||||
if (!lib) return notFound();
|
||||
|
||||
const source = Bun.file(path.join(process.cwd(), lib.source));
|
||||
|
||||
if (!(await source.exists())) return notFound();
|
||||
|
||||
const response = new Response(source, {
|
||||
headers: {
|
||||
"Content-Type": "text/javascript; charset=utf-8",
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
},
|
||||
});
|
||||
|
||||
response.headers.delete("content-disposition");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function notFound() {
|
||||
return new Response("Not Found", { status: 404, headers: { "Cache-Control": "no-store" } });
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { z } from "zod";
|
||||
import createBotGraphic from "@/src/functions/backend/db/bot-graphics/create-bot-graphic";
|
||||
import graphicRef from "@/src/utils/graphic-ref";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import { GraphicAuthoringNote } from "./graphic-authoring-note";
|
||||
import type { GraphicToolContext } from "./graphic-tool-context";
|
||||
|
||||
type Args = {
|
||||
title: string;
|
||||
html: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot make a visual (a chart, diagram, timeline, comparison or
|
||||
* illustrated explainer) as HTML the chat shows inline. Stored in the app
|
||||
* (`bot_graphics`), so it needs no computer.
|
||||
*/
|
||||
export default function createGraphicTool(context: GraphicToolContext): HarnessTool<Args> {
|
||||
return {
|
||||
name: "create_graphic",
|
||||
description: `Make a visual for the user, shown inline in the chat: a chart, diagram, flow, timeline, comparison table, dashboard or illustrated explainer. Use it when a picture makes something clearer than text does; keep the reply's words short beside it. ${GraphicAuthoringNote} Write the [graphic:ID] ref it returns in your reply where it should show. To change it later, update_graphic it rather than making another.`,
|
||||
schema: z.object({
|
||||
title: z.string().trim().min(1).max(200).describe(`What it shows, e.g. "Q3 revenue by region"`),
|
||||
html: z.string().min(1).describe("The document"),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const id = await createBotGraphic({
|
||||
user_id: context.user_id,
|
||||
bot_id: context.bot_id,
|
||||
session_id: context.session_id,
|
||||
workspace_id: context.workspace_id,
|
||||
job_id: context.job_id,
|
||||
title: args.title,
|
||||
html: args.html,
|
||||
});
|
||||
const ref = graphicRef({ id });
|
||||
|
||||
context.onGraphic?.({ ref });
|
||||
|
||||
return `Made graphic ${id}, "${args.title.trim()}". Write ${ref} in your reply where it should show; the user sees it there.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { HarnessTool } from "../../types";
|
||||
import createGraphicTool from "./create-graphic-tool";
|
||||
import type { GraphicToolContext } from "./graphic-tool-context";
|
||||
import listGraphicsTool from "./list-graphics-tool";
|
||||
import readGraphicTool from "./read-graphic-tool";
|
||||
import updateGraphicTool from "./update-graphic-tool";
|
||||
|
||||
/**
|
||||
* The tools a bot makes visuals with (`src/harness/tools/graphics/`):
|
||||
* making, changing, reading and listing the HTML graphics it made for the
|
||||
* run's user. Every run gets them; they need no computer, since the app
|
||||
* stores and serves graphics itself.
|
||||
*/
|
||||
export default function grabBotGraphicTools(context: GraphicToolContext): HarnessTool[] {
|
||||
return [
|
||||
createGraphicTool(context),
|
||||
updateGraphicTool(context),
|
||||
readGraphicTool(context),
|
||||
listGraphicsTool(context),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { GraphicsLibPrefix, GraphicsLibs } from "@/src/dict/graphics-dict";
|
||||
|
||||
/**
|
||||
* How a graphic's HTML must be written, in the tool descriptions: what the
|
||||
* sandbox lets it load and the theme it can draw with.
|
||||
*/
|
||||
export const GraphicAuthoringNote = `Write one self-contained HTML document (or a fragment), up to ${AppData.BotGraphicMaxChars} characters: inline <style>, <svg> and <script>. It runs sandboxed with no network: outside URLs (CDNs, fetch, remote images or fonts, links that leave) are blocked, so put the data in the document. The only scripts you can load are these, by exactly these tags:
|
||||
${GraphicsLibs.map((lib) => `- <script src="${GraphicsLibPrefix}/${lib.file}"></script>: ${lib.description}`).join("\n")}
|
||||
The app's theme is set as CSS variables: var(--c-paper) (the background, already applied to body), var(--c-surface), var(--c-sunken), var(--c-ink) (text), var(--c-ink-secondary), var(--c-border), var(--c-border-strong), var(--font-sans), var(--font-mono). The root has the class "dark" in the dark theme: use the variables, or pick colours that read on both. It shows about 700px wide in the chat and full screen when opened, so keep it responsive; its height grows to fit.`;
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* What the graphic tools of one run work with: the bot, the user it works
|
||||
* for and the place, and a callback that puts a graphic on its message.
|
||||
*/
|
||||
export type GraphicToolContext = {
|
||||
bot_id: number | string;
|
||||
user_id: number | string;
|
||||
session_id?: number | string | null;
|
||||
workspace_id?: number | string | null;
|
||||
job_id?: number | string | null;
|
||||
/**
|
||||
* Called with a graphic's ref when the run makes or changes it, so its
|
||||
* message always shows it.
|
||||
*/
|
||||
onGraphic?: (params: { ref: string }) => void;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
import listBotGraphics from "@/src/functions/backend/db/bot-graphics/list-bot-graphics";
|
||||
import graphicRef from "@/src/utils/graphic-ref";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import type { GraphicToolContext } from "./graphic-tool-context";
|
||||
|
||||
type Args = {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot find the graphics it made for this user here, newest first,
|
||||
* so it changes one rather than making it again.
|
||||
*/
|
||||
export default function listGraphicsTool(context: GraphicToolContext): HarnessTool<Args> {
|
||||
return {
|
||||
name: "list_graphics",
|
||||
description: `List the graphics you made for this user in this conversation's place, newest first. Check it before making a graphic the user may already have.`,
|
||||
schema: z.object({
|
||||
limit: z.number().int().min(1).max(50).optional(),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const graphics = await listBotGraphics({
|
||||
bot_id: context.bot_id,
|
||||
user_id: context.user_id,
|
||||
workspace_id: context.workspace_id,
|
||||
limit: args.limit,
|
||||
});
|
||||
|
||||
if (!graphics.length) return `You haven't made any graphics for this user here.`;
|
||||
|
||||
return graphics
|
||||
.map(
|
||||
(graphic) =>
|
||||
`- ${graphicRef({ id: graphic.id })} "${graphic.title}", version ${graphic.version || 1}, ${graphic.chars} characters, updated ${new Date(Number(graphic.updated_at)).toUTCString()}`,
|
||||
)
|
||||
.join("\n");
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
import grabBotGraphic from "@/src/functions/backend/db/bot-graphics/grab-bot-graphic";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import type { GraphicToolContext } from "./graphic-tool-context";
|
||||
|
||||
type Args = {
|
||||
graphic_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot read the HTML of a graphic it made for this user, to edit it.
|
||||
*/
|
||||
export default function readGraphicTool(context: GraphicToolContext): HarnessTool<Args> {
|
||||
return {
|
||||
name: "read_graphic",
|
||||
description: `Read the HTML of a graphic you made for this user, to change it with update_graphic.`,
|
||||
schema: z.object({
|
||||
graphic_id: z.number().int().positive().describe("The ID from its [graphic:ID] ref"),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const graphic = await grabBotGraphic({ graphic_id: args.graphic_id, user_id: context.user_id });
|
||||
|
||||
if (!graphic || Number(graphic.bot_id) != Number(context.bot_id)) {
|
||||
return `Graphic ${args.graphic_id} isn't one you made for this user.`;
|
||||
}
|
||||
|
||||
return `Graphic ${graphic.id}, "${graphic.title}" (version ${graphic.version || 1}):\n\n${graphic.html || ""}`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
import updateBotGraphic from "@/src/functions/backend/db/bot-graphics/update-bot-graphic";
|
||||
import graphicRef from "@/src/utils/graphic-ref";
|
||||
import type { HarnessTool } from "../../types";
|
||||
import { GraphicAuthoringNote } from "./graphic-authoring-note";
|
||||
import type { GraphicToolContext } from "./graphic-tool-context";
|
||||
|
||||
type Args = {
|
||||
graphic_id: number;
|
||||
title?: string;
|
||||
html?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a bot replace a graphic it made for this user. Every message
|
||||
* naming it shows the new version.
|
||||
*/
|
||||
export default function updateGraphicTool(context: GraphicToolContext): HarnessTool<Args> {
|
||||
return {
|
||||
name: "update_graphic",
|
||||
description: `Change a graphic you made for this user: new html replaces the whole document (read_graphic first to start from the current one), a new title renames it. Every message showing it shows the new version. ${GraphicAuthoringNote}`,
|
||||
schema: z.object({
|
||||
graphic_id: z.number().int().positive().describe("The ID from its [graphic:ID] ref"),
|
||||
title: z.string().trim().min(1).max(200).optional(),
|
||||
html: z.string().min(1).optional().describe("The whole new document"),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
const version = await updateBotGraphic({
|
||||
graphic_id: args.graphic_id,
|
||||
bot_id: context.bot_id,
|
||||
user_id: context.user_id,
|
||||
title: args.title,
|
||||
html: args.html,
|
||||
});
|
||||
const ref = graphicRef({ id: args.graphic_id });
|
||||
|
||||
context.onGraphic?.({ ref });
|
||||
|
||||
return `Updated graphic ${args.graphic_id} (now version ${version}). Write ${ref} in your reply to show it again.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function readDark() {
|
||||
return typeof document != "undefined" && document.documentElement.classList.contains("dark");
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the page shows the dark theme now (`dark` on the root, set by
|
||||
* `ThemeInitScript` and the theme toggles), kept live as it changes.
|
||||
*/
|
||||
export default function useDocumentDark() {
|
||||
const [dark, setDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDark(readDark());
|
||||
|
||||
const observer = new MutationObserver(() => setDark(readDark()));
|
||||
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return dark;
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import { createContext } from "react";
|
||||
export type RailDrawerContextObject = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
/**
|
||||
* False on a page without the rail (`isRaillessPage`), where the menu
|
||||
* button has nothing to open.
|
||||
*/
|
||||
has_rail: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -14,4 +19,5 @@ export type RailDrawerContextObject = {
|
||||
export const RailDrawerContext = createContext<RailDrawerContextObject>({
|
||||
open: false,
|
||||
setOpen: () => {},
|
||||
has_rail: true,
|
||||
});
|
||||
|
||||
@@ -6,10 +6,13 @@ import HeaderIconButton from "@/src/components/general/header-icon-button";
|
||||
import { RailDrawerContext } from "../(data)/rail-drawer-context";
|
||||
|
||||
/**
|
||||
* Opens the rail's drawer below `md`; the inline rail has no need for it.
|
||||
* Opens the rail's drawer below `md`; the inline rail has no need for it,
|
||||
* and a page without the rail has none to open.
|
||||
*/
|
||||
export default function RailDrawerButton() {
|
||||
const { setOpen } = useContext(RailDrawerContext);
|
||||
const { setOpen, has_rail } = useContext(RailDrawerContext);
|
||||
|
||||
if (!has_rail) return null;
|
||||
|
||||
return (
|
||||
<span className="inline-flex md:hidden">
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import isRaillessPage from "./is-railless-page";
|
||||
|
||||
describe("isRaillessPage", () => {
|
||||
test("a graphic's own page has no rail", () => {
|
||||
expect(isRaillessPage({ pathname: "/admin/bots/3/graphics/5" })).toBe(true);
|
||||
expect(isRaillessPage({ pathname: "/admin/bots/3/graphics/5/" })).toBe(true);
|
||||
});
|
||||
|
||||
test("every other page keeps it", () => {
|
||||
expect(isRaillessPage({ pathname: "/admin/bots/3" })).toBe(false);
|
||||
expect(isRaillessPage({ pathname: "/admin/bots/3/graphics" })).toBe(false);
|
||||
expect(isRaillessPage({ pathname: "/admin/settings/general" })).toBe(false);
|
||||
expect(isRaillessPage({ pathname: undefined })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Pages that fill the window without the rail: a bot graphic's own page
|
||||
* (`/admin/bots/<bot>/graphics/<id>`), which is about one graphic and
|
||||
* leads back to its conversation from its header.
|
||||
*/
|
||||
export default function isRaillessPage({ pathname }: { pathname?: string | null }) {
|
||||
return /^\/admin\/bots\/\d+\/graphics\/\d+\/?$/.test(pathname || "");
|
||||
}
|
||||
@@ -11,11 +11,13 @@ import BotsRail from "./(sections)/bots-rail";
|
||||
import LiveSocket from "./(sections)/live-socket";
|
||||
import AddFlow from "./(partials)/add-flow";
|
||||
import OfflineBanner from "./(partials)/offline-banner";
|
||||
import isRaillessPage from "./(utils)/is-railless-page";
|
||||
|
||||
/**
|
||||
* Full-bleed shell for every `/admin/*` page: the rail of bot conversations
|
||||
* and workspaces, then the page (a conversation, a workspace, or
|
||||
* settings), inside the Add flow both can open. No cards, no shadows;
|
||||
* settings), inside the Add flow both can open. A graphic's own page
|
||||
* (`isRaillessPage`) goes without the rail. No cards, no shadows;
|
||||
* regions are separated by 1px hairlines. Below `md` the rail slides over
|
||||
* the page instead of taking width. Height is `dvh`, so the on-screen
|
||||
* keyboard doesn't push the composer out of view.
|
||||
@@ -50,9 +52,11 @@ export default function BotsLayout({
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [drawer_open]);
|
||||
|
||||
const has_rail = !isRaillessPage({ pathname });
|
||||
|
||||
const drawer = useMemo(
|
||||
() => ({ open: drawer_open, setOpen: setDrawerOpen }),
|
||||
[drawer_open],
|
||||
() => ({ open: drawer_open, setOpen: setDrawerOpen, has_rail }),
|
||||
[drawer_open, has_rail],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -61,7 +65,7 @@ export default function BotsLayout({
|
||||
<Stack className="w-full h-dvh gap-0! items-stretch overflow-hidden bg-paper text-ink pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
|
||||
{online ? null : <OfflineBanner />}
|
||||
<Row className="grow min-h-0 w-full gap-0! flex-nowrap items-stretch overflow-hidden">
|
||||
<BotsRail />
|
||||
{has_rail ? <BotsRail /> : null}
|
||||
<Row className="grow min-w-0 h-full gap-0! flex-nowrap items-stretch">
|
||||
{children}
|
||||
</Row>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import fetchApi from "@moduletrace/twui/utils/fetch/fetchApi";
|
||||
import type { APIResponseObject } from "@moduletrace/bunext/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { BotGraphicView } from "@/src/types";
|
||||
|
||||
type Params = {
|
||||
graphic_id: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Graphics read lately, by id, so a thread naming one twice asks once. An
|
||||
* entry is read again after `FreshMs`, since a bot may have changed it.
|
||||
*/
|
||||
const loaded = new Map<number, { at: number; view: Promise<BotGraphicView | null> }>();
|
||||
const FreshMs = 10_000;
|
||||
|
||||
function loadGraphic({ graphic_id }: Params) {
|
||||
const cached = loaded.get(graphic_id);
|
||||
|
||||
if (cached && Date.now() - cached.at < FreshMs) return cached.view;
|
||||
|
||||
const view = fetchApi<{}, APIResponseObject<BotGraphicView>>(
|
||||
`/api/admin/graphics/get-graphic?graphic_id=${graphic_id}`,
|
||||
{ method: "GET" },
|
||||
)
|
||||
.then((res) => (res?.success && res.singleRes ? res.singleRes : null))
|
||||
.catch(() => null);
|
||||
|
||||
loaded.set(graphic_id, { at: Date.now(), view });
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* A graphic a bot named (`[graphic:ID]`), for its card: its title, bot and
|
||||
* version. Only the user it was made for reads it; anyone else gets null.
|
||||
*/
|
||||
export default function useBotGraphic({ graphic_id }: Params) {
|
||||
const [view, setView] = useState<BotGraphicView | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
|
||||
loadGraphic({ graphic_id }).then((result) => {
|
||||
if (!live) return;
|
||||
setView(result);
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [graphic_id]);
|
||||
|
||||
return { view, loading };
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useState } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { ChartColumn, Maximize2, SquareArrowOutUpRight } from "lucide-react";
|
||||
import GraphicFrame from "@/src/components/general/graphic-frame";
|
||||
import GraphicViewerModal from "@/src/components/general/graphic-viewer-modal";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import { graphicPageHref } from "@/src/utils/graphic-url";
|
||||
import useBotGraphic from "../(hooks)/use-bot-graphic";
|
||||
|
||||
type Props = {
|
||||
graphic_id: number;
|
||||
};
|
||||
|
||||
const ActionClassName =
|
||||
"shrink-0 p-1! text-ink-secondary dark:text-ink-secondary hover:text-ink rounded-default-xs";
|
||||
|
||||
/**
|
||||
* A graphic a bot made (`graphicRef`), shown where the bot named it: its
|
||||
* title and the graphic itself, sized to fit (up to
|
||||
* `AppData.BotGraphicInlineMaxHeight`), with ways to open it large or on
|
||||
* its own page. Built from spans, since the ref sits in a paragraph.
|
||||
* Anyone it wasn't made for sees its number only.
|
||||
*/
|
||||
export default function GraphicRefCard({ graphic_id }: Props) {
|
||||
const { view, loading } = useBotGraphic({ graphic_id });
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
if (!view) {
|
||||
return (
|
||||
<Span className="font-bold">
|
||||
{loading
|
||||
? `graphic ${graphic_id}`
|
||||
: `graphic ${graphic_id} (not available)`}
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Span className="flex w-full flex-col my-1 overflow-hidden rounded-default border border-solid border-hairline">
|
||||
<Span className="flex w-full flex-nowrap items-center gap-2 pl-3 pr-1.5 py-1.5 border-0 border-b border-solid border-hairline bg-surface">
|
||||
<ChartColumn
|
||||
size={14}
|
||||
aria-hidden
|
||||
className="shrink-0 text-ink-secondary"
|
||||
/>
|
||||
<Span className="min-w-0 grow truncate text-[13px] leading-5 font-bold text-ink">
|
||||
{view.title}
|
||||
</Span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
color="text"
|
||||
title="Open large"
|
||||
aria-label="Open large"
|
||||
onClick={() => setOpen(true)}
|
||||
className={ActionClassName}
|
||||
>
|
||||
<Maximize2 size={14} aria-hidden />
|
||||
</Button>
|
||||
<Link
|
||||
href={graphicPageHref({
|
||||
graphic_id: view.id,
|
||||
bot_id: view.bot_id,
|
||||
})}
|
||||
title="Open on its own page"
|
||||
aria-label="Open on its own page"
|
||||
className={ActionClassName}
|
||||
target="_blank"
|
||||
>
|
||||
<SquareArrowOutUpRight size={14} aria-hidden />
|
||||
</Link>
|
||||
</Span>
|
||||
<GraphicFrame
|
||||
graphic_id={view.id}
|
||||
title={view.title}
|
||||
version={view.version}
|
||||
max_height={AppData.BotGraphicInlineMaxHeight}
|
||||
/>
|
||||
{open ? (
|
||||
<GraphicViewerModal
|
||||
graphic={view}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
</Span>
|
||||
);
|
||||
}
|
||||
@@ -13,10 +13,12 @@ import {
|
||||
WorkspaceRefHrefPrefix,
|
||||
} from "@/src/utils/link-computer-refs";
|
||||
import { parseFolderRefHref } from "@/src/utils/folder-ref";
|
||||
import { graphicRefId } from "@/src/utils/graphic-ref";
|
||||
import BotRefChip from "./bot-ref-chip";
|
||||
import ComputerRefChip from "./computer-ref-chip";
|
||||
import FileRefChip from "./file-ref-chip";
|
||||
import FolderRefChip from "./folder-ref-chip";
|
||||
import GraphicRefCard from "./graphic-ref-card";
|
||||
import HookProposalRefChip from "./hook-proposal-ref-chip";
|
||||
import ProposalRefChip from "./proposal-ref-chip";
|
||||
import SecretRefChip from "./secret-ref-chip";
|
||||
@@ -32,7 +34,7 @@ import WorkspaceRefChip from "./workspace-ref-chip";
|
||||
* folder it named a chip that opens it in the files browser, an MCP server,
|
||||
* tool, skill or hook it proposed a chip that opens the confirm card, a
|
||||
* secret it showed a masked chip, a secret it asked for a chip that opens
|
||||
* the card it's typed into;
|
||||
* the card it's typed into, a graphic it made the graphic itself;
|
||||
* anything else opens in a new tab.
|
||||
*/
|
||||
export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
|
||||
@@ -99,6 +101,10 @@ export default function MarkdownLink({ href, children }: ComponentProps<"a">) {
|
||||
if (request_id) return <SecretRequestRefChip request_id={request_id} />;
|
||||
}
|
||||
|
||||
const graphic_id = graphicRefId({ href });
|
||||
|
||||
if (graphic_id) return <GraphicRefCard graphic_id={graphic_id} />;
|
||||
|
||||
const folder_ref = parseFolderRefHref({ href });
|
||||
|
||||
if (folder_ref) return <FolderRefChip spec={folder_ref.spec} path={folder_ref.path} />;
|
||||
|
||||
@@ -5,8 +5,8 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* One message's copy button, in the row under a user group beside its
|
||||
* timestamp. A message with no text renders nothing at all.
|
||||
* One message's copy button, in the row under it beside its timestamp. A
|
||||
* message with no text renders nothing at all.
|
||||
*/
|
||||
export default function UserMessageCopy({ text }: Props) {
|
||||
if (!text.trim()) return null;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import copyableMessages from "../(utils)/copyable-messages";
|
||||
import type { MessageGroup } from "../(utils)/group-messages";
|
||||
import UserMessage from "./user-message";
|
||||
import UserMessageCopy from "./user-message-copy";
|
||||
import GroupTimestamp from "./group-timestamp";
|
||||
|
||||
type Props = {
|
||||
bot: BotWithLastMessage;
|
||||
@@ -13,24 +9,16 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Right-aligned ink bubbles (each message's files above it), then one
|
||||
* row under the group: a copy button per message with text, left of the
|
||||
* group's time.
|
||||
* Right-aligned ink bubbles (each message's files above it), each with
|
||||
* its own copy button and time under it, so two messages sent close
|
||||
* together never share one.
|
||||
*/
|
||||
export default function UserMessageGroup({ bot, group }: Props) {
|
||||
const copyable = copyableMessages({ messages: group.messages });
|
||||
|
||||
return (
|
||||
<Stack className="w-full gap-1! items-end">
|
||||
{group.messages.map((message) => (
|
||||
<UserMessage key={message.id} message={message} bot_name={bot.name || "the bot"} />
|
||||
))}
|
||||
<Row className="gap-3! items-center">
|
||||
{copyable.map((message) => (
|
||||
<UserMessageCopy key={message.id} text={message.text || ""} />
|
||||
))}
|
||||
<GroupTimestamp at={group.at} />
|
||||
</Row>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useContext } from "react";
|
||||
import { Reply } from "lucide-react";
|
||||
import type { BotMessageWithAttachments } from "@/src/types";
|
||||
import { BotMessageActionsContext } from "../(data)/message-actions-context";
|
||||
import MessageActionButton from "./message-action-button";
|
||||
|
||||
type Props = {
|
||||
message: BotMessageWithAttachments;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reply to one of the user's own messages, beside its copy button. A
|
||||
* message still on its way has only a temporary (negative) id, so it gets
|
||||
* no button until the saved one replaces it.
|
||||
*/
|
||||
export default function UserMessageReply({ message }: Props) {
|
||||
const { onReply } = useContext(BotMessageActionsContext);
|
||||
|
||||
if (!(Number(message.id) > 0)) return null;
|
||||
|
||||
return (
|
||||
<MessageActionButton
|
||||
title="Reply"
|
||||
aria_label="Reply to this message"
|
||||
onClick={() => onReply({ message })}
|
||||
>
|
||||
<Reply size={14} aria-hidden />
|
||||
</MessageActionButton>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import { memo } from "react";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { BotMessageWithAttachments } from "@/src/types";
|
||||
import MessageAttachments from "./message-attachments";
|
||||
import MessageReactions from "./message-reactions";
|
||||
import MessageReplyQuote from "./message-reply-quote";
|
||||
import UserBubble from "./user-bubble";
|
||||
import UserMessageCopy from "./user-message-copy";
|
||||
import UserMessageReply from "./user-message-reply";
|
||||
import GroupTimestamp from "./group-timestamp";
|
||||
|
||||
type Props = {
|
||||
message: BotMessageWithAttachments;
|
||||
@@ -14,9 +18,9 @@ type Props = {
|
||||
/**
|
||||
* One user message: the quoted message it answers, its files, then its
|
||||
* text (a file-only message has no bubble) and the bot's reactions to it.
|
||||
* The user doesn't reply to or react to their own messages, but they can
|
||||
* copy one: the group's copy buttons sit beside its timestamp, in
|
||||
* `UserMessageGroup`. Memoized, like `BotMessageLine`.
|
||||
* The user doesn't react to their own messages, but they can copy one or
|
||||
* reply to it: those buttons sit beside its own time. Memoized, like
|
||||
* `BotMessageLine`.
|
||||
*/
|
||||
function UserMessage({ message, bot_name }: Props) {
|
||||
return (
|
||||
@@ -36,6 +40,11 @@ function UserMessage({ message, bot_name }: Props) {
|
||||
reactions={message.reactions}
|
||||
/>
|
||||
) : null}
|
||||
<Row className="gap-3! items-center">
|
||||
<UserMessageCopy text={message.text || ""} />
|
||||
<UserMessageReply message={message} />
|
||||
<GroupTimestamp at={Number(message.created_at || 0)} />
|
||||
</Row>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import type { BotMessageWithAttachments } from "@/src/types";
|
||||
import copyableMessages from "./copyable-messages";
|
||||
|
||||
function message(text?: string): BotMessageWithAttachments {
|
||||
return { id: 1, author: "user", text } as BotMessageWithAttachments;
|
||||
}
|
||||
|
||||
describe("copyableMessages", () => {
|
||||
test("keeps the messages that have text", () => {
|
||||
const kept = copyableMessages({
|
||||
messages: [message("hello"), message(undefined), message("hi")],
|
||||
});
|
||||
|
||||
expect(kept.map((one) => one.text)).toEqual(["hello", "hi"]);
|
||||
});
|
||||
|
||||
test("drops text that is only whitespace", () => {
|
||||
expect(copyableMessages({ messages: [message(" ")] })).toEqual([]);
|
||||
});
|
||||
|
||||
test("a group with no text has nothing to copy", () => {
|
||||
expect(copyableMessages({ messages: [message(), message()] })).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { BotMessageWithAttachments } from "@/src/types";
|
||||
|
||||
/**
|
||||
* The messages of a group that can be copied: a file-only message has no
|
||||
* text, and its copy button renders nothing, so it would only leave a
|
||||
* gap in the row.
|
||||
*/
|
||||
export default function copyableMessages({
|
||||
messages,
|
||||
}: {
|
||||
messages: BotMessageWithAttachments[];
|
||||
}): BotMessageWithAttachments[] {
|
||||
return messages.filter((message) => Boolean(message.text?.trim()));
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabBotGraphicView from "@/src/functions/backend/db/bot-graphics/grab-bot-graphic-view";
|
||||
import { AdminHomePath } from "@/src/dict/admin-sections-dict";
|
||||
|
||||
type Params = Parameters<BunextPageServerFn<PagePropsType>>[0];
|
||||
|
||||
/**
|
||||
* A graphic's page: the graphic, for the user it was made for. Anyone
|
||||
* else, or a missing graphic, lands on the bot's conversation (which
|
||||
* leads on to the bots when that isn't theirs either).
|
||||
*/
|
||||
export default async function grabGraphicPageProps(params: Params) {
|
||||
const { user } = await userAuth({ req: params.req });
|
||||
|
||||
if (!user?.id) {
|
||||
return { redirect: { destination: "/auth/login" } };
|
||||
}
|
||||
|
||||
const bot_id = Number(params.query?.bot_id);
|
||||
const graphic_id = Number(params.query?.graphic_id);
|
||||
const graphic =
|
||||
Number.isInteger(graphic_id) && graphic_id > 0
|
||||
? await grabBotGraphicView({ graphic_id, user_id: user.id }).catch(() => null)
|
||||
: null;
|
||||
|
||||
if (!graphic) {
|
||||
return { redirect: { destination: bot_id ? `/admin/bots/${bot_id}` : AdminHomePath } };
|
||||
}
|
||||
|
||||
return { props: { graphic } };
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { useContext } from "react";
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import { MessagesSquare } from "lucide-react";
|
||||
import GraphicFrame from "@/src/components/general/graphic-frame";
|
||||
import SettingsPaneHeader from "@/src/pages/admin/settings/(partials)/settings-pane-header";
|
||||
import { AppContext } from "@/src/pages/__root";
|
||||
import conversationHref from "@/src/utils/conversation-href";
|
||||
import Center from "@moduletrace/twui/layout/Center";
|
||||
|
||||
/**
|
||||
* A graphic filling the window (the page has no rail), under a header
|
||||
* naming it and its bot, with a link back to the conversation it was made
|
||||
* in.
|
||||
*/
|
||||
export default function GraphicPageSection() {
|
||||
const { pageProps } = useContext(AppContext);
|
||||
const graphic = pageProps?.graphic;
|
||||
|
||||
if (!graphic) return null;
|
||||
|
||||
return (
|
||||
<Stack className="grow min-w-0 h-full gap-0! items-stretch">
|
||||
<SettingsPaneHeader
|
||||
title={graphic.title}
|
||||
description={`By ${graphic.bot_name}`}
|
||||
action={
|
||||
<Link
|
||||
href={conversationHref({
|
||||
bot_id: graphic.bot_id,
|
||||
workspace_id: graphic.workspace_id,
|
||||
session_id: graphic.session_id,
|
||||
})}
|
||||
title="Back to the conversation"
|
||||
aria-label="Back to the conversation"
|
||||
className="p-2 text-ink-secondary hover:text-ink rounded-default hover:bg-sunken"
|
||||
>
|
||||
<MessagesSquare size={16} aria-hidden />
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<Center>
|
||||
<Stack className="grow min-h-0 gap-0! items-stretch max-w-[1200px] w-full">
|
||||
<GraphicFrame
|
||||
graphic_id={graphic.id}
|
||||
title={graphic.title}
|
||||
version={graphic.version}
|
||||
className="grow h-full"
|
||||
/>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageServerFn } from "@moduletrace/bunext/types";
|
||||
import grabGraphicPageProps from "./(functions)/grab-graphic-page-props";
|
||||
|
||||
export const server: BunextPageServerFn<PagePropsType> = async (params) => {
|
||||
return await grabGraphicPageProps(params);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { PagePropsType } from "@/src/types";
|
||||
import type { BunextPageModuleMetaFn } from "@moduletrace/bunext/types";
|
||||
import { SiteData } from "@/src/data/site-data";
|
||||
import GraphicPageSection from "./(sections)/graphic-page-section";
|
||||
|
||||
export default function AdminBotGraphicPage() {
|
||||
return <GraphicPageSection />;
|
||||
}
|
||||
|
||||
export const meta: BunextPageModuleMetaFn = async (params) => {
|
||||
const graphic = (params.serverRes?.props as PagePropsType | undefined)?.graphic;
|
||||
|
||||
return {
|
||||
title: `${graphic?.title || "Graphic"} | ${SiteData["SiteName"]}`,
|
||||
description: graphic ? `A graphic by ${graphic.bot_name}.` : "A graphic a bot made.",
|
||||
};
|
||||
};
|
||||
@@ -85,6 +85,18 @@ The computer keeps the session, like everything else in its browser (extensions,
|
||||
3. Click **Hand back to** the bot. It gets a message that you're done and carries on.
|
||||
|
||||
A task that hit the wall stops and reports instead of waiting, so after you hand back the bot picks the work up again from the chat.`,
|
||||
},
|
||||
{
|
||||
id: "graphics",
|
||||
title: "A bot's graphic is blank or looks wrong",
|
||||
summary: "A chart or diagram in a message doesn't draw, or is missing data or images.",
|
||||
body: `Bots can answer with a graphic: a chart, diagram or visual report drawn in HTML and shown right in the message. Use the buttons on its title bar to open it large or on its own page.
|
||||
|
||||
Graphics are sandboxed: they can't reach the app or the internet, so nothing in one can read your conversation or send it anywhere. That also means a graphic can only use what's written into it, plus the chart and diagram libraries Cobble ships (Chart.js and Mermaid). If one is blank or missing pieces, it most likely tried to load something from the web (a CDN script, an online image or font).
|
||||
|
||||
Ask the bot to fix it, e.g. "the chart is blank, redraw it without outside files". It changes the same graphic, and every message showing it shows the new version. A graphic that tries to open another page is closed in place.
|
||||
|
||||
Only you see the graphics a bot made for you.`,
|
||||
},
|
||||
{
|
||||
id: "computer-not-ready",
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import userAuth from "@/src/functions/backend/auth/user-auth";
|
||||
import grabBotGraphicView from "@/src/functions/backend/db/bot-graphics/grab-bot-graphic-view";
|
||||
import { BotGraphicIdSchema } from "@/src/schemas/bot-graphic-schema";
|
||||
import type { BotGraphicView } from "@/src/types";
|
||||
import AppError from "@/src/utils/app-error";
|
||||
import parseWithSchema from "@/src/utils/parse-with-schema";
|
||||
import safeErrorMessage from "@/src/utils/safe-error-message";
|
||||
import type { APIResponseObject, BunextAPIRouteHandler } from "@moduletrace/bunext/types";
|
||||
|
||||
/**
|
||||
* `GET ?graphic_id=`: a graphic a bot made for the session user, as its
|
||||
* card names it. Never the HTML (`/graphics/<id>` serves that, sandboxed),
|
||||
* and nobody else reads it.
|
||||
*/
|
||||
export const handler: BunextAPIRouteHandler<APIResponseObject<BotGraphicView>> = async ({ req, query }) => {
|
||||
try {
|
||||
if (req.method != "GET") {
|
||||
throw new AppError(`Method not allowed`);
|
||||
}
|
||||
|
||||
const { user } = await userAuth({ req });
|
||||
|
||||
if (!user?.logged_in_status || !user.id) {
|
||||
return { success: false, msg: `Unauthorized`, logoutUser: true };
|
||||
}
|
||||
|
||||
const { graphic_id } = parseWithSchema({ schema: BotGraphicIdSchema, data: query });
|
||||
const view = await grabBotGraphicView({ graphic_id, user_id: user.id });
|
||||
|
||||
if (!view) throw new AppError(`This graphic isn't available.`);
|
||||
|
||||
return { success: true, singleRes: view };
|
||||
} catch (error: unknown) {
|
||||
return { success: false, msg: safeErrorMessage({ error }) };
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const BotGraphicIdSchema = z.object({
|
||||
graphic_id: z.coerce.number().int().positive("Pick a graphic"),
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import migrateBotTypes from "./functions/backend/db/bot-types/migrate-bot-types"
|
||||
import clearImplicitSessionLinks from "./functions/backend/db/bot-sessions/clear-implicit-session-links";
|
||||
import syncKnowledgebaseFts from "./functions/backend/db/knowledgebases/fts/sync-knowledgebase-fts";
|
||||
import handleComputerProxy from "./functions/server/computers/handle-computer-proxy";
|
||||
import handleGraphicsRequest from "./functions/server/graphics/handle-graphics-request";
|
||||
import handlePwaFile from "./functions/server/pwa/handle-pwa-file";
|
||||
import handleInstallIconFile from "./functions/server/pwa/handle-install-icon-file";
|
||||
import handleBuildInfo from "./functions/server/pwa/handle-build-info";
|
||||
@@ -106,6 +107,14 @@ const server = Bun.serve({
|
||||
|
||||
if (build_info) return build_info;
|
||||
|
||||
/**
|
||||
* Graphics open in a frame inside the app, never as a page of
|
||||
* it, so the loading page never stands in for one.
|
||||
*/
|
||||
const graphic = await handleGraphicsRequest({ req, url });
|
||||
|
||||
if (graphic) return await compressResponse({ req, response: graphic });
|
||||
|
||||
const loading_page = await handlePrecacheGate({ req, url });
|
||||
|
||||
if (loading_page) return loading_page;
|
||||
|
||||
@@ -653,6 +653,21 @@ export type PersonalSecret = {
|
||||
* A secret a bot asked the user for (\`[secret-request:ID]\`), as its chip
|
||||
* and card show it. Never holds a value.
|
||||
*/
|
||||
/**
|
||||
* A bot graphic (`[graphic:ID]`) as its card and page name it. The HTML
|
||||
* itself is only ever served at `/graphics/<id>`.
|
||||
*/
|
||||
export type BotGraphicView = {
|
||||
id: number;
|
||||
title: string;
|
||||
version: number;
|
||||
bot_id: number;
|
||||
bot_name: string;
|
||||
workspace_id: number | null;
|
||||
session_id: number | null;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
export type SecretRequestView = {
|
||||
id: number;
|
||||
key: string;
|
||||
@@ -1869,6 +1884,10 @@ export type PagePropsType = {
|
||||
* ones), for the files modal and the chips that name one.
|
||||
*/
|
||||
bot_files?: BotFileScopesPayload | null;
|
||||
/**
|
||||
* On a graphic's own page: the graphic (never its HTML).
|
||||
*/
|
||||
graphic?: BotGraphicView | null;
|
||||
/**
|
||||
* Whether the open conversation's mic (transcription) and read-aloud
|
||||
* (speech) are turned on for the bot there.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { runGraphicsSql, viewerGraphicsSql } from "./bot-graphic-sql";
|
||||
import type { SqlCondition } from "./crew-access/types";
|
||||
|
||||
const db = new Database(":memory:");
|
||||
|
||||
beforeAll(() => {
|
||||
db.run(`CREATE TABLE bot_graphics (id INTEGER PRIMARY KEY, user_id INTEGER, bot_id INTEGER)`);
|
||||
db.run(`INSERT INTO bot_graphics VALUES (1, 7, 2), (2, 7, 3), (3, 9, 2)`);
|
||||
});
|
||||
|
||||
function ids({ condition }: { condition: SqlCondition }) {
|
||||
return db
|
||||
.query(`SELECT g.id FROM bot_graphics g WHERE ${condition.sql} ORDER BY g.id`)
|
||||
.all(...condition.values)
|
||||
.map((row) => (row as { id: number }).id);
|
||||
}
|
||||
|
||||
describe("viewerGraphicsSql", () => {
|
||||
test("a user sees only the graphics made for them", () => {
|
||||
expect(ids({ condition: viewerGraphicsSql({ user_id: 7 }) })).toEqual([1, 2]);
|
||||
expect(ids({ condition: viewerGraphicsSql({ user_id: 9 }) })).toEqual([3]);
|
||||
expect(ids({ condition: viewerGraphicsSql({ user_id: 1 }) })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runGraphicsSql", () => {
|
||||
test("a run edits only its bot's graphics for its user", () => {
|
||||
expect(ids({ condition: runGraphicsSql({ user_id: 7, bot_id: 2 }) })).toEqual([1]);
|
||||
expect(ids({ condition: runGraphicsSql({ user_id: 9, bot_id: 3 }) })).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { SqlCondition } from "./crew-access/types";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The graphics table's alias in the query.
|
||||
*/
|
||||
alias?: string;
|
||||
user_id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphics a user sees: only those a bot made for them, like the
|
||||
* conversation they were made in. Nobody else, crew managers included,
|
||||
* reads another user's graphic.
|
||||
*/
|
||||
export function viewerGraphicsSql({ alias = "g", user_id }: Params): SqlCondition {
|
||||
return { sql: `${alias}.user_id = ?`, values: [Number(user_id)] };
|
||||
}
|
||||
|
||||
/**
|
||||
* The graphics a run edits: those its bot made for its user.
|
||||
*/
|
||||
export function runGraphicsSql({ alias = "g", user_id, bot_id }: Params & { bot_id: number | string }): SqlCondition {
|
||||
return { sql: `${alias}.user_id = ? AND ${alias}.bot_id = ?`, values: [Number(user_id), Number(bot_id)] };
|
||||
}
|
||||
@@ -20,6 +20,10 @@ describe("exportMessageText", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("words graphics by number", () => {
|
||||
expect(exportMessageText({ text: "See [graphic:5]", ...names, heading_offset: 0 })).toBe("See (graphic 5)");
|
||||
});
|
||||
|
||||
test("leaves refs that are already links alone", () => {
|
||||
expect(exportMessageText({ text: "[bot:3](#x)", ...names, heading_offset: 0 })).toBe("[bot:3](#x)");
|
||||
});
|
||||
|
||||
@@ -15,8 +15,8 @@ type Params = {
|
||||
/**
|
||||
* A message's markdown as it reads outside the app: its refs in words
|
||||
* (`[bot:3]` as `@Ada`, `[workspace:2]` as `#Launch`, `[computer:3]` as
|
||||
* "computer 3", a folder as its path, a secret by number only, never its
|
||||
* value) and its headings dropped by
|
||||
* "computer 3", a folder as its path, a graphic and a secret by number only, never
|
||||
* a secret's value) and its headings dropped by
|
||||
* `heading_offset`. Code blocks are left exactly as they are.
|
||||
*/
|
||||
export default function exportMessageText({
|
||||
@@ -85,6 +85,7 @@ function wordRefs({
|
||||
.replace(/\[hook-proposal:(\d+)\](?!\()/g, "hook proposal $1")
|
||||
.replace(/\[secret:(\d+)\](?!\()/g, "(secret $1, hidden)")
|
||||
.replace(/\[secret-request:(\d+)\](?!\()/g, "(secret request $1)")
|
||||
.replace(/\[graphic:(\d+)\](?!\()/g, "(graphic $1)")
|
||||
.replace(/\[file:(\d+)\](?!\()/g, "file $1")
|
||||
.replace(/\[folder:[a-z]+:\d+:([^\]\n]+)\](?!\()/g, "`$1`")
|
||||
.replace(/\[folder:([a-z]+):(\d+)\](?!\()/g, "the $1 $2 folder");
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import graphicDocument from "./graphic-document";
|
||||
|
||||
const vars = {
|
||||
light: { "--c-paper": "#fff" },
|
||||
dark: { "--c-paper": "#000" },
|
||||
fonts: { "--font-sans": "sans-serif" },
|
||||
};
|
||||
|
||||
describe("graphicDocument", () => {
|
||||
test("wraps a fragment in a themed document", () => {
|
||||
const doc = graphicDocument({ html: "<svg></svg>", title: "A <b>", vars });
|
||||
|
||||
expect(doc.startsWith("<!DOCTYPE html><html><head>")).toBe(true);
|
||||
expect(doc).toContain("<title>A <b></title>");
|
||||
expect(doc).toContain("--c-paper:#fff;");
|
||||
expect(doc).toContain(":root.dark{color-scheme:dark;--c-paper:#000;}");
|
||||
expect(doc).toContain("<body><svg></svg><script>");
|
||||
expect(doc).toContain("cobble:graphic-size");
|
||||
});
|
||||
|
||||
test("styles scrollbars in the theme's colours", () => {
|
||||
const doc = graphicDocument({ html: "<p>x</p>", title: "T", vars });
|
||||
|
||||
expect(doc).toContain("::-webkit-scrollbar-thumb{background-color:var(--c-border-strong);");
|
||||
expect(doc).toContain("scrollbar-color:var(--c-border-strong) transparent;");
|
||||
});
|
||||
|
||||
test("puts the theme ahead of a whole document's own styles", () => {
|
||||
const doc = graphicDocument({
|
||||
html: `<!doctype html><html lang="en"><head><style>body{color:red}</style></head><body><p>Hi</p></body></html>`,
|
||||
title: "T",
|
||||
dark: true,
|
||||
vars,
|
||||
});
|
||||
|
||||
expect(doc.indexOf("--c-paper")).toBeLessThan(doc.indexOf("body{color:red}"));
|
||||
expect(doc).toContain(`classList.toggle("dark", true)`);
|
||||
expect(doc.indexOf("cobble:graphic-size")).toBeLessThan(doc.lastIndexOf("</body>"));
|
||||
expect(doc.match(/<!doctype/gi)?.length).toBe(1);
|
||||
});
|
||||
|
||||
test("gives a document without a head one", () => {
|
||||
const doc = graphicDocument({ html: `<html><body>x</body></html>`, title: "T", vars });
|
||||
|
||||
expect(doc).toContain(`<html><head><meta charset="utf-8">`);
|
||||
});
|
||||
|
||||
test("drops a refresh that would navigate the frame away", () => {
|
||||
const doc = graphicDocument({
|
||||
html: `<head><meta http-equiv="refresh" content="0;url=https://evil.test/?d=1"></head>`,
|
||||
title: "T",
|
||||
vars,
|
||||
});
|
||||
|
||||
expect(doc).not.toContain("evil.test");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { GraphicSizeMessageType } from "@/src/dict/graphics-dict";
|
||||
import type { ThemeVars } from "./parse-theme-vars";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The graphic as the bot wrote it: a whole document or a fragment.
|
||||
*/
|
||||
html: string;
|
||||
title: string;
|
||||
dark?: boolean;
|
||||
vars: ThemeVars;
|
||||
};
|
||||
|
||||
/**
|
||||
* A bot graphic as it's served: the app's theme colours and fonts set
|
||||
* ahead of the bot's own styles (as `--c-paper`, `--c-ink`, ... and
|
||||
* `--font-sans`), `dark` on the root in the dark theme, and a script that
|
||||
* tells the thread how tall it is. A fragment is wrapped into a document;
|
||||
* a refresh tag (which would navigate the frame away) is dropped.
|
||||
*/
|
||||
export default function graphicDocument({ html, title, dark, vars }: Params) {
|
||||
const head = `<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>${escapeText(title)}</title><style>${themeCss({ vars })}</style><script>document.documentElement.classList.toggle("dark", ${dark ? "true" : "false"});</script>`;
|
||||
const tail = `<script>${SizeScript}</script>`;
|
||||
const body = html.replace(/<meta\b[^>]*http-equiv\s*=\s*["']?refresh[^>]*>/gi, "");
|
||||
|
||||
const with_head = /<head\b[^>]*>/i.test(body)
|
||||
? body.replace(/<head\b[^>]*>/i, (tag) => `${tag}${head}`)
|
||||
: /<html\b[^>]*>/i.test(body)
|
||||
? body.replace(/<html\b[^>]*>/i, (tag) => `${tag}<head>${head}</head>`)
|
||||
: `<!DOCTYPE html><html><head>${head}</head><body>${body}</body></html>`;
|
||||
|
||||
const last_body_end = with_head.toLowerCase().lastIndexOf("</body>");
|
||||
|
||||
const document =
|
||||
last_body_end >= 0
|
||||
? `${with_head.slice(0, last_body_end)}${tail}${with_head.slice(last_body_end)}`
|
||||
: `${with_head}${tail}`;
|
||||
|
||||
return /^\s*<!doctype/i.test(document) ? document : `<!DOCTYPE html>${document}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the body's height to the page framing it whenever it changes.
|
||||
*/
|
||||
const SizeScript = `(function(){var last=0;function post(){var b=document.body;if(!b)return;var h=Math.ceil(b.scrollHeight);if(h==last)return;last=h;parent.postMessage({type:${JSON.stringify(GraphicSizeMessageType)},height:h},"*");}if(window.ResizeObserver&&document.body){new ResizeObserver(post).observe(document.body);}addEventListener("load",post);post();})();`;
|
||||
|
||||
/**
|
||||
* Thin scrollbars in the theme's colours, for the graphic and anything in
|
||||
* it that scrolls: a rounded thumb inset in a clear track where the
|
||||
* browser takes `::-webkit-scrollbar` (Chrome, Safari), the standard
|
||||
* properties elsewhere (Firefox), since Chrome drops the rounded thumb
|
||||
* once those are set.
|
||||
*/
|
||||
const ScrollbarCss = [
|
||||
`::-webkit-scrollbar{width:10px;height:10px;}`,
|
||||
`::-webkit-scrollbar-track,::-webkit-scrollbar-corner{background:transparent;}`,
|
||||
`::-webkit-scrollbar-thumb{background-color:var(--c-border-strong);border:3px solid transparent;border-radius:999px;background-clip:padding-box;}`,
|
||||
`::-webkit-scrollbar-thumb:hover{background-color:var(--c-ink-secondary);}`,
|
||||
`@supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:var(--c-border-strong) transparent;}}`,
|
||||
].join("");
|
||||
|
||||
function themeCss({ vars }: { vars: ThemeVars }) {
|
||||
const list = (record: Record<string, string>) =>
|
||||
Object.entries(record)
|
||||
.map(([name, value]) => `${name}:${value};`)
|
||||
.join("");
|
||||
|
||||
return [
|
||||
`:root{color-scheme:light;${list(vars.light)}${list(vars.fonts)}}`,
|
||||
`:root.dark{color-scheme:dark;${list(vars.dark)}}`,
|
||||
`html,body{margin:0;}`,
|
||||
`body{padding:16px;background:var(--c-paper);color:var(--c-ink);font-family:var(--font-sans);font-size:14px;line-height:1.5;}`,
|
||||
ScrollbarCss,
|
||||
].join("");
|
||||
}
|
||||
|
||||
function escapeText(text: string) {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import graphicHeaders from "./graphic-headers";
|
||||
|
||||
describe("graphicHeaders", () => {
|
||||
const csp = graphicHeaders({ origin: "https://app.test" })["Content-Security-Policy"]!;
|
||||
const directives = csp.split("; ");
|
||||
|
||||
test("sandboxes scripts away from the app's origin", () => {
|
||||
expect(directives).toContain("sandbox allow-scripts");
|
||||
expect(csp).not.toContain("allow-same-origin");
|
||||
expect(csp).not.toContain("allow-top-navigation");
|
||||
expect(csp).not.toContain("allow-popups");
|
||||
});
|
||||
|
||||
test("allows no network beyond the app's graphic libraries", () => {
|
||||
expect(directives).toContain("default-src 'none'");
|
||||
expect(directives).toContain("connect-src 'none'");
|
||||
expect(directives).toContain("script-src 'unsafe-inline' https://app.test/graphics-lib/");
|
||||
expect(directives).toContain("img-src data: blob:");
|
||||
});
|
||||
|
||||
test("is only framed by the app and never cached", () => {
|
||||
const headers = graphicHeaders({ origin: "https://app.test" });
|
||||
|
||||
expect(directives).toContain("frame-ancestors 'self'");
|
||||
expect(headers["Cache-Control"]).toBe("private, no-store");
|
||||
expect(headers["X-Content-Type-Options"]).toBe("nosniff");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { GraphicsLibPrefix } from "@/src/dict/graphics-dict";
|
||||
|
||||
type Params = {
|
||||
/**
|
||||
* The app's origin as the browser reached it, e.g.
|
||||
* `https://cobble.example.com`: the only place a graphic loads scripts
|
||||
* from, and only its libraries.
|
||||
*/
|
||||
origin: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The headers a bot graphic is served with. Its HTML is a bot's and may
|
||||
* carry what a prompt injection put there, so it gets:
|
||||
*
|
||||
* - `sandbox allow-scripts` without `allow-same-origin`: scripts run (for
|
||||
* charts) but under an origin of its own, so no cookie, storage or API of
|
||||
* the app's is reachable.
|
||||
* - no network (`default-src 'none'`, `connect-src 'none'`): nothing it
|
||||
* holds can be sent anywhere, and only the app's graphic libraries load.
|
||||
* - framed only by the app (`frame-ancestors 'self'`), never cached.
|
||||
*/
|
||||
export default function graphicHeaders({ origin }: Params): Record<string, string> {
|
||||
return {
|
||||
"Content-Type": "text/html; charset=utf-8",
|
||||
"Content-Security-Policy": [
|
||||
"sandbox allow-scripts",
|
||||
"default-src 'none'",
|
||||
`script-src 'unsafe-inline' ${origin}${GraphicsLibPrefix}/`,
|
||||
"style-src 'unsafe-inline'",
|
||||
"img-src data: blob:",
|
||||
"font-src data:",
|
||||
"connect-src 'none'",
|
||||
"form-action 'none'",
|
||||
"base-uri 'none'",
|
||||
"frame-ancestors 'self'",
|
||||
].join("; "),
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Cache-Control": "private, no-store",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import graphicRef, { graphicRefId } from "./graphic-ref";
|
||||
import linkComputerRefs from "./link-computer-refs";
|
||||
|
||||
describe("graphicRef", () => {
|
||||
test("names a graphic", () => {
|
||||
expect(graphicRef({ id: 5 })).toBe("[graphic:5]");
|
||||
});
|
||||
|
||||
test("reads the id back from its link", () => {
|
||||
expect(graphicRefId({ href: "#graphic-5" })).toBe(5);
|
||||
expect(graphicRefId({ href: "#graphic-0" })).toBeNull();
|
||||
expect(graphicRefId({ href: "#graphic-x" })).toBeNull();
|
||||
expect(graphicRefId({ href: "#file-5" })).toBeNull();
|
||||
expect(graphicRefId({ href: null })).toBeNull();
|
||||
});
|
||||
|
||||
test("becomes a link in a message", () => {
|
||||
expect(linkComputerRefs({ text: "Here: [graphic:5]" })).toBe("Here: [graphic 5](#graphic-5)");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
type Params = {
|
||||
id: number | string;
|
||||
};
|
||||
|
||||
/**
|
||||
* How a bot names a graphic it made: `[graphic:5]`. The chat turns it
|
||||
* into a link (`linkComputerRefs`) that renders as the graphic itself,
|
||||
* framed in the thread, with ways to open it full size.
|
||||
*/
|
||||
export default function graphicRef({ id }: Params) {
|
||||
return `[graphic:${id}]`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The link target a graphic ref becomes, e.g. `#graphic-5`.
|
||||
*/
|
||||
export const GraphicRefHrefPrefix = "#graphic-";
|
||||
|
||||
/**
|
||||
* The id in a graphic ref's link target, or null.
|
||||
*/
|
||||
export function graphicRefId({ href }: { href?: string | null }) {
|
||||
if (!href?.startsWith(GraphicRefHrefPrefix)) return null;
|
||||
|
||||
const id = Number(href.slice(GraphicRefHrefPrefix.length));
|
||||
|
||||
return Number.isInteger(id) && id > 0 ? id : null;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { GraphicsPrefix } from "@/src/dict/graphics-dict";
|
||||
|
||||
type Params = {
|
||||
graphic_id: number;
|
||||
dark?: boolean;
|
||||
/**
|
||||
* The version shown, so a changed graphic loads afresh.
|
||||
*/
|
||||
version?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Where a graphic's sandboxed document is served, in a theme.
|
||||
*/
|
||||
export default function graphicUrl({ graphic_id, dark, version }: Params) {
|
||||
const query = new URLSearchParams({ theme: dark ? "dark" : "light" });
|
||||
|
||||
if (version) query.set("v", String(version));
|
||||
|
||||
return `${GraphicsPrefix}/${graphic_id}?${query.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The app page that shows a graphic full size, beside the rail.
|
||||
*/
|
||||
export function graphicPageHref({ graphic_id, bot_id }: { graphic_id: number; bot_id: number }) {
|
||||
return `/admin/bots/${bot_id}/graphics/${graphic_id}`;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GraphicRefHrefPrefix } from "./graphic-ref";
|
||||
|
||||
/**
|
||||
* The link target a computer ref (`computerRef`) becomes, e.g.
|
||||
* `#computer-3`. The chat's markdown renders these links as buttons that
|
||||
@@ -70,7 +72,8 @@ export const SecretRequestRefHrefPrefix = "#secret-request-";
|
||||
* (`[workspace:2]`), file ref (`[file:12]`), folder ref
|
||||
* (`[folder:computer:3:seo/raw]`), proposal ref (`[proposal:7]`) and hook
|
||||
* proposal ref (`[hook-proposal:4]`), secret ref (`[secret:12]`) and secret
|
||||
* request ref (`[secret-request:4]`) in a message into a markdown link, so it
|
||||
* request ref (`[secret-request:4]`) and graphic ref (`[graphic:5]`) in a
|
||||
* message into a markdown link, so it
|
||||
* survives markdown rendering as a link the chat can swap for a button,
|
||||
* chip or download.
|
||||
*
|
||||
@@ -106,6 +109,7 @@ export default function linkComputerRefs({ text }: { text: string }) {
|
||||
/\[secret-request:(\d+)\](?!\()/g,
|
||||
(_, id: string) => `[secret request ${id}](${SecretRequestRefHrefPrefix}${id})`,
|
||||
)
|
||||
.replace(/\[graphic:(\d+)\](?!\()/g, (_, id: string) => `[graphic ${id}](${GraphicRefHrefPrefix}${id})`)
|
||||
.replace(/\[file:(\d+)\](?!\()/g, (_, id: string) => `[file ${id}](${FileRefHrefPrefix}${id})`)
|
||||
.replace(/\[folder:([a-z]+):(\d+):([^\]\n]+)\](?!\()/g, (_, volume: string, owner: string, path: string) =>
|
||||
`[${path}](${FolderRefHrefPrefix}${encodeURIComponent(`${volume}:${owner}:${path}`)})`,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Paths the loading page never stands in for: the app's code, live data,
|
||||
* media, proxied computers and the files the loading page itself needs.
|
||||
* media, proxied computers, bot graphics and the files the loading page
|
||||
* itself needs.
|
||||
*/
|
||||
const OpenPrefixes = ["/.bunext/", "/api/", "/ws", "/media", "/computers/"];
|
||||
const OpenPrefixes = ["/.bunext/", "/api/", "/ws", "/media", "/computers/", "/graphics/", "/graphics-lib/"];
|
||||
|
||||
type Params = {
|
||||
method: string;
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import parseThemeVars from "./parse-theme-vars";
|
||||
|
||||
describe("parseThemeVars", () => {
|
||||
test("reads both schemes and the fonts", () => {
|
||||
const css = `:root {\n --c-paper: #fff;\n --m-fast: 1ms;\n}\n\n.dark {\n --c-paper: #000;\n}\n\n@theme inline {\n --font-sans:\n "A", sans-serif;\n --color-ink: var(--c-ink);\n}\n`;
|
||||
|
||||
expect(parseThemeVars({ css })).toEqual({
|
||||
light: { "--c-paper": "#fff" },
|
||||
dark: { "--c-paper": "#000" },
|
||||
fonts: { "--font-sans": `"A", sans-serif` },
|
||||
});
|
||||
});
|
||||
|
||||
test("finds the app's own tokens", () => {
|
||||
const css = fs.readFileSync(path.join(process.cwd(), "src/styles/main.css"), "utf-8");
|
||||
const vars = parseThemeVars({ css });
|
||||
|
||||
for (const name of ["--c-paper", "--c-ink", "--c-border"]) {
|
||||
expect(vars.light[name]).toBeTruthy();
|
||||
expect(vars.dark[name]).toBeTruthy();
|
||||
expect(vars.light[name]).not.toBe(vars.dark[name]);
|
||||
}
|
||||
|
||||
expect(vars.fonts["--font-sans"]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
export type ThemeVars = {
|
||||
/**
|
||||
* The `:root` block's `--c-*` colours, keyed with their `--` names.
|
||||
*/
|
||||
light: Record<string, string>;
|
||||
/**
|
||||
* The `.dark` block's `--c-*` colours.
|
||||
*/
|
||||
dark: Record<string, string>;
|
||||
/**
|
||||
* `--font-sans` and `--font-mono` from the `@theme inline` block.
|
||||
*/
|
||||
fonts: Record<string, string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* The app's neutral colours (light and dark) and font stacks from
|
||||
* `src/styles/main.css`, for documents served outside the app's own
|
||||
* styles (a bot graphic), so they match the theme without a copy of it.
|
||||
*/
|
||||
export default function parseThemeVars({ css }: { css: string }): ThemeVars {
|
||||
return {
|
||||
light: blockVars({ block: css.match(/(?:^|\n):root\s*\{([\s\S]*?)\n\}/)?.[1], prefix: "--c-" }),
|
||||
dark: blockVars({ block: css.match(/(?:^|\n)\.dark\s*\{([\s\S]*?)\n\}/)?.[1], prefix: "--c-" }),
|
||||
fonts: blockVars({ block: css.match(/@theme\s+inline\s*\{([\s\S]*?)\n\}/)?.[1], prefix: "--font-" }),
|
||||
};
|
||||
}
|
||||
|
||||
function blockVars({ block, prefix }: { block?: string; prefix: string }) {
|
||||
const vars: Record<string, string> = {};
|
||||
|
||||
if (!block) return vars;
|
||||
|
||||
for (const match of block.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;{}]+);/g)) {
|
||||
const [, name, value] = match;
|
||||
|
||||
if (name?.startsWith(prefix) && value) vars[name] = value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
return vars;
|
||||
}
|
||||
Reference in New Issue
Block a user