@bongos/core 1.23.21 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +983 -428
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +6 -3
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +94 -0
- package/clients/bongos-client/index.cjs +94 -0
- package/clients/bongos-client/index.d.ts +154 -4
- package/clients/bongos-client/index.mjs +94 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +10 -1
- package/docs/api/openapi.json +8619 -5885
- package/docs/api-reference.md +68 -9
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +4 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +55 -6
- package/docs/page-readings.json +1185 -1011
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/CLAUDE.md +15 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/module.json +26 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +252 -0
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +108 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +50 -18
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/grading/grader-subagent.js +30 -11
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +156 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/city.html +15 -15
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +24 -0
- package/modules/lifecycle/db-analytics.js +10 -3
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +48 -18
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/db-versions.js +21 -8
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/onboarding/howto-moments.js +8 -1
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +8 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +286 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +4 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +129 -12
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +16 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/src/platform-server.js +27 -1
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/batched_writes_parity.mjs +340 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/conductor_main_e2e.mjs +4 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/per-row-loops-legacy.cjs +245 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_archive_disposition.mjs +5 -5
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +146 -23
- package/tests/grader.mjs +31 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_city.mjs +8 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/module_skills_hook.mjs +69 -0
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/public_landing_projects.mjs +37 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +58 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_close_route.mjs +1 -1
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
// modules/hall-ui/public/ai-studio.js — the studio's AI pages,
|
|
2
|
+
// /builders/ai-studio (task 1004921, goal 1000136, ADR 0392).
|
|
3
|
+
//
|
|
4
|
+
// Built 1:1 on the owner's pick, option C of the AI Studio options canvas
|
|
5
|
+
// ("Three steps: pick, describe, make"), with the owner's addition (2026-10-29,
|
|
6
|
+
// "when the image is made it needs the option to edit/update the prompt"): the
|
|
7
|
+
// results page carries the request in an editable box, and "Make it again"
|
|
8
|
+
// sends the changed words on the same files, paid the same way.
|
|
9
|
+
//
|
|
10
|
+
// One page, four views, each a full page in the studio's room (routeOf in
|
|
11
|
+
// ai-studio-lib.js reads the address):
|
|
12
|
+
// ?step=pick What are you starting from? Each file uploads as it is
|
|
13
|
+
// picked, one per call (POST /ai-studio/uploads).
|
|
14
|
+
// ?step=describe What should it make?
|
|
15
|
+
// ?step=make How do you want to pay? A card press sends the request
|
|
16
|
+
// (POST /ai-studio/requests) and opens its page.
|
|
17
|
+
// ?request=N the waiting page (GET /ai-studio/requests/N, polled), then
|
|
18
|
+
// the result: the pictures, the reply, the request to change
|
|
19
|
+
// and make again, and the receipt.
|
|
20
|
+
// ?view=balance what is left, add money (GET /ai-studio/topup/quote, POST
|
|
21
|
+
// /ai-studio/topup), the month and every charge and refund.
|
|
22
|
+
//
|
|
23
|
+
// The draft (the picked files and the words) lives in sessionStorage, so the
|
|
24
|
+
// three steps survive a reload and the back button. A key is NEVER asked for on
|
|
25
|
+
// this page; the "add your own key" line links to Settings, the only place one
|
|
26
|
+
// is entered.
|
|
27
|
+
//
|
|
28
|
+
// A file can go with one request only (the server's rule), so "Make it again"
|
|
29
|
+
// and "Change files" carry the earlier request's uploads by their address and
|
|
30
|
+
// upload a fresh copy of each when the new request is sent.
|
|
31
|
+
|
|
32
|
+
(() => {
|
|
33
|
+
'use strict';
|
|
34
|
+
|
|
35
|
+
const API = '/api/bongos';
|
|
36
|
+
const lib = window.OTBAiStudioLib;
|
|
37
|
+
const studioLib = window.OTBStudioLib;
|
|
38
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
39
|
+
const toast = window.OTB.toast;
|
|
40
|
+
const esc = window.OTB.escapeHtml;
|
|
41
|
+
|
|
42
|
+
// The only writes this page makes, pinned in tests/hall_ai_studio.mjs. None
|
|
43
|
+
// of them carries a key.
|
|
44
|
+
const WRITE_ROUTES = Object.freeze({
|
|
45
|
+
upload: () => `${API}/ai-studio/uploads`,
|
|
46
|
+
request: () => `${API}/ai-studio/requests`,
|
|
47
|
+
topup: () => `${API}/ai-studio/topup`,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const DRAFT_KEY = 'otb.ai-studio.draft';
|
|
51
|
+
const POLL_MS = 2500;
|
|
52
|
+
const SETTINGS_KEYS = '/builders/settings#ai-keys';
|
|
53
|
+
|
|
54
|
+
const page = document.getElementById('ais-page');
|
|
55
|
+
const main = document.getElementById('hall-main');
|
|
56
|
+
const fileInput = document.getElementById('ais-file');
|
|
57
|
+
|
|
58
|
+
const state = {
|
|
59
|
+
signedIn: false,
|
|
60
|
+
off: false,
|
|
61
|
+
keys: null,
|
|
62
|
+
balance: null,
|
|
63
|
+
route: null,
|
|
64
|
+
draft: readDraft(),
|
|
65
|
+
request: null,
|
|
66
|
+
pick: 0, // which picture is shown big on the result
|
|
67
|
+
timers: [],
|
|
68
|
+
busy: false,
|
|
69
|
+
topup: { amount: 20, other: false, quote: null },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ---- the draft ------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
function blankDraft() { return { files: [], prompt: '', from: null }; }
|
|
75
|
+
function readDraft() {
|
|
76
|
+
try {
|
|
77
|
+
const d = JSON.parse(sessionStorage.getItem(DRAFT_KEY) || 'null');
|
|
78
|
+
if (d && Array.isArray(d.files)) return { files: d.files.filter((f) => f && f.url), prompt: String(d.prompt || ''), from: d.from || null };
|
|
79
|
+
} catch (_) { /* private mode, or nothing saved */ }
|
|
80
|
+
return blankDraft();
|
|
81
|
+
}
|
|
82
|
+
function saveDraft() {
|
|
83
|
+
try {
|
|
84
|
+
const keep = { ...state.draft, files: state.draft.files.filter((f) => f.status === 'ready') };
|
|
85
|
+
sessionStorage.setItem(DRAFT_KEY, JSON.stringify(keep));
|
|
86
|
+
} catch (_) { /* the draft then lives only as long as the page */ }
|
|
87
|
+
}
|
|
88
|
+
function clearDraft() { state.draft = blankDraft(); saveDraft(); }
|
|
89
|
+
function readyFiles() { return state.draft.files.filter((f) => f.status === 'ready'); }
|
|
90
|
+
|
|
91
|
+
// ---- navigation -----------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
function go(search, { replace = false } = {}) {
|
|
94
|
+
const url = `/builders/ai-studio${search ? `?${search}` : ''}`;
|
|
95
|
+
if (replace) history.replaceState(null, '', url); else history.pushState(null, '', url);
|
|
96
|
+
render({ focus: true });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function stopTimers() { state.timers.forEach((t) => clearInterval(t)); state.timers = []; }
|
|
100
|
+
|
|
101
|
+
function focusHead() {
|
|
102
|
+
const h = page.querySelector('[data-head]');
|
|
103
|
+
if (h) h.focus({ preventScroll: false });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function render({ focus = false } = {}) {
|
|
107
|
+
stopTimers();
|
|
108
|
+
main.removeAttribute('data-making');
|
|
109
|
+
state.route = lib.routeOf(location.search);
|
|
110
|
+
main.setAttribute('data-view', state.route.view === 'step' ? state.route.step : state.route.view);
|
|
111
|
+
if (!state.signedIn) { paintNote('Sign in to make something with AI.'); return; }
|
|
112
|
+
if (state.off) { paintNote('Making things with AI is not switched on for this project.'); return; }
|
|
113
|
+
if (state.route.view === 'balance') await paintBalance();
|
|
114
|
+
else if (state.route.view === 'request') await openRequest(state.route.id);
|
|
115
|
+
else await paintStep(state.route.step, state.route.from);
|
|
116
|
+
if (focus) focusHead();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function backLink() {
|
|
120
|
+
return '<a class="st-doc__back ais-back" href="/builders/studio">← Studio</a>';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function paintNote(text) {
|
|
124
|
+
page.innerHTML = `${backLink()}<p class="ais-quiet ais-note" data-head tabindex="-1">${esc(text)}</p>`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- the three steps ------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
function stepHead(step) {
|
|
130
|
+
const marks = lib.stepMarks(step).map((m) => `<span class="ais-mark ais-mark--${m.state}">${esc(m.text)}</span>`).join('');
|
|
131
|
+
return `<div class="ais-bar">${backLink()}<span class="ais-marks">${marks}</span></div>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function thumb(f, big) {
|
|
135
|
+
const cls = `ais-thumb${big ? ' ais-thumb--big' : ''}`;
|
|
136
|
+
if (lib.isImage(f.content_type) && f.url) return `<img class="${cls}" src="${esc(f.url)}" alt="">`;
|
|
137
|
+
return `<span class="${cls} ais-thumb--pdf" aria-hidden="true">PDF</span>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function paintStep(step, from) {
|
|
141
|
+
if (from && state.draft.from !== from) await carryFrom(from);
|
|
142
|
+
const files = readyFiles();
|
|
143
|
+
if (step !== 'pick' && !files.length) { go('step=pick', { replace: true }); return; }
|
|
144
|
+
if (step === 'make' && !state.draft.prompt.trim()) { go('step=describe', { replace: true }); return; }
|
|
145
|
+
if (step === 'pick') paintPick();
|
|
146
|
+
else if (step === 'describe') paintDescribe();
|
|
147
|
+
else await paintMake();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// "Change files" from a result: that request's uploads, carried by address,
|
|
151
|
+
// and its words, so the steps open where the artist left them.
|
|
152
|
+
async function carryFrom(id) {
|
|
153
|
+
const res = await api.request('GET', `${API}/ai-studio/requests/${id}`);
|
|
154
|
+
if (!res.ok || !res.data || !res.data.request) return;
|
|
155
|
+
const r = res.data.request;
|
|
156
|
+
const prompt = state.draft.from === null && state.draft.prompt ? state.draft.prompt : r.prompt;
|
|
157
|
+
state.draft = {
|
|
158
|
+
files: (r.files || []).filter((f) => f.role === 'upload').map((f) => ({
|
|
159
|
+
id: null, url: f.url, name: f.name, size_bytes: f.size_bytes, content_type: f.content_type, status: 'ready', carried: true,
|
|
160
|
+
})),
|
|
161
|
+
prompt: String(prompt || ''),
|
|
162
|
+
from: id,
|
|
163
|
+
};
|
|
164
|
+
saveDraft();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function paintPick() {
|
|
168
|
+
const files = state.draft.files;
|
|
169
|
+
const rows = files.map((f, i) => {
|
|
170
|
+
const right = f.status === 'uploading' ? '<span class="ais-dim">uploading…</span>'
|
|
171
|
+
: f.status === 'error' ? `<span class="ais-err-inline">${esc(f.error || 'could not be sent')}</span>`
|
|
172
|
+
: `<span class="ais-dim">${esc(lib.sizeText(f.size_bytes))}</span>`;
|
|
173
|
+
return `<li class="ais-row">${thumb(f)}<span class="ais-grow">${esc(f.name || 'file')}</span>${right}<button type="button" class="ais-remove" data-remove="${i}">remove</button></li>`;
|
|
174
|
+
}).join('');
|
|
175
|
+
const ready = readyFiles().length;
|
|
176
|
+
const uploading = files.some((f) => f.status === 'uploading');
|
|
177
|
+
page.innerHTML = `${stepHead('pick')}
|
|
178
|
+
<h1 class="ais-ask" data-head tabindex="-1">What are you starting from?</h1>
|
|
179
|
+
<div class="ais-drop" id="ais-drop">
|
|
180
|
+
<span>Drop sketches, pictures or a PDF here, or <button type="button" class="ais-link" id="ais-choose">choose files</button></span>
|
|
181
|
+
<span class="ais-dim">Up to ${lib.MAX_FILES}. Pictures up to 4 MB, PDFs up to 8 MB.</span>
|
|
182
|
+
</div>
|
|
183
|
+
${rows ? `<ul class="ais-rows">${rows}</ul>` : ''}
|
|
184
|
+
<div class="ais-go">
|
|
185
|
+
<button type="button" class="ais-btn" id="ais-next" ${ready && !uploading ? '' : 'disabled'}>Next</button>
|
|
186
|
+
<span class="ais-faint">${ready ? `${ready} ${ready === 1 ? 'file' : 'files'} ready` : ''}</span>
|
|
187
|
+
</div>`;
|
|
188
|
+
page.querySelector('#ais-choose').addEventListener('click', () => fileInput.click());
|
|
189
|
+
const drop = page.querySelector('#ais-drop');
|
|
190
|
+
drop.addEventListener('dragover', (ev) => { ev.preventDefault(); drop.classList.add('ais-drop--over'); });
|
|
191
|
+
drop.addEventListener('dragleave', () => drop.classList.remove('ais-drop--over'));
|
|
192
|
+
drop.addEventListener('drop', (ev) => {
|
|
193
|
+
ev.preventDefault();
|
|
194
|
+
drop.classList.remove('ais-drop--over');
|
|
195
|
+
addFiles(ev.dataTransfer ? ev.dataTransfer.files : []);
|
|
196
|
+
});
|
|
197
|
+
page.querySelectorAll('[data-remove]').forEach((b) => b.addEventListener('click', () => {
|
|
198
|
+
state.draft.files.splice(Number(b.getAttribute('data-remove')), 1);
|
|
199
|
+
saveDraft();
|
|
200
|
+
paintPick();
|
|
201
|
+
}));
|
|
202
|
+
page.querySelector('#ais-next').addEventListener('click', () => go('step=describe'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readBase64(blob) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const r = new FileReader();
|
|
208
|
+
r.onload = () => resolve(String(r.result).replace(/^data:[^,]*,/, ''));
|
|
209
|
+
r.onerror = () => reject(new Error('read failed'));
|
|
210
|
+
r.readAsDataURL(blob);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function uploadBlob(blob, name, contentType) {
|
|
215
|
+
const data = await readBase64(blob);
|
|
216
|
+
const res = await api.request('POST', WRITE_ROUTES.upload(), { body: { name, content_type: contentType, data } });
|
|
217
|
+
if (!res.ok || !res.data || !res.data.file) {
|
|
218
|
+
const msg = res.data && res.data.error && res.data.error.message;
|
|
219
|
+
throw new Error(msg || 'could not be sent');
|
|
220
|
+
}
|
|
221
|
+
return res.data.file;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function addFiles(list) {
|
|
225
|
+
for (const file of Array.from(list || [])) {
|
|
226
|
+
const entry = { id: null, url: '', name: file.name, size_bytes: file.size, content_type: file.type, status: 'uploading' };
|
|
227
|
+
const no = lib.checkFile(file, state.draft.files.filter((f) => f.status !== 'error').length);
|
|
228
|
+
if (no) { entry.status = 'error'; entry.error = no; state.draft.files.push(entry); continue; }
|
|
229
|
+
state.draft.files.push(entry);
|
|
230
|
+
uploadBlob(file, file.name, file.type).then((f) => {
|
|
231
|
+
Object.assign(entry, { id: f.id, url: f.url, size_bytes: f.size_bytes, status: 'ready' });
|
|
232
|
+
}).catch((err) => {
|
|
233
|
+
Object.assign(entry, { status: 'error', error: err.message });
|
|
234
|
+
}).finally(() => { saveDraft(); if (state.route.view === 'step' && state.route.step === 'pick') paintPick(); });
|
|
235
|
+
}
|
|
236
|
+
if (state.route.view === 'step' && state.route.step === 'pick') paintPick();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function paintDescribe() {
|
|
240
|
+
page.innerHTML = `${stepHead('describe')}
|
|
241
|
+
<h1 class="ais-ask"><label for="ais-prompt" data-head tabindex="-1">What should it make?</label></h1>
|
|
242
|
+
<textarea class="ais-field ais-field--tall" id="ais-prompt" maxlength="4000" placeholder="Say what you want, in your own words."></textarea>
|
|
243
|
+
<p class="ais-faint">Your project's look and words are added for you, so you don't need to describe them.</p>
|
|
244
|
+
<div class="ais-go">
|
|
245
|
+
<button type="button" class="ais-btn" id="ais-next">Next</button>
|
|
246
|
+
<button type="button" class="ais-ghost" id="ais-back">Back</button>
|
|
247
|
+
</div>`;
|
|
248
|
+
const box = page.querySelector('#ais-prompt');
|
|
249
|
+
const next = page.querySelector('#ais-next');
|
|
250
|
+
box.value = state.draft.prompt;
|
|
251
|
+
const sync = () => { next.disabled = !box.value.trim(); };
|
|
252
|
+
sync();
|
|
253
|
+
box.addEventListener('input', () => { state.draft.prompt = box.value; saveDraft(); sync(); });
|
|
254
|
+
next.addEventListener('click', () => go('step=make'));
|
|
255
|
+
page.querySelector('#ais-back').addEventListener('click', () => go('step=pick'));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function loadKeysAndBalance() {
|
|
259
|
+
const [k, b] = await Promise.all([
|
|
260
|
+
api.request('GET', `${API}/ai-studio/keys`),
|
|
261
|
+
api.request('GET', `${API}/ai-studio/balance`),
|
|
262
|
+
]);
|
|
263
|
+
state.keys = k.ok ? k.data : null;
|
|
264
|
+
state.balance = b.ok ? b.data : null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function paintMake() {
|
|
268
|
+
await loadKeysAndBalance();
|
|
269
|
+
const files = readyFiles();
|
|
270
|
+
const c = lib.payChoices(state.keys, state.balance);
|
|
271
|
+
const thumbs = files.slice(0, 3).map((f) => thumb(f)).join('');
|
|
272
|
+
const ownCard = c.own ? `
|
|
273
|
+
<button type="button" class="st-action ais-card ais-card--own" data-pay="own">
|
|
274
|
+
<span class="st-action__label">My own key</span>
|
|
275
|
+
<span class="ais-card__line">Free here. ${esc(c.own.billedBy)} bills you for it directly.</span>
|
|
276
|
+
<span class="ais-card__foot">${esc(c.own.name)} key ending ${esc(c.own.last4 || '')}</span>
|
|
277
|
+
</button>` : '';
|
|
278
|
+
const balanceCard = c.payments ? `
|
|
279
|
+
<button type="button" class="st-action ais-card ais-card--ours" data-pay="ours" ${c.short ? 'disabled' : ''}>
|
|
280
|
+
<span class="st-action__label">My balance</span>
|
|
281
|
+
<span class="ais-card__line">Usually a few cents. Up to ${esc(c.setAside)} is set aside, and the rest comes back.</span>
|
|
282
|
+
<span class="ais-card__foot">${esc(c.left)}</span>
|
|
283
|
+
</button>` : '';
|
|
284
|
+
let foot = 'You only pay for what it really cost. If it fails, nothing is taken.';
|
|
285
|
+
let extra = '';
|
|
286
|
+
if (c.payments && c.short) extra = `<p class="ais-faint">Your balance is under ${esc(c.setAside)}. <a href="/builders/ai-studio?view=balance">Add money</a> to pay this way.</p>`;
|
|
287
|
+
if (!c.own) extra += `<p class="ais-faint">Have your own OpenRouter or Gemini key? <a href="${SETTINGS_KEYS}">Add it in Settings</a> and make things for free here.</p>`;
|
|
288
|
+
if (!c.own && !c.payments) foot = 'Paying from a balance is not set up here yet, so add your own key in Settings to make something.';
|
|
289
|
+
page.innerHTML = `${stepHead('make')}
|
|
290
|
+
<h1 class="ais-ask" data-head tabindex="-1">How do you want to pay?</h1>
|
|
291
|
+
<div class="ais-row ais-row--summary">${thumbs}<span class="ais-grow ais-soft">${esc(lib.quoteLine(state.draft.prompt))}</span><a class="ais-link" href="/builders/ai-studio?step=describe" data-nav="step=describe">change</a></div>
|
|
292
|
+
${ownCard || balanceCard ? `<div class="ais-cards">${ownCard}${balanceCard}</div>` : ''}
|
|
293
|
+
<p class="ais-faint">${esc(foot)}</p>
|
|
294
|
+
${extra}
|
|
295
|
+
<p class="ais-err" id="ais-err" role="alert" hidden></p>`;
|
|
296
|
+
page.querySelectorAll('[data-pay]').forEach((b) => b.addEventListener('click', () => sendRequest(b.getAttribute('data-pay'), state.draft.prompt, readyFiles(), b)));
|
|
297
|
+
wireNavLinks();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Same-page links move through history, so the steps keep the back button.
|
|
301
|
+
function wireNavLinks() {
|
|
302
|
+
page.querySelectorAll('[data-nav]').forEach((a) => a.addEventListener('click', (ev) => {
|
|
303
|
+
ev.preventDefault();
|
|
304
|
+
go(a.getAttribute('data-nav'));
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function showError(text) {
|
|
309
|
+
const el = page.querySelector('#ais-err');
|
|
310
|
+
if (el) { el.textContent = text; el.hidden = false; } else toast(text);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Sends one request: uploads a copy of each carried file first (a file can
|
|
314
|
+
// go with one request only), then the request itself, then opens its page.
|
|
315
|
+
async function sendRequest(payWith, prompt, files, button) {
|
|
316
|
+
if (state.busy) return;
|
|
317
|
+
if (!String(prompt || '').trim()) { showError('Say what it should make first.'); return; }
|
|
318
|
+
state.busy = true;
|
|
319
|
+
const buttons = page.querySelectorAll('button');
|
|
320
|
+
buttons.forEach((b) => { b.disabled = true; });
|
|
321
|
+
if (button) button.setAttribute('aria-busy', 'true');
|
|
322
|
+
try {
|
|
323
|
+
const ids = [];
|
|
324
|
+
for (const f of files) {
|
|
325
|
+
if (f.id && !f.carried) { ids.push(f.id); continue; }
|
|
326
|
+
const got = await fetch(f.url, { credentials: 'same-origin' });
|
|
327
|
+
if (!got.ok) throw new Error(`${f.name || 'A file'} could not be read again. Pick it again.`);
|
|
328
|
+
const copy = await uploadBlob(await got.blob(), f.name, f.content_type);
|
|
329
|
+
ids.push(copy.id);
|
|
330
|
+
}
|
|
331
|
+
const res = await api.request('POST', WRITE_ROUTES.request(), { body: { prompt: String(prompt).trim(), file_ids: ids, pay_with: payWith } });
|
|
332
|
+
if (!res.ok || !res.data || !res.data.request) {
|
|
333
|
+
const msg = res.data && res.data.error && res.data.error.message;
|
|
334
|
+
throw new Error(msg || 'The request could not be sent. Try again.');
|
|
335
|
+
}
|
|
336
|
+
clearDraft();
|
|
337
|
+
state.busy = false;
|
|
338
|
+
go(`request=${res.data.request.id}`);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
state.busy = false;
|
|
341
|
+
buttons.forEach((b) => { b.disabled = false; });
|
|
342
|
+
if (button) button.removeAttribute('aria-busy');
|
|
343
|
+
showError(err.message);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ---- a request: waiting, then the result ----------------------------------
|
|
348
|
+
|
|
349
|
+
async function fetchRequest(id) {
|
|
350
|
+
const res = await api.request('GET', `${API}/ai-studio/requests/${id}`);
|
|
351
|
+
return res.ok && res.data ? res.data.request : null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function openRequest(id) {
|
|
355
|
+
const req = await fetchRequest(id);
|
|
356
|
+
if (!req) { paintNote('That request could not be found.'); return; }
|
|
357
|
+
state.request = req;
|
|
358
|
+
state.pick = 0;
|
|
359
|
+
if (lib.isFinished(req.status)) { await paintResult(req); return; }
|
|
360
|
+
paintWaiting(req);
|
|
361
|
+
const opened = Date.now();
|
|
362
|
+
const started = Date.parse(req.created_at);
|
|
363
|
+
const base = Number.isFinite(started) && started <= opened ? started : opened;
|
|
364
|
+
const tick = () => paintWaitingClock(req, (Date.now() - base) / 1000);
|
|
365
|
+
tick();
|
|
366
|
+
state.timers.push(setInterval(tick, 1000));
|
|
367
|
+
state.timers.push(setInterval(async () => {
|
|
368
|
+
const next = await fetchRequest(id);
|
|
369
|
+
if (!next || state.route.view !== 'request' || state.route.id !== id) return;
|
|
370
|
+
Object.assign(req, next);
|
|
371
|
+
if (lib.isFinished(next.status)) { stopTimers(); main.removeAttribute('data-making'); await paintResult(next); focusHead(); }
|
|
372
|
+
}, POLL_MS));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function paintWaiting(req) {
|
|
376
|
+
main.setAttribute('data-making', 'on');
|
|
377
|
+
page.innerHTML = `${backLink()}
|
|
378
|
+
<div class="ais-wait">
|
|
379
|
+
<h1 class="ais-clock" data-head tabindex="-1"><span id="ais-clock">0:00</span><span class="ais-sr"> so far</span></h1>
|
|
380
|
+
<p class="ais-soft">usually done in about a minute</p>
|
|
381
|
+
<ul class="ais-checks" id="ais-checks" aria-live="polite"></ul>
|
|
382
|
+
<p class="ais-faint">The lamp stays on until it's done. You can close this page; the result waits in your studio.</p>
|
|
383
|
+
</div>`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function paintWaitingClock(req, sec) {
|
|
387
|
+
const clockEl = page.querySelector('#ais-clock');
|
|
388
|
+
if (clockEl) clockEl.textContent = lib.clock(sec);
|
|
389
|
+
const list = page.querySelector('#ais-checks');
|
|
390
|
+
if (!list) return;
|
|
391
|
+
const uploads = (req.files || []).filter((f) => f.role === 'upload').length;
|
|
392
|
+
const html = lib.waitSteps(req.status, sec, uploads || 1).map((s) => `<li class="ais-check ais-check--${s.state}"><span class="ais-check__mark" aria-hidden="true">${s.state === 'done' ? '✓' : ''}</span><span>${esc(s.text)}</span>${s.state === 'done' ? '<span class="ais-sr"> (done)</span>' : ''}</li>`).join('');
|
|
393
|
+
if (list.innerHTML !== html) list.innerHTML = html;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function paintResult(req) {
|
|
397
|
+
if (!state.balance) await loadKeysAndBalance();
|
|
398
|
+
const outputs = (req.files || []).filter((f) => f.role === 'output' && lib.isImage(f.content_type));
|
|
399
|
+
const uploads = (req.files || []).filter((f) => f.role === 'upload');
|
|
400
|
+
const pick = Math.min(state.pick, Math.max(0, outputs.length - 1));
|
|
401
|
+
const big = outputs[pick];
|
|
402
|
+
const thumbs = outputs.length > 1 ? outputs.map((f, i) => `<button type="button" class="ais-pick${i === pick ? ' ais-pick--on' : ''}" data-pick="${i}" aria-label="Picture ${i + 1}" aria-pressed="${i === pick}"><img src="${esc(f.url)}" alt=""></button>`).join('') : '';
|
|
403
|
+
const rc = lib.receipt(req, state.balance && state.balance.price_multiplier);
|
|
404
|
+
const receiptHtml = rc.rows.length ? `<div class="ais-receipt">${rc.rows.map(([k, v]) => `<div class="ais-line"><span class="ais-grow">${esc(k)}</span><span>${esc(v)}</span></div>`).join('')}${rc.note ? `<div class="ais-line"><span class="ais-grow ais-faint ais-wrap">${esc(rc.note)}</span></div>` : ''}</div>` : '';
|
|
405
|
+
const failed = req.status !== 'done';
|
|
406
|
+
main.setAttribute('data-view', 'result');
|
|
407
|
+
page.innerHTML = `
|
|
408
|
+
<h1 class="ais-sr" data-head tabindex="-1">${failed ? 'That did not work' : 'Your result'}</h1>
|
|
409
|
+
<div class="ais-result">
|
|
410
|
+
<div class="ais-result__art">
|
|
411
|
+
${backLink()}
|
|
412
|
+
${big ? `<img class="ais-big" src="${esc(big.url)}" alt="Picture ${pick + 1} made from your request">` : `<div class="ais-big ais-big--none"><span class="ais-faint">${failed ? 'No picture this time.' : 'An answer only, no picture this time.'}</span></div>`}
|
|
413
|
+
<div class="ais-thumbs">${thumbs}<span class="ais-grow"></span>${big ? `<a class="ais-ghost" href="${esc(big.url)}" download>Save picture</a>` : ''}</div>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="ais-result__words">
|
|
416
|
+
${failed ? `<p class="ais-err">${esc(req.error || 'It did not work, and nothing was charged.')}</p>` : ''}
|
|
417
|
+
<div class="ais-reply">${lib.replyHtml(req.reply_md, esc)}</div>
|
|
418
|
+
<div class="ais-again">
|
|
419
|
+
<label class="ais-head" for="ais-again">Your request</label>
|
|
420
|
+
<textarea class="ais-field" id="ais-again" maxlength="4000"></textarea>
|
|
421
|
+
<p class="ais-faint">${esc(lib.againLine(uploads.length))}</p>
|
|
422
|
+
<div class="ais-go ais-go--wrap">
|
|
423
|
+
<button type="button" class="ais-btn" id="ais-remake">Make it again</button>
|
|
424
|
+
<a class="ais-ghost" href="/builders/ai-studio?step=pick&from=${esc(req.id)}" id="ais-change">Change files</a>
|
|
425
|
+
<a class="ais-ghost" href="/builders/ai-studio?step=pick" id="ais-new">Start something new</a>
|
|
426
|
+
</div>
|
|
427
|
+
<p class="ais-err" id="ais-err" role="alert" hidden></p>
|
|
428
|
+
</div>
|
|
429
|
+
${receiptHtml}
|
|
430
|
+
</div>
|
|
431
|
+
</div>`;
|
|
432
|
+
const box = page.querySelector('#ais-again');
|
|
433
|
+
box.value = req.prompt || '';
|
|
434
|
+
page.querySelectorAll('[data-pick]').forEach((b) => b.addEventListener('click', () => { state.pick = Number(b.getAttribute('data-pick')); paintResult(req); }));
|
|
435
|
+
const carried = uploads.map((f) => ({ id: null, url: f.url, name: f.name, size_bytes: f.size_bytes, content_type: f.content_type, status: 'ready', carried: true }));
|
|
436
|
+
page.querySelector('#ais-remake').addEventListener('click', (ev) => {
|
|
437
|
+
sendRequest(req.key_source === 'own' ? 'own' : 'ours', box.value, carried, ev.currentTarget);
|
|
438
|
+
});
|
|
439
|
+
page.querySelector('#ais-change').addEventListener('click', (ev) => {
|
|
440
|
+
ev.preventDefault();
|
|
441
|
+
state.draft = { files: carried, prompt: box.value, from: req.id };
|
|
442
|
+
saveDraft();
|
|
443
|
+
go('step=pick');
|
|
444
|
+
});
|
|
445
|
+
page.querySelector('#ais-new').addEventListener('click', (ev) => { ev.preventDefault(); clearDraft(); go('step=pick'); });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- the balance ----------------------------------------------------------
|
|
449
|
+
|
|
450
|
+
async function paintBalance() {
|
|
451
|
+
const [b, r] = await Promise.all([
|
|
452
|
+
api.request('GET', `${API}/ai-studio/balance`),
|
|
453
|
+
api.request('GET', `${API}/ai-studio/requests`),
|
|
454
|
+
]);
|
|
455
|
+
state.balance = b.ok ? b.data : null;
|
|
456
|
+
const data = state.balance || {};
|
|
457
|
+
const prompts = {};
|
|
458
|
+
for (const q of (r.ok && r.data && r.data.requests) || []) prompts[q.id] = q.prompt;
|
|
459
|
+
const payments = !!data.balance;
|
|
460
|
+
const sum = lib.monthSummary(data.ledger || [], new Date());
|
|
461
|
+
const all = state.route.all;
|
|
462
|
+
const t = state.topup;
|
|
463
|
+
const chips = lib.TOPUP_CHOICES.map((n) => `<button type="button" class="ais-chip" data-amount="${n}" aria-pressed="${!t.other && t.amount === n}">$${n}</button>`).join('')
|
|
464
|
+
+ `<button type="button" class="ais-chip" data-amount="other" aria-pressed="${t.other}">other</button>`;
|
|
465
|
+
const topupHtml = payments ? `
|
|
466
|
+
<div class="ais-topup">
|
|
467
|
+
<span class="ais-soft" id="ais-topup-label">Add how much?</span>
|
|
468
|
+
<div class="ais-chips" role="group" aria-labelledby="ais-topup-label">${chips}</div>
|
|
469
|
+
${t.other ? `<label class="ais-other"><span class="ais-faint">Amount in dollars</span><input type="number" id="ais-other" min="5" max="500" step="1" value="${esc(t.amount)}"></label>` : ''}
|
|
470
|
+
<div class="ais-go"><button type="button" class="ais-btn" id="ais-add">Add $${esc(t.amount)}</button><span class="ais-faint" id="ais-quote"></span></div>
|
|
471
|
+
<p class="ais-err" id="ais-err" role="alert" hidden></p>
|
|
472
|
+
${data.test_mode ? '<p class="ais-faint">Test mode: no real money moves.</p>' : ''}
|
|
473
|
+
</div>` : '<p class="ais-faint">Paying from a balance is not set up here yet. You can still make things on your own key: <a href="/builders/settings#ai-keys">add it in Settings</a>.</p>';
|
|
474
|
+
const monthLines = sum.lines.map((l) => `<div class="ais-line"><span class="ais-grow">${esc(l.label)}</span><span class="ais-num ais-tone-${l.tone || 'plain'}">${esc(l.value)}</span></div>`).join('')
|
|
475
|
+
|| '<div class="ais-line"><span class="ais-grow ais-faint">Nothing yet this month.</span></div>';
|
|
476
|
+
const fmtDate = (s) => { const d = new Date(s); return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString('en', { month: 'short', day: 'numeric' }); };
|
|
477
|
+
const allLines = (data.ledger || []).map((row) => {
|
|
478
|
+
const l = lib.ledgerLine(row, prompts);
|
|
479
|
+
const what = row.request_id ? `<a href="/builders/ai-studio?request=${esc(row.request_id)}">${esc(l.what)}</a>` : esc(l.what);
|
|
480
|
+
return `<div class="ais-line"><span class="ais-date">${esc(fmtDate(row.created_at))}</span><span class="ais-grow">${what}</span><span class="ais-faint ais-detail">${esc(l.detail)}</span><span class="ais-num ais-tone-${l.tone || 'plain'}">${esc(l.value)}</span></div>`;
|
|
481
|
+
}).join('');
|
|
482
|
+
page.innerHTML = `${backLink()}
|
|
483
|
+
<div class="ais-balance">
|
|
484
|
+
${payments ? `<h1 class="ais-figure" data-head tabindex="-1"><span class="ais-big-num">${esc(lib.balanceUsd(data.balance.available_micro))}</span> <span class="ais-soft">left</span></h1>` : '<h1 class="ais-sr" data-head tabindex="-1">Your AI balance</h1>'}
|
|
485
|
+
${topupHtml}
|
|
486
|
+
<div class="ais-ledger">
|
|
487
|
+
<span class="ais-head">${esc(sum.month)}</span>
|
|
488
|
+
${monthLines}
|
|
489
|
+
<div class="ais-line"><a class="ais-link" href="/builders/ai-studio?view=balance${all ? '' : '&all=1'}" data-nav="view=balance${all ? '' : '&all=1'}">${all ? 'Show this month only' : 'See every charge and refund →'}</a></div>
|
|
490
|
+
${all ? (allLines ? `<div class="ais-all">${allLines}</div>` : '<p class="ais-faint">No charges or refunds yet.</p>') : ''}
|
|
491
|
+
</div>
|
|
492
|
+
</div>`;
|
|
493
|
+
wireNavLinks();
|
|
494
|
+
if (!payments) return;
|
|
495
|
+
page.querySelectorAll('[data-amount]').forEach((btn) => btn.addEventListener('click', () => {
|
|
496
|
+
const v = btn.getAttribute('data-amount');
|
|
497
|
+
if (v === 'other') { t.other = true; } else { t.other = false; t.amount = Number(v); }
|
|
498
|
+
paintBalance().then(() => { const o = page.querySelector('#ais-other'); if (o) o.focus(); });
|
|
499
|
+
}));
|
|
500
|
+
const other = page.querySelector('#ais-other');
|
|
501
|
+
if (other) other.addEventListener('change', () => { t.amount = Math.round(Number(other.value) || 0); paintBalance(); });
|
|
502
|
+
page.querySelector('#ais-add').addEventListener('click', addMoney);
|
|
503
|
+
quote(t.amount);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function quote(amount) {
|
|
507
|
+
const el = page.querySelector('#ais-quote');
|
|
508
|
+
const res = await api.request('GET', `${API}/ai-studio/topup/quote?amount_usd=${encodeURIComponent(amount)}`);
|
|
509
|
+
if (!el) return;
|
|
510
|
+
if (!res.ok || !res.data || !res.data.quote) {
|
|
511
|
+
el.textContent = (res.data && res.data.error && res.data.error.message) || '';
|
|
512
|
+
const add = page.querySelector('#ais-add');
|
|
513
|
+
if (add) add.disabled = true;
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const q = res.data.quote;
|
|
517
|
+
el.textContent = `${lib.usd(q.credited_micro)} lands in your balance; your card is charged ${lib.usd(q.charge_micro)}.`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function addMoney(ev) {
|
|
521
|
+
if (state.busy) return;
|
|
522
|
+
state.busy = true;
|
|
523
|
+
const btn = ev.currentTarget;
|
|
524
|
+
btn.disabled = true;
|
|
525
|
+
const res = await api.request('POST', WRITE_ROUTES.topup(), { body: { amount_usd: state.topup.amount } });
|
|
526
|
+
state.busy = false;
|
|
527
|
+
if (!res.ok) {
|
|
528
|
+
btn.disabled = false;
|
|
529
|
+
showError((res.data && res.data.error && res.data.error.message) || 'The money could not be added. Try again.');
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
toast(`Added ${lib.usd(res.data.topup.credited_micro)}.`);
|
|
533
|
+
await paintBalance();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ---- the sky, and boot ----------------------------------------------------
|
|
537
|
+
|
|
538
|
+
function paintSky() {
|
|
539
|
+
if (studioLib && document.body) document.body.setAttribute('data-sky', studioLib.skyAt(new Date()));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
fileInput.addEventListener('change', () => { addFiles(fileInput.files); fileInput.value = ''; });
|
|
543
|
+
window.addEventListener('popstate', () => render({ focus: true }));
|
|
544
|
+
|
|
545
|
+
(async () => {
|
|
546
|
+
paintSky();
|
|
547
|
+
setInterval(paintSky, 60000);
|
|
548
|
+
const me = await api.request('GET', `${API}/me`);
|
|
549
|
+
state.signedIn = !!me.ok;
|
|
550
|
+
if (state.signedIn) {
|
|
551
|
+
const status = await api.request('GET', `${API}/ai-studio/status`);
|
|
552
|
+
state.off = !status.ok;
|
|
553
|
+
}
|
|
554
|
+
await render();
|
|
555
|
+
})();
|
|
556
|
+
})();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_": "The studio's AI pages (task 1004921, goal 1000136), built 1:1 on option C of the AI Studio options canvas plus the owner's addition (change the request on the result and make it again). Against the kit's own stub the page renders its could-not-read states (no ai-studio feeds). TO SEE IT POPULATED, point the kit at the hall-preview harness with the module on: `node scripts/hall-preview/server.js --port 4637 --fixture-me --modules-on ai-studio` then `render.js --base-in http://builders.localhost:4637 --base-out http://builders.localhost:4637`. The harness answers keys, balance, the top-up quote and the request list from the ai-studio__*.json fixtures; a request by id (1002 running, 1003 failed, 1004 own Gemini key, any other the finished result on the balance); an upload, a request and a top-up from its CANNED WRITES; and every file as a stand-in picture. ?from=1001 opens the steps on that request's files and words, which is how the populated steps render without an upload.",
|
|
3
|
+
"page": "/builders/ai-studio",
|
|
4
|
+
"surface": "hall-ui",
|
|
5
|
+
"stub": {
|
|
6
|
+
"prefix": "/builders/"
|
|
7
|
+
},
|
|
8
|
+
"modeQuery": false,
|
|
9
|
+
"ignoreRequests": [
|
|
10
|
+
"/api/(gds|bongos)/me$",
|
|
11
|
+
"/api/(gds|bongos)/me/",
|
|
12
|
+
"/api/(gds|bongos)/ai-studio/",
|
|
13
|
+
"/api/(gds|bongos)/core-update"
|
|
14
|
+
],
|
|
15
|
+
"ignoreConsole": [
|
|
16
|
+
"/api/(gds|bongos)/",
|
|
17
|
+
"\\b401\\b",
|
|
18
|
+
"\\b403\\b",
|
|
19
|
+
"\\b404\\b",
|
|
20
|
+
"\\b501\\b",
|
|
21
|
+
"Unauthorized",
|
|
22
|
+
"Forbidden",
|
|
23
|
+
"Not Found",
|
|
24
|
+
"load failed"
|
|
25
|
+
],
|
|
26
|
+
"states": {
|
|
27
|
+
"pick": {
|
|
28
|
+
"auth": true,
|
|
29
|
+
"_": "Step 1: what are you starting from, with the three files of request 1001.",
|
|
30
|
+
"url": "/builders/ai-studio?step=pick&from=1001",
|
|
31
|
+
"actions": [["wait", 900]],
|
|
32
|
+
"expect": { "visible": ["#ais-page h1", ".ais-drop", ".ais-rows"] }
|
|
33
|
+
},
|
|
34
|
+
"describe": {
|
|
35
|
+
"auth": true,
|
|
36
|
+
"_": "Step 2: what should it make.",
|
|
37
|
+
"url": "/builders/ai-studio?step=describe&from=1001",
|
|
38
|
+
"actions": [["wait", 900]],
|
|
39
|
+
"expect": { "visible": ["#ais-prompt"] }
|
|
40
|
+
},
|
|
41
|
+
"make": {
|
|
42
|
+
"auth": true,
|
|
43
|
+
"_": "Step 3: how do you want to pay, the own-key card and the balance card.",
|
|
44
|
+
"url": "/builders/ai-studio?step=make&from=1001",
|
|
45
|
+
"actions": [["wait", 1200]],
|
|
46
|
+
"expect": { "visible": [".ais-card--own", ".ais-card--ours"] }
|
|
47
|
+
},
|
|
48
|
+
"waiting": {
|
|
49
|
+
"auth": true,
|
|
50
|
+
"_": "The waiting page: the clock, the three lines, the lamp on.",
|
|
51
|
+
"url": "/builders/ai-studio?request=1002",
|
|
52
|
+
"actions": [["wait", 1500]],
|
|
53
|
+
"expect": { "visible": ["#ais-clock", "#ais-checks"] }
|
|
54
|
+
},
|
|
55
|
+
"result": {
|
|
56
|
+
"auth": true,
|
|
57
|
+
"_": "The result on the balance: the picture, the reply, the request to change and make again, the receipt with the provider's cost and our markup apart.",
|
|
58
|
+
"url": "/builders/ai-studio?request=1001",
|
|
59
|
+
"actions": [["wait", 1200]],
|
|
60
|
+
"expect": { "visible": [".ais-big", "#ais-again", "#ais-remake", ".ais-receipt"] }
|
|
61
|
+
},
|
|
62
|
+
"result-own": {
|
|
63
|
+
"auth": true,
|
|
64
|
+
"_": "A result on the builder's own Gemini key: cost not reported, nothing charged.",
|
|
65
|
+
"url": "/builders/ai-studio?request=1004",
|
|
66
|
+
"actions": [["wait", 1200]],
|
|
67
|
+
"expect": { "visible": [".ais-receipt", "#ais-again"] }
|
|
68
|
+
},
|
|
69
|
+
"failed": {
|
|
70
|
+
"auth": true,
|
|
71
|
+
"_": "A failed run: the error in words, no receipt, the request to try again.",
|
|
72
|
+
"url": "/builders/ai-studio?request=1003",
|
|
73
|
+
"actions": [["wait", 1200]],
|
|
74
|
+
"expect": { "visible": [".ais-err", "#ais-remake"] }
|
|
75
|
+
},
|
|
76
|
+
"balance": {
|
|
77
|
+
"auth": true,
|
|
78
|
+
"_": "The balance: what is left, add money with the card charge, this month.",
|
|
79
|
+
"url": "/builders/ai-studio?view=balance",
|
|
80
|
+
"actions": [["wait", 1200]],
|
|
81
|
+
"expect": { "visible": [".ais-big-num", "#ais-add", ".ais-ledger"] }
|
|
82
|
+
},
|
|
83
|
+
"balance-all": {
|
|
84
|
+
"auth": true,
|
|
85
|
+
"_": "Every charge and refund.",
|
|
86
|
+
"url": "/builders/ai-studio?view=balance&all=1",
|
|
87
|
+
"actions": [["wait", 1200]],
|
|
88
|
+
"expect": { "visible": [".ais-all"] }
|
|
89
|
+
},
|
|
90
|
+
"out": {
|
|
91
|
+
"auth": false
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|