savant-code 0.0.19 → 0.0.20
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/README.md +381 -550
- package/{cli/release/index.js → index.js} +0 -0
- package/package.json +44 -71
- package/.agents/skills/coding-csharp/SKILL.md +0 -82
- package/.agents/skills/coding-go/SKILL.md +0 -76
- package/.agents/skills/coding-java/SKILL.md +0 -79
- package/.agents/skills/coding-python/SKILL.md +0 -87
- package/.agents/skills/coding-rust/SKILL.md +0 -83
- package/.agents/skills/coding-typescript/SKILL.md +0 -90
- package/.agents/skills/release-workflow/SKILL.md +0 -69
- package/.bun-version +0 -1
- package/.commandcode/settings.json +0 -11
- package/.commandcode/taste/workflow/taste.md +0 -6
- package/.env.example +0 -95
- package/.gitattributes +0 -43
- package/.githooks/pre-push +0 -35
- package/.github/workflows/build-release-binaries.yml +0 -163
- package/.markdownlint.json +0 -42
- package/.markdownlintignore +0 -21
- package/.prettierignore +0 -9
- package/.prettierrc +0 -23
- package/.savantignore +0 -3
- package/AGENTS.md +0 -97
- package/ARCHITECTURE.md +0 -241
- package/CHANGELOG.md +0 -2631
- package/CONTRIBUTING.md +0 -88
- package/ECHO-freebuff.md +0 -22
- package/ECHO.md +0 -722
- package/FREEREADME.md +0 -35
- package/LEARNINGS.md +0 -345
- package/NOTICE +0 -15
- package/README.zh-CN.md +0 -479
- package/SECURITY.md +0 -8
- package/VERSION +0 -1
- package/WINDOWS.md +0 -238
- package/agents/README.md +0 -41
- package/agents/base-chat.ts +0 -44
- package/agents/basher.ts +0 -107
- package/agents/browser-use/browser-use.test.ts +0 -211
- package/agents/browser-use/browser-use.ts +0 -259
- package/agents/constants.ts +0 -1
- package/agents/context-pruner.ts +0 -1076
- package/agents/detective/detective.ts +0 -168
- package/agents/editor/best-of-n/best-of-n-selector2.ts +0 -148
- package/agents/editor/best-of-n/editor-implementor-gpt-5.ts +0 -7
- package/agents/editor/best-of-n/editor-implementor-opus.ts +0 -7
- package/agents/editor/best-of-n/editor-implementor.ts +0 -196
- package/agents/editor/best-of-n/editor-multi-prompt.ts +0 -265
- package/agents/file-explorer/directory-lister.ts +0 -92
- package/agents/file-explorer/glob-matcher.ts +0 -96
- package/agents/forge/forge.ts +0 -146
- package/agents/librarian/librarian.test.ts +0 -289
- package/agents/librarian/librarian.ts +0 -158
- package/agents/package.json +0 -11
- package/agents/recorder/recorder.ts +0 -104
- package/agents/researcher/researcher-docs.ts +0 -32
- package/agents/researcher/researcher-web.ts +0 -40
- package/agents/savant/savant-analyze.ts +0 -11
- package/agents/savant/savant-free-deepseek-flash.ts +0 -13
- package/agents/savant/savant-free-deepseek.ts +0 -13
- package/agents/savant/savant-free-evals.ts +0 -9
- package/agents/savant/savant-free-glm.ts +0 -13
- package/agents/savant/savant-free-kimi.ts +0 -13
- package/agents/savant/savant-free-mimo-pro.ts +0 -13
- package/agents/savant/savant-free-mimo.ts +0 -13
- package/agents/savant/savant-free-minimax-m3.ts +0 -13
- package/agents/savant/savant-free.ts +0 -9
- package/agents/savant/savant-scaffold.ts +0 -15
- package/agents/savant/savant.ts +0 -706
- package/agents/scout/scout.ts +0 -305
- package/agents/scribe/scribe.ts +0 -54
- package/agents/thinker/thinker-gemini.ts +0 -26
- package/agents/thinker/thinker-gpt.ts +0 -21
- package/agents/thinker/thinker-with-files-gemini.ts +0 -66
- package/agents/thinker/thinker.ts +0 -99
- package/agents/tmux-cli.ts +0 -724
- package/agents/tsconfig.json +0 -12
- package/agents/types/agent-definition.ts +0 -1
- package/agents/types/secret-agent-definition.ts +0 -103
- package/agents/types/tools.ts +0 -465
- package/agents/types/util-types.ts +0 -167
- package/agents/verifier/verifier.ts +0 -80
- package/art/savant-redesign.md +0 -7
- package/art/savant-text-graf.md +0 -5
- package/art/savant_ascii.md +0 -88
- package/assets/banner.png +0 -0
- package/bun.lock +0 -1979
- package/bunfig.toml +0 -7
- package/cli/README.md +0 -111
- package/cli/bunfig.toml +0 -2
- package/cli/cli_exit +0 -1
- package/cli/package.json +0 -66
- package/cli/release/README.md +0 -348
- package/cli/release/package.json +0 -56
- package/cli/release-core/README.md +0 -20
- package/cli/release-core/package.json +0 -4
- package/cli/release-core/prepare-package.js +0 -31
- package/cli/release-staging/README.md +0 -70
- package/cli/release-staging/index.js +0 -42
- package/cli/release-staging/package.json +0 -43
- package/cli/scripts/build-binary.ts +0 -469
- package/cli/scripts/clean.ts +0 -16
- package/cli/scripts/prebuild-agents.ts +0 -250
- package/cli/scripts/release.ts +0 -103
- package/cli/scripts/smoke-binary.ts +0 -280
- package/cli/scripts/test-sdk-file-hooks.sh +0 -87
- package/cli/scripts/validate-cli-with-tmux.sh +0 -43
- package/cli/src/__tests__/README.md +0 -256
- package/cli/src/__tests__/bash-mode.test.ts +0 -442
- package/cli/src/__tests__/cli-args.test.ts +0 -241
- package/cli/src/__tests__/e2e/first-time-login.test.ts +0 -173
- package/cli/src/__tests__/e2e/logout-relogin-flow.test.ts +0 -110
- package/cli/src/__tests__/e2e/returning-user-auth.test.ts +0 -111
- package/cli/src/__tests__/e2e-cli.test.ts +0 -185
- package/cli/src/__tests__/helpers/mock-api-client.ts +0 -64
- package/cli/src/__tests__/helpers/terminal-watchdog-fixture.ts +0 -50
- package/cli/src/__tests__/home-directory-detection.test.ts +0 -213
- package/cli/src/__tests__/integration/agent-toolnames-validation.test.ts +0 -63
- package/cli/src/__tests__/integration/api-integration.test.ts +0 -289
- package/cli/src/__tests__/integration/credentials-storage.test.ts +0 -453
- package/cli/src/__tests__/integration/local-agents.test.ts +0 -1187
- package/cli/src/__tests__/integration/login-flow-instrumentation.test.ts +0 -146
- package/cli/src/__tests__/integration/login-polling-working.test.ts +0 -287
- package/cli/src/__tests__/integration/usage-refresh-on-completion.test.ts +0 -177
- package/cli/src/__tests__/integration-tmux.test.ts +0 -186
- package/cli/src/__tests__/mocks/hover-toggle-controller.ts +0 -71
- package/cli/src/__tests__/path-completion.test.ts +0 -243
- package/cli/src/__tests__/project-files-chat-id.test.ts +0 -34
- package/cli/src/__tests__/release/proxy-http-get.test.ts +0 -670
- package/cli/src/__tests__/release/wrapper-safety.test.ts +0 -177
- package/cli/src/__tests__/rerender-perf.integration.test.ts +0 -283
- package/cli/src/__tests__/terminal-reset-sequences.test.ts +0 -36
- package/cli/src/__tests__/terminal-watchdog.test.ts +0 -114
- package/cli/src/__tests__/test-utils.ts +0 -266
- package/cli/src/__tests__/tmux-poc.ts +0 -150
- package/cli/src/__tests__/unit/agent-mode-toggle.test.ts +0 -138
- package/cli/src/__tests__/unit/copy-button.test.tsx +0 -188
- package/cli/src/__tests__/unit/create-run-config.test.ts +0 -77
- package/cli/src/__tests__/unit/publish-confirmation.test.ts +0 -70
- package/cli/src/__tests__/unit/segmented-control.test.ts +0 -167
- package/cli/src/__tests__/utils/env.test.ts +0 -210
- package/cli/src/__tests__/utils/project-picker.test.ts +0 -39
- package/cli/src/agents/bundled-agents.generated.d.ts +0 -14
- package/cli/src/agents/bundled-agents.generated.ts +0 -1962
- package/cli/src/app.tsx +0 -383
- package/cli/src/chat.tsx +0 -2008
- package/cli/src/cli-args.ts +0 -183
- package/cli/src/commands/__tests__/bash-command.test.ts +0 -449
- package/cli/src/commands/__tests__/command-args.test.ts +0 -325
- package/cli/src/commands/__tests__/copy-conversation.test.ts +0 -240
- package/cli/src/commands/__tests__/dev-command.test.ts +0 -128
- package/cli/src/commands/__tests__/diagnostics-command.test.ts +0 -35
- package/cli/src/commands/__tests__/image.test.ts +0 -99
- package/cli/src/commands/__tests__/init.test.ts +0 -479
- package/cli/src/commands/__tests__/loop-command.test.ts +0 -89
- package/cli/src/commands/__tests__/new-command-rotates-chat.test.ts +0 -87
- package/cli/src/commands/__tests__/permissions-command.test.ts +0 -93
- package/cli/src/commands/__tests__/process-diagnostics.test.ts +0 -54
- package/cli/src/commands/__tests__/prompt-builders.test.ts +0 -69
- package/cli/src/commands/__tests__/registry-gating.test.ts +0 -125
- package/cli/src/commands/__tests__/rewind-command.test.ts +0 -227
- package/cli/src/commands/__tests__/router-connect-chatgpt.test.ts +0 -72
- package/cli/src/commands/__tests__/router-input.test.ts +0 -302
- package/cli/src/commands/__tests__/router-provider-setup.test.ts +0 -180
- package/cli/src/commands/__tests__/savant-free-command-aliases.test.ts +0 -56
- package/cli/src/commands/ads.ts +0 -45
- package/cli/src/commands/command-registry.ts +0 -1112
- package/cli/src/commands/copy-conversation.ts +0 -386
- package/cli/src/commands/goal.ts +0 -87
- package/cli/src/commands/health-command.ts +0 -61
- package/cli/src/commands/help.ts +0 -13
- package/cli/src/commands/image.ts +0 -20
- package/cli/src/commands/init.ts +0 -121
- package/cli/src/commands/loop.ts +0 -182
- package/cli/src/commands/process-diagnostics.ts +0 -120
- package/cli/src/commands/prompt-builders.ts +0 -140
- package/cli/src/commands/publish.ts +0 -261
- package/cli/src/commands/rewind.ts +0 -130
- package/cli/src/commands/router-utils.ts +0 -80
- package/cli/src/commands/router.ts +0 -589
- package/cli/src/commands/telemetry.ts +0 -68
- package/cli/src/commands/usage.ts +0 -26
- package/cli/src/components/__tests__/ad-banner.test.tsx +0 -84
- package/cli/src/components/__tests__/block-helpers.test.tsx +0 -139
- package/cli/src/components/__tests__/clickable.test.tsx +0 -39
- package/cli/src/components/__tests__/grid-layout.integration.test.tsx +0 -365
- package/cli/src/components/__tests__/grid-layout.test.tsx +0 -1033
- package/cli/src/components/__tests__/markdown-content.test.tsx +0 -163
- package/cli/src/components/__tests__/message-block.completion.test.tsx +0 -104
- package/cli/src/components/__tests__/message-block.streaming.test.tsx +0 -78
- package/cli/src/components/__tests__/message-with-agents.test.tsx +0 -571
- package/cli/src/components/__tests__/multiline-input.test.tsx +0 -1129
- package/cli/src/components/__tests__/selectable-list.test.ts +0 -232
- package/cli/src/components/__tests__/shimmer-text.test.tsx +0 -32
- package/cli/src/components/__tests__/status-indicator.test.tsx +0 -184
- package/cli/src/components/__tests__/user-error-banner.test.tsx +0 -100
- package/cli/src/components/ad-banner.tsx +0 -351
- package/cli/src/components/agent-checklist.tsx +0 -385
- package/cli/src/components/agent-mode-toggle.tsx +0 -294
- package/cli/src/components/ask-user/__tests__/multiple-choice-form.test.ts +0 -432
- package/cli/src/components/ask-user/__tests__/validation.test.ts +0 -213
- package/cli/src/components/ask-user/components/accordion-question.tsx +0 -153
- package/cli/src/components/ask-user/components/custom-answer-input.tsx +0 -66
- package/cli/src/components/ask-user/components/options-list.tsx +0 -139
- package/cli/src/components/ask-user/components/question-header.tsx +0 -80
- package/cli/src/components/ask-user/components/question-option.tsx +0 -94
- package/cli/src/components/ask-user/constants.ts +0 -35
- package/cli/src/components/ask-user/index.tsx +0 -660
- package/cli/src/components/ask-user/utils/validation.ts +0 -85
- package/cli/src/components/attachment-card.tsx +0 -67
- package/cli/src/components/blocks/agent-block-grid.tsx +0 -58
- package/cli/src/components/blocks/agent-branch-item.tsx +0 -230
- package/cli/src/components/blocks/agent-branch-wrapper.tsx +0 -524
- package/cli/src/components/blocks/agent-list-branch.tsx +0 -77
- package/cli/src/components/blocks/ask-user-branch.tsx +0 -98
- package/cli/src/components/blocks/block-helpers.tsx +0 -257
- package/cli/src/components/blocks/blocks-renderer.tsx +0 -296
- package/cli/src/components/blocks/content-with-markdown.tsx +0 -34
- package/cli/src/components/blocks/copy-button.tsx +0 -106
- package/cli/src/components/blocks/copyable-block.tsx +0 -47
- package/cli/src/components/blocks/image-block.tsx +0 -128
- package/cli/src/components/blocks/implementor-row.tsx +0 -453
- package/cli/src/components/blocks/markdown-content.tsx +0 -360
- package/cli/src/components/blocks/markdown-renderables.tsx +0 -347
- package/cli/src/components/blocks/single-block.tsx +0 -210
- package/cli/src/components/blocks/thinking-block.tsx +0 -77
- package/cli/src/components/blocks/tool-block-group.tsx +0 -72
- package/cli/src/components/blocks/tool-branch.tsx +0 -196
- package/cli/src/components/blocks/user-content-copy.tsx +0 -171
- package/cli/src/components/bottom-banner.tsx +0 -145
- package/cli/src/components/build-mode-buttons.tsx +0 -110
- package/cli/src/components/button.tsx +0 -75
- package/cli/src/components/chat-header.tsx +0 -11
- package/cli/src/components/chat-history-screen.tsx +0 -417
- package/cli/src/components/chat-input-bar.tsx +0 -535
- package/cli/src/components/chatgpt-connect-banner.tsx +0 -234
- package/cli/src/components/clickable.tsx +0 -149
- package/cli/src/components/collapse-button.tsx +0 -36
- package/cli/src/components/command-palette.tsx +0 -166
- package/cli/src/components/copy-button.tsx +0 -219
- package/cli/src/components/dialog.tsx +0 -111
- package/cli/src/components/elapsed-timer.tsx +0 -56
- package/cli/src/components/error-boundary.tsx +0 -63
- package/cli/src/components/feedback-container.tsx +0 -206
- package/cli/src/components/feedback-icon-button.tsx +0 -89
- package/cli/src/components/feedback-input-mode.tsx +0 -352
- package/cli/src/components/file-attachment-card.tsx +0 -89
- package/cli/src/components/grid-layout.tsx +0 -83
- package/cli/src/components/help-banner.tsx +0 -136
- package/cli/src/components/highlighted-text.tsx +0 -52
- package/cli/src/components/image-card.tsx +0 -167
- package/cli/src/components/image-thumbnail.tsx +0 -112
- package/cli/src/components/input-cursor.tsx +0 -77
- package/cli/src/components/input-mode-banner.tsx +0 -70
- package/cli/src/components/load-previous-button.tsx +0 -48
- package/cli/src/components/login-modal.tsx +0 -450
- package/cli/src/components/message-block.tsx +0 -337
- package/cli/src/components/message-footer.tsx +0 -269
- package/cli/src/components/message-with-agents.tsx +0 -594
- package/cli/src/components/mode-divider.tsx +0 -43
- package/cli/src/components/model-picker.tsx +0 -371
- package/cli/src/components/multiline-input.tsx +0 -1276
- package/cli/src/components/out-of-credits-banner.tsx +0 -155
- package/cli/src/components/pending-attachments-banner.tsx +0 -125
- package/cli/src/components/pending-bash-message.tsx +0 -57
- package/cli/src/components/progress-bar.tsx +0 -81
- package/cli/src/components/project-picker-screen.tsx +0 -469
- package/cli/src/components/provider-picker.tsx +0 -139
- package/cli/src/components/publish-confirmation.tsx +0 -514
- package/cli/src/components/publish-container.tsx +0 -737
- package/cli/src/components/raised-pill.tsx +0 -89
- package/cli/src/components/renderers/plan-box.tsx +0 -70
- package/cli/src/components/review-screen.tsx +0 -114
- package/cli/src/components/rewind-picker.tsx +0 -276
- package/cli/src/components/right-sidebar.tsx +0 -296
- package/cli/src/components/savant-free-active-session-summary.tsx +0 -63
- package/cli/src/components/savant-free-landing-screen.tsx +0 -756
- package/cli/src/components/savant-free-model-selector.tsx +0 -815
- package/cli/src/components/savant-free-referral-banner.tsx +0 -423
- package/cli/src/components/savant-free-superseded-screen.tsx +0 -62
- package/cli/src/components/savant-ui/animation/pulse.tsx +0 -26
- package/cli/src/components/savant-ui/animation/typewriter.tsx +0 -40
- package/cli/src/components/savant-ui/branding.tsx +0 -59
- package/cli/src/components/savant-ui/data-display/badge.tsx +0 -52
- package/cli/src/components/savant-ui/data-display/code-block.tsx +0 -55
- package/cli/src/components/savant-ui/data-display/key-value.tsx +0 -34
- package/cli/src/components/savant-ui/data-display/sparkline.tsx +0 -42
- package/cli/src/components/savant-ui/data-display/timeline.tsx +0 -44
- package/cli/src/components/savant-ui/data-display/tree-view.tsx +0 -55
- package/cli/src/components/savant-ui/echo/__tests__/sidebar-collapse.test.tsx +0 -90
- package/cli/src/components/savant-ui/echo/agent-stack.tsx +0 -72
- package/cli/src/components/savant-ui/echo/agent-status.tsx +0 -66
- package/cli/src/components/savant-ui/echo/fid-card.tsx +0 -89
- package/cli/src/components/savant-ui/echo/fid-list.tsx +0 -68
- package/cli/src/components/savant-ui/echo/loop-status-panel.tsx +0 -64
- package/cli/src/components/savant-ui/echo/perfection-loop.tsx +0 -118
- package/cli/src/components/savant-ui/echo/phase-indicator.tsx +0 -68
- package/cli/src/components/savant-ui/echo/phase-info.ts +0 -72
- package/cli/src/components/savant-ui/echo/token-meter.tsx +0 -39
- package/cli/src/components/savant-ui/feedback/alert.tsx +0 -45
- package/cli/src/components/savant-ui/feedback/cost-tracker.tsx +0 -54
- package/cli/src/components/savant-ui/feedback/progress-bar.tsx +0 -71
- package/cli/src/components/savant-ui/feedback/spinner.tsx +0 -40
- package/cli/src/components/savant-ui/icon-theme-keys.ts +0 -67
- package/cli/src/components/savant-ui/icon.tsx +0 -53
- package/cli/src/components/savant-ui/index.ts +0 -75
- package/cli/src/components/savant-ui/input/select.tsx +0 -26
- package/cli/src/components/savant-ui/input/toggle.tsx +0 -28
- package/cli/src/components/savant-ui/layout/grid.tsx +0 -52
- package/cli/src/components/savant-ui/layout/header.tsx +0 -28
- package/cli/src/components/savant-ui/navigation/stepper.tsx +0 -41
- package/cli/src/components/savant-ui/primitives/key-hint.tsx +0 -45
- package/cli/src/components/savant-ui/primitives/key-value-row.tsx +0 -41
- package/cli/src/components/savant-ui/primitives/panel.tsx +0 -42
- package/cli/src/components/savant-ui/primitives/separator.tsx +0 -34
- package/cli/src/components/savant-ui/primitives/sidebar-section.tsx +0 -64
- package/cli/src/components/savant-ui/primitives/spacer.tsx +0 -13
- package/cli/src/components/savant-ui/primitives/stack.tsx +0 -35
- package/cli/src/components/savant-ui/theme.ts +0 -45
- package/cli/src/components/scroll-to-bottom-button.tsx +0 -34
- package/cli/src/components/segmented-control.tsx +0 -199
- package/cli/src/components/selectable-list.tsx +0 -249
- package/cli/src/components/selected-chips.tsx +0 -86
- package/cli/src/components/separator.tsx +0 -40
- package/cli/src/components/session-ended-banner.tsx +0 -183
- package/cli/src/components/shimmer-text.tsx +0 -254
- package/cli/src/components/status-bar.tsx +0 -279
- package/cli/src/components/subscription-limit-banner.tsx +0 -269
- package/cli/src/components/suggested-prompts.tsx +0 -184
- package/cli/src/components/suggestion-menu.tsx +0 -219
- package/cli/src/components/terminal-command-display.tsx +0 -152
- package/cli/src/components/terminal-link.tsx +0 -97
- package/cli/src/components/text-attachment-card.tsx +0 -77
- package/cli/src/components/thinking.tsx +0 -108
- package/cli/src/components/toast.tsx +0 -109
- package/cli/src/components/tools/__tests__/apply-patch.test.tsx +0 -97
- package/cli/src/components/tools/__tests__/code-search.test.tsx +0 -45
- package/cli/src/components/tools/__tests__/gravity-index.test.ts +0 -76
- package/cli/src/components/tools/__tests__/render-ui.test.tsx +0 -556
- package/cli/src/components/tools/__tests__/run-terminal-command.test.ts +0 -274
- package/cli/src/components/tools/__tests__/tool-call-item.test.tsx +0 -73
- package/cli/src/components/tools/apply-patch.tsx +0 -101
- package/cli/src/components/tools/code-search.tsx +0 -50
- package/cli/src/components/tools/composio.tsx +0 -140
- package/cli/src/components/tools/diff-viewer.tsx +0 -21
- package/cli/src/components/tools/glob.tsx +0 -66
- package/cli/src/components/tools/gravity-index.tsx +0 -80
- package/cli/src/components/tools/list-directory.tsx +0 -66
- package/cli/src/components/tools/read-docs.tsx +0 -36
- package/cli/src/components/tools/read-files.tsx +0 -97
- package/cli/src/components/tools/read-subtree.tsx +0 -40
- package/cli/src/components/tools/read-url.tsx +0 -32
- package/cli/src/components/tools/registry.ts +0 -131
- package/cli/src/components/tools/render-ui.tsx +0 -466
- package/cli/src/components/tools/run-terminal-command.tsx +0 -86
- package/cli/src/components/tools/skill.tsx +0 -26
- package/cli/src/components/tools/str-replace.tsx +0 -77
- package/cli/src/components/tools/suggest-followups.tsx +0 -373
- package/cli/src/components/tools/task-completed.tsx +0 -16
- package/cli/src/components/tools/tool-call-item.tsx +0 -163
- package/cli/src/components/tools/types.ts +0 -100
- package/cli/src/components/tools/web-search.tsx +0 -32
- package/cli/src/components/tools/write-file.tsx +0 -20
- package/cli/src/components/tools/write-todos.tsx +0 -105
- package/cli/src/components/top-banner.tsx +0 -187
- package/cli/src/components/usage-banner.tsx +0 -296
- package/cli/src/components/user-error-banner.tsx +0 -54
- package/cli/src/components/validation-error-popover.tsx +0 -191
- package/cli/src/data/slash-commands.ts +0 -306
- package/cli/src/hooks/__tests__/holds-live-savant-free-slot.test.ts +0 -49
- package/cli/src/hooks/__tests__/run-outcome.test.ts +0 -178
- package/cli/src/hooks/__tests__/session-fetch-signal.test.ts +0 -45
- package/cli/src/hooks/__tests__/use-activity-query.test.ts +0 -810
- package/cli/src/hooks/__tests__/use-ask-user-bridge.test.ts +0 -175
- package/cli/src/hooks/__tests__/use-auth-query.test.ts +0 -21
- package/cli/src/hooks/__tests__/use-chat-messages-collapse.test.ts +0 -96
- package/cli/src/hooks/__tests__/use-connection-status.test.ts +0 -112
- package/cli/src/hooks/__tests__/use-directory-browser.test.ts +0 -215
- package/cli/src/hooks/__tests__/use-gravity-ad.test.ts +0 -73
- package/cli/src/hooks/__tests__/use-grid-layout.test.ts +0 -346
- package/cli/src/hooks/__tests__/use-input-history.test.ts +0 -704
- package/cli/src/hooks/__tests__/use-loop-scheduler.test.ts +0 -247
- package/cli/src/hooks/__tests__/use-path-tab-completion.test.ts +0 -324
- package/cli/src/hooks/__tests__/use-queue-controls.test.ts +0 -68
- package/cli/src/hooks/__tests__/use-searchable-list.test.ts +0 -364
- package/cli/src/hooks/__tests__/use-send-message-timer.test.ts +0 -146
- package/cli/src/hooks/__tests__/use-suggestion-engine-mention.test.ts +0 -364
- package/cli/src/hooks/__tests__/use-suggestion-engine.test.ts +0 -485
- package/cli/src/hooks/__tests__/use-terminal-focus.test.ts +0 -66
- package/cli/src/hooks/__tests__/use-terminal-layout.test.ts +0 -675
- package/cli/src/hooks/__tests__/use-timeout.test.ts +0 -330
- package/cli/src/hooks/__tests__/use-usage-query.test.ts +0 -526
- package/cli/src/hooks/__tests__/use-user-details-query.test.ts +0 -225
- package/cli/src/hooks/helpers/__tests__/send-message.test.ts +0 -1851
- package/cli/src/hooks/helpers/send-message.ts +0 -553
- package/cli/src/hooks/run-outcome.ts +0 -64
- package/cli/src/hooks/stream-state.ts +0 -106
- package/cli/src/hooks/use-activity-query.ts +0 -696
- package/cli/src/hooks/use-agent-validation.ts +0 -80
- package/cli/src/hooks/use-ask-user-bridge.ts +0 -102
- package/cli/src/hooks/use-auth-query.ts +0 -261
- package/cli/src/hooks/use-auth-state.ts +0 -155
- package/cli/src/hooks/use-chat-input.ts +0 -87
- package/cli/src/hooks/use-chat-keyboard.ts +0 -325
- package/cli/src/hooks/use-chat-messages.ts +0 -273
- package/cli/src/hooks/use-chat-state.ts +0 -219
- package/cli/src/hooks/use-chat-streaming.ts +0 -270
- package/cli/src/hooks/use-chat-ui.ts +0 -136
- package/cli/src/hooks/use-clipboard.ts +0 -154
- package/cli/src/hooks/use-connection-status.ts +0 -156
- package/cli/src/hooks/use-directory-browser.ts +0 -88
- package/cli/src/hooks/use-elapsed-time.ts +0 -116
- package/cli/src/hooks/use-event.ts +0 -36
- package/cli/src/hooks/use-exit-handler.ts +0 -108
- package/cli/src/hooks/use-fetch-login-url.ts +0 -65
- package/cli/src/hooks/use-fids.ts +0 -48
- package/cli/src/hooks/use-fingerprint.ts +0 -64
- package/cli/src/hooks/use-gravity-ad.ts +0 -688
- package/cli/src/hooks/use-grid-layout.ts +0 -78
- package/cli/src/hooks/use-input-history.ts +0 -173
- package/cli/src/hooks/use-login-keyboard-handlers.ts +0 -74
- package/cli/src/hooks/use-login-polling.ts +0 -120
- package/cli/src/hooks/use-logo.tsx +0 -178
- package/cli/src/hooks/use-loop-scheduler.ts +0 -481
- package/cli/src/hooks/use-message-queue.ts +0 -301
- package/cli/src/hooks/use-now.ts +0 -20
- package/cli/src/hooks/use-path-tab-completion.ts +0 -88
- package/cli/src/hooks/use-publish-mutation.ts +0 -60
- package/cli/src/hooks/use-queue-controls.ts +0 -70
- package/cli/src/hooks/use-queue-ui.ts +0 -55
- package/cli/src/hooks/use-savant-free-ctrl-c-exit.ts +0 -23
- package/cli/src/hooks/use-savant-free-session-progress.ts +0 -34
- package/cli/src/hooks/use-savant-free-session.ts +0 -679
- package/cli/src/hooks/use-savant-free-streak-query.ts +0 -72
- package/cli/src/hooks/use-scaffold-confirm.ts +0 -62
- package/cli/src/hooks/use-scaffold-revert-subscriber.ts +0 -66
- package/cli/src/hooks/use-scroll-management.ts +0 -182
- package/cli/src/hooks/use-searchable-list.ts +0 -99
- package/cli/src/hooks/use-send-message.ts +0 -1038
- package/cli/src/hooks/use-sheen-animation.tsx +0 -88
- package/cli/src/hooks/use-subscription-query.ts +0 -106
- package/cli/src/hooks/use-suggestion-engine.ts +0 -793
- package/cli/src/hooks/use-terminal-breakpoints.ts +0 -77
- package/cli/src/hooks/use-terminal-dimensions.ts +0 -50
- package/cli/src/hooks/use-terminal-focus.ts +0 -146
- package/cli/src/hooks/use-terminal-layout.ts +0 -189
- package/cli/src/hooks/use-theme.tsx +0 -140
- package/cli/src/hooks/use-timeout.ts +0 -88
- package/cli/src/hooks/use-toast.ts +0 -114
- package/cli/src/hooks/use-update-preference.ts +0 -77
- package/cli/src/hooks/use-usage-monitor.ts +0 -59
- package/cli/src/hooks/use-usage-query.ts +0 -138
- package/cli/src/hooks/use-user-details-query.ts +0 -104
- package/cli/src/hooks/use-why-did-you-update.ts +0 -178
- package/cli/src/index.tsx +0 -484
- package/cli/src/init/__tests__/init-direnv.test.ts +0 -597
- package/cli/src/init/init-app.ts +0 -54
- package/cli/src/init/init-direnv.ts +0 -133
- package/cli/src/login/constants.ts +0 -71
- package/cli/src/login/login-flow.ts +0 -224
- package/cli/src/login/plain-login.ts +0 -107
- package/cli/src/login/utils.ts +0 -183
- package/cli/src/native/ripgrep.ts +0 -82
- package/cli/src/polyfills/bun-strip-ansi.ts +0 -12
- package/cli/src/pre-init/load-dev-env.ts +0 -98
- package/cli/src/pre-init/tree-sitter-wasm.ts +0 -98
- package/cli/src/project-files.ts +0 -102
- package/cli/src/state/__tests__/chat-store-focus.test.ts +0 -22
- package/cli/src/state/__tests__/feedback-store.test.ts +0 -260
- package/cli/src/state/chat-history-store.ts +0 -27
- package/cli/src/state/chat-store.ts +0 -791
- package/cli/src/state/feedback-store.ts +0 -162
- package/cli/src/state/gateway-catalog-store.ts +0 -45
- package/cli/src/state/login-store.ts +0 -150
- package/cli/src/state/message-block-store.ts +0 -143
- package/cli/src/state/model-picker-store.ts +0 -67
- package/cli/src/state/provider-picker-store.ts +0 -53
- package/cli/src/state/publish-store.ts +0 -141
- package/cli/src/state/review-store.ts +0 -24
- package/cli/src/state/rewind-picker-store.ts +0 -74
- package/cli/src/state/savant-free-model-store.ts +0 -59
- package/cli/src/state/savant-free-session-store.ts +0 -32
- package/cli/src/test-setup.ts +0 -20
- package/cli/src/testing/env.ts +0 -46
- package/cli/src/types/chat-state.ts +0 -15
- package/cli/src/types/chat.ts +0 -242
- package/cli/src/types/contracts/send-message.ts +0 -16
- package/cli/src/types/env.ts +0 -107
- package/cli/src/types/function-params.ts +0 -35
- package/cli/src/types/opentui-augmentation.d.ts +0 -11
- package/cli/src/types/react19-compat.d.ts +0 -19
- package/cli/src/types/savant-free-session.ts +0 -17
- package/cli/src/types/store.ts +0 -112
- package/cli/src/types/theme-system.ts +0 -194
- package/cli/src/types/utils.ts +0 -3
- package/cli/src/utils/__tests__/activity-tracker.test.ts +0 -440
- package/cli/src/utils/__tests__/agent-display.test.ts +0 -139
- package/cli/src/utils/__tests__/analytics-client.test.ts +0 -311
- package/cli/src/utils/__tests__/anonymous-id.test.ts +0 -74
- package/cli/src/utils/__tests__/arrays.test.ts +0 -264
- package/cli/src/utils/__tests__/bash-context-processor.test.ts +0 -38
- package/cli/src/utils/__tests__/block-processor.test.ts +0 -690
- package/cli/src/utils/__tests__/chat-history.test.ts +0 -247
- package/cli/src/utils/__tests__/chat-input-key-intercept.test.ts +0 -40
- package/cli/src/utils/__tests__/chat-layout.test.ts +0 -32
- package/cli/src/utils/__tests__/chat-meta.test.ts +0 -160
- package/cli/src/utils/__tests__/chatgpt-oauth.test.ts +0 -50
- package/cli/src/utils/__tests__/clipboard.test.ts +0 -998
- package/cli/src/utils/__tests__/code-search-summary.test.ts +0 -84
- package/cli/src/utils/__tests__/collapse-helpers.test.ts +0 -1272
- package/cli/src/utils/__tests__/error-handling.test.ts +0 -611
- package/cli/src/utils/__tests__/feedback-helpers.test.ts +0 -452
- package/cli/src/utils/__tests__/feedback-submission.test.ts +0 -26
- package/cli/src/utils/__tests__/fetch-usage.test.ts +0 -355
- package/cli/src/utils/__tests__/fingerprint.test.ts +0 -147
- package/cli/src/utils/__tests__/format-elapsed-time.test.ts +0 -103
- package/cli/src/utils/__tests__/format-timeout.test.ts +0 -87
- package/cli/src/utils/__tests__/image-dimensions.test.ts +0 -243
- package/cli/src/utils/__tests__/image-processor.test.ts +0 -250
- package/cli/src/utils/__tests__/implementor-helpers.test.ts +0 -1457
- package/cli/src/utils/__tests__/keyboard-actions.test.ts +0 -686
- package/cli/src/utils/__tests__/layout-helpers.test.ts +0 -36
- package/cli/src/utils/__tests__/lazy-response-ads.test.ts +0 -167
- package/cli/src/utils/__tests__/logger-sanitize-secrets.test.ts +0 -80
- package/cli/src/utils/__tests__/markdown-renderer.test.tsx +0 -518
- package/cli/src/utils/__tests__/markdown-streaming.test.ts +0 -64
- package/cli/src/utils/__tests__/message-block-helpers.test.ts +0 -1197
- package/cli/src/utils/__tests__/message-updater.test.ts +0 -659
- package/cli/src/utils/__tests__/ollama-onboarding.test.ts +0 -239
- package/cli/src/utils/__tests__/openrouter-models.test.ts +0 -295
- package/cli/src/utils/__tests__/osc-timeout-scenarios.test.ts +0 -172
- package/cli/src/utils/__tests__/pending-attachments.test.ts +0 -278
- package/cli/src/utils/__tests__/provider-setup.test.ts +0 -183
- package/cli/src/utils/__tests__/run-state-storage.test.ts +0 -741
- package/cli/src/utils/__tests__/savant-code-api.test.ts +0 -634
- package/cli/src/utils/__tests__/savant-free-instance-owner.test.ts +0 -69
- package/cli/src/utils/__tests__/savant-free-model-navigation.test.ts +0 -104
- package/cli/src/utils/__tests__/savant-free-premium-reset.test.ts +0 -81
- package/cli/src/utils/__tests__/savant-free-referral-cache.test.ts +0 -67
- package/cli/src/utils/__tests__/savant-free-session-display.test.ts +0 -23
- package/cli/src/utils/__tests__/savant-free-streak-line.test.ts +0 -76
- package/cli/src/utils/__tests__/sdk-event-handlers.test.ts +0 -615
- package/cli/src/utils/__tests__/send-message-helpers.test.ts +0 -1875
- package/cli/src/utils/__tests__/send-message-timer.test.ts +0 -68
- package/cli/src/utils/__tests__/settings.test.ts +0 -138
- package/cli/src/utils/__tests__/strings.test.ts +0 -210
- package/cli/src/utils/__tests__/syntax-theme.test.ts +0 -91
- package/cli/src/utils/__tests__/terminal-color-detection.test.ts +0 -425
- package/cli/src/utils/__tests__/terminal-enter-detection.test.ts +0 -77
- package/cli/src/utils/__tests__/text-layout.test.ts +0 -82
- package/cli/src/utils/__tests__/think-tag-parser.test.ts +0 -159
- package/cli/src/utils/__tests__/trace-writer.test.ts +0 -193
- package/cli/src/utils/__tests__/trim-chat-logs.test.ts +0 -79
- package/cli/src/utils/__tests__/usage-banner-state.test.ts +0 -298
- package/cli/src/utils/__tests__/validation-error-formatting.test.ts +0 -126
- package/cli/src/utils/__tests__/write-file-atomic.test.ts +0 -130
- package/cli/src/utils/active-run.ts +0 -22
- package/cli/src/utils/activity-tracker.ts +0 -66
- package/cli/src/utils/agent-display.ts +0 -87
- package/cli/src/utils/agent-helpers.ts +0 -47
- package/cli/src/utils/ai-message-id.ts +0 -12
- package/cli/src/utils/analytics.ts +0 -407
- package/cli/src/utils/anonymous-id.ts +0 -87
- package/cli/src/utils/arrays.ts +0 -117
- package/cli/src/utils/auth.ts +0 -258
- package/cli/src/utils/bash-context-processor.ts +0 -47
- package/cli/src/utils/bash-messages.ts +0 -108
- package/cli/src/utils/block-margins.ts +0 -35
- package/cli/src/utils/block-operations.ts +0 -548
- package/cli/src/utils/block-processor.ts +0 -212
- package/cli/src/utils/chat-history.ts +0 -265
- package/cli/src/utils/chat-input-key-intercept.ts +0 -52
- package/cli/src/utils/chat-layout.ts +0 -44
- package/cli/src/utils/chat-meta.ts +0 -101
- package/cli/src/utils/chat-scroll-accel.ts +0 -151
- package/cli/src/utils/chatgpt-oauth.ts +0 -350
- package/cli/src/utils/clipboard-image.ts +0 -610
- package/cli/src/utils/clipboard.ts +0 -307
- package/cli/src/utils/code-search-summary.ts +0 -69
- package/cli/src/utils/collapse-helpers.ts +0 -265
- package/cli/src/utils/config-dir.ts +0 -30
- package/cli/src/utils/constants.ts +0 -159
- package/cli/src/utils/create-event-handler-state.ts +0 -79
- package/cli/src/utils/create-run-config.ts +0 -160
- package/cli/src/utils/db-storage.ts +0 -254
- package/cli/src/utils/detect-shell.ts +0 -112
- package/cli/src/utils/directory-browser.ts +0 -74
- package/cli/src/utils/engagement.ts +0 -55
- package/cli/src/utils/env.ts +0 -109
- package/cli/src/utils/error-handling.ts +0 -246
- package/cli/src/utils/error-messages.ts +0 -84
- package/cli/src/utils/feedback-helpers.ts +0 -115
- package/cli/src/utils/feedback-submission.ts +0 -24
- package/cli/src/utils/fetch-usage.ts +0 -75
- package/cli/src/utils/fid-loader.ts +0 -135
- package/cli/src/utils/fingerprint.ts +0 -263
- package/cli/src/utils/finish-logic.ts +0 -154
- package/cli/src/utils/format-elapsed-time.ts +0 -33
- package/cli/src/utils/format-session-units.ts +0 -6
- package/cli/src/utils/format-timeout.ts +0 -28
- package/cli/src/utils/format-validation-errors-for-message.ts +0 -85
- package/cli/src/utils/git.ts +0 -17
- package/cli/src/utils/glyphs.ts +0 -188
- package/cli/src/utils/helpers.ts +0 -47
- package/cli/src/utils/image-display.ts +0 -73
- package/cli/src/utils/image-handler.ts +0 -336
- package/cli/src/utils/image-processor.ts +0 -133
- package/cli/src/utils/image-thumbnail.ts +0 -83
- package/cli/src/utils/implementor-helpers.ts +0 -817
- package/cli/src/utils/input-modes.ts +0 -196
- package/cli/src/utils/keyboard-actions.ts +0 -394
- package/cli/src/utils/keypad-keys.ts +0 -47
- package/cli/src/utils/layout-helpers.ts +0 -36
- package/cli/src/utils/lazy-response-ads.ts +0 -1
- package/cli/src/utils/loading-phrases.ts +0 -68
- package/cli/src/utils/local-agent-registry.ts +0 -611
- package/cli/src/utils/log-shipper.ts +0 -116
- package/cli/src/utils/logger.ts +0 -435
- package/cli/src/utils/markdown-renderer.tsx +0 -1315
- package/cli/src/utils/math.ts +0 -3
- package/cli/src/utils/message-block-helpers.ts +0 -667
- package/cli/src/utils/message-history.ts +0 -153
- package/cli/src/utils/message-tree-utils.ts +0 -67
- package/cli/src/utils/message-updater.ts +0 -279
- package/cli/src/utils/ollama-onboarding.ts +0 -215
- package/cli/src/utils/open-file.ts +0 -144
- package/cli/src/utils/open-url.ts +0 -63
- package/cli/src/utils/openrouter-models.ts +0 -945
- package/cli/src/utils/path-completion.ts +0 -111
- package/cli/src/utils/path-helpers.ts +0 -38
- package/cli/src/utils/pending-attachments.ts +0 -363
- package/cli/src/utils/post-processing.ts +0 -99
- package/cli/src/utils/project-picker.ts +0 -12
- package/cli/src/utils/provider-setup.ts +0 -259
- package/cli/src/utils/recent-projects.ts +0 -156
- package/cli/src/utils/renderer-cleanup.ts +0 -142
- package/cli/src/utils/response-ad-positions.ts +0 -1
- package/cli/src/utils/rewind.ts +0 -175
- package/cli/src/utils/run-state-storage.ts +0 -480
- package/cli/src/utils/savant-code-api.ts +0 -686
- package/cli/src/utils/savant-code-client.ts +0 -217
- package/cli/src/utils/savant-free-agent-selection.ts +0 -12
- package/cli/src/utils/savant-free-exit.ts +0 -24
- package/cli/src/utils/savant-free-instance-owner.ts +0 -66
- package/cli/src/utils/savant-free-model-navigation.ts +0 -46
- package/cli/src/utils/savant-free-premium-reset.ts +0 -52
- package/cli/src/utils/savant-free-referral-cache.ts +0 -35
- package/cli/src/utils/savant-free-session-display.ts +0 -21
- package/cli/src/utils/savant-free-streak-line.ts +0 -67
- package/cli/src/utils/sdk-event-handlers.ts +0 -587
- package/cli/src/utils/send-message-helpers.ts +0 -199
- package/cli/src/utils/send-message-timer.ts +0 -110
- package/cli/src/utils/settings.ts +0 -388
- package/cli/src/utils/skill-registry.ts +0 -100
- package/cli/src/utils/spawn-agent-matcher.ts +0 -56
- package/cli/src/utils/status-indicator-state.ts +0 -100
- package/cli/src/utils/stream-chunk-processor.ts +0 -65
- package/cli/src/utils/strings.ts +0 -272
- package/cli/src/utils/subscription.ts +0 -31
- package/cli/src/utils/syntax-highlighter.tsx +0 -19
- package/cli/src/utils/syntax-theme.ts +0 -131
- package/cli/src/utils/terminal-color-detection.ts +0 -472
- package/cli/src/utils/terminal-enter-detection.ts +0 -63
- package/cli/src/utils/terminal-images.ts +0 -223
- package/cli/src/utils/terminal-reset-sequences.ts +0 -33
- package/cli/src/utils/terminal-title.ts +0 -113
- package/cli/src/utils/terminal-watchdog.ts +0 -246
- package/cli/src/utils/text-layout.ts +0 -153
- package/cli/src/utils/theme-config.ts +0 -143
- package/cli/src/utils/theme-system.ts +0 -1265
- package/cli/src/utils/think-tag-parser.ts +0 -105
- package/cli/src/utils/trace-writer.ts +0 -147
- package/cli/src/utils/ui-constants.ts +0 -69
- package/cli/src/utils/usage-banner-state.ts +0 -146
- package/cli/src/utils/validation-error-formatting.ts +0 -92
- package/cli/src/utils/validation-error-helpers.ts +0 -19
- package/cli/src/utils/version.ts +0 -23
- package/cli/src/utils/word-wrap-utils.ts +0 -53
- package/cli/src/utils/write-file-atomic.ts +0 -82
- package/cli/src/utils/yield-to-event-loop.ts +0 -9
- package/cli/tsconfig.json +0 -26
- package/coding-standards/csharp.md +0 -77
- package/coding-standards/go.md +0 -71
- package/coding-standards/java.md +0 -74
- package/coding-standards/python.md +0 -82
- package/coding-standards/rust.md +0 -78
- package/coding-standards/typescript.md +0 -85
- package/coding-standards/x402.md +0 -227
- package/common/README.md +0 -41
- package/common/common_exit +0 -1
- package/common/package.json +0 -45
- package/common/src/__tests__/agent-validation.test.ts +0 -861
- package/common/src/__tests__/agents.test.ts +0 -16
- package/common/src/__tests__/dynamic-agent-template-schema.test.ts +0 -420
- package/common/src/__tests__/env-ci.test.ts +0 -167
- package/common/src/__tests__/env-process.test.ts +0 -145
- package/common/src/__tests__/free-agents.test.ts +0 -269
- package/common/src/__tests__/handlesteps-parsing.test.ts +0 -247
- package/common/src/__tests__/model-config.test.ts +0 -41
- package/common/src/__tests__/project-file-tree.test.ts +0 -82
- package/common/src/__tests__/response-ad-positions.test.ts +0 -78
- package/common/src/__tests__/savant-free-models.test.ts +0 -460
- package/common/src/__tests__/savant-free-referral-tiers.test.ts +0 -122
- package/common/src/__tests__/user-state.test.ts +0 -30
- package/common/src/actions.ts +0 -215
- package/common/src/analytics-core.ts +0 -73
- package/common/src/analytics.ts +0 -96
- package/common/src/api-keys/constants.ts +0 -26
- package/common/src/browser-actions.ts +0 -414
- package/common/src/constants/agents.ts +0 -250
- package/common/src/constants/analytics-events.ts +0 -318
- package/common/src/constants/anthropic.ts +0 -72
- package/common/src/constants/byok.ts +0 -2
- package/common/src/constants/chatgpt-oauth.ts +0 -85
- package/common/src/constants/composio.ts +0 -34
- package/common/src/constants/feedback.ts +0 -18
- package/common/src/constants/free-agents.ts +0 -347
- package/common/src/constants/gemini.ts +0 -6
- package/common/src/constants/grant-priorities.ts +0 -13
- package/common/src/constants/hosts.ts +0 -6
- package/common/src/constants/images.ts +0 -53
- package/common/src/constants/index.ts +0 -8
- package/common/src/constants/knowledge.ts +0 -46
- package/common/src/constants/limits.ts +0 -23
- package/common/src/constants/model-config.ts +0 -469
- package/common/src/constants/paths.ts +0 -69
- package/common/src/constants/savant-code-config.ts +0 -12
- package/common/src/constants/savant-free-gemini-thinker.ts +0 -30
- package/common/src/constants/savant-free-models.ts +0 -855
- package/common/src/constants/savant-free-referral-tiers.ts +0 -169
- package/common/src/constants/skills.ts +0 -60
- package/common/src/constants/subscription-plans.ts +0 -54
- package/common/src/constants/ui.ts +0 -25
- package/common/src/env-ci.ts +0 -36
- package/common/src/env-process.ts +0 -95
- package/common/src/env-schema.ts +0 -42
- package/common/src/env.ts +0 -69
- package/common/src/mcp/client.ts +0 -308
- package/common/src/old-constants.ts +0 -10
- package/common/src/project-file-tree.ts +0 -361
- package/common/src/reddit-capi.ts +0 -175
- package/common/src/schemas/feedback.ts +0 -57
- package/common/src/schemas/logs.ts +0 -66
- package/common/src/templates/agent-validation.ts +0 -436
- package/common/src/templates/initial-agents-dir/LICENSE +0 -202
- package/common/src/templates/initial-agents-dir/README.md +0 -314
- package/common/src/templates/initial-agents-dir/examples/01-basic-diff-reviewer.ts +0 -17
- package/common/src/templates/initial-agents-dir/examples/02-intermediate-git-committer.ts +0 -78
- package/common/src/templates/initial-agents-dir/examples/03-advanced-file-explorer.ts +0 -76
- package/common/src/templates/initial-agents-dir/my-custom-agent.ts +0 -40
- package/common/src/templates/initial-agents-dir/package.json +0 -6
- package/common/src/templates/initial-agents-dir/skills/README.md +0 -66
- package/common/src/templates/initial-agents-dir/skills/example-skill/SKILL.md +0 -29
- package/common/src/templates/initial-agents-dir/types/agent-definition.ts +0 -484
- package/common/src/templates/initial-agents-dir/types/tools.ts +0 -452
- package/common/src/templates/initial-agents-dir/types/util-types.ts +0 -167
- package/common/src/testing/TESTING_PATTERNS.md +0 -353
- package/common/src/testing/errors.ts +0 -33
- package/common/src/testing/fixtures/agent-runtime.ts +0 -325
- package/common/src/testing/impl/agent-runtime.ts +0 -6
- package/common/src/testing/index.ts +0 -84
- package/common/src/testing/mock-modules.ts +0 -54
- package/common/src/testing/mock-types.ts +0 -123
- package/common/src/testing/mocks/analytics.ts +0 -262
- package/common/src/testing/mocks/child-process.ts +0 -93
- package/common/src/testing/mocks/crypto.ts +0 -218
- package/common/src/testing/mocks/database.ts +0 -337
- package/common/src/testing/mocks/filesystem.ts +0 -166
- package/common/src/testing/mocks/index.ts +0 -83
- package/common/src/testing/mocks/logger.ts +0 -136
- package/common/src/testing/mocks/stream.ts +0 -314
- package/common/src/testing/mocks/timers.ts +0 -132
- package/common/src/testing/mocks/tree-sitter.ts +0 -127
- package/common/src/testing/setup.ts +0 -282
- package/common/src/testing-env-ci.ts +0 -15
- package/common/src/testing-env-process.ts +0 -78
- package/common/src/tools/__tests__/compile-tool-definitions.test.ts +0 -34
- package/common/src/tools/__tests__/thought-session.test.ts +0 -256
- package/common/src/tools/compile-tool-definitions.ts +0 -194
- package/common/src/tools/constants.ts +0 -132
- package/common/src/tools/list.ts +0 -193
- package/common/src/tools/params/__tests__/coerce-to-array.test.ts +0 -237
- package/common/src/tools/params/__tests__/sequential-thinking-coercion.test.ts +0 -98
- package/common/src/tools/params/tool/add-message.ts +0 -36
- package/common/src/tools/params/tool/add-subgoal.ts +0 -57
- package/common/src/tools/params/tool/apply-patch.ts +0 -110
- package/common/src/tools/params/tool/ask-user.ts +0 -181
- package/common/src/tools/params/tool/browser-logs.ts +0 -85
- package/common/src/tools/params/tool/code-search.ts +0 -144
- package/common/src/tools/params/tool/composio.ts +0 -131
- package/common/src/tools/params/tool/create-plan.ts +0 -80
- package/common/src/tools/params/tool/end-turn.ts +0 -54
- package/common/src/tools/params/tool/find-files.ts +0 -60
- package/common/src/tools/params/tool/glob.ts +0 -74
- package/common/src/tools/params/tool/gravity-index.ts +0 -92
- package/common/src/tools/params/tool/list-directory.ts +0 -58
- package/common/src/tools/params/tool/lookup-agent-info.ts +0 -37
- package/common/src/tools/params/tool/propose-str-replace.ts +0 -103
- package/common/src/tools/params/tool/propose-write-file.ts +0 -71
- package/common/src/tools/params/tool/read-docs.ts +0 -85
- package/common/src/tools/params/tool/read-files.ts +0 -61
- package/common/src/tools/params/tool/read-subtree.ts +0 -83
- package/common/src/tools/params/tool/read-url.ts +0 -81
- package/common/src/tools/params/tool/render-ui.ts +0 -203
- package/common/src/tools/params/tool/run-file-change-hooks.ts +0 -57
- package/common/src/tools/params/tool/run-readonly-command.ts +0 -76
- package/common/src/tools/params/tool/run-terminal-command.ts +0 -185
- package/common/src/tools/params/tool/sequential-thinking.ts +0 -110
- package/common/src/tools/params/tool/set-messages.ts +0 -42
- package/common/src/tools/params/tool/set-output.ts +0 -62
- package/common/src/tools/params/tool/set-scaffold-complete.ts +0 -51
- package/common/src/tools/params/tool/skill.ts +0 -56
- package/common/src/tools/params/tool/spawn-agent-inline.ts +0 -57
- package/common/src/tools/params/tool/spawn-agents.ts +0 -154
- package/common/src/tools/params/tool/str-replace.ts +0 -107
- package/common/src/tools/params/tool/suggest-followups.ts +0 -94
- package/common/src/tools/params/tool/task-completed.ts +0 -58
- package/common/src/tools/params/tool/think-deeply.ts +0 -53
- package/common/src/tools/params/tool/transition-phase.ts +0 -57
- package/common/src/tools/params/tool/update-subgoal.ts +0 -89
- package/common/src/tools/params/tool/web-search.ts +0 -73
- package/common/src/tools/params/tool/write-file.ts +0 -71
- package/common/src/tools/params/tool/write-todos.ts +0 -67
- package/common/src/tools/params/utils.ts +0 -152
- package/common/src/tools/safety-registry.ts +0 -274
- package/common/src/tools/safety.ts +0 -42
- package/common/src/tools/sequential-thinking.ts +0 -340
- package/common/src/tools/utils.ts +0 -25
- package/common/src/types/__tests__/dynamic-agent-template.test.ts +0 -20
- package/common/src/types/agent-template.ts +0 -223
- package/common/src/types/api/agents/publish.ts +0 -63
- package/common/src/types/contracts/agent-runtime.ts +0 -84
- package/common/src/types/contracts/analytics.ts +0 -12
- package/common/src/types/contracts/bigquery.ts +0 -55
- package/common/src/types/contracts/billing.ts +0 -46
- package/common/src/types/contracts/client.ts +0 -52
- package/common/src/types/contracts/database.ts +0 -104
- package/common/src/types/contracts/env.ts +0 -203
- package/common/src/types/contracts/llm.ts +0 -183
- package/common/src/types/contracts/logger.ts +0 -29
- package/common/src/types/contracts/logs.ts +0 -34
- package/common/src/types/contracts/trace.ts +0 -21
- package/common/src/types/dynamic-agent-template.ts +0 -290
- package/common/src/types/filesystem.ts +0 -10
- package/common/src/types/function-params.ts +0 -35
- package/common/src/types/grant.ts +0 -20
- package/common/src/types/gravity-index.ts +0 -170
- package/common/src/types/json.ts +0 -26
- package/common/src/types/mcp.ts +0 -26
- package/common/src/types/messages/content-part.ts +0 -59
- package/common/src/types/messages/data-content.ts +0 -14
- package/common/src/types/messages/provider-metadata.ts +0 -10
- package/common/src/types/messages/savant-code-message.ts +0 -55
- package/common/src/types/organization.ts +0 -118
- package/common/src/types/print-mode.ts +0 -167
- package/common/src/types/publisher.ts +0 -67
- package/common/src/types/savant-free-session.ts +0 -295
- package/common/src/types/savant-free-streak.ts +0 -6
- package/common/src/types/session-state.ts +0 -237
- package/common/src/types/skill.ts +0 -56
- package/common/src/types/source.ts +0 -11
- package/common/src/types/spawn.ts +0 -13
- package/common/src/types/subscription.ts +0 -68
- package/common/src/types/usage.ts +0 -16
- package/common/src/types/util.ts +0 -3
- package/common/src/util/__tests__/analytics-dispatcher.test.ts +0 -145
- package/common/src/util/__tests__/analytics-log.test.ts +0 -110
- package/common/src/util/__tests__/analytics-sampling.test.ts +0 -131
- package/common/src/util/__tests__/axiom-only-log.test.ts +0 -64
- package/common/src/util/__tests__/engagement-tracker.test.ts +0 -115
- package/common/src/util/__tests__/error-abort.test.ts +0 -822
- package/common/src/util/__tests__/error-api-details.test.ts +0 -190
- package/common/src/util/__tests__/format-code-search.test.ts +0 -60
- package/common/src/util/__tests__/log-mirror.test.ts +0 -31
- package/common/src/util/__tests__/messages.test.ts +0 -1118
- package/common/src/util/__tests__/partial-json-delta.test.ts +0 -505
- package/common/src/util/__tests__/paths.test.ts +0 -289
- package/common/src/util/__tests__/promise.test.ts +0 -334
- package/common/src/util/__tests__/protocol-config.test.ts +0 -119
- package/common/src/util/__tests__/rate-limit.test.ts +0 -28
- package/common/src/util/__tests__/reddit-savant-free-retention.test.ts +0 -120
- package/common/src/util/__tests__/savant-free-streak.test.ts +0 -142
- package/common/src/util/__tests__/saxy.test.ts +0 -1008
- package/common/src/util/__tests__/string.test.ts +0 -238
- package/common/src/util/__tests__/zoned-time.test.ts +0 -88
- package/common/src/util/agent-file-utils.ts +0 -110
- package/common/src/util/agent-id-parsing.ts +0 -147
- package/common/src/util/agent-name-normalization.ts +0 -39
- package/common/src/util/analytics-dispatcher.ts +0 -81
- package/common/src/util/analytics-log.ts +0 -84
- package/common/src/util/analytics-sampling.ts +0 -225
- package/common/src/util/array.ts +0 -63
- package/common/src/util/axiom-only-log.ts +0 -79
- package/common/src/util/cache-debug.ts +0 -191
- package/common/src/util/credentials.ts +0 -12
- package/common/src/util/currency.ts +0 -25
- package/common/src/util/dates.ts +0 -81
- package/common/src/util/engagement-tracker.ts +0 -133
- package/common/src/util/error.ts +0 -580
- package/common/src/util/file.ts +0 -363
- package/common/src/util/format-code-search.ts +0 -115
- package/common/src/util/lazy-response-ads.ts +0 -93
- package/common/src/util/log-data.ts +0 -57
- package/common/src/util/log-mirror.ts +0 -34
- package/common/src/util/lru-cache.ts +0 -67
- package/common/src/util/messages.ts +0 -600
- package/common/src/util/model-utils.ts +0 -25
- package/common/src/util/object.ts +0 -137
- package/common/src/util/param-helpers.ts +0 -49
- package/common/src/util/partial-json-delta.ts +0 -100
- package/common/src/util/paths.ts +0 -194
- package/common/src/util/promise.ts +0 -87
- package/common/src/util/protocol-config.ts +0 -141
- package/common/src/util/random.ts +0 -15
- package/common/src/util/rate-limit.ts +0 -56
- package/common/src/util/reddit-capi-events.ts +0 -40
- package/common/src/util/reddit-savant-free-retention.ts +0 -69
- package/common/src/util/response-ad-positions.ts +0 -54
- package/common/src/util/savant-free-privacy.ts +0 -78
- package/common/src/util/savant-free-streak.ts +0 -153
- package/common/src/util/saxy.ts +0 -739
- package/common/src/util/skills.ts +0 -32
- package/common/src/util/stop-sequence.ts +0 -60
- package/common/src/util/string.ts +0 -419
- package/common/src/util/system-info.ts +0 -51
- package/common/src/util/type-narrowing.ts +0 -125
- package/common/src/util/xml.ts +0 -17
- package/common/src/util/zod-schema.ts +0 -80
- package/common/src/util/zoned-time.ts +0 -136
- package/common/src/utils/ask-user-bridge.ts +0 -47
- package/common/tsconfig.json +0 -8
- package/database.db +0 -0
- package/dev/LEARNINGS.md +0 -309
- package/dev/fids/.gitkeep +0 -0
- package/dev/fids/archive/.gitkeep +0 -0
- package/dev/fids/archive/FID-2026-0802-008-sdk-package-audit-client-run-run-state.md +0 -238
- package/dev/fids/archive/FID-2026-0803-001-echo-enforcement-layer-drift.md +0 -404
- package/dev/fids/archive/FID-2026-0803-002-llm-providers-database-audit.md +0 -348
- package/dev/fids/archive/FID-2026-0803-003-sdk-impl-common-util-audit.md +0 -277
- package/dev/fids/archive/FID-2026-0803-004-checkpoint-rewind.md +0 -181
- package/dev/fids/archive/FID-2026-0803-005-quality-scan-hygiene-fixes.md +0 -265
- package/dev/fids/archive/FID-2026-0803-006-code-map-audit-hygiene.md +0 -243
- package/dev/fids/archive/FID-2026-0803-007-evals-benchmark-audit-hygiene.md +0 -296
- package/dev/fids/archive/FID-2026-0803-009-echo-enforcement-doc-drift.md +0 -112
- package/dev/fids/archive/FID-2026-0803-010-database-llm-providers-low-fixes.md +0 -255
- package/dev/fids/archive/FID-2026-0803-011-build-artifact-hygiene.md +0 -169
- package/dev/fids/archive/FID-2026-0803-012-release-readiness-audit.md +0 -162
- package/dev/fids/archive/FID-2026-0803-013-agent-roster-over-reporting.md +0 -146
- package/dev/fids/archive/FID-2026-0803-014-freebuff-to-savant-rebrand-sweep.md +0 -185
- package/dev/nova/reports/2026-08-02-top-25-ai-inference-providers-competitive-intel.md +0 -213
- package/dev/nova/specs/echo-v0.1.2-freebuff.md +0 -391
- package/dev/nova/specs/goal-loop-feature-spec.md +0 -245
- package/dev/nova/specs/launch-strategy-research-prompt.md +0 -88
- package/dev/releases/README.md +0 -50
- package/dev/scratchpad/.gitkeep +0 -0
- package/dev/session-summaries/.gitkeep +0 -0
- package/dev/session-summaries/2026-07-16-1255.md +0 -128
- package/dev/session-summaries/2026-07-17-1000.md +0 -64
- package/dev/session-summaries/2026-07-19-eslint-zero-tolerance-cleanup.md +0 -145
- package/dev/session-summaries/2026-07-19-fid-026-debugging-and-rename.md +0 -79
- package/dev/session-summaries/2026-07-19-fid-026-phase-b-rebrand.md +0 -76
- package/dev/session-summaries/2026-07-19-fid-027-clean-break.md +0 -47
- package/dev/session-summaries/2026-07-19-pre-push-house-cleaning.md +0 -100
- package/dev/session-summaries/2026-07-19-v0.0.2-release-session.md +0 -229
- package/dev/session-summaries/2026-07-20-1805-fid-loop-closure.md +0 -95
- package/dev/session-summaries/2026-07-22-1800.md +0 -52
- package/dev/session-summaries/2026-07-23-fsm-optimization.md +0 -88
- package/dev/session-summaries/2026-07-25-0000.md +0 -80
- package/dev/session-summaries/2026-07-25-1200-context-compaction.md +0 -154
- package/dev/session-summaries/2026-07-25-1600-layer4-reactive-compact.md +0 -96
- package/dev/session-summaries/2026-07-25-1700-dev-folder-audit.md +0 -161
- package/dev/session-summaries/2026-07-25-2000-benchmark-v2-filters.md +0 -147
- package/dev/session-summaries/2026-07-25-2000.md +0 -77
- package/dev/session-summaries/2026-07-27-0000.md +0 -63
- package/dev/session-summaries/2026-07-28-history-capture-handoff.md +0 -206
- package/dev/session-summaries/2026-07-31-freebuff-echo-compliance-remediation.md +0 -156
- package/dev/session-summaries/2026-07-31-pre-launch-optimization-execution.md +0 -46
- package/dev/session-summaries/2026-0721-1800.md +0 -44
- package/dev/session-summaries/2026-08-01-sidebar-folded-startup.md +0 -159
- package/dev/session-summaries/2026-08-02-0.0.15-release-closeout.md +0 -32
- package/dev/session-summaries/2026-08-02-repository-hygiene.md +0 -51
- package/dev/session-summaries/2026-08-03-agent-roster-fix-and-ready-check-closeout.md +0 -73
- package/dev/session-summaries/2026-08-03-build-artifact-hygiene-closeout.md +0 -40
- package/dev/session-summaries/2026-08-03-code-map-audit-closeout.md +0 -44
- package/dev/session-summaries/2026-08-03-database-llm-providers-low-fixes-closeout.md +0 -47
- package/dev/session-summaries/2026-08-03-echo-enforcement-doc-drift-closeout.md +0 -43
- package/dev/session-summaries/2026-08-03-evals-benchmark-audit-closeout.md +0 -50
- package/dev/session-summaries/2026-08-03-quality-scan-hygiene-fixes-closeout.md +0 -57
- package/dev/session-summaries/2026-08-03-quality-session-checkpoint-rewind-closeout.md +0 -56
- package/dev/session-summaries/2026-08-03-release-readiness-audit-closeout.md +0 -60
- package/dev/test-prompts/archive/0.0.2-final-pass.md +0 -209
- package/dev/test-prompts/archive/agent-capabilities-full-test.md +0 -361
- package/dev/test-prompts/archive/agent-capabilities-test.md +0 -471
- package/dev/test-prompts/archive/comprehensive-az-system-test-v1.md +0 -441
- package/dev/test-prompts/archive/comprehensive-az-system-test-v2-report.md +0 -377
- package/dev/test-prompts/archive/comprehensive-az-system-test-v2.md +0 -544
- package/dev/test-prompts/archive/comprehensive-az-system-test.md +0 -396
- package/dev/test-prompts/archive/comprehensive-az-test-final.md +0 -900
- package/dev/test-prompts/archive/comprehensive-az-test-v11.md +0 -141
- package/dev/test-prompts/archive/comprehensive-az-test-v12.md +0 -29
- package/dev/test-prompts/archive/comprehensive-az-test-v6.md +0 -631
- package/dev/test-prompts/archive/comprehensive-az-test-v7.md +0 -625
- package/dev/test-prompts/archive/fid-007-ability-confirmation.md +0 -248
- package/dev/test-prompts/archive/fid-2026-0801-006-thinker-tool-call-boundary.md +0 -86
- package/dev/test-prompts/archive/fid-2026-0801-007-child-tool-set-fallback-cli.md +0 -101
- package/dev/test-prompts/archive/fid-2026-0801-008-provider-tool-call-accumulation-cli.md +0 -111
- package/dev/test-prompts/archive/fid-2026-0801-012-thinker-state-output-cli.md +0 -134
- package/dev/test-prompts/archive/gemini-deep-research-sidebar-highlight.md +0 -598
- package/dev/test-prompts/archive/goal-loop-cli-test.md +0 -142
- package/dev/test-prompts/archive/rebrand-qa.md +0 -258
- package/dev/test-prompts/archive/release-az-test-fid-085.md +0 -306
- package/dev/test-prompts/archive/release-az-test-fid-087.md +0 -219
- package/dev/test-prompts/archive/release-az-test-fid-2026-0726-001.md +0 -270
- package/dev/test-prompts/archive/release-az-test-fid-2026-0728-008.md +0 -213
- package/dev/test-prompts/archive/release-az-test-fid-2026-0728-launch-tracks.md +0 -253
- package/dev/test-prompts/archive/release-az-test-fid-2026-0731-pre-launch.md +0 -102
- package/docs/AI Coding Agents Market Research.md +0 -516
- package/docs/Agent Harness Feature Pairing Research.md +0 -166
- package/docs/CLI Agent Inference Backend Research.md +0 -446
- package/docs/Codebuff Rebranding And Migration Plan.md +0 -418
- package/docs/ECHO-EVOLUTION-ARCH.md +0 -77
- package/docs/Harness Engineering for Coding Agents Research.md +0 -196
- package/docs/Launch Plan Review and Optimization.md +0 -178
- package/docs/OpenTUI Sidebar Bug Fix.md +0 -245
- package/docs/SAVANT-VERSIONING.md +0 -31
- package/docs/Savant Code Launch Strategy.md +0 -270
- package/docs/Savant-Code Business And Backend Research.md +0 -222
- package/docs/agents-and-tools.md +0 -217
- package/docs/cloudflare-llms-full.md +0 -9623
- package/docs/design/OpenTUI Terminal Visualization Guide.md +0 -471
- package/docs/design/database-architecture.md +0 -217
- package/docs/design/deep-research-report.md +0 -295
- package/docs/design/thinker-sequentialthinking-regression-diagnostic.md +0 -366
- package/docs/discord-server-design.md +0 -167
- package/docs/gravity-integration-starter.md +0 -202
- package/docs/launch/hn-first-comment.md +0 -44
- package/docs/launch/hn-post.md +0 -35
- package/docs/launch/incident-response.md +0 -88
- package/docs/launch/landing/index.html +0 -233
- package/docs/launch/mastodon-thread.md +0 -52
- package/docs/launch/newsletter-pitch.md +0 -49
- package/docs/launch/twitter-thread.md +0 -62
- package/docs/privacy.md +0 -186
- package/docs/reports/Savant-Code Benchmark Specification.md +0 -206
- package/docs/reports/Thinker Agent Architecture Research.md +0 -284
- package/docs/reports/adoptable-features-2026-07-25.md +0 -604
- package/docs/reports/adoptable-features-master.md +0 -480
- package/docs/reports/codebuff-discord-feedback.md +0 -43
- package/docs/reports/feature-parity-report.md +0 -810
- package/docs/reports/repos/AionUi.md +0 -75
- package/docs/reports/repos/OpenHands.md +0 -76
- package/docs/reports/repos/SWE-agent.md +0 -82
- package/docs/reports/repos/agno.md +0 -89
- package/docs/reports/repos/aider.md +0 -74
- package/docs/reports/repos/cline.md +0 -83
- package/docs/reports/repos/codex.md +0 -83
- package/docs/reports/repos/gemini-cli.md +0 -92
- package/docs/reports/repos/goose.md +0 -65
- package/docs/reports/repos/gpt-pilot.md +0 -52
- package/docs/reports/repos/hermes-agent.md +0 -101
- package/docs/reports/repos/kilocode.md +0 -79
- package/docs/reports/repos/openclaude.md +0 -99
- package/docs/reports/repos/openclaw.md +0 -100
- package/docs/reports/repos/opencode-dev.md +0 -78
- package/docs/reports/repos/zero.md +0 -123
- package/docs/reports/savant-code-benchmark-v2-2026-08-03.md +0 -76
- package/docs/savant-code-modes.md +0 -338
- package/docs/testing.md +0 -51
- package/docs/visual-mockup-neon-slate.txt +0 -41
- package/eslint.config.js +0 -211
- package/evals/README.md +0 -45
- package/evals/benchmark/README.md +0 -449
- package/evals/benchmark/agent-runner.ts +0 -217
- package/evals/benchmark/analyze-task-scores.ts +0 -496
- package/evals/benchmark/eval-codebuff-hard.json +0 -3343
- package/evals/benchmark/eval-codebuff.json +0 -3193
- package/evals/benchmark/eval-codebuff2.json +0 -2494
- package/evals/benchmark/eval-manifold-hard.json +0 -2525
- package/evals/benchmark/eval-manifold.json +0 -1675
- package/evals/benchmark/eval-manifold2.json +0 -1945
- package/evals/benchmark/eval-plane-hard.json +0 -4372
- package/evals/benchmark/eval-plane.json +0 -2028
- package/evals/benchmark/eval-plane2.json +0 -3517
- package/evals/benchmark/eval-saleor-hard.json +0 -6081
- package/evals/benchmark/eval-saleor.json +0 -1829
- package/evals/benchmark/eval-saleor2.json +0 -3273
- package/evals/benchmark/eval-task-generator.ts +0 -158
- package/evals/benchmark/filter-supplemental-files.ts +0 -225
- package/evals/benchmark/format-output.ts +0 -215
- package/evals/benchmark/gen-evals.ts +0 -294
- package/evals/benchmark/gen-repo-eval.ts +0 -71
- package/evals/benchmark/judge.ts +0 -308
- package/evals/benchmark/lessons-extractor.ts +0 -247
- package/evals/benchmark/main-hard-tasks.ts +0 -49
- package/evals/benchmark/main-single-eval.ts +0 -23
- package/evals/benchmark/main.ts +0 -29
- package/evals/benchmark/meta-analyzer.ts +0 -329
- package/evals/benchmark/pick-commits.ts +0 -627
- package/evals/benchmark/run-benchmark.ts +0 -657
- package/evals/benchmark/runners/claude.ts +0 -176
- package/evals/benchmark/runners/codex.ts +0 -143
- package/evals/benchmark/runners/index.ts +0 -4
- package/evals/benchmark/runners/opencode.ts +0 -253
- package/evals/benchmark/runners/runner.ts +0 -13
- package/evals/benchmark/runners/savant.ts +0 -148
- package/evals/benchmark/setup-test-repo.ts +0 -314
- package/evals/benchmark/trace-analyzer.ts +0 -257
- package/evals/benchmark/trace-utils.ts +0 -80
- package/evals/benchmark/types.ts +0 -83
- package/evals/bunfig.toml +0 -4
- package/evals/logger.ts +0 -87
- package/evals/package.json +0 -31
- package/evals/subagents/test-repo-utils.ts +0 -131
- package/evals/tsconfig.json +0 -13
- package/evals/v2/README.md +0 -149
- package/evals/v2/reports/report.json +0 -261
- package/evals/v2/reports/report.md +0 -15
- package/evals/v2/schema/task.schema.json +0 -172
- package/evals/v2/src/cli.ts +0 -216
- package/evals/v2/src/golden.ts +0 -73
- package/evals/v2/src/harness.ts +0 -293
- package/evals/v2/src/metrics.ts +0 -326
- package/evals/v2/src/registry.ts +0 -179
- package/evals/v2/src/reports.ts +0 -126
- package/evals/v2/src/runner.ts +0 -143
- package/evals/v2/src/runners/savant.ts +0 -105
- package/evals/v2/src/sandbox.ts +0 -42
- package/evals/v2/src/sandboxes/docker.ts +0 -49
- package/evals/v2/src/sandboxes/tempdir.ts +0 -120
- package/evals/v2/src/schema.ts +0 -83
- package/evals/v2/src/trace.ts +0 -186
- package/evals/v2/src/verify.ts +0 -199
- package/evals/v2/tasks/error_recovery/env-fault/calculator.js +0 -11
- package/evals/v2/tasks/error_recovery/env-fault/calculator.test.js +0 -15
- package/evals/v2/tasks/error_recovery/env-fault/golden.patch +0 -15
- package/evals/v2/tasks/error_recovery/env-fault/task.yaml +0 -20
- package/evals/v2/tasks/multi_agent_orchestration/options-contract/app.js +0 -5
- package/evals/v2/tasks/multi_agent_orchestration/options-contract/app.test.js +0 -7
- package/evals/v2/tasks/multi_agent_orchestration/options-contract/golden.patch +0 -19
- package/evals/v2/tasks/multi_agent_orchestration/options-contract/greet.js +0 -3
- package/evals/v2/tasks/multi_agent_orchestration/options-contract/orchestration.test.js +0 -30
- package/evals/v2/tasks/multi_agent_orchestration/options-contract/task.yaml +0 -29
- package/evals/v2/tasks/pure_coding/add-fix/add.js +0 -3
- package/evals/v2/tasks/pure_coding/add-fix/add.test.js +0 -11
- package/evals/v2/tasks/pure_coding/add-fix/golden.patch +0 -7
- package/evals/v2/tasks/pure_coding/add-fix/task.yaml +0 -20
- package/evals/v2/tasks/pure_coding/rename-greet/app.js +0 -5
- package/evals/v2/tasks/pure_coding/rename-greet/app.test.js +0 -32
- package/evals/v2/tasks/pure_coding/rename-greet/golden.patch +0 -19
- package/evals/v2/tasks/pure_coding/rename-greet/greet.js +0 -5
- package/evals/v2/tasks/pure_coding/rename-greet/task.yaml +0 -21
- package/evals/v2/tests/golden.test.ts +0 -104
- package/evals/v2/tests/harness.test.ts +0 -204
- package/evals/v2/tests/metrics.test.ts +0 -353
- package/evals/v2/tests/registry.test.ts +0 -210
- package/evals/v2/tests/reports.test.ts +0 -218
- package/evals/v2/tests/savant-runner.test.ts +0 -218
- package/evals/v2/tests/schema.test.ts +0 -127
- package/evals/v2/tests/tempdir-sandbox.test.ts +0 -49
- package/evals/v2/tests/trace.test.ts +0 -76
- package/evals/v2/tests/verify.test.ts +0 -224
- package/license +0 -202
- package/packages/agent-runtime/README.md +0 -46
- package/packages/agent-runtime/agent_exit +0 -1
- package/packages/agent-runtime/bunfig.toml +0 -2
- package/packages/agent-runtime/package.json +0 -34
- package/packages/agent-runtime/src/__tests__/apply-patch-tool.test.ts +0 -202
- package/packages/agent-runtime/src/__tests__/cost-aggregation.test.ts +0 -397
- package/packages/agent-runtime/src/__tests__/generate-diffs-prompt.test.ts +0 -107
- package/packages/agent-runtime/src/__tests__/get-file-reading-updates.test.ts +0 -59
- package/packages/agent-runtime/src/__tests__/gravity-index-tool.test.ts +0 -501
- package/packages/agent-runtime/src/__tests__/loop-agent-steps.test.ts +0 -1512
- package/packages/agent-runtime/src/__tests__/main-prompt.test.ts +0 -452
- package/packages/agent-runtime/src/__tests__/n-parameter.test.ts +0 -984
- package/packages/agent-runtime/src/__tests__/process-file-block.test.ts +0 -196
- package/packages/agent-runtime/src/__tests__/process-str-replace.test.ts +0 -512
- package/packages/agent-runtime/src/__tests__/programmatic-tool-authorization.test.ts +0 -219
- package/packages/agent-runtime/src/__tests__/prompt-caching-subagents.test.ts +0 -749
- package/packages/agent-runtime/src/__tests__/prompts-schema-handling.test.ts +0 -454
- package/packages/agent-runtime/src/__tests__/propose-tools.test.ts +0 -824
- package/packages/agent-runtime/src/__tests__/read-docs-tool.test.ts +0 -394
- package/packages/agent-runtime/src/__tests__/run-agent-step-prefill.test.ts +0 -161
- package/packages/agent-runtime/src/__tests__/run-agent-step-tools.test.ts +0 -612
- package/packages/agent-runtime/src/__tests__/run-programmatic-step.test.ts +0 -1743
- package/packages/agent-runtime/src/__tests__/sandbox-generator.test.ts +0 -154
- package/packages/agent-runtime/src/__tests__/spawn-agents-image-content.test.ts +0 -354
- package/packages/agent-runtime/src/__tests__/spawn-agents-message-history.test.ts +0 -280
- package/packages/agent-runtime/src/__tests__/spawn-agents-permissions.test.ts +0 -664
- package/packages/agent-runtime/src/__tests__/stream-parser-abort.test.ts +0 -250
- package/packages/agent-runtime/src/__tests__/stream-parser-reasoning.test.ts +0 -194
- package/packages/agent-runtime/src/__tests__/subagent-streaming.test.ts +0 -230
- package/packages/agent-runtime/src/__tests__/test-utils.ts +0 -96
- package/packages/agent-runtime/src/__tests__/thinker-convergence-gate.test.ts +0 -242
- package/packages/agent-runtime/src/__tests__/to-token-count-input-schema.test.ts +0 -81
- package/packages/agent-runtime/src/__tests__/tool-executor-sandbox.test.ts +0 -167
- package/packages/agent-runtime/src/__tests__/tool-stream-parser.test.ts +0 -682
- package/packages/agent-runtime/src/__tests__/tool-validation-error.test.ts +0 -1380
- package/packages/agent-runtime/src/__tests__/web-search-tool.test.ts +0 -402
- package/packages/agent-runtime/src/__tests__/xml-tool-result-ordering.test.ts +0 -264
- package/packages/agent-runtime/src/constants.ts +0 -12
- package/packages/agent-runtime/src/context-compactor.ts +0 -425
- package/packages/agent-runtime/src/find-files/__tests__/request-files-prompt.test.ts +0 -190
- package/packages/agent-runtime/src/find-files/custom-file-picker-config.ts +0 -61
- package/packages/agent-runtime/src/find-files/request-files-prompt.ts +0 -462
- package/packages/agent-runtime/src/generate-diffs-prompt.ts +0 -38
- package/packages/agent-runtime/src/get-file-reading-updates.ts +0 -27
- package/packages/agent-runtime/src/llm-api/__tests__/gemini-with-fallbacks.test.ts +0 -267
- package/packages/agent-runtime/src/llm-api/__tests__/serper-api.test.ts +0 -297
- package/packages/agent-runtime/src/llm-api/claude.ts +0 -7
- package/packages/agent-runtime/src/llm-api/context7-api.ts +0 -290
- package/packages/agent-runtime/src/llm-api/gemini-with-fallbacks.ts +0 -104
- package/packages/agent-runtime/src/llm-api/savant-code-web-api.ts +0 -370
- package/packages/agent-runtime/src/llm-api/serper-api.ts +0 -193
- package/packages/agent-runtime/src/main-prompt.ts +0 -251
- package/packages/agent-runtime/src/mcp-constants.ts +0 -12
- package/packages/agent-runtime/src/mcp.ts +0 -83
- package/packages/agent-runtime/src/process-file-block.ts +0 -118
- package/packages/agent-runtime/src/process-str-replace.ts +0 -213
- package/packages/agent-runtime/src/prompt-agent-stream.ts +0 -119
- package/packages/agent-runtime/src/run-agent-step.ts +0 -1632
- package/packages/agent-runtime/src/run-programmatic-step.ts +0 -633
- package/packages/agent-runtime/src/system-prompt/prompts.ts +0 -215
- package/packages/agent-runtime/src/system-prompt/search-system-prompt.ts +0 -89
- package/packages/agent-runtime/src/system-prompt/truncate-file-tree.ts +0 -414
- package/packages/agent-runtime/src/templates/README.md +0 -160
- package/packages/agent-runtime/src/templates/__tests__/agent-registry.test.ts +0 -489
- package/packages/agent-runtime/src/templates/__tests__/strings.test.ts +0 -465
- package/packages/agent-runtime/src/templates/agent-registry.ts +0 -122
- package/packages/agent-runtime/src/templates/prompts.ts +0 -189
- package/packages/agent-runtime/src/templates/strings.ts +0 -292
- package/packages/agent-runtime/src/templates/types.ts +0 -61
- package/packages/agent-runtime/src/tool-stream-parser.ts +0 -260
- package/packages/agent-runtime/src/tools/filter-tool-set.ts +0 -18
- package/packages/agent-runtime/src/tools/handlers/__tests__/glob.test.ts +0 -331
- package/packages/agent-runtime/src/tools/handlers/__tests__/read-subtree.test.ts +0 -346
- package/packages/agent-runtime/src/tools/handlers/__tests__/run-readonly-command.test.ts +0 -284
- package/packages/agent-runtime/src/tools/handlers/handler-function-type.ts +0 -77
- package/packages/agent-runtime/src/tools/handlers/list.ts +0 -105
- package/packages/agent-runtime/src/tools/handlers/tool/__tests__/checkpoint-store.test.ts +0 -394
- package/packages/agent-runtime/src/tools/handlers/tool/__tests__/skill.test.ts +0 -98
- package/packages/agent-runtime/src/tools/handlers/tool/__tests__/write-file.test.ts +0 -24
- package/packages/agent-runtime/src/tools/handlers/tool/add-message.ts +0 -37
- package/packages/agent-runtime/src/tools/handlers/tool/add-subgoal.ts +0 -30
- package/packages/agent-runtime/src/tools/handlers/tool/apply-patch.ts +0 -120
- package/packages/agent-runtime/src/tools/handlers/tool/ask-user.ts +0 -25
- package/packages/agent-runtime/src/tools/handlers/tool/browser-logs.ts +0 -21
- package/packages/agent-runtime/src/tools/handlers/tool/checkpoint-store.ts +0 -378
- package/packages/agent-runtime/src/tools/handlers/tool/code-search.ts +0 -21
- package/packages/agent-runtime/src/tools/handlers/tool/composio.ts +0 -52
- package/packages/agent-runtime/src/tools/handlers/tool/create-plan.ts +0 -63
- package/packages/agent-runtime/src/tools/handlers/tool/end-turn.ts +0 -15
- package/packages/agent-runtime/src/tools/handlers/tool/find-files.ts +0 -161
- package/packages/agent-runtime/src/tools/handlers/tool/glob.ts +0 -22
- package/packages/agent-runtime/src/tools/handlers/tool/gravity-index.ts +0 -230
- package/packages/agent-runtime/src/tools/handlers/tool/list-directory.ts +0 -22
- package/packages/agent-runtime/src/tools/handlers/tool/lookup-agent-info.ts +0 -105
- package/packages/agent-runtime/src/tools/handlers/tool/propose-str-replace.ts +0 -111
- package/packages/agent-runtime/src/tools/handlers/tool/propose-write-file.ts +0 -89
- package/packages/agent-runtime/src/tools/handlers/tool/proposed-content-store.ts +0 -64
- package/packages/agent-runtime/src/tools/handlers/tool/read-docs.ts +0 -160
- package/packages/agent-runtime/src/tools/handlers/tool/read-files.ts +0 -43
- package/packages/agent-runtime/src/tools/handlers/tool/read-subtree.ts +0 -198
- package/packages/agent-runtime/src/tools/handlers/tool/read-url.ts +0 -21
- package/packages/agent-runtime/src/tools/handlers/tool/render-ui.ts +0 -15
- package/packages/agent-runtime/src/tools/handlers/tool/run-file-change-hooks.ts +0 -20
- package/packages/agent-runtime/src/tools/handlers/tool/run-readonly-command.ts +0 -215
- package/packages/agent-runtime/src/tools/handlers/tool/run-terminal-command.ts +0 -33
- package/packages/agent-runtime/src/tools/handlers/tool/sequential-thinking.ts +0 -53
- package/packages/agent-runtime/src/tools/handlers/tool/set-messages.ts +0 -19
- package/packages/agent-runtime/src/tools/handlers/tool/set-output.ts +0 -104
- package/packages/agent-runtime/src/tools/handlers/tool/set-scaffold-complete.ts +0 -32
- package/packages/agent-runtime/src/tools/handlers/tool/skill.ts +0 -146
- package/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-inline.ts +0 -149
- package/packages/agent-runtime/src/tools/handlers/tool/spawn-agent-utils.ts +0 -436
- package/packages/agent-runtime/src/tools/handlers/tool/spawn-agents.ts +0 -286
- package/packages/agent-runtime/src/tools/handlers/tool/str-replace.ts +0 -135
- package/packages/agent-runtime/src/tools/handlers/tool/suggest-followups.ts +0 -20
- package/packages/agent-runtime/src/tools/handlers/tool/task-completed.ts +0 -15
- package/packages/agent-runtime/src/tools/handlers/tool/think-deeply.ts +0 -25
- package/packages/agent-runtime/src/tools/handlers/tool/transition-phase.ts +0 -160
- package/packages/agent-runtime/src/tools/handlers/tool/update-subgoal.ts +0 -48
- package/packages/agent-runtime/src/tools/handlers/tool/web-search.ts +0 -151
- package/packages/agent-runtime/src/tools/handlers/tool/write-file.ts +0 -281
- package/packages/agent-runtime/src/tools/handlers/tool/write-todos.ts +0 -19
- package/packages/agent-runtime/src/tools/prompts.ts +0 -233
- package/packages/agent-runtime/src/tools/sandbox/__tests__/engine.test.ts +0 -169
- package/packages/agent-runtime/src/tools/sandbox/__tests__/shell-denylist.test.ts +0 -104
- package/packages/agent-runtime/src/tools/sandbox/engine.ts +0 -109
- package/packages/agent-runtime/src/tools/sandbox/index.ts +0 -14
- package/packages/agent-runtime/src/tools/sandbox/shell-denylist.ts +0 -119
- package/packages/agent-runtime/src/tools/stream-parser.ts +0 -407
- package/packages/agent-runtime/src/tools/thinker-convergence-gate.ts +0 -162
- package/packages/agent-runtime/src/tools/thought-session-store.ts +0 -43
- package/packages/agent-runtime/src/tools/tool-executor.ts +0 -1098
- package/packages/agent-runtime/src/util/__tests__/messages.test.ts +0 -995
- package/packages/agent-runtime/src/util/__tests__/parse-tool-calls-from-text.test.ts +0 -363
- package/packages/agent-runtime/src/util/__tests__/simplify-tool-results.test.ts +0 -382
- package/packages/agent-runtime/src/util/__tests__/stream-xml-parser.test.ts +0 -285
- package/packages/agent-runtime/src/util/__tests__/think-tags.test.ts +0 -49
- package/packages/agent-runtime/src/util/__tests__/token-counter.test.ts +0 -121
- package/packages/agent-runtime/src/util/activity-tracking.ts +0 -252
- package/packages/agent-runtime/src/util/agent-output.ts +0 -102
- package/packages/agent-runtime/src/util/cache-debug.ts +0 -330
- package/packages/agent-runtime/src/util/format-value.ts +0 -16
- package/packages/agent-runtime/src/util/messages.ts +0 -461
- package/packages/agent-runtime/src/util/parse-tool-calls-from-text.ts +0 -122
- package/packages/agent-runtime/src/util/render-read-files-result.ts +0 -18
- package/packages/agent-runtime/src/util/simplify-tool-results.ts +0 -61
- package/packages/agent-runtime/src/util/stream-xml-parser.ts +0 -259
- package/packages/agent-runtime/src/util/think-tags.ts +0 -33
- package/packages/agent-runtime/src/util/token-counter.ts +0 -136
- package/packages/agent-runtime/tsconfig.json +0 -8
- package/packages/code-map/README.md +0 -43
- package/packages/code-map/__tests__/integration.test.ts +0 -278
- package/packages/code-map/__tests__/languages.test.ts +0 -240
- package/packages/code-map/__tests__/parse.test.ts +0 -640
- package/packages/code-map/__tests__/test-langs/test.c +0 -42
- package/packages/code-map/__tests__/test-langs/test.cpp +0 -34
- package/packages/code-map/__tests__/test-langs/test.cs +0 -34
- package/packages/code-map/__tests__/test-langs/test.go +0 -27
- package/packages/code-map/__tests__/test-langs/test.java +0 -31
- package/packages/code-map/__tests__/test-langs/test.js +0 -50
- package/packages/code-map/__tests__/test-langs/test.php +0 -34
- package/packages/code-map/__tests__/test-langs/test.py +0 -25
- package/packages/code-map/__tests__/test-langs/test.rb +0 -28
- package/packages/code-map/__tests__/test-langs/test.rs +0 -28
- package/packages/code-map/__tests__/test-langs/test.ts +0 -31
- package/packages/code-map/package.json +0 -33
- package/packages/code-map/src/index.ts +0 -4
- package/packages/code-map/src/init-node.ts +0 -185
- package/packages/code-map/src/languages.ts +0 -344
- package/packages/code-map/src/parse.ts +0 -410
- package/packages/code-map/src/tree-sitter-queries/readme.md +0 -24
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-c-tags.scm +0 -16
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-c_sharp-tags.scm +0 -23
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-cpp-tags.scm +0 -29
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-go-tags.scm +0 -26
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-java-tags.scm +0 -19
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-javascript-tags.scm +0 -16
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-php-tags.scm +0 -23
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-python-tags.scm +0 -12
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-ruby-tags.scm +0 -58
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-rust-tags.scm +0 -26
- package/packages/code-map/src/tree-sitter-queries/tree-sitter-typescript-tags.scm +0 -22
- package/packages/code-map/src/types.ts +0 -11
- package/packages/code-map/src/utils.ts +0 -12
- package/packages/code-map/tsconfig.json +0 -10
- package/packages/database/README.md +0 -43
- package/packages/database/package.json +0 -28
- package/packages/database/src/__tests__/service.test.ts +0 -167
- package/packages/database/src/index.ts +0 -146
- package/packages/database/src/service.ts +0 -342
- package/packages/database/tsconfig.json +0 -9
- package/packages/llm-providers/README.md +0 -42
- package/packages/llm-providers/package.json +0 -42
- package/packages/llm-providers/src/ollama/__tests__/detect.test.ts +0 -59
- package/packages/llm-providers/src/ollama/detect.ts +0 -113
- package/packages/llm-providers/src/ollama/index.ts +0 -2
- package/packages/llm-providers/src/openai-compatible/chat/convert-to-openai-compatible-chat-messages.test.ts +0 -855
- package/packages/llm-providers/src/openai-compatible/chat/convert-to-openai-compatible-chat-messages.ts +0 -177
- package/packages/llm-providers/src/openai-compatible/chat/get-response-metadata.ts +0 -15
- package/packages/llm-providers/src/openai-compatible/chat/map-openai-compatible-finish-reason.ts +0 -19
- package/packages/llm-providers/src/openai-compatible/chat/openai-compatible-api-types.ts +0 -60
- package/packages/llm-providers/src/openai-compatible/chat/openai-compatible-chat-language-model.test.ts +0 -549
- package/packages/llm-providers/src/openai-compatible/chat/openai-compatible-chat-language-model.ts +0 -474
- package/packages/llm-providers/src/openai-compatible/chat/openai-compatible-chat-options.ts +0 -25
- package/packages/llm-providers/src/openai-compatible/chat/openai-compatible-metadata-extractor.ts +0 -49
- package/packages/llm-providers/src/openai-compatible/chat/openai-compatible-prepare-tools.ts +0 -162
- package/packages/llm-providers/src/openai-compatible/chat/stream-transform.test.ts +0 -106
- package/packages/llm-providers/src/openai-compatible/chat/stream-transform.ts +0 -585
- package/packages/llm-providers/src/openai-compatible/completion/convert-to-openai-compatible-completion-prompt.ts +0 -98
- package/packages/llm-providers/src/openai-compatible/completion/openai-compatible-completion-language-model.test.ts +0 -173
- package/packages/llm-providers/src/openai-compatible/completion/openai-compatible-completion-language-model.ts +0 -414
- package/packages/llm-providers/src/openai-compatible/completion/openai-compatible-completion-options.ts +0 -33
- package/packages/llm-providers/src/openai-compatible/embedding/openai-compatible-embedding-model.ts +0 -143
- package/packages/llm-providers/src/openai-compatible/embedding/openai-compatible-embedding-options.ts +0 -21
- package/packages/llm-providers/src/openai-compatible/image/openai-compatible-image-model.ts +0 -129
- package/packages/llm-providers/src/openai-compatible/image/openai-compatible-image-settings.ts +0 -1
- package/packages/llm-providers/src/openai-compatible/index.ts +0 -27
- package/packages/llm-providers/src/openai-compatible/openai-compatible-error.ts +0 -34
- package/packages/llm-providers/src/openai-compatible/openai-compatible-provider.ts +0 -174
- package/packages/llm-providers/src/openai-compatible/version.ts +0 -5
- package/packages/llm-providers/tsconfig.json +0 -9
- package/protocol.config.yaml +0 -113
- package/savant-free/README.md +0 -73
- package/savant-free/SPEC.md +0 -385
- package/savant-free/cli/build.ts +0 -49
- package/savant-free/cli/release/README.md +0 -55
- package/savant-free/cli/release/index.js +0 -37
- package/savant-free/cli/release/package.json +0 -43
- package/savant-free/cli/release.ts +0 -128
- package/savant-free/cli/smoke-test.test.ts +0 -257
- package/savant-free/e2e/README.md +0 -173
- package/savant-free/e2e/agent/savant-free-tester.ts +0 -52
- package/savant-free/e2e/tests/ads-behavior.e2e.test.ts +0 -51
- package/savant-free/e2e/tests/agent-startup.e2e.test.ts +0 -61
- package/savant-free/e2e/tests/code-edit.e2e.test.ts +0 -78
- package/savant-free/e2e/tests/help-command.e2e.test.ts +0 -97
- package/savant-free/e2e/tests/knowledge-file.e2e.test.ts +0 -66
- package/savant-free/e2e/tests/slash-commands.e2e.test.ts +0 -119
- package/savant-free/e2e/tests/startup.e2e.test.ts +0 -56
- package/savant-free/e2e/tests/terminal-command.e2e.test.ts +0 -71
- package/savant-free/e2e/tests/version.e2e.test.ts +0 -50
- package/savant-free/e2e/utils/binary-helpers.ts +0 -24
- package/savant-free/e2e/utils/index.ts +0 -17
- package/savant-free/e2e/utils/savant-free-session.ts +0 -229
- package/savant-free/e2e/utils/tmux-custom-tools.ts +0 -156
- package/savant-free/e2e/utils/tmux-helpers.ts +0 -79
- package/savant-free/package.json +0 -20
- package/scripts/eslint-rules/__fixtures__/debug-ast.mjs +0 -20
- package/scripts/eslint-rules/__fixtures__/probe.mjs +0 -41
- package/scripts/eslint-rules/__fixtures__/probe2.mjs +0 -43
- package/scripts/eslint-rules/__fixtures__/rule-test.mjs +0 -44
- package/scripts/eslint-rules/__fixtures__/rule-test2.mjs +0 -27
- package/scripts/eslint-rules/__fixtures__/unknown-bad.ts +0 -21
- package/scripts/eslint-rules/__fixtures__/unknown-good.ts +0 -35
- package/scripts/eslint-rules/no-unknown-in-signatures.js +0 -141
- package/scripts/release.py +0 -360
- package/scripts/run-az-test.sh +0 -1150
- package/scripts/sync-agents.py +0 -337
- package/scripts/tmux/README.md +0 -359
- package/scripts/tmux/package.json +0 -9
- package/scripts/tmux/tmux-capture.sh +0 -231
- package/scripts/tmux/tmux-cli.sh +0 -160
- package/scripts/tmux/tmux-env.sh +0 -34
- package/scripts/tmux/tmux-send.sh +0 -339
- package/scripts/tmux/tmux-start.sh +0 -264
- package/scripts/tmux/tmux-stop.sh +0 -157
- package/scripts/tmux/tmux-viewer/README.md +0 -254
- package/scripts/tmux/tmux-viewer/components/session-viewer.tsx +0 -556
- package/scripts/tmux/tmux-viewer/components/theme.ts +0 -54
- package/scripts/tmux/tmux-viewer/gif-encoder-2.d.ts +0 -28
- package/scripts/tmux/tmux-viewer/gif-exporter.ts +0 -278
- package/scripts/tmux/tmux-viewer/index.tsx +0 -237
- package/scripts/tmux/tmux-viewer/package.json +0 -9
- package/scripts/tmux/tmux-viewer/session-loader.ts +0 -234
- package/scripts/tmux/tmux-viewer/tsconfig.json +0 -9
- package/scripts/tmux/tmux-viewer/types.ts +0 -76
- package/sdk/CHANGELOG.md +0 -126
- package/sdk/PUBLISHING.md +0 -55
- package/sdk/README.md +0 -334
- package/sdk/bunfig.toml +0 -6
- package/sdk/e2e/README.md +0 -158
- package/sdk/e2e/custom-agents/api-integration-agent.e2e.test.ts +0 -145
- package/sdk/e2e/custom-agents/apply-patch-tool.e2e.test.ts +0 -62
- package/sdk/e2e/custom-agents/database-query-agent.e2e.test.ts +0 -135
- package/sdk/e2e/custom-agents/weather-agent.e2e.test.ts +0 -120
- package/sdk/e2e/examples/code-explainer.example.ts +0 -62
- package/sdk/e2e/examples/code-reviewer.example.ts +0 -52
- package/sdk/e2e/examples/commit-message-generator.example.ts +0 -61
- package/sdk/e2e/examples/sdk-lint.example.ts +0 -65
- package/sdk/e2e/examples/sdk-refactor.example.ts +0 -63
- package/sdk/e2e/examples/sdk-test-gen.example.ts +0 -62
- package/sdk/e2e/features/knowledge-files.e2e.test.ts +0 -82
- package/sdk/e2e/features/max-agent-steps.e2e.test.ts +0 -65
- package/sdk/e2e/features/project-files.e2e.test.ts +0 -80
- package/sdk/e2e/integration/connection-check.integration.test.ts +0 -33
- package/sdk/e2e/integration/event-ordering.integration.test.ts +0 -186
- package/sdk/e2e/integration/event-types.integration.test.ts +0 -183
- package/sdk/e2e/integration/stream-chunks.integration.test.ts +0 -191
- package/sdk/e2e/streaming/concurrent-streams.e2e.test.ts +0 -164
- package/sdk/e2e/streaming/subagent-streaming.e2e.test.ts +0 -158
- package/sdk/e2e/utils/__tests__/event-collector.test.ts +0 -297
- package/sdk/e2e/utils/e2e-mocks.ts +0 -458
- package/sdk/e2e/utils/event-collector.ts +0 -145
- package/sdk/e2e/utils/get-api-key.ts +0 -76
- package/sdk/e2e/utils/index.ts +0 -3
- package/sdk/e2e/utils/test-fixtures.ts +0 -196
- package/sdk/e2e/workflows/error-recovery.e2e.test.ts +0 -112
- package/sdk/e2e/workflows/multi-turn-conversation.e2e.test.ts +0 -132
- package/sdk/examples/readme-example-1.ts +0 -34
- package/sdk/examples/readme-example-2.ts +0 -76
- package/sdk/package.json +0 -82
- package/sdk/scripts/build.ts +0 -349
- package/sdk/scripts/fetch-ripgrep.ts +0 -172
- package/sdk/scripts/publish.ts +0 -51
- package/sdk/scripts/release.js +0 -100
- package/sdk/scripts/verify.ts +0 -204
- package/sdk/sdk_exit +0 -1
- package/sdk/smoke-test-dist.ts +0 -248
- package/sdk/src/__tests__/apply-overrides-resume.test.ts +0 -83
- package/sdk/src/__tests__/apply-patch.test.ts +0 -438
- package/sdk/src/__tests__/build-file-tree.test.ts +0 -31
- package/sdk/src/__tests__/change-file.test.ts +0 -212
- package/sdk/src/__tests__/client.test.ts +0 -142
- package/sdk/src/__tests__/clone-session-state.test.ts +0 -158
- package/sdk/src/__tests__/code-search.test.ts +0 -896
- package/sdk/src/__tests__/composio.test.ts +0 -86
- package/sdk/src/__tests__/credentials.test.ts +0 -344
- package/sdk/src/__tests__/database.test.ts +0 -153
- package/sdk/src/__tests__/env.test.ts +0 -137
- package/sdk/src/__tests__/error-utils.test.ts +0 -253
- package/sdk/src/__tests__/fixtures/windows-stubborn-grandchild.ts +0 -26
- package/sdk/src/__tests__/initial-session-state.test.ts +0 -447
- package/sdk/src/__tests__/knowledge-file-selection.test.ts +0 -335
- package/sdk/src/__tests__/load-agents.test.ts +0 -935
- package/sdk/src/__tests__/load-mcp-config.test.ts +0 -285
- package/sdk/src/__tests__/load-skills.test.ts +0 -325
- package/sdk/src/__tests__/model-provider.test.ts +0 -44
- package/sdk/src/__tests__/path-utils.test.ts +0 -66
- package/sdk/src/__tests__/read-files.test.ts +0 -573
- package/sdk/src/__tests__/read-url.test.ts +0 -371
- package/sdk/src/__tests__/researcher-web.integration.test.ts +0 -130
- package/sdk/src/__tests__/run-cancellation.test.ts +0 -1321
- package/sdk/src/__tests__/run-error-preserves-history.test.ts +0 -335
- package/sdk/src/__tests__/run-event-dispatch.test.ts +0 -80
- package/sdk/src/__tests__/run-file-filter.test.ts +0 -530
- package/sdk/src/__tests__/run-handle-event.test.ts +0 -142
- package/sdk/src/__tests__/run-mcp-tool-filter.test.ts +0 -124
- package/sdk/src/__tests__/run-state-child-process.test.ts +0 -72
- package/sdk/src/__tests__/run-terminal-command.test.ts +0 -236
- package/sdk/src/__tests__/run.integration.test.ts +0 -170
- package/sdk/src/__tests__/user-knowledge-files.test.ts +0 -361
- package/sdk/src/__tests__/validate-agents.test.ts +0 -943
- package/sdk/src/agents/load-agents.ts +0 -339
- package/sdk/src/agents/load-mcp-config.ts +0 -275
- package/sdk/src/client.ts +0 -87
- package/sdk/src/composio.ts +0 -78
- package/sdk/src/constants.ts +0 -29
- package/sdk/src/credentials.ts +0 -303
- package/sdk/src/custom-tool.ts +0 -66
- package/sdk/src/env.ts +0 -116
- package/sdk/src/error-utils.ts +0 -127
- package/sdk/src/impl/__tests__/llm-chatgpt-oauth-policy.test.ts +0 -67
- package/sdk/src/impl/__tests__/llm-native-tool-call.test.ts +0 -38
- package/sdk/src/impl/__tests__/llm-stream-yielded-content.test.ts +0 -118
- package/sdk/src/impl/__tests__/model-provider-free-mode.test.ts +0 -166
- package/sdk/src/impl/__tests__/prompt-result.test.ts +0 -210
- package/sdk/src/impl/__tests__/provider-options-metadata.test.ts +0 -67
- package/sdk/src/impl/agent-runtime.ts +0 -159
- package/sdk/src/impl/chatgpt-backend-fetch.ts +0 -588
- package/sdk/src/impl/database.ts +0 -516
- package/sdk/src/impl/llm.ts +0 -852
- package/sdk/src/impl/model-provider.ts +0 -638
- package/sdk/src/impl/openrouter-key-resolver.ts +0 -78
- package/sdk/src/index.ts +0 -133
- package/sdk/src/native/ripgrep.ts +0 -141
- package/sdk/src/retry-config.ts +0 -51
- package/sdk/src/run-state.ts +0 -897
- package/sdk/src/run.ts +0 -1222
- package/sdk/src/skills/load-skills.ts +0 -253
- package/sdk/src/testing/env.ts +0 -19
- package/sdk/src/tools/apply-patch.ts +0 -690
- package/sdk/src/tools/change-file.ts +0 -155
- package/sdk/src/tools/code-search.ts +0 -547
- package/sdk/src/tools/glob.ts +0 -58
- package/sdk/src/tools/index.ts +0 -19
- package/sdk/src/tools/list-directory.ts +0 -52
- package/sdk/src/tools/path-utils.ts +0 -101
- package/sdk/src/tools/read-files.ts +0 -110
- package/sdk/src/tools/read-url.ts +0 -469
- package/sdk/src/tools/run-file-change-hooks.ts +0 -22
- package/sdk/src/tools/run-terminal-command.ts +0 -483
- package/sdk/src/tools/ssrf.ts +0 -120
- package/sdk/src/types/env.ts +0 -40
- package/sdk/src/utils/logger.ts +0 -15
- package/sdk/src/validate-agents.ts +0 -157
- package/sdk/test/cjs-compatibility/package-lock.json +0 -423
- package/sdk/test/cjs-compatibility/package.json +0 -21
- package/sdk/test/cjs-compatibility/test-imports.js +0 -77
- package/sdk/test/cjs-compatibility/test-types.ts +0 -54
- package/sdk/test/cjs-compatibility/tsconfig.json +0 -17
- package/sdk/test/esm-compatibility/package-lock.json +0 -423
- package/sdk/test/esm-compatibility/package.json +0 -21
- package/sdk/test/esm-compatibility/test-imports.js +0 -83
- package/sdk/test/esm-compatibility/test-types.ts +0 -59
- package/sdk/test/esm-compatibility/tsconfig.json +0 -17
- package/sdk/test/ripgrep-bundling/package-lock.json +0 -423
- package/sdk/test/ripgrep-bundling/package.json +0 -21
- package/sdk/test/ripgrep-bundling/test-ripgrep-types.ts +0 -132
- package/sdk/test/ripgrep-bundling/test-ripgrep.js +0 -257
- package/sdk/test/ripgrep-bundling/tsconfig.json +0 -15
- package/sdk/test/setup-env.ts +0 -54
- package/sdk/test/test-sdk.ts +0 -25
- package/sdk/test/tree-sitter-queries/package-lock.json +0 -47
- package/sdk/test/tree-sitter-queries/package.json +0 -12
- package/sdk/test/tree-sitter-queries/test-query-files.js +0 -256
- package/sdk/tsconfig.build.json +0 -32
- package/sdk/tsconfig.json +0 -27
- package/sdk/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/sdk/vendor/ripgrep/arm64-linux/rg +0 -0
- package/sdk/vendor/ripgrep/x64-darwin/rg +0 -0
- package/sdk/vendor/ripgrep/x64-linux/rg +0 -0
- package/sdk/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/templates/FID-TEMPLATE.md +0 -125
- package/templates/README-TEMPLATE.md +0 -87
- package/templates/SESSION-SUMMARY.md +0 -135
- package/tsconfig.base.json +0 -18
- package/tsconfig.json +0 -27
- /package/{cli/release-core/http.js → http.js} +0 -0
- /package/{cli/release-core/launcher.js → launcher.js} +0 -0
|
@@ -1,1945 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"repoUrl": "https://github.com/manifoldmarkets/manifold",
|
|
3
|
-
"generationDate": "2025-12-10",
|
|
4
|
-
"initCommand": "yarn install && git checkout -- yarn.lock",
|
|
5
|
-
"finalCheckCommands": [
|
|
6
|
-
"yarn --cwd common tsc --noEmit",
|
|
7
|
-
"yarn --cwd backend/api compile",
|
|
8
|
-
"yarn --cwd backend/scheduler compile",
|
|
9
|
-
"yarn --cwd backend/shared build",
|
|
10
|
-
"yarn --cwd web tsc --noEmit"
|
|
11
|
-
],
|
|
12
|
-
"evalCommits": [
|
|
13
|
-
{
|
|
14
|
-
"id": "add-achievements",
|
|
15
|
-
"sha": "60532e591c2207e314b4e0716b26b8f77a91bdd9",
|
|
16
|
-
"parentSha": "538b6b617d048694e016764197d75cf491a3c5c7",
|
|
17
|
-
"spec": "Implement a user Achievements feature spanning backend, SQL, scheduler, API schema, frontend UI, and assets.\n\nBackend API\n- Add a GET endpoint get-user-achievements that returns a flat object of user achievement stats plus ranks/percentiles per stat.\n- Create a handler under backend/api/src/get-user-achievements.ts that:\n - Accepts { userId: string }.\n - Uses createSupabaseDirectClient from shared/supabase/init to run a single SQL query assembling all raw achievement stats for the user (creatorTraders, totalReferrals, totalReferredProfitMana, totalVolumeMana, seasonsGoldOrHigher, seasonsPlatinumOrHigher, seasonsDiamondOrHigher, seasonsMasters, largestLeagueSeasonEarnings, numberOfComments, totalLiquidityCreatedMarkets, totalTradesCount, totalMarketsCreated, accountAgeYears, profitableMarketsCount, unprofitableMarketsCount, largestProfitableTradeValue, largestUnprofitableTradeValue, longestBettingStreak, modTicketsResolved, charityDonatedMana).\n - Includes a ranks_json object with rank values pulled from materialized views (one rank field per stat listed above; percentiles should be null here).\n - Converts ranks_json into a ranks object, computing percentiles in-process as rank/N*100 where N is the total number of users from mv_ach_volume (if rank or N is missing, leave percentile null). Ensure each ranks key is present with { rank: number | null; percentile: number | null }.\n - Returns numeric fields as Numbers and a ranks object keyed by: creatorTraders, totalReferrals, totalReferredProfit, volume, trades, marketsCreated, comments, seasonsGoldOrHigher, seasonsPlatinumOrHigher, seasonsDiamondOrHigher, seasonsMasters, largestLeagueSeasonEarnings, liquidity, profitableMarkets, unprofitableMarkets, largestProfitableTrade, largestUnprofitableTrade, accountAge, longestBettingStreak, modTickets, charityDonated.\n- Wire the handler in backend/api/src/routes.ts by importing the function and adding a 'get-user-achievements' key mapped to it.\n\nScheduler\n- Add job helpers in backend/scheduler/src/jobs/refresh-achievement-mvs.ts to refresh each MV concurrently (using refresh materialized view concurrently <name>), with functions: refreshAchVolume, refreshAchComments, refreshAchLeagues, refreshAchPnl, refreshAchTxns, refreshAchCreatorContracts, refreshAchReferrals, refreshAchCreatorTraders, refreshAchAccountAge.\n- Add a job backend/scheduler/src/jobs/update-ach-trades.ts that incrementally maintains the ach_trades table by:\n - Ensuring rows exist for all users (insert ... on conflict do nothing).\n - Counting new qualifying trades since last_updated from contract_bets (excluding redemptions, canceled, API bets; only MANA contracts; only filled), grouped per user; update total_trades_count and last_updated.\n - Recomputing trades_rank across ach_trades based on total_trades_count.\n- Register the above jobs in backend/scheduler/src/jobs/index.ts. Keep the actual scheduled createJob calls commented out but include the imports and a commented block of staggered nightly cron schedules.\n\nSupabase SQL (materialized views and table)\n- Create backend/supabase/achievement_materialized_views.sql defining:\n - mv_ach_volume (per-user total_volume_mana and volume_rank), with unique index on user_id.\n - ach_trades table (user_id PK, total_trades_count, trades_rank, last_updated with default epoch), with unique index on user_id.\n - mv_ach_comments counting distinct comments with at least one like (number_of_comments and comments_rank), with unique index.\n - mv_ach_leagues aggregating seasons at or above Gold/Platinum/Diamond, Masters exact count, and largest_league_season_earnings, plus ranks for each metric, with unique index.\n - mv_ach_pnl aggregating profitable/unprofitable markets and largest profitable/unprofitable trade values, plus ranks, with unique index.\n - mv_ach_txns_achievements aggregating mod_tickets_resolved (ADMIN_REWARD), charity_donated_mana across tokens (M$, CASH, SPICE with unit conversions), longest_betting_streak from BETTING_STREAK_BONUS, plus ranks, with unique index.\n - mv_ach_creator_contracts aggregating total_markets_created (markets with >=2 unique bettors) and total_liquidity_created_markets for MANA creator markets, with ranks and unique index.\n - mv_ach_referrals (total_referrals, total_referred_profit_mana) from user_referrals_profit with ranks and unique index.\n - mv_ach_creator_traders reading users.data->creatorTraders.allTime with rank and unique index.\n - mv_ach_account_age computing account_age_years and rank with unique index.\n\nAPI Schema\n- Extend common/src/api/schema.ts with a 'get-user-achievements' entry:\n - method: GET; visibility: public; authed: false; cache: light strategy.\n - props: { userId: string } strict.\n - returns: object containing all numeric fields listed above and ranks with keys and shapes as described (each entry: { rank: number | null; percentile: number | null }).\n\nFrontend UI\n- Update the user profile page web/pages/[username]/index.tsx to add an Achievements tab.\n - Add an AchievementsSection component that:\n - Calls unauthedApi('get-user-achievements', { userId }).\n - Handles loading and error states; renders nothing if no data.\n - Defines rank mapping per displayed achievement ID and zero-value handling (suppress rank/percentile when raw value is zero).\n - Buckets achievements by rank thresholds: Top 50, Top 200, Top 1000, Top 5000, Top 20,000, All Users; sorts within a bucket by rank then percentile ascending.\n - Renders a responsive grid of AchievementBadgeCard components per bucket.\n - Add an AchievementBadgeCard component that displays:\n - A gradient ring color per bucket, a circular image, title, description, value.\n - A hover tooltip showing Rank and percentile (as “In the top X% of all users”), and the value.\n - Import and use Image from next/image to display badge images.\n - Use existing formatting helpers to format MANA, USD, and counts for values.\n\nAssets\n- Add badge images under web/public/achievement-badges/ with the following filenames (must match code references): accountAgeYears.png, charityDonatedMana.png, creatorTraders.png, largestLeagueSeasonEarnings.png, largestProfitableTradeValue.png, largestUnprofitableTradeValue.png, longestBettingStreak.png, modTicketsResolved.png, numberOfComments.png, profitableMarketsCount.png, seasonsDiamondOrHigher.png, seasonsGoldOrHigher.png, seasonsMasters.png, seasonsPlatinumOrHigher.png, totalLiquidityCreatedMarkets.png, totalMarketsCreated.png, totalReferrals.png, totalReferredProfitMana.png, totalTradesCount.png, totalVolumeMana.png, unprofitableMarketsCount.png. (Other unused illustrative images can be present but must not break imports.)\n\nAcceptance criteria\n- GET /api/v0/get-user-achievements returns the specified fields for a valid userId; percentiles are present (or null when rank is null/zero raw value).\n- The user profile shows a new “Achievements” tab with grouped badges; ranks/percentiles are hidden for zero-value achievements; tooltips and values render and bucket ordering/sorting is correct.\n- Achievements ranks are drawn from materialized views and the ach_trades table; views refresh functions exist; the scheduler imports jobs and contains commented-out cron registrations.\n- All new SQL objects have unique indexes on user_id where materialized views are created.\n- Type definitions compile without TS errors and endpoint is included in common schema.\n",
|
|
18
|
-
"prompt": "Add a public “Achievements” feature to user profiles. Include a new GET API that returns a user’s achievement stats and ranks, scheduled jobs to keep achievement rankings fresh, database views/tables to compute those rankings, and a new Achievements tab showing badges on the profile page. The API should aggregate a user’s activity (trades, volume, comments, creators’ traders, referrals, creator market counts/liquidity, league season stats, betting streaks, moderator tickets, charity donations, account age) and provide a rank and percentile for each metric. On the frontend, fetch these data for a user ID and render a grid of badge cards grouped into rank-based buckets with images and tooltips. Ensure types are added to the shared API schema, the route is wired, and the UI is responsive and uses the project’s formatting utilities.",
|
|
19
|
-
"supplementalFiles": ["web/lib/api/api.ts"],
|
|
20
|
-
"fileDiffs": [
|
|
21
|
-
{
|
|
22
|
-
"path": "backend/api/src/get-user-achievements.ts",
|
|
23
|
-
"status": "added",
|
|
24
|
-
"diff": "Index: backend/api/src/get-user-achievements.ts\n===================================================================\n--- backend/api/src/get-user-achievements.ts\t538b6b6 (parent)\n+++ backend/api/src/get-user-achievements.ts\t60532e5 (commit)\n@@ -0,0 +1,261 @@\n+import { APIHandler } from 'api/helpers/endpoint'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+\n+// Returns flat achievement-style statistics for a given user (no ranks)\n+export const getUserAchievements: APIHandler<'get-user-achievements'> = async ({\n+ userId,\n+}) => {\n+ const pg = createSupabaseDirectClient()\n+\n+ const queryWithPerMV = `with base as (\n+ select $1::text as uid\n+ ),\n+ \n+ creators as (\n+ select (data->'creatorTraders'->>'allTime')::int as creator_traders\n+ from users\n+ where id = (select uid from base)\n+ ),\n+ referrals as (\n+ select total_referrals, total_referred_profit as total_referred_profit_mana\n+ from user_referrals_profit\n+ where id = (select uid from base)\n+ ),\n+ mod_tickets as (\n+ select count(*) as mod_tickets_resolved\n+ from txns\n+ where category = 'ADMIN_REWARD' and to_id = (select uid from base)\n+ ),\n+ charity as (\n+ select coalesce(sum(case when token = 'M$' then amount / 100.0 else 0 end), 0)\n+ + coalesce(sum(case when token = 'CASH' then amount else 0 end), 0)\n+ + coalesce(sum(case when token = 'SPICE' then amount / 1000.0 else 0 end), 0) as charity_donated_mana\n+ from txns\n+ where category = 'CHARITY' and from_id = (select uid from base)\n+ ),\n+ volume as (\n+ select\n+ sum(case when c.token = 'MANA' then (coalesce((ucm.data->>'totalAmountSold')::numeric,0) + coalesce((ucm.data->>'totalAmountInvested')::numeric,0)) else 0 end) as total_volume_mana\n+ from user_contract_metrics ucm\n+ join contracts c on c.id = ucm.contract_id\n+ where ucm.user_id = (select uid from base) and ucm.answer_id is null\n+ ),\n+ leagues_cte as (\n+ select\n+ count(*) filter (where division >= 3) as seasons_gold_or_higher,\n+ count(*) filter (where division >= 4) as seasons_platinum_or_higher,\n+ count(*) filter (where division >= 5) as seasons_diamond_or_higher,\n+ count(*) filter (where division = 6) as seasons_masters\n+ from leagues\n+ where user_id = (select uid from base)\n+ ),\n+ league_ranks as (\n+ select\n+ max(mana_earned) as largest_league_season_earnings\n+ from user_league_info\n+ where user_id = (select uid from base)\n+ ),\n+ comments as (\n+ select\n+ (select count(*) from contract_comments where user_id = (select uid from base)) +\n+ (select count(*) from old_post_comments where user_id = (select uid from base)) as number_of_comments\n+ ),\n+ liquidity as (\n+ select coalesce(sum((c.data->>'totalLiquidity')::numeric), 0) as total_liquidity_created_markets\n+ from contracts c\n+ where c.creator_id = (select uid from base) and c.token = 'MANA'\n+ ),\n+ trades as (\n+ select coalesce(total_trades_count, 0) as total_trades_count\n+ from ach_trades\n+ where user_id = (select uid from base)\n+ ),\n+ markets_created as (\n+ select count(*) as total_markets_created\n+ from contracts c\n+ where c.creator_id = (select uid from base)\n+ and c.token = 'MANA'\n+ ),\n+ account_age as (\n+ select round(\n+ (\n+ extract(epoch from (now() - u.created_time)) / (365.25*24*60*60)\n+ )::numeric,\n+ 2\n+ ) as account_age_years\n+ from users u\n+ where u.id = (select uid from base)\n+ ),\n+ trade_stats as (\n+ select\n+ count(*) filter (where coalesce(ucm.profit, 0) > 0) as profitable_trades_count,\n+ count(*) filter (where coalesce(ucm.profit, 0) < 0) as unprofitable_trades_count,\n+ max(ucm.profit) filter (where coalesce(ucm.profit, 0) > 0) as largest_profitable_trade_value,\n+ min(ucm.profit) filter (where coalesce(ucm.profit, 0) < 0) as largest_unprofitable_trade_value\n+ from user_contract_metrics ucm\n+ join contracts c on c.id = ucm.contract_id\n+ where ucm.user_id = (select uid from base)\n+ and ucm.answer_id is null\n+ and c.token = 'MANA'\n+ ),\n+ longest_streak as (\n+ select coalesce(max((tx.data->'data'->>'currentBettingStreak')::int), 0) as longest_betting_streak\n+ from txns tx\n+ where tx.to_id = (select uid from base)\n+ and tx.category = 'BETTING_STREAK_BONUS'\n+ )\n+ select\n+ (select uid from base) as user_id,\n+ coalesce(creators.creator_traders, 0) as creator_traders,\n+ coalesce(referrals.total_referrals, 0) as total_referrals,\n+ coalesce(referrals.total_referred_profit_mana, 0) as total_referred_profit_mana,\n+ coalesce(volume.total_volume_mana, 0) as total_volume_mana,\n+ coalesce(leagues_cte.seasons_gold_or_higher, 0) as seasons_gold_or_higher,\n+ coalesce(leagues_cte.seasons_platinum_or_higher, 0) as seasons_platinum_or_higher,\n+ coalesce(leagues_cte.seasons_diamond_or_higher, 0) as seasons_diamond_or_higher,\n+ coalesce(leagues_cte.seasons_masters, 0) as seasons_masters,\n+ coalesce(league_ranks.largest_league_season_earnings, 0) as largest_league_season_earnings,\n+ coalesce(comments.number_of_comments, 0) as number_of_comments,\n+ coalesce(liquidity.total_liquidity_created_markets, 0) as total_liquidity_created_markets,\n+ coalesce(trades.total_trades_count, 0) as total_trades_count,\n+ coalesce(markets_created.total_markets_created, 0) as total_markets_created,\n+ coalesce(account_age.account_age_years, 0) as account_age_years,\n+ coalesce(trade_stats.profitable_trades_count, 0) as profitable_markets_count,\n+ coalesce(trade_stats.unprofitable_trades_count, 0) as unprofitable_markets_count,\n+ coalesce(trade_stats.largest_profitable_trade_value, 0) as largest_profitable_trade_value,\n+ coalesce(trade_stats.largest_unprofitable_trade_value, 0) as largest_unprofitable_trade_value,\n+ coalesce(longest_streak.longest_betting_streak, 0) as longest_betting_streak,\n+ coalesce(mod_tickets.mod_tickets_resolved, 0) as mod_tickets_resolved,\n+ coalesce(charity.charity_donated_mana, 0) as charity_donated_mana,\n+ json_build_object(\n+ 'creatorTraders', json_build_object('rank', (select creator_traders_rank from mv_ach_creator_traders where user_id = (select uid from base)), 'percentile', null),\n+ 'totalReferrals', json_build_object('rank', (select total_referrals_rank from mv_ach_referrals where user_id = (select uid from base)), 'percentile', null),\n+ 'totalReferredProfit', json_build_object('rank', (select total_referred_profit_rank from mv_ach_referrals where user_id = (select uid from base)), 'percentile', null),\n+ 'volume', json_build_object('rank', (select volume_rank from mv_ach_volume where user_id = (select uid from base)), 'percentile', null),\n+ 'trades', json_build_object('rank', (select trades_rank from ach_trades where user_id = (select uid from base)), 'percentile', null),\n+ 'marketsCreated', json_build_object('rank', (select markets_created_rank from mv_ach_creator_contracts where user_id = (select uid from base)), 'percentile', null),\n+ 'comments', json_build_object('rank', (select comments_rank from mv_ach_comments where user_id = (select uid from base)), 'percentile', null),\n+ 'seasonsGoldOrHigher', json_build_object('rank', (select seasons_gold_or_higher_rank from mv_ach_leagues where user_id = (select uid from base)), 'percentile', null),\n+ 'seasonsPlatinumOrHigher', json_build_object('rank', (select seasons_platinum_or_higher_rank from mv_ach_leagues where user_id = (select uid from base)), 'percentile', null),\n+ 'seasonsDiamondOrHigher', json_build_object('rank', (select seasons_diamond_or_higher_rank from mv_ach_leagues where user_id = (select uid from base)), 'percentile', null),\n+ 'seasonsMasters', json_build_object('rank', (select seasons_masters_rank from mv_ach_leagues where user_id = (select uid from base)), 'percentile', null),\n+ 'largestLeagueSeasonEarnings', json_build_object('rank', (select largest_league_season_earnings_rank from mv_ach_leagues where user_id = (select uid from base)), 'percentile', null),\n+ 'liquidity', json_build_object('rank', (select liquidity_rank from mv_ach_creator_contracts where user_id = (select uid from base)), 'percentile', null),\n+ 'profitableMarkets', json_build_object('rank', (select profitable_markets_rank from mv_ach_pnl where user_id = (select uid from base)), 'percentile', null),\n+ 'unprofitableMarkets', json_build_object('rank', (select unprofitable_markets_rank from mv_ach_pnl where user_id = (select uid from base)), 'percentile', null),\n+ 'largestProfitableTrade', json_build_object('rank', (select largest_profitable_trade_rank from mv_ach_pnl where user_id = (select uid from base)), 'percentile', null),\n+ 'largestUnprofitableTrade', json_build_object('rank', (select largest_unprofitable_trade_rank from mv_ach_pnl where user_id = (select uid from base)), 'percentile', null),\n+ 'accountAge', json_build_object('rank', (select account_age_rank from mv_ach_account_age where user_id = (select uid from base)), 'percentile', null),\n+ 'longestBettingStreak', json_build_object('rank', (select longest_betting_streak_rank from mv_ach_txns_achievements where user_id = (select uid from base)), 'percentile', null),\n+ 'modTickets', json_build_object('rank', (select mod_tickets_rank from mv_ach_txns_achievements where user_id = (select uid from base)), 'percentile', null),\n+ 'charityDonated', json_build_object('rank', (select charity_donated_rank from mv_ach_txns_achievements where user_id = (select uid from base)), 'percentile', null)\n+ ) as ranks_json\n+ from base\n+ \n+ left join creators on true\n+ left join referrals on true\n+ left join mod_tickets on true\n+ left join charity on true\n+ left join volume on true\n+ left join leagues_cte on true\n+ left join league_ranks on true\n+ left join comments on true\n+ left join liquidity on true\n+ left join trades on true\n+ left join markets_created on true\n+ left join account_age on true\n+ left join trade_stats on true\n+ left join longest_streak on true`\n+\n+ const result = await pg.oneOrNone(queryWithPerMV, [userId])\n+\n+ const defaultRanks = {\n+ creatorTraders: { rank: null, percentile: null },\n+ totalReferrals: { rank: null, percentile: null },\n+ totalReferredProfit: { rank: null, percentile: null },\n+ volume: { rank: null, percentile: null },\n+ trades: { rank: null, percentile: null },\n+ marketsCreated: { rank: null, percentile: null },\n+ comments: { rank: null, percentile: null },\n+ seasonsGoldOrHigher: { rank: null, percentile: null },\n+ seasonsPlatinumOrHigher: { rank: null, percentile: null },\n+ seasonsDiamondOrHigher: { rank: null, percentile: null },\n+ seasonsMasters: { rank: null, percentile: null },\n+ largestLeagueSeasonEarnings: { rank: null, percentile: null },\n+ liquidity: { rank: null, percentile: null },\n+ profitableMarkets: { rank: null, percentile: null },\n+ unprofitableMarkets: { rank: null, percentile: null },\n+ largestProfitableTrade: { rank: null, percentile: null },\n+ largestUnprofitableTrade: { rank: null, percentile: null },\n+ accountAge: { rank: null, percentile: null },\n+ longestBettingStreak: { rank: null, percentile: null },\n+ modTickets: { rank: null, percentile: null },\n+ charityDonated: { rank: null, percentile: null },\n+ }\n+\n+ const rawRanks = (result?.ranks_json as any) ?? defaultRanks\n+ const { n } = await pg.one('select count(*)::int as n from mv_ach_volume')\n+ const N = Number(n ?? 0)\n+ const conv = (e?: { rank: number | null; percentile: number | null }) =>\n+ e?.rank && N > 0\n+ ? { rank: e.rank, percentile: (e.rank / N) * 100 }\n+ : { rank: e?.rank ?? null, percentile: null }\n+\n+ const ranks = {\n+ creatorTraders: conv(rawRanks.creatorTraders),\n+ totalReferrals: conv(rawRanks.totalReferrals),\n+ totalReferredProfit: conv(rawRanks.totalReferredProfit),\n+ volume: conv(rawRanks.volume),\n+ trades: conv(rawRanks.trades),\n+ marketsCreated: conv(rawRanks.marketsCreated),\n+ comments: conv(rawRanks.comments),\n+ seasonsGoldOrHigher: conv(rawRanks.seasonsGoldOrHigher),\n+ seasonsPlatinumOrHigher: conv(rawRanks.seasonsPlatinumOrHigher),\n+ seasonsDiamondOrHigher: conv(rawRanks.seasonsDiamondOrHigher),\n+ seasonsMasters: conv(rawRanks.seasonsMasters),\n+ largestLeagueSeasonEarnings: conv(rawRanks.largestLeagueSeasonEarnings),\n+ liquidity: conv(rawRanks.liquidity),\n+ profitableMarkets: conv(rawRanks.profitableMarkets),\n+ unprofitableMarkets: conv(rawRanks.unprofitableMarkets),\n+ largestProfitableTrade: conv(rawRanks.largestProfitableTrade),\n+ largestUnprofitableTrade: conv(rawRanks.largestUnprofitableTrade),\n+ accountAge: conv(rawRanks.accountAge),\n+ longestBettingStreak: conv(rawRanks.longestBettingStreak),\n+ modTickets: conv(rawRanks.modTickets),\n+ charityDonated: conv(rawRanks.charityDonated),\n+ }\n+\n+ return {\n+ userId,\n+ creatorTraders: Number(result?.creator_traders ?? 0),\n+ totalReferrals: Number(result?.total_referrals ?? 0),\n+ totalReferredProfitMana: Number(result?.total_referred_profit_mana ?? 0),\n+ totalVolumeMana: Number(result?.total_volume_mana ?? 0),\n+ seasonsGoldOrHigher: Number(result?.seasons_gold_or_higher ?? 0),\n+ seasonsPlatinumOrHigher: Number(result?.seasons_platinum_or_higher ?? 0),\n+ seasonsDiamondOrHigher: Number(result?.seasons_diamond_or_higher ?? 0),\n+ seasonsMasters: Number(result?.seasons_masters ?? 0),\n+ largestLeagueSeasonEarnings: Number(\n+ result?.largest_league_season_earnings ?? 0\n+ ),\n+ numberOfComments: Number(result?.number_of_comments ?? 0),\n+ totalLiquidityCreatedMarkets: Number(\n+ result?.total_liquidity_created_markets ?? 0\n+ ),\n+ totalTradesCount: Number(result?.total_trades_count ?? 0),\n+ totalMarketsCreated: Number(result?.total_markets_created ?? 0),\n+ accountAgeYears: Number(result?.account_age_years ?? 0),\n+ profitableMarketsCount: Number(result?.profitable_markets_count ?? 0),\n+ unprofitableMarketsCount: Number(result?.unprofitable_markets_count ?? 0),\n+ largestProfitableTradeValue: Number(\n+ result?.largest_profitable_trade_value ?? 0\n+ ),\n+ largestUnprofitableTradeValue: Number(\n+ result?.largest_unprofitable_trade_value ?? 0\n+ ),\n+ longestBettingStreak: Number(result?.longest_betting_streak ?? 0),\n+ modTicketsResolved: Number(result?.mod_tickets_resolved ?? 0),\n+ charityDonatedMana: Number(result?.charity_donated_mana ?? 0),\n+ ranks,\n+ }\n+}\n"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"path": "backend/api/src/routes.ts",
|
|
28
|
-
"status": "modified",
|
|
29
|
-
"diff": "Index: backend/api/src/routes.ts\n===================================================================\n--- backend/api/src/routes.ts\t538b6b6 (parent)\n+++ backend/api/src/routes.ts\t60532e5 (commit)\n@@ -182,8 +182,9 @@\n import { purchaseContractBoost } from './purchase-boost'\n import { referUser } from './refer-user'\n import { updatePost } from './update-post'\n import { validateiap } from './validate-iap'\n+import { getUserAchievements } from './get-user-achievements'\n \n export const handlers: { [k in APIPath]: APIHandler<k> } = {\n 'refresh-all-clients': refreshAllClients,\n bet: placeBet,\n@@ -372,5 +373,6 @@\n 'follow-post': followPost,\n 'edit-post-comment': editPostComment,\n 'user-comments': getUserComments,\n 'get-user-last-active-time': getUserLastActiveTime,\n+ 'get-user-achievements': getUserAchievements,\n } as const\n"
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"path": "backend/scheduler/src/jobs/index.ts",
|
|
33
|
-
"status": "modified",
|
|
34
|
-
"diff": "Index: backend/scheduler/src/jobs/index.ts\n===================================================================\n--- backend/scheduler/src/jobs/index.ts\t538b6b6 (parent)\n+++ backend/scheduler/src/jobs/index.ts\t60532e5 (commit)\n@@ -38,8 +38,20 @@\n import { sendStreakExpirationNotification } from './streak-expiration-notice'\n import { updateLeague } from './update-league'\n import { updateLeagueRanks } from './update-league-ranks'\n import { updateStatsCore } from './update-stats'\n+import { updateAchTrades } from './update-ach-trades'\n+import {\n+ refreshAchAccountAge,\n+ refreshAchComments,\n+ refreshAchCreatorContracts,\n+ refreshAchCreatorTraders,\n+ refreshAchLeagues,\n+ refreshAchPnl,\n+ refreshAchReferrals,\n+ refreshAchTxns,\n+ refreshAchVolume,\n+} from './refresh-achievement-mvs'\n \n export function createJobs() {\n return [\n createJob(\n@@ -138,8 +150,59 @@\n 'clean-old-notifications',\n '0 30 2 * * *', // 230 AM daily\n cleanOldNotifications\n ),\n+ // // Achievement MV refreshes (nightly, staggered ~10 mins apart)\n+ // createJob(\n+ // 'update-ach-trades',\n+ // '0 10 2 * * *', // 2:10 AM daily\n+ // updateAchTrades\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-volume',\n+ // '0 30 2 * * *', // 2:30 AM\n+ // refreshAchVolume\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-comments',\n+ // '0 40 2 * * *', // 2:40 AM\n+ // refreshAchComments\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-creator-contracts',\n+ // '0 50 2 * * *', // 2:50 AM\n+ // refreshAchCreatorContracts\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-referrals',\n+ // '0 0 3 * * *', // 3:00 AM\n+ // refreshAchReferrals\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-creator-traders',\n+ // '0 10 3 * * *', // 3:10 AM\n+ // refreshAchCreatorTraders\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-leagues',\n+ // '0 20 3 * * *', // 3:20 AM\n+ // refreshAchLeagues\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-pnl',\n+ // '0 30 3 * * *', // 3:30 AM\n+ // refreshAchPnl\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-txns',\n+ // '0 40 3 * * *', // 3:40 AM\n+ // refreshAchTxns\n+ // ),\n+ // createJob(\n+ // 'refresh-ach-account-age',\n+ // '0 50 3 * * *', // 3:50 AM\n+ // refreshAchAccountAge\n+ // ),\n createJob(\n 'update-user-metric-periods',\n '0 0 2 * * *', // 2 AM daily\n async () => {\n"
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
"path": "backend/scheduler/src/jobs/refresh-achievement-mvs.ts",
|
|
38
|
-
"status": "added",
|
|
39
|
-
"diff": "Index: backend/scheduler/src/jobs/refresh-achievement-mvs.ts\n===================================================================\n--- backend/scheduler/src/jobs/refresh-achievement-mvs.ts\t538b6b6 (parent)\n+++ backend/scheduler/src/jobs/refresh-achievement-mvs.ts\t60532e5 (commit)\n@@ -0,0 +1,22 @@\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { log } from 'shared/utils'\n+\n+async function refreshMV(name: string) {\n+ const pg = createSupabaseDirectClient()\n+ log(`Refreshing MV: ${name}`)\n+ // Use CONCURRENTLY to reduce locking; requires unique index which we have.\n+ await pg.none(`refresh materialized view concurrently ${name}`)\n+ log(`Refreshed MV: ${name}`)\n+}\n+\n+export const refreshAchVolume = async () => refreshMV('mv_ach_volume')\n+export const refreshAchComments = async () => refreshMV('mv_ach_comments')\n+export const refreshAchLeagues = async () => refreshMV('mv_ach_leagues')\n+export const refreshAchPnl = async () => refreshMV('mv_ach_pnl')\n+export const refreshAchTxns = async () => refreshMV('mv_ach_txns_achievements')\n+export const refreshAchCreatorContracts = async () =>\n+ refreshMV('mv_ach_creator_contracts')\n+export const refreshAchReferrals = async () => refreshMV('mv_ach_referrals')\n+export const refreshAchCreatorTraders = async () =>\n+ refreshMV('mv_ach_creator_traders')\n+export const refreshAchAccountAge = async () => refreshMV('mv_ach_account_age')\n"
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
"path": "backend/scheduler/src/jobs/update-ach-trades.ts",
|
|
43
|
-
"status": "added",
|
|
44
|
-
"diff": "Index: backend/scheduler/src/jobs/update-ach-trades.ts\n===================================================================\n--- backend/scheduler/src/jobs/update-ach-trades.ts\t538b6b6 (parent)\n+++ backend/scheduler/src/jobs/update-ach-trades.ts\t60532e5 (commit)\n@@ -0,0 +1,56 @@\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { log } from 'shared/utils'\n+\n+export const updateAchTrades = async () => {\n+ const pg = createSupabaseDirectClient()\n+\n+ log('update-ach-trades: ensure rows for all users')\n+ await pg.none(`\n+ insert into ach_trades (user_id, total_trades_count, last_updated)\n+ select u.id, 0, to_timestamp(0)\n+ from users u\n+ on conflict (user_id) do nothing\n+ `)\n+\n+ log('update-ach-trades: apply incremental deltas')\n+ await pg.none(`\n+ with deltas as (\n+ select\n+ b.user_id,\n+ count(*)::int as delta_trades,\n+ max(b.created_time) as new_last\n+ from contract_bets b\n+ join contracts c on c.id = b.contract_id\n+ left join ach_trades at on at.user_id = b.user_id\n+ where coalesce(b.is_redemption, false) = false\n+ and coalesce(b.is_filled, true) = true\n+ and coalesce(b.is_cancelled, false) = false\n+ and (b.is_api is null or b.is_api = false)\n+ and c.token = 'MANA'\n+ and b.created_time > coalesce(at.last_updated, to_timestamp(0))\n+ group by b.user_id\n+ )\n+ insert into ach_trades (user_id, total_trades_count, last_updated)\n+ select user_id, delta_trades, new_last from deltas\n+ on conflict (user_id) do update\n+ set total_trades_count = ach_trades.total_trades_count + excluded.total_trades_count,\n+ last_updated = greatest(ach_trades.last_updated, excluded.last_updated)\n+ `)\n+\n+ log('update-ach-trades: recompute ranks')\n+ await pg.none(`\n+ with ranks as (\n+ select\n+ user_id,\n+ rank() over (order by total_trades_count desc) as r,\n+ count(*) over () as n\n+ from ach_trades\n+ )\n+ update ach_trades t\n+ set trades_rank = r.r\n+ from ranks r\n+ where t.user_id = r.user_id\n+ `)\n+\n+ log('update-ach-trades: done')\n+}\n"
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
"path": "backend/supabase/achievement_materialized_views.sql",
|
|
48
|
-
"status": "added",
|
|
49
|
-
"diff": "Index: backend/supabase/achievement_materialized_views.sql\n===================================================================\n--- backend/supabase/achievement_materialized_views.sql\t538b6b6 (parent)\n+++ backend/supabase/achievement_materialized_views.sql\t60532e5 (commit)\n@@ -0,0 +1,621 @@\n+-- Volume\n+create materialized view if not exists\n+ mv_ach_volume as\n+with\n+ mana_contracts as (\n+ select\n+ id\n+ from\n+ contracts\n+ where\n+ token = 'MANA'\n+ ),\n+ volume as (\n+ select\n+ ucm.user_id,\n+ sum(\n+ coalesce((ucm.data ->> 'totalAmountSold')::numeric, 0) + coalesce((ucm.data ->> 'totalAmountInvested')::numeric, 0)\n+ ) as total_volume_mana\n+ from\n+ user_contract_metrics ucm\n+ join mana_contracts mc on mc.id = ucm.contract_id\n+ where\n+ ucm.answer_id is null\n+ group by\n+ ucm.user_id\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(v.total_volume_mana, 0) as total_volume_mana,\n+ rank() over (\n+ order by\n+ coalesce(v.total_volume_mana, 0) desc\n+ ) as volume_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(v.total_volume_mana, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as volume_percentile\n+from\n+ all_users u\n+ left join volume v on v.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_volume_user_id_idx on public.mv_ach_volume (user_id);\n+\n+-- Trades\n+create table if not exists\n+ ach_trades (\n+ user_id text primary key,\n+ total_trades_count integer not null default 0,\n+ trades_rank integer,\n+ last_updated timestamp with time zone not null default to_timestamp(0)\n+ );\n+\n+create unique index if not exists ach_trades_user_id_idx on public.ach_trades (user_id);\n+\n+-- Comments (liked-only)\n+create materialized view if not exists\n+ mv_ach_comments as\n+with\n+ liked_comments as (\n+ select\n+ content_owner_id as user_id,\n+ count(distinct content_id) as liked_comments\n+ from\n+ user_reactions\n+ where\n+ content_type = 'comment'\n+ and reaction_type = 'like'\n+ group by\n+ content_owner_id\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(lc.liked_comments, 0) as number_of_comments,\n+ rank() over (\n+ order by\n+ coalesce(lc.liked_comments, 0) desc\n+ ) as comments_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(lc.liked_comments, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as comments_percentile\n+from\n+ all_users u\n+ left join liked_comments lc on lc.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_comments_user_id_idx on public.mv_ach_comments (user_id);\n+\n+-- Leagues (grouped)\n+create materialized view if not exists\n+ mv_ach_leagues as\n+with\n+ leagues_agg as (\n+ select\n+ l.user_id,\n+ count(*) filter (\n+ where\n+ l.division >= 3\n+ ) as seasons_gold_or_higher,\n+ count(*) filter (\n+ where\n+ l.division >= 4\n+ ) as seasons_platinum_or_higher,\n+ count(*) filter (\n+ where\n+ l.division >= 5\n+ ) as seasons_diamond_or_higher,\n+ count(*) filter (\n+ where\n+ l.division = 6\n+ ) as seasons_masters,\n+ max(l.mana_earned) as largest_league_season_earnings\n+ from\n+ leagues l\n+ group by\n+ l.user_id\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(la.seasons_gold_or_higher, 0) as seasons_gold_or_higher,\n+ coalesce(la.seasons_platinum_or_higher, 0) as seasons_platinum_or_higher,\n+ coalesce(la.seasons_diamond_or_higher, 0) as seasons_diamond_or_higher,\n+ coalesce(la.seasons_masters, 0) as seasons_masters,\n+ coalesce(la.largest_league_season_earnings, 0) as largest_league_season_earnings,\n+ rank() over (\n+ order by\n+ coalesce(la.seasons_gold_or_higher, 0) desc\n+ ) as seasons_gold_or_higher_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(la.seasons_gold_or_higher, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as seasons_gold_or_higher_percentile,\n+ rank() over (\n+ order by\n+ coalesce(la.seasons_platinum_or_higher, 0) desc\n+ ) as seasons_platinum_or_higher_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(la.seasons_platinum_or_higher, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as seasons_platinum_or_higher_percentile,\n+ rank() over (\n+ order by\n+ coalesce(la.seasons_diamond_or_higher, 0) desc\n+ ) as seasons_diamond_or_higher_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(la.seasons_diamond_or_higher, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as seasons_diamond_or_higher_percentile,\n+ rank() over (\n+ order by\n+ coalesce(la.seasons_masters, 0) desc\n+ ) as seasons_masters_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(la.seasons_masters, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as seasons_masters_percentile,\n+ rank() over (\n+ order by\n+ coalesce(la.largest_league_season_earnings, 0) desc\n+ ) as largest_league_season_earnings_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(la.largest_league_season_earnings, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as largest_league_season_earnings_percentile\n+from\n+ all_users u\n+ left join leagues_agg la on la.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_leagues_user_id_idx on public.mv_ach_leagues (user_id);\n+\n+-- PnL-like (profitable/unprofitable counts and largest trades)\n+create materialized view if not exists\n+ mv_ach_pnl as\n+with\n+ mana_contracts as (\n+ select\n+ id\n+ from\n+ contracts\n+ where\n+ token = 'MANA'\n+ ),\n+ ucm_pnl as (\n+ select\n+ ucm.user_id,\n+ count(*) filter (\n+ where\n+ coalesce(ucm.profit, 0) > 0\n+ ) as profitable_markets_count,\n+ count(*) filter (\n+ where\n+ coalesce(ucm.profit, 0) < 0\n+ ) as unprofitable_markets_count,\n+ max(ucm.profit) filter (\n+ where\n+ coalesce(ucm.profit, 0) > 0\n+ ) as largest_profitable_trade_value,\n+ min(ucm.profit) filter (\n+ where\n+ coalesce(ucm.profit, 0) < 0\n+ ) as largest_unprofitable_trade_value\n+ from\n+ user_contract_metrics ucm\n+ join mana_contracts mc on mc.id = ucm.contract_id\n+ where\n+ ucm.answer_id is null\n+ group by\n+ ucm.user_id\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(pnl.profitable_markets_count, 0) as profitable_markets_count,\n+ coalesce(pnl.unprofitable_markets_count, 0) as unprofitable_markets_count,\n+ coalesce(pnl.largest_profitable_trade_value, 0) as largest_profitable_trade_value,\n+ coalesce(pnl.largest_unprofitable_trade_value, 0) as largest_unprofitable_trade_value,\n+ rank() over (\n+ order by\n+ coalesce(pnl.profitable_markets_count, 0) desc\n+ ) as profitable_markets_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(pnl.profitable_markets_count, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as profitable_markets_percentile,\n+ rank() over (\n+ order by\n+ coalesce(pnl.unprofitable_markets_count, 0) desc\n+ ) as unprofitable_markets_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(pnl.unprofitable_markets_count, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as unprofitable_markets_percentile,\n+ rank() over (\n+ order by\n+ coalesce(pnl.largest_profitable_trade_value, 0) desc\n+ ) as largest_profitable_trade_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(pnl.largest_profitable_trade_value, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as largest_profitable_trade_percentile,\n+ rank() over (\n+ order by\n+ coalesce(pnl.largest_unprofitable_trade_value, 0) asc\n+ ) as largest_unprofitable_trade_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(pnl.largest_unprofitable_trade_value, 0) asc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as largest_unprofitable_trade_percentile\n+from\n+ all_users u\n+ left join ucm_pnl pnl on pnl.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_pnl_user_id_idx on public.mv_ach_pnl (user_id);\n+\n+-- Combined: Txns-based achievements (mod tickets resolved + charity donated + longest streak)\n+create materialized view if not exists\n+ mv_ach_txns_achievements as\n+with\n+ mod_tickets_agg as (\n+ select\n+ to_id as user_id,\n+ count(*) as mod_tickets_resolved\n+ from\n+ txns\n+ where\n+ category = 'ADMIN_REWARD'\n+ group by\n+ to_id\n+ ),\n+ charity_agg as (\n+ select\n+ from_id as user_id,\n+ sum(\n+ case\n+ when token = 'M$' then amount / 100.0\n+ when token = 'CASH' then amount\n+ when token = 'SPICE' then amount / 1000.0\n+ else 0\n+ end\n+ ) as charity_donated_mana\n+ from\n+ txns\n+ where\n+ category = 'CHARITY'\n+ group by\n+ from_id\n+ ),\n+ streaks_agg as (\n+ select\n+ to_id as user_id,\n+ coalesce(\n+ max((data -> 'data' ->> 'currentBettingStreak')::int),\n+ 0\n+ ) as longest_betting_streak\n+ from\n+ txns\n+ where\n+ category = 'BETTING_STREAK_BONUS'\n+ group by\n+ to_id\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(m.mod_tickets_resolved, 0) as mod_tickets_resolved,\n+ coalesce(c.charity_donated_mana, 0) as charity_donated_mana,\n+ coalesce(s.longest_betting_streak, 0) as longest_betting_streak,\n+ rank() over (\n+ order by\n+ coalesce(m.mod_tickets_resolved, 0) desc\n+ ) as mod_tickets_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(m.mod_tickets_resolved, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as mod_tickets_percentile,\n+ rank() over (\n+ order by\n+ coalesce(c.charity_donated_mana, 0) desc\n+ ) as charity_donated_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(c.charity_donated_mana, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as charity_donated_percentile,\n+ rank() over (\n+ order by\n+ coalesce(s.longest_betting_streak, 0) desc\n+ ) as longest_betting_streak_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(s.longest_betting_streak, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as longest_betting_streak_percentile\n+from\n+ all_users u\n+ left join mod_tickets_agg m on m.user_id = u.user_id\n+ left join charity_agg c on c.user_id = u.user_id\n+ left join streaks_agg s on s.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_txns_achievements_user_id_idx on public.mv_ach_txns_achievements (user_id);\n+\n+-- Combined: Creator-contract achievements (markets created + liquidity)\n+create materialized view if not exists\n+ mv_ach_creator_contracts as\n+with\n+ mana_contracts as (\n+ select\n+ id,\n+ creator_id,\n+ (data ->> 'uniqueBettorCount')::int as unique_bettors,\n+ (data ->> 'totalLiquidity')::numeric as total_liq\n+ from\n+ contracts\n+ where\n+ token = 'MANA'\n+ ),\n+ creator_agg as (\n+ select\n+ creator_id as user_id,\n+ count(*) filter (\n+ where\n+ unique_bettors >= 2\n+ ) as total_markets_created,\n+ coalesce(sum(total_liq), 0) as total_liquidity_created_markets\n+ from\n+ mana_contracts\n+ group by\n+ creator_id\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(ca.total_markets_created, 0) as total_markets_created,\n+ coalesce(ca.total_liquidity_created_markets, 0) as total_liquidity_created_markets,\n+ rank() over (\n+ order by\n+ coalesce(ca.total_markets_created, 0) desc\n+ ) as markets_created_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(ca.total_markets_created, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as markets_created_percentile,\n+ rank() over (\n+ order by\n+ coalesce(ca.total_liquidity_created_markets, 0) desc\n+ ) as liquidity_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(ca.total_liquidity_created_markets, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as liquidity_percentile\n+from\n+ all_users u\n+ left join creator_agg ca on ca.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_creator_contracts_user_id_idx on public.mv_ach_creator_contracts (user_id);\n+\n+-- Referrals (count and referred profit)\n+create materialized view if not exists\n+ mv_ach_referrals as\n+with\n+ referrals as (\n+ select\n+ id as user_id,\n+ coalesce(total_referrals, 0) as total_referrals,\n+ coalesce(total_referred_profit, 0) as total_referred_profit_mana\n+ from\n+ user_referrals_profit\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(r.total_referrals, 0) as total_referrals,\n+ coalesce(r.total_referred_profit_mana, 0) as total_referred_profit_mana,\n+ rank() over (\n+ order by\n+ coalesce(r.total_referrals, 0) desc\n+ ) as total_referrals_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(r.total_referrals, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as total_referrals_percentile,\n+ rank() over (\n+ order by\n+ coalesce(r.total_referred_profit_mana, 0) desc\n+ ) as total_referred_profit_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(r.total_referred_profit_mana, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as total_referred_profit_percentile\n+from\n+ all_users u\n+ left join referrals r on r.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_referrals_user_id_idx on public.mv_ach_referrals (user_id);\n+\n+-- Creator traders (unique traders on your markets)\n+create materialized view if not exists\n+ mv_ach_creator_traders as\n+with\n+ creator_traders as (\n+ select\n+ id as user_id,\n+ coalesce((data -> 'creatorTraders' ->> 'allTime')::int, 0) as creator_traders\n+ from\n+ users\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(ct.creator_traders, 0) as creator_traders,\n+ rank() over (\n+ order by\n+ coalesce(ct.creator_traders, 0) desc\n+ ) as creator_traders_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(ct.creator_traders, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as creator_traders_percentile\n+from\n+ all_users u\n+ left join creator_traders ct on ct.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_creator_traders_user_id_idx on public.mv_ach_creator_traders (user_id);\n+\n+-- Account age (in years at refresh time)\n+create materialized view if not exists\n+ mv_ach_account_age as\n+with\n+ ages as (\n+ select\n+ id as user_id,\n+ extract(\n+ epoch\n+ from\n+ (now() - created_time)\n+ ) / (365.25 * 24 * 60 * 60) as account_age_years\n+ from\n+ users\n+ ),\n+ all_users as (\n+ select\n+ id as user_id\n+ from\n+ users\n+ )\n+select\n+ u.user_id,\n+ coalesce(a.account_age_years, 0) as account_age_years,\n+ rank() over (\n+ order by\n+ coalesce(a.account_age_years, 0) desc\n+ ) as account_age_rank,\n+ (\n+ (\n+ (count(*) over ()) - rank() over (\n+ order by\n+ coalesce(a.account_age_years, 0) desc\n+ ) + 1\n+ )::numeric / (count(*) over ())\n+ ) * 100 as account_age_percentile\n+from\n+ all_users u\n+ left join ages a on a.user_id = u.user_id;\n+\n+create unique index if not exists mv_ach_account_age_user_id_idx on public.mv_ach_account_age (user_id);\n"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"path": "common/src/api/schema.ts",
|
|
53
|
-
"status": "modified",
|
|
54
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\t538b6b6 (parent)\n+++ common/src/api/schema.ts\t60532e5 (commit)\n@@ -1980,8 +1980,81 @@\n authed: true,\n returns: {} as { isSportsInterested: boolean },\n props: z.object({}).strict(),\n },\n+ 'get-user-achievements': {\n+ method: 'GET',\n+ visibility: 'public',\n+ authed: false,\n+ cache: LIGHT_CACHE_STRATEGY,\n+ returns: {} as {\n+ userId: string\n+ creatorTraders: number\n+ totalReferrals: number\n+ totalReferredProfitMana: number\n+ totalVolumeMana: number\n+ seasonsGoldOrHigher: number\n+ seasonsPlatinumOrHigher: number\n+ seasonsDiamondOrHigher: number\n+ seasonsMasters: number\n+ numberOfComments: number\n+ totalLiquidityCreatedMarkets: number\n+ totalTradesCount: number\n+ totalMarketsCreated: number\n+ accountAgeYears: number\n+ profitableMarketsCount: number\n+ unprofitableMarketsCount: number\n+ largestProfitableTradeValue: number\n+ largestUnprofitableTradeValue: number\n+ longestBettingStreak: number\n+ largestLeagueSeasonEarnings: number\n+ modTicketsResolved: number\n+ charityDonatedMana: number\n+ ranks: {\n+ creatorTraders: { rank: number | null; percentile: number | null }\n+ totalReferrals: { rank: number | null; percentile: number | null }\n+ totalReferredProfit: { rank: number | null; percentile: number | null }\n+ volume: { rank: number | null; percentile: number | null }\n+ trades: { rank: number | null; percentile: number | null }\n+ marketsCreated: { rank: number | null; percentile: number | null }\n+ comments: { rank: number | null; percentile: number | null }\n+ seasonsGoldOrHigher: { rank: number | null; percentile: number | null }\n+ seasonsPlatinumOrHigher: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ seasonsDiamondOrHigher: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ seasonsMasters: { rank: number | null; percentile: number | null }\n+ largestLeagueSeasonEarnings: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ liquidity: { rank: number | null; percentile: number | null }\n+ profitableMarkets: { rank: number | null; percentile: number | null }\n+ unprofitableMarkets: { rank: number | null; percentile: number | null }\n+ largestProfitableTrade: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ largestUnprofitableTrade: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ accountAge: { rank: number | null; percentile: number | null }\n+ longestBettingStreak: { rank: number | null; percentile: number | null }\n+ modTickets: { rank: number | null; percentile: number | null }\n+ charityDonated: { rank: number | null; percentile: number | null }\n+ }\n+ },\n+ props: z\n+ .object({\n+ userId: z.string(),\n+ })\n+ .strict(),\n+ },\n 'get-site-activity': {\n method: 'GET',\n visibility: 'public',\n authed: false,\n"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"path": "web/pages/[username]/index.tsx",
|
|
58
|
-
"status": "modified",
|
|
59
|
-
"diff": "Index: web/pages/[username]/index.tsx\n===================================================================\n--- web/pages/[username]/index.tsx\t538b6b6 (parent)\n+++ web/pages/[username]/index.tsx\t60532e5 (commit)\n@@ -13,8 +13,9 @@\n import { unauthedApi } from 'common/util/api'\n import { buildArray } from 'common/util/array'\n import { removeUndefinedProps } from 'common/util/object'\n import Head from 'next/head'\n+import Image from 'next/image'\n import Link from 'next/link'\n import { useRouter } from 'next/router'\n import { useEffect, useState } from 'react'\n import { UserBetsTable } from 'web/components/bet/user-bets-table'\n@@ -61,8 +62,13 @@\n import { db } from 'web/lib/supabase/db'\n import { getAverageUserRating, getUserRating } from 'web/lib/supabase/reviews'\n import Custom404 from 'web/pages/404'\n import { UserPayments } from 'web/pages/payments'\n+import {\n+ formatMoney,\n+ formatWithCommas,\n+ formatMoneyUSD,\n+} from 'common/util/format'\n \n export const getStaticProps = async (props: {\n params: {\n username: string\n@@ -426,8 +432,19 @@\n </>\n ),\n },\n {\n+ title: 'Achievements',\n+ prerender: true,\n+ stackedTabIcon: <TrophyIcon className=\"h-5\" />,\n+ content: (\n+ <>\n+ <Spacer h={4} />\n+ <AchievementsSection userId={user.id} />\n+ </>\n+ ),\n+ },\n+ {\n title: 'Balance log',\n stackedTabIcon: <ViewListIcon className=\"h-5\" />,\n content: <BalanceChangeTable user={user} />,\n queryString: balanceChangesKey,\n@@ -537,8 +554,515 @@\n </Row>\n )\n }\n \n+function AchievementsSection(props: { userId: string }) {\n+ const { userId } = props\n+ const [loading, setLoading] = useState(true)\n+ const [error, setError] = useState<string | null>(null)\n+ type RanksType = {\n+ creatorTraders: { rank: number | null; percentile: number | null }\n+ totalReferrals: { rank: number | null; percentile: number | null }\n+ totalReferredProfit: { rank: number | null; percentile: number | null }\n+ volume: { rank: number | null; percentile: number | null }\n+ trades: { rank: number | null; percentile: number | null }\n+ marketsCreated: { rank: number | null; percentile: number | null }\n+ comments: { rank: number | null; percentile: number | null }\n+ seasonsGoldOrHigher: { rank: number | null; percentile: number | null }\n+ seasonsPlatinumOrHigher: { rank: number | null; percentile: number | null }\n+ seasonsDiamondOrHigher: { rank: number | null; percentile: number | null }\n+ seasonsMasters: { rank: number | null; percentile: number | null }\n+ largestLeagueSeasonEarnings: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ liquidity: { rank: number | null; percentile: number | null }\n+ profitableMarkets: { rank: number | null; percentile: number | null }\n+ unprofitableMarkets: { rank: number | null; percentile: number | null }\n+ largestProfitableTrade: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ largestUnprofitableTrade: {\n+ rank: number | null\n+ percentile: number | null\n+ }\n+ accountAge: { rank: number | null; percentile: number | null }\n+ longestBettingStreak: { rank: number | null; percentile: number | null }\n+ modTickets: { rank: number | null; percentile: number | null }\n+ charityDonated: { rank: number | null; percentile: number | null }\n+ }\n+\n+ const [data, setData] = useState<{\n+ userId: string\n+ creatorTraders: number\n+ totalReferrals: number\n+ totalReferredProfitMana: number\n+ totalVolumeMana: number\n+ seasonsGoldOrHigher: number\n+ seasonsPlatinumOrHigher: number\n+ seasonsDiamondOrHigher: number\n+ seasonsMasters: number\n+ numberOfComments: number\n+ totalLiquidityCreatedMarkets: number\n+ totalTradesCount: number\n+ totalMarketsCreated: number\n+ accountAgeYears: number\n+ profitableMarketsCount: number\n+ unprofitableMarketsCount: number\n+ largestProfitableTradeValue: number\n+ largestUnprofitableTradeValue: number\n+ longestBettingStreak: number\n+ modTicketsResolved: number\n+ charityDonatedMana: number\n+ largestLeagueSeasonEarnings: number\n+ ranks: RanksType\n+ } | null>(null)\n+\n+ useEffect(() => {\n+ let isMounted = true\n+ setLoading(true)\n+ setError(null)\n+ unauthedApi('get-user-achievements', { userId })\n+ .then((resp) => {\n+ if (isMounted) setData(resp)\n+ })\n+ .catch((e) => {\n+ if (isMounted) setError(e?.message ?? 'Failed to load achievements')\n+ })\n+ .finally(() => {\n+ if (isMounted) setLoading(false)\n+ })\n+ return () => {\n+ isMounted = false\n+ }\n+ }, [userId])\n+\n+ if (loading)\n+ return (\n+ <Row className=\"text-ink-600 items-center gap-2\">\n+ Loading achievements…\n+ </Row>\n+ )\n+ if (error) return <div className=\"text-error\">{error}</div>\n+ if (!data) return null\n+\n+ type Rankish =\n+ | { rank: number | null; percentile: number | null }\n+ | null\n+ | undefined\n+\n+ const _p = (r?: Rankish) => r?.percentile ?? null\n+ const _r = (r?: Rankish) => r?.rank ?? null\n+\n+ // Rank key per achievement id\n+ const rankKeyById: Record<string, keyof RanksType> = {\n+ totalVolumeMana: 'volume',\n+ totalReferrals: 'totalReferrals',\n+ totalReferredProfitMana: 'totalReferredProfit',\n+ creatorTraders: 'creatorTraders',\n+ totalLiquidityCreatedMarkets: 'liquidity',\n+ profitableMarketsCount: 'profitableMarkets',\n+ unprofitableMarketsCount: 'unprofitableMarkets',\n+ largestProfitableTradeValue: 'largestProfitableTrade',\n+ largestUnprofitableTradeValue: 'largestUnprofitableTrade',\n+ seasonsGoldOrHigher: 'seasonsGoldOrHigher',\n+ seasonsPlatinumOrHigher: 'seasonsPlatinumOrHigher',\n+ seasonsDiamondOrHigher: 'seasonsDiamondOrHigher',\n+ seasonsMasters: 'seasonsMasters',\n+ largestLeagueSeasonEarnings: 'largestLeagueSeasonEarnings',\n+ numberOfComments: 'comments',\n+ totalTradesCount: 'trades',\n+ totalMarketsCreated: 'marketsCreated',\n+ accountAgeYears: 'accountAge',\n+ longestBettingStreak: 'longestBettingStreak',\n+ modTicketsResolved: 'modTickets',\n+ charityDonatedMana: 'charityDonated',\n+ }\n+\n+ // Raw numeric values by id for zero-handling\n+ const valueById: Record<string, number> = {\n+ totalVolumeMana: data.totalVolumeMana,\n+ totalReferrals: data.totalReferrals,\n+ totalReferredProfitMana: data.totalReferredProfitMana,\n+ creatorTraders: data.creatorTraders,\n+ totalLiquidityCreatedMarkets: data.totalLiquidityCreatedMarkets,\n+ profitableMarketsCount: data.profitableMarketsCount,\n+ unprofitableMarketsCount: data.unprofitableMarketsCount,\n+ largestProfitableTradeValue: data.largestProfitableTradeValue,\n+ largestUnprofitableTradeValue: data.largestUnprofitableTradeValue,\n+ seasonsGoldOrHigher: data.seasonsGoldOrHigher,\n+ seasonsPlatinumOrHigher: data.seasonsPlatinumOrHigher,\n+ seasonsDiamondOrHigher: data.seasonsDiamondOrHigher,\n+ seasonsMasters: data.seasonsMasters,\n+ largestLeagueSeasonEarnings: data.largestLeagueSeasonEarnings,\n+ numberOfComments: data.numberOfComments,\n+ totalTradesCount: data.totalTradesCount,\n+ totalMarketsCreated: data.totalMarketsCreated,\n+ accountAgeYears: data.accountAgeYears,\n+ longestBettingStreak: data.longestBettingStreak,\n+ modTicketsResolved: data.modTicketsResolved,\n+ charityDonatedMana: data.charityDonatedMana,\n+ }\n+\n+ const defs = [\n+ {\n+ id: 'totalVolumeMana',\n+ title: 'Any Whales?',\n+ desc: 'Total trading volume.',\n+ fmt: () => formatMoney(data.totalVolumeMana, 'MANA'),\n+ },\n+ {\n+ id: 'totalReferrals',\n+ title: 'Manifold Hype Man',\n+ desc: 'Friends you brought to Manifold.',\n+ fmt: () => formatWithCommas(data.totalReferrals),\n+ },\n+ {\n+ id: 'totalReferredProfitMana',\n+ title: 'Proud Parent',\n+ desc: 'Profit earned by your referrals.',\n+ fmt: () => formatMoney(data.totalReferredProfitMana, 'MANA'),\n+ },\n+ {\n+ id: 'creatorTraders',\n+ title: 'Fan Favorite',\n+ desc: 'Unique traders on your markets.',\n+ fmt: () => formatWithCommas(data.creatorTraders),\n+ },\n+ {\n+ id: 'totalLiquidityCreatedMarkets',\n+ title: 'No Slippage Here',\n+ desc: 'Total liquidity across all your created markets.',\n+ fmt: () => formatMoney(data.totalLiquidityCreatedMarkets, 'MANA'),\n+ },\n+ {\n+ id: 'profitableMarketsCount',\n+ title: 'Market Maven',\n+ desc: 'Number of markets you made a profit on.',\n+ fmt: () => formatWithCommas(data.profitableMarketsCount),\n+ },\n+ {\n+ id: 'unprofitableMarketsCount',\n+ title: 'Ineffective Altruism',\n+ desc: 'Number of markets you lost mana on.',\n+ fmt: () => formatWithCommas(data.unprofitableMarketsCount),\n+ },\n+ {\n+ id: 'largestProfitableTradeValue',\n+ title: 'Biggest Win',\n+ desc: 'Largest profit made on a single market.',\n+ fmt: () => formatMoney(data.largestProfitableTradeValue, 'MANA'),\n+ },\n+ {\n+ id: 'largestUnprofitableTradeValue',\n+ title: 'Wealth Redistributor',\n+ desc: 'Largest loss made on a single market.',\n+ fmt: () => formatMoney(data.largestUnprofitableTradeValue, 'MANA'),\n+ },\n+ {\n+ id: 'seasonsGoldOrHigher',\n+ title: 'Gleaming Gold',\n+ desc: 'Seasons finished Gold or higher.',\n+ fmt: () => formatWithCommas(data.seasonsGoldOrHigher),\n+ },\n+ {\n+ id: 'seasonsPlatinumOrHigher',\n+ title: 'Positively Platinum',\n+ desc: 'Seasons finished Platinum or higher.',\n+ fmt: () => formatWithCommas(data.seasonsPlatinumOrHigher),\n+ },\n+ {\n+ id: 'seasonsDiamondOrHigher',\n+ title: 'Diamond Hands',\n+ desc: 'Seasons finished Diamond or higher.',\n+ fmt: () => formatWithCommas(data.seasonsDiamondOrHigher),\n+ },\n+ {\n+ id: 'seasonsMasters',\n+ title: 'Master Mind',\n+ desc: 'Seasons finished Masters.',\n+ fmt: () => formatWithCommas(data.seasonsMasters),\n+ },\n+ {\n+ id: 'largestLeagueSeasonEarnings',\n+ title: 'Sensational Season',\n+ desc: 'Largest earnings in a single season.',\n+ fmt: () => formatMoney(data.largestLeagueSeasonEarnings, 'MANA'),\n+ },\n+\n+ {\n+ id: 'numberOfComments',\n+ title: 'Chatterbox',\n+ desc: 'Number of comments you’ve posted with at least 1 like.',\n+ fmt: () => formatWithCommas(data.numberOfComments),\n+ },\n+ {\n+ id: 'totalTradesCount',\n+ title: 'High Frequency Trader',\n+ desc: 'Total number of trades executed (excludes API trades).',\n+ fmt: () => formatWithCommas(data.totalTradesCount),\n+ },\n+ {\n+ id: 'totalMarketsCreated',\n+ title: 'Doing The Hard Part',\n+ desc: 'Number of markets you’ve created.',\n+ fmt: () => formatWithCommas(data.totalMarketsCreated),\n+ },\n+ {\n+ id: 'accountAgeYears',\n+ title: 'Age Is Just A Number',\n+ desc: 'Account age in years.',\n+ fmt: () => {\n+ const yearsFloat = data.accountAgeYears\n+ const totalMonths = Math.round(yearsFloat * 12)\n+ const years = Math.floor(totalMonths / 12)\n+ const months = totalMonths % 12\n+ const yLabel = years === 1 ? 'year' : 'years'\n+ const mLabel = months === 1 ? 'month' : 'months'\n+ return `${years} ${yLabel} ${months} ${mLabel}`\n+ },\n+ },\n+ {\n+ id: 'longestBettingStreak',\n+ title: 'Longest Daily Streak',\n+ desc: 'Longest consecutive days trading.',\n+ fmt: () => formatWithCommas(data.longestBettingStreak),\n+ },\n+ {\n+ id: 'modTicketsResolved',\n+ title: 'Helpful Moderator',\n+ desc: 'Mod tickets you’ve resolved (since mod rewards were introduced).',\n+ fmt: () => formatWithCommas(data.modTicketsResolved),\n+ },\n+ {\n+ id: 'charityDonatedMana',\n+ title: 'Giver',\n+ desc: 'Total donated to charity (USD).',\n+ fmt: () => formatMoneyUSD(data.charityDonatedMana, true),\n+ },\n+ ] as const\n+\n+ const ACHS = defs.map(({ id, title, desc, fmt }) => {\n+ const key = rankKeyById[id]\n+ const raw = valueById[id] ?? 0\n+ const rk = key ? data.ranks?.[key]?.rank ?? null : null\n+ const pc = key ? data.ranks?.[key]?.percentile ?? null : null\n+ return {\n+ id,\n+ title,\n+ desc,\n+ value: fmt(),\n+ rank: raw === 0 ? null : rk,\n+ percentile: raw === 0 ? null : pc,\n+ }\n+ })\n+\n+ const bucketOf = (rank: number | null) => {\n+ if (rank == null) return 'All Users'\n+ if (rank <= 50) return 'Top 50 Users'\n+ if (rank <= 200) return 'Top 200 Users'\n+ if (rank <= 1000) return 'Top 1000 Users'\n+ if (rank <= 5000) return 'Top 5000 Users'\n+ if (rank <= 20000) return 'Top 20,000 Users'\n+ return 'All Users'\n+ }\n+\n+ const bucketOrder = [\n+ 'Top 50 Users',\n+ 'Top 200 Users',\n+ 'Top 1000 Users',\n+ 'Top 5000 Users',\n+ 'Top 20,000 Users',\n+ 'All Users',\n+ ] as const\n+ const byBucket: Record<\n+ (typeof bucketOrder)[number],\n+ (typeof ACHS)[number][]\n+ > = {\n+ 'Top 50 Users': [],\n+ 'Top 200 Users': [],\n+ 'Top 1000 Users': [],\n+ 'Top 5000 Users': [],\n+ 'Top 20,000 Users': [],\n+ 'All Users': [],\n+ }\n+\n+ ACHS.forEach((a) => {\n+ byBucket[bucketOf(a.rank)].push(a as any)\n+ })\n+\n+ return (\n+ <Col className=\"gap-6\">\n+ {bucketOrder.map((bucket) => {\n+ const items = byBucket[bucket]\n+ if (!items.length) return null\n+ const sorted = items.slice().sort((a, b) => {\n+ const ra = a.rank ?? Infinity\n+ const rb = b.rank ?? Infinity\n+ if (ra !== rb) return ra - rb\n+ const pa = a.percentile ?? Infinity\n+ const pb = b.percentile ?? Infinity\n+ return pa - pb\n+ })\n+ return (\n+ <Col key={bucket} className=\"gap-3\">\n+ <div className=\"text-ink-800 flex items-center gap-2 text-sm font-semibold uppercase tracking-wider\">\n+ {bucket}\n+ <span className=\"bg-ink-200 h-px flex-1\" />\n+ </div>\n+\n+ <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3\">\n+ {sorted.map((a) => (\n+ <AchievementBadgeCard\n+ key={a.id}\n+ title={a.title}\n+ description={a.desc}\n+ value={a.value}\n+ rank={a.rank}\n+ percentile={a.percentile}\n+ imageSrc={(() => {\n+ const IMAGE_BY_ID: Record<string, string> = {\n+ accountAgeYears:\n+ '/achievement-badges/accountAgeYears.png',\n+ charityDonatedMana:\n+ '/achievement-badges/charityDonatedMana.png',\n+ creatorTraders: '/achievement-badges/creatorTraders.png',\n+ largestProfitableTradeValue:\n+ '/achievement-badges/largestProfitableTradeValue.png',\n+ largestLeagueSeasonEarnings:\n+ '/achievement-badges/largestLeagueSeasonEarnings.png',\n+ largestUnprofitableTradeValue:\n+ '/achievement-badges/largestUnprofitableTradeValue.png',\n+ longestBettingStreak:\n+ '/achievement-badges/longestBettingStreak.png',\n+ modTicketsResolved:\n+ '/achievement-badges/modTicketsResolved.png',\n+ numberOfComments:\n+ '/achievement-badges/numberOfComments.png',\n+ profitableMarketsCount:\n+ '/achievement-badges/profitableMarketsCount.png',\n+ seasonsDiamondOrHigher:\n+ '/achievement-badges/seasonsDiamondOrHigher.png',\n+ seasonsGoldOrHigher:\n+ '/achievement-badges/seasonsGoldOrHigher.png',\n+ seasonsMasters: '/achievement-badges/seasonsMasters.png',\n+ seasonsPlatinumOrHigher:\n+ '/achievement-badges/seasonsPlatinumOrHigher.png',\n+ totalLiquidityCreatedMarkets:\n+ '/achievement-badges/totalLiquidityCreatedMarkets.png',\n+ totalMarketsCreated:\n+ '/achievement-badges/totalMarketsCreated.png',\n+ totalReferrals: '/achievement-badges/totalReferrals.png',\n+ totalReferredProfitMana:\n+ '/achievement-badges/totalReferredProfitMana.png',\n+ totalTradesCount:\n+ '/achievement-badges/totalTradesCount.png',\n+ totalVolumeMana:\n+ '/achievement-badges/totalVolumeMana.png',\n+ unprofitableMarketsCount:\n+ '/achievement-badges/unprofitableMarketsCount.png',\n+ }\n+ return (\n+ IMAGE_BY_ID[a.id] ||\n+ '/achievement-badges/totalVolumeMana.png'\n+ )\n+ })()}\n+ bucket={bucket}\n+ />\n+ ))}\n+ </div>\n+ </Col>\n+ )\n+ })}\n+ </Col>\n+ )\n+}\n+\n+function AchievementBadgeCard(props: {\n+ title: string\n+ description: string\n+ value: string\n+ rank: number | null\n+ percentile: number | null\n+ bucket:\n+ | 'Top 50 Users'\n+ | 'Top 200 Users'\n+ | 'Top 1000 Users'\n+ | 'Top 5000 Users'\n+ | 'Top 20,000 Users'\n+ | 'All Users'\n+ imageSrc?: string\n+}) {\n+ const { title, description, value, rank, percentile, bucket, imageSrc } =\n+ props\n+\n+ const bucketStyle: Record<typeof props.bucket, string> = {\n+ 'Top 50 Users': 'from-fuchsia-500 to-indigo-500',\n+ 'Top 200 Users': 'from-indigo-500 to-sky-500',\n+ 'Top 1000 Users': 'from-sky-500 to-teal-500',\n+ 'Top 5000 Users': 'from-emerald-500 to-lime-500',\n+ 'Top 20,000 Users': 'from-slate-500 to-zinc-500',\n+ 'All Users': 'from-zinc-400 to-zinc-600',\n+ }\n+\n+ return (\n+ <div\n+ className=\"border-ink-200 group relative rounded-xl border p-[1px] transition-shadow hover:shadow-lg\"\n+ aria-label={title}\n+ >\n+ {/* gradient ring */}\n+ <div\n+ className={clsx(\n+ 'h-full rounded-xl bg-gradient-to-br',\n+ bucketStyle[bucket]\n+ )}\n+ >\n+ <div className=\"bg-canvas-0 flex h-full flex-col items-center space-y-3 rounded-[11px] px-4 pb-6 pt-6\">\n+ <div className=\" ring-ink-300/50 flex h-32 w-32 items-center justify-center overflow-hidden rounded-full ring-1\">\n+ {imageSrc ? (\n+ <Image\n+ src={imageSrc}\n+ alt={title}\n+ width={128}\n+ height={128}\n+ className=\"h-32 w-32 scale-125 rounded-full object-contain\"\n+ />\n+ ) : null}\n+ </div>\n+ <div className=\"pt-4 text-center\">\n+ <div className=\"text-ink-900 text-lg font-semibold\">{title}</div>\n+ <div className=\"text-ink-600 mt-1 text-sm leading-snug\">\n+ {description}\n+ </div>\n+ <div className=\"text-ink-900 mt-3 text-lg\">{value}</div>\n+ </div>\n+\n+ {/* side hover tooltip */}\n+ <div className=\"pointer-events-none absolute left-full top-4 z-20 hidden pl-3 group-hover:block\">\n+ <div className=\"bg-canvas-50 text-ink-900 border-ink-200 w-64 rounded-md border p-3 shadow-xl\">\n+ <div className=\"mt-1 text-lg font-semibold\">\n+ Rank: {rank ?? 'N/A'}\n+ </div>\n+ <div className=\"text-ink-600 my-1 text-sm\">\n+ {percentile != null\n+ ? `In the top ${(() => {\n+ const s = Number(\n+ Math.max(percentile, 0.01).toFixed(2)\n+ ).toString()\n+ return s\n+ })()}% of all users`\n+ : 'N/A'}\n+ </div>\n+ <div className=\"text-ink-600 text-sm\">Value: {value}</div>\n+ </div>\n+ </div>\n+ </div>\n+ </div>\n+ </div>\n+ )\n+}\n+\n function FollowsDialog(props: {\n user: User\n followingIds: string[] | undefined\n followerIds: string[] | undefined\n"
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
"path": "web/public/achievement-badges/accountAgeYears.png",
|
|
63
|
-
"status": "added",
|
|
64
|
-
"diff": "Index: web/public/achievement-badges/accountAgeYears.png\n===================================================================\n--- web/public/achievement-badges/accountAgeYears.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/accountAgeYears.png\t60532e5 (commit)\n"
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
"path": "web/public/achievement-badges/charityDonatedMana.png",
|
|
68
|
-
"status": "added",
|
|
69
|
-
"diff": "Index: web/public/achievement-badges/charityDonatedMana.png\n===================================================================\n--- web/public/achievement-badges/charityDonatedMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/charityDonatedMana.png\t60532e5 (commit)\n"
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
"path": "web/public/achievement-badges/creatorTraders.png",
|
|
73
|
-
"status": "added",
|
|
74
|
-
"diff": "Index: web/public/achievement-badges/creatorTraders.png\n===================================================================\n--- web/public/achievement-badges/creatorTraders.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/creatorTraders.png\t60532e5 (commit)\n"
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
"path": "web/public/achievement-badges/highestBalanceMana.png",
|
|
78
|
-
"status": "added",
|
|
79
|
-
"diff": "Index: web/public/achievement-badges/highestBalanceMana.png\n===================================================================\n--- web/public/achievement-badges/highestBalanceMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/highestBalanceMana.png\t60532e5 (commit)\n"
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
"path": "web/public/achievement-badges/highestInvestedMana.png",
|
|
83
|
-
"status": "added",
|
|
84
|
-
"diff": "Index: web/public/achievement-badges/highestInvestedMana.png\n===================================================================\n--- web/public/achievement-badges/highestInvestedMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/highestInvestedMana.png\t60532e5 (commit)\n"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"path": "web/public/achievement-badges/highestLoanMana.png",
|
|
88
|
-
"status": "added",
|
|
89
|
-
"diff": "Index: web/public/achievement-badges/highestLoanMana.png\n===================================================================\n--- web/public/achievement-badges/highestLoanMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/highestLoanMana.png\t60532e5 (commit)\n"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
"path": "web/public/achievement-badges/highestNetworthMana.png",
|
|
93
|
-
"status": "added",
|
|
94
|
-
"diff": "Index: web/public/achievement-badges/highestNetworthMana.png\n===================================================================\n--- web/public/achievement-badges/highestNetworthMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/highestNetworthMana.png\t60532e5 (commit)\n"
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"path": "web/public/achievement-badges/largestLeagueSeasonEarnings.png",
|
|
98
|
-
"status": "added",
|
|
99
|
-
"diff": "Index: web/public/achievement-badges/largestLeagueSeasonEarnings.png\n===================================================================\n--- web/public/achievement-badges/largestLeagueSeasonEarnings.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/largestLeagueSeasonEarnings.png\t60532e5 (commit)\n"
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
"path": "web/public/achievement-badges/largestProfitableTradeValue.png",
|
|
103
|
-
"status": "added",
|
|
104
|
-
"diff": "Index: web/public/achievement-badges/largestProfitableTradeValue.png\n===================================================================\n--- web/public/achievement-badges/largestProfitableTradeValue.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/largestProfitableTradeValue.png\t60532e5 (commit)\n"
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
"path": "web/public/achievement-badges/largestUnprofitableTradeValue.png",
|
|
108
|
-
"status": "added",
|
|
109
|
-
"diff": "Index: web/public/achievement-badges/largestUnprofitableTradeValue.png\n===================================================================\n--- web/public/achievement-badges/largestUnprofitableTradeValue.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/largestUnprofitableTradeValue.png\t60532e5 (commit)\n"
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
"path": "web/public/achievement-badges/longestBettingStreak.png",
|
|
113
|
-
"status": "added",
|
|
114
|
-
"diff": "Index: web/public/achievement-badges/longestBettingStreak.png\n===================================================================\n--- web/public/achievement-badges/longestBettingStreak.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/longestBettingStreak.png\t60532e5 (commit)\n"
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
"path": "web/public/achievement-badges/modTicketsResolved.png",
|
|
118
|
-
"status": "added",
|
|
119
|
-
"diff": "Index: web/public/achievement-badges/modTicketsResolved.png\n===================================================================\n--- web/public/achievement-badges/modTicketsResolved.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/modTicketsResolved.png\t60532e5 (commit)\n"
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
"path": "web/public/achievement-badges/numberOfComments.png",
|
|
123
|
-
"status": "added",
|
|
124
|
-
"diff": "Index: web/public/achievement-badges/numberOfComments.png\n===================================================================\n--- web/public/achievement-badges/numberOfComments.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/numberOfComments.png\t60532e5 (commit)\n"
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
"path": "web/public/achievement-badges/profitableMarketsCount.png",
|
|
128
|
-
"status": "added",
|
|
129
|
-
"diff": "Index: web/public/achievement-badges/profitableMarketsCount.png\n===================================================================\n--- web/public/achievement-badges/profitableMarketsCount.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/profitableMarketsCount.png\t60532e5 (commit)\n"
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
"path": "web/public/achievement-badges/seasonsDiamondOrHigher.png",
|
|
133
|
-
"status": "added",
|
|
134
|
-
"diff": "Index: web/public/achievement-badges/seasonsDiamondOrHigher.png\n===================================================================\n--- web/public/achievement-badges/seasonsDiamondOrHigher.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/seasonsDiamondOrHigher.png\t60532e5 (commit)\n"
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
"path": "web/public/achievement-badges/seasonsGoldOrHigher.png",
|
|
138
|
-
"status": "added",
|
|
139
|
-
"diff": "Index: web/public/achievement-badges/seasonsGoldOrHigher.png\n===================================================================\n--- web/public/achievement-badges/seasonsGoldOrHigher.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/seasonsGoldOrHigher.png\t60532e5 (commit)\n"
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
"path": "web/public/achievement-badges/seasonsMasters.png",
|
|
143
|
-
"status": "added",
|
|
144
|
-
"diff": "Index: web/public/achievement-badges/seasonsMasters.png\n===================================================================\n--- web/public/achievement-badges/seasonsMasters.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/seasonsMasters.png\t60532e5 (commit)\n"
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
"path": "web/public/achievement-badges/seasonsPlatinumOrHigher.png",
|
|
148
|
-
"status": "added",
|
|
149
|
-
"diff": "Index: web/public/achievement-badges/seasonsPlatinumOrHigher.png\n===================================================================\n--- web/public/achievement-badges/seasonsPlatinumOrHigher.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/seasonsPlatinumOrHigher.png\t60532e5 (commit)\n"
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
"path": "web/public/achievement-badges/totalLiquidityCreatedMarkets.png",
|
|
153
|
-
"status": "added",
|
|
154
|
-
"diff": "Index: web/public/achievement-badges/totalLiquidityCreatedMarkets.png\n===================================================================\n--- web/public/achievement-badges/totalLiquidityCreatedMarkets.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalLiquidityCreatedMarkets.png\t60532e5 (commit)\n"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
"path": "web/public/achievement-badges/totalMarketsCreated.png",
|
|
158
|
-
"status": "added",
|
|
159
|
-
"diff": "Index: web/public/achievement-badges/totalMarketsCreated.png\n===================================================================\n--- web/public/achievement-badges/totalMarketsCreated.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalMarketsCreated.png\t60532e5 (commit)\n"
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
"path": "web/public/achievement-badges/totalProfitMana.png",
|
|
163
|
-
"status": "added",
|
|
164
|
-
"diff": "Index: web/public/achievement-badges/totalProfitMana.png\n===================================================================\n--- web/public/achievement-badges/totalProfitMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalProfitMana.png\t60532e5 (commit)\n"
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
"path": "web/public/achievement-badges/totalReferrals.png",
|
|
168
|
-
"status": "added",
|
|
169
|
-
"diff": "Index: web/public/achievement-badges/totalReferrals.png\n===================================================================\n--- web/public/achievement-badges/totalReferrals.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalReferrals.png\t60532e5 (commit)\n"
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
"path": "web/public/achievement-badges/totalReferredProfitMana.png",
|
|
173
|
-
"status": "added",
|
|
174
|
-
"diff": "Index: web/public/achievement-badges/totalReferredProfitMana.png\n===================================================================\n--- web/public/achievement-badges/totalReferredProfitMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalReferredProfitMana.png\t60532e5 (commit)\n"
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
"path": "web/public/achievement-badges/totalTradesCount.png",
|
|
178
|
-
"status": "added",
|
|
179
|
-
"diff": "Index: web/public/achievement-badges/totalTradesCount.png\n===================================================================\n--- web/public/achievement-badges/totalTradesCount.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalTradesCount.png\t60532e5 (commit)\n"
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
"path": "web/public/achievement-badges/totalVolumeMana.png",
|
|
183
|
-
"status": "added",
|
|
184
|
-
"diff": "Index: web/public/achievement-badges/totalVolumeMana.png\n===================================================================\n--- web/public/achievement-badges/totalVolumeMana.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/totalVolumeMana.png\t60532e5 (commit)\n"
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
"path": "web/public/achievement-badges/unprofitableMarketsCount.png",
|
|
188
|
-
"status": "added",
|
|
189
|
-
"diff": "Index: web/public/achievement-badges/unprofitableMarketsCount.png\n===================================================================\n--- web/public/achievement-badges/unprofitableMarketsCount.png\t538b6b6 (parent)\n+++ web/public/achievement-badges/unprofitableMarketsCount.png\t60532e5 (commit)\n"
|
|
190
|
-
}
|
|
191
|
-
]
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
"id": "add-date-markets",
|
|
195
|
-
"sha": "e96bced24acd6e3e19bff1fd849b6dc35ec317f1",
|
|
196
|
-
"parentSha": "d70bc7a4d2878aa28eca08c69e2786f034a254e0",
|
|
197
|
-
"spec": "Implement a new predictive market outcome type: DATE.\n\nFunctional requirements\n- A DATE market can be created with answers (ranges or thresholds) and midpoints (timestamps in ms), with shouldAnswersSumToOne controlling buckets vs thresholds.\n- The market stores an IANA timezone string and an optional display mode 'clock' or 'default'.\n- Creators can use AI to generate date ranges (both buckets and thresholds) and regenerate midpoints when ranges change.\n- Expected date displays on contract pages, tables, and OG metadata; optionally as a live clock when display='clock'.\n- A time-series chart shows expected date over time with date-formatted Y-axis and time-based X-axis.\n- Bets and resolutions for DATE use existing cpmm-multi-1 paths without adding new outcome-type-specific branches.\n- Duplicating a DATE market carries over answers and midpoints (no unit); search filters include DATE; notifications and resolution helpers treat DATE like multi contracts, respecting independent vs sum-to-one behavior.\n\nCommon (types, unions, helpers)\n- Define MultiDate type (like MultiNumeric but with outcomeType 'DATE', timezone: string, optional display: 'clock').\n- Extend unions/aliases to include MultiDate: AnyContractType, MarketContract, MultiContract; include 'DATE' in CREATEABLE_OUTCOME_TYPES; exclude from isSpecialLoveContract.\n- Add chart ValueKind 'date'.\n- Add multi-date helpers: getExpectedDate (handles buckets and thresholds with tail handling) and formatExpectedDate; reuse getMinMax to return min/max of date midpoints.\n- Rename numeric-only constant MULTI_NUMERIC_BUCKETS_MAX to NUMBER_BUCKETS_MAX where used for numeric bucket precision checks (DATE does not use unit or number bucket limits).\n\nValidation schemas\n- Add createMultiDateSchema with fields: outcomeType 'DATE', answers (string[]; length <= MAX_MULTI_NUMERIC_ANSWERS), midpoints (number[] ms; length <= same), shouldAnswersSumToOne (boolean), addAnswersMode 'DISABLED', timezone (string).\n- Update createMarketProps to include createMultiDateSchema.\n- Update updateMarketProps to accept display?: 'clock' | 'default'.\n- Update API schema:\n - generate-ai-date-ranges props: { question, min, max, description? }; returns { buckets: {answers, midpoints}, thresholds: {answers, midpoints} }.\n - regenerate-date-midpoints props: { question, description?, answers, min, max, tab: 'thresholds' | 'buckets' }; returns { midpoints } with ms epoch values.\n- Update market-search-types to include 'DATE'.\n\nBackend API\n- Create market: validate DATE using createMultiDateSchema; enforce answers.length >= 2 and answers.length === midpoints.length; pass timezone to contract creation and native columns.\n- AI date ranges:\n - generate-ai-date-ranges: prompt the model to return both buckets and thresholds with answers and midpoints as ISO date strings. Convert midpoints to ms (convertDateMidpointsToTimes), assert uniqueness and ascending order; return both sets.\n - regenerate-date-midpoints: accept tab; prompt for midpoint dates only, convert to ms, assert uniqueness and ascending order; return ms array.\n- Update update-market to persist display.\n- Clean contract for static props: preserve answers for DATE.\n- Place bet / resolve: rely on mechanism branching (cpmm-1 vs cpmm-multi-1) so DATE is included automatically.\n- Notifications and resolve helpers: use MarketContract and include DATE in multi-choice logic (independent multi if shouldAnswersSumToOne is false).\n\nEconomy and payouts\n- Treat DATE like multi when computing ante (common/economy).\n- Collapse fixed payout helpers for multi contracts to accept MultiContract (includes DATE) when calculating payouts.\n\nContract creation\n- Implement getDateProps in new-contract.ts to build MultiDate contracts with provided answers/midpoints, shouldAnswersSumToOne, and timezone. Do not set unit for DATE.\n\nWeb UI and charting\n- Add MultiDate chart component rendering expected-date history:\n - For buckets, sum prob*midpoint across answers.\n - For thresholds, use discrete probabilities (diff of cumulative thresholds) with a tail beyond the last threshold interval.\n - Use yKind='date' and date-formatted ticks in the generic chart.\n- Enhance generic chart to support yKind 'date' with appropriate tick formatting (years, month-year, month-day based on span).\n- Expected date display component:\n - When unresolved, show formatted expected date with animated number; if display='clock', show live Clock to expected date with tz tooltip.\n - When resolved, show resolved answer text or cancel label.\n- Contract overview for DATE: show time range picker, the date chart, and answers panel; show resolution panel based on shouldAnswersSumToOne.\n- Treat DATE as multi in:\n - contract-page bet data fetching (include DATE in multiPoints processing).\n - contracts table / SEO expected value formatting.\n - answers panel prop types (use MultiContract).\n - hide binary multi panels where DATE should not appear.\n - duplicate-contract: carry over answers and midpoints; only set unit for MULTI_NUMERIC.\n\nClock component\n- Support year or ms timestamp inputs; add size prop (xs|sm|md|lg) that controls text sizing and layout; update to compute durations from ms target.\n\nAcceptance\n- Creating a DATE market via UI with AI-generated ranges is possible; input fields collect min/max date strings; switching between tabs (buckets/thresholds) updates shouldAnswersSumToOne and keeps separate answers/midpoints per tab; midpoint regeneration debounced and robust to errors.\n- Expected date displays consistently across contract page, tables, and OG metadata. Clock mode works with visible ticking and timezone tooltip.\n- Chart renders and scales correctly, with date-formatted Y-axis and time-based X-axis; zoom/slider integration works.\n- Bets and resolution paths work without DATE-specific conditionals beyond mechanism-based branching.\n- Search includes DATE filter; notifications are sent for DATE resolutions; duplicate-contract URL includes answers and midpoints for DATE.\n- API contracts conform to updated schemas; midpoints are validated (unique, ascending) and converted from ISO strings to epoch ms.",
|
|
198
|
-
"prompt": "Add a new Date market outcome type across backend, shared, and web. The type should behave like multi-choice numeric markets but represent answer midpoints as timestamps. Support two modes: buckets (answers sum to one) and thresholds (cumulative). Include timezone on creation and a toggleable clock display. Provide AI endpoints to generate date ranges (both buckets and thresholds) and to regenerate midpoint dates when ranges change. Update types, schemas, creation flows, expected date calculation/formatting, charting with date axes, and UI so DATE is handled like other cpmm-multi-1 markets for betting, resolving, duplication, search, and notifications.",
|
|
199
|
-
"supplementalFiles": [
|
|
200
|
-
"common/src/calculate.ts",
|
|
201
|
-
"common/src/answer.ts",
|
|
202
|
-
"common/src/numeric-constants.ts",
|
|
203
|
-
"web/lib/api/api.ts",
|
|
204
|
-
"web/components/charts/contract/single-value.tsx",
|
|
205
|
-
"web/components/sized-container.tsx",
|
|
206
|
-
"web/components/widgets/tooltip.tsx",
|
|
207
|
-
"backend/api/src/helpers/rate-limit.ts",
|
|
208
|
-
"backend/api/src/helpers/endpoint.ts",
|
|
209
|
-
"web/components/charts/contract/number.tsx"
|
|
210
|
-
],
|
|
211
|
-
"fileDiffs": [
|
|
212
|
-
{
|
|
213
|
-
"path": "backend/api/src/create-market.ts",
|
|
214
|
-
"status": "modified",
|
|
215
|
-
"diff": "Index: backend/api/src/create-market.ts\n===================================================================\n--- backend/api/src/create-market.ts\td70bc7a (parent)\n+++ backend/api/src/create-market.ts\te96bced (commit)\n@@ -7,8 +7,9 @@\n createNumericSchema,\n createPollSchema,\n toLiteMarket,\n createMultiNumericSchema,\n+ createMultiDateSchema,\n } from 'common/api/market-types'\n import { ValidatedAPIParams } from 'common/api/schema'\n import {\n Contract,\n@@ -127,8 +128,9 @@\n takerAPIOrdersDisabled,\n liquidityTier,\n unit,\n midpoints,\n+ timezone,\n } = validateMarketBody(body)\n \n const userId = auth.uid\n \n@@ -220,8 +222,9 @@\n sportsLeague,\n takerAPIOrdersDisabled,\n unit: unit ?? '',\n midpoints: midpoints,\n+ timezone: timezone,\n })\n )\n const nativeKeys = nativeContractColumnsArray.map(camelCase)\n const nativeValues = nativeKeys\n@@ -353,9 +356,10 @@\n shouldAnswersSumToOne: boolean | undefined,\n totalBounty: number | undefined,\n isAutoBounty: boolean | undefined,\n unit: string | undefined,\n- midpoints: number[] | undefined\n+ midpoints: number[] | undefined,\n+ timezone: string | undefined\n \n if (outcomeType === 'PSEUDO_NUMERIC') {\n const parsed = validateMarketType(outcomeType, createNumericSchema, body)\n \n@@ -423,8 +427,22 @@\n 400,\n 'Number of answers must match number of midpoints.'\n )\n }\n+ if (outcomeType === 'DATE') {\n+ ;({ answers, midpoints, shouldAnswersSumToOne, timezone } =\n+ validateMarketType(outcomeType, createMultiDateSchema, body))\n+ if (answers.length < 2)\n+ throw new APIError(\n+ 400,\n+ 'Numeric markets must have at least 2 answer buckets.'\n+ )\n+ if (answers.length !== midpoints.length)\n+ throw new APIError(\n+ 400,\n+ 'Number of answers must match number of midpoints.'\n+ )\n+ }\n \n if (outcomeType === 'MULTIPLE_CHOICE') {\n ;({\n answers,\n@@ -483,8 +501,9 @@\n answerImageUrls,\n takerAPIOrdersDisabled,\n unit,\n midpoints,\n+ timezone,\n }\n }\n \n function validateMarketType<T extends z.ZodType>(\n"
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
"path": "backend/api/src/generate-ai-date-ranges.ts",
|
|
219
|
-
"status": "modified",
|
|
220
|
-
"diff": "Index: backend/api/src/generate-ai-date-ranges.ts\n===================================================================\n--- backend/api/src/generate-ai-date-ranges.ts\td70bc7a (parent)\n+++ backend/api/src/generate-ai-date-ranges.ts\te96bced (commit)\n@@ -1,185 +1,210 @@\n import { APIHandler } from './helpers/endpoint'\n import { track } from 'shared/analytics'\n-import {\n- models,\n- promptClaudeParsingJson,\n-} from 'shared/helpers/claude'\n+import { models, promptClaudeParsingJson } from 'shared/helpers/claude'\n import { log } from 'shared/utils'\n import { rateLimitByUser } from './helpers/rate-limit'\n import { HOUR_MS } from 'common/util/time'\n import {\n assertMidpointsAreAscending,\n assertMidpointsAreUnique,\n- RangeResponse,\n } from './generate-ai-numeric-ranges'\n // Shared guidelines for both threshold and bucket ranges\n // Base system prompt template that both threshold and bucket prompts will use\n \n-const baseDateSystemPrompt = () => {\n+type DateRangeResponse = {\n+ answers: string[]\n+ midpoints: string[]\n+}\n+\n+const baseDateSystemPrompt = (type: 'buckets' | 'thresholds') => {\n return `\n You are a helpful AI assistant that generates date ranges for prediction market questions.\n \n GUIDLINES:\n - Generate 2-12 ranges that cover the entire span from start to end\n - Err on the side of fewer (3-5) ranges when possible\n- - Favor human-friendly ranges like:\n- * Round numbers\n- * Smaller ranges for more precision when values are likely to be close\n+ - Do NOT generate more than 12 ranges\n+ - Today's date is ${new Date().toISOString()}\n - Each range should have a midpoint value for expected value calculations\n+ - The midpoints should be dates that represent the exact middle date of the range.\n - Return the ranges and associated midpoints in ascending order\n- - If the unit is a year/month/day and the max-min < 10 just use single years/months/days as buckets.\n- - If the unit is a unit of time, the midpoint should be the ms from now to the target date.\n+ ${\n+ type === 'buckets'\n+ ? '- If the min/max is a year/month/day and the max-min < 10 just use single years/months/days as buckets.'\n+ : `- If the min/max is a year/month/day and the max-min < 10 just use single 'before year/ before month/ before day' as thresholds.`\n+ }\n - ONLY return a single JSON object without any other text or formatting:\n {\n answers: array of range strings,\n- midpoints: array of corresponding midpoint numbers\n+ midpoints: array of corresponding midpoint dates\n }\n- ${bucketExamples}\n+ ${type === 'buckets' ? bucketExamples : thresholdExamples}\n `\n }\n \n const bucketExamples = `\n EXAMPLES:\n Question: If convicted, when will SBF serve time? (min: 2025, max: 2028)\n {\n \"answers\": [\"2025\", \"2026\", \"2027\", \"2028\"],\n- \"midpoints\": [2025.5, 2026.5, 2027.5, 2028.5]\n+ \"midpoints\": [\"2025-07-02\", \"2026-07-02\", \"2027-07-02\", \"2028-07-02\"]\n }\n+\n+ Question: When will the next US recession begin? (min: Q1 2025, max: Q4 2026)\n+ {\n+ \"answers\": [\"Q1 2025\", \"Q2 2025\", \"Q3 2025\", \"Q4 2025\", \"Q1 2026\", \"Q2 2026\", \"Q3 2026\", \"Q4 2026\"],\n+ \"midpoints\": [\"2025-02-14\", \"2025-05-14\", \"2025-08-14\", \"2025-11-14\", \"2026-02-14\", \"2026-05-14\", \"2026-08-14\", \"2026-11-14\"]\n+ }\n+\n+ Question: When will SpaceX complete its first crewed Mars mission? (min: 2025, max: 2035)\n+ {\n+ \"answers\": [\"2025\", \"2026\", \"2027\", \"2028\", \"2029\", \"2030\", \"2031\", \"2032\", \"2033\", \"2034\", \"2035\"],\n+ \"midpoints\": [\"2025-07-02\", \"2026-07-02\", \"2027-07-02\", \"2028-07-02\", \"2029-07-02\", \"2030-07-02\", \"2031-07-02\", \"2032-07-02\", \"2033-07-02\", \"2034-07-02\", \"2035-07-02\"]\n+ }\n+\n+ Question: When will artificial general intelligence be achieved? (min: 2025, max: 2035)\n+ {\n+ \"answers\": [\"2025\", \"2026\", \"2027\", \"2028\", \"2029\", \"2030\", \"2031\", \"2032\", \"2033\", \"2034\", \"2035\"],\n+ \"midpoints\": [\"2025-07-02\", \"2026-07-02\", \"2027-07-02\", \"2028-07-02\", \"2029-07-02\", \"2030-07-02\", \"2031-07-02\", \"2032-07-02\", \"2033-07-02\", \"2034-07-02\", \"2035-07-02\"]\n+ }\n `\n \n-const giveTimeExample = () =>\n- `The current time is ${new Date().toLocaleDateString('en-US', {\n- year: 'numeric',\n- month: 'long',\n- day: 'numeric',\n- })}`\n+const thresholdExamples = `\n+EXAMPLES:\n+ Question: If convicted, when will SBF start serving time? (min: 2025, max: 2028)\n+ {\n+ \"answers\": [\"Before 2026\", \"Before 2027\", \"Before 2028\", \"Before 2029\"],\n+ \"midpoints\": [\"2025-07-02\", \"2026-07-02\", \"2027-07-02\", \"2028-07-02\"]\n+ }\n \n+ Question: When will the next US recession begin? (min: Q1 2025, max: Q4 2026)\n+ {\n+ \"answers\": [\"Before Q1 2025\", \"Before Q2 2025\", \"Before Q3 2025\", \"Before Q4 2025\", \"Before Q1 2026\", \"Before Q2 2026\", \"Before Q3 2026\", \"Before Q4 2026\"],\n+ \"midpoints\": [\"2025-02-14\", \"2025-05-14\", \"2025-08-14\", \"2025-11-14\", \"2026-02-14\", \"2026-05-14\", \"2026-08-14\", \"2026-11-14\"]\n+ }\n+\n+ Question: When will SpaceX complete its first crewed Mars mission? (min: 2026, max: 2035)\n+ {\n+ \"answers\": [\"Before 2027\", \"Before 2028\", \"Before 2029\", \"Before 2030\", \"Before 2031\", \"Before 2032\", \"Before 2033\", \"Before 2034\", \"Before 2035\"],\n+ \"midpoints\": [\"2026-07-02\", \"2027-07-02\", \"2028-07-02\", \"2029-07-02\", \"2030-07-02\", \"2031-07-02\", \"2032-07-02\", \"2033-07-02\", \"2034-07-02\", \"2035-07-02\"]\n+ }\n+\n+ Question: When will artificial general intelligence be achieved? (min: 2025, max: 2035)\n+ {\n+ \"answers\": [\"Before 2026\", \"Before 2027\", \"Before 2028\", \"Before 2029\", \"Before 2030\", \"Before 2031\", \"Before 2032\", \"Before 2033\", \"Before 2034\", \"Before 2035\"],\n+ \"midpoints\": [\"2026-07-02\", \"2027-07-02\", \"2028-07-02\", \"2029-07-02\", \"2030-07-02\", \"2031-07-02\", \"2032-07-02\", \"2033-07-02\", \"2034-07-02\", \"2035-07-02\"]\n+ }\n+`\n+\n+const giveTimeExample = () => `The current time is ${new Date().toISOString()}`\n+\n const userPrompt = (\n question: string,\n min: string,\n max: string,\n- unit: string,\n description?: string\n ) => {\n return `Question: ${question} ${\n description && description !== '<p></p>'\n ? `\\nDescription: ${description}`\n : ''\n- }\\nStart: ${min} End: ${max} Unit: ${unit} ${giveTimeExample()}`\n+ }\\nStart: ${min} End: ${max} ${giveTimeExample()}`\n }\n \n export const generateAIDateRanges: APIHandler<'generate-ai-date-ranges'> =\n rateLimitByUser(\n async (props, auth) => {\n- const { question, min, max, description, unit } = props\n+ const { question, min, max, description } = props\n \n- const prompt = userPrompt(question, min, max, unit, description)\n+ const prompt = userPrompt(question, min, max, description)\n \n- const bucketSystemPrompt = baseDateSystemPrompt()\n+ // Prepare system prompts\n+ const bucketSystemPrompt = baseDateSystemPrompt('buckets')\n+ const thresholdSystemPrompt = baseDateSystemPrompt('thresholds')\n \n- const buckets = await promptClaudeParsingJson<RangeResponse>(prompt, {\n- model: models.sonnet,\n- system: bucketSystemPrompt,\n- })\n+ // Generate both bucket and threshold ranges in parallel\n+ const [buckets, thresholds] = await Promise.all([\n+ promptClaudeParsingJson<DateRangeResponse>(prompt, {\n+ model: models.sonnet,\n+ system: bucketSystemPrompt,\n+ }),\n+ promptClaudeParsingJson<DateRangeResponse>(prompt, {\n+ model: models.sonnet,\n+ system: thresholdSystemPrompt,\n+ }),\n+ ])\n \n- assertMidpointsAreUnique(buckets.midpoints)\n- assertMidpointsAreAscending(buckets.midpoints)\n- buckets.midpoints = convertTimeMidpointsToDates(unit, buckets.midpoints)\n- log('buckets', buckets)\n+ // Process bucket results\n+ const convertedBuckets = {\n+ ...buckets,\n+ midpoints: convertDateMidpointsToTimes(buckets.midpoints),\n+ }\n+ assertMidpointsAreUnique(convertedBuckets.midpoints)\n+ assertMidpointsAreAscending(convertedBuckets.midpoints)\n+ log('buckets', convertedBuckets)\n+\n+ // Process threshold results\n+ const convertedThresholds = {\n+ ...thresholds,\n+ midpoints: convertDateMidpointsToTimes(thresholds.midpoints),\n+ }\n+ assertMidpointsAreUnique(convertedThresholds.midpoints)\n+ assertMidpointsAreAscending(convertedThresholds.midpoints)\n+ log('thresholds', convertedThresholds)\n+\n track(auth.uid, 'generate-ai-date-ranges', {\n question,\n })\n \n return {\n- buckets,\n+ buckets: convertedBuckets,\n+ thresholds: convertedThresholds,\n }\n },\n { maxCalls: 60, windowMs: HOUR_MS }\n )\n \n export const regenerateDateMidpoints: APIHandler<'regenerate-date-midpoints'> =\n rateLimitByUser(\n async (props, auth) => {\n- const { question, description, answers, min, max, unit } = props\n+ const { question, description, answers, min, max, tab } = props\n \n const prompt = `${userPrompt(\n question,\n min,\n max,\n- unit,\n description\n )}\\nRanges: ${answers.join(', ')}.\n- Generate appropriate midpoints for each range.\n+ Generate appropriate midpoint dates for each range.\n RULES:\n- - The midpoints should be numbers that represent the expected value for each range.\n- - If the range is a log scale, use the geometric mean for the midpoint.\n+ - The midpoints should be the exact middle date of the range.\n \n- ${bucketExamples}\n+ ${tab === 'buckets' ? bucketExamples : thresholdExamples}\n \n- Return ONLY an array of midpoint numbers, one for each range, without any other text or formatting.`\n+ Return ONLY an array of midpoint dates, one for each range, without any other text or formatting.`\n \n- const result = await promptClaudeParsingJson<number[]>(prompt, {\n+ const result = await promptClaudeParsingJson<string[]>(prompt, {\n model: models.sonnet,\n })\n log('claudeResponse', result)\n \n- track(auth.uid, 'regenerate-numeric-midpoints', {\n+ track(auth.uid, 'regenerate-date-midpoints', {\n answers,\n })\n \n- assertMidpointsAreUnique(result)\n- assertMidpointsAreAscending(result)\n+ const convertedMidpoints = convertDateMidpointsToTimes(result)\n \n- return { midpoints: convertTimeMidpointsToDates(unit, result) }\n+ assertMidpointsAreUnique(convertedMidpoints)\n+ assertMidpointsAreAscending(convertedMidpoints)\n+\n+ return { midpoints: convertedMidpoints }\n },\n { maxCalls: 60, windowMs: HOUR_MS }\n )\n \n-const convertTimeMidpointsToDates = (unit: string, midpoints: number[]) => {\n- const now = new Date()\n- const currentYear = now.getFullYear()\n- const currentMonth = now.getMonth()\n- const currentDay = now.getDate()\n-\n+const convertDateMidpointsToTimes = (midpoints: string[]) => {\n return midpoints.map((midpoint) => {\n- if (unit.trim().toLowerCase() === 'year') {\n- // For years, calculate milliseconds from now to the target year point\n- const targetYear = Math.floor(midpoint)\n- const fraction = midpoint - targetYear\n-\n- // Create date for the target year (same month/day as today)\n- const targetDate = new Date(targetYear, currentMonth, currentDay)\n-\n- // Add the fraction of the year in milliseconds if there is one\n- if (fraction > 0) {\n- const millisecondsInYear = 365.25 * 24 * 60 * 60 * 1000\n- const additionalMilliseconds = Math.floor(fraction * millisecondsInYear)\n- targetDate.setTime(targetDate.getTime() + additionalMilliseconds)\n- }\n-\n- // Return milliseconds from now to target date\n- // If target is in the past, we'll return a small positive value (1 day)\n- const msDiff = targetDate.getTime() - now.getTime()\n- return msDiff > 0 ? msDiff : 24 * 60 * 60 * 1000 // Return at least 1 day if in past\n- } else if (unit.trim().toLowerCase() === 'month') {\n- // For months, add the number of months to current date\n- const targetDate = new Date(\n- currentYear,\n- currentMonth + midpoint,\n- currentDay\n- )\n- const msDiff = targetDate.getTime() - now.getTime()\n- return msDiff > 0 ? msDiff : 24 * 60 * 60 * 1000\n- } else if (unit.trim().toLowerCase() === 'day') {\n- // For days, add the number of days to current date\n- const targetDate = new Date(\n- now.getTime() + midpoint * 24 * 60 * 60 * 1000\n- )\n- const msDiff = targetDate.getTime() - now.getTime()\n- return msDiff > 0 ? msDiff : 24 * 60 * 60 * 1000\n- } else {\n- // Fallback for any other time unit\n- return midpoint\n- }\n+ const date = new Date(midpoint)\n+ return date.valueOf()\n })\n }\n"
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
"path": "backend/api/src/get-related-markets.ts",
|
|
224
|
-
"status": "modified",
|
|
225
|
-
"diff": "Index: backend/api/src/get-related-markets.ts\n===================================================================\n--- backend/api/src/get-related-markets.ts\td70bc7a (parent)\n+++ backend/api/src/get-related-markets.ts\te96bced (commit)\n@@ -60,9 +60,12 @@\n export const cleanContractForStaticProps = (c: Contract) =>\n ({\n ...c,\n description: '',\n- answers: c.outcomeType === 'MULTI_NUMERIC' ? c.answers : [],\n+ answers:\n+ c.outcomeType === 'MULTI_NUMERIC' || c.outcomeType === 'DATE'\n+ ? c.answers\n+ : [],\n groupSlugs: [],\n } as Contract)\n \n const refreshedRelatedMarkets = async (\n"
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
"path": "backend/api/src/place-bet.ts",
|
|
229
|
-
"status": "modified",
|
|
230
|
-
"diff": "Index: backend/api/src/place-bet.ts\n===================================================================\n--- backend/api/src/place-bet.ts\td70bc7a (parent)\n+++ backend/api/src/place-bet.ts\te96bced (commit)\n@@ -238,14 +238,9 @@\n ) => {\n const { amount, expiresMillisAfter } = body\n const { outcomeType, mechanism } = contract\n \n- if (\n- (outcomeType == 'BINARY' ||\n- outcomeType === 'PSEUDO_NUMERIC' ||\n- outcomeType === 'STONK') &&\n- mechanism == 'cpmm-1'\n- ) {\n+ if (mechanism == 'cpmm-1') {\n // eslint-disable-next-line prefer-const\n let { outcome, limitProb, expiresAt } = body\n if (expiresAt && expiresAt < Date.now())\n throw new APIError(400, 'Bet cannot expire in the past.')\n@@ -273,14 +268,9 @@\n balanceByUserId,\n expiresAt,\n expiresMillisAfter\n )\n- } else if (\n- (outcomeType === 'MULTIPLE_CHOICE' ||\n- outcomeType === 'NUMBER' ||\n- outcomeType === 'MULTI_NUMERIC') &&\n- mechanism == 'cpmm-multi-1'\n- ) {\n+ } else if (mechanism == 'cpmm-multi-1') {\n const { shouldAnswersSumToOne } = contract\n if (!body.answerId || !answers) {\n throw new APIError(400, 'answerId must be specified for multi bets')\n }\n"
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
"path": "backend/api/src/resolve-market.ts",
|
|
234
|
-
"status": "modified",
|
|
235
|
-
"diff": "Index: backend/api/src/resolve-market.ts\n===================================================================\n--- backend/api/src/resolve-market.ts\td70bc7a (parent)\n+++ backend/api/src/resolve-market.ts\te96bced (commit)\n@@ -115,10 +115,9 @@\n contract: Contract,\n props: ValidatedAPIParams<'market/:contractId/resolve'>\n ) {\n const { outcomeType } = contract\n- const isMultiChoice =\n- outcomeType === 'MULTIPLE_CHOICE' || outcomeType === 'MULTI_NUMERIC'\n+ const isMultiChoice = contract.mechanism === 'cpmm-multi-1'\n \n if (\n outcomeType === 'BINARY' ||\n (isMultiChoice && !contract.shouldAnswersSumToOne)\n"
|
|
236
|
-
},
|
|
237
|
-
{
|
|
238
|
-
"path": "backend/api/src/update-market.ts",
|
|
239
|
-
"status": "modified",
|
|
240
|
-
"diff": "Index: backend/api/src/update-market.ts\n===================================================================\n--- backend/api/src/update-market.ts\td70bc7a (parent)\n+++ backend/api/src/update-market.ts\te96bced (commit)\n@@ -27,8 +27,9 @@\n closeTime,\n sort,\n question,\n coverImageUrl,\n+ display,\n \n description: raw,\n descriptionHtml: html,\n descriptionMarkdown: markdown,\n@@ -65,8 +66,9 @@\n unlistedById: visibility === 'unlisted' ? auth.uid : undefined,\n addAnswersMode,\n sort,\n description,\n+ display,\n lastUpdatedTime: Date.now(),\n })\n await updateContract(pg, contractId, {\n ...update,\n"
|
|
241
|
-
},
|
|
242
|
-
{
|
|
243
|
-
"path": "backend/shared/src/create-notification.ts",
|
|
244
|
-
"status": "modified",
|
|
245
|
-
"diff": "Index: backend/shared/src/create-notification.ts\n===================================================================\n--- backend/shared/src/create-notification.ts\td70bc7a (parent)\n+++ backend/shared/src/create-notification.ts\te96bced (commit)\n@@ -20,9 +20,9 @@\n MANIFOLD_USER_USERNAME,\n PrivateUser,\n User,\n } from 'common/user'\n-import { Contract, renderResolution } from 'common/contract'\n+import { Contract, MarketContract, renderResolution } from 'common/contract'\n import { getContract, getPrivateUser, getUser, isProd, log } from 'shared/utils'\n import { ContractComment } from 'common/comment'\n import {\n forEach,\n@@ -1214,9 +1214,9 @@\n )\n }\n \n export const createContractResolvedNotifications = async (\n- contract: Contract,\n+ contract: MarketContract,\n resolver: User,\n creator: User,\n outcome: string,\n probabilityInt: number | undefined,\n@@ -1235,9 +1235,10 @@\n let resolutionText = outcome ?? contract.question\n const { token } = contract\n const isMultiChoice =\n contract.outcomeType === 'MULTIPLE_CHOICE' ||\n- contract.outcomeType === 'MULTI_NUMERIC'\n+ contract.outcomeType === 'MULTI_NUMERIC' ||\n+ contract.outcomeType === 'DATE'\n const isIndependentMulti = isMultiChoice && !contract.shouldAnswersSumToOne\n \n if (isIndependentMulti) {\n const answer = contract.answers.find((answer) => answer.id === answerId)\n"
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
"path": "backend/shared/src/resolve-market-helpers.ts",
|
|
249
|
-
"status": "modified",
|
|
250
|
-
"diff": "Index: backend/shared/src/resolve-market-helpers.ts\n===================================================================\n--- backend/shared/src/resolve-market-helpers.ts\td70bc7a (parent)\n+++ backend/shared/src/resolve-market-helpers.ts\te96bced (commit)\n@@ -210,9 +210,9 @@\n \n const resolvedContract = {\n ...unresolvedContract,\n ...updatedContractAttrs,\n- } as Contract\n+ } as MarketContract\n \n // handle exploit where users can get negative payouts\n const negPayoutThreshold =\n resolvedContract.uniqueBettorCount < 100 ? 0 : -1000\n"
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
"path": "common/src/api/market-search-types.ts",
|
|
254
|
-
"status": "modified",
|
|
255
|
-
"diff": "Index: common/src/api/market-search-types.ts\n===================================================================\n--- common/src/api/market-search-types.ts\td70bc7a (parent)\n+++ common/src/api/market-search-types.ts\te96bced (commit)\n@@ -48,8 +48,9 @@\n z.literal('STONK'),\n z.literal('POLL'),\n z.literal('NUMBER'),\n z.literal('MULTI_NUMERIC'),\n+ z.literal('DATE'),\n ])\n .default('ALL'),\n offset: z.coerce.number().gte(0).default(0),\n limit: z.coerce.number().gt(0).lte(1000).default(100),\n"
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
"path": "common/src/api/market-types.ts",
|
|
259
|
-
"status": "modified",
|
|
260
|
-
"diff": "Index: common/src/api/market-types.ts\n===================================================================\n--- common/src/api/market-types.ts\td70bc7a (parent)\n+++ common/src/api/market-types.ts\te96bced (commit)\n@@ -301,8 +301,17 @@\n addAnswersMode: z.enum(['DISABLED']).default('DISABLED'),\n unit: z.string(),\n })\n \n+export const createMultiDateSchema = z.object({\n+ outcomeType: z.enum(['DATE']),\n+ answers: z.array(z.string().trim().min(1)).max(MAX_MULTI_NUMERIC_ANSWERS),\n+ midpoints: z.array(z.number().safe()).max(MAX_MULTI_NUMERIC_ANSWERS),\n+ shouldAnswersSumToOne: z.boolean(),\n+ addAnswersMode: z.enum(['DISABLED']).default('DISABLED'),\n+ timezone: z.string(),\n+})\n+\n export const createBountySchema = z.object({\n outcomeType: z.enum(['BOUNTIED_QUESTION']),\n totalBounty: z.number().min(MINIMUM_BOUNTY),\n isAutoBounty: z.boolean().optional(),\n@@ -348,8 +357,9 @@\n createPollSchema,\n createBinarySchema,\n createNumberSchema,\n createMultiNumericSchema,\n+ createMultiDateSchema,\n ])\n )\n \n export const updateMarketProps = z\n@@ -364,8 +374,9 @@\n description: z.string().optional(),\n descriptionHtml: z.string().optional(),\n descriptionMarkdown: z.string().optional(),\n descriptionJson: z.string().optional(),\n+ display: z.enum(['clock', 'default']).optional(),\n })\n .strict()\n \n // resolve market\n"
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
"path": "common/src/api/schema.ts",
|
|
264
|
-
"status": "modified",
|
|
265
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\td70bc7a (parent)\n+++ common/src/api/schema.ts\te96bced (commit)\n@@ -2231,16 +2231,16 @@\n visibility: 'public',\n authed: true,\n returns: {} as {\n buckets: { answers: string[]; midpoints: number[] }\n+ thresholds: { answers: string[]; midpoints: number[] }\n },\n props: z\n .object({\n question: z.string(),\n min: z.string(),\n max: z.string(),\n description: z.string().optional(),\n- unit: z.string(),\n })\n .strict(),\n },\n 'regenerate-numeric-midpoints': {\n@@ -2271,9 +2271,9 @@\n question: z.string(),\n answers: z.array(z.string()),\n min: z.string(),\n max: z.string(),\n- unit: z.string(),\n+ tab: z.enum(['thresholds', 'buckets']),\n })\n .strict(),\n },\n } as const)\n"
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
"path": "common/src/calculate-fixed-payouts.ts",
|
|
269
|
-
"status": "modified",
|
|
270
|
-
"diff": "Index: common/src/calculate-fixed-payouts.ts\n===================================================================\n--- common/src/calculate-fixed-payouts.ts\td70bc7a (parent)\n+++ common/src/calculate-fixed-payouts.ts\te96bced (commit)\n@@ -1,13 +1,8 @@\n import { sum } from 'lodash'\n import { Bet } from './bet'\n import { getProbability } from './calculate'\n-import {\n- CPMMContract,\n- CPMMMultiContract,\n- CPMMNumericContract,\n- MultiNumeric,\n-} from './contract'\n+import { CPMMContract, MultiContract } from './contract'\n \n export function calculateFixedPayout(\n contract: CPMMContract,\n bet: Bet,\n@@ -41,12 +36,9 @@\n const betP = outcome === 'YES' ? prob : 1 - prob\n return betP * shares\n }\n \n-function calculateBetPayoutMulti(\n- contract: CPMMMultiContract | CPMMNumericContract | MultiNumeric,\n- bet: Bet\n-) {\n+function calculateBetPayoutMulti(contract: MultiContract, bet: Bet) {\n let prob = 0\n const { answerId } = bet\n if (answerId) {\n const { answers, resolutions } = contract\n@@ -68,9 +60,9 @@\n return betP * shares\n }\n \n export function calculateFixedPayoutMulti(\n- contract: CPMMMultiContract | CPMMNumericContract | MultiNumeric,\n+ contract: MultiContract,\n bet: Bet,\n outcome: string\n ) {\n if (outcome === 'CANCEL') return calculateFixedCancelPayout(bet)\n"
|
|
271
|
-
},
|
|
272
|
-
{
|
|
273
|
-
"path": "common/src/chart.ts",
|
|
274
|
-
"status": "modified",
|
|
275
|
-
"diff": "Index: common/src/chart.ts\n===================================================================\n--- common/src/chart.ts\td70bc7a (parent)\n+++ common/src/chart.ts\te96bced (commit)\n@@ -4,9 +4,15 @@\n \n export type Point<X, Y, T = unknown> = { x: X; y: Y; obj?: T }\n export type HistoryPoint<T = unknown> = Point<number, number, T>\n export type DistributionPoint<T = unknown> = Point<number, number, T>\n-export type ValueKind = 'Ṁ' | 'percent' | 'amount' | 'spice' | 'sweepies'\n+export type ValueKind =\n+ | 'Ṁ'\n+ | 'percent'\n+ | 'amount'\n+ | 'spice'\n+ | 'sweepies'\n+ | 'date'\n \n export type MultiPoints = { [answerId: string]: HistoryPoint<never>[] }\n \n /** answer -> base 64 encoded */\n"
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
"path": "common/src/contract-params.ts",
|
|
279
|
-
"status": "modified",
|
|
280
|
-
"diff": "Index: common/src/contract-params.ts\n===================================================================\n--- common/src/contract-params.ts\td70bc7a (parent)\n+++ common/src/contract-params.ts\te96bced (commit)\n@@ -237,9 +237,10 @@\n export const shouldHideGraph = (contract: Contract) => {\n if (contract.mechanism !== 'cpmm-multi-1') return false\n if (\n contract.outcomeType == 'NUMBER' ||\n- contract.outcomeType == 'MULTI_NUMERIC'\n+ contract.outcomeType == 'MULTI_NUMERIC' ||\n+ contract.outcomeType == 'DATE'\n )\n return false\n const defaultSort = getDefaultSort(contract)\n const sortedAnswers = sortAnswers(contract, contract.answers, defaultSort)\n"
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
"path": "common/src/contract-seo.ts",
|
|
284
|
-
"status": "modified",
|
|
285
|
-
"diff": "Index: common/src/contract-seo.ts\n===================================================================\n--- common/src/contract-seo.ts\td70bc7a (parent)\n+++ common/src/contract-seo.ts\te96bced (commit)\n@@ -5,8 +5,9 @@\n import { formatMoneyNumber, formatPercent } from './util/format'\n import { getFormattedNumberExpectedValue } from 'common/number'\n import { sortAnswers } from './answer'\n import { getFormattedExpectedValue } from './multi-numeric'\n+import { getFormattedExpectedDate } from './multi-date'\n \n export const getContractOGProps = (\n contract: Contract\n ): Omit<OgCardProps, 'points'> => {\n@@ -40,8 +41,10 @@\n outcomeType === 'NUMBER'\n ? getFormattedNumberExpectedValue(contract)\n : outcomeType === 'MULTI_NUMERIC'\n ? getFormattedExpectedValue(contract)\n+ : outcomeType === 'DATE'\n+ ? getFormattedExpectedDate(contract)\n : outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'STONK'\n ? getFormattedMappedValue(contract, getDisplayProbability(contract))\n : undefined\n \n"
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
"path": "common/src/contract.ts",
|
|
289
|
-
"status": "modified",
|
|
290
|
-
"diff": "Index: common/src/contract.ts\n===================================================================\n--- common/src/contract.ts\td70bc7a (parent)\n+++ common/src/contract.ts\te96bced (commit)\n@@ -43,9 +43,9 @@\n | (NonBet & BountiedQuestion)\n | (NonBet & Poll)\n | CPMMNumber\n | MultiNumeric\n-\n+ | MultiDate\n export type Contract<T extends AnyContractType = AnyContractType> = {\n id: string\n slug: string // auto-generated; must be unique\n \n@@ -130,14 +130,15 @@\n export type CPMMContract = Contract & CPMM\n export type CPMMMultiContract = Contract & CPMMMulti\n export type CPMMNumericContract = Contract & CPMMNumber\n export type MultiNumericContract = Contract & MultiNumeric\n+export type MultiDateContract = Contract & MultiDate\n export type MarketContract =\n | CPMMContract\n | CPMMMultiContract\n | CPMMNumericContract\n | MultiNumericContract\n-\n+ | MultiDateContract\n export type BinaryContract = Contract & Binary\n export type PseudoNumericContract = Contract & PseudoNumeric\n export type QuadraticFundingContract = Contract & QuadraticFunding\n export type StonkContract = Contract & Stonk\n@@ -199,8 +200,9 @@\n export const isSpecialLoveContract = (contract: Contract) =>\n contract.mechanism === 'cpmm-multi-1' &&\n contract.outcomeType !== 'NUMBER' &&\n contract.outcomeType !== 'MULTI_NUMERIC' &&\n+ contract.outcomeType !== 'DATE' &&\n contract.specialLiquidityPerAnswer\n \n export type CPMMNumber = {\n mechanism: 'cpmm-multi-1'\n@@ -278,8 +280,14 @@\n resolution?: string | 'CHOOSE_MULTIPLE' | 'CANCEL'\n sort?: SortType\n }\n \n+export type MultiDate = Omit<MultiNumeric, 'outcomeType' | 'unit'> & {\n+ outcomeType: 'DATE'\n+ timezone: string\n+ display?: 'clock'\n+}\n+\n export type Stonk = {\n outcomeType: 'STONK'\n initialProbability: number\n }\n@@ -310,8 +318,9 @@\n export type MultiContract =\n | CPMMMultiContract\n | CPMMNumericContract\n | MultiNumericContract\n+ | MultiDateContract\n \n type AnyOutcomeType =\n | Binary\n | QuadraticFunding\n@@ -321,8 +330,10 @@\n | Number\n | CPMMMulti\n | PseudoNumeric\n | MultiNumeric\n+ | MultiDate\n+\n export type OutcomeType = AnyOutcomeType['outcomeType']\n export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'\n export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const\n export const CREATEABLE_OUTCOME_TYPES = [\n@@ -333,8 +344,9 @@\n 'BOUNTIED_QUESTION',\n 'POLL',\n 'NUMBER',\n 'MULTI_NUMERIC',\n+ 'DATE',\n ] as const\n \n export const CREATEABLE_NON_PREDICTIVE_OUTCOME_TYPES = [\n 'POLL',\n@@ -416,9 +428,9 @@\n export const MAX_QUESTION_LENGTH = 120\n export const MAX_DESCRIPTION_LENGTH = 16000\n \n export const CPMM_MIN_POOL_QTY = 0.01\n-export const MULTI_NUMERIC_BUCKETS_MAX = 50\n+export const NUMBER_BUCKETS_MAX = 50\n export const NUMBER_CREATION_ENABLED = false\n \n export type Visibility = 'public' | 'unlisted'\n export const VISIBILITIES = ['public', 'unlisted'] as const\n"
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
"path": "common/src/economy.ts",
|
|
294
|
-
"status": "modified",
|
|
295
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\td70bc7a (parent)\n+++ common/src/economy.ts\te96bced (commit)\n@@ -12,9 +12,13 @@\n if (outcomeType === 'POLL') {\n return 10\n }\n \n- if (outcomeType === 'MULTIPLE_CHOICE' || outcomeType === 'MULTI_NUMERIC') {\n+ if (\n+ outcomeType === 'MULTIPLE_CHOICE' ||\n+ outcomeType === 'MULTI_NUMERIC' ||\n+ outcomeType === 'DATE'\n+ ) {\n const tierIndex =\n liquidityTiers.findIndex((tier) => tier >= liquidityTier) ??\n liquidityTiers[liquidityTiers.length - 1]\n return numAnswers\n"
|
|
296
|
-
},
|
|
297
|
-
{
|
|
298
|
-
"path": "common/src/multi-date.ts",
|
|
299
|
-
"status": "added",
|
|
300
|
-
"diff": "Index: common/src/multi-date.ts\n===================================================================\n--- common/src/multi-date.ts\td70bc7a (parent)\n+++ common/src/multi-date.ts\te96bced (commit)\n@@ -0,0 +1,103 @@\n+import { sum } from 'lodash'\n+import { getAnswerProbability } from './calculate'\n+import { filterDefined } from './util/array'\n+import { getInitialAnswerProbability } from './calculate'\n+import { MultiDateContract } from './contract'\n+import { getMinMax } from './multi-numeric'\n+export const MAX_MULTI_NUMERIC_ANSWERS = 12\n+\n+export function getExpectedDate(\n+ contract: MultiDateContract,\n+ initialOnly?: boolean\n+) {\n+ const { answers, shouldAnswersSumToOne } = contract\n+\n+ const answerProbabilities = filterDefined(\n+ answers.map((a) =>\n+ initialOnly\n+ ? getInitialAnswerProbability(contract, a)\n+ : getAnswerProbability(contract, a.id)\n+ )\n+ )\n+ const answerMidpoints = answers.map((a) => a.midpoint!)\n+\n+ if (shouldAnswersSumToOne) {\n+ // For bucket-style answers, simple weighted average\n+ return sum(answerProbabilities.map((p, i) => p * answerMidpoints[i]))\n+ } else {\n+ // For threshold-style answers, calculate discrete probabilities\n+ // First, get the discrete probabilities for each time period\n+ const discreteProbs = []\n+\n+ // First period: probability of the first threshold\n+ discreteProbs.push(answerProbabilities[0])\n+\n+ // Middle periods: difference between adjacent thresholds\n+ for (let i = 1; i < answerProbabilities.length; i++) {\n+ discreteProbs.push(answerProbabilities[i] - answerProbabilities[i - 1])\n+ }\n+\n+ // Final period: remaining probability\n+ const afterLastProb =\n+ 1 - answerProbabilities[answerProbabilities.length - 1]\n+\n+ // Calculate expected value using discrete probabilities\n+ let expectedValue = 0\n+\n+ // Sum probability-weighted time periods\n+ for (let i = 0; i < answerProbabilities.length; i++) {\n+ expectedValue += answerMidpoints[i] * discreteProbs[i]\n+ }\n+\n+ // Handle the \"after last threshold\" case by using the last midpoint plus one time period\n+ if (afterLastProb > 0 && answerMidpoints.length > 0) {\n+ const lastMidpoint = answerMidpoints[answerMidpoints.length - 1]\n+ const secondLastMidpoint = answerMidpoints[answerMidpoints.length - 2]\n+ const timePeriod = lastMidpoint - secondLastMidpoint\n+ const beyondLastMidpoint = lastMidpoint + timePeriod\n+ expectedValue += beyondLastMidpoint * afterLastProb\n+ }\n+\n+ return expectedValue\n+ }\n+}\n+\n+export function formatExpectedDate(\n+ value: number,\n+ contract: MultiDateContract,\n+ contractPageView = true\n+) {\n+ const { answers } = contract\n+ const { min, max } = getMinMax(contract)\n+ // There are a few NaN & undefined values on dev\n+ if (isNaN(value) || min === undefined || max === undefined || max === min)\n+ return 'N/A'\n+ if (answers.length == 0) return '' // probably from static props\n+\n+ const formatter = new Intl.DateTimeFormat(\n+ 'en-US',\n+ contractPageView\n+ ? {\n+ year: 'numeric',\n+ month: 'long',\n+ day: 'numeric',\n+ }\n+ : {\n+ month: 'numeric',\n+ day: 'numeric',\n+ year: '2-digit',\n+ }\n+ )\n+ return formatter.format(value)\n+}\n+\n+export function getFormattedExpectedDate(\n+ contract: MultiDateContract,\n+ contractPageView = true\n+) {\n+ return formatExpectedDate(\n+ getExpectedDate(contract),\n+ contract,\n+ contractPageView\n+ )\n+}\n"
|
|
301
|
-
},
|
|
302
|
-
{
|
|
303
|
-
"path": "common/src/multi-numeric.ts",
|
|
304
|
-
"status": "modified",
|
|
305
|
-
"diff": "Index: common/src/multi-numeric.ts\n===================================================================\n--- common/src/multi-numeric.ts\td70bc7a (parent)\n+++ common/src/multi-numeric.ts\te96bced (commit)\n@@ -1,9 +1,9 @@\n import { sum } from 'lodash'\n import { getAnswerProbability } from './calculate'\n import { filterDefined } from './util/array'\n import { getInitialAnswerProbability } from './calculate'\n-import { MultiNumericContract } from './contract'\n+import { MultiDateContract, MultiNumericContract } from './contract'\n import { formatLargeNumber } from './util/format'\n export const MAX_MULTI_NUMERIC_ANSWERS = 12\n \n export function getExpectedValue(\n@@ -31,9 +31,11 @@\n )\n )\n }\n \n-export const getMinMax = (contract: MultiNumericContract) => {\n+export const getMinMax = (\n+ contract: MultiNumericContract | MultiDateContract\n+) => {\n const { answers } = contract\n const min = Math.min(...answers.map((a) => a.midpoint!))\n const max = Math.max(...answers.map((a) => a.midpoint!))\n return { min, max }\n"
|
|
306
|
-
},
|
|
307
|
-
{
|
|
308
|
-
"path": "common/src/new-contract.ts",
|
|
309
|
-
"status": "modified",
|
|
310
|
-
"diff": "Index: common/src/new-contract.ts\n===================================================================\n--- common/src/new-contract.ts\td70bc7a (parent)\n+++ common/src/new-contract.ts\te96bced (commit)\n@@ -8,8 +8,9 @@\n CPMMMulti,\n CPMMNumber,\n CREATEABLE_OUTCOME_TYPES,\n Contract,\n+ MultiDate,\n MultiNumeric,\n NonBet,\n Poll,\n PseudoNumeric,\n@@ -65,8 +66,9 @@\n \n // Multi-numeric\n unit: string | undefined\n midpoints: number[] | undefined\n+ timezone: string | undefined\n }\n ) {\n const {\n id,\n@@ -97,8 +99,9 @@\n takerAPIOrdersDisabled,\n siblingContractId,\n unit,\n midpoints,\n+ timezone,\n } = props\n const createdTime = Date.now()\n \n const propsByOutcomeType = {\n@@ -129,8 +132,18 @@\n ante,\n unit ?? '',\n shouldAnswersSumToOne ?? true\n ),\n+ DATE: () =>\n+ getDateProps(\n+ id,\n+ creator.id,\n+ answers,\n+ midpoints ?? [],\n+ ante,\n+ shouldAnswersSumToOne ?? true,\n+ timezone ?? ''\n+ ),\n }[outcomeType]()\n \n const contract: Contract = removeUndefinedProps({\n id,\n@@ -364,9 +377,40 @@\n }\n \n return system\n }\n+const getDateProps = (\n+ contractId: string,\n+ userId: string,\n+ answers: string[],\n+ midpoints: number[],\n+ ante: number,\n+ shouldAnswersSumToOne: boolean,\n+ timezone: string\n+) => {\n+ const answerObjects = createAnswers(\n+ contractId,\n+ userId,\n+ 'DISABLED',\n+ shouldAnswersSumToOne,\n+ ante,\n+ answers,\n+ { midpoints }\n+ )\n+ const system: MultiDate = {\n+ mechanism: 'cpmm-multi-1',\n+ outcomeType: 'DATE',\n+ shouldAnswersSumToOne,\n+ addAnswersMode: 'DISABLED',\n+ answers: answerObjects,\n+ totalLiquidity: ante,\n+ subsidyPool: 0,\n+ timezone,\n+ }\n \n+ return system\n+}\n+\n function createAnswers(\n contractId: string,\n userId: string,\n addAnswersMode: add_answers_mode,\n"
|
|
311
|
-
},
|
|
312
|
-
{
|
|
313
|
-
"path": "common/src/number.ts",
|
|
314
|
-
"status": "modified",
|
|
315
|
-
"diff": "Index: common/src/number.ts\n===================================================================\n--- common/src/number.ts\td70bc7a (parent)\n+++ common/src/number.ts\te96bced (commit)\n@@ -1,5 +1,5 @@\n-import { CPMMNumericContract, MULTI_NUMERIC_BUCKETS_MAX } from 'common/contract'\n+import { CPMMNumericContract, NUMBER_BUCKETS_MAX } from 'common/contract'\n import { filterDefined } from 'common/util/array'\n import { find, findLast, mean, sum } from 'lodash'\n import {\n getAnswerProbability,\n@@ -46,9 +46,9 @@\n min: number,\n max: number,\n precision: number\n ) => {\n- const highestPrecision = (max - min) / MULTI_NUMERIC_BUCKETS_MAX\n+ const highestPrecision = (max - min) / NUMBER_BUCKETS_MAX\n const lowestPrecision = (max - min) / 2\n const hasPrecisionError =\n !precision || precision < highestPrecision || precision > lowestPrecision\n if (hasPrecisionError) return []\n"
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
"path": "web/components/answers/answers-panel.tsx",
|
|
319
|
-
"status": "modified",
|
|
320
|
-
"diff": "Index: web/components/answers/answers-panel.tsx\n===================================================================\n--- web/components/answers/answers-panel.tsx\td70bc7a (parent)\n+++ web/components/answers/answers-panel.tsx\te96bced (commit)\n@@ -23,9 +23,8 @@\n import {\n CPMMMultiContract,\n Contract,\n MultiContract,\n- MultiNumericContract,\n contractPath,\n tradingAllowed,\n } from 'common/contract'\n import { isAdminId, isModId } from 'common/envs/constants'\n@@ -93,9 +92,9 @@\n const DEBOUNCE_DELAY = 100\n \n // full resorting, hover, clickiness, search and add\n export function AnswersPanel(props: {\n- contract: CPMMMultiContract | MultiNumericContract\n+ contract: MultiContract\n selectedAnswerIds: string[]\n sort: MultiSort\n setSort: (sort: MultiSort) => void\n query: string\n@@ -562,9 +561,9 @@\n )\n }\n \n export function Answer(props: {\n- contract: CPMMMultiContract | MultiNumericContract\n+ contract: MultiContract\n answer: Answer\n unfilledBets?: Array<LimitBet>\n color: string\n user: User | undefined | null\n"
|
|
321
|
-
},
|
|
322
|
-
{
|
|
323
|
-
"path": "web/components/buttons/duplicate-contract-button.tsx",
|
|
324
|
-
"status": "modified",
|
|
325
|
-
"diff": "Index: web/components/buttons/duplicate-contract-button.tsx\n===================================================================\n--- web/components/buttons/duplicate-contract-button.tsx\td70bc7a (parent)\n+++ web/components/buttons/duplicate-contract-button.tsx\te96bced (commit)\n@@ -58,17 +58,23 @@\n }\n \n if (\n contract.outcomeType === 'MULTIPLE_CHOICE' ||\n- contract.outcomeType === 'MULTI_NUMERIC'\n+ contract.outcomeType === 'MULTI_NUMERIC' ||\n+ contract.outcomeType === 'DATE'\n ) {\n params.answers = contract.answers\n .filter((a) => !a.isOther)\n .map((a) => a.text)\n }\n+ if (\n+ contract.outcomeType === 'MULTI_NUMERIC' ||\n+ contract.outcomeType === 'DATE'\n+ ) {\n+ params.midpoints = contract.answers.map((a) => a.midpoint!)\n+ }\n if (contract.outcomeType === 'MULTI_NUMERIC') {\n params.unit = contract.unit\n- params.midpoints = contract.answers.map((a) => a.midpoint!)\n }\n \n if (contract.mechanism === 'cpmm-multi-1') {\n params.addAnswersMode = contract.addAnswersMode\n"
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"path": "web/components/charts/contract/multi-date.tsx",
|
|
329
|
-
"status": "added",
|
|
330
|
-
"diff": "Index: web/components/charts/contract/multi-date.tsx\n===================================================================\n--- web/components/charts/contract/multi-date.tsx\td70bc7a (parent)\n+++ web/components/charts/contract/multi-date.tsx\te96bced (commit)\n@@ -0,0 +1,169 @@\n+import { useMemo } from 'react'\n+import { last, map, max, sum, zip, min, keyBy } from 'lodash'\n+import { scaleLinear, scaleTime } from 'd3-scale'\n+import { MultiDateContract } from 'common/contract'\n+import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'\n+import { getEndDate, getRightmostVisibleDate, ZoomParams } from '../helpers'\n+import { SingleValueHistoryChart } from '../generic-charts'\n+import { SingleContractChartTooltip } from './single-value'\n+\n+import { MultiPoints } from 'common/chart'\n+import { getMinMax } from 'common/src/multi-numeric'\n+import { formatExpectedDate, getExpectedDate } from 'common/multi-date'\n+import { getAnswerProbAtEveryBetTime } from 'common/contract-params'\n+\n+export const getDateBetPoints = (\n+ contract: MultiDateContract,\n+ bets: MultiPoints\n+) => {\n+ const { answers, shouldAnswersSumToOne } = contract\n+ const answersById = keyBy(answers, 'id')\n+ const filledInBetPoints = getAnswerProbAtEveryBetTime(bets, contract)\n+ if (shouldAnswersSumToOne) {\n+ // multiply the prob value by the value of the bucket\n+ const expectedValues = Object.entries(filledInBetPoints).map(\n+ ([answerId, pts]) =>\n+ pts.map((pt) => ({\n+ x: pt.x,\n+ y: pt.y * answersById[answerId].midpoint!,\n+ }))\n+ )\n+ return map(\n+ zip(...expectedValues) as Array<{ x: number; y: number }[]>,\n+ (group) => ({\n+ y: sum(group.map((pt) => pt.y)) ?? 0,\n+ x: group[0].x,\n+ })\n+ )\n+ } else {\n+ // Handle threshold-style expected value calculation\n+ // First, organize points by timestamp\n+ const pointsByTimestamp: Record<number, Record<string, number>> = {}\n+\n+ // Group all probabilities by timestamp\n+ Object.entries(filledInBetPoints).forEach(([answerId, pts]) => {\n+ pts.forEach((pt) => {\n+ const timestamp = pt.x\n+ if (!pointsByTimestamp[timestamp]) {\n+ pointsByTimestamp[timestamp] = {}\n+ }\n+ pointsByTimestamp[timestamp][answerId] = pt.y\n+ })\n+ })\n+\n+ // Calculate expected value at each timestamp\n+ return Object.entries(pointsByTimestamp)\n+ .map(([timestamp, probsByAnswer]) => {\n+ const answerIds = answers.map((a) => a.id)\n+ const probabilities = answerIds.map((id) => probsByAnswer[id] || 0)\n+\n+ // Calculate expected value using threshold method\n+ let expectedValue = 0\n+\n+ // For each threshold except the last one\n+ for (let i = 0; i < probabilities.length - 1; i++) {\n+ // Calculate the probability of being in this \"bucket\" between thresholds\n+ const bucketProbability =\n+ i === 0\n+ ? probabilities[i] // First threshold\n+ : probabilities[i] - probabilities[i - 1] // Difference between thresholds\n+\n+ // Add the contribution to the expected value\n+ expectedValue +=\n+ answersById[answerIds[i]].midpoint! * bucketProbability\n+ }\n+\n+ // Calculate the tail probability (events after the last threshold)\n+ const lastIndex = probabilities.length - 1\n+ const lastThresholdProb = probabilities[lastIndex]\n+ const prevThresholdProb =\n+ lastIndex > 0 ? probabilities[lastIndex - 1] : 0\n+\n+ // Probability of the last discrete bucket\n+ const lastBucketProb = lastThresholdProb - prevThresholdProb\n+ expectedValue +=\n+ answersById[answerIds[lastIndex]].midpoint! * lastBucketProb\n+\n+ // Handle probability beyond the last threshold (tail probability)\n+ const tailProb = 1 - lastThresholdProb\n+ if (tailProb > 0) {\n+ const lastMidpoint = answersById[answerIds[lastIndex]].midpoint!\n+ const prevMidpoint = answersById[answerIds[lastIndex - 1]].midpoint!\n+ const timePeriod = lastMidpoint - prevMidpoint\n+ const beyondLastMidpoint = lastMidpoint + timePeriod\n+\n+ // Add the contribution from events beyond the last threshold\n+ expectedValue += beyondLastMidpoint * tailProb\n+ }\n+\n+ return {\n+ x: parseInt(timestamp),\n+ y: expectedValue,\n+ }\n+ })\n+ .sort((a, b) => a.x - b.x) // Sort by timestamp\n+ }\n+}\n+\n+export const MultiDateContractChart = (props: {\n+ contract: MultiDateContract\n+ multiPoints: MultiPoints\n+ width: number\n+ height: number\n+ zoomParams?: ZoomParams\n+ showZoomer?: boolean\n+}) => {\n+ const { contract, width, multiPoints, height, zoomParams, showZoomer } = props\n+ const start = contract.createdTime\n+ const end = getEndDate(contract)\n+ const endP = getExpectedDate(contract)\n+ const stringifiedMultiPoints = JSON.stringify(multiPoints)\n+ const betPoints = useMemo(\n+ () => getDateBetPoints(contract, multiPoints),\n+ [stringifiedMultiPoints]\n+ )\n+ const now = useMemo(() => Date.now(), [stringifiedMultiPoints, endP])\n+\n+ const singlePointData = useMemo(\n+ () => [\n+ ...(betPoints || []),\n+ {\n+ x: end ?? now,\n+ y: endP,\n+ },\n+ ],\n+ [betPoints, end, endP, now]\n+ )\n+ const { min: answerMin, max: answerMax } = getMinMax(contract)\n+ const allYs = singlePointData.map((p) => p.y)\n+ const minY = min([...allYs, answerMin])!\n+ const maxY = max([...allYs, answerMax])!\n+ const rightmostDate = getRightmostVisibleDate(end, last(betPoints)?.x, now)\n+ const xScale = scaleTime([start, rightmostDate], [0, width])\n+\n+ // Scale for dates on Y-axis (time values in milliseconds)\n+ const yScale = scaleLinear([minY, maxY], [height, 0])\n+\n+ return (\n+ <SingleValueHistoryChart\n+ w={width}\n+ h={height}\n+ xScale={xScale}\n+ yScale={yScale}\n+ rightmostDate={rightmostDate}\n+ negativeThreshold={minY}\n+ zoomParams={zoomParams}\n+ showZoomer={showZoomer}\n+ data={singlePointData}\n+ Tooltip={(props) => (\n+ <SingleContractChartTooltip\n+ ttProps={props}\n+ xScale={zoomParams?.viewXScale ?? xScale}\n+ formatY={(y) => formatExpectedDate(y, contract)}\n+ />\n+ )}\n+ color={NUMERIC_GRAPH_COLOR}\n+ yKind=\"date\"\n+ />\n+ )\n+}\n"
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
"path": "web/components/charts/generic-charts.tsx",
|
|
334
|
-
"status": "modified",
|
|
335
|
-
"diff": "Index: web/components/charts/generic-charts.tsx\n===================================================================\n--- web/components/charts/generic-charts.tsx\td70bc7a (parent)\n+++ web/components/charts/generic-charts.tsx\te96bced (commit)\n@@ -48,8 +48,9 @@\n ZoomParams,\n } from './helpers'\n import { ZoomSlider } from './zoom-slider'\n import { NUMERIC_GRAPH_COLOR } from 'common/numeric-constants'\n+import { timeFormat } from 'd3-time-format'\n \n const interpolateY = (\n curve: CurveFactory,\n x: number,\n@@ -810,8 +811,22 @@\n : yKind === 'Ṁ' || yKind === 'spice'\n ? (n) => formatMoneyNumber(n)\n : yKind === 'sweepies'\n ? (n) => formatSweepiesNumber(n)\n+ : yKind === 'date'\n+ ? (n) => {\n+ const date = new Date(n)\n+ const timeSpan = max - min\n+ if (timeSpan > 3 * 365 * 24 * 60 * 60 * 1000) {\n+ // > 3 years\n+ return timeFormat('%Y')(date) // Years only\n+ } else if (timeSpan > 60 * 24 * 60 * 60 * 1000) {\n+ // > 60 days\n+ return timeFormat('%b %Y')(date) // Month and year\n+ } else {\n+ return timeFormat('%b %d')(date) // Month, day\n+ }\n+ }\n : (n) => formatWithCommas(n)\n )\n \n return { xAxis, yAxis }\n"
|
|
336
|
-
},
|
|
337
|
-
{
|
|
338
|
-
"path": "web/components/clock/clock.tsx",
|
|
339
|
-
"status": "modified",
|
|
340
|
-
"diff": "Index: web/components/clock/clock.tsx\n===================================================================\n--- web/components/clock/clock.tsx\td70bc7a (parent)\n+++ web/components/clock/clock.tsx\te96bced (commit)\n@@ -33,68 +33,194 @@\n seconds,\n }\n }\n \n-export const Clock = (props: { year: number }) => {\n- const { year } = props\n- const [timeUntil, setTimeUntil] = useState(getTimeUntil(year))\n+export const getTimeUntilFromMs = (ms: number) => {\n+ const currentTime = dayjs()\n+ const eventTime = dayjs(ms)\n+ const difference = eventTime.diff(currentTime)\n+\n+ const years = Math.floor(dayjs.duration(difference).asYears())\n+ const months = Math.floor(dayjs.duration(difference).asMonths()) % 12\n+ const days = Math.floor(dayjs.duration(difference).asDays()) % 30\n+ const hours = Math.floor(dayjs.duration(difference).asHours()) % 24\n+ const minutes = Math.floor(dayjs.duration(difference).asMinutes()) % 60\n+ const seconds = Math.floor(dayjs.duration(difference).asSeconds()) % 60\n+\n+ return {\n+ years,\n+ months,\n+ days,\n+ hours,\n+ minutes,\n+ seconds,\n+ }\n+}\n+\n+export const Clock = (props: {\n+ year?: number\n+ ms?: number\n+ className?: string\n+ size?: 'xs' | 'sm' | 'md' | 'lg'\n+}) => {\n+ const { year, ms, className, size = 'lg' } = props\n+ const [timeUntil, setTimeUntil] = useState(\n+ ms !== undefined\n+ ? getTimeUntilFromMs(ms)\n+ : year !== undefined\n+ ? getTimeUntil(year)\n+ : getTimeUntil(0)\n+ )\n const isClient = useIsClient()\n useEffect(() => {\n const timer = setInterval(() => {\n- setTimeUntil(getTimeUntil(year))\n+ if (ms !== undefined) {\n+ setTimeUntil(getTimeUntilFromMs(ms))\n+ } else if (year !== undefined) {\n+ setTimeUntil(getTimeUntil(year))\n+ }\n }, 1000)\n \n return () => {\n clearInterval(timer)\n }\n- }, [year])\n- const dotClass = ' h-1 w-1 sm:h-1.5 sm:w-1.5 rounded-sm'\n+ }, [year, ms])\n+\n+ // Sizing configuration based on size prop\n+ const sizeConfig = {\n+ xs: {\n+ clockText: 'text-xs sm:text-sm',\n+ dotSize: 'h-0.5 w-0.5 sm:h-1 sm:w-1',\n+ dotGap: 'gap-1 sm:gap-1.5',\n+ padding: 'p-1 sm:p-1.5',\n+ displayHeight: { mobile: 15, desktop: 20 },\n+ unitText: 'text-[8px] sm:text-xs',\n+ columnGap: 'gap-0.5 sm:gap-1',\n+ ringSize: 'ring-2',\n+ },\n+ sm: {\n+ clockText: 'text-sm sm:text-base',\n+ dotSize: 'h-0.5 w-0.5 sm:h-1 sm:w-1',\n+ dotGap: 'gap-1.5 sm:gap-2',\n+ padding: 'p-2 sm:p-2.5',\n+ displayHeight: { mobile: 20, desktop: 30 },\n+ unitText: 'text-[9px] sm:text-xs',\n+ columnGap: 'gap-0.5 sm:gap-1.5',\n+ ringSize: 'ring-2',\n+ },\n+ md: {\n+ clockText: 'text-base sm:text-2xl',\n+ dotSize: 'h-1 w-1 sm:h-1.5 sm:w-1.5',\n+ dotGap: 'gap-2 sm:gap-3',\n+ padding: 'p-3 sm:p-3.5',\n+ displayHeight: { mobile: 25, desktop: 40 },\n+ unitText: 'text-[10px] sm:text-sm',\n+ columnGap: 'gap-1 sm:gap-2',\n+ ringSize: 'ring-6',\n+ },\n+ lg: {\n+ clockText: 'text-xl sm:text-7xl',\n+ dotSize: 'h-1 w-1 sm:h-1.5 sm:w-1.5',\n+ dotGap: 'gap-2.5 sm:gap-4',\n+ padding: 'p-4 sm:p-4',\n+ displayHeight: { mobile: 30, desktop: 60 },\n+ unitText: 'text-xs sm:text-sm',\n+ columnGap: 'gap-1.5 sm:gap-3',\n+ ringSize: 'ring-8',\n+ },\n+ }\n+\n+ const config = sizeConfig[size]\n+ const dotClass = clsx(config.dotSize, 'rounded-sm')\n+\n const colon = (\n- <Col className={'h-full justify-center gap-2.5 sm:gap-4'}>\n+ <Col className={clsx('h-full justify-center', config.dotGap)}>\n <div style={{ backgroundColor: 'red' }} className={dotClass} />\n <div\n style={{ backgroundColor: 'red' }}\n className={clsx('mb-2 sm:mb-4', dotClass)}\n />\n </Col>\n )\n+\n return (\n- <Row className={'gap-1 text-xl sm:text-7xl'}>\n+ <Row className={clsx(config.clockText, 'gap-1', className)}>\n <Row\n style={{ color: 'red' }}\n- className={\n- 'justify-center gap-1.5 rounded-lg bg-gray-900 p-4 ring-8 ring-gray-300 sm:gap-3 sm:pl-10 sm:pr-6'\n- }\n+ className={clsx(\n+ 'justify-center rounded-lg bg-gray-900 ring ring-gray-300',\n+ config.padding,\n+ config.columnGap,\n+ config.ringSize\n+ )}\n >\n- <TimeUnit value={timeUntil.years.toString()} unit={'years'} />\n+ <TimeUnit\n+ value={timeUntil.years.toString()}\n+ unit={'years'}\n+ displayHeight={config.displayHeight}\n+ unitTextClass={config.unitText}\n+ />\n {colon}\n- <TimeUnit value={timeUntil.months.toString()} unit={'months'} />\n+ <TimeUnit\n+ value={timeUntil.months.toString()}\n+ unit={'months'}\n+ displayHeight={config.displayHeight}\n+ unitTextClass={config.unitText}\n+ />\n {colon}\n- <TimeUnit value={timeUntil.days.toString()} unit={'days'} />\n+ <TimeUnit\n+ value={timeUntil.days.toString()}\n+ unit={'days'}\n+ displayHeight={config.displayHeight}\n+ unitTextClass={config.unitText}\n+ />\n {colon}\n- <TimeUnit value={timeUntil.hours.toString()} unit={'hours'} />\n+ <TimeUnit\n+ value={timeUntil.hours.toString()}\n+ unit={'hours'}\n+ displayHeight={config.displayHeight}\n+ unitTextClass={config.unitText}\n+ />\n {colon}\n- <TimeUnit unit={'minutes'} value={timeUntil.minutes.toString()} />\n+ <TimeUnit\n+ unit={'minutes'}\n+ value={timeUntil.minutes.toString()}\n+ displayHeight={config.displayHeight}\n+ unitTextClass={config.unitText}\n+ />\n {colon}\n {/* We want to show the -- for the changing seconds */}\n <TimeUnit\n unit={'seconds'}\n value={isClient ? timeUntil.seconds : null}\n+ displayHeight={config.displayHeight}\n+ unitTextClass={config.unitText}\n />\n </Row>\n </Row>\n )\n }\n-const TimeUnit = (props: { value: number | string | null; unit: string }) => {\n- const { value, unit } = props\n+\n+const TimeUnit = (props: {\n+ value: number | string | null\n+ unit: string\n+ displayHeight?: { mobile: number; desktop: number }\n+ unitTextClass?: string\n+}) => {\n+ const {\n+ value,\n+ unit,\n+ displayHeight = { mobile: 30, desktop: 60 },\n+ unitTextClass = 'text-xs font-bold sm:text-sm',\n+ } = props\n return (\n <Col className={'items-center'}>\n <div className={'sm:hidden'}>\n- <Display height={30} value={value} />\n+ <Display height={displayHeight.mobile} value={value} />\n </div>\n <div className={'hidden sm:block'}>\n- <Display height={60} value={value} />\n+ <Display height={displayHeight.desktop} value={value} />\n </div>\n- <span className={'text-xs font-bold sm:text-sm'}>{unit}</span>\n+ <span className={unitTextClass}>{unit}</span>\n </Col>\n )\n }\n"
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
"path": "web/components/contract/contract-info-dialog.tsx",
|
|
344
|
-
"status": "modified",
|
|
345
|
-
"diff": "Index: web/components/contract/contract-info-dialog.tsx\n===================================================================\n--- web/components/contract/contract-info-dialog.tsx\td70bc7a (parent)\n+++ web/components/contract/contract-info-dialog.tsx\te96bced (commit)\n@@ -449,8 +449,31 @@\n />\n </td>\n </tr>\n )}\n+\n+ {!hideAdvanced && contract.outcomeType === 'DATE' && (\n+ <tr className={clsx(isMod && 'bg-purple-500/30')}>\n+ <td>\n+ 🕒 Clock mode{' '}\n+ <InfoTooltip\n+ text={'Display date as a clock instead of the default view'}\n+ />\n+ </td>\n+ <td>\n+ <CheckOrSwitch\n+ canToggle={isMod || isCreator}\n+ on={contract.display === 'clock'}\n+ setOn={(on) =>\n+ updateMarket({\n+ contractId: contract.id,\n+ display: on ? 'clock' : 'default',\n+ })\n+ }\n+ />\n+ </td>\n+ </tr>\n+ )}\n </tbody>\n </Table>\n )\n }\n"
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
"path": "web/components/contract/contract-overview.tsx",
|
|
349
|
-
"status": "modified",
|
|
350
|
-
"diff": "Index: web/components/contract/contract-overview.tsx\n===================================================================\n--- web/components/contract/contract-overview.tsx\td70bc7a (parent)\n+++ web/components/contract/contract-overview.tsx\te96bced (commit)\n@@ -12,8 +12,9 @@\n BountiedQuestionContract,\n CPMMMultiContract,\n CPMMNumericContract,\n Contract,\n+ MultiDateContract,\n MultiNumericContract,\n PseudoNumericContract,\n StonkContract,\n getMainBinaryMCAnswer,\n@@ -37,8 +38,9 @@\n import { NumberContractChart } from 'web/components/charts/contract/number'\n import { UserPositionSearchButton } from 'web/components/charts/user-position-search-button'\n import {\n BinaryResolutionOrChance,\n+ MultiDateResolutionOrExpectation,\n MultiNumericResolutionOrExpectation,\n NumberResolutionOrExpectation,\n PseudoNumericResolutionOrExpectation,\n StonkPrice,\n@@ -78,8 +80,9 @@\n import { LoadingIndicator } from '../widgets/loading-indicator'\n import { GradientContainer } from '../widgets/gradient-container'\n import { getIsLive } from 'common/sports-info'\n import { MultiNumericContractChart } from '../charts/contract/multi-numeric'\n+import { MultiDateContractChart } from '../charts/contract/multi-date'\n \n export const ContractOverview = memo(\n (props: {\n contract: Contract\n@@ -164,8 +167,19 @@\n resolutionRating={resolutionRating}\n onAnswerCommentClick={onAnswerCommentClick}\n />\n )\n+ case 'DATE':\n+ return (\n+ <MultiDateOverview\n+ contract={contract}\n+ points={betPoints as MultiPoints}\n+ showResolver={showResolver}\n+ setShowResolver={setShowResolver}\n+ resolutionRating={resolutionRating}\n+ onAnswerCommentClick={onAnswerCommentClick}\n+ />\n+ )\n case 'NUMBER':\n return (\n <NumberOverview\n contract={contract}\n@@ -655,26 +669,99 @@\n const [showZoomer, setShowZoomer] = useState(false)\n const { currentTimePeriod, setTimePeriod, maxRange, zoomParams } =\n useTimePicker(contract, () => setShowZoomer(true))\n \n- const shouldAnswersSumToOne =\n- 'shouldAnswersSumToOne' in contract ? contract.shouldAnswersSumToOne : true\n-\n- const [query, setQuery] = usePersistentInMemoryState(\n- '',\n- 'create-answer-text' + contract.id\n+ return (\n+ <>\n+ <Row className=\"relative justify-between gap-2\">\n+ <MultiNumericResolutionOrExpectation contract={contract} />\n+ <Row className={'relative gap-1'}>\n+ <TimeRangePicker\n+ className=\"h-[2.4rem]\"\n+ currentTimePeriod={currentTimePeriod}\n+ setCurrentTimePeriod={setTimePeriod}\n+ maxRange={maxRange}\n+ color=\"indigo\"\n+ />\n+ </Row>\n+ </Row>\n+ <SizedContainer\n+ className={clsx('mb-12 h-[150px] w-full pb-4 pr-10 sm:h-[200px]')}\n+ >\n+ {(w, h) => (\n+ <MultiNumericContractChart\n+ width={w}\n+ height={h}\n+ multiPoints={points}\n+ zoomParams={zoomParams}\n+ contract={contract}\n+ showZoomer={showZoomer}\n+ />\n+ )}\n+ </SizedContainer>\n+ {!contract.shouldAnswersSumToOne ? (\n+ <IndependentAnswersResolvePanel\n+ show={showResolver}\n+ contract={contract}\n+ onClose={() => setShowResolver(false)}\n+ />\n+ ) : showResolver ? (\n+ <GradientContainer>\n+ <AnswersResolvePanel\n+ contract={contract}\n+ onClose={() => setShowResolver(false)}\n+ />\n+ </GradientContainer>\n+ ) : null}\n+ {!showResolver && (\n+ <>\n+ {resolutionRating}\n+ <AnswersPanel\n+ hideSearch\n+ selectedAnswerIds={[]}\n+ contract={contract}\n+ onAnswerCommentClick={onAnswerCommentClick}\n+ sort={getDefaultSort(contract)}\n+ setSort={() => {}}\n+ query={''}\n+ setQuery={() => {}}\n+ />\n+ {tradingAllowed(contract) && (\n+ <UserBetsSummary\n+ className=\"border-ink-200 !mb-2 mt-2 \"\n+ contract={contract}\n+ />\n+ )}\n+ </>\n+ )}\n+ </>\n )\n+}\n+const MultiDateOverview = (props: {\n+ points: MultiPoints\n+ contract: MultiDateContract\n+ showResolver: boolean\n+ resolutionRating?: ReactNode\n+ setShowResolver: (show: boolean) => void\n+ onAnswerCommentClick: (answer: Answer) => void\n+}) => {\n+ const {\n+ points,\n+ contract,\n+ showResolver,\n+ resolutionRating,\n+ setShowResolver,\n+ onAnswerCommentClick,\n+ } = props\n \n- const defaultSort = getDefaultSort(contract)\n- const [sort, setSort] = usePersistentInMemoryState<MultiSort>(\n- defaultSort,\n- 'answer-sort' + contract.id\n- )\n+ const [showZoomer, setShowZoomer] = useState(false)\n+ const { currentTimePeriod, setTimePeriod, maxRange, zoomParams } =\n+ useTimePicker(contract, () => setShowZoomer(true))\n \n return (\n <>\n <Row className=\"relative justify-between gap-2\">\n- <MultiNumericResolutionOrExpectation contract={contract} />\n+ <MultiDateResolutionOrExpectation contract={contract} />\n <Row className={'relative gap-1'}>\n <TimeRangePicker\n className=\"h-[2.4rem]\"\n currentTimePeriod={currentTimePeriod}\n@@ -687,9 +774,9 @@\n <SizedContainer\n className={clsx('mb-12 h-[150px] w-full pb-4 pr-10 sm:h-[200px]')}\n >\n {(w, h) => (\n- <MultiNumericContractChart\n+ <MultiDateContractChart\n width={w}\n height={h}\n multiPoints={points}\n zoomParams={zoomParams}\n@@ -697,9 +784,9 @@\n showZoomer={showZoomer}\n />\n )}\n </SizedContainer>\n- {!shouldAnswersSumToOne && contract.mechanism === 'cpmm-multi-1' ? (\n+ {!contract.shouldAnswersSumToOne ? (\n <IndependentAnswersResolvePanel\n show={showResolver}\n contract={contract}\n onClose={() => setShowResolver(false)}\n@@ -719,12 +806,12 @@\n hideSearch\n selectedAnswerIds={[]}\n contract={contract}\n onAnswerCommentClick={onAnswerCommentClick}\n- sort={sort}\n- setSort={setSort}\n- query={query}\n- setQuery={setQuery}\n+ sort={getDefaultSort(contract)}\n+ setSort={() => {}}\n+ query={''}\n+ setQuery={() => {}}\n />\n {tradingAllowed(contract) && (\n <UserBetsSummary\n className=\"border-ink-200 !mb-2 mt-2 \"\n"
|
|
351
|
-
},
|
|
352
|
-
{
|
|
353
|
-
"path": "web/components/contract/contract-page.tsx",
|
|
354
|
-
"status": "modified",
|
|
355
|
-
"diff": "Index: web/components/contract/contract-page.tsx\n===================================================================\n--- web/components/contract/contract-page.tsx\td70bc7a (parent)\n+++ web/components/contract/contract-page.tsx\te96bced (commit)\n@@ -594,15 +594,17 @@\n multiPointsString,\n } = props\n \n const isNumber = outcomeType === 'NUMBER'\n- const isMultiNumeric = outcomeType === 'MULTI_NUMERIC'\n+ const isMultiNumeric =\n+ outcomeType === 'MULTI_NUMERIC' || outcomeType === 'DATE'\n \n const newBets = useContractBets(\n contractId,\n {\n afterTime: lastBetTime ?? 0,\n includeZeroShareRedemptions: true,\n+ // TODO: this shows the redemptions in the trades tab??\n filterRedemptions: !isNumber && !isMultiNumeric,\n },\n useIsPageVisible,\n (params) => api('bets', params)\n@@ -627,9 +629,10 @@\n const betPoints = useMemo(() => {\n if (\n outcomeType === 'MULTIPLE_CHOICE' ||\n outcomeType === 'NUMBER' ||\n- outcomeType === 'MULTI_NUMERIC'\n+ outcomeType === 'MULTI_NUMERIC' ||\n+ outcomeType === 'DATE'\n ) {\n const data = multiPointsString\n ? unserializeBase64Multi(multiPointsString)\n : {}\n"
|
|
356
|
-
},
|
|
357
|
-
{
|
|
358
|
-
"path": "web/components/contract/contract-price.tsx",
|
|
359
|
-
"status": "modified",
|
|
360
|
-
"diff": "Index: web/components/contract/contract-price.tsx\n===================================================================\n--- web/components/contract/contract-price.tsx\td70bc7a (parent)\n+++ web/components/contract/contract-price.tsx\te96bced (commit)\n@@ -1,7 +1,8 @@\n import {\n BinaryContract,\n CPMMNumericContract,\n+ MultiDateContract,\n MultiNumericContract,\n PseudoNumericContract,\n StonkContract,\n } from 'common/contract'\n@@ -26,8 +27,11 @@\n getNumberExpectedValue,\n answerTextToRange,\n } from 'common/src/number'\n import { formatExpectedValue, getExpectedValue } from 'common/multi-numeric'\n+import { formatExpectedDate } from 'common/multi-date'\n+import { getExpectedDate } from 'common/multi-date'\n+import { Clock } from '../clock/clock'\n \n export function BinaryResolutionOrChance(props: {\n contract: BinaryContract\n className?: string\n@@ -213,9 +217,52 @@\n )}\n </span>\n )\n }\n+export function MultiDateResolutionOrExpectation(props: {\n+ contract: MultiDateContract\n+ className?: string\n+}) {\n+ const { contract, className } = props\n+ const { answers, resolution, timezone, display } = contract\n+ const resolvedAnswer = answers.find((a) => a.id === resolution)\n+ const value = getExpectedDate(contract)\n+ const formattedValue = formatExpectedDate(value, contract)\n+ const spring = useAnimatedNumber(value)\n \n+ return (\n+ <span\n+ className={clsx(\n+ 'items-baseline text-2xl sm:inline-flex sm:text-3xl',\n+ className\n+ )}\n+ >\n+ {resolution ? (\n+ <>\n+ <div className=\"mr-2 text-base\">Resolved</div>\n+ {resolution === 'CANCEL' ? (\n+ <CancelLabel />\n+ ) : (\n+ <MultiNumericValueLabel\n+ formattedValue={resolvedAnswer?.text ?? formattedValue}\n+ />\n+ )}\n+ </>\n+ ) : display === 'clock' ? (\n+ <Tooltip text={`tz: ${timezone}`} placement=\"bottom\">\n+ <Clock ms={value} size=\"sm\" />\n+ </Tooltip>\n+ ) : (\n+ <Tooltip text={`tz: ${timezone}`} placement=\"bottom\">\n+ <animated.div className={'mr-2 inline-block'}>\n+ {spring.to((val) => formatExpectedDate(val, contract))}\n+ </animated.div>\n+ </Tooltip>\n+ )}\n+ </span>\n+ )\n+}\n+\n export function StonkPrice(props: {\n contract: StonkContract\n className?: string\n }) {\n"
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
"path": "web/components/contract/contracts-table.tsx",
|
|
364
|
-
"status": "modified",
|
|
365
|
-
"diff": "Index: web/components/contract/contracts-table.tsx\n===================================================================\n--- web/components/contract/contracts-table.tsx\td70bc7a (parent)\n+++ web/components/contract/contracts-table.tsx\te96bced (commit)\n@@ -32,8 +32,9 @@\n import { Tooltip } from '../widgets/tooltip'\n import { SpiceCoin } from 'web/public/custom-components/spiceCoin'\n import { SweepiesCoin } from 'web/public/custom-components/sweepiesCoin'\n import { getFormattedExpectedValue } from 'common/multi-numeric'\n+import { getFormattedExpectedDate } from 'common/multi-date'\n \n export function ContractsTable(props: {\n contracts: Contract[]\n onContractClick?: (contract: Contract) => void\n@@ -218,8 +219,12 @@\n case 'MULTI_NUMERIC': {\n const val = getFormattedExpectedValue(contract, false)\n return <span className={clsx(probTextColor, className)}>{val}</span>\n }\n+ case 'DATE': {\n+ const val = getFormattedExpectedDate(contract, false)\n+ return <span className={clsx(probTextColor, className)}>{val}</span>\n+ }\n case 'MULTIPLE_CHOICE': {\n return <ContractMinibar width={width} contract={contract} />\n }\n case 'QUADRATIC_FUNDING': {\n"
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
"path": "web/components/contract/feed-contract-card.tsx",
|
|
369
|
-
"status": "modified",
|
|
370
|
-
"diff": "Index: web/components/contract/feed-contract-card.tsx\n===================================================================\n--- web/components/contract/feed-contract-card.tsx\td70bc7a (parent)\n+++ web/components/contract/feed-contract-card.tsx\te96bced (commit)\n@@ -290,9 +290,10 @@\n \n {isBinaryMc &&\n contract.mechanism === 'cpmm-multi-1' &&\n contract.outcomeType !== 'NUMBER' &&\n- contract.outcomeType !== 'MULTI_NUMERIC' && (\n+ contract.outcomeType !== 'MULTI_NUMERIC' &&\n+ contract.outcomeType !== 'DATE' && (\n <BinaryMultiAnswersPanel\n contract={contract}\n feedReason={feedReason}\n />\n"
|
|
371
|
-
},
|
|
372
|
-
{
|
|
373
|
-
"path": "web/components/dashboard/horizontal-dashboard-card.tsx",
|
|
374
|
-
"status": "modified",
|
|
375
|
-
"diff": "Index: web/components/dashboard/horizontal-dashboard-card.tsx\n===================================================================\n--- web/components/dashboard/horizontal-dashboard-card.tsx\td70bc7a (parent)\n+++ web/components/dashboard/horizontal-dashboard-card.tsx\te96bced (commit)\n@@ -154,9 +154,10 @@\n \n {isBinaryMc &&\n contract.mechanism === 'cpmm-multi-1' &&\n contract.outcomeType !== 'NUMBER' &&\n- contract.outcomeType !== 'MULTI_NUMERIC' && (\n+ contract.outcomeType !== 'MULTI_NUMERIC' &&\n+ contract.outcomeType !== 'DATE' && (\n <BinaryMultiAnswersPanel contract={contract} />\n )}\n \n {isBinaryCpmm && (showGraph || !ignore) && (\n"
|
|
376
|
-
},
|
|
377
|
-
{
|
|
378
|
-
"path": "web/components/new-contract/choosing-contract-form.tsx",
|
|
379
|
-
"status": "modified",
|
|
380
|
-
"diff": "Index: web/components/new-contract/choosing-contract-form.tsx\n===================================================================\n--- web/components/new-contract/choosing-contract-form.tsx\td70bc7a (parent)\n+++ web/components/new-contract/choosing-contract-form.tsx\te96bced (commit)\n@@ -67,8 +67,9 @@\n value:\n | CreateableOutcomeType\n | 'INDEPENDENT_MULTIPLE_CHOICE'\n | 'DEPENDENT_MULTIPLE_CHOICE'\n+ | 'DATE'\n visual: ReactNode\n className?: string\n backgroundColor?: string\n selectClassName?: string\n"
|
|
381
|
-
},
|
|
382
|
-
{
|
|
383
|
-
"path": "web/components/new-contract/contract-params-form.tsx",
|
|
384
|
-
"status": "modified",
|
|
385
|
-
"diff": "Index: web/components/new-contract/contract-params-form.tsx\n===================================================================\n--- web/components/new-contract/contract-params-form.tsx\td70bc7a (parent)\n+++ web/components/new-contract/contract-params-form.tsx\te96bced (commit)\n@@ -8,9 +8,9 @@\n Contract,\n CreateableOutcomeType,\n MAX_DESCRIPTION_LENGTH,\n MAX_QUESTION_LENGTH,\n- MULTI_NUMERIC_BUCKETS_MAX,\n+ NUMBER_BUCKETS_MAX,\n NON_BETTING_OUTCOMES,\n twombaContractPath,\n Visibility,\n } from 'common/contract'\n@@ -69,8 +69,9 @@\n import { randomString } from 'common/util/random'\n import { formatWithToken } from 'common/util/format'\n import { BiUndo } from 'react-icons/bi'\n import { liquidityTiers } from 'common/tier'\n+import { MultiNumericDateSection } from './multi-numeric-date-section'\n \n export function ContractParamsForm(props: {\n creator: User\n outcomeType: CreateableOutcomeType\n@@ -132,9 +133,10 @@\n // For multiple choice, init to 2 empty answers\n const defaultAnswers =\n outcomeType === 'MULTIPLE_CHOICE' ||\n outcomeType == 'POLL' ||\n- outcomeType == 'MULTI_NUMERIC'\n+ outcomeType == 'MULTI_NUMERIC' ||\n+ outcomeType == 'DATE'\n ? ['', '']\n : []\n \n const answersKey = 'new-answers-with-other' + paramsKey\n@@ -162,9 +164,9 @@\n params?.addAnswersMode ?? 'DISABLED',\n addAnswersModeKey\n )\n const shouldAnswersSumToOne =\n- outcomeType === 'MULTI_NUMERIC'\n+ outcomeType === 'MULTI_NUMERIC' || outcomeType === 'DATE'\n ? multiNumericSumsToOne\n : params?.shouldAnswersSumToOne ?? false\n \n // NOTE: if you add another user-controlled state variable, you should also add it to the duplication parameters and resetProperties()\n@@ -326,9 +328,13 @@\n }, [outcomeType])\n \n const isValidQuestion =\n question.length > 0 && question.length <= MAX_QUESTION_LENGTH\n- const hasAnswers = outcomeType === 'MULTIPLE_CHOICE' || outcomeType === 'POLL'\n+ const hasAnswers =\n+ outcomeType === 'MULTIPLE_CHOICE' ||\n+ outcomeType === 'POLL' ||\n+ outcomeType === 'MULTI_NUMERIC' ||\n+ outcomeType === 'DATE'\n const isValidMultipleChoice =\n !hasAnswers || answers.every((answer) => answer.trim().length > 0)\n \n const isValidDate =\n@@ -349,9 +355,9 @@\n isFinite(max) &&\n min < max\n \n const midpointsError =\n- outcomeType === 'MULTI_NUMERIC'\n+ outcomeType === 'MULTI_NUMERIC' || outcomeType === 'DATE'\n ? midpoints.length !== answers.length\n : false\n \n const isValid =\n@@ -368,12 +374,11 @@\n isValidMultipleChoice &&\n !midpointsError &&\n (outcomeType !== 'BOUNTIED_QUESTION' || bountyAmount !== undefined) &&\n (outcomeType === 'NUMBER'\n- ? numberOfBuckets <= MULTI_NUMERIC_BUCKETS_MAX && numberOfBuckets >= 2\n+ ? numberOfBuckets <= NUMBER_BUCKETS_MAX && numberOfBuckets >= 2\n : true) &&\n- (outcomeType !== 'MULTI_NUMERIC' ||\n- ((minMaxValid || isValidMultipleChoice) && unit !== ''))\n+ (outcomeType !== 'MULTI_NUMERIC' || (minMaxValid && unit !== ''))\n \n const [errorText, setErrorText] = useState<string>('')\n useEffect(() => {\n setErrorText('')\n@@ -468,9 +473,12 @@\n isLogScale,\n groupIds: selectedGroups.map((g) => g.id),\n answers,\n midpoints,\n- addAnswersMode: isMultiNumeric ? 'DISABLED' : addAnswersMode,\n+ addAnswersMode:\n+ outcomeType === 'MULTI_NUMERIC' || outcomeType === 'DATE'\n+ ? 'DISABLED'\n+ : addAnswersMode,\n shouldAnswersSumToOne,\n visibility,\n utcOffset: new Date().getTimezoneOffset(),\n totalBounty: bountyAmount,\n@@ -482,8 +490,9 @@\n sportsStartTimestamp: params?.sportsStartTimestamp,\n sportsEventId: params?.sportsEventId,\n sportsLeague: params?.sportsLeague,\n unit: unit.trim(),\n+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n })\n \n const newContract = await api('market', createProps as any)\n \n@@ -541,8 +550,9 @@\n \n const isMulti = outcomeType === 'MULTIPLE_CHOICE'\n const isNumber = outcomeType === 'NUMBER'\n const isMultiNumeric = outcomeType === 'MULTI_NUMERIC'\n+ const isDate = outcomeType === 'DATE'\n \n const [isGeneratingDescription, setIsGeneratingDescription] = useState(false)\n const [preGenerateContent, setPreGenerateContent] = useState<\n JSONContent | undefined\n@@ -716,8 +726,26 @@\n setShouldAnswersSumToOne={setMultiNumericSumsToOne}\n unit={unit}\n setUnit={setUnit}\n />\n+ )}{' '}\n+ {isDate && (\n+ <MultiNumericDateSection\n+ paramsKey={paramsKey}\n+ submitState={submitState}\n+ question={question}\n+ description={editor?.getHTML()}\n+ answers={answers}\n+ setAnswers={setAnswers}\n+ midpoints={midpoints}\n+ setMidpoints={setMidpoints}\n+ minString={minString}\n+ setMinString={setMinString}\n+ maxString={maxString}\n+ setMaxString={setMaxString}\n+ shouldAnswersSumToOne={shouldAnswersSumToOne}\n+ setShouldAnswersSumToOne={setMultiNumericSumsToOne}\n+ />\n )}\n {outcomeType === 'PSEUDO_NUMERIC' && (\n <PseudoNumericRangeSection\n minString={minString}\n"
|
|
386
|
-
},
|
|
387
|
-
{
|
|
388
|
-
"path": "web/components/new-contract/create-contract-types.tsx",
|
|
389
|
-
"status": "modified",
|
|
390
|
-
"diff": "Index: web/components/new-contract/create-contract-types.tsx\n===================================================================\n--- web/components/new-contract/create-contract-types.tsx\td70bc7a (parent)\n+++ web/components/new-contract/create-contract-types.tsx\te96bced (commit)\n@@ -2,8 +2,9 @@\n BsFillCheckCircleFill,\n BsFillXCircleFill,\n BsUiChecks,\n BsUiChecksGrid,\n+ BsCalendar2Date,\n } from 'react-icons/bs'\n import { Col } from 'web/components/layout/col'\n import { CgPoll } from 'react-icons/cg'\n import { GoNumber } from 'react-icons/go'\n@@ -81,8 +82,22 @@\n ),\n shouldSumToOne: true,\n outcomeType: 'MULTI_NUMERIC',\n },\n+ DATE: {\n+ label: 'Date',\n+ value: 'DATE',\n+ name: 'date',\n+ descriptor: 'A question with a date answer.',\n+ example: 'When will OpenAI release GPT-7?',\n+ visual: (\n+ <Col className=\"text-primary-400 relative my-auto h-12 w-12\">\n+ <BsCalendar2Date className=\"h-12 w-12\" />\n+ </Col>\n+ ),\n+ shouldSumToOne: true,\n+ outcomeType: 'DATE',\n+ },\n } as const\n \n export const NON_PREDICTIVE_CONTRACT_TYPES = {\n // BOUNTIED_QUESTION: {\n"
|
|
391
|
-
},
|
|
392
|
-
{
|
|
393
|
-
"path": "web/components/new-contract/multi-numeric-date-section.tsx",
|
|
394
|
-
"status": "added",
|
|
395
|
-
"diff": "Index: web/components/new-contract/multi-numeric-date-section.tsx\n===================================================================\n--- web/components/new-contract/multi-numeric-date-section.tsx\td70bc7a (parent)\n+++ web/components/new-contract/multi-numeric-date-section.tsx\te96bced (commit)\n@@ -0,0 +1,398 @@\n+import { Col } from 'web/components/layout/col'\n+import { InfoTooltip } from 'web/components/widgets/info-tooltip'\n+import { Row } from 'web/components/layout/row'\n+import { Input } from 'web/components/widgets/input'\n+import { useState, useCallback, useEffect } from 'react'\n+import { Button } from '../buttons/button'\n+import { api, APIError } from 'web/lib/api/api'\n+import { XIcon } from '@heroicons/react/solid'\n+import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'\n+import { ControlledTabs } from '../layout/tabs'\n+import { debounce } from 'lodash'\n+import { MAX_MULTI_NUMERIC_ANSWERS } from 'common/multi-numeric'\n+\n+export const MultiNumericDateSection = (props: {\n+ submitState: 'EDITING' | 'LOADING' | 'DONE'\n+ question: string\n+ minString: string\n+ setMinString: (value: string) => void\n+ maxString: string\n+ setMaxString: (value: string) => void\n+ description?: string\n+ answers: string[]\n+ paramsKey: string\n+ setAnswers: (answers: string[]) => void\n+ midpoints: number[]\n+ setMidpoints: (midpoints: number[]) => void\n+ shouldAnswersSumToOne: boolean\n+ setShouldAnswersSumToOne: (shouldAnswersSumToOne: boolean) => void\n+}) => {\n+ const {\n+ paramsKey,\n+ submitState,\n+ question,\n+ description,\n+ answers,\n+ midpoints,\n+ setAnswers,\n+ setMidpoints,\n+ minString,\n+ setMinString,\n+ maxString,\n+ setMaxString,\n+ setShouldAnswersSumToOne,\n+ shouldAnswersSumToOne,\n+ } = props\n+ const defaultAnswers = ['', '']\n+ const [isGeneratingRanges, setIsGeneratingRanges] = useState(false)\n+ // thresholds\n+ const [thresholdAnswers, setThresholdAnswers] = usePersistentLocalState<\n+ string[]\n+ >(\n+ shouldAnswersSumToOne ? defaultAnswers : answers,\n+ 'threshold-answers' + paramsKey\n+ )\n+ const [thresholdMidpoints, setThresholdMidpoints] = usePersistentLocalState<\n+ number[]\n+ >(shouldAnswersSumToOne ? [] : midpoints, 'threshold-midpoints' + paramsKey)\n+ // buckets\n+ const [bucketAnswers, setBucketAnswers] = usePersistentLocalState<string[]>(\n+ !shouldAnswersSumToOne ? defaultAnswers : answers,\n+ 'bucket-answers' + paramsKey\n+ )\n+ const [bucketMidpoints, setBucketMidpoints] = usePersistentLocalState<\n+ number[]\n+ >(!shouldAnswersSumToOne ? [] : midpoints, 'bucket-midpoints' + paramsKey)\n+ const minMaxError = minString === '' || maxString === ''\n+ const [error, setError] = useState<string>('')\n+ const [regenerateError, setRegenerateError] = useState<string>('')\n+ const [maxAnswersReached, setMaxAnswersReached] = useState<boolean>(false)\n+\n+ const selectedTab = shouldAnswersSumToOne ? 'buckets' : 'thresholds'\n+\n+ // Check if max answers limit is reached\n+ useEffect(() => {\n+ const currentAnswers =\n+ selectedTab === 'buckets' ? bucketAnswers : thresholdAnswers\n+ setMaxAnswersReached(currentAnswers.length > MAX_MULTI_NUMERIC_ANSWERS)\n+ }, [selectedTab, bucketAnswers.length, thresholdAnswers.length])\n+\n+ const generateRanges = async () => {\n+ setError('')\n+ setRegenerateError('')\n+ if (!question || minString === '' || maxString === '') return\n+ setIsGeneratingRanges(true)\n+ try {\n+ const result = await api('generate-ai-date-ranges', {\n+ question,\n+ description,\n+ min: minString,\n+ max: maxString,\n+ })\n+\n+ setThresholdAnswers(result.thresholds.answers)\n+ setThresholdMidpoints(result.thresholds.midpoints)\n+ setBucketAnswers(result.buckets.answers)\n+ setBucketMidpoints(result.buckets.midpoints)\n+ } catch (e) {\n+ console.error('Error generating date ranges:', e)\n+ if (e instanceof APIError) {\n+ setError(e.message.slice(0, 100))\n+ } else {\n+ setError('An error occurred while generating date ranges.')\n+ }\n+ }\n+ setIsGeneratingRanges(false)\n+ }\n+\n+ const removeAnswer = (i: number, tab: 'thresholds' | 'buckets') => {\n+ const newAnswers =\n+ tab === 'thresholds'\n+ ? thresholdAnswers.slice(0, i).concat(thresholdAnswers.slice(i + 1))\n+ : bucketAnswers.slice(0, i).concat(bucketAnswers.slice(i + 1))\n+ const newMidpoints =\n+ tab === 'thresholds'\n+ ? thresholdMidpoints.slice(0, i).concat(thresholdMidpoints.slice(i + 1))\n+ : bucketMidpoints.slice(0, i).concat(bucketMidpoints.slice(i + 1))\n+ if (tab === 'thresholds') {\n+ setThresholdAnswers(newAnswers)\n+ setThresholdMidpoints(newMidpoints)\n+ } else {\n+ setBucketAnswers(newAnswers)\n+ setBucketMidpoints(newMidpoints)\n+ }\n+ }\n+\n+ const handleRangeBlur = () => {\n+ setError('')\n+ setRegenerateError('')\n+ if (!minMaxError && question) {\n+ generateRanges()\n+ }\n+ }\n+\n+ const handleTabChange = (tab: 'thresholds' | 'buckets') => {\n+ setShouldAnswersSumToOne(tab === 'buckets')\n+ // Switch between threshold and bucket answers without any post-processing\n+ if (tab === 'thresholds') {\n+ setAnswers(thresholdAnswers)\n+ setMidpoints(thresholdMidpoints)\n+ } else {\n+ setAnswers(bucketAnswers)\n+ setMidpoints(bucketMidpoints)\n+ }\n+ }\n+\n+ useEffect(() => {\n+ handleTabChange(selectedTab)\n+ }, [\n+ JSON.stringify(thresholdAnswers),\n+ JSON.stringify(bucketAnswers),\n+ JSON.stringify(thresholdMidpoints),\n+ JSON.stringify(bucketMidpoints),\n+ ])\n+\n+ const handleAnswerChanged = async (\n+ answers: string[],\n+ min: string,\n+ max: string,\n+ tab: 'thresholds' | 'buckets'\n+ ) => {\n+ setRegenerateError('')\n+ // Only regenerate midpoints if we have min and max\n+ if (min === '' || max === '') return\n+\n+ try {\n+ // Call regenerate-date-midpoints without tab parameter\n+ const result = await api('regenerate-date-midpoints', {\n+ question,\n+ answers,\n+ min,\n+ max,\n+ description,\n+ tab,\n+ })\n+\n+ // Update midpoints state\n+ setMidpoints(result.midpoints)\n+\n+ // Update the tab-specific state\n+ if (tab === 'thresholds') {\n+ setThresholdMidpoints(result.midpoints)\n+ } else {\n+ setBucketMidpoints(result.midpoints)\n+ }\n+ } catch (e) {\n+ console.error('Error regenerating date midpoints:', e)\n+ if (e instanceof APIError) {\n+ setRegenerateError(e.message.slice(0, 100))\n+ } else {\n+ setRegenerateError(\n+ 'An error occurred while regenerating date midpoints.'\n+ )\n+ }\n+ }\n+ }\n+\n+ // Create debounced version of handleAnswerChanged\n+ const debouncedHandleAnswerChanged = useCallback(\n+ debounce(\n+ (answers: string[], tab: 'thresholds' | 'buckets') =>\n+ handleAnswerChanged(answers, minString, maxString, tab),\n+ 1500\n+ ),\n+ [minString, maxString, setMidpoints]\n+ )\n+\n+ const addAnswer = () => {\n+ if (selectedTab === 'thresholds') {\n+ if (thresholdAnswers.length < MAX_MULTI_NUMERIC_ANSWERS) {\n+ setThresholdAnswers([...thresholdAnswers, ''])\n+ }\n+ } else {\n+ if (bucketAnswers.length < MAX_MULTI_NUMERIC_ANSWERS) {\n+ setBucketAnswers([...bucketAnswers, ''])\n+ }\n+ }\n+ }\n+\n+ const tabs = [\n+ {\n+ title: 'Buckets',\n+ content: (\n+ <Col className=\"mt-2 gap-2\">\n+ {bucketAnswers.map((answer, i) => (\n+ <Row key={i} className=\"items-center gap-2\">\n+ {i + 1}.{' '}\n+ <Input\n+ className=\"w-full\"\n+ value={answer}\n+ onChange={(e) => {\n+ const newAnswers = [...bucketAnswers]\n+ newAnswers[i] = e.target.value\n+ setBucketAnswers(newAnswers)\n+ debouncedHandleAnswerChanged(newAnswers, 'buckets')\n+ }}\n+ />\n+ {bucketAnswers.length > 2 && (\n+ <button\n+ onClick={() => removeAnswer(i, 'buckets')}\n+ type=\"button\"\n+ className=\"hover:bg-canvas-50 border-ink-300 text-ink-700 bg-canvas-0 focus:ring-primary-500 inline-flex items-center rounded-full border p-1 text-xs font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2\"\n+ >\n+ <XIcon className=\"h-5 w-5\" aria-hidden=\"true\" />\n+ </button>\n+ )}\n+ </Row>\n+ ))}\n+ <Row className=\"justify-end gap-2\">\n+ <Button\n+ color=\"none\"\n+ disabled={bucketAnswers.length >= MAX_MULTI_NUMERIC_ANSWERS}\n+ onClick={addAnswer}\n+ className=\"hover:bg-canvas-50 border-ink-300 text-ink-700 bg-canvas-0 focus:ring-primary-500 inline-flex items-center rounded border px-2.5 py-1.5 text-xs font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2\"\n+ >\n+ Add bucket\n+ </Button>\n+ </Row>\n+ </Col>\n+ ),\n+ },\n+ {\n+ title: 'Thresholds',\n+ content: (\n+ <Col className=\"mt-2 gap-2\">\n+ {thresholdAnswers.map((answer, i) => (\n+ <Row key={i} className=\"items-center gap-2\">\n+ {i + 1}.{' '}\n+ <Input\n+ className=\"w-full\"\n+ value={answer}\n+ onChange={(e) => {\n+ const newAnswers = [...thresholdAnswers]\n+ newAnswers[i] = e.target.value\n+ setThresholdAnswers(newAnswers)\n+ debouncedHandleAnswerChanged(newAnswers, 'thresholds')\n+ }}\n+ />\n+ {thresholdAnswers.length > 2 && (\n+ <button\n+ onClick={() => removeAnswer(i, 'thresholds')}\n+ type=\"button\"\n+ className=\"hover:bg-canvas-50 border-ink-300 text-ink-700 bg-canvas-0 focus:ring-primary-500 inline-flex items-center rounded-full border p-1 text-xs font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2\"\n+ >\n+ <XIcon className=\"h-5 w-5\" aria-hidden=\"true\" />\n+ </button>\n+ )}\n+ </Row>\n+ ))}\n+ <Row className=\"justify-end gap-2\">\n+ <Button\n+ color=\"none\"\n+ disabled={thresholdAnswers.length >= MAX_MULTI_NUMERIC_ANSWERS}\n+ onClick={addAnswer}\n+ className=\"hover:bg-canvas-50 border-ink-300 text-ink-700 bg-canvas-0 focus:ring-primary-500 inline-flex items-center rounded border px-2.5 py-1.5 text-xs font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2\"\n+ >\n+ Add threshold\n+ </Button>\n+ </Row>\n+ </Col>\n+ ),\n+ },\n+ ]\n+ const titles = tabs.map((tab) => tab.title.toLowerCase())\n+\n+ return (\n+ <Col>\n+ <Row className={' flex-wrap'}>\n+ <Col className=\"mb-2 w-full items-start\">\n+ <Row className=\"items-baseline gap-1 px-1 py-2\">\n+ <span className=\"\">Date range</span>\n+ <InfoTooltip text=\"The start and end dates for your question. Choose dates the value could reasonably be expected to fall between.\" />\n+ </Row>\n+ <Row className={'w-full gap-2'}>\n+ <Input\n+ type=\"text\"\n+ error={!!error}\n+ className=\"w-full\"\n+ placeholder=\"Start\"\n+ onClick={(e) => e.stopPropagation()}\n+ onChange={(e) => setMinString(e.target.value)}\n+ onBlur={handleRangeBlur}\n+ disabled={submitState === 'LOADING'}\n+ value={minString}\n+ />\n+\n+ <Input\n+ type=\"text\"\n+ error={!!error}\n+ className=\"w-full\"\n+ placeholder=\"End\"\n+ onClick={(e) => e.stopPropagation()}\n+ onChange={(e) => setMaxString(e.target.value)}\n+ onBlur={handleRangeBlur}\n+ disabled={submitState === 'LOADING'}\n+ value={maxString}\n+ />\n+ <Button\n+ className=\"hidden whitespace-nowrap sm:inline-flex\"\n+ color=\"indigo-outline\"\n+ onClick={generateRanges}\n+ loading={isGeneratingRanges}\n+ disabled={\n+ !question ||\n+ isGeneratingRanges ||\n+ minString === '' ||\n+ maxString === '' ||\n+ minMaxError\n+ }\n+ >\n+ {answers.length > 0\n+ ? 'Regenerate date ranges'\n+ : 'Generate date ranges'}\n+ </Button>\n+ </Row>\n+ </Col>\n+ </Row>\n+ <Row className=\"mb-2 w-full gap-2 sm:hidden\">\n+ <Button\n+ color=\"indigo-outline\"\n+ onClick={generateRanges}\n+ loading={isGeneratingRanges}\n+ disabled={\n+ !question ||\n+ isGeneratingRanges ||\n+ minString === '' ||\n+ maxString === '' ||\n+ minMaxError\n+ }\n+ >\n+ {answers.length > 0\n+ ? 'Regenerate date ranges'\n+ : 'Generate date ranges'}\n+ </Button>\n+ </Row>\n+ {error && (\n+ <div className=\"text-scarlet-500 mb-2 mt-2 text-sm\">{error}</div>\n+ )}\n+\n+ <ControlledTabs\n+ activeIndex={titles.indexOf(selectedTab)}\n+ onClick={(title) => {\n+ handleTabChange(title.toLowerCase() as 'thresholds' | 'buckets')\n+ }}\n+ tabs={tabs}\n+ />\n+ {regenerateError && (\n+ <div className=\"text-scarlet-500 mb-2 mt-2 text-sm\">\n+ {regenerateError}\n+ </div>\n+ )}\n+ {maxAnswersReached && (\n+ <div className=\"mb-2 mt-2 text-sm text-amber-500\">\n+ Maximum of {MAX_MULTI_NUMERIC_ANSWERS} answers reached.\n+ </div>\n+ )}\n+ </Col>\n+ )\n+}\n"
|
|
396
|
-
},
|
|
397
|
-
{
|
|
398
|
-
"path": "web/components/new-contract/number-range-section.tsx",
|
|
399
|
-
"status": "modified",
|
|
400
|
-
"diff": "Index: web/components/new-contract/number-range-section.tsx\n===================================================================\n--- web/components/new-contract/number-range-section.tsx\td70bc7a (parent)\n+++ web/components/new-contract/number-range-section.tsx\te96bced (commit)\n@@ -7,9 +7,9 @@\n getMultiNumericAnswerBucketRangeNames,\n getMultiNumericAnswerBucketRanges,\n } from 'common/src/number'\n import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'\n-import { MULTI_NUMERIC_BUCKETS_MAX } from 'common/contract'\n+import { NUMBER_BUCKETS_MAX } from 'common/contract'\n import ShortToggle from 'web/components/widgets/short-toggle'\n import { track } from 'web/lib/service/analytics'\n import { useEvent } from 'client-common/hooks/use-event'\n \n@@ -38,9 +38,9 @@\n paramsKey,\n } = props\n const boundsDefined = min !== undefined && max !== undefined\n const highestPrecision = boundsDefined\n- ? (max - min) / MULTI_NUMERIC_BUCKETS_MAX\n+ ? (max - min) / NUMBER_BUCKETS_MAX\n : undefined\n const lowestPrecision = boundsDefined ? (max - min) / 2 : undefined\n const numberOfBuckets = boundsDefined\n ? getMultiNumericAnswerBucketRangeNames(min, max, precision ?? 1).length\n"
|
|
401
|
-
},
|
|
402
|
-
{
|
|
403
|
-
"path": "web/components/outcome-label.tsx",
|
|
404
|
-
"status": "modified",
|
|
405
|
-
"diff": "Index: web/components/outcome-label.tsx\n===================================================================\n--- web/components/outcome-label.tsx\td70bc7a (parent)\n+++ web/components/outcome-label.tsx\te96bced (commit)\n@@ -85,9 +85,9 @@\n </span>\n )\n }\n \n- if (outcomeType === 'MULTIPLE_CHOICE' || outcomeType === 'MULTI_NUMERIC') {\n+ if (mechanism === 'cpmm-multi-1') {\n return (\n <span>\n {answer && (\n <MultiOutcomeLabel\n"
|
|
406
|
-
}
|
|
407
|
-
]
|
|
408
|
-
},
|
|
409
|
-
{
|
|
410
|
-
"id": "add-free-markets",
|
|
411
|
-
"sha": "ddb266fa95162854d4de4dbd7ea4fee3d8ae25d5",
|
|
412
|
-
"parentSha": "7173b73b777cc2871f29a10e958533e447c58435",
|
|
413
|
-
"spec": "Implement a free-market creation feature and update ante generation across backend and web:\n\n1) Add a new constant for the free-market user\n- File: common/src/economy.ts\n- Export a new constant FREE_MARKET_USER_ID set to the provided user ID string. Ensure it is exported alongside existing economy constants so both backend and web can import it.\n\n2) Allow free creation for the special user in the API and fund via house provider\n- File: backend/api/src/create-market.ts\n- Imports:\n - Add FREE_MARKET_USER_ID from common/economy.\n - Add isProd from shared/utils.\n - Add DEV_HOUSE_LIQUIDITY_PROVIDER_ID and HOUSE_LIQUIDITY_PROVIDER_ID from common/antes.\n- Compute ante and totalMarketCost as usual.\n- Introduce a boolean isFree that is true if auth.uid equals FREE_MARKET_USER_ID and totalMarketCost <= 100.\n- Replace the existing balance check to:\n - If not isFree and totalMarketCost > user.balance, return 403 with the existing balance error.\n - If isFree, enforce a daily limit: query the database for count of contracts where creator_id = auth.uid and created_time > now() - interval '1 day'. If count >= 5, return 403 with the message: \"Tumblingnomics has reached its breaking point. No more free markets today.\"\n- After inserting the new contract (and any answers), before transferring the ante, determine the liquidity provider:\n - const house = isProd() ? HOUSE_LIQUIDITY_PROVIDER_ID : DEV_HOUSE_LIQUIDITY_PROVIDER_ID\n - const providerId = isFree ? house : userId\n- Use providerId as fromId in the CREATE_CONTRACT_ANTE transaction (runTxnOutsideBetQueue) instead of the userId.\n- Update the generateAntes call to match the new signature detailed below, passing providerId and removing the outcomeType argument.\n\n3) Simplify generateAntes signature to remove outcomeType and update logic\n- File: backend/shared/src/create-contract-helpers.ts\n- Remove OutcomeType import and parameter from generateAntes. The new signature should be:\n - generateAntes(pg: SupabaseDirectClient, providerId: string, contract: Contract, ante: number, totalMarketCost: number)\n- Implementation requirements:\n - For cpmm-multi-1 without shouldAnswersSumToOne, iterate answers and insert initial ante liquidity per answer using getCpmmInitialLiquidity with providerId.\n - Else for cpmm-multi-1 or cpmm-1, insert a single initial ante liquidity using getCpmmInitialLiquidity with providerId.\n - Compute drizzledAmount = totalMarketCost - ante; if > 0 and mechanism is cpmm-1 or cpmm-multi-1, run a transaction to:\n - Transfer drizzledAmount from providerId to contract (category 'ADD_SUBSIDY', token 'M$').\n - Insert a new liquidity provision for drizzledAmount via getNewLiquidityProvision.\n - Update the contract subsidyPool and totalLiquidity by drizzledAmount.\n\n4) Update all generateAntes call sites to match the new signature\n- File: backend/api/src/create-market.ts\n - Update the call to: await generateAntes(tx, providerId, contract, ante, totalMarketCost)\n- File: backend/scripts/reimburse-broken-markets.ts\n - Update the import to use the shared helper (shared/create-contract-helpers) if needed, and update the call to remove the outcomeType argument: await generateAntes(tx, c.user_id, contract, c.amount, c.amount)\n\n5) Update the web creation form to allow free creation for the special user\n- File: web/components/new-contract/contract-params-form.tsx\n- Imports:\n - Add FREE_MARKET_USER_ID from common/economy.\n- After computing ante and other validation flags, compute isFree = creator.id === FREE_MARKET_USER_ID && ante <= 100.\n- Update the isValid expression to allow creation when isFree regardless of balance: replace ante <= balance with (isFree ? true : ante <= balance).\n- When rendering the CostSection, pass a balance prop of 100 if isFree, else the user's actual balance. This keeps the UI from showing 'Insufficient balance' for free markets within the ante cap.\n\nBehavioral outcomes:\n- The specified user can create up to 5 markets per rolling 24 hours without needing balance, as long as the total creation cost (ante + extraLiquidity) is <= 100. These are funded by the house liquidity provider chosen by environment. All other users or higher costs use the normal flow and require sufficient balance.\n- Initial ante and any additional drizzled liquidity are attributed to the providerId (house for free, user otherwise).\n- The UI reflects the free-creation state by bypassing the balance requirement and preventing spurious insufficient balance warnings for the capped free case.",
|
|
414
|
-
"prompt": "Add a free market creation capability for a designated internal user. This user should be able to create up to a handful of markets per day without needing balance when the required liquidity is small, and those markets should be funded by the house account depending on environment. Simplify the ante/liquidity generation helper by removing unnecessary parameters, and update the server to attribute both the initial ante and any extra subsidy to the selected funding account. Finally, update the market creation form so this user can create small-ante markets without tripping balance validation and without seeing an incorrect insufficient balance warning.",
|
|
415
|
-
"supplementalFiles": [
|
|
416
|
-
"backend/shared/src/utils.ts",
|
|
417
|
-
"common/src/antes.ts",
|
|
418
|
-
"backend/shared/src/helpers/add-house-subsidy.ts",
|
|
419
|
-
"backend/api/src/remove-liquidity.ts",
|
|
420
|
-
"web/components/new-contract/cost-section.tsx",
|
|
421
|
-
"backend/scripts/reimburse-broken-markets.ts"
|
|
422
|
-
],
|
|
423
|
-
"fileDiffs": [
|
|
424
|
-
{
|
|
425
|
-
"path": "backend/api/src/create-market.ts",
|
|
426
|
-
"status": "modified",
|
|
427
|
-
"diff": "Index: backend/api/src/create-market.ts\n===================================================================\n--- backend/api/src/create-market.ts\t7173b73 (parent)\n+++ backend/api/src/create-market.ts\tddb266f (commit)\n@@ -20,9 +20,9 @@\n nativeContractColumnsArray,\n NUMBER_CREATION_ENABLED,\n PollVoterVisibility,\n } from 'common/contract'\n-import { getAnte } from 'common/economy'\n+import { FREE_MARKET_USER_ID, getAnte } from 'common/economy'\n import { MAX_GROUPS_PER_MARKET } from 'common/group'\n import { getNewContract } from 'common/new-contract'\n import { getPseudoProbability } from 'common/pseudo-numeric'\n import { STONK_INITIAL_PROB } from 'common/stonk'\n@@ -44,10 +44,15 @@\n import {\n addGroupToContract,\n canUserAddGroupToMarket,\n } from 'shared/update-group-contracts-internal'\n-import { contractColumnsToSelect, htmlToRichText, log } from 'shared/utils'\n import {\n+ contractColumnsToSelect,\n+ htmlToRichText,\n+ isProd,\n+ log,\n+} from 'shared/utils'\n+import {\n broadcastNewAnswer,\n broadcastNewContract,\n } from 'shared/websockets/helpers'\n import { APIError, AuthedUser, type APIHandler } from './helpers/endpoint'\n@@ -60,8 +65,12 @@\n import { betsQueue } from 'shared/helpers/fn-queue'\n import { convertUser } from 'common/supabase/users'\n import { camelCase, first } from 'lodash'\n import { getMultiNumericAnswerBucketRangeNames } from 'common/number'\n+import {\n+ DEV_HOUSE_LIQUIDITY_PROVIDER_ID,\n+ HOUSE_LIQUIDITY_PROVIDER_ID,\n+} from 'common/antes'\n type Body = ValidatedAPIParams<'market'>\n \n export const createMarket: APIHandler<'market'> = async (body, auth) => {\n const pg = createSupabaseDirectClient()\n@@ -181,10 +190,22 @@\n const user = first(userAndSlugResult[0].map(convertUser))\n if (!user) throw new APIError(401, 'Your account was not found')\n if (user.isBannedFromPosting) throw new APIError(403, 'You are banned')\n \n- if (totalMarketCost > user.balance)\n+ const isFree = userId === FREE_MARKET_USER_ID && totalMarketCost <= 100\n+ if (!isFree && totalMarketCost > user.balance)\n throw new APIError(403, `Balance must be at least ${totalMarketCost}.`)\n+ if (isFree) {\n+ const contractsToday = await tx.oneOrNone(\n+ `select count(*) from contracts where creator_id = $1 and created_time > now() - interval '1 day'`,\n+ [userId]\n+ )\n+ if (contractsToday && contractsToday.count >= 5)\n+ throw new APIError(\n+ 403,\n+ 'Tumblingnomics has reached its breaking point. No more free markets today.'\n+ )\n+ }\n \n const slug = getSlug(!!first(userAndSlugResult[1]), proposedSlug)\n \n const contract = getNewContract(\n@@ -254,10 +275,14 @@\n \n if (result[1].length > 0 && contract.mechanism === 'cpmm-multi-1') {\n contract.answers = result[1].map(convertAnswer)\n }\n+ const house = isProd()\n+ ? HOUSE_LIQUIDITY_PROVIDER_ID\n+ : DEV_HOUSE_LIQUIDITY_PROVIDER_ID\n+ const providerId = isFree ? house : userId\n await runTxnOutsideBetQueue(tx, {\n- fromId: userId,\n+ fromId: providerId,\n fromType: 'USER',\n toId: contract.id,\n toType: 'CONTRACT',\n amount: ante,\n@@ -271,16 +296,9 @@\n question,\n ante: ante || 0,\n })\n \n- await generateAntes(\n- tx,\n- userId,\n- contract,\n- outcomeType,\n- ante,\n- totalMarketCost\n- )\n+ await generateAntes(tx, providerId, contract, ante, totalMarketCost)\n \n return { contract, user }\n })\n }, [userId])\n"
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
"path": "backend/shared/src/create-contract-helpers.ts",
|
|
431
|
-
"status": "modified",
|
|
432
|
-
"diff": "Index: backend/shared/src/create-contract-helpers.ts\n===================================================================\n--- backend/shared/src/create-contract-helpers.ts\t7173b73 (parent)\n+++ backend/shared/src/create-contract-helpers.ts\tddb266f (commit)\n@@ -1,12 +1,7 @@\n import { getNewLiquidityProvision } from 'common/add-liquidity'\n import { getCpmmInitialLiquidity } from 'common/antes'\n-import {\n- BinaryContract,\n- Contract,\n- CPMMMultiContract,\n- OutcomeType,\n-} from 'common/contract'\n+import { BinaryContract, Contract, CPMMMultiContract } from 'common/contract'\n import { updateContract } from './supabase/contracts'\n import { SupabaseDirectClient } from './supabase/init'\n import { insertLiquidity } from './supabase/liquidity'\n import { FieldVal } from './supabase/utils'\n@@ -15,9 +10,8 @@\n export async function generateAntes(\n pg: SupabaseDirectClient,\n providerId: string,\n contract: Contract,\n- outcomeType: OutcomeType,\n ante: number,\n totalMarketCost: number\n ) {\n if (\n"
|
|
433
|
-
},
|
|
434
|
-
{
|
|
435
|
-
"path": "common/src/economy.ts",
|
|
436
|
-
"status": "modified",
|
|
437
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\t7173b73 (parent)\n+++ common/src/economy.ts\tddb266f (commit)\n@@ -164,4 +164,5 @@\n export const PROFIT_FEE_FRACTION = 0.1\n export const BOOST_COST_MANA = 10000\n export const DEV_BOOST_STRIPE_PRICE_ID = 'price_1QuI5BGdoFKoCJW7lMjCIuKW'\n export const PROD_BOOST_STRIPE_PRICE_ID = 'price_1QuItEGdoFKoCJW7t9qtiGoD'\n+export const FREE_MARKET_USER_ID = '6hHpzvRG0pMq8PNJs7RZj2qlZGn2'\n"
|
|
438
|
-
},
|
|
439
|
-
{
|
|
440
|
-
"path": "web/components/new-contract/contract-params-form.tsx",
|
|
441
|
-
"status": "modified",
|
|
442
|
-
"diff": "Index: web/components/new-contract/contract-params-form.tsx\n===================================================================\n--- web/components/new-contract/contract-params-form.tsx\t7173b73 (parent)\n+++ web/components/new-contract/contract-params-form.tsx\tddb266f (commit)\n@@ -16,8 +16,9 @@\n PollVoterVisibility,\n contractPath,\n } from 'common/contract'\n import {\n+ FREE_MARKET_USER_ID,\n getAnte,\n getUniqueBettorBonusAmount,\n MINIMUM_BOUNTY,\n } from 'common/economy'\n@@ -433,12 +434,13 @@\n const midpointsError =\n outcomeType === 'MULTI_NUMERIC' || outcomeType === 'DATE'\n ? midpoints.length !== answers.length\n : false\n+ const isFree = creator.id === FREE_MARKET_USER_ID && ante <= 100\n \n const isValid =\n isValidQuestion &&\n- ante <= balance &&\n+ (isFree ? true : ante <= balance) &&\n isValidDate &&\n isValidTopics &&\n (outcomeType !== 'PSEUDO_NUMERIC' ||\n (initialValue !== undefined &&\n@@ -1086,9 +1088,9 @@\n }}\n />\n </Row>\n <CostSection\n- balance={balance}\n+ balance={isFree ? 100 : balance}\n numAnswers={numAnswers}\n outcomeType={outcomeType}\n liquidityTier={liquidityTier}\n setLiquidityTier={setLiquidityTier}\n"
|
|
443
|
-
}
|
|
444
|
-
]
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
"id": "add-last-active",
|
|
448
|
-
"sha": "b62da9abba0cb1429828ddbe19279be30c00949f",
|
|
449
|
-
"parentSha": "cba2ea1b6f187af3d36865310719c3c17bbe91c7",
|
|
450
|
-
"spec": "Implement a new API endpoint and update the user hovercard to show a user's last active time.\n\n1) Add a new GET API endpoint definition\n- File: common/src/api/schema.ts\n- Add a new entry named 'get-user-last-active-time' with:\n - method: GET\n - authed: false\n - visibility: undocumented\n - cache: use the default cache strategy constant already used for other GET endpoints\n - props: an object with a single userId: string\n - returns: an object { lastActiveTime: number | null }\n\n2) Create the backend handler for the endpoint\n- File: backend/api/src/get-user-last-active-time.ts (new)\n- Export a handler typed as APIHandler<'get-user-last-active-time'>\n- Accept the props containing userId and create a direct supabase client\n- Run a single SQL query against user_contract_views to compute the maximum activity time in milliseconds, using greatest over the three last_*_ts columns converted to millis via ts_to_millis, filtering rows by the provided userId and ensuring at least one of the three timestamps is non-null; order by that computed value descending and take the first row, returning the computed value as lastActiveTime, or null if none exists\n- Return { lastActiveTime: number | null } from the handler\n\n3) Register the new handler in the API routes map\n- File: backend/api/src/routes.ts\n- Import the handler from './get-user-last-active-time'\n- Add a new entry mapping the 'get-user-last-active-time' API path to the imported handler in the handlers object\n\n4) Update the user hovercard to display the last active time\n- File: web/components/user/user-hovercard.tsx\n- Import and use the client hook to GET the new endpoint with { userId }\n- Compute lastActiveTime in the component as the maximum of the returned lastActiveTime and the user's lastBetTime (if present)\n- Replace the previous admin-only Last [trade term] display with an unconditional Last active display that shows a relative timestamp when lastActiveTime is non-zero, or 'Never' otherwise\n- Remove any no-longer-used imports and variables tied to the old Last [trade term] UI section\n\nExpected behavior:\n- The API endpoint returns the latest user activity time derived from user_contract_views (or null if none)\n- The user hovercard shows a 'Last active:' line for all viewers, based on the latest of view activity and lastBetTime, using the existing relative timestamp component\n- No authentication is required to fetch this data, and the endpoint uses the default caching strategy",
|
|
451
|
-
"prompt": "Add a public GET endpoint that returns a user's last active time and show it on the user hovercard. The endpoint should compute the latest activity timestamp based on a user's contract view times and return it as milliseconds since epoch (or null if not available). Register the endpoint in the backend routes. Then update the user hovercard to query this endpoint for the viewed user and render a 'Last active' line that shows a relative time based on the maximum of the returned value and the user's last bet time. Remove the previous admin-only last bet display and make the 'Last active' visible to all users.",
|
|
452
|
-
"supplementalFiles": [
|
|
453
|
-
"backend/api/src/helpers/endpoint.ts",
|
|
454
|
-
"web/hooks/use-api-getter.ts",
|
|
455
|
-
"client-common/src/hooks/use-api-getter.ts",
|
|
456
|
-
"web/components/relative-timestamp.tsx",
|
|
457
|
-
"backend/shared/src/supabase/init.ts",
|
|
458
|
-
"backend/supabase/user_contract_views.sql"
|
|
459
|
-
],
|
|
460
|
-
"fileDiffs": [
|
|
461
|
-
{
|
|
462
|
-
"path": "backend/api/src/get-user-last-active-time.ts",
|
|
463
|
-
"status": "added",
|
|
464
|
-
"diff": "Index: backend/api/src/get-user-last-active-time.ts\n===================================================================\n--- backend/api/src/get-user-last-active-time.ts\tcba2ea1 (parent)\n+++ backend/api/src/get-user-last-active-time.ts\tb62da9a (commit)\n@@ -1,1 +1,33 @@\n-[NEW FILE]\n\\n+import { APIHandler } from 'api/helpers/endpoint'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+\n+export const getUserLastActiveTime: APIHandler<\n+ 'get-user-last-active-time'\n+> = async (body) => {\n+ const { userId } = body\n+ const pg = createSupabaseDirectClient()\n+\n+ const result = await pg.oneOrNone(\n+ `select greatest(\n+ coalesce(ts_to_millis(last_card_view_ts), 0),\n+ coalesce(ts_to_millis(last_page_view_ts), 0), \n+ coalesce(ts_to_millis(last_promoted_view_ts), 0)\n+ ) as last_active_time\n+ from user_contract_views \n+ where user_id = $1\n+ and (last_card_view_ts is not null \n+ or last_page_view_ts is not null \n+ or last_promoted_view_ts is not null)\n+ order by greatest(\n+ coalesce(ts_to_millis(last_card_view_ts), 0),\n+ coalesce(ts_to_millis(last_page_view_ts), 0), \n+ coalesce(ts_to_millis(last_promoted_view_ts), 0)\n+ ) desc\n+ limit 1`,\n+ [userId]\n+ )\n+\n+ return {\n+ lastActiveTime: result?.last_active_time || null,\n+ }\n+}\n"
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
"path": "backend/api/src/routes.ts",
|
|
468
|
-
"status": "modified",
|
|
469
|
-
"diff": "Index: backend/api/src/routes.ts\n===================================================================\n--- backend/api/src/routes.ts\tcba2ea1 (parent)\n+++ backend/api/src/routes.ts\tb62da9a (commit)\n@@ -174,8 +174,9 @@\n import { dismissUserReport } from './dismiss-user-report'\n import { followPost } from './follow-post'\n import { editPostComment, updatePostComment } from './edit-post-comment'\n import { getUserComments } from './get-comments'\n+import { getUserLastActiveTime } from './get-user-last-active-time'\n export const handlers: { [k in APIPath]: APIHandler<k> } = {\n 'refresh-all-clients': refreshAllClients,\n bet: placeBet,\n 'multi-bet': placeMultiBet,\n@@ -359,5 +360,6 @@\n 'dismiss-user-report': dismissUserReport,\n 'follow-post': followPost,\n 'edit-post-comment': editPostComment,\n 'user-comments': getUserComments,\n+ 'get-user-last-active-time': getUserLastActiveTime,\n } as const\n"
|
|
470
|
-
},
|
|
471
|
-
{
|
|
472
|
-
"path": "common/src/api/schema.ts",
|
|
473
|
-
"status": "modified",
|
|
474
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\tcba2ea1 (parent)\n+++ common/src/api/schema.ts\tb62da9a (commit)\n@@ -183,12 +183,14 @@\n method: 'POST',\n visibility: 'public',\n authed: true,\n returns: {} as { success: boolean },\n- props: z.object({ \n- commentPath: z.string(),\n- action: z.enum(['hide', 'delete']).optional().default('hide')\n- }).strict(),\n+ props: z\n+ .object({\n+ commentPath: z.string(),\n+ action: z.enum(['hide', 'delete']).optional().default('hide'),\n+ })\n+ .strict(),\n },\n 'pin-comment': {\n method: 'POST',\n visibility: 'undocumented',\n@@ -2376,8 +2378,20 @@\n })\n .strict(),\n returns: {} as { success: boolean },\n },\n+ 'get-user-last-active-time': {\n+ method: 'GET',\n+ authed: false,\n+ visibility: 'undocumented',\n+ cache: DEFAULT_CACHE_STRATEGY,\n+ props: z\n+ .object({\n+ userId: z.string(),\n+ })\n+ .strict(),\n+ returns: {} as { lastActiveTime: number | null },\n+ },\n } as const)\n \n export type APIPath = keyof typeof API\n export type APISchema<N extends APIPath> = (typeof API)[N]\n"
|
|
475
|
-
},
|
|
476
|
-
{
|
|
477
|
-
"path": "web/components/user/user-hovercard.tsx",
|
|
478
|
-
"status": "modified",
|
|
479
|
-
"diff": "Index: web/components/user/user-hovercard.tsx\n===================================================================\n--- web/components/user/user-hovercard.tsx\tcba2ea1 (parent)\n+++ web/components/user/user-hovercard.tsx\tb62da9a (commit)\n@@ -10,10 +10,10 @@\n import { RelativeTimestampNoTooltip } from '../relative-timestamp'\n import dayjs from 'dayjs'\n import { Col } from '../layout/col'\n import { FullUser } from 'common/api/user-types'\n-import { TRADE_TERM } from 'common/envs/constants'\n import { SimpleCopyTextButton } from 'web/components/buttons/copy-link-button'\n+import { useAPIGetter } from 'web/hooks/use-api-getter'\n import {\n autoUpdate,\n flip,\n offset,\n@@ -96,8 +96,15 @@\n \n const followingIds = useFollows(userId)\n const followerIds = useFollowers(userId)\n const isMod = useAdminOrMod()\n+ const { data: lastActiveData } = useAPIGetter('get-user-last-active-time', {\n+ userId,\n+ })\n+ const lastActiveTime = Math.max(\n+ lastActiveData?.lastActiveTime ?? 0,\n+ user?.lastBetTime ?? 0\n+ )\n \n return user ? (\n <div\n ref={ref}\n@@ -157,23 +164,21 @@\n </Row>\n </Col>\n </div>\n \n- {isMod && (\n- <div className=\"py-1\">\n- <div className=\"text-ink-700 block px-4 py-2 text-sm\">\n- <span className=\"font-semibold\">Last {TRADE_TERM}:</span>{' '}\n- {user.lastBetTime ? (\n- <RelativeTimestampNoTooltip\n- time={user.lastBetTime}\n- className=\"text-ink-700\"\n- />\n- ) : (\n- 'Never'\n- )}\n- </div>\n+ <div className=\"py-1\">\n+ <div className=\"text-ink-700 block px-4 py-2 text-sm\">\n+ <span className=\"font-semibold\">Last active:</span>{' '}\n+ {lastActiveTime !== 0 ? (\n+ <RelativeTimestampNoTooltip\n+ time={lastActiveTime}\n+ className=\"text-ink-700\"\n+ />\n+ ) : (\n+ 'Never'\n+ )}\n </div>\n- )}\n+ </div>\n </div>\n ) : null\n }\n )\n"
|
|
480
|
-
}
|
|
481
|
-
]
|
|
482
|
-
},
|
|
483
|
-
{
|
|
484
|
-
"id": "add-mcp-endpoint",
|
|
485
|
-
"sha": "3eac52d4c811dceb380a42d555bf86b58ccf7915",
|
|
486
|
-
"parentSha": "75c29e8308b4f47ca46d03c21a6b9307ce6cd5f5",
|
|
487
|
-
"spec": "Implement an MCP-compatible HTTP endpoint to expose search and data retrieval tools backed by existing API handlers and schemas, and extend search/lite market behavior as follows:\n\n1) Add MCP server and route\n- Dependency: Add @modelcontextprotocol/sdk to backend/api/package.json dependencies.\n- New file: backend/api/src/mcp.ts that:\n - Creates a new Server from @modelcontextprotocol/sdk/server with capabilities.tools set.\n - Implements a ListTools handler exposing four tools with JSON schemas:\n - search-markets: Accepts the same fields as searchProps (common/src/api/market-search-types.ts) but requires term; supports contractType, filter, limit, offset, creatorId, sort. Use the same enum values as the API.\n - get-market: { id: string }.\n - get-user: { username: string }.\n - get-bets: Accepts the same fields as API.bets.props.\n - Implements CallTool handler that:\n - Increments a counter metrics.inc('mcp/request_count', { name }) for each tool invocation.\n - Validates arguments against existing Zod schemas from common/src/api/schema.ts:\n - API['search-markets'].props for search-markets\n - API['market/:id'].props for get-market\n - API['user/:username'].props for get-user\n - API.bets.props for get-bets\n - Calls the corresponding backend handlers:\n - search-markets: backend/api/src/search-contracts.searchMarketsLite with props mapped to include sensible defaults (token='MANA', forYou='0', isPrizeMarket='0') and includeLiteAnswers=true.\n - get-market: backend/api/src/get-market.getMarket({ id })\n - get-user: backend/api/src/get-user.getUser({ username })\n - get-bets: backend/api/src/get-bets.getBets(props)\n - Returns the tool result as JSON-serialized text content in the MCP response.\n - Converts Zod errors to McpError with ErrorCode.InvalidParams and internal errors to McpError with ErrorCode.InternalError.\n - Exports handleMcpRequest(req, res) that:\n - Creates a StreamableHTTPServerTransport, connects the server, forwards the request body, and closes transport/server on response close.\n - Logs and returns a JSON-RPC error with HTTP 500 if an error occurs and headers are not yet sent.\n- Route: In backend/api/src/app.ts, import handleMcpRequest from './mcp' and register:\n - app.post('/v0/mcp', express.json(), allowCorsUnrestricted, handleMcpRequest)\n\n2) Add MCP metric\n- In backend/shared/src/monitoring/metrics.ts, add a new CUSTOM_METRICS entry:\n - 'mcp/request_count': { metricKind: 'CUMULATIVE', valueKind: 'int64Value' }\n\n3) Extend search API schema and behavior\n- In common/src/api/market-search-types.ts:\n - Import coerceBoolean from './zod-types'.\n - Change sort default to 'score' (from 'most-popular').\n - Change token default to 'MANA' (from 'ALL').\n - Add includeLiteAnswers?: coerceBoolean.optional().\n- In backend/api/src/search-contracts.ts:\n - Accept includeLiteAnswers from props in searchMarketsLite and pass it to toLiteMarket(contract, includeLiteAnswers) when mapping.\n - Ensure the multi-query typed map returns items typed as { data: Contract; searchType: SearchTypes }[] for type safety.\n - After constructing the combined results, perform uniqBy(..., 'id'), slice(0, limit), then orderBy using sortFields[sort].sortCallback with direction from sortFields[sort].order.\n\n4) Extend LiteMarket serialization\n- In common/src/api/market-types.ts:\n - Update toLiteMarket signature to toLiteMarket(contract: Contract, includeLiteAnswers?: boolean).\n - When includeLiteAnswers is true and contract.mechanism === 'cpmm-multi-1', include answers: an array of { id, text, probability } for each contract answer (compute probability via getAnswerProbability). Keep the existing LiteMarket shape otherwise.\n\n5) Integration and validation\n- Ensure the new MCP route uses express.json() and allowCorsUnrestricted.\n- Ensure TypeScript builds without errors and no regressions on existing endpoints.\n\nAcceptance criteria\n- POST /v0/mcp accepts valid MCP JSON-RPC requests for search-markets, get-market, get-user, get-bets, validates input per API schemas, executes handlers, and returns the results as JSON in MCP format.\n- Each MCP tool invocation increments the 'mcp/request_count' metric with { name: <tool> }.\n- /v0/search-markets (regular API) supports includeLiteAnswers; when true for cpmm-multi-1 markets, LiteMarket responses include minimal answers.\n- Search defaults for sort and token are updated (sort='score', token='MANA').\n- Updated search ordering logic returns results consistently sorted per sortFields, post-uniquing, within the specified limit.",
|
|
488
|
-
"prompt": "Expose a new MCP HTTP endpoint that lets AI tools search markets and fetch market, user, and bet data using the existing API logic. The endpoint should handle MCP JSON-RPC POST requests, list four tools (search-markets, get-market, get-user, get-bets), validate inputs against the existing API schemas, call the appropriate handlers, return JSON content, and record per-tool request metrics. Also update the market search API to optionally include minimal answers in lite market responses when requested and adjust default search parameters to reflect current behavior. Ensure the new route supports JSON parsing and CORS.",
|
|
489
|
-
"supplementalFiles": [
|
|
490
|
-
"backend/api/src/get-bets.ts",
|
|
491
|
-
"backend/api/src/get-market.ts",
|
|
492
|
-
"backend/api/src/get-user.ts",
|
|
493
|
-
"backend/api/src/helpers/endpoint.ts",
|
|
494
|
-
"backend/api/src/routes.ts",
|
|
495
|
-
"common/src/api/schema.ts",
|
|
496
|
-
"common/src/supabase/bets.ts"
|
|
497
|
-
],
|
|
498
|
-
"fileDiffs": [
|
|
499
|
-
{
|
|
500
|
-
"path": "backend/api/package.json",
|
|
501
|
-
"status": "modified",
|
|
502
|
-
"diff": "Index: backend/api/package.json\n===================================================================\n--- backend/api/package.json\t75c29e8 (parent)\n+++ backend/api/package.json\t3eac52d (commit)\n@@ -30,8 +30,9 @@\n \"@google-cloud/monitoring\": \"4.0.0\",\n \"@google-cloud/secret-manager\": \"4.2.1\",\n \"@google/generative-ai\": \"0.22.0\",\n \"@mendable/firecrawl-js\": \"1.8.5\",\n+ \"@modelcontextprotocol/sdk\": \"1.17.1\",\n \"@supabase/supabase-js\": \"2.38.5\",\n \"@tiptap/core\": \"2.0.0-beta.204\",\n \"@tiptap/extension-image\": \"2.0.0-beta.204\",\n \"@tiptap/extension-link\": \"2.0.0-beta.204\",\n"
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
"path": "backend/api/src/app.ts",
|
|
506
|
-
"status": "modified",
|
|
507
|
-
"diff": "Index: backend/api/src/app.ts\n===================================================================\n--- backend/api/src/app.ts\t75c29e8 (parent)\n+++ backend/api/src/app.ts\t3eac52d (commit)\n@@ -1,18 +1,19 @@\n-import { hrtime } from 'node:process'\n+import { API, type APIPath } from 'common/api/schema'\n+import { APIError, pathWithPrefix } from 'common/api/utils'\n+import { randomString } from 'common/util/random'\n+import { assertUnreachable } from 'common/util/types'\n+import * as compression from 'compression'\n import * as cors from 'cors'\n import * as express from 'express'\n import { ErrorRequestHandler, RequestHandler } from 'express'\n-import { log, metrics } from 'shared/utils'\n-import * as compression from 'compression'\n+import { hrtime } from 'node:process'\n import { withMonitoringContext } from 'shared/monitoring/context'\n-import { APIError, pathWithPrefix } from 'common/api/utils'\n-import { API, type APIPath } from 'common/api/schema'\n-import { assertUnreachable } from 'common/util/types'\n+import { log, metrics } from 'shared/utils'\n import { typedEndpoint } from './helpers/endpoint'\n-import { randomString } from 'common/util/random'\n-import { handlers } from './routes'\n+import { handleMcpRequest } from './mcp'\n import { addOldRoutes } from './old-routes'\n+import { handlers } from './routes'\n \n export const allowCorsUnrestricted: RequestHandler = cors({\n origin: '*',\n maxAge: 86400, // 24 hours\n@@ -127,5 +128,9 @@\n } else {\n assertUnreachable(api, 'Unsupported API method')\n }\n })\n+\n+// Add MCP POST endpoint\n+app.post('/v0/mcp', express.json(), allowCorsUnrestricted, handleMcpRequest)\n+\n addOldRoutes(app)\n"
|
|
508
|
-
},
|
|
509
|
-
{
|
|
510
|
-
"path": "backend/api/src/mcp.ts",
|
|
511
|
-
"status": "added",
|
|
512
|
-
"diff": "Index: backend/api/src/mcp.ts\n===================================================================\n--- backend/api/src/mcp.ts\t75c29e8 (parent)\n+++ backend/api/src/mcp.ts\t3eac52d (commit)\n@@ -0,0 +1,345 @@\n+#!/usr/bin/env node\n+import { Server } from '@modelcontextprotocol/sdk/server/index.js'\n+import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHTTP.js'\n+import {\n+ CallToolRequestSchema,\n+ ErrorCode,\n+ ListToolsRequestSchema,\n+ McpError,\n+} from '@modelcontextprotocol/sdk/types.js'\n+import { API } from 'common/api/schema'\n+import { NON_POINTS_BETS_LIMIT } from 'common/supabase/bets'\n+import { Request, Response } from 'express'\n+import { log, metrics } from 'shared/utils'\n+import { z } from 'zod'\n+import { getBets } from './get-bets'\n+import { getMarket } from './get-market'\n+import { getUser } from './get-user'\n+import { searchMarketsLite } from './search-contracts'\n+\n+function getServer(): Server {\n+ const server = new Server(\n+ {\n+ name: 'manifold-markets',\n+ version: '0.2.0',\n+ },\n+ {\n+ capabilities: {\n+ tools: {},\n+ },\n+ }\n+ )\n+\n+ // List available tools\n+ server.setRequestHandler(ListToolsRequestSchema, async () => ({\n+ tools: [\n+ {\n+ name: 'search-markets',\n+ description: 'Search for prediction markets with optional filters',\n+ inputSchema: {\n+ type: 'object',\n+ properties: {\n+ term: { type: 'string', description: 'Search query' },\n+ contractType: {\n+ type: 'string',\n+ enum: [\n+ 'ALL',\n+ 'BINARY',\n+ 'MULTIPLE_CHOICE',\n+ 'POLL',\n+ 'MULTI_NUMERIC',\n+ 'DATE',\n+ ],\n+ description: 'Question type (default: ALL)',\n+ },\n+ filter: {\n+ type: 'string',\n+ enum: ['open', 'resolved', 'all'],\n+ description:\n+ 'Filter by question state. Resolved means the event has happened. (default: all)',\n+ },\n+ limit: {\n+ type: 'number',\n+ minimum: 1,\n+ maximum: 1000,\n+ description: 'Max number of results (default: 100)',\n+ },\n+ offset: {\n+ type: 'number',\n+ description: 'Offset for pagination (default: 0)',\n+ },\n+ creatorId: {\n+ type: 'string',\n+ description: 'Optional. Creator (user) ID to filter by',\n+ },\n+ sort: {\n+ type: 'string',\n+ enum: ['newest', 'score', 'liquidity'],\n+ description: 'Sort order (default: score)',\n+ },\n+ },\n+ required: ['term'],\n+ },\n+ },\n+ {\n+ name: 'get-market',\n+ description: 'Get detailed information about a specific market',\n+ inputSchema: {\n+ type: 'object',\n+ properties: {\n+ id: { type: 'string', description: 'Market ID' },\n+ },\n+ required: ['id'],\n+ },\n+ },\n+ {\n+ name: 'get-user',\n+ description: 'Get user information by username',\n+ inputSchema: {\n+ type: 'object',\n+ properties: {\n+ username: { type: 'string', description: 'Username' },\n+ },\n+ required: ['username'],\n+ },\n+ },\n+ {\n+ name: 'get-bets',\n+ description:\n+ 'Get bets from markets or for users with various filtering options',\n+ inputSchema: {\n+ type: 'object',\n+ properties: {\n+ id: {\n+ type: 'string',\n+ description: 'Optional. Bet ID to filter by',\n+ },\n+ userId: {\n+ type: 'string',\n+ description: 'Optional. User ID to filter by',\n+ },\n+ username: {\n+ type: 'string',\n+ description: 'Optional. Username to filter by',\n+ },\n+ contractId: {\n+ oneOf: [\n+ { type: 'string' },\n+ { type: 'array', items: { type: 'string' } },\n+ ],\n+ description: 'Optional. Contract ID(s) to filter by',\n+ },\n+ contractSlug: {\n+ type: 'string',\n+ description: 'Optional. Contract slug to filter by',\n+ },\n+ answerId: {\n+ type: 'string',\n+ description: 'Optional. Answer ID to filter by',\n+ },\n+ limit: {\n+ type: 'number',\n+ minimum: 0,\n+ maximum: NON_POINTS_BETS_LIMIT,\n+ description: 'Optional. Number of bets to return (default: 1000)',\n+ },\n+ before: {\n+ type: 'string',\n+ description: 'Optional. Get bets before this bet ID',\n+ },\n+ after: {\n+ type: 'string',\n+ description: 'Optional. Get bets after this bet ID',\n+ },\n+ beforeTime: {\n+ type: 'number',\n+ description: 'Optional. Get bets before this timestamp',\n+ },\n+ afterTime: {\n+ type: 'number',\n+ description: 'Optional. Get bets after this timestamp',\n+ },\n+ order: {\n+ type: 'string',\n+ enum: ['asc', 'desc'],\n+ description: 'Optional. Sort order by creation time',\n+ },\n+ kinds: {\n+ type: 'string',\n+ enum: ['open-limit'],\n+ description: 'Optional. Filter by bet kind',\n+ },\n+ minAmount: {\n+ type: 'number',\n+ minimum: 0,\n+ description: 'Optional. Minimum bet amount',\n+ },\n+ filterRedemptions: {\n+ type: 'boolean',\n+ description: 'Optional. Filter redemptions',\n+ },\n+ },\n+ required: [],\n+ },\n+ },\n+ ],\n+ }))\n+\n+ // Handle tool execution\n+ server.setRequestHandler(CallToolRequestSchema, async (request) => {\n+ const { name, arguments: args } = request.params\n+ metrics.inc('mcp/request_count', { name })\n+ try {\n+ switch (name) {\n+ case 'search-markets': {\n+ const params = API['search-markets'].props.parse(args)\n+\n+ // Map the params to match the search-markets API schema\n+ const searchParams = {\n+ term: params.term,\n+ limit: params.limit,\n+ filter: params.filter,\n+ sort: params.sort,\n+ contractType: params.contractType,\n+ offset: params.offset,\n+ token: 'MANA' as const,\n+ forYou: '0' as const,\n+ isPrizeMarket: '0' as const,\n+ includeLiteAnswers: true,\n+ }\n+\n+ try {\n+ const markets = await searchMarketsLite(\n+ searchParams,\n+ undefined, // auth not required for this endpoint\n+ {} as Request // minimal request object since it's not used\n+ )\n+ return {\n+ content: [\n+ {\n+ type: 'text',\n+ text: JSON.stringify(markets, null, 2),\n+ },\n+ ],\n+ }\n+ } catch (error: any) {\n+ throw new McpError(\n+ ErrorCode.InternalError,\n+ `Search markets error: ${error.message}`\n+ )\n+ }\n+ }\n+\n+ case 'get-market': {\n+ const { id } = API['market/:id'].props.parse(args)\n+\n+ try {\n+ const market = await getMarket({ id })\n+ return {\n+ content: [\n+ {\n+ type: 'text',\n+ text: JSON.stringify(market, null, 2),\n+ },\n+ ],\n+ }\n+ } catch (error: any) {\n+ throw new McpError(\n+ ErrorCode.InternalError,\n+ `Get market error: ${error.message}`\n+ )\n+ }\n+ }\n+\n+ case 'get-user': {\n+ const { username } = API['user/:username'].props.parse(args)\n+\n+ try {\n+ const user = await getUser({ username })\n+ return {\n+ content: [\n+ {\n+ type: 'text',\n+ text: JSON.stringify(user, null, 2),\n+ },\n+ ],\n+ }\n+ } catch (error: any) {\n+ throw new McpError(\n+ ErrorCode.InternalError,\n+ `Get user error: ${error.message}`\n+ )\n+ }\n+ }\n+\n+ case 'get-bets': {\n+ const params = API.bets.props.parse(args)\n+\n+ try {\n+ const bets = await getBets(\n+ params,\n+ undefined, // auth not required for this endpoint\n+ {} as Request // minimal request object since it's not used\n+ )\n+ return {\n+ content: [\n+ {\n+ type: 'text',\n+ text: JSON.stringify(bets, null, 2),\n+ },\n+ ],\n+ }\n+ } catch (error: any) {\n+ throw new McpError(\n+ ErrorCode.InternalError,\n+ `Get bets error: ${error.message}`\n+ )\n+ }\n+ }\n+\n+ default:\n+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`)\n+ }\n+ } catch (error) {\n+ if (error instanceof z.ZodError) {\n+ throw new McpError(\n+ ErrorCode.InvalidParams,\n+ `Invalid parameters: ${error.errors\n+ .map((e) => `${e.path.join('.')}: ${e.message}`)\n+ .join(', ')}`\n+ )\n+ }\n+ throw error\n+ }\n+ })\n+\n+ return server\n+}\n+\n+export const handleMcpRequest = async (req: Request, res: Response) => {\n+ try {\n+ const server = getServer()\n+ const transport: StreamableHTTPServerTransport =\n+ new StreamableHTTPServerTransport({\n+ sessionIdGenerator: undefined,\n+ })\n+ res.on('close', () => {\n+ transport.close()\n+ server.close()\n+ })\n+ await server.connect(transport)\n+ await transport.handleRequest(req, res, req.body)\n+ } catch (error) {\n+ log.error('Error handling MCP request:', { error })\n+ if (!res.headersSent) {\n+ res.status(500).json({\n+ jsonrpc: '2.0',\n+ error: {\n+ code: -32603,\n+ message: 'Internal server error',\n+ },\n+ id: null,\n+ })\n+ }\n+ }\n+}\n"
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
"path": "backend/api/src/search-contracts.ts",
|
|
516
|
-
"status": "modified",
|
|
517
|
-
"diff": "Index: backend/api/src/search-contracts.ts\n===================================================================\n--- backend/api/src/search-contracts.ts\t75c29e8 (parent)\n+++ backend/api/src/search-contracts.ts\t3eac52d (commit)\n@@ -1,30 +1,32 @@\n-import { z } from 'zod'\n+import { searchProps } from 'common/api/market-search-types'\n+import { toLiteMarket } from 'common/api/market-types'\n+import { Contract } from 'common/contract'\n+import { convertContract } from 'common/supabase/contracts'\n+import { orderBy, uniqBy } from 'lodash'\n+import { getGroupIdFromSlug } from 'shared/supabase/groups'\n import {\n createSupabaseDirectClient,\n SupabaseDirectClient,\n } from 'shared/supabase/init'\n-import { type APIHandler } from './helpers/endpoint'\n import {\n- getSearchContractSQL,\n- getForYouSQL,\n- sortFields,\n basicSearchSQL,\n+ getForYouSQL,\n+ getSearchContractSQL,\n SearchTypes,\n+ sortFields,\n } from 'shared/supabase/search-contracts'\n-import { getGroupIdFromSlug } from 'shared/supabase/groups'\n-import { orderBy, uniqBy } from 'lodash'\n-import { convertContract } from 'common/supabase/contracts'\n import { log } from 'shared/utils'\n-import { toLiteMarket } from 'common/api/market-types'\n-import { searchProps } from 'common/api/market-search-types'\n+import { z } from 'zod'\n+import { type APIHandler } from './helpers/endpoint'\n \n export const searchMarketsLite: APIHandler<'search-markets'> = async (\n props,\n auth\n ) => {\n+ const { includeLiteAnswers } = props\n const contracts = await search(props, auth?.uid)\n- return contracts.map(toLiteMarket)\n+ return contracts.map((c) => toLiteMarket(c, includeLiteAnswers))\n }\n \n export const searchMarketsFull: APIHandler<'search-markets-full'> = async (\n props,\n@@ -146,13 +148,14 @@\n contractsWithoutStopwords,\n contractsWithMatchingAnswers,\n contractsWithStopwords,\n contractDescriptionMatches,\n- ] = results.map((result, i) =>\n- result.map((r: any) => ({\n- data: convertContract(r),\n- searchType: searchTypes[i],\n- }))\n+ ] = results.map(\n+ (result, i) =>\n+ result.map((r: any) => ({\n+ data: convertContract(r),\n+ searchType: searchTypes[i],\n+ })) as { data: Contract; searchType: SearchTypes }[]\n )\n \n const contractsOfSimilarRelevance = orderBy(\n [\n@@ -165,16 +168,20 @@\n (c.searchType === 'answer' ? 0.5 : 1),\n sortFields[sort].order.includes('DESC') ? 'desc' : 'asc'\n )\n \n- return uniqBy(\n- [\n- ...contractsWithStopwords,\n- ...contractsOfSimilarRelevance,\n- ...contractDescriptionMatches,\n- ].map((c) => c.data),\n- 'id'\n- ).slice(0, limit)\n+ return orderBy(\n+ uniqBy(\n+ [\n+ ...contractsWithStopwords, // most obviously relevant\n+ ...contractsOfSimilarRelevance, // next most relevant\n+ ...contractDescriptionMatches, // least obviously relevant\n+ ].map((c) => c.data),\n+ 'id'\n+ ).slice(0, limit),\n+ (c) => sortFields[sort].sortCallback(c),\n+ sortFields[sort].order.includes('DESC') ? 'desc' : 'asc'\n+ )\n }\n }\n \n const getAllSubTopicsForParentTopicIds = async (\n"
|
|
518
|
-
},
|
|
519
|
-
{
|
|
520
|
-
"path": "backend/shared/src/monitoring/metrics.ts",
|
|
521
|
-
"status": "modified",
|
|
522
|
-
"diff": "Index: backend/shared/src/monitoring/metrics.ts\n===================================================================\n--- backend/shared/src/monitoring/metrics.ts\t75c29e8 (parent)\n+++ backend/shared/src/monitoring/metrics.ts\t3eac52d (commit)\n@@ -1,5 +1,5 @@\n-import { isEqual, flatten } from 'lodash'\n+import { flatten, isEqual } from 'lodash'\n \n // see https://cloud.google.com/monitoring/api/ref_v3/rest/v3/projects.metricDescriptors#MetricKind\n export type MetricKind = 'GAUGE' | 'CUMULATIVE'\n \n@@ -90,8 +90,12 @@\n 'vercel/revalidations_failed': {\n metricKind: 'CUMULATIVE',\n valueKind: 'int64Value',\n },\n+ 'mcp/request_count': {\n+ metricKind: 'CUMULATIVE',\n+ valueKind: 'int64Value',\n+ },\n } as const satisfies { [k: string]: MetricDescriptor }\n \n // the typing for all this could be way fancier, but seems overkill\n \n"
|
|
523
|
-
},
|
|
524
|
-
{
|
|
525
|
-
"path": "common/src/api/market-search-types.ts",
|
|
526
|
-
"status": "modified",
|
|
527
|
-
"diff": "Index: common/src/api/market-search-types.ts\n===================================================================\n--- common/src/api/market-search-types.ts\t75c29e8 (parent)\n+++ common/src/api/market-search-types.ts\t3eac52d (commit)\n@@ -1,5 +1,6 @@\n import { z } from 'zod'\n+import { coerceBoolean } from './zod-types'\n \n export const FIRESTORE_DOC_REF_ID_REGEX = /^[a-zA-Z0-9_-]{1,}$/\n \n export const searchProps = z\n@@ -36,9 +37,9 @@\n z.literal('bounty-amount'),\n z.literal('prob-descending'),\n z.literal('prob-ascending'),\n ])\n- .default('most-popular'),\n+ .default('score'),\n contractType: z\n .union([\n z.literal('ALL'),\n z.literal('BINARY'),\n@@ -77,10 +78,11 @@\n z.literal('CASH'),\n z.literal('ALL'),\n z.literal('CASH_AND_MANA'),\n ])\n- .default('ALL'),\n+ .default('MANA'),\n gids: z.string().optional(),\n liquidity: z.coerce.number().optional(),\n hasBets: z.union([z.literal('1'), z.literal('0')]).optional(),\n+ includeLiteAnswers: coerceBoolean.optional(),\n })\n .strict()\n"
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
"path": "common/src/api/market-types.ts",
|
|
531
|
-
"status": "modified",
|
|
532
|
-
"diff": "Index: common/src/api/market-types.ts\n===================================================================\n--- common/src/api/market-types.ts\t75c29e8 (parent)\n+++ common/src/api/market-types.ts\t3eac52d (commit)\n@@ -10,16 +10,16 @@\n } from 'common/contract'\n import { MINIMUM_BOUNTY } from 'common/economy'\n import { DOMAIN } from 'common/envs/constants'\n import { MAX_ID_LENGTH } from 'common/group'\n+import { MAX_MULTI_NUMERIC_ANSWERS } from 'common/multi-numeric'\n import { getMappedValue } from 'common/pseudo-numeric'\n+import { liquidityTiers } from 'common/tier'\n import { removeUndefinedProps } from 'common/util/object'\n import { richTextToString } from 'common/util/parse'\n+import { randomStringRegex } from 'common/util/random'\n import { z } from 'zod'\n import { coerceBoolean, contentSchema } from './zod-types'\n-import { randomStringRegex } from 'common/util/random'\n-import { MAX_MULTI_NUMERIC_ANSWERS } from 'common/multi-numeric'\n-import { liquidityTiers } from 'common/tier'\n \n export type LiteMarket = {\n // Unique identifier for this market\n id: string\n@@ -91,9 +91,12 @@\n coverImageUrl?: string\n groupSlugs?: string[]\n }\n \n-export function toLiteMarket(contract: Contract): LiteMarket {\n+export function toLiteMarket(\n+ contract: Contract,\n+ includeLiteAnswers?: boolean\n+): LiteMarket {\n const {\n id,\n creatorId,\n creatorUsername,\n@@ -136,8 +139,16 @@\n const value = getMappedValue(contract, contract.prob)\n const { min, max, isLogScale } = contract\n numericValues = { value, min, max, isLogScale }\n }\n+ const answers =\n+ includeLiteAnswers && contract.mechanism === 'cpmm-multi-1'\n+ ? contract.answers?.map((answer) => ({\n+ id: answer.id,\n+ text: answer.text,\n+ probability: getAnswerProbability(contract, answer.id),\n+ }))\n+ : undefined\n \n return removeUndefinedProps({\n id,\n creatorId,\n@@ -171,8 +182,9 @@\n lastCommentTime,\n ...numericValues,\n token,\n siblingContractId,\n+ answers,\n \n // Manifold love props.\n loverUserId1,\n loverUserId2,\n"
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
"path": "yarn.lock",
|
|
536
|
-
"status": "modified",
|
|
537
|
-
"diff": "Index: yarn.lock\n===================================================================\n--- yarn.lock\t75c29e8 (parent)\n+++ yarn.lock\t3eac52d (commit)\n@@ -2777,8 +2777,26 @@\n typescript-event-target \"^1.1.1\"\n zod \"^3.23.8\"\n zod-to-json-schema \"^3.23.0\"\n \n+\"@modelcontextprotocol/sdk@1.17.1\":\n+ version \"1.17.1\"\n+ resolved \"https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.17.1.tgz#a3628ae2ca0b4a2e6088202b5ee417d884a88537\"\n+ integrity sha512-CPle1OQehbWqd25La9Ack5B07StKIxh4+Bf19qnpZKJC1oI22Y0czZHbifjw1UoczIfKBwBDAp/dFxvHG13B5A==\n+ dependencies:\n+ ajv \"^6.12.6\"\n+ content-type \"^1.0.5\"\n+ cors \"^2.8.5\"\n+ cross-spawn \"^7.0.5\"\n+ eventsource \"^3.0.2\"\n+ eventsource-parser \"^3.0.0\"\n+ express \"^5.0.1\"\n+ express-rate-limit \"^7.5.0\"\n+ pkce-challenge \"^5.0.0\"\n+ raw-body \"^3.0.0\"\n+ zod \"^3.23.8\"\n+ zod-to-json-schema \"^3.24.1\"\n+\n \"@next/env@15.0.4\":\n version \"15.0.4\"\n resolved \"https://registry.yarnpkg.com/@next/env/-/env-15.0.4.tgz#97da0fe3bae2f2b2968c4c925d7936660f5b3836\"\n integrity sha512-WNRvtgnRVDD4oM8gbUcRc27IAhaL4eXQ/2ovGbgLnPGUvdyDr8UdXP4Q/IBDdAdojnD2eScryIDirv0YUCjUVw==\n@@ -6002,8 +6020,16 @@\n integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==\n dependencies:\n event-target-shim \"^5.0.0\"\n \n+accepts@^2.0.0:\n+ version \"2.0.0\"\n+ resolved \"https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895\"\n+ integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==\n+ dependencies:\n+ mime-types \"^3.0.0\"\n+ negotiator \"^1.0.0\"\n+\n accepts@~1.3.5, accepts@~1.3.8:\n version \"1.3.8\"\n resolved \"https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e\"\n integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==\n@@ -6060,9 +6086,9 @@\n integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==\n dependencies:\n humanize-ms \"^1.2.1\"\n \n-ajv@^6.12.3, ajv@^6.12.4:\n+ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.6:\n version \"6.12.6\"\n resolved \"https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4\"\n integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==\n dependencies:\n@@ -6606,8 +6632,23 @@\n raw-body \"2.5.1\"\n type-is \"~1.6.18\"\n unpipe \"1.0.0\"\n \n+body-parser@^2.2.0:\n+ version \"2.2.0\"\n+ resolved \"https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.0.tgz#f7a9656de305249a715b549b7b8fd1ab9dfddcfa\"\n+ integrity sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==\n+ dependencies:\n+ bytes \"^3.1.2\"\n+ content-type \"^1.0.5\"\n+ debug \"^4.4.0\"\n+ http-errors \"^2.0.0\"\n+ iconv-lite \"^0.6.3\"\n+ on-finished \"^2.4.1\"\n+ qs \"^6.14.0\"\n+ raw-body \"^3.0.0\"\n+ type-is \"^2.0.0\"\n+\n boolbase@^1.0.0:\n version \"1.0.0\"\n resolved \"https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e\"\n integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==\n@@ -6692,13 +6733,21 @@\n version \"3.0.0\"\n resolved \"https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048\"\n integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==\n \n-bytes@3.1.2:\n+bytes@3.1.2, bytes@^3.1.2:\n version \"3.1.2\"\n resolved \"https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5\"\n integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==\n \n+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:\n+ version \"1.0.2\"\n+ resolved \"https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6\"\n+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==\n+ dependencies:\n+ es-errors \"^1.3.0\"\n+ function-bind \"^1.1.2\"\n+\n call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5:\n version \"1.0.5\"\n resolved \"https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513\"\n integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==\n@@ -6717,8 +6766,16 @@\n function-bind \"^1.1.2\"\n get-intrinsic \"^1.2.4\"\n set-function-length \"^1.2.1\"\n \n+call-bound@^1.0.2:\n+ version \"1.0.4\"\n+ resolved \"https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a\"\n+ integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==\n+ dependencies:\n+ call-bind-apply-helpers \"^1.0.2\"\n+ get-intrinsic \"^1.3.0\"\n+\n callsites@^3.0.0:\n version \"3.1.0\"\n resolved \"https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73\"\n integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==\n@@ -7023,8 +7080,20 @@\n integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==\n dependencies:\n safe-buffer \"5.2.1\"\n \n+content-disposition@^1.0.0:\n+ version \"1.0.0\"\n+ resolved \"https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2\"\n+ integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==\n+ dependencies:\n+ safe-buffer \"5.2.1\"\n+\n+content-type@^1.0.5:\n+ version \"1.0.5\"\n+ resolved \"https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918\"\n+ integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==\n+\n content-type@~1.0.4:\n version \"1.0.4\"\n resolved \"https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b\"\n integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==\n@@ -7043,13 +7112,23 @@\n version \"1.0.6\"\n resolved \"https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c\"\n integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=\n \n+cookie-signature@^1.2.1:\n+ version \"1.2.2\"\n+ resolved \"https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793\"\n+ integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==\n+\n cookie@0.5.0:\n version \"0.5.0\"\n resolved \"https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b\"\n integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==\n \n+cookie@^0.7.1:\n+ version \"0.7.2\"\n+ resolved \"https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7\"\n+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==\n+\n core-decorators@^0.17.0:\n version \"0.17.0\"\n resolved \"https://registry.yarnpkg.com/core-decorators/-/core-decorators-0.17.0.tgz#3f43180a86d2ab0cc51069f46a1ec3e49e7cebd6\"\n integrity sha512-dBTL931yH4iZRlknHHkqtvPuGiDAEyTcudUnji3W0+mcNIHTrCmXvlqSyE743tzYtIeujLB00H9G/NdAmE3rPg==\n@@ -7075,9 +7154,9 @@\n version \"1.0.3\"\n resolved \"https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85\"\n integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==\n \n-cors@2.8.5:\n+cors@2.8.5, cors@^2.8.5:\n version \"2.8.5\"\n resolved \"https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29\"\n integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==\n dependencies:\n@@ -7131,8 +7210,17 @@\n path-key \"^3.1.0\"\n shebang-command \"^2.0.0\"\n which \"^2.0.1\"\n \n+cross-spawn@^7.0.5:\n+ version \"7.0.6\"\n+ resolved \"https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f\"\n+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==\n+ dependencies:\n+ path-key \"^3.1.0\"\n+ shebang-command \"^2.0.0\"\n+ which \"^2.0.1\"\n+\n crypt@0.0.2:\n version \"0.0.2\"\n resolved \"https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b\"\n integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==\n@@ -7426,8 +7514,15 @@\n integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==\n dependencies:\n ms \"^2.1.1\"\n \n+debug@^4.3.5, debug@^4.4.0:\n+ version \"4.4.1\"\n+ resolved \"https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b\"\n+ integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==\n+ dependencies:\n+ ms \"^2.1.3\"\n+\n decompress-response@^6.0.0:\n version \"6.0.0\"\n resolved \"https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc\"\n integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==\n@@ -7494,9 +7589,9 @@\n version \"1.0.0\"\n resolved \"https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619\"\n integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=\n \n-depd@2.0.0:\n+depd@2.0.0, depd@^2.0.0:\n version \"2.0.0\"\n resolved \"https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df\"\n integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==\n \n@@ -7641,8 +7736,17 @@\n dependencies:\n no-case \"^3.0.4\"\n tslib \"^2.0.3\"\n \n+dunder-proto@^1.0.1:\n+ version \"1.0.1\"\n+ resolved \"https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a\"\n+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==\n+ dependencies:\n+ call-bind-apply-helpers \"^1.0.1\"\n+ es-errors \"^1.3.0\"\n+ gopd \"^1.2.0\"\n+\n duplexify@^4.0.0:\n version \"4.1.2\"\n resolved \"https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0\"\n integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==\n@@ -7701,8 +7805,13 @@\n version \"9.2.2\"\n resolved \"https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72\"\n integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==\n \n+encodeurl@^2.0.0:\n+ version \"2.0.0\"\n+ resolved \"https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58\"\n+ integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==\n+\n encodeurl@~1.0.2:\n version \"1.0.2\"\n resolved \"https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59\"\n integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=\n@@ -7847,8 +7956,13 @@\n integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==\n dependencies:\n get-intrinsic \"^1.2.4\"\n \n+es-define-property@^1.0.1:\n+ version \"1.0.1\"\n+ resolved \"https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa\"\n+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==\n+\n es-errors@^1.2.1, es-errors@^1.3.0:\n version \"1.3.0\"\n resolved \"https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f\"\n integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==\n@@ -7880,8 +7994,15 @@\n integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==\n dependencies:\n es-errors \"^1.3.0\"\n \n+es-object-atoms@^1.1.1:\n+ version \"1.1.1\"\n+ resolved \"https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1\"\n+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==\n+ dependencies:\n+ es-errors \"^1.3.0\"\n+\n es-set-tostringtag@^2.0.1:\n version \"2.0.1\"\n resolved \"https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8\"\n integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==\n@@ -8247,9 +8368,9 @@\n version \"2.0.3\"\n resolved \"https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64\"\n integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==\n \n-etag@~1.8.1:\n+etag@^1.8.1, etag@~1.8.1:\n version \"1.8.1\"\n resolved \"https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887\"\n integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=\n \n@@ -8257,8 +8378,20 @@\n version \"5.0.1\"\n resolved \"https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789\"\n integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==\n \n+eventsource-parser@^3.0.0, eventsource-parser@^3.0.1:\n+ version \"3.0.3\"\n+ resolved \"https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.3.tgz#e9af1d40b77e6268cdcbc767321e8b9f066adea8\"\n+ integrity sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==\n+\n+eventsource@^3.0.2:\n+ version \"3.0.7\"\n+ resolved \"https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989\"\n+ integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==\n+ dependencies:\n+ eventsource-parser \"^3.0.1\"\n+\n execa@^5.0.0:\n version \"5.1.1\"\n resolved \"https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd\"\n integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==\n@@ -8317,8 +8450,13 @@\n integrity sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==\n dependencies:\n basic-auth \"^2.0.1\"\n \n+express-rate-limit@^7.5.0:\n+ version \"7.5.1\"\n+ resolved \"https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz#8c3a42f69209a3a1c969890070ece9e20a879dec\"\n+ integrity sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==\n+\n express@4.18.1:\n version \"4.18.1\"\n resolved \"https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf\"\n integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==\n@@ -8354,8 +8492,41 @@\n type-is \"~1.6.18\"\n utils-merge \"1.0.1\"\n vary \"~1.1.2\"\n \n+express@^5.0.1:\n+ version \"5.1.0\"\n+ resolved \"https://registry.yarnpkg.com/express/-/express-5.1.0.tgz#d31beaf715a0016f0d53f47d3b4d7acf28c75cc9\"\n+ integrity sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==\n+ dependencies:\n+ accepts \"^2.0.0\"\n+ body-parser \"^2.2.0\"\n+ content-disposition \"^1.0.0\"\n+ content-type \"^1.0.5\"\n+ cookie \"^0.7.1\"\n+ cookie-signature \"^1.2.1\"\n+ debug \"^4.4.0\"\n+ encodeurl \"^2.0.0\"\n+ escape-html \"^1.0.3\"\n+ etag \"^1.8.1\"\n+ finalhandler \"^2.1.0\"\n+ fresh \"^2.0.0\"\n+ http-errors \"^2.0.0\"\n+ merge-descriptors \"^2.0.0\"\n+ mime-types \"^3.0.0\"\n+ on-finished \"^2.4.1\"\n+ once \"^1.4.0\"\n+ parseurl \"^1.3.3\"\n+ proxy-addr \"^2.0.7\"\n+ qs \"^6.14.0\"\n+ range-parser \"^1.2.1\"\n+ router \"^2.2.0\"\n+ send \"^1.1.0\"\n+ serve-static \"^2.2.0\"\n+ statuses \"^2.0.1\"\n+ type-is \"^2.0.1\"\n+ vary \"^1.1.2\"\n+\n extend@^3.0.2, extend@~3.0.2:\n version \"3.0.2\"\n resolved \"https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa\"\n integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==\n@@ -8514,8 +8685,20 @@\n parseurl \"~1.3.3\"\n statuses \"2.0.1\"\n unpipe \"~1.0.0\"\n \n+finalhandler@^2.1.0:\n+ version \"2.1.0\"\n+ resolved \"https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f\"\n+ integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==\n+ dependencies:\n+ debug \"^4.4.0\"\n+ encodeurl \"^2.0.0\"\n+ escape-html \"^1.0.3\"\n+ on-finished \"^2.4.1\"\n+ parseurl \"^1.3.3\"\n+ statuses \"^2.0.1\"\n+\n find-up@^4.0.0, find-up@^4.1.0:\n version \"4.1.0\"\n resolved \"https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19\"\n integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==\n@@ -8715,8 +8898,13 @@\n version \"0.5.2\"\n resolved \"https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7\"\n integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=\n \n+fresh@^2.0.0:\n+ version \"2.0.0\"\n+ resolved \"https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4\"\n+ integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==\n+\n fs-constants@^1.0.0:\n version \"1.0.0\"\n resolved \"https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad\"\n integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==\n@@ -8831,13 +9019,37 @@\n has-proto \"^1.0.1\"\n has-symbols \"^1.0.3\"\n hasown \"^2.0.0\"\n \n+get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:\n+ version \"1.3.0\"\n+ resolved \"https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01\"\n+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==\n+ dependencies:\n+ call-bind-apply-helpers \"^1.0.2\"\n+ es-define-property \"^1.0.1\"\n+ es-errors \"^1.3.0\"\n+ es-object-atoms \"^1.1.1\"\n+ function-bind \"^1.1.2\"\n+ get-proto \"^1.0.1\"\n+ gopd \"^1.2.0\"\n+ has-symbols \"^1.1.0\"\n+ hasown \"^2.0.2\"\n+ math-intrinsics \"^1.1.0\"\n+\n get-package-type@^0.1.0:\n version \"0.1.0\"\n resolved \"https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a\"\n integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==\n \n+get-proto@^1.0.1:\n+ version \"1.0.1\"\n+ resolved \"https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1\"\n+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==\n+ dependencies:\n+ dunder-proto \"^1.0.1\"\n+ es-object-atoms \"^1.0.0\"\n+\n get-stream@^6.0.0:\n version \"6.0.1\"\n resolved \"https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7\"\n integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==\n@@ -9074,8 +9286,13 @@\n integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==\n dependencies:\n get-intrinsic \"^1.1.3\"\n \n+gopd@^1.2.0:\n+ version \"1.2.0\"\n+ resolved \"https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1\"\n+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==\n+\n graceful-fs@^4.1.9, graceful-fs@^4.2.4, graceful-fs@^4.2.9:\n version \"4.2.11\"\n resolved \"https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3\"\n integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==\n@@ -9172,8 +9389,13 @@\n version \"1.0.3\"\n resolved \"https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8\"\n integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==\n \n+has-symbols@^1.1.0:\n+ version \"1.1.0\"\n+ resolved \"https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338\"\n+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==\n+\n has-tostringtag@^1.0.0:\n version \"1.0.0\"\n resolved \"https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25\"\n integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==\n@@ -9239,9 +9461,9 @@\n domhandler \"^5.0.3\"\n domutils \"^3.0.1\"\n entities \"^4.4.0\"\n \n-http-errors@2.0.0:\n+http-errors@2.0.0, http-errors@^2.0.0:\n version \"2.0.0\"\n resolved \"https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3\"\n integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==\n dependencies:\n@@ -9332,8 +9554,15 @@\n integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==\n dependencies:\n safer-buffer \">= 2.1.2 < 3\"\n \n+iconv-lite@0.6.3, iconv-lite@^0.6.3:\n+ version \"0.6.3\"\n+ resolved \"https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501\"\n+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==\n+ dependencies:\n+ safer-buffer \">= 2.1.2 < 3.0.0\"\n+\n idb@7.1.1:\n version \"7.1.1\"\n resolved \"https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b\"\n integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==\n@@ -9623,8 +9852,13 @@\n version \"3.0.3\"\n resolved \"https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283\"\n integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==\n \n+is-promise@^4.0.0:\n+ version \"4.0.0\"\n+ resolved \"https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3\"\n+ integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==\n+\n is-regex@^1.1.4:\n version \"1.1.4\"\n resolved \"https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958\"\n integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==\n@@ -10687,8 +10921,13 @@\n version \"1.2.6\"\n resolved \"https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46\"\n integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==\n \n+math-intrinsics@^1.1.0:\n+ version \"1.1.0\"\n+ resolved \"https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9\"\n+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\n+\n md5@^2.3.0:\n version \"2.3.0\"\n resolved \"https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f\"\n integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==\n@@ -10716,8 +10955,13 @@\n version \"0.3.0\"\n resolved \"https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748\"\n integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=\n \n+media-typer@^1.1.0:\n+ version \"1.1.0\"\n+ resolved \"https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561\"\n+ integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==\n+\n memoize-one@^6.0.0:\n version \"6.0.0\"\n resolved \"https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045\"\n integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==\n@@ -10726,8 +10970,13 @@\n version \"1.0.1\"\n resolved \"https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61\"\n integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=\n \n+merge-descriptors@^2.0.0:\n+ version \"2.0.0\"\n+ resolved \"https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808\"\n+ integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==\n+\n merge-stream@^2.0.0:\n version \"2.0.0\"\n resolved \"https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60\"\n integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==\n@@ -10754,15 +11003,27 @@\n version \"1.52.0\"\n resolved \"https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70\"\n integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==\n \n+mime-db@^1.54.0:\n+ version \"1.54.0\"\n+ resolved \"https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5\"\n+ integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==\n+\n mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:\n version \"2.1.35\"\n resolved \"https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a\"\n integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==\n dependencies:\n mime-db \"1.52.0\"\n \n+mime-types@^3.0.0, mime-types@^3.0.1:\n+ version \"3.0.1\"\n+ resolved \"https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce\"\n+ integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==\n+ dependencies:\n+ mime-db \"^1.54.0\"\n+\n mime@1.6.0:\n version \"1.6.0\"\n resolved \"https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1\"\n integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==\n@@ -10855,9 +11116,9 @@\n version \"2.1.2\"\n resolved \"https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009\"\n integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==\n \n-ms@2.1.3, ms@^2.0.0, ms@^2.1.1:\n+ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3:\n version \"2.1.3\"\n resolved \"https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2\"\n integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==\n \n@@ -10904,8 +11165,13 @@\n version \"0.6.3\"\n resolved \"https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd\"\n integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==\n \n+negotiator@^1.0.0:\n+ version \"1.0.0\"\n+ resolved \"https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a\"\n+ integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==\n+\n neo-async@^2.6.2:\n version \"2.6.2\"\n resolved \"https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f\"\n integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==\n@@ -11187,9 +11453,9 @@\n call-bind \"^1.0.7\"\n define-properties \"^1.2.1\"\n es-object-atoms \"^1.0.0\"\n \n-on-finished@2.4.1:\n+on-finished@2.4.1, on-finished@^2.4.1:\n version \"2.4.1\"\n resolved \"https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f\"\n integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==\n dependencies:\n@@ -11383,9 +11649,9 @@\n integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==\n dependencies:\n entities \"^4.4.0\"\n \n-parseurl@~1.3.3:\n+parseurl@^1.3.3, parseurl@~1.3.3:\n version \"1.3.3\"\n resolved \"https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4\"\n integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==\n \n@@ -11428,8 +11694,13 @@\n version \"0.1.7\"\n resolved \"https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c\"\n integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=\n \n+path-to-regexp@^8.0.0:\n+ version \"8.2.0\"\n+ resolved \"https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4\"\n+ integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==\n+\n path-type@^4.0.0:\n version \"4.0.0\"\n resolved \"https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b\"\n integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==\n@@ -11577,8 +11848,13 @@\n version \"4.0.6\"\n resolved \"https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9\"\n integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==\n \n+pkce-challenge@^5.0.0:\n+ version \"5.0.0\"\n+ resolved \"https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.0.tgz#c3a405cb49e272094a38e890a2b51da0228c4d97\"\n+ integrity sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==\n+\n pkg-dir@^4.2.0:\n version \"4.2.0\"\n resolved \"https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3\"\n integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==\n@@ -11921,9 +12197,9 @@\n \"@protobufjs/utf8\" \"^1.1.0\"\n \"@types/node\" \">=13.7.0\"\n long \"^5.0.0\"\n \n-proxy-addr@~2.0.7:\n+proxy-addr@^2.0.7, proxy-addr@~2.0.7:\n version \"2.0.7\"\n resolved \"https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025\"\n integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==\n dependencies:\n@@ -11993,8 +12269,15 @@\n integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==\n dependencies:\n side-channel \"^1.0.4\"\n \n+qs@^6.14.0:\n+ version \"6.14.0\"\n+ resolved \"https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930\"\n+ integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==\n+ dependencies:\n+ side-channel \"^1.1.0\"\n+\n qs@^6.6.0, qs@^6.9.4:\n version \"6.11.2\"\n resolved \"https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9\"\n integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==\n@@ -12048,9 +12331,9 @@\n dependencies:\n discontinuous-range \"1.0.0\"\n ret \"~0.1.10\"\n \n-range-parser@~1.2.1:\n+range-parser@^1.2.1, range-parser@~1.2.1:\n version \"1.2.1\"\n resolved \"https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031\"\n integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==\n \n@@ -12063,8 +12346,18 @@\n http-errors \"2.0.0\"\n iconv-lite \"0.4.24\"\n unpipe \"1.0.0\"\n \n+raw-body@^3.0.0:\n+ version \"3.0.0\"\n+ resolved \"https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f\"\n+ integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==\n+ dependencies:\n+ bytes \"3.1.2\"\n+ http-errors \"2.0.0\"\n+ iconv-lite \"0.6.3\"\n+ unpipe \"1.0.0\"\n+\n rc@^1.2.7:\n version \"1.2.8\"\n resolved \"https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed\"\n integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==\n@@ -12492,8 +12785,19 @@\n version \"1.3.3\"\n resolved \"https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.3.tgz#3f67fc106288b84b71532b4a5fd9d4881e4457f0\"\n integrity sha512-85aZYCxweiD5J8yTEbw+E6A27zSnLPNDL0WfPdw3YYodq7WjnTKo0q4dtyQ2gz23iPT8Q9CUyJtAaUNcTxRf5Q==\n \n+router@^2.2.0:\n+ version \"2.2.0\"\n+ resolved \"https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef\"\n+ integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==\n+ dependencies:\n+ debug \"^4.4.0\"\n+ depd \"^2.0.0\"\n+ is-promise \"^4.0.0\"\n+ parseurl \"^1.3.3\"\n+ path-to-regexp \"^8.0.0\"\n+\n run-parallel@^1.1.9:\n version \"1.2.0\"\n resolved \"https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee\"\n integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==\n@@ -12554,9 +12858,9 @@\n call-bind \"^1.0.6\"\n es-errors \"^1.3.0\"\n is-regex \"^1.1.4\"\n \n-\"safer-buffer@>= 2.1.2 < 3\", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:\n+\"safer-buffer@>= 2.1.2 < 3\", \"safer-buffer@>= 2.1.2 < 3.0.0\", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:\n version \"2.1.2\"\n resolved \"https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a\"\n integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==\n \n@@ -12636,8 +12940,25 @@\n on-finished \"2.4.1\"\n range-parser \"~1.2.1\"\n statuses \"2.0.1\"\n \n+send@^1.1.0, send@^1.2.0:\n+ version \"1.2.0\"\n+ resolved \"https://registry.yarnpkg.com/send/-/send-1.2.0.tgz#32a7554fb777b831dfa828370f773a3808d37212\"\n+ integrity sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==\n+ dependencies:\n+ debug \"^4.3.5\"\n+ encodeurl \"^2.0.0\"\n+ escape-html \"^1.0.3\"\n+ etag \"^1.8.1\"\n+ fresh \"^2.0.0\"\n+ http-errors \"^2.0.0\"\n+ mime-types \"^3.0.1\"\n+ ms \"^2.1.3\"\n+ on-finished \"^2.4.1\"\n+ range-parser \"^1.2.1\"\n+ statuses \"^2.0.1\"\n+\n serve-static@1.15.0:\n version \"1.15.0\"\n resolved \"https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540\"\n integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==\n@@ -12646,8 +12967,18 @@\n escape-html \"~1.0.3\"\n parseurl \"~1.3.3\"\n send \"0.18.0\"\n \n+serve-static@^2.2.0:\n+ version \"2.2.0\"\n+ resolved \"https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.0.tgz#9c02564ee259bdd2251b82d659a2e7e1938d66f9\"\n+ integrity sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==\n+ dependencies:\n+ encodeurl \"^2.0.0\"\n+ escape-html \"^1.0.3\"\n+ parseurl \"^1.3.3\"\n+ send \"^1.2.0\"\n+\n set-function-length@^1.1.1:\n version \"1.1.1\"\n resolved \"https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed\"\n integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==\n@@ -12752,8 +13083,37 @@\n version \"1.8.1\"\n resolved \"https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680\"\n integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==\n \n+side-channel-list@^1.0.0:\n+ version \"1.0.0\"\n+ resolved \"https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad\"\n+ integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==\n+ dependencies:\n+ es-errors \"^1.3.0\"\n+ object-inspect \"^1.13.3\"\n+\n+side-channel-map@^1.0.1:\n+ version \"1.0.1\"\n+ resolved \"https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42\"\n+ integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==\n+ dependencies:\n+ call-bound \"^1.0.2\"\n+ es-errors \"^1.3.0\"\n+ get-intrinsic \"^1.2.5\"\n+ object-inspect \"^1.13.3\"\n+\n+side-channel-weakmap@^1.0.2:\n+ version \"1.0.2\"\n+ resolved \"https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea\"\n+ integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==\n+ dependencies:\n+ call-bound \"^1.0.2\"\n+ es-errors \"^1.3.0\"\n+ get-intrinsic \"^1.2.5\"\n+ object-inspect \"^1.13.3\"\n+ side-channel-map \"^1.0.1\"\n+\n side-channel@^1.0.4:\n version \"1.0.4\"\n resolved \"https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf\"\n integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==\n@@ -12771,8 +13131,19 @@\n es-errors \"^1.3.0\"\n get-intrinsic \"^1.2.4\"\n object-inspect \"^1.13.1\"\n \n+side-channel@^1.1.0:\n+ version \"1.1.0\"\n+ resolved \"https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9\"\n+ integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==\n+ dependencies:\n+ es-errors \"^1.3.0\"\n+ object-inspect \"^1.13.3\"\n+ side-channel-list \"^1.0.0\"\n+ side-channel-map \"^1.0.1\"\n+ side-channel-weakmap \"^1.0.2\"\n+\n signal-exit@^3.0.3, signal-exit@^3.0.7:\n version \"3.0.7\"\n resolved \"https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9\"\n integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==\n@@ -12926,8 +13297,13 @@\n version \"2.0.1\"\n resolved \"https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63\"\n integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==\n \n+statuses@^2.0.1:\n+ version \"2.0.2\"\n+ resolved \"https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382\"\n+ integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==\n+\n stream-events@^1.0.5:\n version \"1.0.5\"\n resolved \"https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5\"\n integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==\n@@ -13676,8 +14052,17 @@\n version \"0.21.3\"\n resolved \"https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37\"\n integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==\n \n+type-is@^2.0.0, type-is@^2.0.1:\n+ version \"2.0.1\"\n+ resolved \"https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97\"\n+ integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==\n+ dependencies:\n+ content-type \"^1.0.5\"\n+ media-typer \"^1.1.0\"\n+ mime-types \"^3.0.0\"\n+\n type-is@~1.6.18:\n version \"1.6.18\"\n resolved \"https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131\"\n integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==\n@@ -13958,9 +14343,9 @@\n \"@jridgewell/trace-mapping\" \"^0.3.12\"\n \"@types/istanbul-lib-coverage\" \"^2.0.1\"\n convert-source-map \"^1.6.0\"\n \n-vary@^1, vary@~1.1.2:\n+vary@^1, vary@^1.1.2, vary@~1.1.2:\n version \"1.1.2\"\n resolved \"https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc\"\n integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=\n \n@@ -14299,8 +14684,13 @@\n version \"3.23.5\"\n resolved \"https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.23.5.tgz#ec23def47dcafe3a4d640eba6a346b34f9a693a5\"\n integrity sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==\n \n+zod-to-json-schema@^3.24.1:\n+ version \"3.24.6\"\n+ resolved \"https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d\"\n+ integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==\n+\n zod@3.21.4:\n version \"3.21.4\"\n resolved \"https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db\"\n integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==\n"
|
|
538
|
-
}
|
|
539
|
-
]
|
|
540
|
-
},
|
|
541
|
-
{
|
|
542
|
-
"id": "add-min-bet-filter",
|
|
543
|
-
"sha": "0ce35f5f3c2cdbeac1f48d94eb37abfbad0730f5",
|
|
544
|
-
"parentSha": "1413c780ef6b191a7e282ea91aa58f7591dc0d6c",
|
|
545
|
-
"spec": "Implement a minimum bet amount filter for contract bets and switch the Trades tab to infinite loading.\n\nBackend API and schema\n1) Update common/src/api/schema.ts\n- In API['bets'].props, add an optional minAmount: coerce to number, positive, optional. This ensures api('bets', { minAmount }) is type-safe and validated.\n- Do not alter bet-points props.\n\n2) Update backend/api/src/get-bets.ts\n- Read minAmount from the validated props of getBetsInternal.\n- Add minAmount to the opts object passed to getBetsWithFilter so it propagates to the shared query layer.\n\n3) Update backend/shared/src/supabase/bets.ts\n- Extend getBetsWithFilter to accept minAmount via APIParams<'bets'>.\n- If minAmount is provided, add a WHERE clause to filter by absolute bet amount: abs((contract_bets.data->>'amount')::numeric) >= ${minAmount} using the existing SQL builder's where() with a bound parameter.\n- Keep the existing conditions intact (filterRedemptions, includeZeroShareRedemptions, etc.).\n\nFrontend Trades tab UI and loading\n4) Update web/components/contract/contract-tabs.tsx (BetsTabContent component)\n- Replace the paginated loading (page state, ITEMS_PER_PAGE slicing, Pagination component) with infinite loading using LoadMoreUntilNotVisible from web/components/widgets/visibility-observer.\n- Add a minimum bet amount dropdown control above the list:\n - Use DropdownMenu and generateFilterDropdownItems.\n - Options: Any amount (undefined), M$100+ (100), M$1,000+ (1000), M$10,000+ (10000).\n - Persist the selected option index using usePersistentInMemoryState keyed by `bet-amount-filter-${contract.id}`; default to the first option.\n- Apply the filter:\n - For the initial props.bets array, filter client-side by Math.abs(bet.amount) >= selectedMinAmount when selectedMinAmount is defined.\n - Preserve prior grouping/ordering (NUMBER contracts remain grouped by betGroupId; other contracts list single bets).\n- Infinite loading:\n - Implement a loadMore function that:\n - Exits early if nothing more to load; otherwise calls api('bets', { contractId, beforeTime: oldestLoadedBetTime, limit: 50, filterRedemptions: !isNumber, includeZeroShareRedemptions: isNumber, minAmount: selectedMinAmount }).\n - Appends unique results to olderBets and returns true if any new bets were added; false otherwise.\n - Reset olderBets and trigger an initial load when the min-amount selection changes.\n - Render <LoadMoreUntilNotVisible loadMore={loadMore} /> at the end of the list.\n- Skeleton loading rows:\n - Show skeleton rows only if more items are expected AND the filter is set to Any amount (i.e., minAmount is undefined).\n - Limit the number of skeleton rows to at most 10.\n- Remove the Pagination import and component usage; import and use LoadMoreUntilNotVisible.\n- Track selection changes with an analytics event (e.g., 'change-bet-amount-filter') including contract slug/name and the selected minimum amount.\n\nImports/cleanup\n5) Adjust imports in contract-tabs.tsx:\n- Remove Pagination import.\n- Import { VisibilityObserver, LoadMoreUntilNotVisible } from widgets/visibility-observer.\n- Ensure ChevronDownIcon, DropdownMenu, and generateFilterDropdownItems are imported for the dropdown.\n\nBehavioral expectations\n- Calling GET /bets with minAmount should return only bets whose absolute amount meets/exceeds the threshold, without affecting existing filters or ordering.\n- The Trades tab should show a Min amount dropdown. Changing the filter immediately re-queries older results with the server-side filter and filters the initial batch client-side.\n- The list loads more automatically until not visible, without a manual pagination control.\n- NUMBER contracts continue to group and order bet groups as before.",
|
|
546
|
-
"prompt": "Add a minimum bet amount filter to the bets endpoint and surface it on the contract Trades tab with infinite loading.\n\nGoals:\n- Extend the GET /bets API to accept an optional minAmount parameter that filters by the absolute value of the bet amount.\n- Wire this through the backend query so only bets with |amount| >= minAmount are returned when provided.\n- On the Trades tab in a contract page, add a Min amount dropdown with a few thresholds, persist the selection, and filter the initially provided bets client-side while passing minAmount to subsequent server fetches.\n- Replace the existing paginated list with infinite loading that autoloads more until the sentinel is no longer visible.\n\nNotes:\n- Preserve existing filtering and grouping behavior for different contract types, including multi-numeric bet grouping.\n- Hide loading skeleton rows when a min-amount filter is active; otherwise show a small number of skeleton rows when more data is expected.\n- Keep the code style and existing patterns (DropdownMenu, generateFilterDropdownItems, usePersistentInMemoryState, LoadMoreUntilNotVisible).",
|
|
547
|
-
"supplementalFiles": [
|
|
548
|
-
"web/components/widgets/visibility-observer.tsx",
|
|
549
|
-
"web/components/widgets/dropdown-menu.tsx",
|
|
550
|
-
"web/components/search/search-dropdown-helpers.tsx",
|
|
551
|
-
"backend/api/src/routes.ts",
|
|
552
|
-
"web/lib/api/api.ts"
|
|
553
|
-
],
|
|
554
|
-
"fileDiffs": [
|
|
555
|
-
{
|
|
556
|
-
"path": "backend/api/src/get-bets.ts",
|
|
557
|
-
"status": "modified",
|
|
558
|
-
"diff": "Index: backend/api/src/get-bets.ts\n===================================================================\n--- backend/api/src/get-bets.ts\t1413c78 (parent)\n+++ backend/api/src/get-bets.ts\t0ce35f5 (commit)\n@@ -21,8 +21,9 @@\n beforeTime,\n afterTime,\n order,\n kinds,\n+ minAmount,\n filterRedemptions,\n includeZeroShareRedemptions,\n commentRepliesOnly,\n count,\n@@ -80,8 +81,9 @@\n : afterTime ?? afterBetTime,\n limit,\n order,\n kinds,\n+ minAmount,\n filterRedemptions,\n includeZeroShareRedemptions,\n count,\n points,\n"
|
|
559
|
-
},
|
|
560
|
-
{
|
|
561
|
-
"path": "backend/shared/src/supabase/bets.ts",
|
|
562
|
-
"status": "modified",
|
|
563
|
-
"diff": "Index: backend/shared/src/supabase/bets.ts\n===================================================================\n--- backend/shared/src/supabase/bets.ts\t1413c78 (parent)\n+++ backend/shared/src/supabase/bets.ts\t0ce35f5 (commit)\n@@ -48,8 +48,9 @@\n limit: limitValue,\n kinds,\n count,\n points,\n+ minAmount,\n } = options\n \n const conditions = buildArray(\n !contractId && [\n@@ -86,9 +87,14 @@\n \n filterRedemptions && where('is_redemption = false'),\n \n !includeZeroShareRedemptions &&\n- where(`(shares != 0 or is_redemption = false or loan_amount != 0)`)\n+ where(`(shares != 0 or is_redemption = false or loan_amount != 0)`),\n+\n+ minAmount !== undefined &&\n+ where(`abs((contract_bets.data->>'amount')::numeric) >= ${minAmount}`, {\n+ minAmount,\n+ })\n )\n const selection = count\n ? select('count(contract_bets.*)')\n : points\n"
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
"path": "common/src/api/schema.ts",
|
|
567
|
-
"status": "modified",
|
|
568
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\t1413c78 (parent)\n+++ common/src/api/schema.ts\t0ce35f5 (commit)\n@@ -411,8 +411,9 @@\n beforeTime: z.coerce.number().optional(),\n afterTime: z.coerce.number().optional(),\n order: z.enum(['asc', 'desc']).optional(),\n kinds: z.enum(['open-limit']).optional(),\n+ minAmount: z.coerce.number().positive().optional(),\n // undocumented fields. idk what a good api interface would be\n filterRedemptions: coerceBoolean.optional(),\n includeZeroShareRedemptions: coerceBoolean.optional(),\n commentRepliesOnly: coerceBoolean.optional(),\n"
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
"path": "web/components/contract/contract-tabs.tsx",
|
|
572
|
-
"status": "modified",
|
|
573
|
-
"diff": "Index: web/components/contract/contract-tabs.tsx\n===================================================================\n--- web/components/contract/contract-tabs.tsx\t1413c78 (parent)\n+++ web/components/contract/contract-tabs.tsx\t0ce35f5 (commit)\n@@ -29,11 +29,13 @@\n import { shortFormatNumber, maybePluralize } from 'common/util/format'\n import { MINUTE_MS } from 'common/util/time'\n import { UserPositionsTable } from 'web/components/contract/user-positions-table'\n import { LoadingIndicator } from 'web/components/widgets/loading-indicator'\n-import { Pagination } from 'web/components/widgets/pagination'\n import { Tooltip } from 'web/components/widgets/tooltip'\n-import { VisibilityObserver } from 'web/components/widgets/visibility-observer'\n+import {\n+ VisibilityObserver,\n+ LoadMoreUntilNotVisible,\n+} from 'web/components/widgets/visibility-observer'\n import { useEvent } from 'client-common/hooks/use-event'\n import { useLiquidity } from 'web/hooks/use-liquidity'\n import { useUser } from 'web/hooks/use-user'\n import { track } from 'web/lib/service/analytics'\n@@ -549,17 +551,30 @@\n const { contract, setReplyToBet, totalBets } = props\n const { outcomeType } = contract\n const [olderBets, setOlderBets] = useState<Bet[]>([])\n \n- const [page, setPage] = useState(0)\n+ const [minAmountFilterIndex, setMinAmountFilterIndex] =\n+ usePersistentInMemoryState(0, `bet-amount-filter-${contract.id}`)\n const isNumber = outcomeType === 'NUMBER'\n- const ITEMS_PER_PAGE = 50 * (isNumber ? contract.answers.length : 1)\n- const bets = [...props.bets, ...olderBets]\n+\n+ // Min amount filter options\n+ const minAmountOptions = [\n+ { label: 'Any amount', value: undefined },\n+ { label: 'M$100+', value: 100 },\n+ { label: 'M$1,000+', value: 1000 },\n+ { label: 'M$10,000+', value: 10000 },\n+ ]\n+ const selectedMinAmount = minAmountOptions[minAmountFilterIndex].value\n+\n+ // Filter initial bets on client side, server will filter olderBets\n+ const filteredInitialBets = selectedMinAmount\n+ ? props.bets.filter((bet) => Math.abs(bet.amount) >= selectedMinAmount)\n+ : props.bets\n+\n+ const bets = [...filteredInitialBets, ...olderBets]\n listenToOrderUpdates(contract.id, setOlderBets, true)\n \n const oldestBet = minBy(bets, (b) => b.createdTime)\n- const start = page * ITEMS_PER_PAGE\n- const end = start + ITEMS_PER_PAGE\n \n const lps = useLiquidity(contract.id) ?? []\n const visibleLps = lps.filter(\n (l) =>\n@@ -594,52 +609,100 @@\n \n const totalItems = totalBets + visibleLps.length\n const totalLoadedItems = bets.length + visibleLps.length\n \n- const limit = (items.length - (page + 1) * ITEMS_PER_PAGE) * -1\n- const shouldLoadMore = limit > 0 && totalLoadedItems < totalItems\n+ const shouldLoadMore = totalLoadedItems < totalItems\n const [now] = useState(Date.now())\n const oldestBetTime = oldestBet?.createdTime ?? now\n- useEffect(() => {\n- if (!shouldLoadMore) return\n- api('bets', {\n- contractId: contract.id,\n- beforeTime: oldestBetTime,\n- limit,\n- filterRedemptions: !isNumber,\n- includeZeroShareRedemptions: isNumber,\n- })\n- .then((olderBets) => {\n- setOlderBets((bets) => uniqBy([...bets, ...olderBets], (b) => b.id))\n+\n+ const loadMore = useEvent(async () => {\n+ if (!shouldLoadMore) return false\n+\n+ try {\n+ const newBets = await api('bets', {\n+ contractId: contract.id,\n+ beforeTime: oldestBetTime,\n+ limit: 50,\n+ filterRedemptions: !isNumber,\n+ includeZeroShareRedemptions: isNumber,\n+ minAmount: selectedMinAmount,\n })\n- .catch((err) => {\n- console.error(err)\n- })\n- }, [contract.id, limit, oldestBetTime, shouldLoadMore])\n \n- const pageItems = sortBy(items, (item) =>\n+ if (newBets.length > 0) {\n+ setOlderBets((bets) => uniqBy([...bets, ...newBets], (b) => b.id))\n+ return true\n+ }\n+ return false\n+ } catch (err) {\n+ console.error(err)\n+ return false\n+ }\n+ })\n+ useEffect(() => {\n+ setOlderBets([])\n+ loadMore()\n+ }, [selectedMinAmount])\n+\n+ const allItems = sortBy(items, (item) =>\n item.type === 'bet'\n ? -item.bet.createdTime\n : item.type === 'liquidity'\n ? -item.lp.createdTime\n : item.type === 'betGroup'\n ? -item.bets[0].createdTime\n : undefined\n- ).slice(start, end)\n+ )\n \n const scrollRef = useRef<HTMLDivElement>(null)\n const isCashContract = contract.token === 'CASH'\n \n- // Determine how many loading rows to show (up to ITEMS_PER_PAGE)\n+ // Determine how many loading rows to show\n const numLoadingRows = shouldLoadMore\n- ? Math.min(ITEMS_PER_PAGE, Math.max(0, totalBets - pageItems.length))\n+ ? Math.min(10, Math.max(0, totalBets - allItems.length))\n : 0\n \n return (\n <>\n <div ref={scrollRef} />\n+\n+ {/* Minimum bet amount filter */}\n+ <Row className=\"mb-4 justify-end\">\n+ <Row className=\"items-center gap-1\">\n+ <span className=\"text-ink-400 text-sm\">Min amount:</span>\n+ <DropdownMenu\n+ items={generateFilterDropdownItems(\n+ minAmountOptions.map((option, i) => ({\n+ label: option.label,\n+ value: i.toString(),\n+ })),\n+ (value: string) => {\n+ const newIndex = parseInt(value)\n+ setMinAmountFilterIndex(newIndex)\n+ setOlderBets([]) // Clear older bets to refetch with new filter\n+ track('change-bet-amount-filter', {\n+ contractSlug: contract.slug,\n+ contractName: contract.question,\n+ minAmount: minAmountOptions[newIndex].value,\n+ })\n+ }\n+ )}\n+ buttonContent={\n+ <Row className=\"text-ink-600 w-28 items-center text-sm\">\n+ <span className=\"whitespace-nowrap\">\n+ {minAmountOptions[minAmountFilterIndex].label}\n+ </span>\n+ <ChevronDownIcon className=\"h-4 w-4\" />\n+ </Row>\n+ }\n+ menuWidth={'w-36'}\n+ selectedItemName={minAmountOptions[minAmountFilterIndex].label}\n+ closeOnClick\n+ />\n+ </Row>\n+ </Row>\n+\n <Col className=\"mb-4 items-start gap-7\">\n- {pageItems.map((item) =>\n+ {allItems.map((item) =>\n item.type === 'bet' ? (\n <FeedBet\n onReply={setReplyToBet}\n key={item.id}\n@@ -665,23 +728,15 @@\n )\n )}\n {/* Render skeleton loading rows */}\n {shouldLoadMore &&\n+ !minAmountFilterIndex &&\n Array(numLoadingRows)\n .fill(0)\n .map((_, i) => <LoadingBetRow key={`loading-${i}`} />)}\n </Col>\n- <Pagination\n- page={page}\n- pageSize={ITEMS_PER_PAGE}\n- totalItems={totalItems}\n- setPage={(page) => {\n- setPage(page)\n- scrollRef.current?.scrollIntoView({\n- block: 'center',\n- })\n- }}\n- />\n+\n+ <LoadMoreUntilNotVisible loadMore={loadMore} />\n </>\n )\n })\n \n"
|
|
574
|
-
}
|
|
575
|
-
]
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
"id": "automate-leagues",
|
|
579
|
-
"sha": "14e0625213c12dd277d202c79a5e84f8ae1abb6f",
|
|
580
|
-
"parentSha": "495e67b9a79556cd6480ffd09f574e97b4770eb4",
|
|
581
|
-
"spec": "Implement database-driven, automated league season management and update clients and schedulers accordingly.\n\n1) Create DB table to drive season timing and status\n- Add a new Postgres table leagues_season_end_times with columns: season (int, primary key), end_time (timestamptz), status (text enum constrained to 'active' | 'processing' | 'complete' with default 'active'). Enable row level security.\n- Backfill historical seasons (1..23) as complete with their end_time timestamps. Do not pre-create the next active season row; allow automation to insert it.\n- Location: backend/supabase/leagues_season_end_times.sql\n\n2) Add shared helpers for season state in Supabase\n- In backend/shared/src/supabase/leagues.ts implement:\n - Types: SeasonStatus = 'active' | 'processing' | 'complete' and SeasonEndTimeInfo { season: number; end_time: number; status: SeasonStatus } (end_time in ms).\n - getSeasonEndTimeRow(pg, season) -> select season, ts_to_millis(end_time) end_time, status from leagues_season_end_times for the given season; return null if missing.\n - insertSeasonEndTime(pg, season, endTime: Date) -> insert row with default status 'active'; on conflict do nothing.\n - updateSeasonStatus(pg, season, status) -> update status.\n - getEffectiveCurrentSeason() -> return season flagged as active; if none, log error and return latest completed + 1; if table empty, return 1.\n\n3) Add public API endpoint for season info\n- Define API schema entry 'get-season-info' (GET, public, no auth) with optional query prop season (positive integer) and returns { season, startTime: epoch ms, endTime: epoch ms | null, status: 'active' | 'processing' | 'complete' }.\n- Implement backend/api/src/get-season-info.ts handler to:\n - Use createSupabaseDirectClient.\n - Resolve season = props.season or await getEffectiveCurrentSeason().\n - Load seasonInfo via getSeasonEndTimeRow(pg, season). If absent, respond 404.\n - Compute startTime in ms from LEAGUES_START + (season-1) months.\n - For status 'active', set endTime to null (hidden/mystery to clients). For other statuses, return end_time from DB.\n- Register the handler in backend/api/src/routes.ts: import './get-season-info' and map 'get-season-info' to getSeasonInfo. Also fix getRelatedMarkets import to './get-related-markets'.\n\n4) Automate league rollover via scheduler\n- Add backend/scheduler/src/jobs/auto-leagues-cycle.ts that:\n - Ensures an 'active' season row exists. If missing, create one with a randomized end time between the start of the next month (PDT) and the start of the following day; then generateNextSeason(pg, currentSeason) and insertBots(pg, currentSeason).\n - When now >= end_time and status is 'active', start a single database transaction that:\n 1) updateSeasonStatus(currentSeason, 'processing')\n 2) run updateLeague(currentSeason, tx) and updateLeagueRanks(currentSeason, tx)\n 3) prepare nextSeason = currentSeason + 1; generateNextSeason(tx, nextSeason) and insertBots(tx, nextSeason)\n 4) insertSeasonEndTime(tx, nextSeason, random end time in proper window) to mark it active by default\n 5) run updateLeague(nextSeason, tx) and updateLeagueRanks(nextSeason, tx)\n 6) send end-of-season payouts/notifications for currentSeason using sendEndOfSeasonNotificationsAndBonuses(tx, currentSeason, nextSeason)\n 7) updateSeasonStatus(currentSeason, 'complete')\n - Use dayjs with timezone (America/Los_Angeles) for end time windows.\n- Register a new job in backend/scheduler/src/jobs/index.ts with createJob('auto-leagues-cycle', '0 */10 * * * *', autoLeaguesCycle) to run every 10 minutes.\n- Ensure existing jobs calling updateLeague and updateLeagueRanks call them via lambdas (() => updateLeague()) and (() => updateLeagueRanks()) without passing season, letting them resolve the current season.\n\n5) Update league update jobs to use DB-driven season window\n- backend/scheduler/src/jobs/update-league.ts:\n - Accept optional parameters (manualSeason?: number, tx?: SupabaseDirectClient). Default pg to tx or a new client.\n - Determine season = manualSeason ?? await getEffectiveCurrentSeason(). Load seasonInfo via getSeasonEndTimeRow(pg, season); if not found, log and exit.\n - Use getSeasonDates(season) to compute start time; use seasonInfo.end_time for the actual end bound.\n - If Date.now() > seasonEnd, exit early. Adjust logs to use log instead of console.\n - Keep bets/contracts logic as before, but ensure contracts filter uses coalesce((data->'isRanked')::boolean, true) = true and skip when no betContractIds.\n - Remove creatorFees from aggregation (commented out) so only trader profit contributes to mana_earned.\n- backend/scheduler/src/jobs/update-league-ranks.ts:\n - Accept optional (manualSeason?: number, tx?: SupabaseDirectClient). Default to getEffectiveCurrentSeason() and pg = tx ?? create client.\n\n6) Bulkify league change notifications and payouts\n- backend/shared/src/create-notification.ts:\n - Replace deprecated createLeagueChangedNotification with createLeagueChangedNotifications(pg, data: LeagueChangeNotificationData[]), which:\n - Bulk fetches private users for provided userIds.\n - Respects notification preferences through getNotificationDestinationsForUser with reason 'league_changed'.\n - Builds Notification objects with per-user NanoID, reason 'league_changed', sourceType 'league_change', sourceId id, sourceText = bonusAmount as string, and data: LeagueChangeData.\n - Performs a bulkInsertNotifications call.\n- backend/shared/src/generate-leagues.ts:\n - Use getEffectiveCurrentSeason() instead of constants.\n - Refactor generateCohorts to be a pure function (no pg), returning a userId -> { division, cohort } map. Log useful shapes.\n - In addToLeagueIfNotInOne and addNewUserToLeague, use getEffectiveCurrentSeason() and call createLeagueChangedNotifications(pg, [ ... ]) with bonusAmount: 0.\n- backend/shared/src/payout-leagues.ts:\n - Replace per-user runTxnFromBank with a bulk payout flow inside a transaction: compute eligible users (where prize > 0 and no existing LEAGUE_PRIZE txn for prevSeason), aggregate balance increments and TxnData entries, apply bulkIncrementBalances(pg, increments) and insertTxns(pg, txnDatas), then call createLeagueChangedNotifications(pg, notifications). Accept SupabaseTransaction and signature sendEndOfSeasonNotificationsAndBonuses(pg, prevSeason, newSeason).\n\n7) Update common leagues API and consumer code\n- common/src/leagues.ts:\n - Remove exported constants SEASONS, CURRENT_SEASON, and SEASON_END_TIMES.\n - Change getSeasonDates(season) to return { start: Date, approxEnd: Date } where approxEnd is LEAGUES_START + season months + 1 day and serves as a display fallback. Add a comment noting that the authoritative end time now lives in leagues_season_end_times.\n - Export LeagueChangeNotificationData type as LeagueChangeData & { userId: string }.\n - Update parseLeaguePath(slugs, rowsBySeason, seasons, userId?) to accept an array of available seasons and fallback to the latest if the slug isn't valid.\n\n8) Update client code to depend on server-provided season info\n- web/hooks/use-leagues.ts: Add useAPIGetter('get-season-info', {}) to retrieve the current season and pass season to getLeagueInfo(userId, season). Store league info keyed by user.\n- web/lib/supabase/leagues.ts: Update getLeagueInfo(userId, season) to query user_league_info for that season instead of using a CURRENT_SEASON constant.\n- web/components/leagues/mana-earned-breakdown.tsx: Use getSeasonDates(season).approxEnd for end bound.\n- web/pages/leagues/[[...leagueSlugs]].tsx:\n - Add getStaticPaths returning { paths: [], fallback: 'blocking' }.\n - Add getStaticProps to fetch current season info and the requested season info (if present in URL) via api('get-season-info', ...). Inject into page props: { initialSeasonInfo, currentSeasonInfo }.\n - Build seasons array [1..currentSeasonInfo.season]. Initialize selected season from initialSeasonInfo or fallback.\n - Replace usage of CURRENT_SEASON/SEASONS and getSeasonStatus; use seasonInfo.status ('active' | 'processing' | 'complete'). For UI, treat closingPeriod when status is 'active' and approxEnd is within 1 day.\n - Update routing calls to replace(...) without shallow where appropriate. Pass seasons into parseLeaguePath.\n - Continue to use getSeasonCountdownEnd(season) and getSeasonDates(season).approxEnd for display.\n\n9) API route import fix\n- In backend/api/src/routes.ts change import of getRelatedMarkets to a relative './get-related-markets'.\n\n10) Documentation adjustments\n- README.md: Update architecture wording to reflect the Supabase migration status and that clients primarily read from Supabase. Update common/ directory description to include shared/ usage.\n\n11) Remove stale script\n- Delete backend/scripts/generate-next-season.ts (obsolete once automation is active).\n\nAcceptance criteria\n- leagues_season_end_times exists with RLS enabled; historical seasons present as 'complete'; exactly one 'active' season at runtime once initialized by the scheduler job.\n- GET /api/v0/get-season-info returns season metadata; for active seasons, endTime is null; for completed/processing seasons, endTime is populated.\n- Scheduler job auto-leagues-cycle runs every 10 minutes, initializes missing active season rows, and transitions seasons atomically when end_time passes; logs progress.\n- updateLeague and updateLeagueRanks operate on the effective current season and stop after season end_time has passed.\n- End-of-season payouts and notifications are delivered in bulk only once per user; notifications are created via createLeagueChangedNotifications.\n- Web leagues page renders using season info from the API and no longer uses CURRENT_SEASON/SEASONS or getSeasonStatus.\n- All references to CURRENT_SEASON/SEASONS removed from edited areas; parseLeaguePath updated to accept seasons array.\n- No regressions in other league pages/components; tests/build pass.",
|
|
582
|
-
"prompt": "Implement database-driven, automated league seasons. Replace hardcoded season constants and client-side season status with a server-managed season table and a public API. Add a scheduler job that maintains the active season, triggers rollover when the end time passes, and performs end-of-season updates, payouts, and notifications atomically. Update the leagues page and hooks to fetch season info from the API, and adjust backend jobs to compute league stats using the DB season window. Ensure end-of-season prizes and notifications are processed in bulk and idempotently. Include a SQL script to create/backfill the season table and minor documentation updates.",
|
|
583
|
-
"supplementalFiles": [
|
|
584
|
-
"backend/scheduler/src/jobs/helpers.ts",
|
|
585
|
-
"backend/scheduler/src/index.ts",
|
|
586
|
-
"common/src/api/utils.ts",
|
|
587
|
-
"common/src/notification.ts",
|
|
588
|
-
"common/src/supabase/utils.ts",
|
|
589
|
-
"backend/shared/src/supabase/init.ts",
|
|
590
|
-
"backend/shared/src/utils.ts",
|
|
591
|
-
"backend/shared/src/supabase/users.ts",
|
|
592
|
-
"backend/shared/src/txn/run-txn.ts",
|
|
593
|
-
"backend/shared/src/resolve-market-helpers.ts",
|
|
594
|
-
"web/lib/api/api.ts",
|
|
595
|
-
"web/hooks/use-api-getter.ts",
|
|
596
|
-
"web/lib/supabase/db.ts",
|
|
597
|
-
"web/components/notifications/income-summary-notifications.tsx",
|
|
598
|
-
"common/src/supabase/leagues.ts",
|
|
599
|
-
"backend/api/src/get-related-markets.ts"
|
|
600
|
-
],
|
|
601
|
-
"fileDiffs": [
|
|
602
|
-
{
|
|
603
|
-
"path": "README.md",
|
|
604
|
-
"status": "modified",
|
|
605
|
-
"diff": "Index: README.md\n===================================================================\n--- README.md\t495e67b (parent)\n+++ README.md\t14e0625 (commit)\n@@ -13,21 +13,19 @@\n See [`web/README.md`][web-readme] for more details on hacking on the web client.\n \n ## General architecture\n \n-Manifold's public API and web app are hosted by [Vercel][vercel]. Our data has been stored in Firebase's database [Cloud Firestore][cloud-firestore] but we are currently migrating it to SQL hosted on [Supabase][supabase].\n+Manifold's public API and web app are hosted by [Vercel][vercel]. Our data has been stored in Firebase's database [Cloud Firestore][cloud-firestore] but we have almost enitrely migrated it to SQL hosted on [Supabase][supabase].\n \n-We often use firebase and supabase directly on the client to get the data. However, for complicated operations (like buying shares) we have a separate internal HTTP API deployed in a docker container in google cloud. This is seperate from the public-facing api hosted via Vercel; see [`functions/README.md`][functions-readme] for more details.\n+We often use supabase directly on the client to get the data. However, for complicated operations (like buying shares) we have a separate internal HTTP API deployed in a docker container in google cloud. This is seperate from the public-facing api hosted via Vercel; see [`functions/README.md`][functions-readme] for more details.\n \n ## Directory overview\n \n - [web/](./web/): UI and business logic for the client. Where most of the site lives. The public API endpoints are also in here.\n \n - [backend/](./backend/): All the rest of the stuff we run on GCP.\n \n-- [common/](./common/): Typescript library code shared between `web/` & `backend/`. If you want to look at how the market math\n- works, most of that's in here (it gets called from the `bet` and `sellBet` endpoints in `functions/`.) Also\n- contains in `common/envs` configuration for the different environments (i.e. prod, dev, Manifold for Teams instances.)\n+- [common/](./common/): Typescript library code shared between `web/` & `backend/` & `shared/`. If you want to look at how the market math works, most of that's in here (it gets called from the `bet` and `sellBet` endpoints in `functions/`.) Also contains in `common/envs` configuration for the different environments (i.e. prod, dev, Manifold for Teams instances.)\n \n - [docs/](./docs/): Manifold's public documentation that lives at https://docs.manifold.markets.\n \n ## Contributing\n"
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
"path": "backend/api/src/get-season-info.ts",
|
|
609
|
-
"status": "added",
|
|
610
|
-
"diff": "Index: backend/api/src/get-season-info.ts\n===================================================================\n--- backend/api/src/get-season-info.ts\t495e67b (parent)\n+++ backend/api/src/get-season-info.ts\t14e0625 (commit)\n@@ -0,0 +1,34 @@\n+import { APIError, APIHandler } from 'api/helpers/endpoint'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import {\n+ getSeasonEndTimeRow,\n+ getEffectiveCurrentSeason,\n+} from 'shared/supabase/leagues'\n+import { LEAGUES_START } from 'common/leagues'\n+\n+export const getSeasonInfo: APIHandler<'get-season-info'> = async (props) => {\n+ const pg = createSupabaseDirectClient()\n+ const season = props.season ?? (await getEffectiveCurrentSeason())\n+\n+ const seasonInfo = await getSeasonEndTimeRow(pg, season)\n+ if (!seasonInfo) {\n+ throw new APIError(404, 'Season info not found')\n+ }\n+\n+ const startTime = new Date(LEAGUES_START)\n+ startTime.setMonth(startTime.getMonth() + season - 1)\n+ const startTimeMs = startTime.getTime()\n+ const { status, end_time } = seasonInfo // Use status directly from DB\n+ let endTime: number | null = null\n+ // end time is a mystery if status is active\n+ if (status !== 'active') {\n+ endTime = end_time\n+ }\n+\n+ return {\n+ season,\n+ startTime: startTimeMs,\n+ endTime,\n+ status,\n+ }\n+}\n"
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
"path": "backend/api/src/league-activity.ts",
|
|
614
|
-
"status": "modified",
|
|
615
|
-
"diff": "Index: backend/api/src/league-activity.ts\n===================================================================\n--- backend/api/src/league-activity.ts\t495e67b (parent)\n+++ backend/api/src/league-activity.ts\t14e0625 (commit)\n@@ -38,9 +38,9 @@\n [season, cohort],\n (row: { user_id: string }) => row.user_id\n )\n \n- const { start, end } = getSeasonDates(season)\n+ const { start, approxEnd: end } = getSeasonDates(season)\n \n const bets = await pg.map<Bet>(\n `select\n cb.data\n"
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
"path": "backend/api/src/routes.ts",
|
|
619
|
-
"status": "modified",
|
|
620
|
-
"diff": "Index: backend/api/src/routes.ts\n===================================================================\n--- backend/api/src/routes.ts\t495e67b (parent)\n+++ backend/api/src/routes.ts\t14e0625 (commit)\n@@ -116,9 +116,9 @@\n import { getNotifications } from 'api/get-notifications'\n import { getCheckoutSession } from 'api/gidx/get-checkout-session'\n import { completeCheckoutSession } from 'api/gidx/complete-checkout-session'\n import { getContractTopics } from './get-contract-topics'\n-import { getRelatedMarkets } from 'api/get-related-markets'\n+import { getRelatedMarkets } from './get-related-markets'\n import { getRelatedMarketsByGroup } from './get-related-markets-by-group'\n import { followContract } from './follow-contract'\n import { getUserLimitOrdersWithContracts } from 'api/get-user-limit-orders-with-contracts'\n import { getInterestingGroupsFromViews } from 'api/get-interesting-groups-from-views'\n@@ -174,9 +174,14 @@\n import { inferNumericUnit } from './infer-numeric-unit'\n import { generateConciseTitle } from './generate-concise-title'\n import { getCloseDateEndpoint } from './get-close-date'\n import { referUser } from './refer-user'\n-import { saveMarketDraft, getMarketDrafts, deleteMarketDraft } from './market-drafts'\n+import {\n+ saveMarketDraft,\n+ getMarketDrafts,\n+ deleteMarketDraft,\n+} from './market-drafts'\n+import { getSeasonInfo } from './get-season-info'\n \n export const handlers: { [k in APIPath]: APIHandler<k> } = {\n 'refresh-all-clients': refreshAllClients,\n bet: placeBet,\n@@ -366,5 +371,6 @@\n 'refer-user': referUser,\n 'save-market-draft': saveMarketDraft,\n 'get-market-drafts': getMarketDrafts,\n 'delete-market-draft': deleteMarketDraft,\n+ 'get-season-info': getSeasonInfo,\n }\n"
|
|
621
|
-
},
|
|
622
|
-
{
|
|
623
|
-
"path": "backend/scheduler/src/jobs/auto-leagues-cycle.ts",
|
|
624
|
-
"status": "added",
|
|
625
|
-
"diff": "Index: backend/scheduler/src/jobs/auto-leagues-cycle.ts\n===================================================================\n--- backend/scheduler/src/jobs/auto-leagues-cycle.ts\t495e67b (parent)\n+++ backend/scheduler/src/jobs/auto-leagues-cycle.ts\t14e0625 (commit)\n@@ -0,0 +1,196 @@\n+import { SupabaseDirectClient } from 'shared/supabase/init'\n+import {\n+ SeasonEndTimeInfo,\n+ SeasonStatus,\n+ getSeasonEndTimeRow,\n+ insertSeasonEndTime,\n+ updateSeasonStatus,\n+ getEffectiveCurrentSeason,\n+} from 'shared/supabase/leagues'\n+import { LEAGUES_START } from 'common/leagues'\n+import { generateNextSeason, insertBots } from 'shared/generate-leagues'\n+import { sendEndOfSeasonNotificationsAndBonuses } from 'shared/payout-leagues'\n+import { updateLeague } from './update-league'\n+import { updateLeagueRanks } from './update-league-ranks'\n+import { log } from 'shared/utils'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { HOUR_MS } from 'common/util/time'\n+import * as dayjs from 'dayjs'\n+import * as utc from 'dayjs/plugin/utc'\n+import * as timezone from 'dayjs/plugin/timezone'\n+dayjs.extend(utc)\n+dayjs.extend(timezone)\n+\n+// How many days into the next month the season *could* end (randomly chosen).\n+const PACIFIC_TIMEZONE = 'America/Los_Angeles'\n+\n+export async function autoLeaguesCycle() {\n+ const pg = createSupabaseDirectClient()\n+ const currentSeason = await getEffectiveCurrentSeason()\n+\n+ log(`Running autoLeaguesCycle for active season: ${currentSeason}`)\n+\n+ let seasonInfo = await getSeasonEndTimeRow(pg, currentSeason)\n+ const now = Date.now()\n+\n+ // Initialization: Ensure the active season has an end time row.\n+ if (!seasonInfo) {\n+ log(`No row found for active season ${currentSeason}. Creating one.`)\n+ const randomEndTime = calculateRandomEndTime(currentSeason, now)\n+ await insertSeasonEndTime(pg, currentSeason, randomEndTime) // Inserts with status 'active'\n+ await generateNextSeason(pg, currentSeason)\n+ await insertBots(pg, currentSeason)\n+ seasonInfo = await getSeasonEndTimeRow(pg, currentSeason) // Re-fetch\n+ if (!seasonInfo) {\n+ log.error(\n+ `Failed to create and fetch row for season ${currentSeason}! Aborting.`\n+ )\n+ return\n+ }\n+ log(\n+ `Created row for season ${currentSeason} with end time ${dayjs(\n+ seasonInfo.end_time\n+ )\n+ .tz(PACIFIC_TIMEZONE)\n+ .toISOString()}`\n+ )\n+ }\n+\n+ log(\n+ `Found season info for ${currentSeason}: end_time=${dayjs(\n+ seasonInfo.end_time\n+ )\n+ .tz(PACIFIC_TIMEZONE)\n+ .toISOString()}, status=${seasonInfo.status}`\n+ )\n+\n+ // Check if the season end time has passed and it is currently active.\n+ if (now >= seasonInfo.end_time && seasonInfo.status === 'active') {\n+ log(`Season ${currentSeason} end time has passed. Processing rollover...`)\n+ let nextSeason: number | undefined // Declare outside to use in success log\n+ // Wrap the sequence in a transaction\n+ await pg.tx(async (tx) => {\n+ // 1. Mark current season as processing\n+ log(\n+ `Updating season ${currentSeason} status to 'processing' within transaction.`,\n+ { season: currentSeason }\n+ )\n+ await updateSeasonStatus(tx, currentSeason, 'processing')\n+\n+ // 2. Run final league updates for the season\n+ log(\n+ `Running updateLeague for season ${currentSeason} within transaction...`,\n+ { season: currentSeason }\n+ )\n+ await updateLeague(currentSeason, tx)\n+ log(\n+ `Running updateLeagueRanks for season ${currentSeason} within transaction...`,\n+ { season: currentSeason }\n+ )\n+ await updateLeagueRanks(currentSeason, tx)\n+\n+ // 3. Generate the *next* season's structure\n+ nextSeason = currentSeason + 1 // Assign here\n+ log(\n+ `Generating leagues for next season ${nextSeason} within transaction...`,\n+ { nextSeason }\n+ )\n+ await generateNextSeason(tx, nextSeason)\n+ await insertBots(tx, nextSeason)\n+\n+ // 4. Create the row for the *next* season and set its status to active\n+ log(\n+ `Creating row for next season ${nextSeason} with status 'active' within transaction.`,\n+ { nextSeason }\n+ )\n+ const nextSeasonEndTime = calculateRandomEndTime(nextSeason, now)\n+ await insertSeasonEndTime(tx, nextSeason, nextSeasonEndTime)\n+\n+ // 5. Run first league updates for the *next* season\n+ log(\n+ `Running updateLeague for season ${nextSeason} within transaction...`,\n+ { season: nextSeason }\n+ )\n+ await updateLeague(nextSeason, tx)\n+\n+ log(\n+ `Running updateLeagueRanks for season ${nextSeason} within transaction...`,\n+ { season: nextSeason }\n+ )\n+ await updateLeagueRanks(nextSeason, tx)\n+\n+ // 6. Send notifications and bonuses for the *completed* season\n+ log(\n+ `Sending end-of-season payouts/notifications for ${currentSeason} within transaction...`,\n+ { season: currentSeason }\n+ )\n+ await sendEndOfSeasonNotificationsAndBonuses(\n+ tx,\n+ currentSeason,\n+ nextSeason\n+ )\n+\n+ // 7. Mark the current season as complete\n+ log(\n+ `Updating season ${currentSeason} status to 'complete' within transaction.`,\n+ { season: currentSeason }\n+ )\n+ await updateSeasonStatus(tx, currentSeason, 'complete')\n+ })\n+\n+ // If the transaction succeeded:\n+ log(\n+ `Season ${currentSeason} rollover transaction complete. Season ${\n+ nextSeason ?? '(unknown)'\n+ } is now active.`\n+ )\n+ } else if (seasonInfo.status === 'processing') {\n+ log(`Season ${currentSeason} is already processing. Skipping.`)\n+ } else if (seasonInfo.status === 'complete') {\n+ log(\n+ `Season ${currentSeason} has already been completed. Waiting for next active season.`\n+ )\n+ // If getEffectiveCurrentSeason returns this one, there's an issue there or in state management.\n+ log.warn(\n+ `getEffectiveCurrentSeason returned ${currentSeason}, but its status is 'complete'. Check logic.`\n+ )\n+ } else {\n+ log(`Season ${currentSeason} end time has not yet passed.`)\n+ }\n+}\n+\n+// Helper to calculate a random end time for a *given* season\n+const calculateRandomEndTime = (season: number, now: number): Date => {\n+ // Calculate the start date of the given season\n+ const seasonStartDate = dayjs(LEAGUES_START)\n+ .tz(PACIFIC_TIMEZONE)\n+ .add(season - 1, 'month')\n+\n+ // Calculate the start of the month *after* the season month\n+ const minEndTime = seasonStartDate\n+ .add(1, 'month')\n+ .startOf('month')\n+ .tz(PACIFIC_TIMEZONE) // Keep PT\n+\n+ // Calculate the latest possible end time (start of the day after the offset)\n+ const latestEndTime = minEndTime\n+ .add(1, 'day')\n+ .startOf('day')\n+ .tz(PACIFIC_TIMEZONE) // Keep PT\n+\n+ // If the calculated latest end time is somehow *before* the minimum required time,\n+ if (latestEndTime.isBefore(minEndTime)) {\n+ log.warn(\n+ `Calculated latest end time ${latestEndTime.toISOString()} is before minimum end time ${minEndTime.toISOString()} for season ${season}. Using minimum time.`\n+ )\n+ return minEndTime.toDate()\n+ }\n+\n+ // Generate a random timestamp between min and max end times\n+ const randomEndTimeMillis =\n+ minEndTime.valueOf() +\n+ Math.random() * (latestEndTime.valueOf() - minEndTime.valueOf())\n+\n+ // Return as a JS Date object\n+ return dayjs(randomEndTimeMillis).tz(PACIFIC_TIMEZONE).toDate()\n+}\n"
|
|
626
|
-
},
|
|
627
|
-
{
|
|
628
|
-
"path": "backend/scheduler/src/jobs/index.ts",
|
|
629
|
-
"status": "modified",
|
|
630
|
-
"diff": "Index: backend/scheduler/src/jobs/index.ts\n===================================================================\n--- backend/scheduler/src/jobs/index.ts\t495e67b (parent)\n+++ backend/scheduler/src/jobs/index.ts\t14e0625 (commit)\n@@ -36,11 +36,17 @@\n import { updateUserPortfolioHistoriesCore } from 'shared/update-user-portfolio-histories-core'\n import { isProd } from 'shared/utils'\n import { sendMarketMovementNotifications } from 'shared/send-market-movement-notifications'\n import { sendUnseenMarketMovementNotifications } from 'shared/send-unseen-notifications'\n+import { autoLeaguesCycle } from './auto-leagues-cycle'\n \n export function createJobs() {\n return [\n+ createJob(\n+ 'auto-leagues-cycle',\n+ '0 */10 * * * *', // every 10 minutes\n+ autoLeaguesCycle\n+ ),\n // Hourly jobs:\n createJob(\n 'send-market-close-emails',\n '0 0 * * * *', // every hour\n@@ -63,9 +69,9 @@\n ),\n createJob(\n 'update-league',\n '0 */15 * * * *', // every 15 minutes\n- updateLeague\n+ () => updateLeague()\n ),\n createJob(\n 'check-push-receipts',\n '0 15 * * * *', // on the 15th minute of every hour\n@@ -181,9 +187,9 @@\n ),\n createJob(\n 'update-league-ranks',\n '0 0 0 * * *', // every day at midnight\n- updateLeagueRanks\n+ () => updateLeagueRanks()\n ),\n createJob(\n 'reset-betting-streaks',\n '0 0 0 * * *', // every day at midnight\n"
|
|
631
|
-
},
|
|
632
|
-
{
|
|
633
|
-
"path": "backend/scheduler/src/jobs/update-league-ranks.ts",
|
|
634
|
-
"status": "modified",
|
|
635
|
-
"diff": "Index: backend/scheduler/src/jobs/update-league-ranks.ts\n===================================================================\n--- backend/scheduler/src/jobs/update-league-ranks.ts\t495e67b (parent)\n+++ backend/scheduler/src/jobs/update-league-ranks.ts\t14e0625 (commit)\n@@ -1,13 +1,19 @@\n import { log, revalidateStaticProps } from 'shared/utils'\n-import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import {\n+ SupabaseDirectClient,\n+ createSupabaseDirectClient,\n+} from 'shared/supabase/init'\n import { bulkUpdate } from 'shared/supabase/utils'\n-import { SEASONS } from 'common/leagues'\n+import { getEffectiveCurrentSeason } from 'shared/supabase/leagues'\n \n-export async function updateLeagueRanks() {\n- const pg = createSupabaseDirectClient()\n+export async function updateLeagueRanks(\n+ manualSeason?: number,\n+ tx?: SupabaseDirectClient\n+) {\n+ const pg = tx ?? createSupabaseDirectClient()\n \n- const season = SEASONS[SEASONS.length - 1]\n+ const season = manualSeason ?? (await getEffectiveCurrentSeason())\n \n log('Loading user ranks...')\n const userIds = await pg.manyOrNone<{ id: string; rank: number }>(\n `select id, rank from users\n"
|
|
636
|
-
},
|
|
637
|
-
{
|
|
638
|
-
"path": "backend/scheduler/src/jobs/update-league.ts",
|
|
639
|
-
"status": "modified",
|
|
640
|
-
"diff": "Index: backend/scheduler/src/jobs/update-league.ts\n===================================================================\n--- backend/scheduler/src/jobs/update-league.ts\t495e67b (parent)\n+++ backend/scheduler/src/jobs/update-league.ts\t14e0625 (commit)\n@@ -5,19 +5,32 @@\n SupabaseDirectClient,\n createSupabaseDirectClient,\n } from 'shared/supabase/init'\n import { bulkUpdate } from 'shared/supabase/utils'\n-import { CURRENT_SEASON, getSeasonDates } from 'common/leagues'\n+import { getSeasonDates } from 'common/leagues'\n import { getProfitMetrics } from 'common/calculate'\n import { convertContract } from 'common/supabase/contracts'\n+import {\n+ getEffectiveCurrentSeason,\n+ getSeasonEndTimeRow,\n+} from 'shared/supabase/leagues'\n \n-export async function updateLeague() {\n- const pg = createSupabaseDirectClient()\n+export async function updateLeague(\n+ manualSeason?: number,\n+ tx?: SupabaseDirectClient\n+) {\n+ const pg = tx ?? createSupabaseDirectClient()\n \n- const season = CURRENT_SEASON\n- const { start, end } = getSeasonDates(season)\n+ const season = manualSeason ?? (await getEffectiveCurrentSeason())\n+ let seasonInfo = await getSeasonEndTimeRow(pg, season)\n+ if (!seasonInfo) {\n+ log('Season info not found. Exiting.')\n+ return\n+ }\n+ const { start } = getSeasonDates(season)\n+ log(`Season start: ${start}`)\n const seasonStart = start.getTime()\n- const seasonEnd = end.getTime()\n+ const seasonEnd = seasonInfo.end_time\n \n if (Date.now() > seasonEnd) {\n log('Season has ended. Exiting.')\n return\n@@ -31,29 +44,28 @@\n [season],\n (r) => r.id as string\n )\n log(`Loaded ${userIds.length} user ids.`)\n- // Earned fees from bets in your markets during the season.\n- const creatorFees = await pg.manyOrNone<{\n- user_id: string\n- category: string\n- amount: number\n- }>(\n- `select\n- contracts.creator_id as user_id,\n- 'CREATOR_FEE' as category,\n- sum((cb.data->'fees'->>'creatorFee')::numeric) as amount\n- from contract_bets cb\n- join contracts on contracts.id = cb.contract_id\n- where\n- cb.created_time > millis_to_ts($2)\n- and cb.created_time < millis_to_ts($3)\n- group by contracts.creator_id\n- `,\n- [season, seasonStart, seasonEnd]\n- )\n \n- console.log('creator fees', creatorFees.length)\n+ // // Earned fees from bets in your markets during the season.\n+ // const creatorFees = await pg.manyOrNone<{\n+ // user_id: string\n+ // category: string\n+ // amount: number\n+ // }>(\n+ // `select\n+ // contracts.creator_id as user_id,\n+ // 'CREATOR_FEE' as category,\n+ // sum((cb.data->'fees'->>'creatorFee')::numeric) as amount\n+ // from contract_bets cb\n+ // join contracts on contracts.id = cb.contract_id\n+ // where\n+ // cb.created_time > millis_to_ts($2)\n+ // and cb.created_time < millis_to_ts($3)\n+ // group by contracts.creator_id\n+ // `,\n+ // [season, seasonStart, seasonEnd]\n+ // )\n \n log('Loading bets...')\n const betData = await pg.manyOrNone<{ data: Bet }>(\n `select cb.data\n@@ -93,14 +105,10 @@\n !EXCLUDED_CONTRACT_SLUGS.has(contract.slug)\n ) {\n const { profit } = getProfitMetrics(contract, contractBets)\n if (isNaN(profit)) {\n- console.error(\n- 'Profit is NaN! contract',\n- contract.slug,\n- contract.id,\n- 'userId',\n- userId\n+ log.error(\n+ `Profit is NaN! contract ${contract.slug} (${contract.id}) userId ${userId}`\n )\n continue\n }\n \n@@ -114,9 +122,9 @@\n })\n }\n \n const amountByUserId = groupBy(\n- [...userProfit, ...creatorFees].map((u) => ({\n+ userProfit.map((u) => ({\n ...u,\n amount: +u.amount,\n })),\n 'user_id'\n@@ -135,22 +143,23 @@\n mana_earned_breakdown: `${JSON.stringify(manaEarnedBreakdown)}::jsonb`,\n })\n }\n \n- console.log('Mana earned updates', manaEarnedUpdates.length)\n+ log(`Mana earned updates: ${manaEarnedUpdates.length}`)\n \n await bulkUpdate(pg, 'leagues', ['user_id', 'season'], manaEarnedUpdates)\n log('Done.')\n }\n \n const getRelevantContracts = async (pg: SupabaseDirectClient, bets: Bet[]) => {\n const betContractIds = uniq(bets.map((b) => b.contractId))\n+ if (betContractIds.length === 0) return []\n return await pg.map(\n `select * from contracts\n where id in ($1:list)\n and token = 'MANA'\n and visibility = 'public'\n- and (data->'isRanked')::boolean is not false`,\n+ and coalesce((data->'isRanked')::boolean, true) = true`,\n [betContractIds],\n convertContract\n )\n }\n"
|
|
641
|
-
},
|
|
642
|
-
{
|
|
643
|
-
"path": "backend/scripts/generate-next-season.ts",
|
|
644
|
-
"status": "deleted",
|
|
645
|
-
"diff": "Index: backend/scripts/generate-next-season.ts\n===================================================================\n--- backend/scripts/generate-next-season.ts\t495e67b (parent)\n+++ backend/scripts/generate-next-season.ts\t14e0625 (commit)\n@@ -1,17 +0,0 @@\n-import { CURRENT_SEASON } from 'common/leagues'\n-import { runScript } from 'run-script'\n-import { generateNextSeason, insertBots } from 'shared/generate-leagues'\n-\n-if (require.main === module) {\n- runScript(async ({ pg }) => {\n- const newSeason = 24\n- if ((newSeason as any) <= CURRENT_SEASON) {\n- console.log('Are you sure you want to generate the current season?')\n- return\n- }\n-\n- await generateNextSeason(pg, newSeason)\n- await insertBots(pg, newSeason)\n- console.log('Completed generateNextSeason.')\n- })\n-}\n"
|
|
646
|
-
},
|
|
647
|
-
{
|
|
648
|
-
"path": "backend/shared/src/create-notification.ts",
|
|
649
|
-
"status": "modified",
|
|
650
|
-
"diff": "Index: backend/shared/src/create-notification.ts\n===================================================================\n--- backend/shared/src/create-notification.ts\t495e67b (parent)\n+++ backend/shared/src/create-notification.ts\t14e0625 (commit)\n@@ -73,9 +73,9 @@\n getUniqueBettorIdsForAnswer,\n getUniqueVoterIds,\n } from 'shared/supabase/contracts'\n import { richTextToString } from 'common/util/parse'\n-import { league_user_info } from 'common/leagues'\n+import { LeagueChangeNotificationData } from 'common/leagues'\n import { hasUserSeenMarket } from 'shared/helpers/seen-markets'\n import { isAdminId, isModId } from 'common/envs/constants'\n import {\n bulkInsertNotifications,\n@@ -898,46 +898,74 @@\n await createPushNotifications(bulkPushNotifications)\n await bulkInsertNotifications(bulkNotifications, pg)\n }\n \n-/** @deprecated until bulkified **/\n-export const createLeagueChangedNotification = async (\n- userId: string,\n- previousLeague: league_user_info | undefined,\n- newLeague: { season: number; division: number; cohort: string },\n- bonusAmount: number,\n- pg: SupabaseDirectClient\n+export const createLeagueChangedNotifications = async (\n+ pg: SupabaseDirectClient,\n+ data: LeagueChangeNotificationData[]\n ) => {\n- const privateUser = await getPrivateUser(userId)\n- if (!privateUser) return\n- const { sendToBrowser } = getNotificationDestinationsForUser(\n- privateUser,\n- 'league_changed'\n+ if (data.length === 0) return\n+\n+ log(`Creating ${data.length} league change notifications.`)\n+\n+ const userIds = data.map((d) => d.userId)\n+\n+ // Fetch all relevant private users in bulk\n+ const privateUsers = await pg.map(\n+ `select * from private_users where id = any($1)`,\n+ [userIds],\n+ convertPrivateUser\n )\n- if (!sendToBrowser) return\n+ const privateUserMap = new Map(privateUsers.map((user) => [user.id, user]))\n \n- const id = nanoid(6)\n- const data: LeagueChangeData = {\n- previousLeague,\n- newLeague,\n- bonusAmount,\n+ const bulkNotifications: Notification[] = []\n+\n+ for (const item of data) {\n+ const privateUser = privateUserMap.get(item.userId)\n+ if (!privateUser) {\n+ log(`Could not find private user ${item.userId}`)\n+ continue\n+ }\n+\n+ // Check notification preferences\n+ const { sendToBrowser } = getNotificationDestinationsForUser(\n+ privateUser,\n+ 'league_changed'\n+ )\n+ if (!sendToBrowser) continue\n+\n+ // Construct notification data\n+ const id = nanoid(6) // Still need a unique ID per notification\n+ const notificationData: LeagueChangeData = {\n+ previousLeague: item.previousLeague,\n+ newLeague: item.newLeague,\n+ bonusAmount: item.bonusAmount,\n+ }\n+\n+ const notification: Notification = {\n+ id,\n+ userId: item.userId,\n+ reason: 'league_changed',\n+ createdTime: Date.now(),\n+ isSeen: false,\n+ sourceId: id, // Use the generated id as sourceId for uniqueness? Or maybe season identifier? Let's use id for now.\n+ sourceText: item.bonusAmount.toString(),\n+ sourceType: 'league_change',\n+ sourceUpdateType: 'created',\n+ sourceUserName: '', // Not relevant for this type\n+ sourceUserUsername: '', // Not relevant for this type\n+ sourceUserAvatarUrl: '', // Not relevant for this type\n+ data: notificationData,\n+ }\n+ bulkNotifications.push(notification)\n }\n- const notification: Notification = {\n- id,\n- userId,\n- reason: 'league_changed',\n- createdTime: Date.now(),\n- isSeen: false,\n- sourceId: id,\n- sourceText: bonusAmount.toString(),\n- sourceType: 'league_change',\n- sourceUpdateType: 'created',\n- sourceUserName: '',\n- sourceUserUsername: '',\n- sourceUserAvatarUrl: '',\n- data,\n+\n+ if (bulkNotifications.length > 0) {\n+ await bulkInsertNotifications(bulkNotifications, pg)\n+ log(`Inserted ${bulkNotifications.length} league change notifications.`)\n+ } else {\n+ log('No league change notifications met filter criteria.')\n }\n- await insertNotificationToSupabase(notification, pg)\n }\n \n export const createLikeNotification = async (reaction: Reaction) => {\n const { reaction_id, content_owner_id, user_id, content_id, content_type } =\n"
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
"path": "backend/shared/src/generate-leagues.ts",
|
|
654
|
-
"status": "modified",
|
|
655
|
-
"diff": "Index: backend/shared/src/generate-leagues.ts\n===================================================================\n--- backend/shared/src/generate-leagues.ts\t495e67b (parent)\n+++ backend/shared/src/generate-leagues.ts\t14e0625 (commit)\n@@ -2,28 +2,28 @@\n import { pgp, SupabaseDirectClient } from './supabase/init'\n import { genNewAdjectiveAnimal } from 'common/util/adjective-animal'\n import { BOT_USERNAMES, OPTED_OUT_OF_LEAGUES } from 'common/envs/constants'\n import {\n- CURRENT_SEASON,\n getCohortSize,\n getDivisionChange,\n getSeasonDates,\n league_user_info,\n MAX_COHORT_SIZE,\n- SEASONS,\n } from 'common/leagues'\n import { getCurrentPortfolio } from './helpers/portfolio'\n-import { createLeagueChangedNotification } from 'shared/create-notification'\n+import { createLeagueChangedNotifications } from 'shared/create-notification'\n import { bulkInsert } from './supabase/utils'\n import { log } from './utils'\n+import { getEffectiveCurrentSeason } from './supabase/leagues'\n \n export async function generateNextSeason(\n pg: SupabaseDirectClient,\n season: number\n ) {\n- console.log('Generating season', season)\n+ log(`Generating season ${season}`)\n const prevSeason = season - 1\n const startDate = getSeasonDates(prevSeason).start\n+ // const startDate = new Date('2025-01-01')\n const rows = await pg.manyOrNone<league_user_info>(\n `select * from user_league_info\n where season = $1\n order by mana_earned desc`,\n@@ -46,12 +46,12 @@\n )\n const activeUserIdsSet = new Set(activeUserIds.map((u) => u.user_id))\n \n const usersByDivision = generateDivisions(rows, activeUserIdsSet)\n- console.log('usersByDivision', usersByDivision)\n+ log(`usersByDivision`, { usersByDivision })\n \n- const userCohorts = await generateCohorts(pg, usersByDivision)\n- console.log('user cohorts', userCohorts)\n+ const userCohorts = generateCohorts(usersByDivision)\n+ log(`user cohorts`, { userCohorts })\n \n const leagueInserts = Object.entries(userCohorts).map(\n ([userId, { division, cohort }]) => ({\n user_id: userId,\n@@ -59,10 +59,11 @@\n division,\n cohort,\n })\n )\n- console.log('league inserts', leagueInserts)\n- console.log('Inserting', leagueInserts.length, 'cohort rows')\n+ log(`league inserts`, { leagueInserts })\n+ log(`Inserting ${leagueInserts.length} cohort rows`)\n+ if (leagueInserts.length === 0) return\n \n // Bulk insert leagues.\n const insertStatement =\n pgp.helpers.insert(\n@@ -83,9 +84,9 @@\n const usersByNewDivision: Record<number, string[]> = {}\n \n const rowsByCohort = groupBy(rows, 'cohort')\n const activeRows = rows.filter((r) => activeUserIds.has(r.user_id))\n- console.log('rows', rows.length, 'active rows', activeRows.length)\n+ log(`rows: ${rows.length}, active rows: ${activeRows.length}`)\n \n for (const row of activeRows) {\n const { user_id, division, cohort, rank, mana_earned } = row\n \n@@ -107,12 +108,9 @@\n \n return usersByNewDivision\n }\n \n-const generateCohorts = async (\n- pg: SupabaseDirectClient,\n- usersByDivision: { [division: number]: string[] }\n-) => {\n+const generateCohorts = (usersByDivision: { [division: number]: string[] }) => {\n const userCohorts: {\n [userId: string]: { division: number; cohort: string }\n } = {}\n const cohortSet = new Set<string>()\n@@ -124,17 +122,10 @@\n const numCohorts = Math.ceil(divisionUserIds.length / cohortSize)\n const usersPerCohort = Math.ceil(divisionUserIds.length / numCohorts)\n const cohortsWithOneLess =\n usersPerCohort * numCohorts - divisionUserIds.length\n- console.log(\n- 'division users',\n- divisionUserIds.length,\n- 'numCohorts',\n- numCohorts,\n- 'usersPerCohort',\n- usersPerCohort,\n- 'cohortsWithOneLess',\n- cohortsWithOneLess\n+ log(\n+ `division users: ${divisionUserIds.length}, numCohorts: ${numCohorts}, usersPerCohort: ${usersPerCohort}, cohortsWithOneLess: ${cohortsWithOneLess}`\n )\n \n let remainingUserIds = shuffle(divisionUserIds.concat())\n let i = 0\n@@ -150,9 +141,9 @@\n userCohorts[userId] = { division, cohort }\n }\n remainingUserIds = remainingUserIds.filter((uid) => !userCohorts[uid])\n i++\n- console.log('cohort', cohort, cohortOfUsers.length)\n+ log(`cohort: ${cohort}, cohortOfUsers: ${cohortOfUsers.length}`)\n }\n }\n \n return userCohorts\n@@ -298,11 +289,11 @@\n if (OPTED_OUT_OF_LEAGUES.includes(userId)) {\n log('User opted out of leagues', userId)\n return\n }\n+ log('Adding user to league', userId)\n+ const season = await getEffectiveCurrentSeason()\n \n- const season = SEASONS[SEASONS.length - 1]\n-\n const existingLeague = await pg.oneOrNone<{\n season: number\n division: number\n cohort: string\n@@ -319,29 +310,32 @@\n const data = await addUserToLeague(pg, userId, season, division)\n if (!data) return\n const cohort = data.cohort\n \n- await createLeagueChangedNotification(\n- userId,\n- undefined,\n- { season, division, cohort },\n- 0,\n- pg\n- )\n+ await createLeagueChangedNotifications(pg, [\n+ {\n+ userId,\n+ previousLeague: undefined,\n+ newLeague: { season, division, cohort },\n+ bonusAmount: 0,\n+ },\n+ ])\n return { season, division, cohort }\n }\n \n export const addNewUserToLeague = async (\n pg: SupabaseDirectClient,\n userId: string\n ) => {\n- const data = await addUserToLeague(pg, userId, CURRENT_SEASON, 1)\n+ const season = await getEffectiveCurrentSeason()\n+ const data = await addUserToLeague(pg, userId, season, 1)\n if (!data) return\n- const { season, division, cohort } = data\n- await createLeagueChangedNotification(\n- userId,\n- undefined,\n- { season, division, cohort },\n- 0,\n- pg\n- )\n+ const { division, cohort } = data\n+ await createLeagueChangedNotifications(pg, [\n+ {\n+ userId,\n+ previousLeague: undefined,\n+ newLeague: { season, division, cohort },\n+ bonusAmount: 0,\n+ },\n+ ])\n }\n"
|
|
656
|
-
},
|
|
657
|
-
{
|
|
658
|
-
"path": "backend/shared/src/payout-leagues.ts",
|
|
659
|
-
"status": "modified",
|
|
660
|
-
"diff": "Index: backend/shared/src/payout-leagues.ts\n===================================================================\n--- backend/shared/src/payout-leagues.ts\t495e67b (parent)\n+++ backend/shared/src/payout-leagues.ts\t14e0625 (commit)\n@@ -1,17 +1,21 @@\n-import { getLeaguePrize, league_user_info } from 'common/leagues'\n-import { SupabaseDirectClient } from './supabase/init'\n-import { createLeagueChangedNotification } from './create-notification'\n-import { runTxnFromBank } from './txn/run-txn'\n+import {\n+ getLeaguePrize,\n+ league_user_info,\n+ LeagueChangeNotificationData,\n+} from 'common/leagues'\n+import { SupabaseTransaction } from './supabase/init'\n+import { createLeagueChangedNotifications } from './create-notification'\n+import { TxnData, insertTxns } from './txn/run-txn'\n+import { bulkIncrementBalances } from './supabase/users'\n \n-import { LeaguePrizeTxn } from 'common/txn'\n-import { chunk } from 'lodash'\n+import { log } from './utils'\n \n export const sendEndOfSeasonNotificationsAndBonuses = async (\n- pg: SupabaseDirectClient,\n- prevSeason: number\n+ pg: SupabaseTransaction,\n+ prevSeason: number,\n+ newSeason: number\n ) => {\n- const newSeason = prevSeason + 1\n const prevRows = await pg.manyOrNone<league_user_info>(\n `select * from user_league_info\n where season = $1`,\n [prevSeason]\n@@ -24,71 +28,106 @@\n const prevRowsByUserId = Object.fromEntries(\n prevRows.map((r) => [r.user_id, r])\n )\n \n- for (const rows of chunk(newRows, 5)) {\n- await Promise.all(\n- rows.map((newRow) => {\n- const prevRow = prevRowsByUserId[newRow.user_id] as\n- | league_user_info\n- | undefined\n- if (prevRow) {\n- return sendEndOfSeasonNotificationAndBonus(\n- pg,\n- prevRow,\n- newRow,\n- prevSeason\n- )\n- }\n- return Promise.resolve()\n+ // Fetch all users who already received prizes for this season\n+ const alreadyGotPrizeRows = await pg.manyOrNone<{ to_id: string }>(\n+ `select to_id from txns\n+ where category = 'LEAGUE_PRIZE'\n+ and data->'data'->>'season' = $1`,\n+ [prevSeason.toString()]\n+ )\n+ // Create a Set for efficient lookups\n+ const alreadyGotPrizeUserIds = new Set(\n+ alreadyGotPrizeRows.map((row) => row.to_id)\n+ )\n+\n+ const notificationData: LeagueChangeNotificationData[] = []\n+\n+ const prizesToAward: Array<{\n+ userId: string\n+ prevRow: league_user_info\n+ newRow: league_user_info\n+ prize: number\n+ }> = []\n+\n+ // Check eligibility for users in this chunk\n+ for (const newRow of newRows) {\n+ const prevRow = prevRowsByUserId[newRow.user_id]\n+ if (!prevRow) continue\n+\n+ const prize = getLeaguePrize(prevRow.division, prevRow.rank)\n+ if (!prize || prize <= 0) continue\n+\n+ // Check if prize already awarded using our Set\n+ if (!alreadyGotPrizeUserIds.has(newRow.user_id)) {\n+ prizesToAward.push({\n+ userId: newRow.user_id,\n+ prevRow,\n+ newRow,\n+ prize,\n })\n- )\n+ } else {\n+ log(\n+ `User ${newRow.user_id} already received prize for season ${prevSeason}`\n+ )\n+ }\n }\n-}\n \n-const sendEndOfSeasonNotificationAndBonus = async (\n- pg: SupabaseDirectClient,\n- prevRow: league_user_info,\n- newRow: league_user_info,\n- season: number\n-) => {\n- const { user_id: userId, division, rank } = prevRow\n+ if (prizesToAward.length > 0) {\n+ const balanceIncrements: Array<{\n+ id: string\n+ balance: number\n+ totalDeposits: number\n+ }> = []\n+ const txnDatas: TxnData[] = []\n+ const currentNotifications: LeagueChangeNotificationData[] = []\n \n- const prize = getLeaguePrize(division, rank)\n- if (!prize) return\n+ for (const prizeAward of prizesToAward) {\n+ const { userId, prevRow, newRow, prize } = prizeAward\n \n- const data: Omit<LeaguePrizeTxn, 'fromId' | 'id' | 'createdTime'> = {\n- fromType: 'BANK',\n- toId: userId,\n- toType: 'USER',\n- amount: prize,\n- token: 'M$',\n- category: 'LEAGUE_PRIZE',\n- data: prevRow,\n- }\n+ // Prepare balance increment data\n+ balanceIncrements.push({\n+ id: userId,\n+ balance: prize,\n+ totalDeposits: prize,\n+ })\n \n- const alreadyGotPrize = await pg.oneOrNone(\n- `select * from txns\n- where category = 'LEAGUE_PRIZE'\n- and data->'data'->>'season' = $1\n- and to_id = $2`,\n- [season.toString(), userId]\n- )\n- if (!alreadyGotPrize) {\n- console.log(\n- 'send',\n- newRow.user_id,\n- 'division',\n- division,\n- 'rank',\n- rank,\n- 'prize',\n- prize,\n- data.token\n- )\n+ // Construct transaction data - including fromId now\n+ const txnData: TxnData = {\n+ fromId: 'BANK',\n+ fromType: 'BANK',\n+ toId: userId,\n+ toType: 'USER',\n+ amount: prize,\n+ token: 'M$',\n+ category: 'LEAGUE_PRIZE',\n+ data: { ...prevRow, season: prevSeason },\n+ }\n+ txnDatas.push(txnData)\n \n- await pg.tx(async (tx) => {\n- await runTxnFromBank(tx, data)\n- await createLeagueChangedNotification(userId, prevRow, newRow, prize, tx)\n- })\n+ // Prepare notification data\n+ currentNotifications.push({\n+ userId,\n+ previousLeague: prevRow,\n+ newLeague: newRow,\n+ bonusAmount: prize,\n+ })\n+ }\n+\n+ // Execute bulk updates and inserts within the transaction\n+ // Pass the transaction object 'tx' to the helper functions\n+ await bulkIncrementBalances(pg, balanceIncrements)\n+ await insertTxns(pg, txnDatas)\n+\n+ // Add notifications to the main list only if the transaction succeeds\n+ notificationData.push(...currentNotifications)\n }\n+\n+ // Call the bulk notification function after collecting all data\n+ if (notificationData.length > 0) {\n+ log(`Sending ${notificationData.length} league change notifications.`)\n+ await createLeagueChangedNotifications(pg, notificationData)\n+ } else {\n+ log(`No league change notifications to send for season ${prevSeason}.`)\n+ }\n }\n"
|
|
661
|
-
},
|
|
662
|
-
{
|
|
663
|
-
"path": "backend/shared/src/supabase/leagues.ts",
|
|
664
|
-
"status": "modified",
|
|
665
|
-
"diff": "Index: backend/shared/src/supabase/leagues.ts\n===================================================================\n--- backend/shared/src/supabase/leagues.ts\t495e67b (parent)\n+++ backend/shared/src/supabase/leagues.ts\t14e0625 (commit)\n@@ -1,7 +1,9 @@\n import { APIError } from 'common/api/utils'\n import { convertLeague } from 'common/supabase/leagues'\n import { type SupabaseClient, run } from 'common/supabase/utils'\n+import { createSupabaseDirectClient, SupabaseDirectClient } from './init'\n+import { log } from '../utils' // Assuming log exists\n \n export async function getLeaguesForUser(\n db: SupabaseClient,\n filters: {\n@@ -26,4 +28,83 @@\n \n const res = await run(q)\n return res.data.map(convertLeague)\n }\n+\n+export type SeasonStatus = 'active' | 'processing' | 'complete'\n+\n+export type SeasonEndTimeInfo = {\n+ season: number\n+ end_time: number\n+ status: SeasonStatus\n+}\n+\n+export const getSeasonEndTimeRow = async (\n+ pg: SupabaseDirectClient,\n+ season: number\n+): Promise<SeasonEndTimeInfo | null> => {\n+ const row = await pg.oneOrNone(\n+ `SELECT season, ts_to_millis(end_time) as end_time, status\n+ FROM leagues_season_end_times\n+ WHERE season = $1`,\n+ [season]\n+ )\n+ return row as SeasonEndTimeInfo | null\n+}\n+\n+export const insertSeasonEndTime = async (\n+ pg: SupabaseDirectClient,\n+ season: number,\n+ endTime: Date\n+): Promise<void> => {\n+ await pg.none(\n+ // Inserts with default status 'active'\n+ `INSERT INTO leagues_season_end_times (season, end_time)\n+ VALUES ($1, $2)\n+ ON CONFLICT (season) DO NOTHING`,\n+ [season, endTime.toISOString()]\n+ )\n+}\n+\n+export const updateSeasonStatus = async (\n+ pg: SupabaseDirectClient,\n+ season: number,\n+ status: SeasonStatus\n+): Promise<void> => {\n+ await pg.none(\n+ `UPDATE leagues_season_end_times\n+ SET status = $2\n+ WHERE season = $1`,\n+ [season, status]\n+ )\n+}\n+\n+export const getEffectiveCurrentSeason = async (): Promise<number> => {\n+ const pg = createSupabaseDirectClient()\n+ // Find the season marked as active\n+ const activeSeason = await pg.oneOrNone<{ season: number }>(\n+ `SELECT season FROM leagues_season_end_times WHERE status = 'active' LIMIT 1`\n+ )\n+\n+ if (activeSeason?.season) {\n+ return activeSeason.season\n+ }\n+\n+ // If no season is active (error state or initialization), find the latest completed and return + 1\n+ // This case *shouldn't* happen in normal operation after initialization.\n+ log.error(\n+ 'No active season found in leagues_season_end_times! Falling back to latest completed + 1.'\n+ )\n+ const latestCompleted = await pg.oneOrNone<{ season: number }>(\n+ `SELECT max(season) as season FROM leagues_season_end_times WHERE status = 'complete'`\n+ )\n+\n+ if (latestCompleted?.season) {\n+ return latestCompleted.season + 1\n+ }\n+\n+ // If the table is completely empty (very first run ever)\n+ log.error(\n+ 'leagues_season_end_times table appears empty. Defaulting to season 1.'\n+ )\n+ return 1\n+}\n"
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
"path": "backend/supabase/leagues_season_end_times.sql",
|
|
669
|
-
"status": "added",
|
|
670
|
-
"diff": "Index: backend/supabase/leagues_season_end_times.sql\n===================================================================\n--- backend/supabase/leagues_season_end_times.sql\t495e67b (parent)\n+++ backend/supabase/leagues_season_end_times.sql\t14e0625 (commit)\n@@ -0,0 +1,43 @@\n+create table\n+ leagues_season_end_times (\n+ season int primary key,\n+ end_time timestamp with time zone not null,\n+ status text not null default 'active' check (status in ('active', 'processing', 'complete'))\n+ );\n+\n+-- Row Level Security\n+alter table leagues_season_end_times enable row level security;\n+\n+-- Backfill script for leagues_season_end_times table\n+insert into\n+ leagues_season_end_times (season, end_time, status) -- Changed processed to status\n+values\n+ (1, '2023-06-01T12:06:23-07:00', 'complete'), -- Set status to complete\n+ (2, '2023-07-01T12:22:53-07:00', 'complete'),\n+ (3, '2023-08-01T17:05:29-07:00', 'complete'),\n+ (4, '2023-09-01T20:20:04-07:00', 'complete'),\n+ (5, '2023-10-01T11:17:16-07:00', 'complete'),\n+ (6, '2023-11-01T14:01:38-07:00', 'complete'),\n+ (7, '2023-12-01T14:02:25-08:00', 'complete'),\n+ (8, '2024-01-01T19:06:12-08:00', 'complete'),\n+ (9, '2024-02-01T17:51:49-08:00', 'complete'),\n+ (10, '2024-03-01T15:30:22-08:00', 'complete'),\n+ (11, '2024-04-01T21:43:18-08:00', 'complete'),\n+ (12, '2024-05-01T16:32:08-07:00', 'complete'),\n+ (13, '2024-06-01T11:10:19-07:00', 'complete'),\n+ (14, '2024-07-01T18:41:35-07:00', 'complete'),\n+ (15, '2024-08-01T22:11:54-07:00', 'complete'),\n+ (16, '2024-09-01T12:54:14-07:00', 'complete'),\n+ (17, '2024-10-01T15:55:00-07:00', 'complete'),\n+ (18, '2024-11-02T22:18:29+00:00', 'complete'),\n+ (19, '2024-12-02T10:19:34-08:00', 'complete'),\n+ (20, '2025-01-01T22:06:13-08:00', 'complete'),\n+ (21, '2025-02-01T22:18:13-08:00', 'complete'),\n+ (22, '2025-03-02T02:25:41-08:00', 'complete'),\n+ (23, '2025-04-01T20:32:23-08:00', 'complete')\n+ -- (24, '2025-05-01T00:00:00-07:00', 'active') -- Example: Needs a placeholder or actual end time\n+on conflict (season) do\n+update\n+set\n+ end_time = EXCLUDED.end_time,\n+ status = EXCLUDED.status;\n"
|
|
671
|
-
},
|
|
672
|
-
{
|
|
673
|
-
"path": "common/src/api/schema.ts",
|
|
674
|
-
"status": "modified",
|
|
675
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\t495e67b (parent)\n+++ common/src/api/schema.ts\t14e0625 (commit)\n@@ -2368,8 +2368,22 @@\n id: z.coerce.number(),\n })\n .strict(),\n },\n+ 'get-season-info': {\n+ method: 'GET',\n+ visibility: 'public',\n+ authed: false,\n+ props: z.object({\n+ season: z.coerce.number().int().positive().optional(),\n+ }),\n+ returns: {} as {\n+ season: number\n+ startTime: number // epoch ms\n+ endTime: number | null // epoch ms, null if a *mystery* for clients\n+ status: 'active' | 'processing' | 'complete'\n+ },\n+ },\n } as const)\n \n export type APIPath = keyof typeof API\n export type APISchema<N extends APIPath> = (typeof API)[N]\n"
|
|
676
|
-
},
|
|
677
|
-
{
|
|
678
|
-
"path": "common/src/leagues.ts",
|
|
679
|
-
"status": "modified",
|
|
680
|
-
"diff": "Index: common/src/leagues.ts\n===================================================================\n--- common/src/leagues.ts\t495e67b (parent)\n+++ common/src/leagues.ts\t14e0625 (commit)\n@@ -1,42 +1,9 @@\n+import { LeagueChangeData } from './notification'\n import { Row } from './supabase/utils'\n \n-export type season = (typeof SEASONS)[number]\n-\n-export const SEASONS = [\n- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,\n- 23, 24,\n-] as const\n-export const CURRENT_SEASON = SEASONS[SEASONS.length - 1]\n-\n export const LEAGUES_START = new Date('2023-05-01T00:00:00-07:00') // Pacific Daylight Time (PDT) as time zone offset\n \n-const SEASON_END_TIMES = [\n- new Date('2023-06-01T12:06:23-07:00'),\n- new Date('2023-07-01T12:22:53-07:00'),\n- new Date('2023-08-01T17:05:29-07:00'),\n- new Date('2023-09-01T20:20:04-07:00'),\n- new Date('2023-10-01T11:17:16-07:00'),\n- new Date('2023-11-01T14:01:38-07:00'),\n- new Date('2023-12-01T14:02:25-08:00'),\n- new Date('2024-01-01T19:06:12-08:00'),\n- new Date('2024-02-01T17:51:49-08:00'),\n- new Date('2024-03-01T15:30:22-08:00'),\n- new Date('2024-04-01T21:43:18-08:00'),\n- new Date('2024-05-01T16:32:08-07:00'),\n- new Date('2024-06-01T11:10:19-07:00'),\n- new Date('2024-07-01T18:41:35-07:00'),\n- new Date('2024-08-01T22:11:54-07:00'), // 16\n- new Date('2024-09-01T12:54:14-07:00'),\n- new Date('2024-10-01T15:55:00-07:00'),\n- new Date('2024-11-02T22:18:29+00:00'),\n- new Date('2024-12-02T10:19:34-08:00'),\n- new Date('2025-01-01T22:06:13-08:00'),\n- new Date('2025-02-01T22:18:13-08:00'),\n- new Date('2025-03-02T02:25:41-08:00'),\n- new Date('2025-04-01T20:32:23-08:00'),\n-]\n-\n export type League = {\n season: number\n division: number\n cohort: string\n@@ -57,41 +24,28 @@\n export const getSeasonDates = (season: number) => {\n const start = new Date(LEAGUES_START)\n start.setMonth(start.getMonth() + season - 1)\n \n- let end: Date\n- if (SEASON_END_TIMES[season - 1]) {\n- end = new Date(SEASON_END_TIMES[season - 1])\n- } else {\n- end = new Date(LEAGUES_START)\n- end.setMonth(end.getMonth() + season)\n- // Add a day, though the random close time will be some time before this.\n- end.setDate(end.getDate() + 1)\n- }\n+ // NOTE: The exact season end time used for backend logic (rollover, payouts)\n+ // is now stored in the leagues_season_end_times table in the database.\n+ // This function now calculates an approximate end date primarily for display purposes\n+ // or as a fallback if the database row doesn't exist.\n+ const approxEnd = new Date(LEAGUES_START)\n+ approxEnd.setMonth(approxEnd.getMonth() + season)\n+ // Add a day, just to be safely after the potential random close time.\n+ approxEnd.setDate(approxEnd.getDate() + 1)\n \n- return { start, end }\n+ return { start, approxEnd }\n }\n \n export const getSeasonCountdownEnd = (season: number) => {\n const end = new Date(LEAGUES_START)\n end.setMonth(end.getMonth() + season)\n return end\n }\n \n-export const getSeasonStatus = (season: number) => {\n- const { start } = getSeasonDates(season)\n- const countdownEnd = getSeasonCountdownEnd(season)\n- const now = new Date()\n- if (now < start) {\n- return 'upcoming'\n- } else if (now > countdownEnd) {\n- if (!SEASON_END_TIMES[season - 1]) {\n- return 'closing-period'\n- }\n- return 'ended'\n- } else {\n- return 'current'\n- }\n+export type LeagueChangeNotificationData = LeagueChangeData & {\n+ userId: string\n }\n \n export const DIVISION_NAMES = {\n 0: 'Silicon',\n@@ -265,14 +219,15 @@\n \n export const parseLeaguePath = (\n slugs: string[],\n rowsBySeason: { [season: number]: league_user_info[] },\n+ seasons: number[],\n userId?: string\n ) => {\n const [seasonSlug, divisionSlug, cohortSlug, userIdSlug] = slugs\n let season = +seasonSlug\n- if (!SEASONS.includes(season as season)) {\n- season = CURRENT_SEASON\n+ if (!seasons.includes(season)) {\n+ season = seasons[seasons.length - 1]\n }\n \n const seasonRows = rowsBySeason[season] ?? []\n const userIdToFind = userIdSlug || userId\n"
|
|
681
|
-
},
|
|
682
|
-
{
|
|
683
|
-
"path": "web/components/leagues/mana-earned-breakdown.tsx",
|
|
684
|
-
"status": "modified",
|
|
685
|
-
"diff": "Index: web/components/leagues/mana-earned-breakdown.tsx\n===================================================================\n--- web/components/leagues/mana-earned-breakdown.tsx\t495e67b (parent)\n+++ web/components/leagues/mana-earned-breakdown.tsx\t14e0625 (commit)\n@@ -50,9 +50,9 @@\n // (mana_earned_breakdown.MARKET_BOOST_REDEEM ?? 0) +\n // (mana_earned_breakdown.AD_REDEEM ?? 0),\n // } as { [key: string]: number }\n \n- const { start, end } = getSeasonDates(season)\n+ const { start, approxEnd: end } = getSeasonDates(season)\n const loadingBets = useBetsOnce((params) => api('bets', params), {\n userId: user.id,\n afterTime: start.getTime(),\n beforeTime: end.getTime(),\n"
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
"path": "web/hooks/use-leagues.ts",
|
|
689
|
-
"status": "modified",
|
|
690
|
-
"diff": "Index: web/hooks/use-leagues.ts\n===================================================================\n--- web/hooks/use-leagues.ts\t495e67b (parent)\n+++ web/hooks/use-leagues.ts\t14e0625 (commit)\n@@ -7,21 +7,23 @@\n getOwnedLeagueChats,\n } from 'web/lib/supabase/leagues'\n import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n import { usePersistentLocalState } from './use-persistent-local-state'\n+import { useAPIGetter } from './use-api-getter'\n \n export const useLeagueInfo = (userId: string | null | undefined) => {\n const [leagueInfo, setLeagueInfo] = usePersistentLocalState<\n league_user_info | null | undefined\n >(undefined, `league-info-${userId}`)\n+ const { data: seasonInfo } = useAPIGetter('get-season-info', {})\n \n useEffect(() => {\n- if (userId) {\n- getLeagueInfo(userId).then((result) => {\n+ if (userId && seasonInfo?.season) {\n+ getLeagueInfo(userId, seasonInfo.season).then((result) => {\n setLeagueInfo(result as league_user_info | null)\n })\n }\n- }, [userId])\n+ }, [userId, seasonInfo?.season])\n \n return leagueInfo\n }\n \n"
|
|
691
|
-
},
|
|
692
|
-
{
|
|
693
|
-
"path": "web/lib/supabase/leagues.ts",
|
|
694
|
-
"status": "modified",
|
|
695
|
-
"diff": "Index: web/lib/supabase/leagues.ts\n===================================================================\n--- web/lib/supabase/leagues.ts\t495e67b (parent)\n+++ web/lib/supabase/leagues.ts\t14e0625 (commit)\n@@ -1,14 +1,14 @@\n-import { CURRENT_SEASON, league_user_info } from 'common/leagues'\n+import { league_user_info } from 'common/leagues'\n import { convertSQLtoTS, tsToMillis } from 'common/supabase/utils'\n import { db } from './db'\n \n-export async function getLeagueInfo(userId: string) {\n+export async function getLeagueInfo(userId: string, season: number) {\n const { data } = await db\n .from('user_league_info')\n .select('*')\n .eq('user_id', userId)\n- .eq('season', CURRENT_SEASON)\n+ .eq('season', season)\n .limit(1)\n if (data && data.length > 0) {\n return data[0]\n }\n"
|
|
696
|
-
},
|
|
697
|
-
{
|
|
698
|
-
"path": "web/pages/leagues/[[...leagueSlugs]].tsx",
|
|
699
|
-
"status": "modified",
|
|
700
|
-
"diff": "Index: web/pages/leagues/[[...leagueSlugs]].tsx\n===================================================================\n--- web/pages/leagues/[[...leagueSlugs]].tsx\t495e67b (parent)\n+++ web/pages/leagues/[[...leagueSlugs]].tsx\t14e0625 (commit)\n@@ -4,16 +4,12 @@\n import { useRouter } from 'next/router'\n \n import {\n DIVISION_NAMES,\n- CURRENT_SEASON,\n getLeaguePath,\n league_user_info,\n parseLeaguePath,\n- getSeasonStatus,\n- SEASONS,\n getSeasonMonth,\n- season,\n getSeasonCountdownEnd,\n getSeasonDates,\n getMaxDivisionBySeason,\n getDemotionAndPromotionCountBySeason,\n@@ -40,20 +36,73 @@\n import { InfoTooltip } from 'web/components/widgets/info-tooltip'\n import { LoadingIndicator } from 'web/components/widgets/loading-indicator'\n import { useEffectCheckEquality } from 'web/hooks/use-effect-check-equality'\n import Link from 'next/link'\n+import { DAY_MS } from 'common/util/time'\n+import { api } from 'web/lib/api/api'\n+import { APIResponse } from 'common/api/schema'\n \n-export default function Leagues() {\n+export async function getStaticPaths() {\n+ return { paths: [], fallback: 'blocking' }\n+}\n+\n+export async function getStaticProps(props: {\n+ params: { leagueSlugs: string[] }\n+}) {\n+ try {\n+ // Extract season from URL if available\n+ const leagueSlugs = props.params?.leagueSlugs || []\n+ const seasonParam = leagueSlugs[0]\n+ const season =\n+ seasonParam && !isNaN(parseInt(seasonParam))\n+ ? parseInt(seasonParam)\n+ : undefined\n+\n+ const currentSeasonInfo = await api('get-season-info', {})\n+ const seasonInfo = await api('get-season-info', season ? { season } : {})\n+ return {\n+ props: {\n+ initialSeasonInfo: seasonInfo,\n+ currentSeasonInfo,\n+ },\n+ revalidate: 60, // Revalidate every minute\n+ }\n+ } catch (err) {\n+ console.error('Error fetching season info:', err)\n+ return {\n+ props: {\n+ initialSeasonInfo: null,\n+ },\n+ revalidate: 60, // Retry sooner if there was an error\n+ }\n+ }\n+}\n+\n+interface LeaguesProps {\n+ initialSeasonInfo: APIResponse<'get-season-info'> | null\n+ currentSeasonInfo: APIResponse<'get-season-info'> | null\n+}\n+\n+export default function Leagues(props: LeaguesProps) {\n+ const { initialSeasonInfo, currentSeasonInfo } = props\n const user = useUser()\n \n const [rows, setRows] = usePersistentInMemoryState<league_user_info[]>(\n [],\n 'league-rows'\n )\n \n const rowsBySeason = useMemo(() => groupBy(rows, 'season'), [rows])\n+ const seasonInfo = initialSeasonInfo ?? currentSeasonInfo\n+ const seasons = useMemo(() => {\n+ const seasons = []\n+ for (let i = 1; i <= (currentSeasonInfo?.season ?? 1); i++) {\n+ seasons.push(i)\n+ }\n+ return seasons\n+ }, [currentSeasonInfo?.season])\n \n- const [season, setSeason] = useState<number>(CURRENT_SEASON)\n+ const [season, setSeason] = useState<number>(seasonInfo?.season ?? 1)\n useEffect(() => {\n getLeagueRows(season).then((rows) => {\n setRows((currRows) =>\n currRows.filter((r) => r.season !== season).concat(rows)\n@@ -97,17 +146,16 @@\n const onSetSeason = (newSeason: number) => {\n const { season, division, cohort } = parseLeaguePath(\n [newSeason.toString()],\n rowsBySeason,\n+ seasons,\n user?.id\n )\n \n if (cohort) {\n- replace(getLeaguePath(season, division, cohort), undefined, {\n- shallow: true,\n- })\n+ replace(getLeaguePath(season, division, cohort), undefined)\n } else {\n- replace(`${season}`, undefined, { shallow: true })\n+ replace(`${season}`, undefined)\n }\n }\n \n const onSetDivision = (division: number) => {\n@@ -133,8 +181,9 @@\n \n const { season, division, cohort, highlightedUserId } = parseLeaguePath(\n leagueSlugs ?? [],\n rowsBySeason,\n+ seasons,\n user?.id\n )\n console.log(\n 'setting league',\n@@ -149,12 +198,14 @@\n setHighlightedUserId(highlightedUserId)\n }, [isReady, seasonLoaded, leagueSlugs, user?.id])\n \n const MARKER = '★'\n- const seasonStatus = getSeasonStatus(season)\n+ const seasonStatus = seasonInfo?.status\n const countdownEnd = getSeasonCountdownEnd(season)\n- const { end: seasonEnd } = getSeasonDates(season)\n- const randomPeriodEnd = new Date(countdownEnd.getTime() + 24 * 60 * 60 * 1000)\n+ const { approxEnd: seasonEnd } = getSeasonDates(season)\n+ const closingPeriod =\n+ seasonStatus === 'active' && seasonEnd.getTime() < Date.now() + DAY_MS\n+ const randomPeriodEnd = new Date(countdownEnd.getTime() + DAY_MS)\n \n const userRow = seasonRows.find((row) => row.user_id === user?.id)\n const userDivision = userRow?.division\n const userCohort = userRow?.cohort\n@@ -177,11 +228,11 @@\n <Title className=\"!mb-0\">Leagues</Title>\n <Select\n className=\"!border-ink-200 !h-10\"\n value={season}\n- onChange={(e) => onSetSeason(+e.target.value as season)}\n+ onChange={(e) => onSetSeason(+e.target.value)}\n >\n- {SEASONS.map((season) => (\n+ {seasons.map((season) => (\n <option key={season} value={season}>\n Season {season}: {getSeasonMonth(season)}\n </option>\n ))}\n@@ -198,17 +249,19 @@\n </span>{' '}\n for the most profit (realized and unrealized) on trades placed\n this month!\n <span className={'ml-1'}>\n- {seasonStatus === 'closing-period' && (\n+ {closingPeriod && (\n <>\n Ends randomly within <br />\n <ClockIcon className=\"text-ink-1000 inline h-4 w-4\" />{' '}\n {getCountdownStringHoursMinutes(randomPeriodEnd)}\n </>\n )}\n- {seasonStatus === 'ended' && <>Ended {formatTime(seasonEnd)}</>}\n- {seasonStatus === 'current' && (\n+ {seasonStatus === 'complete' && (\n+ <>Ended {formatTime(seasonEnd)}</>\n+ )}\n+ {seasonStatus === 'active' && (\n <>\n Season ends in:{' '}\n <InfoTooltip\n text={\n"
|
|
701
|
-
}
|
|
702
|
-
]
|
|
703
|
-
},
|
|
704
|
-
{
|
|
705
|
-
"id": "downsample-portfolio-history",
|
|
706
|
-
"sha": "0b8bb4847e740a0165ee2f9c660778a886e1edd6",
|
|
707
|
-
"parentSha": "c65e289c933c1b2e2b23a004fe1178c367d3fb52",
|
|
708
|
-
"spec": "Implement downsampling of old portfolio history points and update the API and scheduler accordingly.\n\n1) API: get-user-portfolio-history\n- File: backend/api/src/get-user-portfolio-history.ts\n- Behavior change when period === 'allTime':\n - Use a cutoff equivalent to the start of the last monthly period rather than null. Compute cutoff using getCutoff('monthly') and use that as the start date.\n - Run a combined query to return up to 1000 random points newer than the cutoff and, additionally, all points older than one month.\n - Merge both result sets, convert with convertPortfolioHistory, and return the combined list sorted ascending by timestamp.\n- For non-allTime periods: behavior remains as returning up to 1000 random points newer than the computed cutoff, then sort by timestamp.\n\n2) Scheduled job wiring\n- File: backend/scheduler/src/jobs/index.ts\n- Register a new daily job named 'downsample-portfolio-history' scheduled for 4:50 AM (cron: '0 50 4 * * *') that executes shared/downsample-portfolio-history.downsamplePortfolioHistory.\n- Remove the existing 'update-cash-stats' job registration from the daily jobs list.\n- Ensure imports are present for the new function. Do not change other job behaviors.\n\n3) Downsampling function\n- File: backend/shared/src/downsample-portfolio-history.ts (new)\n- Export an async function downsamplePortfolioHistory() that:\n - Uses createSupabaseDirectClient() and the shared/utils log() function for logging.\n - Selects eligible user IDs to process: users with a non-null lastBetTime, created more than one month ago, and either not present in portfolios_processed or last_processed older than one week. Support a test mode constant that limits to a specific user ID when true.\n - For each user, inside a transaction:\n - Identify user-days older than one month for which user_portfolio_history has more than 4 points (daily_count > 4) using date_trunc('day', ts).\n - If none, skip.\n - For those user-days, delete all corresponding rows from user_portfolio_history, returning the deleted rows.\n - Insert all deleted rows into user_portfolio_history_archive with identical fields preserved.\n - Compute new aggregated points by grouping the deleted points into 6-hour bins within each day (four bins per day starting at 00:00). For each bin, insert one new row into user_portfolio_history with averaged numeric fields (balance, cash_balance, cash_investment_value, investment_value, loan_total, profit, spice_balance, total_cash_deposits, total_deposits) and timestamps corresponding to the start of each 6-hour bin.\n - Upsert into portfolios_processed with user_id and set last_processed = now().\n - Log start and finish and the number of users processed.\n\n4) SQL schema additions\n- Files: \n - backend/supabase/user_portfolio_history_archive.sql (new)\n - Create table user_portfolio_history_archive with columns matching user_portfolio_history (numeric portfolio fields, ts timestamp without time zone, user_id text, id bigint primary key identity). Enable RLS and add a permissive public read policy for select. No triggers.\n - backend/supabase/portfolios_processed.sql (new)\n - Create table portfolios_processed with primary key user_id (text) and last_processed timestamptz default now(). Enable RLS. No policies are required beyond enabling RLS.\n- Ensure these files mirror the style used by other Supabase schema files (autogenerated header comment where used, RLS enable lines).\n\n5) Test harness update\n- File: backend/shared/src/test-backend-function.ts\n- Replace the previously called debug function with an invocation of downsamplePortfolioHistory() so the job can be executed manually in a debug run. Keep other commented-out debug lines intact.\n\nAcceptance criteria\n- Calling the API endpoint get-user-portfolio-history with period='allTime' returns a chronologically sorted series that includes recent points from the last month (limited to 1000 random samples) combined with all older points (which, after the job runs, are at most four per day due to 6-hour downsampling), converted via convertPortfolioHistory.\n- Running the scheduled job daily inserts aggregated 6-hour-binned points for eligible user-days older than one month when there are more than 4 points per day, archives the original points, and updates portfolios_processed.\n- New SQL tables exist and RLS is enabled; user_portfolio_history existing behavior remains unchanged for new inserts by other processes.\n- The 'update-cash-stats' job is no longer scheduled.\n- No TypeScript type errors; queries run under pg-promise using existing helper patterns (map, manyOrNone, multi, none).",
|
|
709
|
-
"prompt": "We need to reduce the size of user portfolio history data and still serve a usable history for users.\n\nAdd a daily background job that, for older data, reduces point density by keeping four averaged points per day (one every six hours) and archives the originals. Expose this in the existing get-user-portfolio-history API so that when requesting the full history, clients get recent points from the last month plus all older points.\n\nAlso introduce minimal schema to support archiving and tracking when a user's portfolio was last processed, and register the new job in the scheduler while removing the obsolete cash stats job.\n\nEnsure the API returns a single, chronologically sorted series and the new job only processes users who need it.",
|
|
710
|
-
"supplementalFiles": [
|
|
711
|
-
"common/src/supabase/portfolio-metrics.ts",
|
|
712
|
-
"common/src/period.ts",
|
|
713
|
-
"backend/scheduler/src/jobs/helpers.ts",
|
|
714
|
-
"backend/supabase/user_portfolio_history.sql",
|
|
715
|
-
"backend/shared/src/update-user-portfolio-histories-core.ts",
|
|
716
|
-
"backend/shared/src/supabase/init.ts",
|
|
717
|
-
"backend/shared/src/utils.ts",
|
|
718
|
-
"backend/scheduler/src/jobs/update-stats.ts"
|
|
719
|
-
],
|
|
720
|
-
"fileDiffs": [
|
|
721
|
-
{
|
|
722
|
-
"path": "backend/api/src/get-user-portfolio-history.ts",
|
|
723
|
-
"status": "modified",
|
|
724
|
-
"diff": "Index: backend/api/src/get-user-portfolio-history.ts\n===================================================================\n--- backend/api/src/get-user-portfolio-history.ts\tc65e289 (parent)\n+++ backend/api/src/get-user-portfolio-history.ts\t0b8bb48 (commit)\n@@ -1,29 +1,40 @@\n import { sortBy } from 'lodash'\n \n-import { type APIHandler } from './helpers/endpoint'\n-import { createSupabaseDirectClient } from 'shared/supabase/init'\n import { getCutoff } from 'common/period'\n import { convertPortfolioHistory } from 'common/supabase/portfolio-metrics'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { type APIHandler } from './helpers/endpoint'\n \n export const getUserPortfolioHistory: APIHandler<\n 'get-user-portfolio-history'\n > = async (props) => {\n const pg = createSupabaseDirectClient()\n const { userId, period } = props\n+ const isAllTime = period === 'allTime'\n+ const cutoff = isAllTime ? getCutoff('monthly') : getCutoff(period)\n \n- const startDate = new Date(getCutoff(period)).toISOString()\n-\n- const data = await pg.map(\n+ const startDate = new Date(cutoff).toISOString()\n+ const allTimeQuery = isAllTime\n+ ? `select *\n+ from user_portfolio_history\n+ where\n+ user_id = $1\n+ and ts < now() - interval '1 month';`\n+ : ''\n+ const data = await pg.multi(\n `select *\n from user_portfolio_history\n where\n user_id = $1\n- and ($2 is null or ts > $2)\n+ and ts > $2\n order by random()\n- limit 1000`,\n- [userId, period === 'allTime' ? null : startDate],\n- convertPortfolioHistory\n+ limit 1000;\n+ ${allTimeQuery}`,\n+ [userId, startDate]\n )\n+ const latestPoints = data[0].map(convertPortfolioHistory)\n+ const laterPoints = isAllTime ? data[1].map(convertPortfolioHistory) : []\n+ const allPoints = [...latestPoints, ...laterPoints]\n \n- return sortBy(data, 'timestamp')\n+ return sortBy(allPoints, 'timestamp')\n }\n"
|
|
725
|
-
},
|
|
726
|
-
{
|
|
727
|
-
"path": "backend/scheduler/src/jobs/index.ts",
|
|
728
|
-
"status": "modified",
|
|
729
|
-
"diff": "Index: backend/scheduler/src/jobs/index.ts\n===================================================================\n--- backend/scheduler/src/jobs/index.ts\tc65e289 (parent)\n+++ backend/scheduler/src/jobs/index.ts\t0b8bb48 (commit)\n@@ -1,44 +1,45 @@\n-import { createJob } from './helpers'\n-import { updateContractMetricsCore } from 'shared/update-contract-metrics-core'\n-import { sendOnboardingNotificationsInternal } from 'shared/onboarding-helpers'\n-import { updateUserMetricPeriods } from 'shared/update-user-metric-periods'\n-import { cleanOldNotifications } from './clean-old-notifications'\n-import { updateCashStatsCore, updateStatsCore } from './update-stats'\n-import { calculateConversionScore } from 'shared/conversion-score'\n-import { autoAwardBounty } from './auto-award-bounty'\n-import { resetPgStats } from './reset-pg-stats'\n import { calculateUserTopicInterests } from 'shared/calculate-user-topic-interests'\n+import { checkPushNotificationReceipts } from 'shared/check-push-receipts'\n+import { calculateConversionScore } from 'shared/conversion-score'\n+import { downsamplePortfolioHistory } from 'shared/downsample-portfolio-history'\n+import { expireLimitOrders } from 'shared/expire-limit-orders'\n+import { calculateGroupImportanceScore } from 'shared/group-importance-score'\n+import { IMPORTANCE_MINUTE_INTERVAL } from 'shared/importance-score'\n+import { sendOnboardingNotificationsInternal } from 'shared/onboarding-helpers'\n+import { sendMarketMovementNotifications } from 'shared/send-market-movement-notifications'\n+import { sendUnseenMarketMovementNotifications } from 'shared/send-unseen-notifications'\n+import { updateContractMetricsCore } from 'shared/update-contract-metrics-core'\n import {\n CREATOR_UPDATE_FREQUENCY,\n updateCreatorMetricsCore,\n } from 'shared/update-creator-metrics-core'\n-import { sendPortfolioUpdateEmailsToAllUsers } from 'shared/weekly-portfolio-emails'\n+import { updateUserMetricPeriods } from 'shared/update-user-metric-periods'\n+import { updateUserPortfolioHistoriesCore } from 'shared/update-user-portfolio-histories-core'\n+import { isProd } from 'shared/utils'\n import { sendWeeklyMarketsEmails } from 'shared/weekly-markets-emails'\n-import { resetWeeklyEmailsFlags } from './reset-weekly-emails-flags'\n-import { calculateGroupImportanceScore } from 'shared/group-importance-score'\n-import { checkPushNotificationReceipts } from 'shared/check-push-receipts'\n-import { sendStreakExpirationNotification } from './streak-expiration-notice'\n-import { expireLimitOrders } from 'shared/expire-limit-orders'\n+import { sendPortfolioUpdateEmailsToAllUsers } from 'shared/weekly-portfolio-emails'\n+import { autoAwardBounty } from './auto-award-bounty'\n+import { autoLeaguesCycle } from './auto-leagues-cycle'\n+import { cleanOldNotifications } from './clean-old-notifications'\n import { denormalizeAnswers } from './denormalize-answers'\n+import { drizzleLiquidity } from './drizzle-liquidity'\n+import { createJob } from './helpers'\n import { incrementStreakForgiveness } from './increment-streak-forgiveness'\n-import { sendMarketCloseEmails } from './send-market-close-emails'\n import { pollPollResolutions } from './poll-poll-resolutions'\n-import { IMPORTANCE_MINUTE_INTERVAL } from 'shared/importance-score'\n-import { scoreContracts } from './score-contracts'\n-import { updateLeagueRanks } from './update-league-ranks'\n-import { updateLeague } from './update-league'\n-import { drizzleLiquidity } from './drizzle-liquidity'\n import { resetBettingStreaksInternal } from './reset-betting-streaks'\n+import { resetPgStats } from './reset-pg-stats'\n import {\n resetDailyQuestStatsInternal,\n resetWeeklyQuestStatsInternal,\n } from './reset-quests-stats'\n-import { updateUserPortfolioHistoriesCore } from 'shared/update-user-portfolio-histories-core'\n-import { isProd } from 'shared/utils'\n-import { sendMarketMovementNotifications } from 'shared/send-market-movement-notifications'\n-import { sendUnseenMarketMovementNotifications } from 'shared/send-unseen-notifications'\n-import { autoLeaguesCycle } from './auto-leagues-cycle'\n+import { resetWeeklyEmailsFlags } from './reset-weekly-emails-flags'\n+import { scoreContracts } from './score-contracts'\n+import { sendMarketCloseEmails } from './send-market-close-emails'\n+import { sendStreakExpirationNotification } from './streak-expiration-notice'\n+import { updateLeague } from './update-league'\n+import { updateLeagueRanks } from './update-league-ranks'\n+import { updateStatsCore } from './update-stats'\n \n export function createJobs() {\n return [\n createJob(\n@@ -155,13 +156,8 @@\n '0 20 4 * * *', // on 4:20am daily\n () => updateStatsCore(7)\n ),\n createJob(\n- 'update-cash-stats',\n- '0 20 4 * * *', // on 4:20am daily\n- () => updateCashStatsCore(7)\n- ),\n- createJob(\n 'onboarding-notification',\n '0 0 11 * * *', // 11 AM daily\n sendOnboardingNotificationsInternal\n ),\n@@ -204,8 +200,13 @@\n 'reset-weekly-quests-stats',\n '0 0 0 * * 1', // every Monday at midnight\n resetWeeklyQuestStatsInternal\n ),\n+ createJob(\n+ 'downsample-portfolio-history',\n+ '0 50 4 * * *', // every day at 4:50am\n+ downsamplePortfolioHistory\n+ ),\n // Monthly jobs:\n createJob(\n 'increment-streak-forgiveness',\n '0 0 3 1 * *', // 3am PST on the 1st day of the month\n"
|
|
730
|
-
},
|
|
731
|
-
{
|
|
732
|
-
"path": "backend/shared/src/downsample-portfolio-history.ts",
|
|
733
|
-
"status": "added",
|
|
734
|
-
"diff": "Index: backend/shared/src/downsample-portfolio-history.ts\n===================================================================\n--- backend/shared/src/downsample-portfolio-history.ts\tc65e289 (parent)\n+++ backend/shared/src/downsample-portfolio-history.ts\t0b8bb48 (commit)\n@@ -0,0 +1,160 @@\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { log } from 'shared/utils'\n+\n+const TESTING = false\n+export async function downsamplePortfolioHistory() {\n+ const db = createSupabaseDirectClient()\n+ log('Starting portfolio history downsampling')\n+ const userIds = TESTING\n+ ? ['rZe5vReIBSYJpUymsFBWW1aUyp13']\n+ : await db.map(\n+ `select distinct id from users\n+ left join portfolios_processed p on p.user_id = users.id\n+ where data->>'lastBetTime' is not null\n+ and created_time < now() - interval '1 month'\n+ and (p.user_id is null or p.last_processed < now() - interval '1 week')`,\n+ [],\n+ (u) => u.id\n+ )\n+ log(`Downsampling portfolio history for ${userIds.length} users `)\n+\n+ for (const userId of userIds) {\n+ await db.tx(async (tx) => {\n+ const daysToProcess = await tx.manyOrNone<{\n+ user_id: string\n+ day: string\n+ }>(\n+ `\n+ select p.user_id, p.day from (\n+ select\n+ user_id,\n+ date_trunc('day', ts) as day,\n+ count(*) as daily_count\n+ from user_portfolio_history\n+ where user_id = $1\n+ and ts < now() - interval '1 month'\n+ group by user_id, date_trunc('day', ts)\n+ ) as p\n+ where p.daily_count > 4\n+ `,\n+ [userId]\n+ )\n+\n+ if (daysToProcess.length === 0) return\n+\n+ log(`Found ${daysToProcess.length} user-days to process for ${userId}.`)\n+\n+ const userIds = daysToProcess.map((d) => d.user_id)\n+ const days = daysToProcess.map((d) => d.day)\n+\n+ await tx.none(\n+ `\n+ with\n+ days_to_process_unnested as (\n+ select *\n+ from unnest($1::text[], $2::timestamp[]) as t(user_id, day)\n+ ),\n+ points_to_process as (\n+ select p.id\n+ from user_portfolio_history p\n+ join days_to_process_unnested d\n+ on p.user_id = d.user_id\n+ and date_trunc('day', p.ts) = d.day\n+ ),\n+ deleted_points as (\n+ delete from user_portfolio_history\n+ where id in (select id from points_to_process)\n+ returning *\n+ ),\n+ archived_points as (\n+ insert into\n+ user_portfolio_history_archive (\n+ user_id,\n+ ts,\n+ balance,\n+ cash_balance,\n+ investment_value,\n+ total_deposits,\n+ loan_total,\n+ profit,\n+ cash_investment_value,\n+ total_cash_deposits,\n+ spice_balance\n+ )\n+ select\n+ user_id,\n+ ts,\n+ balance,\n+ cash_balance,\n+ investment_value,\n+ total_deposits,\n+ loan_total,\n+ profit,\n+ cash_investment_value,\n+ total_cash_deposits,\n+ spice_balance\n+ from deleted_points\n+ ),\n+ new_points as (\n+ select\n+ p.user_id,\n+ (\n+ date_trunc('day', p.ts) +\n+ (floor(extract(hour from p.ts) / 6) * interval '6 hours')\n+ ) as ts,\n+ avg(p.balance) as balance,\n+ avg(p.cash_balance) as cash_balance,\n+ avg(p.cash_investment_value) as cash_investment_value,\n+ avg(p.investment_value) as investment_value,\n+ avg(p.loan_total) as loan_total,\n+ avg(p.profit) as profit,\n+ avg(p.spice_balance) as spice_balance,\n+ avg(p.total_cash_deposits) as total_cash_deposits,\n+ avg(p.total_deposits) as total_deposits\n+ from deleted_points p\n+ group by\n+ p.user_id,\n+ date_trunc('day', p.ts),\n+ floor(extract(hour from p.ts) / 6)\n+ )\n+ insert into\n+ user_portfolio_history (\n+ user_id,\n+ ts,\n+ balance,\n+ cash_balance,\n+ cash_investment_value,\n+ investment_value,\n+ loan_total,\n+ profit,\n+ spice_balance,\n+ total_cash_deposits,\n+ total_deposits\n+ )\n+ select\n+ user_id,\n+ ts,\n+ balance,\n+ cash_balance,\n+ cash_investment_value,\n+ investment_value,\n+ loan_total,\n+ profit,\n+ spice_balance,\n+ total_cash_deposits,\n+ total_deposits\n+ from new_points\n+ `,\n+ [userIds, days]\n+ )\n+ await tx.none(\n+ `\n+ insert into portfolios_processed (user_id)\n+ values ($1) on conflict (user_id) do update set last_processed = now()\n+ `,\n+ [userId]\n+ )\n+ })\n+ }\n+ log('Finished portfolio history downsampling')\n+}\n"
|
|
735
|
-
},
|
|
736
|
-
{
|
|
737
|
-
"path": "backend/shared/src/test-backend-function.ts",
|
|
738
|
-
"status": "modified",
|
|
739
|
-
"diff": "Index: backend/shared/src/test-backend-function.ts\n===================================================================\n--- backend/shared/src/test-backend-function.ts\tc65e289 (parent)\n+++ backend/shared/src/test-backend-function.ts\t0b8bb48 (commit)\n@@ -1,15 +1,9 @@\n /* eslint-disable */\n import { getServiceAccountCredentials, loadSecretsToEnv } from 'common/secrets'\n import { getLocalEnv } from 'shared/init-admin'\n import { createSupabaseDirectClient } from 'shared/supabase/init'\n-import { updateUserMetricPeriods } from './update-user-metric-periods'\n-import { updateCreatorMetricsCore } from 'shared/update-creator-metrics-core'\n-import { calculateImportanceScore } from 'shared/importance-score'\n-import { backfillUserTopicInterests } from 'shared/backfill-user-topic-interests'\n-import { updateUserPortfolioHistoriesCore } from './update-user-portfolio-histories-core'\n-import { updateContractMetricsCore } from './update-contract-metrics-core'\n-import { updateUserMetricsWithBets } from 'shared/update-user-metrics-with-bets'\n+import { downsamplePortfolioHistory } from './downsample-portfolio-history'\n \n // Ian's file for debugging\n export async function testBackendFunction() {\n const credentials = getServiceAccountCredentials(getLocalEnv())\n@@ -18,9 +12,10 @@\n const pg = createSupabaseDirectClient()\n // await backfillUserTopicInterests(pg)\n // await calculateImportanceScore(db, pg)\n // await updateContractMetricsCore()\n- await updateUserMetricPeriods(['xoo782zW9geixafwEaT7B9Ku3Bj1'])\n+ await downsamplePortfolioHistory()\n+ // await updateUserMetricPeriods(['xoo782zW9geixafwEaT7B9Ku3Bj1'])\n // await updateUserMetricsWithBets()\n // await updateUserPortfolioHistoriesCore(['AJwLWoo3xue32XIiAVrL5SyR1WB2'])\n // await updateCreatorMetricsCore()\n } catch (e) {\n"
|
|
740
|
-
},
|
|
741
|
-
{
|
|
742
|
-
"path": "backend/supabase/portfolios_processed.sql",
|
|
743
|
-
"status": "added",
|
|
744
|
-
"diff": "Index: backend/supabase/portfolios_processed.sql\n===================================================================\n--- backend/supabase/portfolios_processed.sql\tc65e289 (parent)\n+++ backend/supabase/portfolios_processed.sql\t0b8bb48 (commit)\n@@ -0,0 +1,8 @@\n+create table if not exists\n+ portfolios_processed (\n+ user_id text not null primary key,\n+ last_processed timestamp with time zone default now() not null\n+ );\n+\n+-- Row Level Security\n+alter table portfolios_processed enable row level security;\n"
|
|
745
|
-
},
|
|
746
|
-
{
|
|
747
|
-
"path": "backend/supabase/user_portfolio_history_archive.sql",
|
|
748
|
-
"status": "added",
|
|
749
|
-
"diff": "Index: backend/supabase/user_portfolio_history_archive.sql\n===================================================================\n--- backend/supabase/user_portfolio_history_archive.sql\tc65e289 (parent)\n+++ backend/supabase/user_portfolio_history_archive.sql\t0b8bb48 (commit)\n@@ -0,0 +1,26 @@\n+-- This file is autogenerated from regen-schema.ts\n+create table if not exists\n+ user_portfolio_history_archive (\n+ balance numeric,\n+ cash_balance numeric default 0 not null,\n+ cash_investment_value numeric default 0 not null,\n+ id bigint primary key generated always as identity not null,\n+ investment_value numeric,\n+ loan_total numeric,\n+ profit numeric,\n+ spice_balance numeric default 0 not null,\n+ total_cash_deposits numeric default 0 not null,\n+ total_deposits numeric,\n+ ts timestamp without time zone,\n+ user_id text not null\n+ );\n+\n+-- Row Level Security\n+alter table user_portfolio_history_archive enable row level security;\n+\n+-- Policies\n+drop policy if exists \"public read\" on user_portfolio_history_archive;\n+\n+create policy \"public read\" on user_portfolio_history_archive for\n+select\n+ using (true);\n"
|
|
750
|
-
}
|
|
751
|
-
]
|
|
752
|
-
},
|
|
753
|
-
{
|
|
754
|
-
"id": "fix-multi-bet",
|
|
755
|
-
"sha": "1f350619c811ff8d251a5f281147d88d23b5a212",
|
|
756
|
-
"parentSha": "3ff3df4028ee087feea971f8d1720992bd966eed",
|
|
757
|
-
"spec": "Implement correct handling for multi-buys across sums-to-one multiple-choice markets and propagate the new execution semantics throughout the backend, common logic, and docs.\n\nScope and required changes:\n\n1) Add isMultiBet execution mode to place-bet flow\n- File: backend/api/src/place-bet.ts\n - Change executeNewBetResult to accept a new boolean parameter isMultiBet (final argument).\n - Replace existing number-contract checks with isMultiBet so that:\n - Unique bettor bonus logic only applies on firstBetInMultiBet when isMultiBet is true; otherwise behaves as current single-bet flow.\n - For isMultiBet and not firstBetInMultiBet, fetch updated contract metrics for the user (getContractMetrics) after bet insertion to reflect multi-leg progress before building response.\n - Broadcast user updates and contract/answer updates from executeNewBetResult when isMultiBet is true (previously done in on-create-bet for other contracts). Maintain existing behavior for non-multi.\n - Ensure ordering of existing parameters remains unchanged and add the new parameter at the end: (silent?: boolean, isMultiBet?: boolean).\n\n2) Update all executeNewBetResult call sites\n- File: backend/api/src/place-multi-bet.ts\n - In the per-leg loop, pass: firstBetInMultiBet = i === 0, silent = false, isMultiBet = true.\n- File: backend/api/src/multi-sell.ts\n - In the per-leg loop, pass: firstBetInMultiBet = false, silent = false, isMultiBet = true.\n- File: backend/api/src/sell-shares.ts (or equivalent single-sell execution path)\n - Update any call to executeNewBetResult to pass isMultiBet = false (and keep existing flags consistent). Verify all other callsites compile with the new signature.\n\n3) Correct multi-bet arbitrage computation to avoid order/balance reuse and over-spend\n- File: common/src/calculate-cpmm-arbitrage.ts\n - Introduce working copies of unfilledBetsByAnswer and balanceByUserId that are mutated between legs to ensure subsequent legs cannot reuse previously consumed maker order capacity or maker balances.\n - Add a helper applyMakersToWorkingState(makers, ordersToCancel, workingUnfilledBetsByAnswer, workingBalanceByUserId) to:\n - Deduct maker amounts from maker balances used in fills.\n - Update per-order filled amount/shares in workingUnfilledBetsByAnswer.\n - Remove orders that are canceled.\n - Annotate PreliminaryBetResults with an iteration index (0 for the first leg consuming user funds; >0 for arbitrage legs paid by extraMana) so downstream aggregation can mark later taker fills as free.\n - Update getBetResultsAndUpdatedAnswers to mutate and return updated workingUnfilledBetsByAnswer and workingBalanceByUserId after both YES and NO legs; callers carry these across iterations in calculateCpmmMultiArbitrageYesBets.\n - Update calculateCpmmMultiArbitrageYesBets to:\n - Maintain and pass working maps across each iteration; set iteration on each leg’s results.\n - Ensure total consumer spend (sum of taker.amount across all YES legs in the first iteration only) does not exceed the provided initialBetAmount.\n - Update combineBetsOnSameAnswers to, when fillsFollowingFirstAreFree is true, set amount=0 and fees=noFees for taker fills originating from iterations > 0, while still summing shares so probabilities update correctly.\n - Update buyNoSharesUntilAnswersSumToOne so the binary search path does not mutate the working orders/balances (updateOrders=false), and only the final execution path mutates them (updateOrders=true).\n - Update buyNoSharesInAnswers to process answers sequentially, applying makers to the working state between answers when updateOrders is true.\n\n4) Extend unit tests for arbitrage correctness\n- File: common/src/calculate-cpmm-arbitrage.test.ts\n - Add a test asserting that total user taker amount across multi-buy answers does not exceed the provided bet amount.\n - Add a test ensuring that maker limit orders are not overfilled during multi-buy across multiple answers (filled <= orderAmount per bet).\n\n5) Document public API for multi-buys\n- File: docs/docs/api.md\n - Add a new section documenting POST /v0/multi-bet for sums-to-one multiple choice markets, including parameters (contractId, answerIds, amount, limitProb, expiresAt), behavior (YES only; equal shares target; per-leg limit price; internal arbitrage not returned), and an example request/response.\n - Clarify that the response returns one Bet-like object per selected answer, with betId and betGroupId fields included.\n\nAcceptance criteria:\n- Type-checks pass; all executeNewBetResult callsites compile with the new parameter.\n- New tests in calculate-cpmm-arbitrage.test.ts pass and existing tests remain green.\n- In multi-bet execution, user-charged amount across YES legs never exceeds the provided amount; subsequent arbitrage legs are free (amount=0, no fees) but still move probability via shares.\n- Maker limit orders are not overfilled across legs; balances and order capacity are updated as legs execute.\n- Unique bettor bonus and metrics/broadcasting behavior are correct for multi-bet (bonus only on first leg; broadcasting handled within executeNewBetResult for multi-bet).\n- API docs include the new /v0/multi-bet endpoint with clear parameters and example.",
|
|
758
|
-
"prompt": "Implement correct multi-buy behavior for sums-to-one multiple-choice CPMM markets and expose it via a new multi-bet API endpoint. Ensure that placing a multi-bet splits across selected answers to target equal shares, spends no more than the provided amount, and prevents overfilling maker limit orders across legs. Only charge taker fees on the first leg; subsequent arbitrage legs should be free for the taker but still move probabilities. Update the bet execution path to support a multi-bet mode that adjusts bonus, metrics, and broadcasting behavior across legs. Update all internal call sites to the execution function to pass the new multi-bet flag, and add API documentation and tests verifying spend caps and order overfill prevention.",
|
|
759
|
-
"supplementalFiles": [
|
|
760
|
-
"common/src/calculate-cpmm.ts",
|
|
761
|
-
"common/src/fees.ts",
|
|
762
|
-
"common/src/bet.ts",
|
|
763
|
-
"common/src/contract.ts",
|
|
764
|
-
"common/src/new-bet.ts",
|
|
765
|
-
"backend/api/src/helpers/bets.ts"
|
|
766
|
-
],
|
|
767
|
-
"fileDiffs": [
|
|
768
|
-
{
|
|
769
|
-
"path": "backend/api/src/multi-sell.ts",
|
|
770
|
-
"status": "modified",
|
|
771
|
-
"diff": "Index: backend/api/src/multi-sell.ts\n===================================================================\n--- backend/api/src/multi-sell.ts\t3ff3df4 (parent)\n+++ backend/api/src/multi-sell.ts\t1f35061 (commit)\n@@ -1,17 +1,17 @@\n-import * as crypto from 'crypto'\n-import { APIError, type APIHandler } from './helpers/endpoint'\n+import { getUnfilledBetsAndUserBalances } from 'api/helpers/bets'\n import { onCreateBets } from 'api/on-create-bet'\n import { executeNewBetResult } from 'api/place-bet'\n-import { getContract, getUser, log } from 'shared/utils'\n-import { groupBy, keyBy, mapValues, sumBy } from 'lodash'\n+import { isSummary } from 'common/contract-metric'\n import { getCpmmMultiSellSharesInfo } from 'common/sell-bet'\n-import { runTransactionWithRetries } from 'shared/transact-with-retries'\n import { convertBet } from 'common/supabase/bets'\n+import * as crypto from 'crypto'\n+import { groupBy, keyBy, mapValues, sumBy } from 'lodash'\n import { betsQueue } from 'shared/helpers/fn-queue'\n import { getContractMetrics } from 'shared/helpers/user-contract-metrics'\n-import { getUnfilledBetsAndUserBalances } from 'api/helpers/bets'\n-import { isSummary } from 'common/contract-metric'\n+import { runTransactionWithRetries } from 'shared/transact-with-retries'\n+import { getContract, getUser, log } from 'shared/utils'\n+import { APIError, type APIHandler } from './helpers/endpoint'\n \n export const multiSell: APIHandler<'multi-sell'> = async (props, auth, req) => {\n return await betsQueue.enqueueFn(\n () => multiSellMain(props, auth, req),\n@@ -115,9 +115,11 @@\n balancesByUserId,\n undefined,\n betGroupId,\n deterministic,\n- false\n+ false,\n+ false,\n+ true\n )\n results.push(result)\n }\n return results\n"
|
|
772
|
-
},
|
|
773
|
-
{
|
|
774
|
-
"path": "backend/api/src/place-bet.ts",
|
|
775
|
-
"status": "modified",
|
|
776
|
-
"diff": "Index: backend/api/src/place-bet.ts\n===================================================================\n--- backend/api/src/place-bet.ts\t3ff3df4 (parent)\n+++ backend/api/src/place-bet.ts\t1f35061 (commit)\n@@ -1,23 +1,48 @@\n-import { first, isEqual, maxBy, sumBy } from 'lodash'\n-import { APIError, AuthedUser, type APIHandler } from './helpers/endpoint'\n+import {\n+ fetchContractBetDataAndValidate,\n+ getMakerIdsFromBetResult,\n+ getRoundedLimitProb,\n+ getUniqueBettorBonusQuery,\n+ getUserBalancesAndMetrics,\n+ updateMakers,\n+} from 'api/helpers/bets'\n+import { onCreateBets } from 'api/on-create-bet'\n+import { Answer } from 'common/answer'\n+import { ValidatedAPIParams } from 'common/api/schema'\n+import { Bet, getNewBetId, LimitBet, maker } from 'common/bet'\n+import { CpmmState, getCpmmProbability } from 'common/calculate-cpmm'\n import { CPMM_MIN_POOL_QTY, MarketContract } from 'common/contract'\n-import { User } from 'common/user'\n+import { ContractMetric } from 'common/contract-metric'\n+import { FLAT_TRADE_FEE } from 'common/fees'\n import {\n BetInfo,\n CandidateBet,\n getBinaryCpmmBetInfo,\n getNewMultiCpmmBetInfo,\n } from 'common/new-bet'\n-import { removeUndefinedProps } from 'common/util/object'\n-import { Bet, getNewBetId, LimitBet, maker } from 'common/bet'\n+import { convertBet } from 'common/supabase/bets'\n+import { convertContract } from 'common/supabase/contracts'\n+import { convertTxn } from 'common/supabase/txns'\n+import { UniqueBettorBonusTxn } from 'common/txn'\n+import { User } from 'common/user'\n+import { filterDefined } from 'common/util/array'\n import { EPSILON, floatingEqual } from 'common/util/math'\n-import { log } from 'shared/utils'\n-import { Answer } from 'common/answer'\n-import { CpmmState, getCpmmProbability } from 'common/calculate-cpmm'\n-import { ValidatedAPIParams } from 'common/api/schema'\n-import { onCreateBets } from 'api/on-create-bet'\n+import { removeUndefinedProps } from 'common/util/object'\n+import { first, isEqual, maxBy, sumBy } from 'lodash'\n+import { betsQueue, ordersQueue } from 'shared/helpers/fn-queue'\n import {\n+ bulkUpdateContractMetricsQuery,\n+ bulkUpdateUserMetricsWithNewBetsOnly,\n+ getContractMetrics,\n+} from 'shared/helpers/user-contract-metrics'\n+import { partialAnswerToRow } from 'shared/supabase/answers'\n+import {\n+ bulkInsertBetsQuery,\n+ cancelLimitOrdersQuery,\n+ insertZeroAmountLimitBet,\n+} from 'shared/supabase/bets'\n+import {\n createSupabaseDirectClient,\n SupabaseTransaction,\n } from 'shared/supabase/init'\n import {\n@@ -25,42 +50,17 @@\n bulkIncrementBalancesQuery,\n incrementStreakQuery,\n UserUpdate,\n } from 'shared/supabase/users'\n-import { convertBet } from 'common/supabase/bets'\n+import { bulkUpdateQuery, updateDataQuery } from 'shared/supabase/utils'\n+import { runTransactionWithRetries } from 'shared/transact-with-retries'\n+import { log } from 'shared/utils'\n import {\n- bulkInsertBetsQuery,\n- cancelLimitOrdersQuery,\n- insertZeroAmountLimitBet,\n-} from 'shared/supabase/bets'\n-import { betsQueue, ordersQueue } from 'shared/helpers/fn-queue'\n-import { FLAT_TRADE_FEE } from 'common/fees'\n-import { redeemShares } from './redeem-shares'\n-import { partialAnswerToRow } from 'shared/supabase/answers'\n-import { filterDefined } from 'common/util/array'\n-import { convertContract } from 'common/supabase/contracts'\n-import { UniqueBettorBonusTxn } from 'common/txn'\n-import {\n- bulkUpdateContractMetricsQuery,\n- bulkUpdateUserMetricsWithNewBetsOnly,\n- getContractMetrics,\n-} from 'shared/helpers/user-contract-metrics'\n-import { ContractMetric } from 'common/contract-metric'\n-import {\n broadcastUpdatedAnswers,\n broadcastUpdatedContract,\n } from 'shared/websockets/helpers'\n-import { bulkUpdateQuery, updateDataQuery } from 'shared/supabase/utils'\n-import { convertTxn } from 'common/supabase/txns'\n-import {\n- fetchContractBetDataAndValidate,\n- getMakerIdsFromBetResult,\n- getRoundedLimitProb,\n- getUniqueBettorBonusQuery,\n- getUserBalancesAndMetrics,\n- updateMakers,\n-} from 'api/helpers/bets'\n-import { runTransactionWithRetries } from 'shared/transact-with-retries'\n+import { APIError, AuthedUser, type APIHandler } from './helpers/endpoint'\n+import { redeemShares } from './redeem-shares'\n \n export const placeBet: APIHandler<'bet'> = async (props, auth) => {\n const isApi = auth.creds.kind === 'key'\n const { deps, contractId, dryRun } = props\n@@ -332,9 +332,10 @@\n replyToCommentId?: string,\n betGroupId?: string,\n deterministic?: boolean,\n firstBetInMultiBet?: boolean,\n- silent?: boolean\n+ silent?: boolean,\n+ isMultiBet?: boolean\n ) => {\n const {\n newBet,\n otherBetResults,\n@@ -396,9 +397,8 @@\n answerUpdates: undefined,\n updatedMakers: [],\n }\n }\n- const isNumberContract = contract.outcomeType === 'NUMBER'\n const apiFee = isApi ? FLAT_TRADE_FEE : 0\n const betsToInsert: Bet[] = [candidateBet]\n const allOrdersToCancel: LimitBet[] = filterDefined(ordersToCancel ?? [])\n const userBalanceUpdates = [\n@@ -420,9 +420,9 @@\n const sumsToOne =\n contract.mechanism === 'cpmm-multi-1' && contract.shouldAnswersSumToOne\n let bonusTxnQuery = 'select 1 where false'\n if (\n- (!isNumberContract || firstBetInMultiBet) &&\n+ (!isMultiBet || firstBetInMultiBet) &&\n !contractMetrics.find(\n (m) =>\n m.userId === user.id &&\n (sumsToOne ? true : m.answerId == candidateBet.answerId)\n@@ -477,9 +477,9 @@\n )\n betsToInsert.push(...otherBetsToInsert)\n }\n const isUniqueBettor =\n- (!isNumberContract || firstBetInMultiBet) &&\n+ (!isMultiBet || firstBetInMultiBet) &&\n !contractMetrics.find((m) => m.userId === user.id)\n const lastBetTime =\n maxBy(betsToInsert, (b) => b.createdTime)?.createdTime ?? Date.now()\n const contractUpdate: Partial<MarketContract> & { id: string } =\n@@ -511,9 +511,9 @@\n })\n }\n \n const metrics =\n- isNumberContract && !firstBetInMultiBet\n+ isMultiBet && !firstBetInMultiBet\n ? await getContractMetrics(\n pgTrans,\n [user.id],\n contract.id,\n@@ -611,9 +611,9 @@\n | UniqueBettorBonusTxn\n | undefined\n \n // On normal contracts, we do this in on-create-bet\n- if (isNumberContract) {\n+ if (isMultiBet) {\n broadcastUserUpdates(userUpdates)\n broadcastUpdatedContract(newContract.visibility, contractUpdate)\n broadcastUpdatedAnswers(newContract.id, answerUpdates)\n }\n"
|
|
777
|
-
},
|
|
778
|
-
{
|
|
779
|
-
"path": "backend/api/src/place-multi-bet.ts",
|
|
780
|
-
"status": "modified",
|
|
781
|
-
"diff": "Index: backend/api/src/place-multi-bet.ts\n===================================================================\n--- backend/api/src/place-multi-bet.ts\t3ff3df4 (parent)\n+++ backend/api/src/place-multi-bet.ts\t1f35061 (commit)\n@@ -1,17 +1,17 @@\n-import * as crypto from 'crypto'\n-import { APIError, type APIHandler } from './helpers/endpoint'\n-import { getNewMultiCpmmBetsInfo } from 'common/new-bet'\n-import { ValidatedAPIParams } from 'common/api/schema'\n-import { onCreateBets } from 'api/on-create-bet'\n-import { executeNewBetResult } from 'api/place-bet'\n-import { log } from 'shared/utils'\n-import { runTransactionWithRetries } from 'shared/transact-with-retries'\n-import { betsQueue } from 'shared/helpers/fn-queue'\n import {\n fetchContractBetDataAndValidate,\n getRoundedLimitProb,\n } from 'api/helpers/bets'\n+import { onCreateBets } from 'api/on-create-bet'\n+import { executeNewBetResult } from 'api/place-bet'\n+import { ValidatedAPIParams } from 'common/api/schema'\n+import { getNewMultiCpmmBetsInfo } from 'common/new-bet'\n+import * as crypto from 'crypto'\n+import { betsQueue } from 'shared/helpers/fn-queue'\n+import { runTransactionWithRetries } from 'shared/transact-with-retries'\n+import { log } from 'shared/utils'\n+import { APIError, type APIHandler } from './helpers/endpoint'\n \n export const placeMultiBet: APIHandler<'multi-bet'> = async (props, auth) => {\n const isApi = auth.creds.kind === 'key'\n \n@@ -96,9 +96,11 @@\n balanceByUserId,\n undefined,\n betGroupId,\n deterministic,\n- i === 0\n+ i === 0,\n+ false,\n+ true\n )\n results.push(result)\n }\n return results\n"
|
|
782
|
-
},
|
|
783
|
-
{
|
|
784
|
-
"path": "common/src/calculate-cpmm-arbitrage.test.ts",
|
|
785
|
-
"status": "modified",
|
|
786
|
-
"diff": "Index: common/src/calculate-cpmm-arbitrage.test.ts\n===================================================================\n--- common/src/calculate-cpmm-arbitrage.test.ts\t3ff3df4 (parent)\n+++ common/src/calculate-cpmm-arbitrage.test.ts\t1f35061 (commit)\n@@ -1,19 +1,19 @@\n import { groupBy, sumBy } from 'lodash'\n import { Answer } from './answer'\n+import { Bet } from './bet'\n import {\n+ calculateCpmmMultiSumsToOneSale,\n+ getCpmmProbability,\n+} from './calculate-cpmm'\n+import {\n calculateCpmmMultiArbitrageBet,\n calculateCpmmMultiArbitrageSellYesEqually,\n calculateCpmmMultiArbitrageYesBets,\n } from './calculate-cpmm-arbitrage'\n-import {\n- calculateCpmmMultiSumsToOneSale,\n- getCpmmProbability,\n-} from './calculate-cpmm'\n import { getFeeTotal, getTakerFee, noFees } from './fees'\n import { getMultiNumericAnswerBucketRanges } from './number'\n import { getNewSellBetInfo } from './sell-bet'\n-import { Bet } from './bet'\n \n describe('calculateCpmmMultiArbitrageBet', () => {\n it('should sum to 1 after bet', async () => {\n const answers: Answer[] = [\n@@ -321,8 +321,89 @@\n Math.abs(afterSaleAnswerYesShares[i] - preBuyYesShares[i]) < 0.01\n ).toBeTruthy()\n }\n })\n+\n+ it('does not spend more than passed amount across multi-buy answers', async () => {\n+ const answers: Answer[] = getNumericAnswers(0, 100, 10)\n+ const betAmount = 100\n+ const { newBetResults } = calculateCpmmMultiArbitrageYesBets(\n+ answers,\n+ answers.slice(0, 3),\n+ betAmount,\n+ undefined,\n+ [],\n+ {},\n+ noFees\n+ )\n+ const totalUserAmount = sumBy(\n+ newBetResults.flatMap((r) => r.takers),\n+ (t) => t.amount\n+ )\n+ expect(totalUserAmount).toBeLessThanOrEqual(betAmount + 1e-9)\n+ })\n+\n+ it('does not overfill maker limit orders in multi-buy', async () => {\n+ const answers: Answer[] = getNumericAnswers(0, 100, 10)\n+ const [a0, a1] = answers\n+ // Create two small NO limit orders on two answers at low limitProb so they match YES takers\n+ const now = Date.now()\n+ const makeLimit = (\n+ id: string,\n+ answer: Answer,\n+ userId: string,\n+ orderAmount: number\n+ ) => ({\n+ id,\n+ userId,\n+ contractId: 'c1',\n+ answerId: answer.id,\n+ createdTime: now,\n+ amount: 0,\n+ loanAmount: 0,\n+ outcome: 'NO',\n+ shares: 0,\n+ probBefore: answer.prob,\n+ probAfter: answer.prob,\n+ fees: noFees,\n+ isRedemption: false,\n+ visibility: 'public',\n+ orderAmount,\n+ limitProb: 0.05,\n+ isFilled: false,\n+ isCancelled: false,\n+ fills: [],\n+ })\n+ const makerA = makeLimit('makerA1', a0, 'makerA', 5)\n+ const makerB = makeLimit('makerB1', a1, 'makerB', 7)\n+ const unfilledBets = [makerA, makerB]\n+ const balanceByUserId = { makerA: 1e6, makerB: 1e6 }\n+\n+ const { newBetResults, otherBetResults } =\n+ calculateCpmmMultiArbitrageYesBets(\n+ answers,\n+ answers.slice(0, 3),\n+ 100,\n+ undefined,\n+ unfilledBets,\n+ balanceByUserId,\n+ noFees\n+ )\n+\n+ const allMakers = [\n+ ...newBetResults.flatMap((r) => r.makers),\n+ ...otherBetResults.flatMap((r) => r.makers),\n+ ]\n+ const byBetId = groupBy(allMakers, (m) => m.bet.id)\n+ const orderAmountById: Record<string, number> = {\n+ [makerA.id]: makerA.orderAmount,\n+ [makerB.id]: makerB.orderAmount,\n+ }\n+ for (const [betId, makers] of Object.entries(byBetId)) {\n+ const filled = sumBy(makers, (m) => m.amount)\n+ expect(filled).toBeLessThanOrEqual(orderAmountById[betId] + 1e-9)\n+ }\n+ })\n })\n \n // Convert all NO shares into YES shares.\n // E.g. NO shares in answer A will be converted to YES shares in every other answer.\n"
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
"path": "common/src/calculate-cpmm-arbitrage.ts",
|
|
790
|
-
"status": "modified",
|
|
791
|
-
"diff": "Index: common/src/calculate-cpmm-arbitrage.ts\n===================================================================\n--- common/src/calculate-cpmm-arbitrage.ts\t3ff3df4 (parent)\n+++ common/src/calculate-cpmm-arbitrage.ts\t1f35061 (commit)\n@@ -1,17 +1,17 @@\n+import { MAX_CPMM_PROB, MIN_CPMM_PROB } from 'common/contract'\n import { Dictionary, first, groupBy, mapValues, sum, sumBy } from 'lodash'\n import { Answer } from './answer'\n import { Bet, LimitBet, maker } from './bet'\n import {\n calculateAmountToBuySharesFixedP,\n computeFills,\n getCpmmProbability,\n } from './calculate-cpmm'\n+import { Fees, getFeesSplit, getTakerFee, noFees, sumAllFees } from './fees'\n import { binarySearch } from './util/algos'\n import { floatingEqual } from './util/math'\n-import { Fees, getFeesSplit, getTakerFee, noFees, sumAllFees } from './fees'\n import { addObjects } from './util/object'\n-import { MAX_CPMM_PROB, MIN_CPMM_PROB } from 'common/contract'\n \n const DEBUG = false\n export type ArbitrageBetArray = ReturnType<typeof combineBetsOnSameAnswers>\n const noFillsReturn = (\n@@ -138,9 +138,51 @@\n }\n \n export type PreliminaryBetResults = ReturnType<typeof computeFills> & {\n answer: Answer\n+ // iteration index within a multi-buy cycle; 0 is paid with user's amount,\n+ // >0 iterations are funded by arbitrage extraMana and should be free for the taker.\n+ iteration?: number\n }\n+\n+// Mutate working state to reflect maker fills and cancellations, so subsequent legs\n+// cannot reuse the same maker order capacity or balances.\n+const applyMakersToWorkingState = (\n+ makers: maker[],\n+ ordersToCancel: LimitBet[],\n+ workingUnfilledBetsByAnswer: Dictionary<LimitBet[]>,\n+ workingBalanceByUserId: { [userId: string]: number }\n+) => {\n+ for (const maker of makers) {\n+ const { bet, amount, shares } = maker\n+ if (!bet.answerId) {\n+ throw new Error('Multi-bet has no answerId')\n+ }\n+ if (amount > 0) {\n+ const prev = workingBalanceByUserId[bet.userId]\n+ if (prev !== undefined) workingBalanceByUserId[bet.userId] = prev - amount\n+ }\n+ // Update the bet's filled amount in-place inside our working unfilled bets map\n+ const arr = workingUnfilledBetsByAnswer[bet.answerId] ?? []\n+ const idx = arr.findIndex((b) => b.id === bet.id)\n+ if (idx >= 0) {\n+ const updated = { ...arr[idx] }\n+ updated.amount = (updated.amount ?? 0) + amount\n+ updated.shares = (updated.shares ?? 0) + shares\n+ arr[idx] = updated\n+ workingUnfilledBetsByAnswer[bet.answerId] = arr\n+ }\n+ }\n+ if (ordersToCancel.length) {\n+ const cancelIds = new Set(ordersToCancel.map((b) => b.id))\n+ for (const [answerId, arr] of Object.entries(workingUnfilledBetsByAnswer)) {\n+ workingUnfilledBetsByAnswer[answerId] = arr.filter(\n+ (b) => !cancelIds.has(b.id)\n+ )\n+ }\n+ }\n+}\n+\n function calculateCpmmMultiArbitrageBetsYes(\n initialAnswers: Answer[],\n initialAnswersToBuy: Answer[],\n initialBetAmount: number,\n@@ -148,14 +190,17 @@\n unfilledBets: LimitBet[],\n balanceByUserId: { [userId: string]: number },\n collectedFees: Fees\n ) {\n- const unfilledBetsByAnswer = groupBy(unfilledBets, (bet) => bet.answerId)\n+ // Maintain mutable snapshots of unfilled orders and maker balances across the whole multi-buy\n+ let workingUnfilledBetsByAnswer = groupBy(unfilledBets, (bet) => bet.answerId)\n+ let workingBalanceByUserId = { ...balanceByUserId }\n const noBetResults: PreliminaryBetResults[] = []\n const yesBetResults: PreliminaryBetResults[] = []\n \n let updatedAnswers = initialAnswers\n let amountToBet = initialBetAmount\n+ let iteration = 0\n while (amountToBet > 0.01) {\n const answersToBuy = updatedAnswers.filter((a) =>\n initialAnswersToBuy.map((an) => an.id).includes(a.id)\n )\n@@ -168,32 +213,44 @@\n calculateAmountToBuySharesFixedP(\n { pool: { YES: poolYes, NO: poolNo }, p: 0.5, collectedFees },\n yesShares,\n 'YES',\n- unfilledBetsByAnswer[id] ?? [],\n- balanceByUserId\n+ workingUnfilledBetsByAnswer[id] ?? [],\n+ workingBalanceByUserId\n )\n )\n \n const totalYesAmount = sum(yesAmounts)\n return totalYesAmount - amountToBet\n })\n \n- const { noBuyResults, yesBets, newUpdatedAnswers } =\n- getBetResultsAndUpdatedAnswers(\n- answersToBuy,\n- yesAmounts,\n- updatedAnswers,\n- limitProb,\n- unfilledBets,\n- balanceByUserId,\n- collectedFees\n- )\n+ const {\n+ noBuyResults,\n+ yesBets,\n+ newUpdatedAnswers,\n+ updatedUnfilledBetsByAnswer,\n+ updatedBalanceByUserId,\n+ } = getBetResultsAndUpdatedAnswers(\n+ answersToBuy,\n+ yesAmounts,\n+ updatedAnswers,\n+ limitProb,\n+ // Flatten working unfilled bets for API; it will reconstruct its own map\n+ Object.values(workingUnfilledBetsByAnswer).flat(),\n+ workingBalanceByUserId,\n+ collectedFees\n+ )\n+ // Annotate iteration index so we can mark taker fills as free beyond the first.\n+ yesBets.forEach((r) => ((r as PreliminaryBetResults).iteration = iteration))\n+ noBuyResults.noBetResults.forEach((r) => (r.iteration = iteration))\n+ workingUnfilledBetsByAnswer = updatedUnfilledBetsByAnswer\n+ workingBalanceByUserId = updatedBalanceByUserId\n updatedAnswers = newUpdatedAnswers\n \n amountToBet = noBuyResults.extraMana\n noBetResults.push(...noBuyResults.noBetResults)\n yesBetResults.push(...yesBets)\n+ iteration++\n }\n \n const noBetResultsOnBoughtAnswer = combineBetsOnSameAnswers(\n noBetResults,\n@@ -239,25 +296,39 @@\n balanceByUserId: { [userId: string]: number },\n collectedFees: Fees,\n answerIdsWithFees?: string[]\n ) => {\n- const unfilledBetsByAnswer = groupBy(unfilledBets, (bet) => bet.answerId)\n+ // Working maps that will be updated as we simulate each leg to avoid reusing capacity.\n+ const workingUnfilledBetsByAnswer = groupBy(\n+ unfilledBets,\n+ (bet) => bet.answerId\n+ )\n+ const workingBalanceByUserId = { ...balanceByUserId }\n+\n const yesBetResultsAndUpdatedAnswers = answersToBuy.map((answerToBuy, i) => {\n const pool = { YES: answerToBuy.poolYes, NO: answerToBuy.poolNo }\n const yesBetResult = {\n ...computeFills(\n { pool, p: 0.5, collectedFees },\n 'YES',\n yesAmounts[i],\n limitProb,\n- unfilledBetsByAnswer[answerToBuy.id] ?? [],\n- balanceByUserId,\n+ workingUnfilledBetsByAnswer[answerToBuy.id] ?? [],\n+ workingBalanceByUserId,\n undefined,\n answerIdsWithFees && !answerIdsWithFees?.includes(answerToBuy.id)\n ),\n answer: answerToBuy,\n }\n \n+ // Apply the fills to mutate working state for subsequent legs\n+ applyMakersToWorkingState(\n+ yesBetResult.makers,\n+ yesBetResult.ordersToCancel,\n+ workingUnfilledBetsByAnswer,\n+ workingBalanceByUserId\n+ )\n+\n const { cpmmState } = yesBetResult\n const { pool: newPool, p } = cpmmState\n const { YES: poolYes, NO: poolNo } = newPool\n const prob = getCpmmProbability(newPool, p)\n@@ -279,13 +350,23 @@\n newAnswerStates.find(\n (newAnswerState) => newAnswerState.id === answer.id\n ) ?? answer\n ),\n- unfilledBets,\n- balanceByUserId,\n+ // Flatten current working snapshot so NO legs see already-used capacity\n+ Object.values(workingUnfilledBetsByAnswer).flat(),\n+ workingBalanceByUserId,\n collectedFees,\n answerIdsWithFees\n )\n+ // Apply NO leg maker fills to working state as well\n+ for (const noBet of noBuyResults.noBetResults) {\n+ applyMakersToWorkingState(\n+ noBet.makers,\n+ noBet.ordersToCancel,\n+ workingUnfilledBetsByAnswer,\n+ workingBalanceByUserId\n+ )\n+ }\n // Update new answer states from bets placed on all answers\n const newUpdatedAnswers = noBuyResults.noBetResults.map((noBetResult) => {\n const { cpmmState } = noBetResult\n const { pool: newPool, p } = cpmmState\n@@ -302,8 +383,11 @@\n return {\n newUpdatedAnswers,\n yesBets,\n noBuyResults,\n+ // Also return updated state so callers can carry it across iterations\n+ updatedUnfilledBetsByAnswer: workingUnfilledBetsByAnswer,\n+ updatedBalanceByUserId: workingBalanceByUserId,\n }\n }\n \n export const combineBetsOnSameAnswers = (\n@@ -323,21 +407,32 @@\n const totalFees = betsForAnswer.reduce(\n (acc, b) => addObjects(acc, b.totalFees),\n extraFees\n )\n+ // Make extra taker fills beyond the user's paid iteration free by zeroing their amounts and fees,\n+ // while still summing shares so probabilities update correctly.\n+ const takers = betsForAnswer.flatMap((r) => r.takers)\n+ const adjustedTakers = fillsFollowingFirstAreFree\n+ ? (() => {\n+ const cloned = takers.map((t) => ({ ...t }))\n+ let idx = 0\n+ for (const r of betsForAnswer) {\n+ const count = r.takers.length\n+ const slice = cloned.slice(idx, idx + count)\n+ if ((r.iteration ?? 0) > 0) {\n+ for (const t of slice) {\n+ t.amount = 0\n+ t.fees = noFees\n+ }\n+ }\n+ idx += count\n+ }\n+ return cloned\n+ })()\n+ : takers\n return {\n ...bet,\n- takers: fillsFollowingFirstAreFree\n- ? [\n- {\n- ...bet.takers[0],\n- shares: sumBy(\n- betsForAnswer.flatMap((r) => r.takers),\n- 'shares'\n- ),\n- },\n- ]\n- : betsForAnswer.flatMap((r) => r.takers),\n+ takers: adjustedTakers,\n makers: betsForAnswer.flatMap((r) => r.makers),\n ordersToCancel: betsForAnswer.flatMap((r) => r.ordersToCancel),\n outcome,\n cpmmState: { p: 0.5, pool: { YES: poolYes, NO: poolNo }, collectedFees },\n@@ -678,9 +773,9 @@\n ),\n answer,\n }\n })\n-\n+ //{\"id\": \"tQudZcEtlp\", \"slug\": \"whos-gonna-win-gn8sCuyRpl\", \"volume\": 0, \"answers\": [{\"id\": \"Ncus9Qtty2\", \"prob\": 0.16666666666666666, \"text\": \"a\", \"index\": 0, \"poolNo\": 100, \"userId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"isOther\": false, \"poolYes\": 500, \"contractId\": \"tQudZcEtlp\", \"createdTime\": 1755714659074, \"probChanges\": {\"day\": 0, \"week\": 0, \"month\": 0}, \"subsidyPool\": 0, \"totalLiquidity\": 223.60679774997897}, {\"id\": \"CAqyQ8AOSn\", \"prob\": 0.16666666666666666, \"text\": \"b\", \"index\": 1, \"poolNo\": 100, \"userId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"isOther\": false, \"poolYes\": 500, \"contractId\": \"tQudZcEtlp\", \"createdTime\": 1755714659074, \"probChanges\": {\"day\": 0, \"week\": 0, \"month\": 0}, \"subsidyPool\": 0, \"totalLiquidity\": 223.60679774997897}, {\"id\": \"Pc86OAUEsn\", \"prob\": 0.16666666666666666, \"text\": \"c\", \"index\": 2, \"poolNo\": 100, \"userId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"isOther\": false, \"poolYes\": 500, \"contractId\": \"tQudZcEtlp\", \"createdTime\": 1755714659074, \"probChanges\": {\"day\": 0, \"week\": 0, \"month\": 0}, \"subsidyPool\": 0, \"totalLiquidity\": 223.60679774997897}, {\"id\": \"dn0gpUIzpq\", \"prob\": 0.16666666666666666, \"text\": \"d\", \"index\": 3, \"poolNo\": 100, \"userId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"isOther\": false, \"poolYes\": 500, \"contractId\": \"tQudZcEtlp\", \"createdTime\": 1755714659074, \"probChanges\": {\"day\": 0, \"week\": 0, \"month\": 0}, \"subsidyPool\": 0, \"totalLiquidity\": 223.60679774997897}, {\"id\": \"uq5uZd5O0A\", \"prob\": 0.16666666666666666, \"text\": \"e\", \"index\": 4, \"poolNo\": 100, \"userId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"isOther\": false, \"poolYes\": 500, \"contractId\": \"tQudZcEtlp\", \"createdTime\": 1755714659074, \"probChanges\": {\"day\": 0, \"week\": 0, \"month\": 0}, \"subsidyPool\": 0, \"totalLiquidity\": 223.60679774997897}, {\"id\": \"ACNE8CLyyS\", \"prob\": 0.16666666666666666, \"text\": \"Other\", \"index\": 5, \"poolNo\": 100, \"userId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"isOther\": true, \"poolYes\": 500, \"contractId\": \"tQudZcEtlp\", \"createdTime\": 1755714659074, \"probChanges\": {\"day\": 0, \"week\": 0, \"month\": 0}, \"subsidyPool\": 0, \"totalLiquidity\": 223.60679774997897}], \"isRanked\": false, \"question\": \"Who's gonna win?\", \"closeTime\": 1767254340000, \"creatorId\": \"6hHpzvRG0pMq8PNJs7RZj2qlZGn2\", \"mechanism\": \"cpmm-multi-1\", \"elasticity\": 4.99, \"groupSlugs\": [\"nonpredictive\"], \"isResolved\": false, \"visibility\": \"public\", \"createdTime\": 1755714659073, \"creatorName\": \"Ian Bobby\", \"description\": {\"type\": \"doc\", \"content\": [{\"type\": \"paragraph\"}]}, \"outcomeType\": \"MULTIPLE_CHOICE\", \"subsidyPool\": 0, \"collectedFees\": {\"creatorFee\": 0, \"platformFee\": 0, \"liquidityFee\": 0}, \"volume24Hours\": 0, \"addAnswersMode\": \"ANYONE\", \"totalLiquidity\": 1000, \"creatorUsername\": \"IanPhilip\", \"lastUpdatedTime\": 1755714659519, \"popularityScore\": 0, \"creatorAvatarUrl\": \"https://firebasestorage.googleapis.com/v0/b/dev-mantic-markets.appspot.com/o/user-images%2FIanPhilip%2FEyIU8AZ2RC.png?alt=media&token=ff41c9e8-21d5-412d-ac19-854a90cce076\", \"uniqueBettorCount\": 0, \"creatorCreatedTime\": 1668811545000, \"uniqueBettorCountDay\": 0, \"shouldAnswersSumToOne\": true}\n let noBetAmount = betAmount - totalYesAmount\n if (floatingArbitrageEqual(noBetAmount, 0)) {\n noBetAmount = 0\n }\n@@ -730,19 +825,20 @@\n balanceByUserId: { [userId: string]: number },\n collectedFees: Fees,\n answerIdsWithFees?: string[]\n ) => {\n- const unfilledBetsByAnswer = groupBy(unfilledBets, (bet) => bet.answerId)\n+ const baseUnfilledBetsByAnswer = groupBy(unfilledBets, (bet) => bet.answerId)\n \n let maxNoShares = 10\n do {\n const result = buyNoSharesInAnswers(\n answers,\n- unfilledBetsByAnswer,\n- balanceByUserId,\n+ { ...baseUnfilledBetsByAnswer },\n+ { ...balanceByUserId },\n maxNoShares,\n collectedFees,\n- answerIdsWithFees\n+ answerIdsWithFees,\n+ false // don't mutate orders during binary search\n )\n const newPools = result.noBetResults.map((r) => r.cpmmState.pool)\n const probSum = sumBy(newPools, (pool) => getCpmmProbability(pool, 0.5))\n if (probSum < 1) break\n@@ -751,26 +847,28 @@\n \n const noShares = binarySearch(0, maxNoShares, (noShares) => {\n const result = buyNoSharesInAnswers(\n answers,\n- unfilledBetsByAnswer,\n- balanceByUserId,\n+ { ...baseUnfilledBetsByAnswer },\n+ { ...balanceByUserId },\n noShares,\n collectedFees,\n- answerIdsWithFees\n+ answerIdsWithFees,\n+ false // don't mutate orders during binary search\n )\n const newPools = result.noBetResults.map((r) => r.cpmmState.pool)\n const diff = 1 - sumBy(newPools, (pool) => getCpmmProbability(pool, 0.5))\n return diff\n })\n \n return buyNoSharesInAnswers(\n answers,\n- unfilledBetsByAnswer,\n- balanceByUserId,\n+ baseUnfilledBetsByAnswer,\n+ { ...balanceByUserId },\n noShares,\n collectedFees,\n- answerIdsWithFees\n+ answerIdsWithFees,\n+ true // mutate orders in final execution\n )\n }\n \n const buyNoSharesInAnswers = (\n@@ -778,39 +876,53 @@\n unfilledBetsByAnswer: Dictionary<LimitBet[]>,\n balanceByUserId: { [userId: string]: number },\n noShares: number,\n collectedFees: Fees,\n- answerIdsWithFees?: string[]\n+ answerIdsWithFees?: string[],\n+ updateOrders: boolean = true\n ) => {\n- const noAmounts = answers.map(({ id, poolYes, poolNo }) =>\n- calculateAmountToBuySharesFixedP(\n- { pool: { YES: poolYes, NO: poolNo }, p: 0.5, collectedFees },\n+ // Sequentially compute each answer's NO leg, updating state to avoid reusing capacity.\n+ let totalNoAmount = 0\n+ const noBetResults: PreliminaryBetResults[] = []\n+ for (const answer of answers) {\n+ const { id, poolYes, poolNo } = answer\n+ const pool = { YES: poolYes, NO: poolNo }\n+ const noAmount = calculateAmountToBuySharesFixedP(\n+ { pool, p: 0.5, collectedFees },\n noShares,\n 'NO',\n unfilledBetsByAnswer[id] ?? [],\n balanceByUserId,\n !answerIdsWithFees?.includes(id)\n )\n- )\n- const totalNoAmount = sum(noAmounts)\n+ totalNoAmount += noAmount\n \n- const noBetResults = noAmounts.map((noAmount, i) => {\n- const answer = answers[i]\n- const pool = { YES: answer.poolYes, NO: answer.poolNo }\n- return {\n+ const res = {\n ...computeFills(\n { pool, p: 0.5, collectedFees },\n 'NO',\n noAmount,\n undefined,\n- unfilledBetsByAnswer[answer.id] ?? [],\n+ unfilledBetsByAnswer[id] ?? [],\n balanceByUserId,\n undefined,\n- !answerIdsWithFees?.includes(answer.id)\n+ !answerIdsWithFees?.includes(id)\n ),\n answer,\n }\n- })\n+\n+ // Apply maker usage to state so later answers don't reuse.\n+ if (updateOrders) {\n+ applyMakersToWorkingState(\n+ res.makers,\n+ res.ordersToCancel,\n+ unfilledBetsByAnswer,\n+ balanceByUserId\n+ )\n+ }\n+\n+ noBetResults.push(res)\n+ }\n // Identity: No shares in all other answers is equal to noShares * (n-1) mana\n const redeemedAmount = noShares * (answers.length - 1)\n // Fees on arbitrage bets are returned\n const extraMana = redeemedAmount - totalNoAmount\n"
|
|
792
|
-
},
|
|
793
|
-
{
|
|
794
|
-
"path": "docs/docs/api.md",
|
|
795
|
-
"status": "modified",
|
|
796
|
-
"diff": "Index: docs/docs/api.md\n===================================================================\n--- docs/docs/api.md\t3ff3df4 (parent)\n+++ docs/docs/api.md\t1f35061 (commit)\n@@ -979,8 +979,223 @@\n ```\n \n Response type: A `Bet`.\n \n+### `POST /v0/multi-bet`\n+\n+Place multiple YES bets on a sums-to-one multiple choice market, targeting the same number of shares on each selected answer.\n+\n+This is only available on `cpmm-multi-1` markets with `shouldAnswersSumToOne=true` and requires at least two answers in the market. The provided `amount` is spent across the selected answers to purchase equal shares per answer at execution time. If a `limitProb` is provided, each leg will only execute up to that price; any unfilled remainder stays as open limit orders until `expiresAt` or cancellation.\n+\n+Parameters:\n+\n+- `contractId`: The ID of the market to bet on.\n+- `answerIds`: Array of answer IDs to buy on. Minimum length 2. Market must have `shouldAnswersSumToOne=true`.\n+- `amount`: Total amount to spend across the selected answers.\n+- `limitProb`: Optional. A number from `0.01` to `0.99` with two decimal places (whole percentage points). Acts as a per-leg limit price.\n+- `expiresAt`: Optional. Unix timestamp (ms). Any unfilled portion will be canceled at this time.\n+\n+Notes:\n+\n+- Only `YES` is supported; `outcome` is implicitly `YES`.\n+- The API returns only the primary bets on the selected answers. Internal arbitrage trades created to keep probabilities summing to 100% are not included in the response.\n+\n+Example request:\n+\n+```bash\n+curl -X POST https://api.manifold.markets/v0/multi-bet \\\n+ -H 'Authorization: Key YOUR_API_KEY' \\\n+ -H 'Content-Type: application/json' \\\n+ -d '{\n+ \"contractId\":\"tQudZcEtlp\",\n+ \"amount\":100,\n+ \"answerIds\":[\"Ncus9Qtty2\",\"CAqyQ8AOSn\",\"Pc86OAUEsn\"]\n+ }'\n+```\n+\n+Example response (array of bets, one per selected answer):\n+\n+```json\n+[\n+ {\n+ \"orderAmount\": 100.00000000000003,\n+ \"amount\": 33.333333333333286,\n+ \"shares\": 125.88308682212488,\n+ \"isFilled\": true,\n+ \"fills\": [\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 110.27413685175395,\n+ \"amount\": 33.333333333333286,\n+ \"timestamp\": 1755718208318,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ },\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 15.313505455862867,\n+ \"amount\": 0,\n+ \"timestamp\": 1755718208324,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ },\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 0.29544451450806264,\n+ \"amount\": 0,\n+ \"timestamp\": 1755718208329,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ }\n+ ],\n+ \"contractId\": \"tQudZcEtlp\",\n+ \"outcome\": \"YES\",\n+ \"isCancelled\": false,\n+ \"loanAmount\": 0,\n+ \"answerId\": \"Ncus9Qtty2\",\n+ \"probBefore\": 0.2568360631674096,\n+ \"probAfter\": 0.2721147216493064,\n+ \"createdTime\": 1755718208333,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ },\n+ \"isRedemption\": false,\n+ \"visibility\": \"public\",\n+ \"betId\": \"zl2cULd0yq5N\",\n+ \"betGroupId\": \"05bd5240dfca66b2556b77e8\"\n+ },\n+ {\n+ \"orderAmount\": 100.00000000000003,\n+ \"amount\": 33.3333333333334,\n+ \"shares\": 125.88308682212511,\n+ \"isFilled\": true,\n+ \"fills\": [\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 110.27413685175412,\n+ \"amount\": 33.3333333333334,\n+ \"timestamp\": 1755718208318,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ },\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 15.31350545586281,\n+ \"amount\": 0,\n+ \"timestamp\": 1755718208324,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ },\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 0.2954445145081763,\n+ \"amount\": 0,\n+ \"timestamp\": 1755718208330,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ }\n+ ],\n+ \"contractId\": \"tQudZcEtlp\",\n+ \"outcome\": \"YES\",\n+ \"isCancelled\": false,\n+ \"loanAmount\": 0,\n+ \"answerId\": \"CAqyQ8AOSn\",\n+ \"probBefore\": 0.2568360631674102,\n+ \"probAfter\": 0.27211472164930733,\n+ \"createdTime\": 1755718208333,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ },\n+ \"isRedemption\": false,\n+ \"visibility\": \"public\",\n+ \"betId\": \"hAnEpO2dOspd\",\n+ \"betGroupId\": \"05bd5240dfca66b2556b77e8\"\n+ },\n+ {\n+ \"orderAmount\": 100.00000000000003,\n+ \"amount\": 33.33333333333334,\n+ \"shares\": 125.88308682212511,\n+ \"isFilled\": true,\n+ \"fills\": [\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 110.27413685175384,\n+ \"amount\": 33.33333333333334,\n+ \"timestamp\": 1755718208318,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ },\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 15.313505455863208,\n+ \"amount\": 0,\n+ \"timestamp\": 1755718208324,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ },\n+ {\n+ \"matchedBetId\": null,\n+ \"shares\": 0.29544451450806264,\n+ \"amount\": 0,\n+ \"timestamp\": 1755718208330,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ }\n+ }\n+ ],\n+ \"contractId\": \"tQudZcEtlp\",\n+ \"outcome\": \"YES\",\n+ \"isCancelled\": false,\n+ \"loanAmount\": 0,\n+ \"answerId\": \"Pc86OAUEsn\",\n+ \"probBefore\": 0.25683606316740987,\n+ \"probAfter\": 0.27211472164930683,\n+ \"createdTime\": 1755718208333,\n+ \"fees\": {\n+ \"creatorFee\": 0,\n+ \"platformFee\": 0,\n+ \"liquidityFee\": 0\n+ },\n+ \"isRedemption\": false,\n+ \"visibility\": \"public\",\n+ \"betId\": \"p9L5NPO09tCz\",\n+ \"betGroupId\": \"05bd5240dfca66b2556b77e8\"\n+ }\n+]\n+```\n+\n+Response type: Array of `Bet`-like objects (same fields as a `Bet`) with additional `betId` and `betGroupId`.\n+\n ### `POST /v0/bet/cancel/[id]`\n \n Cancel a limit order.\n \n"
|
|
797
|
-
}
|
|
798
|
-
]
|
|
799
|
-
},
|
|
800
|
-
{
|
|
801
|
-
"id": "free-market-creation",
|
|
802
|
-
"sha": "ddb266fa95162854d4de4dbd7ea4fee3d8ae25d5",
|
|
803
|
-
"parentSha": "7173b73b777cc2871f29a10e958533e447c58435",
|
|
804
|
-
"spec": "Implement a 'free market' creation path for a specific user with house-funded ante and a daily limit.\n\nBackend:\n1) In backend/api/src/create-market.ts\n- Define a free-market condition: the creator is FREE_MARKET_USER_ID and totalMarketCost <= 100.\n- If free-market condition is true:\n - Skip enforcing that the user's balance must be at least totalMarketCost.\n - Enforce a daily creation limit: if the user has created 5 or more contracts in the past day, reject with 403 and a clear message.\n - Select the funding providerId as the house liquidity provider based on environment (production uses HOUSE_LIQUIDITY_PROVIDER_ID, non-prod uses DEV_HOUSE_LIQUIDITY_PROVIDER_ID). Otherwise, use the creator's userId as the providerId.\n - When creating the initial funding transaction (CREATE_CONTRACT_ANTE), use providerId (house for free markets; user for others) as fromId, with toId = contract.id and amount = ante.\n - Pass providerId into generateAntes so the initial liquidity is attributed to the chosen provider.\n- Ensure isProd is available for environment detection and that FREE_MARKET_USER_ID is imported from common/economy.\n\n2) In backend/shared/src/create-contract-helpers.ts\n- Change generateAntes signature to remove the outcomeType parameter: generateAntes(pg, providerId, contract, ante, totalMarketCost).\n- Keep its observable behavior intact: it should provision initial liquidity for the given contract and amount, attributing it to providerId.\n- Remove any import/types tied solely to the removed parameter.\n\n3) Update all call sites of generateAntes\n- In backend/api/src/create-market.ts, update to the new signature and pass providerId, contract, ante, totalMarketCost (without outcomeType).\n- Update any scripts or other usages (e.g., backend/scripts/reimburse-broken-markets.ts if present) to match the new signature.\n\n4) Add FREE_MARKET_USER_ID constant\n- In common/src/economy.ts export a constant FREE_MARKET_USER_ID for the user allowed to create free markets.\n\nFrontend:\n5) In web/components/new-contract/contract-params-form.tsx\n- Import FREE_MARKET_USER_ID.\n- Detect free-market condition on the client: creator.id === FREE_MARKET_USER_ID and ante <= 100.\n- Validation: Allow form submission even if ante > creator.balance when free-market condition is true. Otherwise, retain existing balance validation.\n- Display: When passing balance to CostSection, if free-market condition is true, pass 100 instead of the actual user balance so the UI reflects available free amount.\n\nConstraints/Behavioral expectations:\n- Free-market creation applies only to the whitelisted user ID and only when the ante/totalMarketCost does not exceed 100.\n- Free-market creations are limited to 5 per rolling 24-hour period per the server-side check.\n- In free-market cases, the ante is funded by the house liquidity provider (env-specific ID) rather than the user.\n- Non-free cases should remain unchanged: normal balance checks, user-funded ante, and normal call flows.\n- Client-side changes should reflect the ability to proceed despite low balance only for the whitelisted user and within the 100 free amount cap.",
|
|
805
|
-
"prompt": "Implement a mode where a specific whitelisted user can create markets for free up to a small daily budget. When this user creates a market under that limit, the initial ante should be funded by the house instead of the user, with environment-specific house IDs used appropriately, and the server must enforce a daily cap. Update the market creation form so this user isn’t blocked by their own balance for these free creations and the UI shows the effective free balance for the cost section. Also, adjust any helpers to support this behavior and update any impacted function signatures and usages accordingly.",
|
|
806
|
-
"supplementalFiles": [
|
|
807
|
-
"backend/shared/src/utils.ts",
|
|
808
|
-
"common/src/antes.ts",
|
|
809
|
-
"web/components/new-contract/cost-section.tsx",
|
|
810
|
-
"backend/scripts/reimburse-broken-markets.ts"
|
|
811
|
-
],
|
|
812
|
-
"fileDiffs": [
|
|
813
|
-
{
|
|
814
|
-
"path": "backend/api/src/create-market.ts",
|
|
815
|
-
"status": "modified",
|
|
816
|
-
"diff": "Index: backend/api/src/create-market.ts\n===================================================================\n--- backend/api/src/create-market.ts\t7173b73 (parent)\n+++ backend/api/src/create-market.ts\tddb266f (commit)\n@@ -20,9 +20,9 @@\n nativeContractColumnsArray,\n NUMBER_CREATION_ENABLED,\n PollVoterVisibility,\n } from 'common/contract'\n-import { getAnte } from 'common/economy'\n+import { FREE_MARKET_USER_ID, getAnte } from 'common/economy'\n import { MAX_GROUPS_PER_MARKET } from 'common/group'\n import { getNewContract } from 'common/new-contract'\n import { getPseudoProbability } from 'common/pseudo-numeric'\n import { STONK_INITIAL_PROB } from 'common/stonk'\n@@ -44,10 +44,15 @@\n import {\n addGroupToContract,\n canUserAddGroupToMarket,\n } from 'shared/update-group-contracts-internal'\n-import { contractColumnsToSelect, htmlToRichText, log } from 'shared/utils'\n import {\n+ contractColumnsToSelect,\n+ htmlToRichText,\n+ isProd,\n+ log,\n+} from 'shared/utils'\n+import {\n broadcastNewAnswer,\n broadcastNewContract,\n } from 'shared/websockets/helpers'\n import { APIError, AuthedUser, type APIHandler } from './helpers/endpoint'\n@@ -60,8 +65,12 @@\n import { betsQueue } from 'shared/helpers/fn-queue'\n import { convertUser } from 'common/supabase/users'\n import { camelCase, first } from 'lodash'\n import { getMultiNumericAnswerBucketRangeNames } from 'common/number'\n+import {\n+ DEV_HOUSE_LIQUIDITY_PROVIDER_ID,\n+ HOUSE_LIQUIDITY_PROVIDER_ID,\n+} from 'common/antes'\n type Body = ValidatedAPIParams<'market'>\n \n export const createMarket: APIHandler<'market'> = async (body, auth) => {\n const pg = createSupabaseDirectClient()\n@@ -181,10 +190,22 @@\n const user = first(userAndSlugResult[0].map(convertUser))\n if (!user) throw new APIError(401, 'Your account was not found')\n if (user.isBannedFromPosting) throw new APIError(403, 'You are banned')\n \n- if (totalMarketCost > user.balance)\n+ const isFree = userId === FREE_MARKET_USER_ID && totalMarketCost <= 100\n+ if (!isFree && totalMarketCost > user.balance)\n throw new APIError(403, `Balance must be at least ${totalMarketCost}.`)\n+ if (isFree) {\n+ const contractsToday = await tx.oneOrNone(\n+ `select count(*) from contracts where creator_id = $1 and created_time > now() - interval '1 day'`,\n+ [userId]\n+ )\n+ if (contractsToday && contractsToday.count >= 5)\n+ throw new APIError(\n+ 403,\n+ 'Tumblingnomics has reached its breaking point. No more free markets today.'\n+ )\n+ }\n \n const slug = getSlug(!!first(userAndSlugResult[1]), proposedSlug)\n \n const contract = getNewContract(\n@@ -254,10 +275,14 @@\n \n if (result[1].length > 0 && contract.mechanism === 'cpmm-multi-1') {\n contract.answers = result[1].map(convertAnswer)\n }\n+ const house = isProd()\n+ ? HOUSE_LIQUIDITY_PROVIDER_ID\n+ : DEV_HOUSE_LIQUIDITY_PROVIDER_ID\n+ const providerId = isFree ? house : userId\n await runTxnOutsideBetQueue(tx, {\n- fromId: userId,\n+ fromId: providerId,\n fromType: 'USER',\n toId: contract.id,\n toType: 'CONTRACT',\n amount: ante,\n@@ -271,16 +296,9 @@\n question,\n ante: ante || 0,\n })\n \n- await generateAntes(\n- tx,\n- userId,\n- contract,\n- outcomeType,\n- ante,\n- totalMarketCost\n- )\n+ await generateAntes(tx, providerId, contract, ante, totalMarketCost)\n \n return { contract, user }\n })\n }, [userId])\n"
|
|
817
|
-
},
|
|
818
|
-
{
|
|
819
|
-
"path": "backend/shared/src/create-contract-helpers.ts",
|
|
820
|
-
"status": "modified",
|
|
821
|
-
"diff": "Index: backend/shared/src/create-contract-helpers.ts\n===================================================================\n--- backend/shared/src/create-contract-helpers.ts\t7173b73 (parent)\n+++ backend/shared/src/create-contract-helpers.ts\tddb266f (commit)\n@@ -1,12 +1,7 @@\n import { getNewLiquidityProvision } from 'common/add-liquidity'\n import { getCpmmInitialLiquidity } from 'common/antes'\n-import {\n- BinaryContract,\n- Contract,\n- CPMMMultiContract,\n- OutcomeType,\n-} from 'common/contract'\n+import { BinaryContract, Contract, CPMMMultiContract } from 'common/contract'\n import { updateContract } from './supabase/contracts'\n import { SupabaseDirectClient } from './supabase/init'\n import { insertLiquidity } from './supabase/liquidity'\n import { FieldVal } from './supabase/utils'\n@@ -15,9 +10,8 @@\n export async function generateAntes(\n pg: SupabaseDirectClient,\n providerId: string,\n contract: Contract,\n- outcomeType: OutcomeType,\n ante: number,\n totalMarketCost: number\n ) {\n if (\n"
|
|
822
|
-
},
|
|
823
|
-
{
|
|
824
|
-
"path": "common/src/economy.ts",
|
|
825
|
-
"status": "modified",
|
|
826
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\t7173b73 (parent)\n+++ common/src/economy.ts\tddb266f (commit)\n@@ -164,4 +164,5 @@\n export const PROFIT_FEE_FRACTION = 0.1\n export const BOOST_COST_MANA = 10000\n export const DEV_BOOST_STRIPE_PRICE_ID = 'price_1QuI5BGdoFKoCJW7lMjCIuKW'\n export const PROD_BOOST_STRIPE_PRICE_ID = 'price_1QuItEGdoFKoCJW7t9qtiGoD'\n+export const FREE_MARKET_USER_ID = '6hHpzvRG0pMq8PNJs7RZj2qlZGn2'\n"
|
|
827
|
-
},
|
|
828
|
-
{
|
|
829
|
-
"path": "web/components/new-contract/contract-params-form.tsx",
|
|
830
|
-
"status": "modified",
|
|
831
|
-
"diff": "Index: web/components/new-contract/contract-params-form.tsx\n===================================================================\n--- web/components/new-contract/contract-params-form.tsx\t7173b73 (parent)\n+++ web/components/new-contract/contract-params-form.tsx\tddb266f (commit)\n@@ -16,8 +16,9 @@\n PollVoterVisibility,\n contractPath,\n } from 'common/contract'\n import {\n+ FREE_MARKET_USER_ID,\n getAnte,\n getUniqueBettorBonusAmount,\n MINIMUM_BOUNTY,\n } from 'common/economy'\n@@ -433,12 +434,13 @@\n const midpointsError =\n outcomeType === 'MULTI_NUMERIC' || outcomeType === 'DATE'\n ? midpoints.length !== answers.length\n : false\n+ const isFree = creator.id === FREE_MARKET_USER_ID && ante <= 100\n \n const isValid =\n isValidQuestion &&\n- ante <= balance &&\n+ (isFree ? true : ante <= balance) &&\n isValidDate &&\n isValidTopics &&\n (outcomeType !== 'PSEUDO_NUMERIC' ||\n (initialValue !== undefined &&\n@@ -1086,9 +1088,9 @@\n }}\n />\n </Row>\n <CostSection\n- balance={balance}\n+ balance={isFree ? 100 : balance}\n numAnswers={numAnswers}\n outcomeType={outcomeType}\n liquidityTier={liquidityTier}\n setLiquidityTier={setLiquidityTier}\n"
|
|
832
|
-
}
|
|
833
|
-
]
|
|
834
|
-
},
|
|
835
|
-
{
|
|
836
|
-
"id": "implement-market-boosts",
|
|
837
|
-
"sha": "cd6dd8c2753747eff4395814f38c3f6253577006",
|
|
838
|
-
"parentSha": "3db506179f5ce88d0865f7a4d83b465313f19d76",
|
|
839
|
-
"spec": "Implement a paid, schedulable 24-hour homepage boost system for markets (\"boosts\") that affects ranking and UI indicators while active.\n\nDatabase\n- Add a new table: backend/supabase/contract_boosts.sql\n - Columns: id (bigint identity PK), contract_id (FK to contracts.id), user_id (FK to users.id), start_time (timestamptz), end_time (timestamptz).\n - Indexes: (contract_id, start_time, end_time) and (user_id).\n - Enable RLS.\n- Modify backend/supabase/contracts.sql to add a new column to contracts:\n - boosted boolean default false not null.\n - Stop deriving popularity_score from data in the trigger (do not set from JSON).\n\nCommon types and schema\n- Update common/src/supabase/schema.ts to include contract_boosts (Row/Insert/Update/Relationships) and add boosted to contracts Row/Insert/Update types.\n- Update common/src/supabase/contracts.ts convertContract() to map the native boosted column into the Contract shape (default to false if null).\n- In common/src/contract.ts, augment Contract with a boosted: boolean (maintained via native column). Export nativeContractColumnsArray listing all DB-native columns (snake_case) that are stored in contracts outside of the JSON data field.\n- Add BOOST_COST_MANA = 10000 to common/src/economy.ts.\n\nShared utilities and refactor\n- In backend/shared/src/utils.ts define:\n - contractColumnsToSelect as a comma-joined list of the DB-native columns from nativeContractColumnsArray.\n - prefixedContractColumnsToSelect as the same list prefixed with c. for SQL joins.\n- In backend/shared/src/supabase/contracts.ts:\n - updateContract(pg, id, update) should reject attempts to modify native DB columns (throw an error if keys overlap nativeContractColumnsArray).\n - Add updateContractNativeColumns(pg, contractId, updated) to update native columns (using update(), not updateData()), convert the result, and broadcast changes (with camelCased keys) via websockets.\n\nContract creation\n- In backend/api/src/create-market.ts:\n - Derive the set of camelCase keys for nativeContractColumnsArray.\n - Exclude those keys from the serialized JSON data blob.\n - Insert into contracts specifying: id, data, and all native columns in the same order as contractColumnsToSelect. Ensure parameter binding matches the exact native column order.\n\nBoost purchase API\n- Define a new authed POST API route 'purchase-contract-boost' in common/src/api/schema.ts with props { contractId: string, startTime: number (ms) } and returns { success: boolean }.\n- Implement backend/api/src/purchase-contract-boost.ts:\n - Validate the contract exists and is visible.\n - Enforce constraints for the selected start time:\n - No existing boost for the same contract overlaps that start time.\n - A global limit of MAX_ACTIVE_BOOSTS = 5 overlapping boosts for that instant.\n - Violations return HTTP 400 with descriptive messages.\n - Within a DB transaction:\n - Insert a new contract_boosts row with (contract_id, user_id, start_time, end_time = start + 24h).\n - Charge the user M$ via runTxnInBetQueue with category CONTRACT_BOOST_PURCHASE, sending funds to the house bank account (prod vs dev IDs), embedding { contractId, boostId } in txn data.\n - If the selected startTime is now or earlier, immediately set contracts.boosted = true and increase importance_score by +0.25 using updateContractNativeColumns.\n- Register the handler in backend/api/src/routes.ts.\n\nImportance score integration\n- In backend/shared/src/importance-score.ts:\n - Use prefixedContractColumnsToSelect for contract selection and convert to Contract objects as usual.\n - Query currently active boosts (start_time <= now < end_time) and compute a per-contract isBoosted flag.\n - Pass isBoosted into the computeContractScores function.\n - Add a +0.25 boostScore component to the raw importance score when isBoosted is true.\n - When persisting updated contracts via bulk update, include boosted and the new scores (importance_score, freshness_score, daily_score) but only for valid numeric values.\n - Include Boost in the diagnostics output of score breakdowns for logging/inspection.\n\nFrontend\n- Add a boost purchase UX:\n - Component to trigger boost: For eligible markets (public, not closed/resolved), display a button labeled \"Boost market\" with a rocket icon; disable if contract.boosted is true; otherwise open a modal.\n - Modal flow: Explain the 24-hour boost, show or pick a start date (default now; cannot be in the past), show price as a TokenNumber using BOOST_COST_MANA, and handle insufficient balance by prompting AddFundsModal. On confirm, call api('purchase-contract-boost') and surface a success or error toast. Close on success.\n- Indicate boosted state in tables and lists:\n - Add a \"Boosted\" column displaying an icon when contract.boosted is true.\n- Replace TierTooltip with a simplified LiquidityTooltip wherever tier badges were displayed (contract summary, feed card fallback, search list): show a droplet icon with the formatted liquidity amount; remove tier name badges.\n- Update the contract page and share panels to include the boost CTA for both creators and non-creators in appropriate positions without breaking existing layout.\n\nWebsocket updates\n- Ensure updates to native contract columns (including boosted and scores) trigger broadcastUpdatedContract so clients receive real-time updates.\n\nAcceptance criteria\n- Purchasing a boost creates a contract_boosts row, charges exactly M$ 10,000 to the house account, and immediately toggles boosted to true and adds +0.25 to importance when the start time is now (or earlier); future boosts do not set boosted until active.\n- Enforce at most 5 simultaneous boosts for any moment, and prevent the same contract from having overlapping boosts; return clear 400 errors on violations.\n- The importance-score job applies boostScore while active and persists boosted + scores. When a boost window ends, boosted returns to false and the boost component is no longer applied.\n- New contract creation inserts native columns separately from the JSON data with correct ordering and values.\n- UI shows the boost action only when eligible, handles insufficient balance, displays success/error toasts, renders boosted indicators in tables, and shows a liquidity tooltip where tiers used to be.\n- API schema and typings are consistent across client/server for the new endpoint and transaction category.",
|
|
840
|
-
"prompt": "Add a paid 24-hour homepage boost feature for markets. Users should be able to purchase a boost for a specific market to raise its homepage prominence for a day starting now or on a chosen future date. Limit the number of overlapping boosts and prevent overlapping boosts for the same market. Charge the buyer in M$ and record the purchase. While a boost is active, the market should receive a fixed increase in its ranking score and display a boosted indicator in lists. When the boost ends, the boost effect should stop. Update the UI to let users initiate a boost via a modal, pick a start date, see the price, handle insufficient balance, and show success or error messages. Replace the previous tier badge visuals with a simple liquidity tooltip wherever they appeared. Ensure new markets write native contract fields to dedicated DB columns and restrict JSON-based updates from modifying those native fields. Keep API typings, scoring integration, database schema, and websocket broadcasting consistent with existing conventions.",
|
|
841
|
-
"supplementalFiles": [
|
|
842
|
-
"backend/shared/src/txn/run-txn.ts",
|
|
843
|
-
"backend/shared/src/supabase/utils.ts",
|
|
844
|
-
"backend/shared/src/supabase/init.ts",
|
|
845
|
-
"backend/shared/src/websockets/helpers.ts",
|
|
846
|
-
"common/src/supabase/utils.ts",
|
|
847
|
-
"common/src/util/time.ts",
|
|
848
|
-
"web/lib/api/api.ts",
|
|
849
|
-
"web/components/buttons/button.tsx",
|
|
850
|
-
"web/components/layout/modal.tsx",
|
|
851
|
-
"web/components/layout/col.tsx",
|
|
852
|
-
"web/components/layout/row.tsx",
|
|
853
|
-
"web/components/widgets/input.tsx",
|
|
854
|
-
"web/components/widgets/token-number.tsx",
|
|
855
|
-
"web/components/widgets/tooltip.tsx",
|
|
856
|
-
"web/public/custom-components/tiers.tsx"
|
|
857
|
-
],
|
|
858
|
-
"fileDiffs": [
|
|
859
|
-
{
|
|
860
|
-
"path": "backend/api/src/create-market.ts",
|
|
861
|
-
"status": "modified",
|
|
862
|
-
"diff": "Index: backend/api/src/create-market.ts\n===================================================================\n--- backend/api/src/create-market.ts\t3db5061 (parent)\n+++ backend/api/src/create-market.ts\tcd6dd8c (commit)\n@@ -9,13 +9,15 @@\n toLiteMarket,\n } from 'common/api/market-types'\n import { ValidatedAPIParams } from 'common/api/schema'\n import {\n+ Contract,\n MULTI_NUMERIC_CREATION_ENABLED,\n NO_CLOSE_TIME_TYPES,\n OutcomeType,\n add_answers_mode,\n contractUrl,\n+ nativeContractColumnsArray,\n } from 'common/contract'\n import { getAnte } from 'common/economy'\n import { MAX_GROUPS_PER_MARKET } from 'common/group'\n import { getMultiNumericAnswerBucketRangeNames } from 'common/multi-numeric'\n@@ -40,9 +42,9 @@\n import {\n addGroupToContract,\n canUserAddGroupToMarket,\n } from 'shared/update-group-contracts-internal'\n-import { htmlToRichText, log } from 'shared/utils'\n+import { contractColumnsToSelect, htmlToRichText, log } from 'shared/utils'\n import {\n broadcastNewAnswer,\n broadcastNewContract,\n } from 'shared/websockets/helpers'\n@@ -54,9 +56,9 @@\n import { convertAnswer } from 'common/supabase/contracts'\n import { generateAntes } from 'shared/create-contract-helpers'\n import { betsQueue } from 'shared/helpers/fn-queue'\n import { convertUser } from 'common/supabase/users'\n-import { first } from 'lodash'\n+import { camelCase, first } from 'lodash'\n import { getTieredCost } from 'common/tier'\n \n type Body = ValidatedAPIParams<'market'>\n \n@@ -224,16 +226,25 @@\n sportsLeague,\n takerAPIOrdersDisabled,\n })\n )\n+ const nativeKeys = nativeContractColumnsArray.map(camelCase)\n+ const nativeValues = nativeKeys\n+ .filter((col) => col in contract)\n+ .map((col) => contract[col as keyof Contract])\n \n+ const contractDataToInsert = Object.fromEntries(\n+ Object.entries(contract).filter(([key]) => !nativeKeys.includes(key))\n+ )\n const insertAnswersQuery =\n contract.mechanism === 'cpmm-multi-1'\n ? bulkInsertQuery('answers', contract.answers.map(answerToRow), true)\n : 'select 1 where false'\n const contractQuery = pgp.as.format(\n- `insert into contracts (id, data, token) values ($1, $2, $3);`,\n- [contract.id, JSON.stringify(contract), contract.token]\n+ `insert into contracts \n+ (id, ${contractColumnsToSelect})\n+ values ($1, $2, ${nativeValues.map((_, i) => `$${i + 3}`)});`,\n+ [contract.id, JSON.stringify(contractDataToInsert), ...nativeValues]\n )\n const result = await tx.multi(\n `${contractQuery};\n ${insertAnswersQuery};`\n"
|
|
863
|
-
},
|
|
864
|
-
{
|
|
865
|
-
"path": "backend/api/src/purchase-contract-boost.ts",
|
|
866
|
-
"status": "added",
|
|
867
|
-
"diff": "Index: backend/api/src/purchase-contract-boost.ts\n===================================================================\n--- backend/api/src/purchase-contract-boost.ts\t3db5061 (parent)\n+++ backend/api/src/purchase-contract-boost.ts\tcd6dd8c (commit)\n@@ -0,0 +1,87 @@\n+import { APIError, APIHandler } from './helpers/endpoint'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { DAY_MS } from 'common/util/time'\n+import {\n+ DEV_HOUSE_LIQUIDITY_PROVIDER_ID,\n+ HOUSE_LIQUIDITY_PROVIDER_ID,\n+} from 'common/antes'\n+import { getContract, isProd } from 'shared/utils'\n+import { runTxnInBetQueue, TxnData } from 'shared/txn/run-txn'\n+import { ContractBoostPurchaseTxn } from 'common/txn'\n+import { Row } from 'common/supabase/utils'\n+import { BOOST_COST_MANA } from 'common/economy'\n+import { updateContractNativeColumns } from 'shared/supabase/contracts'\n+\n+const MAX_ACTIVE_BOOSTS = 5\n+\n+export const purchaseContractBoost: APIHandler<\n+ 'purchase-contract-boost'\n+> = async (props, auth) => {\n+ const { contractId, startTime } = props\n+ const userId = auth.uid\n+\n+ const pg = createSupabaseDirectClient()\n+\n+ // Check if contract exists and user can see it\n+ const contract = await getContract(pg, contractId)\n+ if (!contract) {\n+ throw new APIError(404, 'Contract not found')\n+ }\n+\n+ // Check if there's already an active boost\n+ const activeBoost = await pg.manyOrNone<Row<'contract_boosts'>>(\n+ `select * from contract_boosts \n+ where millis_to_ts($1) between start_time and end_time`,\n+ [startTime]\n+ )\n+ if (activeBoost.some((b) => b.contract_id === contractId)) {\n+ throw new APIError(\n+ 400,\n+ 'Contract already has an active boost for that time'\n+ )\n+ }\n+ if (activeBoost.length >= MAX_ACTIVE_BOOSTS) {\n+ throw new APIError(\n+ 400,\n+ 'That time period has the maximum number of boosts. Please select a different time.'\n+ )\n+ }\n+\n+ // Start transaction\n+ await pg.tx(async (tx) => {\n+ const boost = await tx.one(\n+ `insert into contract_boosts (contract_id, user_id, start_time, end_time)\n+ values ($1, $2, millis_to_ts($3), millis_to_ts($4))\n+ returning *`,\n+ [contractId, userId, startTime, startTime + DAY_MS]\n+ )\n+\n+ // Charge mana\n+ const txnData: TxnData = {\n+ category: 'CONTRACT_BOOST_PURCHASE',\n+ fromType: 'USER',\n+ toType: 'BANK',\n+ token: 'M$',\n+ data: { contractId, boostId: boost.id },\n+ amount: BOOST_COST_MANA,\n+ fromId: userId,\n+ toId: isProd()\n+ ? HOUSE_LIQUIDITY_PROVIDER_ID\n+ : DEV_HOUSE_LIQUIDITY_PROVIDER_ID,\n+ } as ContractBoostPurchaseTxn\n+\n+ await runTxnInBetQueue(tx, txnData)\n+ })\n+\n+ return {\n+ result: { success: true },\n+ continue: async () => {\n+ if (startTime <= Date.now()) {\n+ await updateContractNativeColumns(pg, contractId, {\n+ boosted: true,\n+ importance_score: contract.importanceScore + 0.25,\n+ })\n+ }\n+ },\n+ }\n+}\n"
|
|
868
|
-
},
|
|
869
|
-
{
|
|
870
|
-
"path": "backend/api/src/routes.ts",
|
|
871
|
-
"status": "modified",
|
|
872
|
-
"diff": "Index: backend/api/src/routes.ts\n===================================================================\n--- backend/api/src/routes.ts\t3db5061 (parent)\n+++ backend/api/src/routes.ts\tcd6dd8c (commit)\n@@ -161,8 +161,9 @@\n import {\n getContractOptionVoters,\n getContractVoters,\n } from './get-contract-voters'\n+import { purchaseContractBoost } from './purchase-contract-boost'\n // we define the handlers in this object in order to typecheck that every API has a handler\n export const handlers: { [k in APIPath]: APIHandler<k> } = {\n 'refresh-all-clients': refreshAllClients,\n bet: placeBet,\n@@ -339,5 +340,6 @@\n 'comment-reactions': getReactions,\n 'mark-all-notifications-new': markallnotificationsnew,\n 'get-contract-voters': getContractVoters,\n 'get-contract-option-voters': getContractOptionVoters,\n+ 'purchase-contract-boost': purchaseContractBoost,\n }\n"
|
|
873
|
-
},
|
|
874
|
-
{
|
|
875
|
-
"path": "backend/shared/src/importance-score.ts",
|
|
876
|
-
"status": "modified",
|
|
877
|
-
"diff": "Index: backend/shared/src/importance-score.ts\n===================================================================\n--- backend/shared/src/importance-score.ts\t3db5061 (parent)\n+++ backend/shared/src/importance-score.ts\tcd6dd8c (commit)\n@@ -9,9 +9,9 @@\n MONTH_MS,\n WEEK_MS,\n YEAR_MS,\n } from 'common/util/time'\n-import { log } from 'shared/utils'\n+import { log, prefixedContractColumnsToSelect } from 'shared/utils'\n import {\n BountiedQuestionContract,\n Contract,\n isMarketRanked,\n@@ -22,8 +22,9 @@\n \n import { BOT_USERNAMES } from 'common/envs/constants'\n import { bulkUpdate } from 'shared/supabase/utils'\n import { convertContract } from 'common/supabase/contracts'\n+import { Row } from 'common/supabase/utils'\n \n export const IMPORTANCE_MINUTE_INTERVAL = 2\n export const MIN_IMPORTANCE_SCORE = 0.1\n \n@@ -37,17 +38,12 @@\n const hourAgo = now - HOUR_MS\n const dayAgo = now - DAY_MS\n const weekAgo = now - 7 * DAY_MS\n const select = (whereClause: string) => `\n- select c.data,\n- conversion_score,\n- importance_score,\n- freshness_score,\n- view_count,\n- daily_score,\n+ select ${prefixedContractColumnsToSelect}\n case when count(a.prob) > 0 then json_agg(a.prob) end as answer_probs\n from contracts c\n- left join answers a on c.id = a.contract_id\n+ left join answers a on c.id = a.contract_id\n ${whereClause}\n group by c.id\n `\n const convertRow = (row: any) => {\n@@ -106,13 +102,17 @@\n const thisWeekTradersByContract = {\n ...(await getContractTraders(pg, weekAgo, contractIds)),\n ...(await getContractVoters(pg, weekAgo, contractIds)),\n }\n+ const activeBoosts = await pg.manyOrNone<Row<'contract_boosts'>>(\n+ `select * from contract_boosts where start_time <= now() and end_time > now()`\n+ )\n \n const contractsWithUpdates: Contract[] = []\n const marketComponents: Record<string, any>[] = []\n \n for (const contract of contracts) {\n+ const boosted = activeBoosts.some((b) => b.contract_id === contract.id)\n const {\n importanceScore,\n freshnessScore,\n dailyScore,\n@@ -124,9 +124,10 @@\n todayLikesByContract[contract.id] ?? 0,\n thisWeekLikesByContract[contract.id] ?? 0,\n todayTradersByContract[contract.id] ?? 0,\n hourAgoTradersByContract[contract.id] ?? 0,\n- thisWeekTradersByContract[contract.id] ?? 0\n+ thisWeekTradersByContract[contract.id] ?? 0,\n+ boosted\n )\n \n if (isNaN(dailyScore)) {\n log.error('NaN daily score for contract ' + contract.id)\n@@ -143,13 +144,15 @@\n if (\n rescoreAll ||\n !floatingEqual(importanceScore, contract.importanceScore, epsilon) ||\n !floatingEqual(freshnessScore, contract.freshnessScore, epsilon) ||\n- !floatingEqual(dailyScore, contract.dailyScore, epsilon)\n+ !floatingEqual(dailyScore, contract.dailyScore, epsilon) ||\n+ boosted !== contract.boosted\n ) {\n contract.importanceScore = importanceScore\n contract.freshnessScore = freshnessScore\n contract.dailyScore = dailyScore\n+ contract.boosted = boosted\n contractsWithUpdates.push(contract)\n marketComponents.push(rawMarketImportanceBreakdown)\n }\n }\n@@ -183,8 +186,9 @@\n Closing: breakdown.closingSoonnnessComponent?.toFixed(2) || '',\n Comments: breakdown.commentScore?.toFixed(2) || '',\n Week: breakdown.thisWeekScoreComponent?.toFixed(2) || '',\n Ranked: breakdown.rankedScore?.toFixed(2) || '',\n+ Boost: breakdown.boostScore?.toFixed(2) || '',\n }\n })\n \n console.table(topContracts)\n@@ -244,14 +248,22 @@\n await bulkUpdate(\n pg,\n 'contracts',\n ['id'],\n- contractsWithUpdates.map((contract) => ({\n- id: contract.id,\n- importance_score: contract.importanceScore,\n- freshness_score: contract.freshnessScore,\n- daily_score: contract.dailyScore,\n- }))\n+ contractsWithUpdates\n+ .filter(\n+ (c) =>\n+ !isNaN(c.importanceScore) &&\n+ !isNaN(c.freshnessScore) &&\n+ !isNaN(c.dailyScore)\n+ )\n+ .map((contract) => ({\n+ id: contract.id,\n+ boosted: contract.boosted,\n+ importance_score: contract.importanceScore,\n+ freshness_score: contract.freshnessScore,\n+ daily_score: contract.dailyScore,\n+ }))\n )\n }\n }\n \n@@ -310,14 +322,13 @@\n likesToday: number,\n likesWeek: number,\n tradersToday: number,\n traderHour: number,\n- tradersWeek: number\n+ tradersWeek: number,\n+ isBoosted: boolean\n ) => {\n const todayScore = likesToday + tradersToday\n const thisWeekScore = likesWeek + tradersWeek\n- const thisWeekScoreWeight = thisWeekScore / 10\n- const popularityScore = todayScore + thisWeekScoreWeight\n const wasCreatedToday = contract.createdTime > now - DAY_MS\n \n const { createdTime, closeTime, isResolved, outcomeType } = contract\n const commentScore = commentsToday\n@@ -366,17 +377,18 @@\n const closingSoonnnessComponent = closingSoonnness\n const commentScoreComponent = commentScore * 2\n const thisWeekScoreComponent = normalize(thisWeekScore, 1000)\n const rankedScore = isMarketRanked(contract) ? 0 : -1\n-\n+ const boostScore = isBoosted ? 0.25 : 0\n const computedRawMarketImportance =\n volume24HoursComponent +\n traderHourComponent +\n todayScoreComponent +\n closingSoonnnessComponent +\n commentScoreComponent +\n thisWeekScoreComponent +\n- rankedScore\n+ rankedScore +\n+ boostScore\n \n const newnessComponent = newness\n const rawPollImportance =\n 2 * normalize(traderHour, 20) +\n@@ -415,8 +427,9 @@\n closingSoonnnessComponent,\n commentScore: commentScoreComponent,\n thisWeekScoreComponent,\n rankedScore,\n+ boostScore,\n // Freshness components\n freshVolume24h,\n freshTodayScore,\n freshLastUpdated,\n@@ -431,9 +444,8 @@\n \n return {\n todayScore,\n thisWeekScore,\n- popularityScore: popularityScore >= 1 ? popularityScore : 0,\n freshnessScore,\n dailyScore,\n importanceScore,\n logOddsChange,\n"
|
|
878
|
-
},
|
|
879
|
-
{
|
|
880
|
-
"path": "backend/shared/src/supabase/contracts.ts",
|
|
881
|
-
"status": "modified",
|
|
882
|
-
"diff": "Index: backend/shared/src/supabase/contracts.ts\n===================================================================\n--- backend/shared/src/supabase/contracts.ts\t3db5061 (parent)\n+++ backend/shared/src/supabase/contracts.ts\tcd6dd8c (commit)\n@@ -1,15 +1,15 @@\n import { SupabaseDirectClient } from 'shared/supabase/init'\n-import { Contract } from 'common/contract'\n+import { Contract, nativeContractColumnsArray } from 'common/contract'\n import { isAdminId } from 'common/envs/constants'\n import { convertContract } from 'common/supabase/contracts'\n import { generateEmbeddings } from 'shared/helpers/openai-utils'\n import { APIError } from 'common/api/utils'\n import { broadcastUpdatedContract } from 'shared/websockets/helpers'\n-import { updateData, DataUpdate } from './utils'\n-import { mapValues, uniq } from 'lodash'\n+import { updateData, DataUpdate, update } from './utils'\n+import { camelCase, mapValues, uniq } from 'lodash'\n import { contractColumnsToSelect, log } from 'shared/utils'\n-\n+import { Tables } from 'common/supabase/utils'\n // used for API to allow slug as param\n export const getContractIdFromSlug = async (\n pg: SupabaseDirectClient,\n slug?: string\n@@ -191,10 +191,14 @@\n \n export const updateContract = async (\n pg: SupabaseDirectClient,\n contractId: string,\n+ // TODO: can't figure out how to exclude native columns from DataUpdate\n update: DataUpdate<'contracts'>\n ) => {\n+ if (Object.keys(update).some((k) => nativeContractColumnsArray.includes(k))) {\n+ throw new APIError(500, 'Cannot update native columns via data update')\n+ }\n const fullUpdate = { ...update, id: contractId }\n const newContract = convertContract(\n await updateData(pg, 'contracts', 'id', fullUpdate)\n )\n@@ -205,4 +209,21 @@\n ) as any\n broadcastUpdatedContract(newContract.visibility, updatedValues)\n return newContract\n }\n+\n+export const updateContractNativeColumns = async (\n+ pg: SupabaseDirectClient,\n+ contractId: string,\n+ updated: Tables['contracts']['Update']\n+) => {\n+ const fullUpdate = { ...updated, id: contractId }\n+ const newContract = convertContract(\n+ await update(pg, 'contracts', 'id', fullUpdate)\n+ )\n+ log('updated contract native columns', updated)\n+ const updatedValues = Object.fromEntries(\n+ Object.entries(fullUpdate).map(([k, v]) => [camelCase(k), v])\n+ ) as Partial<Contract> & { id: string }\n+ broadcastUpdatedContract(newContract.visibility, updatedValues)\n+ return newContract\n+}\n"
|
|
883
|
-
},
|
|
884
|
-
{
|
|
885
|
-
"path": "backend/shared/src/utils.ts",
|
|
886
|
-
"status": "modified",
|
|
887
|
-
"diff": "Index: backend/shared/src/utils.ts\n===================================================================\n--- backend/shared/src/utils.ts\t3db5061 (parent)\n+++ backend/shared/src/utils.ts\tcd6dd8c (commit)\n@@ -1,7 +1,12 @@\n import { generateJSON } from '@tiptap/html'\n import { APIError, getCloudRunServiceUrl } from 'common/api/utils'\n-import { Contract, contractPath, MarketContract } from 'common/contract'\n+import {\n+ Contract,\n+ nativeContractColumnsArray,\n+ contractPath,\n+ MarketContract,\n+} from 'common/contract'\n import { PrivateUser } from 'common/user'\n import { extensions } from 'common/util/parse'\n import * as admin from 'firebase-admin'\n import { first, uniq } from 'lodash'\n@@ -111,11 +116,11 @@\n } else {\n return admin.app().options.projectId === 'mantic-markets'\n }\n }\n-export const contractColumnsToSelect = `data,importance_score,freshness_score,conversion_score,view_count,token`\n-export const prefixedContractColumnsToSelect = contractColumnsToSelect\n- .split(',')\n+\n+export const contractColumnsToSelect = nativeContractColumnsArray.join(',')\n+export const prefixedContractColumnsToSelect = nativeContractColumnsArray\n .map((col) => `c.${col}`)\n .join(',')\n \n export const getContract = async (\n"
|
|
888
|
-
},
|
|
889
|
-
{
|
|
890
|
-
"path": "backend/supabase/contract_boosts.sql",
|
|
891
|
-
"status": "added",
|
|
892
|
-
"diff": "Index: backend/supabase/contract_boosts.sql\n===================================================================\n--- backend/supabase/contract_boosts.sql\t3db5061 (parent)\n+++ backend/supabase/contract_boosts.sql\tcd6dd8c (commit)\n@@ -0,0 +1,16 @@\n+create table if not exists\n+ contract_boosts (\n+ id bigint generated always as identity primary key,\n+ contract_id text not null references contracts (id),\n+ user_id text not null references users (id),\n+ start_time timestamptz not null,\n+ end_time timestamptz not null\n+ );\n+\n+-- Indexes\n+create index if not exists contract_boosts_contract_id_idx on contract_boosts (contract_id, start_time, end_time);\n+\n+create index if not exists contract_boosts_user_id_idx on contract_boosts (user_id);\n+\n+-- Row Level Security\n+alter table contract_boosts enable row level security;\n"
|
|
893
|
-
},
|
|
894
|
-
{
|
|
895
|
-
"path": "backend/supabase/contracts.sql",
|
|
896
|
-
"status": "modified",
|
|
897
|
-
"diff": "Index: backend/supabase/contracts.sql\n===================================================================\n--- backend/supabase/contracts.sql\t3db5061 (parent)\n+++ backend/supabase/contracts.sql\tcd6dd8c (commit)\n@@ -48,9 +48,10 @@\n )\n ),\n unique_bettor_count bigint default 0 not null,\n view_count bigint default 0 not null,\n- visibility text\n+ visibility text,\n+ boosted boolean default false not null\n );\n \n -- Triggers\n create trigger contract_populate before insert\n@@ -90,9 +91,8 @@\n end;\n new.resolution_probability := ((new.data) ->> 'resolutionProbability')::numeric;\n new.resolution := (new.data) ->> 'resolution';\n new.is_spice_payout := coalesce(((new.data) ->> 'isSpicePayout')::boolean, false);\n- new.popularity_score := coalesce(((new.data) ->> 'popularityScore')::numeric, 0);\n new.deleted := coalesce(((new.data) ->> 'deleted')::boolean, false);\n new.group_slugs := case\n when new.data ? 'groupSlugs' then jsonb_array_to_text_array((new.data) -> 'groupSlugs')\n else null\n"
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
"path": "common/src/api/schema.ts",
|
|
901
|
-
"status": "modified",
|
|
902
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\t3db5061 (parent)\n+++ common/src/api/schema.ts\tcd6dd8c (commit)\n@@ -2159,8 +2159,20 @@\n authed: false,\n props: z.object({ contractId: z.string(), optionId: z.string() }),\n returns: [] as DisplayUser[],\n },\n+ 'purchase-contract-boost': {\n+ method: 'POST',\n+ visibility: 'public',\n+ authed: true,\n+ props: z\n+ .object({\n+ contractId: z.string(),\n+ startTime: z.number().positive().finite().safe(),\n+ })\n+ .strict(),\n+ returns: {} as { success: boolean },\n+ },\n } as const)\n \n export type APIPath = keyof typeof API\n export type APISchema<N extends APIPath> = (typeof API)[N]\n"
|
|
903
|
-
},
|
|
904
|
-
{
|
|
905
|
-
"path": "common/src/contract.ts",
|
|
906
|
-
"status": "modified",
|
|
907
|
-
"diff": "Index: common/src/contract.ts\n===================================================================\n--- common/src/contract.ts\t3db5061 (parent)\n+++ common/src/contract.ts\tcd6dd8c (commit)\n@@ -96,8 +96,9 @@\n \n token: ContractToken\n siblingContractId?: string\n \n+ /** @deprecated - no longer used */\n takerAPIOrdersDisabled?: boolean\n \n // Manifold.love\n loverUserId1?: string // The user id's of the pair of lovers referenced in the question.\n@@ -120,8 +121,10 @@\n /** @deprecated - not deprecated, only updated in native column though*/\n conversionScore: number\n /** @deprecated - not deprecated, only updated in native column though*/\n viewCount: number\n+ /** @deprecated - not deprecated, only updated in native column though*/\n+ boosted: boolean\n /** @deprecated - not up-to-date */\n likedByUserCount?: number\n } & T\n \n@@ -536,4 +539,16 @@\n reasoning?: string\n addAnswersMode?: add_answers_mode\n promptVersion: number\n }\n+\n+export const nativeContractColumnsArray = [\n+ 'data',\n+ 'importance_score',\n+ 'freshness_score',\n+ 'conversion_score',\n+ 'view_count',\n+ 'token',\n+ 'boosted',\n+ 'daily_score',\n+ 'popularity_score',\n+]\n"
|
|
908
|
-
},
|
|
909
|
-
{
|
|
910
|
-
"path": "common/src/economy.ts",
|
|
911
|
-
"status": "modified",
|
|
912
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\t3db5061 (parent)\n+++ common/src/economy.ts\tcd6dd8c (commit)\n@@ -153,4 +153,5 @@\n \n export const SWEEPS_MIN_BET = 1\n export const MANA_MIN_BET = 1\n export const PROFIT_FEE_FRACTION = 0.1\n+export const BOOST_COST_MANA = 10000\n"
|
|
913
|
-
},
|
|
914
|
-
{
|
|
915
|
-
"path": "common/src/new-contract.ts",
|
|
916
|
-
"status": "modified",
|
|
917
|
-
"diff": "Index: common/src/new-contract.ts\n===================================================================\n--- common/src/new-contract.ts\t3db5061 (parent)\n+++ common/src/new-contract.ts\tcd6dd8c (commit)\n@@ -169,8 +169,9 @@\n sportsLeague,\n \n takerAPIOrdersDisabled,\n siblingContractId,\n+ boosted: false,\n })\n if (visibility === 'unlisted') {\n contract.unlistedById = creator.id\n }\n"
|
|
918
|
-
},
|
|
919
|
-
{
|
|
920
|
-
"path": "common/src/supabase/contracts.ts",
|
|
921
|
-
"status": "modified",
|
|
922
|
-
"diff": "Index: common/src/supabase/contracts.ts\n===================================================================\n--- common/src/supabase/contracts.ts\t3db5061 (parent)\n+++ common/src/supabase/contracts.ts\tcd6dd8c (commit)\n@@ -135,8 +135,9 @@\n view_count?: number | null\n conversion_score?: number | null\n freshness_score?: number | null\n daily_score?: number | null\n+ boosted?: boolean | null\n token?: string\n }) =>\n removeUndefinedProps({\n ...(c.data as T),\n@@ -146,8 +147,9 @@\n freshnessScore: c.freshness_score,\n viewCount: Number(c.view_count),\n dailyScore: c.daily_score,\n token: c.token,\n+ boosted: c.boosted ?? false,\n } as T)\n \n export const followContract = async (\n db: SupabaseClient,\n"
|
|
923
|
-
},
|
|
924
|
-
{
|
|
925
|
-
"path": "common/src/supabase/schema.ts",
|
|
926
|
-
"status": "modified",
|
|
927
|
-
"diff": "Index: common/src/supabase/schema.ts\n===================================================================\n--- common/src/supabase/schema.ts\t3db5061 (parent)\n+++ common/src/supabase/schema.ts\tcd6dd8c (commit)\n@@ -263,8 +263,54 @@\n user_id?: string\n }\n Relationships: []\n }\n+ contract_boosts: {\n+ Row: {\n+ contract_id: string\n+ end_time: string\n+ id: number\n+ start_time: string\n+ user_id: string\n+ }\n+ Insert: {\n+ contract_id: string\n+ end_time: string\n+ id?: never\n+ start_time: string\n+ user_id: string\n+ }\n+ Update: {\n+ contract_id?: string\n+ end_time?: string\n+ id?: never\n+ start_time?: string\n+ user_id?: string\n+ }\n+ Relationships: [\n+ {\n+ foreignKeyName: 'contract_boosts_contract_id_fkey'\n+ columns: ['contract_id']\n+ isOneToOne: false\n+ referencedRelation: 'contracts'\n+ referencedColumns: ['id']\n+ },\n+ {\n+ foreignKeyName: 'contract_boosts_user_id_fkey'\n+ columns: ['user_id']\n+ isOneToOne: false\n+ referencedRelation: 'user_referrals_profit'\n+ referencedColumns: ['id']\n+ },\n+ {\n+ foreignKeyName: 'contract_boosts_user_id_fkey'\n+ columns: ['user_id']\n+ isOneToOne: false\n+ referencedRelation: 'users'\n+ referencedColumns: ['id']\n+ }\n+ ]\n+ }\n contract_comment_edits: {\n Row: {\n comment_id: string\n contract_id: string\n@@ -443,8 +489,9 @@\n token: string\n unique_bettor_count: number\n view_count: number\n visibility: string | null\n+ boosted: boolean | null\n }\n Insert: {\n close_time?: string | null\n conversion_score?: number\n@@ -476,8 +523,9 @@\n token?: string\n unique_bettor_count?: number\n view_count?: number\n visibility?: string | null\n+ boosted?: boolean | null\n }\n Update: {\n close_time?: string | null\n conversion_score?: number\n@@ -509,8 +557,9 @@\n token?: string\n unique_bettor_count?: number\n view_count?: number\n visibility?: string | null\n+ boosted?: boolean | null\n }\n Relationships: []\n }\n creator_portfolio_history: {\n"
|
|
928
|
-
},
|
|
929
|
-
{
|
|
930
|
-
"path": "common/src/txn.ts",
|
|
931
|
-
"status": "modified",
|
|
932
|
-
"diff": "Index: common/src/txn.ts\n===================================================================\n--- common/src/txn.ts\t3db5061 (parent)\n+++ common/src/txn.ts\tcd6dd8c (commit)\n@@ -59,8 +59,9 @@\n | CashOutPending\n | KycBonus\n | ProfitFee\n | UndoResolutionFee\n+ | ContractBoostPurchase\n \n export type AnyTxnCategory = AnyTxnType['category']\n \n export type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK' | 'AD'\n@@ -588,8 +589,19 @@\n updateType: string\n }\n }\n \n+type ContractBoostPurchase = {\n+ category: 'CONTRACT_BOOST_PURCHASE'\n+ fromType: 'USER'\n+ toType: 'BANK'\n+ token: 'M$'\n+ data: {\n+ contractId: string\n+ boostId: string\n+ }\n+}\n+\n export type AddSubsidyTxn = Txn & AddSubsidy\n export type RemoveSubsidyTxn = Txn & RemoveSubsidy\n export type DonationTxn = Txn & Donation\n export type TipTxn = Txn & Tip\n@@ -643,4 +655,5 @@\n export type CashOutPendingTxn = Txn & CashOutPending\n export type ProfitFeeTxn = Txn & ProfitFee\n export type UndoResolutionFeeTxn = Txn & UndoResolutionFee\n export type AdminRewardTxn = Txn & AdminReward\n+export type ContractBoostPurchaseTxn = Txn & ContractBoostPurchase\n"
|
|
933
|
-
},
|
|
934
|
-
{
|
|
935
|
-
"path": "web/components/contract/add-boost-button.tsx",
|
|
936
|
-
"status": "added",
|
|
937
|
-
"diff": "Index: web/components/contract/add-boost-button.tsx\n===================================================================\n--- web/components/contract/add-boost-button.tsx\t3db5061 (parent)\n+++ web/components/contract/add-boost-button.tsx\tcd6dd8c (commit)\n@@ -0,0 +1,157 @@\n+import { useState } from 'react'\n+import { Button } from '../buttons/button'\n+import { Modal } from '../layout/modal'\n+import { Col } from '../layout/col'\n+import { Row } from '../layout/row'\n+import { Contract } from 'common/contract'\n+import { useUser } from 'web/hooks/use-user'\n+import { api } from 'web/lib/api/api'\n+import { TokenNumber } from '../widgets/token-number'\n+import { AddFundsModal } from '../add-funds-modal'\n+import toast from 'react-hot-toast'\n+import { BsRocketTakeoff } from 'react-icons/bs'\n+import { BOOST_COST_MANA } from 'common/economy'\n+import dayjs from 'dayjs'\n+import { Input } from '../widgets/input'\n+import { HOUR_MS } from 'common/util/time'\n+\n+export function AddBoostButton(props: {\n+ contract: Contract\n+ className?: string\n+}) {\n+ const { contract, className } = props\n+ const [open, setOpen] = useState(false)\n+ const user = useUser()\n+\n+ if (!user) return null\n+\n+ const disabled =\n+ contract.isResolved ||\n+ (contract.closeTime ?? Infinity) < Date.now() ||\n+ contract.visibility !== 'public'\n+\n+ if (disabled) return null\n+\n+ return (\n+ <>\n+ <Button\n+ onClick={() => setOpen(true)}\n+ color=\"gradient-pink\"\n+ size=\"sm\"\n+ className={className}\n+ data-boost-button\n+ disabled={contract.boosted}\n+ >\n+ <BsRocketTakeoff className=\"mr-1 h-5 w-5\" />\n+ {contract.boosted ? 'Market boosted' : 'Boost market'}\n+ </Button>\n+\n+ <BoostPurchaseModal open={open} setOpen={setOpen} contract={contract} />\n+ </>\n+ )\n+}\n+\n+function BoostPurchaseModal(props: {\n+ open: boolean\n+ setOpen: (open: boolean) => void\n+ contract: Contract\n+}) {\n+ const { open, setOpen, contract } = props\n+ const [loading, setLoading] = useState(false)\n+ const [fundsModalOpen, setFundsModalOpen] = useState(false)\n+ const now = Date.now()\n+ const [startTime, setStartTime] = useState(now)\n+ const user = useUser()\n+\n+ if (!user) return null\n+\n+ const notEnoughFunds = (user.balance ?? 0) < BOOST_COST_MANA\n+\n+ const purchaseBoost = async () => {\n+ setLoading(true)\n+ try {\n+ await api('purchase-contract-boost', {\n+ contractId: contract.id,\n+ startTime,\n+ })\n+ toast.success(\n+ 'Market boosted! It will be featured on the homepage for 24 hours.'\n+ )\n+ setOpen(false)\n+ } catch (e) {\n+ console.error(e)\n+ toast.error(e instanceof Error ? e.message : 'Error purchasing boost')\n+ }\n+ setLoading(false)\n+ }\n+\n+ return (\n+ <>\n+ <Modal open={open} setOpen={setOpen} size=\"sm\">\n+ <Col className=\"bg-canvas-0 gap-4 rounded-lg p-6\">\n+ <Row className=\"items-center gap-2 text-xl font-semibold\">\n+ <BsRocketTakeoff className=\"h-6 w-6\" />\n+ Boost this market\n+ </Row>\n+\n+ <div className=\"text-ink-600\">\n+ Boost this market's visibility on the homepage{' '}\n+ {Math.abs(startTime - now) < HOUR_MS\n+ ? 'for the next 24 hours'\n+ : `from ${dayjs(startTime).format('MMM D')} to ${dayjs(startTime)\n+ .add(24, 'hours')\n+ .format('MMM D')}`}\n+ </div>\n+\n+ <Row className=\"items-center gap-2\">\n+ <Button\n+ color=\"indigo\"\n+ onClick={purchaseBoost}\n+ loading={loading}\n+ disabled={notEnoughFunds}\n+ className=\"w-full\"\n+ >\n+ Purchase boost for{' '}\n+ <TokenNumber className=\"mx-1\" amount={BOOST_COST_MANA} />\n+ </Button>\n+ </Row>\n+ <Row className=\"items-center gap-2\">\n+ <div className=\"text-ink-600\">Start time:</div>\n+ <Input\n+ type={'date'}\n+ className=\"dark:date-range-input-white\"\n+ onClick={(e) => e.stopPropagation()}\n+ onChange={(e) => {\n+ const start = dayjs(e.target.value).startOf('day').valueOf()\n+ if (start < Date.now()) {\n+ setStartTime(Date.now())\n+ } else {\n+ setStartTime(start)\n+ }\n+ }}\n+ min={dayjs().format('YYYY-MM-DD')}\n+ max=\"3000-12-31\"\n+ disabled={loading}\n+ value={dayjs(startTime).format('YYYY-MM-DD')}\n+ />\n+ </Row>\n+\n+ {notEnoughFunds && (\n+ <div className=\"text-ink-600 flex items-center gap-2 text-sm\">\n+ <span className=\"text-error\">Insufficient balance</span>\n+ <Button\n+ size=\"xs\"\n+ color=\"gradient-pink\"\n+ onClick={() => setFundsModalOpen(true)}\n+ >\n+ Get mana\n+ </Button>\n+ </div>\n+ )}\n+ </Col>\n+ </Modal>\n+\n+ <AddFundsModal open={fundsModalOpen} setOpen={setFundsModalOpen} />\n+ </>\n+ )\n+}\n"
|
|
938
|
-
},
|
|
939
|
-
{
|
|
940
|
-
"path": "web/components/contract/boost-column.tsx",
|
|
941
|
-
"status": "added",
|
|
942
|
-
"diff": "Index: web/components/contract/boost-column.tsx\n===================================================================\n--- web/components/contract/boost-column.tsx\t3db5061 (parent)\n+++ web/components/contract/boost-column.tsx\tcd6dd8c (commit)\n@@ -0,0 +1,24 @@\n+import { Tooltip } from '../widgets/tooltip'\n+import clsx from 'clsx'\n+import { PlusTier } from 'web/public/custom-components/tiers'\n+import { Placement } from '@floating-ui/react'\n+export function BoostedTooltip(props: {\n+ boosted: boolean\n+ placement?: Placement\n+ className?: string\n+ iconClassName?: string\n+}) {\n+ const { boosted, placement = 'top', className, iconClassName } = props\n+\n+ return (\n+ <Tooltip\n+ text={boosted ? `Boosted market` : ''}\n+ placement={placement}\n+ noTap\n+ className={clsx('flex flex-row items-center gap-0.5', className)}\n+ tooltipClassName=\"z-40\"\n+ >\n+ {boosted ? <PlusTier className={iconClassName} /> : null}\n+ </Tooltip>\n+ )\n+}\n"
|
|
943
|
-
},
|
|
944
|
-
{
|
|
945
|
-
"path": "web/components/contract/contract-page.tsx",
|
|
946
|
-
"status": "modified",
|
|
947
|
-
"diff": "Index: web/components/contract/contract-page.tsx\n===================================================================\n--- web/components/contract/contract-page.tsx\t3db5061 (parent)\n+++ web/components/contract/contract-page.tsx\tcd6dd8c (commit)\n@@ -32,9 +32,8 @@\n import {\n ContractOverview,\n getShouldHideGraph,\n } from 'web/components/contract/contract-overview'\n-import ContractSharePanel from 'web/components/contract/contract-share-panel'\n import { ContractTabs } from 'web/components/contract/contract-tabs'\n import { VisibilityIcon } from 'web/components/contract/contracts-table'\n import { DangerZone } from 'web/components/contract/danger-zone'\n import { EditableQuestionTitle } from 'web/components/contract/editable-question-title'\n@@ -76,8 +75,9 @@\n import { useRouter } from 'next/router'\n import { precacheAnswers } from 'web/hooks/use-answers'\n import { useIsPageVisible } from 'web/hooks/use-page-visible'\n import { api } from 'web/lib/api/api'\n+import ContractSharePanel from './contract-share-panel'\n \n export function ContractPageContent(props: ContractParams) {\n const {\n comments,\n@@ -416,8 +416,9 @@\n chartAnnotations={chartAnnotations}\n hideGraph={hideGraph}\n setHideGraph={setHideGraph}\n />\n+\n {!tradingAllowed(liveContract) && (\n <UserBetsSummary\n className=\"border-ink-200 !mb-2 \"\n contract={liveContract}\n@@ -473,14 +474,30 @@\n setShowReview={setShowReview}\n userHasBet={!!myContractMetrics}\n hasReviewed={!!userHasReviewed}\n />\n+ {!!user && isCreator && (\n+ <ContractSharePanel\n+ isClosed={isClosed}\n+ isCreator={isCreator}\n+ showResolver={showResolver}\n+ contract={liveContract}\n+ />\n+ )}\n <ContractDescription\n contractId={props.contract.id}\n creatorId={props.contract.creatorId}\n isSweeps={isCashContract}\n description={description}\n />\n+ {!!user && !isCreator && (\n+ <ContractSharePanel\n+ isClosed={isClosed}\n+ isCreator={isCreator}\n+ showResolver={showResolver}\n+ contract={liveContract}\n+ />\n+ )}\n <Row className=\"items-center gap-2\">\n <MarketTopics\n contract={props.contract}\n dashboards={dashboards}\n@@ -490,17 +507,8 @@\n </Row>\n \n <Row className=\"my-2 flex-wrap items-center justify-between gap-y-2\"></Row>\n {!user && <SidebarSignUpButton className=\"mb-4 flex md:hidden\" />}\n- {!!user && (\n- <ContractSharePanel\n- isClosed={isClosed}\n- isCreator={isCreator}\n- showResolver={showResolver}\n- // TODO: upgrade tier\n- contract={liveContract}\n- />\n- )}\n \n {isResolved && resolution !== 'CANCEL' && (\n <>\n <ContractLeaderboard\n"
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
"path": "web/components/contract/contract-summary-stats.tsx",
|
|
951
|
-
"status": "modified",
|
|
952
|
-
"diff": "Index: web/components/contract/contract-summary-stats.tsx\n===================================================================\n--- web/components/contract/contract-summary-stats.tsx\t3db5061 (parent)\n+++ web/components/contract/contract-summary-stats.tsx\tcd6dd8c (commit)\n@@ -3,9 +3,9 @@\n import { formatWithToken, shortFormatNumber } from 'common/util/format'\n import { Row } from 'web/components/layout/row'\n import { isBlocked, usePrivateUser, useUser } from 'web/hooks/use-user'\n import { MoneyDisplay } from '../bet/money-display'\n-import { TierTooltip } from '../tiers/tier-tooltip'\n+import { LiquidityTooltip } from '../tiers/liquidity-tooltip'\n import { Tooltip } from '../widgets/tooltip'\n import { BountyLeft } from './bountied-question'\n import { CloseOrResolveTime } from './contract-details'\n import { ReactButton } from './react-button'\n@@ -25,9 +25,9 @@\n financeContract: contract,\n editable,\n isCashContract,\n } = props\n- const { outcomeType, marketTier } = contract\n+ const { outcomeType } = contract\n const privateUser = usePrivateUser()\n const user = useUser()\n \n return (\n@@ -39,9 +39,8 @@\n inEmbed={true}\n />\n ) : (\n <Row className=\"ml-auto gap-4\">\n- {marketTier && <TierTooltip tier={marketTier} contract={contract} />}\n {!isBlocked(privateUser, contract.creatorId) && (\n <ReactButton\n user={user}\n size={'2xs'}\n@@ -61,9 +60,9 @@\n >\n <UserIcon className=\"text-ink-500 h-4 w-4\" />\n <div>{shortFormatNumber(contract.uniqueBettorCount ?? 0)}</div>\n </Tooltip>\n-\n+ <LiquidityTooltip contract={contract} iconClassName=\"text-ink-500\" />\n {!!contract.volume && (\n <Tooltip\n text={`Trading volume: ${formatWithToken({\n amount: contract.volume,\n"
|
|
953
|
-
},
|
|
954
|
-
{
|
|
955
|
-
"path": "web/components/contract/contract-table-col-formats.tsx",
|
|
956
|
-
"status": "modified",
|
|
957
|
-
"diff": "Index: web/components/contract/contract-table-col-formats.tsx\n===================================================================\n--- web/components/contract/contract-table-col-formats.tsx\t3db5061 (parent)\n+++ web/components/contract/contract-table-col-formats.tsx\tcd6dd8c (commit)\n@@ -2,13 +2,13 @@\n import { Contract } from 'common/contract'\n import { useNumContractComments } from 'web/hooks/use-comments'\n import { shortenNumber } from 'common/util/formatNumber'\n import { Row } from '../layout/row'\n-import { TierTooltip } from '../tiers/tier-tooltip'\n import { Action } from './contract-table-action'\n import { ContractStatusLabel } from './contracts-table'\n import { useHasContractMetrics } from 'web/hooks/use-saved-contract-metrics'\n import { Tooltip } from '../widgets/tooltip'\n+import { BoostedTooltip } from './boost-column'\n \n export type ColumnFormat = {\n header: string\n content: (props: { contract: Contract }) => JSX.Element\n@@ -68,19 +68,36 @@\n },\n width: 'w-[80px]',\n }\n \n-export const tierColumn = {\n- header: 'Tier',\n+// export const tierColumn = {\n+// header: 'Tier',\n+// content: (props: { contract: Contract }) => {\n+// const { contract } = props\n+// const marketTier = contract.marketTier\n+// return (\n+// <TierTooltip\n+// placement={'top'}\n+// tier={marketTier!}\n+// contract={contract}\n+// noTitle\n+// className=\"relative mr-0.5 inline-flex h-[1em] w-[1.1em] items-baseline\"\n+// iconClassName=\"absolute inset-0 top-[0.2em]\"\n+// />\n+// )\n+// },\n+// width: 'w-8',\n+// }\n+\n+export const boostedColumn = {\n+ header: 'Boosted',\n content: (props: { contract: Contract }) => {\n const { contract } = props\n- const marketTier = contract.marketTier\n return (\n- <TierTooltip\n+ <BoostedTooltip\n+ boosted={contract.boosted}\n placement={'top'}\n- tier={marketTier!}\n- contract={contract}\n- noTitle\n+ // noTitle\n className=\"relative mr-0.5 inline-flex h-[1em] w-[1.1em] items-baseline\"\n iconClassName=\"absolute inset-0 top-[0.2em]\"\n />\n )\n"
|
|
958
|
-
},
|
|
959
|
-
{
|
|
960
|
-
"path": "web/components/contract/contracts-table.tsx",
|
|
961
|
-
"status": "modified",
|
|
962
|
-
"diff": "Index: web/components/contract/contracts-table.tsx\n===================================================================\n--- web/components/contract/contracts-table.tsx\t3db5061 (parent)\n+++ web/components/contract/contracts-table.tsx\tcd6dd8c (commit)\n@@ -22,9 +22,9 @@\n actionColumn,\n probColumn,\n traderColumn,\n ColumnFormat,\n- tierColumn,\n+ boostedColumn,\n } from './contract-table-col-formats'\n import { UserHovercard } from '../user/user-hovercard'\n import { getFormattedExpectedValue } from 'common/multi-numeric'\n import { removeEmojis } from 'common/util/string'\n@@ -43,9 +43,9 @@\n const {\n contracts,\n onContractClick,\n highlightContractIds,\n- columns = [traderColumn, tierColumn, probColumn, actionColumn],\n+ columns = [boostedColumn, traderColumn, probColumn, actionColumn],\n hideAvatar,\n } = props\n \n const user = useUser()\n"
|
|
963
|
-
},
|
|
964
|
-
{
|
|
965
|
-
"path": "web/components/contract/creator-share-panel.tsx",
|
|
966
|
-
"status": "modified",
|
|
967
|
-
"diff": "Index: web/components/contract/creator-share-panel.tsx\n===================================================================\n--- web/components/contract/creator-share-panel.tsx\t3db5061 (parent)\n+++ web/components/contract/creator-share-panel.tsx\tcd6dd8c (commit)\n@@ -10,12 +10,12 @@\n import { TweetButton } from '../buttons/tweet-button'\n import { Row } from '../layout/row'\n import { GradientContainer } from '../widgets/gradient-container'\n import { AddBountyButton, CancelBountyButton } from './bountied-question'\n-import { UpgradeTierButton } from './upgrade-tier-button'\n import { useNativeInfo } from 'web/components/native-message-provider'\n import { formatMoney } from 'common/util/format'\n import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/economy'\n+import { AddBoostButton } from './add-boost-button'\n \n export function CreatorSharePanel(props: { contract: Contract }) {\n const { contract } = props\n return (\n@@ -23,12 +23,9 @@\n <div className=\"mb-2 flex flex-wrap gap-2\">\n {contract.outcomeType == 'BOUNTIED_QUESTION' && (\n <AddBountyButton contract={contract} />\n )}\n- {(contract.mechanism == 'cpmm-1' ||\n- contract.mechanism == 'cpmm-multi-1') && (\n- <UpgradeTierButton contract={contract} />\n- )}\n+ <AddBoostButton contract={contract} />\n <ShareLinkButton contract={contract} />\n <TweetButton\n tweetText={'I created a question. ' + getShareUrl(contract)}\n className=\"hidden sm:flex\"\n@@ -40,9 +37,9 @@\n \n {contract.outcomeType !== 'POLL' &&\n contract.outcomeType !== 'BOUNTIED_QUESTION' && (\n <div className=\"text-ink-500 text-base\">\n- Earn {formatMoney(UNIQUE_BETTOR_BONUS_AMOUNT)} for each trader.\n+ Earn {formatMoney(UNIQUE_BETTOR_BONUS_AMOUNT)} for each new trader.\n </div>\n )}\n </GradientContainer>\n )\n@@ -55,8 +52,9 @@\n <Row className=\"my-4 flex-wrap gap-4\">\n {contract.outcomeType == 'BOUNTIED_QUESTION' && (\n <AddBountyButton contract={contract} />\n )}\n+ <AddBoostButton contract={contract} />\n <ShareLinkButton contract={contract} />\n <TweetButton\n tweetText={getShareUrl(contract)}\n className=\"hidden sm:flex\"\n"
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
"path": "web/components/contract/feed-contract-card.tsx",
|
|
971
|
-
"status": "modified",
|
|
972
|
-
"diff": "Index: web/components/contract/feed-contract-card.tsx\n===================================================================\n--- web/components/contract/feed-contract-card.tsx\t3db5061 (parent)\n+++ web/components/contract/feed-contract-card.tsx\tcd6dd8c (commit)\n@@ -39,9 +39,9 @@\n import FeedContractCardDescription from '../feed/feed-contract-card-description'\n import { Col } from '../layout/col'\n import { Row } from '../layout/row'\n import { PollPanel } from '../poll/poll-panel'\n-import { TierTooltip } from '../tiers/tier-tooltip'\n+import { LiquidityTooltip } from '../tiers/liquidity-tooltip'\n import { UserHovercard } from '../user/user-hovercard'\n import { ClickFrame } from '../widgets/click-frame'\n import { ReactButton } from './react-button'\n import { TradesButton } from './trades-button'\n@@ -89,9 +89,8 @@\n creatorUsername,\n creatorAvatarUrl,\n outcomeType,\n mechanism,\n- marketTier,\n } = contract\n \n const isBinaryMc = isBinaryMulti(contract)\n const isBinaryCpmm = outcomeType === 'BINARY' && mechanism === 'cpmm-1'\n@@ -211,18 +210,16 @@\n >\n <SweepiesCoin className=\"-mt-0.5\" /> {capitalize(SWEEPIES_NAME)}{' '}\n </span>\n )}\n- {marketTier ? (\n- <TierTooltip tier={marketTier} contract={contract} />\n- ) : feedReason ? (\n+ {feedReason ? (\n <CardReason\n reason={feedReason as any}\n probChange={probChange}\n since={startTime}\n />\n ) : (\n- <></>\n+ <LiquidityTooltip contract={contract} />\n )}\n {hide && (\n <FeedDropdown\n contract={contract}\n"
|
|
973
|
-
},
|
|
974
|
-
{
|
|
975
|
-
"path": "web/components/contract/upgrade-tier-button.tsx",
|
|
976
|
-
"status": "modified",
|
|
977
|
-
"diff": "Index: web/components/contract/upgrade-tier-button.tsx\n===================================================================\n--- web/components/contract/upgrade-tier-button.tsx\t3db5061 (parent)\n+++ web/components/contract/upgrade-tier-button.tsx\tcd6dd8c (commit)\n@@ -14,9 +14,9 @@\n import { Title } from '../widgets/title'\n import { AddLiquidityControl } from './liquidity-modal'\n import { getAnte } from 'common/economy'\n import { TokenNumber } from '../widgets/token-number'\n-import { TierIcon, getPresentedTierName } from '../tiers/tier-tooltip'\n+import { TierIcon, getPresentedTierName } from '../tiers/liquidity-tooltip'\n import { api } from 'web/lib/api/api'\n import { useUser } from 'web/hooks/use-user'\n import { ENV_CONFIG } from 'common/envs/constants'\n import { AddFundsModal } from '../add-funds-modal'\n"
|
|
978
|
-
},
|
|
979
|
-
{
|
|
980
|
-
"path": "web/components/new-contract/cost-section.tsx",
|
|
981
|
-
"status": "modified",
|
|
982
|
-
"diff": "Index: web/components/new-contract/cost-section.tsx\n===================================================================\n--- web/components/new-contract/cost-section.tsx\t3db5061 (parent)\n+++ web/components/new-contract/cost-section.tsx\tcd6dd8c (commit)\n@@ -16,9 +16,9 @@\n PremiumTier,\n } from 'web/public/custom-components/tiers'\n import { TokenNumber } from '../widgets/token-number'\n import { getTieredCost, MarketTierType } from 'common/tier'\n-import { getPresentedTierName } from '../tiers/tier-tooltip'\n+import { getPresentedTierName } from '../tiers/liquidity-tooltip'\n import { ManaCoin } from 'web/public/custom-components/manaCoin'\n import { getContractTypeFromValue } from './create-contract-types'\n import { InfoTooltip } from '../widgets/info-tooltip'\n import { capitalize } from 'lodash'\n"
|
|
983
|
-
},
|
|
984
|
-
{
|
|
985
|
-
"path": "web/components/search.tsx",
|
|
986
|
-
"status": "modified",
|
|
987
|
-
"diff": "Index: web/components/search.tsx\n===================================================================\n--- web/components/search.tsx\t3db5061 (parent)\n+++ web/components/search.tsx\tcd6dd8c (commit)\n@@ -20,8 +20,9 @@\n import { api, searchGroups } from 'web/lib/api/api'\n import { searchUsers } from 'web/lib/supabase/users'\n import {\n actionColumn,\n+ boostedColumn,\n probColumn,\n traderColumn,\n } from './contract/contract-table-col-formats'\n import { ContractsTable, LoadingContractRow } from './contract/contracts-table'\n@@ -483,9 +484,9 @@\n contracts={contracts}\n onContractClick={onContractClick}\n highlightContractIds={highlightContractIds}\n columns={buildArray([\n- // tierColumn,\n+ boostedColumn,\n traderColumn,\n probColumn,\n !hideActions && actionColumn,\n ])}\n"
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
"path": "web/components/tiers/liquidity-tooltip.tsx",
|
|
991
|
-
"status": "renamed",
|
|
992
|
-
"oldPath": "web/components/tiers/tier-tooltip.tsx",
|
|
993
|
-
"diff": "===================================================================\n--- web/components/tiers/tier-tooltip.tsx\t3db5061 (parent)\n+++ web/components/tiers/liquidity-tooltip.tsx\tcd6dd8c (commit)\n@@ -1,67 +1,45 @@\n import { Placement } from '@floating-ui/react'\n import clsx from 'clsx'\n import { Contract } from 'common/contract'\n import { MarketTierType } from 'common/tier'\n-import { formatWithToken } from 'common/util/format'\n+import { formatWithToken, shortFormatNumber } from 'common/util/format'\n import { capitalize } from 'lodash'\n import {\n CrystalTier,\n PlayTier,\n PlusTier,\n PremiumTier,\n } from 'web/public/custom-components/tiers'\n import { Tooltip } from '../widgets/tooltip'\n+import { GiWaterDrop } from 'react-icons/gi'\n \n-export function TierTooltip(props: {\n- tier: MarketTierType\n+export function LiquidityTooltip(props: {\n contract: Contract\n className?: string\n- noTitle?: boolean\n placement?: Placement\n iconClassName?: string\n }) {\n- const {\n- tier,\n- contract,\n- className,\n- noTitle,\n- placement = 'bottom',\n- iconClassName,\n- } = props\n+ const { contract, className, placement = 'bottom', iconClassName } = props\n const { mechanism } = contract\n \n const isCashContract = contract.token === 'CASH'\n \n if (mechanism !== 'cpmm-multi-1' && mechanism !== 'cpmm-1') return <></>\n-\n+ const amount = contract.totalLiquidity\n return (\n <Tooltip\n text={`${formatWithToken({\n- amount: contract.totalLiquidity,\n+ amount,\n token: isCashContract ? 'CASH' : 'M$',\n })} in liquidity subsidies`}\n placement={placement}\n noTap\n className={clsx('flex flex-row items-center gap-0.5', className)}\n tooltipClassName=\"z-40\"\n >\n- <TierIcon tier={tier} className={iconClassName} />\n- {!noTitle && (\n- <div\n- className={clsx(\n- tier == 'plus'\n- ? 'font-semibold text-blue-600 dark:text-blue-500'\n- : tier == 'premium'\n- ? 'font-semibold text-purple-500 dark:text-purple-400'\n- : tier == 'crystal'\n- ? 'bg-gradient-to-r from-pink-700 to-pink-500 bg-clip-text font-semibold text-transparent dark:from-pink-400 dark:to-pink-300'\n- : ''\n- )}\n- >\n- {getPresentedTierName(tier)}\n- </div>\n- )}\n+ <GiWaterDrop className={iconClassName} />\n+ {shortFormatNumber(amount)}\n </Tooltip>\n )\n }\n \n"
|
|
994
|
-
}
|
|
995
|
-
]
|
|
996
|
-
},
|
|
997
|
-
{
|
|
998
|
-
"id": "indie-mc-resolution",
|
|
999
|
-
"sha": "0316bb81bfcf9ab9f3f92c3c49703f56d7f9ab3b",
|
|
1000
|
-
"parentSha": "7eb20fcc6dfcd742b51b53d33ad7d9639955b8cd",
|
|
1001
|
-
"spec": "Implement a new independent multiple-choice resolution flow and integrate it into the app.\n\nScope\n- Files to change:\n - web/components/answers/answer-resolve-panel.tsx\n - web/components/contract/contract-overview.tsx\n - web/components/contract/contract-table-action.tsx\n - web/components/widgets/amount-input.tsx\n\n1) Update answers/answer-resolve-panel.tsx\n- Imports\n - Remove MiniResolutionPanel from the imports in this file where it is no longer used in independent resolution flow.\n - Add imports:\n - YesNoCancelSelector from ../bet/yes-no-selector\n - resolution type from common/contract\n - usePersistentInMemoryState from client-common/hooks/use-persistent-in-memory-state\n- AnswersResolvePanel behavior for non-sum-to-one\n - In AnswersResolvePanel, when contract.shouldAnswersSumToOne is false, replace the existing message with a simple, non-blocking placeholder: `wrong resolution panel` styled with text-ink-500.\n- IndependentAnswersResolvePanel enhancements\n - Update component signature to accept a new prop: show: boolean. If show is false, the component must return null but retain in-memory state (this avoids janky behavior when clearing state after submission).\n - Manage the following persistent, in-memory state:\n - selectedResolutions: { [answerId: string]: resolution } (keyed by answer id)\n - completedResolutions: string[] (list of answer ids successfully resolved in this session)\n - resolutionProbs: { [answerId: string]: number | undefined } (probability for MKT resolutions; store as integer percent)\n - isShowingConfirmation: boolean (toggles showing the review/confirmation step)\n - isSubmitting: boolean (submission-in-progress state)\n - error: string | undefined (for submission or validation errors)\n - At the top of the panel (under ResolveHeader), display a status row showing count of currently selected answers for resolution and a Review & Submit button:\n - Copy: “X answer(s) selected for resolution” where X is selectedCount.\n - Button: Review & Submit; disabled when no answers are selected. Clicking it shows the confirmation screen.\n - Resolve list: For each answer (sorted by resolution and then by prob/index matching existing sorting), render:\n - If answer.resolution exists, render a read-only AnswerBar showing status (AnswerStatus) and no controls.\n - If unresolved:\n - Show the AnswerBar (status, creator avatar/label if applicable) and to the right display the currently selected resolution (YES/NO/CANCEL/MKT and, for MKT, the percentage) if chosen.\n - Below the bar, provide controls:\n - YesNoCancelSelector to set selected resolution for this answer. Selecting MKT should default the percentage to the current probability rounded to an integer percent if not already set.\n - If MKT is selected, show an AmountInput labeled “%” allowing the user to input an integer percent (store in resolutionProbs[answerId]). The input must allow clearing to undefined.\n - Show a small gray Remove button to clear any selected resolution for this answer.\n - Confirmation screen:\n - When isShowingConfirmation is true, replace the panel content with a review list of selected resolutions. Show for each:\n - Answer text\n - Resolution chosen (YES/NO/CANCEL/MKT), and if MKT then also the chosen percent (e.g., “MKT 42%”).\n - A per-answer status indicator: “✓ Completed” for completed ones, and a “Processing…” pulse indicator for the one currently waiting during submission.\n - Inline error area (text-scarlet-500) for validation or submission errors.\n - Buttons:\n - Back (gray) to return to the selection UI when not submitting and not fully completed.\n - Submit All Resolutions (indigo) to begin sequential submission when not submitting and not fully completed. If some resolutions already completed, label the button “Continue Resolving”.\n - While processing, show a disabled loading button labeled “Processing…”.\n - Once all have completed, show a green Done button that clears the stored resolution state and calls onClose.\n - Submission logic:\n - On submit, process selectedResolutions sequentially in stable order (Object.entries iteration is sufficient).\n - Skip any answerId already in completedResolutions.\n - If a selected outcome is MKT but resolutionProbs[answerId] is missing, set an error with a message identifying the missing probability by answer text and stop processing.\n - For each answer:\n - Call api('market/:contractId/resolve', { contractId: contract.id, outcome, answerId, probabilityInt: resolutionProbs[answerId] if MKT })\n - On success, append the answerId to completedResolutions.\n - On failure, set an error (use APIError.message if available; otherwise a generic error) and stop processing.\n - Always reset isSubmitting at the end.\n - Done action:\n - Clear selectedResolutions, completedResolutions, resolutionProbs, set isShowingConfirmation to false, and call onClose.\n\n2) Update contract/contract-overview.tsx\n- For ChoiceOverview (MULTIPLE_CHOICE):\n - When contract.mechanism === 'cpmm-multi-1' and shouldAnswersSumToOne is false, always mount the IndependentAnswersResolvePanel inside a GradientContainer and pass show={showResolver}. If showResolver is false, the component should render null but keep state.\n - Otherwise, when showResolver is true, mount AnswersResolvePanel inside a GradientContainer as before.\n - When showResolver is false, render resolutionRating and AnswersPanel as usual.\n- For MultiNumericOverview (MULTI_NUMERIC):\n - Make the same conditional rendering change as above: mount IndependentAnswersResolvePanel when !shouldAnswersSumToOne and mechanism === 'cpmm-multi-1' with show={showResolver}; otherwise render AnswersResolvePanel when showResolver is true; when showResolver is false, render the default content.\n\n3) Update contract/contract-table-action.tsx\n- Imports\n - Change the import of answers panel to import both AnswersResolvePanel and IndependentAnswersResolvePanel from ../answers/answer-resolve-panel.\n- SmallResolutionPanel modal routing\n - If outcomeType === 'MULTIPLE_CHOICE' and !contract.shouldAnswersSumToOne, render IndependentAnswersResolvePanel with show={true} within the modal. Keep other branches intact for BINARY, PSEUDO_NUMERIC, and standard MULTIPLE_CHOICE.\n\n4) Update widgets/amount-input.tsx\n- Adjust the label position styling by removing the -mt-0.5 class from the label span to improve vertical centering. The label’s class should be: `text-ink-400 absolute top-1/2 my-auto ml-2 -translate-y-1/2`.\n\nBehavioral expectations\n- Independent multiple-choice markets (shouldAnswersSumToOne === false) should allow selecting YES/NO/CANCEL/MKT per answer, optionally setting a percent for MKT, reviewing the pending selections, and submitting them sequentially via the resolve API.\n- The independent resolver should maintain its selection state while hidden (show=false) and only clear after completing and pressing Done (or when explicitly removed per answer).\n- The old sum-to-one AnswersResolvePanel should not handle independent markets; it should show a placeholder text for the unsupported case as specified.\n- The resolver mounting in contract-overview and contract-table-action should reflect the new independent flow as described.",
|
|
1002
|
-
"prompt": "Add an independent multiple-choice resolution flow for non-sum-to-one markets. Provide a per-answer UI to pick YES, NO, CANCEL, or a partial (MKT) resolution with a percent, allow users to review all selections, and submit them in sequence with clear progress and error handling. Integrate this flow in the contract overview and the small modal trigger so that independent markets always use the new flow and keep selection state while hidden. Ensure the standard sum-to-one flow still works for ordinary multiple-choice markets, and polish the amount input label alignment.",
|
|
1003
|
-
"supplementalFiles": [
|
|
1004
|
-
"web/components/resolution-panel.tsx",
|
|
1005
|
-
"web/components/bet/yes-no-selector.tsx",
|
|
1006
|
-
"web/lib/api/api.ts",
|
|
1007
|
-
"web/hooks/use-persistent-local-state.ts",
|
|
1008
|
-
"common/src/contract.ts"
|
|
1009
|
-
],
|
|
1010
|
-
"fileDiffs": [
|
|
1011
|
-
{
|
|
1012
|
-
"path": "web/components/answers/answer-resolve-panel.tsx",
|
|
1013
|
-
"status": "modified",
|
|
1014
|
-
"diff": "Index: web/components/answers/answer-resolve-panel.tsx\n===================================================================\n--- web/components/answers/answer-resolve-panel.tsx\t7eb20fc (parent)\n+++ web/components/answers/answer-resolve-panel.tsx\t0316bb8 (commit)\n@@ -13,13 +13,9 @@\n import { useUser } from 'web/hooks/use-user'\n import { Answer, OTHER_TOOLTIP_TEXT } from 'common/answer'\n import { getAnswerProbability } from 'common/calculate'\n import { useDisplayUserByIdOrAnswer } from 'web/hooks/use-user-supabase'\n-import {\n- MiniResolutionPanel,\n- ResolutionExplainer,\n- ResolveHeader,\n-} from '../resolution-panel'\n+import { ResolutionExplainer, ResolveHeader } from '../resolution-panel'\n import { InfoTooltip } from '../widgets/info-tooltip'\n import {\n AnswerBar,\n CreatorAndAnswerLabel,\n@@ -29,8 +25,11 @@\n } from './answer-components'\n import { useAdmin } from 'web/hooks/use-admin'\n import { AmountInput } from '../widgets/amount-input'\n import { getAnswerColor } from '../charts/contract/choice'\n+import { YesNoCancelSelector } from '../bet/yes-no-selector'\n+import { resolution } from 'common/contract'\n+import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n \n function getAnswerResolveButtonColor(\n resolveOption: string | undefined,\n answers: string[],\n@@ -274,11 +273,9 @@\n onClose={onClose}\n fullTitle={!inModal}\n />\n {!contract.shouldAnswersSumToOne ? (\n- <div className=\"text-scarlet-500\">\n- Independent multiple choice markets cannot currently be resolved.\n- </div>\n+ <div className=\"text-ink-500\">wrong resolution panel</div>\n ) : (\n <>\n <AnswersResolveOptions\n contract={contract}\n@@ -406,10 +403,12 @@\n \n export const IndependentAnswersResolvePanel = (props: {\n contract: MultiContract\n onClose: () => void\n+ // If you remove this, the hiding after clearing the resolutions will be janky\n+ show: boolean\n }) => {\n- const { contract, onClose } = props\n+ const { contract, onClose, show } = props\n \n const isAdmin = useAdmin()\n const user = useUser()\n \n@@ -419,23 +418,286 @@\n (a) => (a.resolution ? -a.subsidyPool : -Infinity),\n (a) => (addAnswersMode === 'ANYONE' ? -1 * a.prob : a.index)\n )\n \n+ // Track resolutions for batch submission\n+ const [selectedResolutions, setSelectedResolutions] =\n+ usePersistentInMemoryState<{\n+ [answerId: string]: resolution\n+ }>({}, 'selectedResolutions')\n+ const [isShowingConfirmation, setIsShowingConfirmation] = useState(false)\n+ const [isSubmitting, setIsSubmitting] = useState(false)\n+ const [error, setError] = useState<string | undefined>(undefined)\n+ const [completedResolutions, setCompletedResolutions] =\n+ usePersistentInMemoryState<string[]>([], 'completedResolutions')\n+ const [resolutionProbs, setResolutionProbs] = usePersistentInMemoryState<{\n+ [answerId: string]: number | undefined\n+ }>({}, 'resolutionProbs')\n+\n+ const handleSelectResolution = (answerId: string, resolution: resolution) => {\n+ setSelectedResolutions((prev) => ({\n+ ...prev,\n+ [answerId]: resolution,\n+ }))\n+\n+ // Reset probability if not MKT\n+ if (resolution !== 'MKT') {\n+ setResolutionProbs((prev) => {\n+ const updated = { ...prev }\n+ delete updated[answerId]\n+ return updated\n+ })\n+ }\n+ }\n+\n+ const handleRemoveResolution = (answerId: string) => {\n+ setSelectedResolutions((prev) => {\n+ const updated = { ...prev }\n+ delete updated[answerId]\n+ return updated\n+ })\n+\n+ // Also remove probability if exists\n+ setResolutionProbs((prev) => {\n+ const updated = { ...prev }\n+ delete updated[answerId]\n+ return updated\n+ })\n+ }\n+\n+ const handleSetResolutionProb = (answerId: string, prob?: number) => {\n+ setResolutionProbs((prev) => ({\n+ ...prev,\n+ [answerId]: prob, // Default to 50% if undefined\n+ }))\n+ }\n+\n+ const selectedCount = Object.keys(selectedResolutions).length\n+\n+ const submitBatchResolutions = async () => {\n+ setIsSubmitting(true)\n+ setError(undefined)\n+\n+ try {\n+ // Process resolutions sequentially\n+ for (const [answerId, outcome] of Object.entries(selectedResolutions)) {\n+ // Skip already resolved answers\n+ if (completedResolutions.includes(answerId)) {\n+ continue\n+ }\n+ if (outcome === 'MKT') {\n+ if (!resolutionProbs[answerId]) {\n+ setError(\n+ `Please set a probability for ${\n+ answers.find((a) => a.id === answerId)?.text\n+ }`\n+ )\n+ break\n+ }\n+ }\n+ try {\n+ await api('market/:contractId/resolve', {\n+ contractId: contract.id,\n+ outcome,\n+ answerId,\n+ probabilityInt:\n+ outcome === 'MKT' ? resolutionProbs[answerId] : undefined,\n+ })\n+\n+ // Mark this resolution as completed\n+ setCompletedResolutions((prev) => [...prev, answerId])\n+ } catch (e) {\n+ setError(\n+ `Error resolving answer: ${\n+ e instanceof APIError ? e.message : 'Unknown error'\n+ }`\n+ )\n+ break\n+ }\n+ }\n+ } catch (e) {\n+ if (e instanceof APIError) {\n+ setError(e.message)\n+ } else {\n+ console.error(e)\n+ setError('Error resolving answers')\n+ }\n+ } finally {\n+ setIsSubmitting(false)\n+ }\n+ }\n+\n+ const handleDone = () => {\n+ // Clear all resolution state\n+ setSelectedResolutions({})\n+ setCompletedResolutions([])\n+ setResolutionProbs({})\n+ setIsShowingConfirmation(false)\n+\n+ // Close the panel\n+ onClose()\n+ }\n+ if (!show) return null\n+\n+ if (isShowingConfirmation) {\n+ const allResolutionIds = Object.keys(selectedResolutions)\n+ const isProcessing = isSubmitting\n+ const hasCompletedAll =\n+ !isSubmitting &&\n+ completedResolutions.length === allResolutionIds.length &&\n+ allResolutionIds.length > 0\n+\n+ // Calculate remaining answers to resolve\n+ const remainingAnswers = allResolutionIds.filter(\n+ (id) => !completedResolutions.includes(id)\n+ )\n+ const hasPartiallyCompleted =\n+ completedResolutions.length > 0 && remainingAnswers.length > 0\n+\n+ return (\n+ <Col className=\"gap-3\">\n+ <div className=\"text-lg font-semibold\">\n+ {!isProcessing && !hasCompletedAll && !hasPartiallyCompleted\n+ ? 'Confirm Resolution'\n+ : !isProcessing && hasPartiallyCompleted\n+ ? 'Continue Resolution'\n+ : isProcessing\n+ ? 'Processing Resolutions...'\n+ : 'Resolutions Complete'}\n+ </div>\n+\n+ <div>\n+ {!isProcessing && !hasCompletedAll && !hasPartiallyCompleted\n+ ? 'You are about to resolve the following answers:'\n+ : !isProcessing && hasPartiallyCompleted\n+ ? `${completedResolutions.length} answer(s) resolved. Continue with remaining ${remainingAnswers.length}`\n+ : isProcessing\n+ ? 'Processing your selected resolutions:'\n+ : 'All resolutions have been processed successfully:'}\n+ </div>\n+\n+ <div className=\"border-ink-200 max-h-60 overflow-y-auto rounded border p-2\">\n+ {Object.entries(selectedResolutions).map(([answerId, resolution]) => {\n+ const answer = answers.find((a) => a.id === answerId)\n+ if (!answer) return null\n+\n+ const isCompleted = completedResolutions.includes(answerId)\n+ const isCurrentlyProcessing =\n+ isSubmitting &&\n+ !isCompleted &&\n+ completedResolutions.length === allResolutionIds.indexOf(answerId)\n+\n+ return (\n+ <div\n+ key={answerId}\n+ className=\"border-ink-100 flex items-center justify-between border-b py-2 last:border-0\"\n+ >\n+ <div className=\"font-medium\">{answer.text}</div>\n+ <Row className=\"items-center gap-2\">\n+ <div\n+ className={clsx(\n+ 'font-semibold',\n+ resolution === 'YES'\n+ ? 'text-green-500'\n+ : resolution === 'NO'\n+ ? 'text-red-500'\n+ : resolution === 'CANCEL'\n+ ? 'text-yellow-500'\n+ : 'text-blue-500'\n+ )}\n+ >\n+ {resolution}\n+ {resolution === 'MKT' &&\n+ resolutionProbs[answerId] &&\n+ ` ${resolutionProbs[answerId]}%`}\n+ </div>\n+\n+ {isCompleted && (\n+ <div className=\"ml-2 text-sm font-medium text-green-500\">\n+ ✓ Completed\n+ </div>\n+ )}\n+\n+ {isCurrentlyProcessing && (\n+ <div className=\"text-ink-500 ml-2 animate-pulse text-sm font-medium\">\n+ Processing...\n+ </div>\n+ )}\n+ </Row>\n+ </div>\n+ )\n+ })}\n+ </div>\n+ {error && <div className=\"text-scarlet-500 p-3\">{error}</div>}\n+ <Row className=\"justify-end gap-3\">\n+ {!isSubmitting && !hasCompletedAll && (\n+ <Button\n+ color=\"gray\"\n+ onClick={() => setIsShowingConfirmation(false)}\n+ >\n+ Back\n+ </Button>\n+ )}\n+\n+ {!isSubmitting && !hasCompletedAll && (\n+ <Button color=\"indigo\" onClick={submitBatchResolutions}>\n+ {hasPartiallyCompleted\n+ ? 'Continue Resolving'\n+ : 'Submit All Resolutions'}\n+ </Button>\n+ )}\n+\n+ {hasCompletedAll && (\n+ <Button color=\"green\" onClick={handleDone}>\n+ Done\n+ </Button>\n+ )}\n+\n+ {isProcessing && (\n+ <Button color=\"indigo\" disabled loading>\n+ Processing...\n+ </Button>\n+ )}\n+ </Row>\n+ </Col>\n+ )\n+ }\n+\n return (\n <Col className=\"gap-3\">\n <ResolveHeader\n contract={contract}\n isCreator={user?.id === contract.creatorId}\n onClose={onClose}\n />\n+\n+ <Row className=\"bg-primary-50 items-center justify-between rounded p-3\">\n+ <div>\n+ <span className=\"font-medium\">{selectedCount}</span> answer\n+ {selectedCount !== 1 ? 's' : ''} selected for resolution\n+ </div>\n+ <Button\n+ color=\"indigo\"\n+ disabled={selectedCount === 0}\n+ onClick={() => setIsShowingConfirmation(true)}\n+ >\n+ Review & Submit\n+ </Button>\n+ </Row>\n+\n <Col className=\"gap-2\">\n {sortedAnswers.map((answer) => (\n <IndependentResolutionAnswerItem\n key={answer.id}\n contract={contract}\n answer={answer}\n color={getAnswerColor(answer)}\n isAdmin={isAdmin}\n+ selectedResolution={selectedResolutions[answer.id]}\n+ resolutionProb={resolutionProbs[answer.id]}\n+ onSelectResolution={handleSelectResolution}\n+ onRemoveResolution={handleRemoveResolution}\n+ onSetResolutionProb={handleSetResolutionProb}\n />\n ))}\n </Col>\n <ResolutionExplainer independentMulti />\n@@ -447,18 +709,87 @@\n contract: MultiContract\n answer: Answer\n color: string\n isAdmin: boolean\n+ selectedResolution?: resolution\n+ resolutionProb?: number\n+ onSelectResolution: (answerId: string, resolution: resolution) => void\n+ onRemoveResolution: (answerId: string) => void\n+ onSetResolutionProb: (answerId: string, prob?: number) => void\n }) {\n- const { contract, answer, color, isAdmin } = props\n+ const {\n+ contract,\n+ answer,\n+ color,\n+ isAdmin,\n+ selectedResolution,\n+ resolutionProb,\n+ onSelectResolution,\n+ onRemoveResolution,\n+ onSetResolutionProb,\n+ } = props\n+\n const answerCreator = useDisplayUserByIdOrAnswer(answer)\n const user = useUser()\n const isCreator = user?.id === contract.creatorId\n \n const prob = getAnswerProbability(contract, answer.id)\n \n const addAnswersMode = contract.addAnswersMode ?? 'DISABLED'\n \n+ // Skip already resolved answers\n+ if (answer.resolution) {\n+ return (\n+ <Col>\n+ <AnswerBar\n+ color={color}\n+ prob={prob}\n+ label={\n+ <Row className={'items-center gap-1'}>\n+ <AnswerStatus contract={contract} answer={answer} />\n+ {answer.isOther ? (\n+ <span>\n+ Other{' '}\n+ <InfoTooltip\n+ className=\"!text-ink-600\"\n+ text={OTHER_TOOLTIP_TEXT}\n+ />\n+ </span>\n+ ) : (\n+ <CreatorAndAnswerLabel\n+ text={answer.text}\n+ createdTime={answer.createdTime}\n+ creator={\n+ addAnswersMode === 'ANYONE'\n+ ? answerCreator ?? false\n+ : undefined\n+ }\n+ className={clsx(\n+ 'items-center text-sm !leading-none sm:text-base'\n+ )}\n+ />\n+ )}\n+ </Row>\n+ }\n+ end={null}\n+ />\n+ </Col>\n+ )\n+ }\n+\n+ const handleOutcomeSelect = (outcome: resolution | undefined) => {\n+ if (!outcome) {\n+ onRemoveResolution(answer.id)\n+ } else {\n+ onSelectResolution(answer.id, outcome)\n+\n+ // Set default probability if MKT selected\n+ if (outcome === 'MKT' && !resolutionProb) {\n+ onSetResolutionProb(answer.id, Math.round(prob * 100))\n+ }\n+ }\n+ }\n+\n return (\n <Col>\n <AnswerBar\n color={color}\n@@ -489,20 +820,68 @@\n />\n )}\n </Row>\n }\n- end={null}\n+ end={\n+ selectedResolution ? (\n+ <div\n+ className={clsx(\n+ 'text-sm font-semibold',\n+ selectedResolution === 'YES'\n+ ? 'text-green-500'\n+ : selectedResolution === 'NO'\n+ ? 'text-red-500'\n+ : selectedResolution === 'CANCEL'\n+ ? 'text-yellow-500'\n+ : 'text-blue-500'\n+ )}\n+ >\n+ {selectedResolution}\n+ {selectedResolution === 'MKT' &&\n+ resolutionProb &&\n+ ` ${resolutionProb}%`}\n+ </div>\n+ ) : null\n+ }\n />\n- {!answer.resolution && (\n- <>\n- <MiniResolutionPanel\n- contract={contract}\n- answer={answer}\n- isAdmin={isAdmin}\n- isCreator={isCreator}\n+ <div className=\"mt-2\">\n+ <Row className=\"flex-wrap gap-4\">\n+ {isAdmin && !isCreator && (\n+ <div className=\"bg-scarlet-50 text-scarlet-500 self-start rounded p-1 text-xs\">\n+ ADMIN\n+ </div>\n+ )}\n+ <YesNoCancelSelector\n+ selected={selectedResolution as resolution | undefined}\n+ onSelect={handleOutcomeSelect}\n />\n- <hr className=\"border-ink-300 mb-2 mt-4\" />\n- </>\n- )}\n+\n+ {selectedResolution === 'MKT' && (\n+ <Row className=\"items-center gap-2\">\n+ <span className=\"text-ink-500 text-sm\">Resolve to</span>\n+ <AmountInput\n+ inputClassName=\"w-20 h-9\"\n+ label=\"%\"\n+ amount={resolutionProb ? Math.round(resolutionProb) : undefined}\n+ onChangeAmount={(value) =>\n+ onSetResolutionProb(answer.id, value ? value : undefined)\n+ }\n+ disableClearButton\n+ />\n+ </Row>\n+ )}\n+\n+ {selectedResolution && (\n+ <Button\n+ color=\"gray\"\n+ size=\"xs\"\n+ onClick={() => onRemoveResolution(answer.id)}\n+ >\n+ Remove\n+ </Button>\n+ )}\n+ </Row>\n+ </div>\n+ <hr className=\"border-ink-300 mb-2 mt-4\" />\n </Col>\n )\n }\n"
|
|
1015
|
-
},
|
|
1016
|
-
{
|
|
1017
|
-
"path": "web/components/contract/contract-overview.tsx",
|
|
1018
|
-
"status": "modified",
|
|
1019
|
-
"diff": "Index: web/components/contract/contract-overview.tsx\n===================================================================\n--- web/components/contract/contract-overview.tsx\t7eb20fc (parent)\n+++ web/components/contract/contract-overview.tsx\t0316bb8 (commit)\n@@ -399,9 +399,8 @@\n </Row>\n </>\n )}\n </Row>\n-\n {contract.mechanism == 'cpmm-multi-1' && !hideGraph && (\n <SizedContainer\n className={clsx(\n 'h-[150px] w-full pb-4 pr-10 sm:h-[250px]',\n@@ -445,25 +444,25 @@\n hoveredAnnotation={hoveredAnnotation}\n setHoveredAnnotation={setHoveredAnnotation}\n />\n ) : null}\n- {showResolver ? (\n- !shouldAnswersSumToOne && contract.mechanism === 'cpmm-multi-1' ? (\n- <GradientContainer>\n- <IndependentAnswersResolvePanel\n- contract={contract}\n- onClose={() => setShowResolver(false)}\n- />\n- </GradientContainer>\n- ) : (\n- <GradientContainer>\n- <AnswersResolvePanel\n- contract={contract as CPMMMultiContract}\n- onClose={() => setShowResolver(false)}\n- />\n- </GradientContainer>\n- )\n- ) : (\n+ {!shouldAnswersSumToOne && contract.mechanism === 'cpmm-multi-1' ? (\n+ <GradientContainer>\n+ <IndependentAnswersResolvePanel\n+ contract={contract}\n+ onClose={() => setShowResolver(false)}\n+ show={showResolver}\n+ />\n+ </GradientContainer>\n+ ) : showResolver ? (\n+ <GradientContainer>\n+ <AnswersResolvePanel\n+ contract={contract as CPMMMultiContract}\n+ onClose={() => setShowResolver(false)}\n+ />\n+ </GradientContainer>\n+ ) : null}\n+ {!showResolver && (\n <>\n {resolutionRating}\n <AnswersPanel\n setDefaultAnswerIdsToGraph={setDefaultAnswerIdsToGraph}\n@@ -700,25 +699,25 @@\n showZoomer={showZoomer}\n />\n )}\n </SizedContainer>\n- {showResolver ? (\n- !shouldAnswersSumToOne && contract.mechanism === 'cpmm-multi-1' ? (\n- <GradientContainer>\n- <IndependentAnswersResolvePanel\n- contract={contract}\n- onClose={() => setShowResolver(false)}\n- />\n- </GradientContainer>\n- ) : (\n- <GradientContainer>\n- <AnswersResolvePanel\n- contract={contract}\n- onClose={() => setShowResolver(false)}\n- />\n- </GradientContainer>\n- )\n- ) : (\n+ {!shouldAnswersSumToOne && contract.mechanism === 'cpmm-multi-1' ? (\n+ <GradientContainer>\n+ <IndependentAnswersResolvePanel\n+ show={showResolver}\n+ contract={contract}\n+ onClose={() => setShowResolver(false)}\n+ />\n+ </GradientContainer>\n+ ) : showResolver ? (\n+ <GradientContainer>\n+ <AnswersResolvePanel\n+ contract={contract}\n+ onClose={() => setShowResolver(false)}\n+ />\n+ </GradientContainer>\n+ ) : null}\n+ {!showResolver && (\n <>\n {resolutionRating}\n <AnswersPanel\n hideSearch\n"
|
|
1020
|
-
},
|
|
1021
|
-
{
|
|
1022
|
-
"path": "web/components/contract/contract-table-action.tsx",
|
|
1023
|
-
"status": "modified",
|
|
1024
|
-
"diff": "Index: web/components/contract/contract-table-action.tsx\n===================================================================\n--- web/components/contract/contract-table-action.tsx\t7eb20fc (parent)\n+++ web/components/contract/contract-table-action.tsx\t0316bb8 (commit)\n@@ -8,9 +8,12 @@\n import { Row } from '../layout/row'\n import { NumericResolutionPanel } from '../numeric-resolution-panel'\n import { ResolutionPanel } from '../resolution-panel'\n import { isClosed } from './contracts-table'\n-import { AnswersResolvePanel } from '../answers/answer-resolve-panel'\n+import {\n+ AnswersResolvePanel,\n+ IndependentAnswersResolvePanel,\n+} from '../answers/answer-resolve-panel'\n import { useUser } from 'web/hooks/use-user'\n import { track } from 'web/lib/service/analytics'\n import { PollPanel } from '../poll/poll-panel'\n import { TRADE_TERM } from 'common/envs/constants'\n@@ -189,8 +192,14 @@\n contract={contract}\n onClose={() => setOpen(false)}\n inModal\n />\n+ ) : outcomeType === 'MULTIPLE_CHOICE' && !contract.shouldAnswersSumToOne ? (\n+ <IndependentAnswersResolvePanel\n+ contract={contract as CPMMMultiContract}\n+ onClose={() => setOpen(false)}\n+ show={true}\n+ />\n ) : outcomeType === 'MULTIPLE_CHOICE' ? (\n <AnswersResolvePanel\n contract={contract as CPMMMultiContract}\n onClose={() => setOpen(false)}\n"
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
"path": "web/components/widgets/amount-input.tsx",
|
|
1028
|
-
"status": "modified",
|
|
1029
|
-
"diff": "Index: web/components/widgets/amount-input.tsx\n===================================================================\n--- web/components/widgets/amount-input.tsx\t7eb20fc (parent)\n+++ web/components/widgets/amount-input.tsx\t0316bb8 (commit)\n@@ -94,9 +94,9 @@\n return (\n <Col className={clsx('relative', className)}>\n <label className=\"font-sm md:font-lg relative\">\n {label && (\n- <span className=\"text-ink-400 absolute top-1/2 my-auto -mt-0.5 ml-2 -translate-y-1/2\">\n+ <span className=\"text-ink-400 absolute top-1/2 my-auto ml-2 -translate-y-1/2\">\n {label}\n </span>\n )}\n <Row>\n"
|
|
1030
|
-
}
|
|
1031
|
-
]
|
|
1032
|
-
},
|
|
1033
|
-
{
|
|
1034
|
-
"id": "modernize-bottom-nav",
|
|
1035
|
-
"sha": "74465a3153ad1c23ba79ed5d8d03e6afb84de773",
|
|
1036
|
-
"parentSha": "baff7da01053a8ea6b270846ce660b070724d1f2",
|
|
1037
|
-
"spec": "Implement mobile bottom navigation updates with active-state solid icons and touch animations, and add solid/outline support for the notifications icon.\n\nRequired changes:\n\n1) Bottom navigation behavior and visuals (web/components/nav/bottom-nav-bar.tsx)\n- Use the solid icon variant for a nav item when its route is the current page. Base the active state on comparing the first path segment of the current pathname to the nav item's href path segment.\n- Pass a solid icon component for each applicable item (e.g., Search, Explore, Inbox), falling back to the outline icon otherwise.\n- For the Inbox item, use the NotificationsIcon with a new solid prop. Render the outline icon when inactive and the filled icon when active.\n- Update class names for items and icons: selected item should only change text color (no background), adopt the new icon sizing, and apply the new touch-press animation class on touch.\n- On the Profile item, add a ring highlight around the avatar when the current page is the user's profile. Adjust the TokenNumber coin vertical offset as in the diff, and remove the sweeps cash TokenNumber display from this bottom bar item.\n\n2) Nav item type extension (web/components/nav/sidebar-item.tsx)\n- Extend the NavItem type with an optional solidIcon?: React.ComponentType<{ className?: string }>. Do not change the existing API otherwise.\n\n3) Notifications icon solid/outline support (web/components/notifications-icon.tsx)\n- Add a boolean prop `solid` (optional) to NotificationsIcon. When `solid` is true, render a filled bell icon; otherwise render the outline bell. Continue to render the unseen notifications bubble behavior as before.\n- Replace outline/solid icons with IoNotificationsOutline and IoNotifications from react-icons/io5. Preserve the className prop behavior and external interface.\n\n4) Touch press animation CSS (web/styles/globals.css)\n- Add keyframes and utility classes for a touch press effect (bounce and scale transitions). Provide classes used by the bottom navigation for press-in and release behavior as in the diff: touch-bounce, touch-scale-down, touch-press-effect, and touch-release, with appropriate durations and easing.\n\nAcceptance criteria:\n- On mobile, active tabs in the bottom navigation show their solid icon variant; inactive tabs show outline icons. Specifically, Explore shows a filled compass when active, Browse uses its solid search icon when active, and Inbox uses a filled bell when active.\n- Tapping bottom nav items applies a visible press effect consistent with the new CSS classes.\n- The Profile bottom nav item shows a ring around the user avatar when viewing the user's own profile. Only the main token balance is shown (no sweeps balance) in this item.\n- The NavItem type supports an optional solidIcon and does not break existing sidebar usage.\n- NotificationsIcon accepts a solid flag and correctly switches between outline and filled icons without breaking unseen count behavior.\n",
|
|
1038
|
-
"prompt": "Modernize the mobile bottom navigation to use filled icons for the active tab, introduce a subtle touch-press animation, and make the notifications icon switch between outline and filled variants based on active state. Add support in the nav item model for providing a solid icon alongside the outline icon. Visually highlight the profile avatar in the bottom bar when the user is on their profile. Keep the current desktop sidebar behavior intact.\n\nHigh-level requirements:\n- Active bottom nav items use a filled icon; inactive items use an outline icon. Ensure this includes Search/Browse, Explore, and Notifications.\n- The notifications bell component should support switching between outline and filled variants via a simple prop, while retaining unseen count behavior.\n- Add a lightweight tap animation for bottom nav items to feel snappier on touch.\n- When the user is on their own profile page, the bottom bar profile avatar should get a clear active highlight. The bottom bar should show only the primary token balance for the profile item.\n- Extend the nav item type to optionally accept a solid icon without changing existing desktop usage.\n\nDo not change unrelated functionality or desktop navigation. Ensure that existing pages and routes continue to work and that the new behavior is visible in the mobile layout.",
|
|
1039
|
-
"supplementalFiles": [
|
|
1040
|
-
"web/components/nav/sidebar.tsx",
|
|
1041
|
-
"web/components/layout/page.tsx",
|
|
1042
|
-
"web/pages/_app.tsx",
|
|
1043
|
-
"web/components/notifications/notification-dropdown.tsx",
|
|
1044
|
-
"web/components/nav/profile-summary.tsx"
|
|
1045
|
-
],
|
|
1046
|
-
"fileDiffs": [
|
|
1047
|
-
{
|
|
1048
|
-
"path": "web/components/nav/bottom-nav-bar.tsx",
|
|
1049
|
-
"status": "modified",
|
|
1050
|
-
"diff": "Index: web/components/nav/bottom-nav-bar.tsx\n===================================================================\n--- web/components/nav/bottom-nav-bar.tsx\tbaff7da (parent)\n+++ web/components/nav/bottom-nav-bar.tsx\t74465a3 (commit)\n@@ -10,14 +10,22 @@\n QuestionMarkCircleIcon,\n SearchIcon,\n UserCircleIcon,\n } from '@heroicons/react/outline'\n-import { MenuAlt3Icon, XIcon } from '@heroicons/react/solid'\n+import {\n+ MenuAlt3Icon,\n+ QuestionMarkCircleIcon as QuestionMarkCircleIconSolid,\n+ // SearchIcon as SearchIconSolid,\n+ UserCircleIcon as UserCircleIconSolid,\n+ XIcon,\n+} from '@heroicons/react/solid'\n import clsx from 'clsx'\n import { User } from 'common/user'\n import Link from 'next/link'\n import { usePathname } from 'next/navigation'\n import { Fragment, useState } from 'react'\n+import { FaSearch as SearchIconSolid } from 'react-icons/fa'\n+import { IoCompass, IoCompassOutline } from 'react-icons/io5'\n import { NotificationsIcon } from 'web/components/notifications-icon'\n import { useIsIframe } from 'web/hooks/use-is-iframe'\n import { useUser } from 'web/hooks/use-user'\n import { firebaseLogin } from 'web/lib/firebase/users'\n@@ -27,62 +35,83 @@\n import { Avatar } from '../widgets/avatar'\n import { TokenNumber } from '../widgets/token-number'\n import Sidebar from './sidebar'\n import { NavItem } from './sidebar-item'\n-import { IoCompassOutline } from 'react-icons/io5'\n \n export const BOTTOM_NAV_BAR_HEIGHT = 58\n \n const itemClass =\n 'sm:hover:bg-ink-200 block w-full py-1 px-3 text-center sm:hover:text-primary-700 transition-colors'\n-const selectedItemClass = 'bg-ink-100 text-primary-700'\n-const touchItemClass = 'bg-primary-100'\n-const iconClassName = 'mx-auto my-1 h-7 w-7'\n+const selectedItemClass = 'text-primary-700'\n+const touchItemClass = 'touch-press-effect'\n+const iconClassName = 'mx-auto my-1 h-[1.6rem] w-[1.6rem]'\n+const exploreIconClassName =\n+ ' h-[1.8rem] w-[1.8rem] !mb-[0.19rem] !mt-[0.135rem]'\n \n+// Wrapper components for NotificationsIcon to work with the navigation system\n+const NotificationsIconOutline = (props: { className?: string }) => (\n+ <NotificationsIcon {...props} solid={false} />\n+)\n+const NotificationsIconSolid = (props: { className?: string }) => (\n+ <NotificationsIcon {...props} solid={true} />\n+)\n+\n function getNavigation(user: User) {\n return [\n {\n name: 'Browse',\n href: '/home',\n icon: SearchIcon,\n+ solidIcon: SearchIconSolid,\n },\n {\n name: 'Explore',\n href: '/explore',\n icon: IoCompassOutline,\n- iconClassName: '!h-[1.9rem] !w-[1.9rem] !mb-[0.19rem] !mt-[0.13rem]',\n+ solidIcon: IoCompass,\n+ iconClassName: exploreIconClassName,\n },\n {\n name: 'Profile',\n href: `/${user.username}`,\n },\n {\n name: 'Inbox',\n href: `/notifications`,\n- icon: NotificationsIcon,\n+ icon: NotificationsIconOutline,\n+ solidIcon: NotificationsIconSolid,\n },\n ]\n }\n \n const signedOutNavigation = () => [\n- { name: 'Browse', href: '/browse', icon: SearchIcon, alwaysShowName: true },\n {\n+ name: 'Browse',\n+ href: '/browse',\n+ icon: SearchIcon,\n+ solidIcon: SearchIconSolid,\n+ alwaysShowName: true,\n+ },\n+ {\n name: 'Explore',\n href: '/explore',\n icon: IoCompassOutline,\n- iconClassName: '!h-[1.9rem] !w-[1.9rem] !mb-[0.19rem] !mt-[0.13rem]',\n+ solidIcon: IoCompass,\n+ iconClassName: exploreIconClassName,\n // prefetch: false, // should we not prefetch this?\n },\n // { name: 'News', href: '/news', icon: NewspaperIcon, alwaysShowName: true },\n {\n name: 'About',\n href: '/about',\n icon: QuestionMarkCircleIcon,\n+ solidIcon: QuestionMarkCircleIconSolid,\n },\n {\n name: 'Sign in',\n onClick: firebaseLogin,\n icon: UserCircleIcon,\n+ solidIcon: UserCircleIconSolid,\n },\n ]\n \n // From https://codepen.io/chris__sev/pen/QWGvYbL\n@@ -145,42 +174,41 @@\n const track = trackCallback(`navbar: ${item.trackingEventName ?? item.name}`)\n const [touched, setTouched] = useState(false)\n \n if (item.name === 'Profile' && user) {\n+ const isOnUserProfile = currentPage === `/${user.username}`\n+\n return (\n <Link\n prefetch={item?.prefetch ?? true}\n href={item.href ?? '#'}\n className={clsx(\n itemClass,\n touched && touchItemClass,\n- currentPage === `/${user.username}` && selectedItemClass,\n+ isOnUserProfile && selectedItemClass,\n className\n )}\n onClick={track}\n onTouchStart={() => setTouched(true)}\n onTouchEnd={() => setTouched(false)}\n >\n <Col className=\"relative mx-auto h-full w-full items-center\">\n- <Avatar size=\"sm\" avatarUrl={user.avatarUrl} noLink />\n- <Row className=\"gap-1\">\n+ <div\n+ className={clsx(\n+ 'rounded-full',\n+ isOnUserProfile && 'ring-2 ring-violet-600'\n+ )}\n+ >\n+ <Avatar size=\"sm\" avatarUrl={user.avatarUrl} noLink />\n+ </div>\n+ <Row className=\"mt-0.5 gap-1\">\n <TokenNumber\n amount={user?.balance}\n className=\"text-violet-600 dark:text-violet-400\"\n numberType=\"short\"\n isInline\n- coinClassName=\"!top-[0.15em]\"\n+ coinClassName=\"!top-[0.1rem]\"\n />\n- {user?.cashBalance >= 1 && (\n- <TokenNumber\n- amount={user?.cashBalance}\n- className=\"text-amber-600 dark:text-amber-400\"\n- coinType=\"sweepies\"\n- numberType=\"short\"\n- isInline\n- coinClassName=\"!top-[0.15em]\"\n- />\n- )}\n </Row>\n </Col>\n </Link>\n )\n@@ -208,8 +236,13 @@\n \n const currentBasePath = currentPage?.split('/')[1] ?? ''\n const itemPath = item.href.split('/')[1]\n const isCurrentPage = currentBasePath === itemPath\n+\n+ // Use solid icon if available and page is active\n+ const IconComponent =\n+ isCurrentPage && item.solidIcon ? item.solidIcon : item.icon\n+\n return (\n <Link\n href={item.href}\n className={clsx(\n@@ -221,10 +254,10 @@\n onClick={track}\n onTouchStart={() => setTouched(true)}\n onTouchEnd={() => setTouched(false)}\n >\n- {item.icon && (\n- <item.icon className={clsx(iconClassName, item.iconClassName)} />\n+ {IconComponent && (\n+ <IconComponent className={clsx(iconClassName, item.iconClassName)} />\n )}\n {children}\n {item.name}\n </Link>\n"
|
|
1051
|
-
},
|
|
1052
|
-
{
|
|
1053
|
-
"path": "web/components/nav/sidebar-item.tsx",
|
|
1054
|
-
"status": "modified",
|
|
1055
|
-
"diff": "Index: web/components/nav/sidebar-item.tsx\n===================================================================\n--- web/components/nav/sidebar-item.tsx\tbaff7da (parent)\n+++ web/components/nav/sidebar-item.tsx\t74465a3 (commit)\n@@ -9,8 +9,9 @@\n trackingEventName?: string\n href?: string\n onClick?: () => void\n icon?: React.ComponentType<{ className?: string }>\n+ solidIcon?: React.ComponentType<{ className?: string }>\n iconClassName?: string\n external?: boolean\n alwaysShowName?: boolean\n prefetch?: boolean\n"
|
|
1056
|
-
},
|
|
1057
|
-
{
|
|
1058
|
-
"path": "web/components/notifications-icon.tsx",
|
|
1059
|
-
"status": "modified",
|
|
1060
|
-
"diff": "Index: web/components/notifications-icon.tsx\n===================================================================\n--- web/components/notifications-icon.tsx\tbaff7da (parent)\n+++ web/components/notifications-icon.tsx\t74465a3 (commit)\n@@ -1,26 +1,34 @@\n 'use client'\n-import { BellIcon } from '@heroicons/react/outline'\n-import { Row } from 'web/components/layout/row'\n-import { useEffect } from 'react'\n-import { usePrivateUser } from 'web/hooks/use-user'\n-import { PrivateUser } from 'common/user'\n-import { usePathname } from 'next/navigation'\n-import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'\n+// import { BellIcon } from '@heroicons/react/outline'\n import {\n NOTIFICATIONS_PER_PAGE,\n useGroupedUnseenNotifications,\n } from 'client-common/hooks/use-notifications'\n-import { api } from 'web/lib/api/api'\n+import { PrivateUser } from 'common/user'\n+import { usePathname } from 'next/navigation'\n+import { useEffect } from 'react'\n+import {\n+ IoNotificationsOutline as BellIcon,\n+ IoNotifications as BellIconSolid,\n+} from 'react-icons/io5'\n+import { Row } from 'web/components/layout/row'\n+import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'\n import { useUnseenPrivateMessageChannels } from 'web/hooks/use-private-messages'\n+import { usePrivateUser } from 'web/hooks/use-user'\n+import { api } from 'web/lib/api/api'\n \n-export function NotificationsIcon(props: { className?: string }) {\n+export function NotificationsIcon(props: {\n+ className?: string\n+ solid?: boolean\n+}) {\n const privateUser = usePrivateUser()\n- const { className } = props\n+ const { className, solid } = props\n+ const Icon = solid ? BellIconSolid : BellIcon\n return (\n <Row className=\"relative justify-center\">\n {privateUser && <UnseenNotificationsBubble privateUser={privateUser} />}\n- <BellIcon className={className} />\n+ <Icon className={className} />\n </Row>\n )\n }\n \n"
|
|
1061
|
-
},
|
|
1062
|
-
{
|
|
1063
|
-
"path": "web/styles/globals.css",
|
|
1064
|
-
"status": "modified",
|
|
1065
|
-
"diff": "Index: web/styles/globals.css\n===================================================================\n--- web/styles/globals.css\tbaff7da (parent)\n+++ web/styles/globals.css\t74465a3 (commit)\n@@ -240,4 +240,52 @@\n \n .text-6xl .coin-offset {\n --coin-top-offset: calc(0em);\n }\n+\n+/* Touch animation: shrink then grow back */\n+@keyframes bounce-press {\n+ 0% {\n+ transform: scale(1);\n+ }\n+ 50% {\n+ transform: scale(0.92);\n+ }\n+ 100% {\n+ transform: scale(1);\n+ }\n+}\n+\n+.touch-bounce {\n+ animation: bounce-press 0.2s ease-in-out;\n+}\n+\n+/* Alternative with active state for continuous press */\n+.touch-scale-down {\n+ transform: scale(0.95);\n+ transition: transform 0.1s ease-in-out;\n+}\n+\n+/* Combined effect: bounce then hold */\n+@keyframes touch-press-bounce {\n+ 0% {\n+ transform: scale(1);\n+ }\n+ 25% {\n+ transform: scale(0.92);\n+ }\n+ 50% {\n+ transform: scale(0.97);\n+ }\n+ 100% {\n+ transform: scale(0.95);\n+ }\n+}\n+\n+.touch-press-effect {\n+ animation: touch-press-bounce 0.15s ease-out forwards;\n+}\n+\n+.touch-release {\n+ transform: scale(1);\n+ transition: transform 0.1s ease-out;\n+}\n"
|
|
1066
|
-
}
|
|
1067
|
-
]
|
|
1068
|
-
},
|
|
1069
|
-
{
|
|
1070
|
-
"id": "normalize-hyphen-search",
|
|
1071
|
-
"sha": "04a6e7e4f7b48abe44efd193a26191bc6ed83b74",
|
|
1072
|
-
"parentSha": "ba89c4e95f7e21e948b5c380a52d1668e8e2f956",
|
|
1073
|
-
"spec": "Implement hyphen-insensitive market search across contract question/description and answer text by normalizing hyphen-like characters in search terms and in FTS vectors.\n\nMake the following changes:\n\n1) Add hyphen normalization helpers in backend/shared/src/helpers/search.ts\n- Introduce a function that removes hyphen variants (regular hyphen, minus, en-dash, em-dash) from a string.\n- Add a new constructPrefixTsQueryNormalized(term: string) that applies the hyphen normalization before existing sanitization, tokenization, and prefix tsquery construction. Keep the existing constructPrefixTsQuery for legacy users unchanged.\n- Log the normalized/sanitized term to aid debugging.\n\n2) Update contracts search to use normalized queries in backend/shared/src/supabase/search-contracts.ts\n- Import and use the new constructPrefixTsQueryNormalized for the 'prefix' search type when querying question_fts via to_tsquery.\n- Normalize the term via the new helper before passing it into websearch_to_tsquery for:\n - question_fts with 'without-stopwords'\n - question_nostop_fts with 'with-stopwords'\n - description_fts with 'description'\n - answers.text_fts subquery for 'answer'\n- Ensure imports are adjusted accordingly (add normalizeHyphens and constructPrefixTsQueryNormalized; keep other imports unchanged). Maintain existing sorting and filtering logic.\n\n3) Create a SQL migration to regenerate normalized FTS vectors in backend/supabase/hyphen-search-migration.sql\n- Define a Postgres function normalize_hyphens(text) that removes hyphen variants using a regex replacement.\n- Drop and recreate the following generated tsvector columns on contracts using normalize_hyphens around the source text while preserving column names:\n - question_fts: to_tsvector('english_extended', normalize_hyphens(question))\n - description_fts: to_tsvector('english_extended', normalize_hyphens(add_creator_name_to_description(data)))\n - question_nostop_fts: to_tsvector('english_nostop_with_prefix', normalize_hyphens(question))\n- Recreate corresponding GIN indexes on these columns with their original names.\n- Do not alter the answers table schema; rely on term normalization for answers search.\n\nAcceptance criteria:\n- Searching for terms with or without hyphens (e.g., \"gpt-5\", \"gpt–5\", \"gpt—5\", \"gpt5\") returns the same set of results for contract questions, descriptions, and answers.\n- The search endpoints continue to work with existing search types and sorting. No regressions in other endpoints that use constructPrefixTsQuery.\n- Database migration completes successfully, replacing the FTS vectors and indexes without changing their names.\n",
|
|
1074
|
-
"prompt": "Add hyphen-insensitive search to the market search feature. Normalize hyphen-like characters in user search terms and regenerate the full-text vector columns so that questions, descriptions, and answers match whether a hyphen is present or not (e.g., \"gpt-5\" should match \"gpt5\"). Keep the current search modes and result ordering intact. Provide a database migration that updates the FTS columns to use normalized text while keeping the same column names and indexes. Limit the change to contracts and answers search; do not modify user or group search behavior.",
|
|
1075
|
-
"supplementalFiles": [
|
|
1076
|
-
"backend/api/src/search-contracts.ts",
|
|
1077
|
-
"backend/shared/src/supabase/sql-builder.ts",
|
|
1078
|
-
"backend/shared/src/supabase/contracts.ts"
|
|
1079
|
-
],
|
|
1080
|
-
"fileDiffs": [
|
|
1081
|
-
{
|
|
1082
|
-
"path": "backend/shared/src/helpers/search.ts",
|
|
1083
|
-
"status": "modified",
|
|
1084
|
-
"diff": "Index: backend/shared/src/helpers/search.ts\n===================================================================\n--- backend/shared/src/helpers/search.ts\tba89c4e (parent)\n+++ backend/shared/src/helpers/search.ts\t04a6e7e (commit)\n@@ -1,6 +1,10 @@\n import { log } from 'shared/utils'\n \n+export const normalizeHyphens = (text: string) => {\n+ return text.replace(/[-−–—]/g, '')\n+}\n+\n export const constructPrefixTsQuery = (term: string) => {\n const sanitized = term\n .replace(/'/g, \"''\")\n .replace(/[!&|():*<>]/g, '')\n@@ -10,8 +14,20 @@\n const tokens = sanitized.split(/\\s+/)\n return tokens.join(' & ') + ':*'\n }\n \n+export const constructPrefixTsQueryNormalized = (term: string) => {\n+ const normalizedTerm = normalizeHyphens(term)\n+ const sanitized = normalizedTerm\n+ .replace(/'/g, \"''\")\n+ .replace(/[!&|():*<>]/g, '')\n+ .trim()\n+ log(`Normalized term: \"${sanitized}\"`)\n+ if (sanitized === '') return ''\n+ const tokens = sanitized.split(/\\s+/)\n+ return tokens.join(' & ') + ':*'\n+}\n+\n export const constructIlikeQuery = (term: string) => {\n const sanitized = term\n .replace(/'/g, \"''\")\n .replace(/[_%()<>]/g, '')\n"
|
|
1085
|
-
},
|
|
1086
|
-
{
|
|
1087
|
-
"path": "backend/shared/src/supabase/search-contracts.ts",
|
|
1088
|
-
"status": "modified",
|
|
1089
|
-
"diff": "Index: backend/shared/src/supabase/search-contracts.ts\n===================================================================\n--- backend/shared/src/supabase/search-contracts.ts\tba89c4e (parent)\n+++ backend/shared/src/supabase/search-contracts.ts\t04a6e7e (commit)\n@@ -1,32 +1,35 @@\n import { Contract, isSportsContract } from 'common/contract'\n+import { PROD_MANIFOLD_LOVE_GROUP_SLUG } from 'common/envs/constants'\n+import { GROUP_SCORE_PRIOR } from 'common/feed'\n+import { tsToMillis } from 'common/supabase/utils'\n+import { answerCostTiers, getTierIndexFromLiquidity } from 'common/tier'\n+import { PrivateUser } from 'common/user'\n+import { buildArray, filterDefined } from 'common/util/array'\n+import {\n+ constructPrefixTsQueryNormalized,\n+ normalizeHyphens,\n+} from 'shared/helpers/search'\n+import { getContractPrivacyWhereSQLFilter } from 'shared/supabase/contracts'\n import { createSupabaseDirectClient } from 'shared/supabase/init'\n import {\n from,\n groupBy,\n join,\n leftJoin,\n- limit as sqlLimit,\n limit as lim,\n orderBy,\n renderSql,\n select,\n+ limit as sqlLimit,\n where,\n withClause,\n } from 'shared/supabase/sql-builder'\n-import { getContractPrivacyWhereSQLFilter } from 'shared/supabase/contracts'\n-import { PROD_MANIFOLD_LOVE_GROUP_SLUG } from 'common/envs/constants'\n-import { constructPrefixTsQuery } from 'shared/helpers/search'\n-import { buildArray, filterDefined } from 'common/util/array'\n import {\n buildUserInterestsCache,\n userIdsToAverageTopicConversionScores,\n } from 'shared/topic-interests'\n import { contractColumnsToSelectWithPrefix, log } from 'shared/utils'\n-import { PrivateUser } from 'common/user'\n-import { GROUP_SCORE_PRIOR } from 'common/feed'\n-import { tsToMillis } from 'common/supabase/utils'\n-import { answerCostTiers, getTierIndexFromLiquidity } from 'common/tier'\n \n const DEFAULT_THRESHOLD = 1000\n type TokenInputType = 'CASH' | 'MANA' | 'ALL' | 'CASH_AND_MANA'\n let importanceScoreThreshold: number | undefined = undefined\n@@ -222,9 +225,11 @@\n \n const answersSubQuery = renderSql(\n select('distinct a.contract_id'),\n from('answers a'),\n- where(`a.text_fts @@ websearch_to_tsquery('english_extended', $1)`, [term])\n+ where(`a.text_fts @@ websearch_to_tsquery('english_extended', $1)`, [\n+ normalizeHyphens(term),\n+ ])\n )\n \n const groupsFilter =\n (groupIds?.length || groupId) &&\n@@ -281,24 +286,24 @@\n term.length && [\n searchType === 'prefix' &&\n where(\n `question_fts @@ to_tsquery('english_extended', $1)`,\n- constructPrefixTsQuery(term)\n+ constructPrefixTsQueryNormalized(term)\n ),\n searchType === 'without-stopwords' &&\n where(\n `question_fts @@ websearch_to_tsquery('english_extended', $1)`,\n- term\n+ normalizeHyphens(term)\n ),\n searchType === 'with-stopwords' &&\n where(\n `question_nostop_fts @@ websearch_to_tsquery('english_nostop_with_prefix', $1)`,\n- term\n+ normalizeHyphens(term)\n ),\n searchType === 'description' &&\n where(\n `description_fts @@ websearch_to_tsquery('english_extended', $1)`,\n- term\n+ normalizeHyphens(term)\n ),\n ],\n \n orderBy(getSearchContractSortSQL(sort)),\n"
|
|
1090
|
-
},
|
|
1091
|
-
{
|
|
1092
|
-
"path": "backend/supabase/hyphen-search-migration.sql",
|
|
1093
|
-
"status": "added",
|
|
1094
|
-
"diff": "Index: backend/supabase/hyphen-search-migration.sql\n===================================================================\n--- backend/supabase/hyphen-search-migration.sql\tba89c4e (parent)\n+++ backend/supabase/hyphen-search-migration.sql\t04a6e7e (commit)\n@@ -1,1 +1,57 @@\n-[NEW FILE]\n\\n+-- Create a function to normalize hyphens for search\n+create\n+or replace function normalize_hyphens (input_text text) returns text immutable strict language sql as $$\n+ SELECT regexp_replace(input_text, '[-−–—]', '', 'g');\n+$$;\n+\n+-- Create a custom text search configuration that handles hyphens\n+create text search configuration public.english_hyphen_normalized (\n+ copy = english_extended\n+);\n+\n+-- Replace existing FTS columns with normalized versions\n+-- This approach uses the same column names to avoid breaking existing code\n+-- Drop existing generated columns and recreate with normalization\n+alter table contracts\n+drop column if exists question_fts cascade;\n+\n+alter table contracts\n+add column question_fts tsvector generated always as (\n+ to_tsvector(\n+ 'english_extended'::regconfig,\n+ normalize_hyphens (question)\n+ )\n+) stored;\n+\n+-- Recreate the question_fts index\n+create index question_fts on public.contracts using gin (question_fts);\n+\n+-- Similarly for descriptions - replace the existing column\n+alter table contracts\n+drop column if exists description_fts cascade;\n+\n+alter table contracts\n+add column description_fts tsvector generated always as (\n+ to_tsvector(\n+ 'english_extended'::regconfig,\n+ normalize_hyphens (add_creator_name_to_description (data))\n+ )\n+) stored;\n+\n+-- Recreate the description_fts index\n+create index description_fts on public.contracts using gin (description_fts);\n+\n+-- Also update question_nostop_fts for the 'with-stopwords' search type\n+alter table contracts\n+drop column if exists question_nostop_fts cascade;\n+\n+alter table contracts\n+add column question_nostop_fts tsvector generated always as (\n+ to_tsvector(\n+ 'english_nostop_with_prefix'::regconfig,\n+ normalize_hyphens (question)\n+ )\n+) stored;\n+\n+-- Recreate the question_nostop_fts index\n+create index question_nostop_fts on public.contracts using gin (question_nostop_fts);\n"
|
|
1095
|
-
}
|
|
1096
|
-
]
|
|
1097
|
-
},
|
|
1098
|
-
{
|
|
1099
|
-
"id": "paginate-comment-threads",
|
|
1100
|
-
"sha": "0b36b52e47c0b09eefc0f4c7e62b1ac9dc7f68db",
|
|
1101
|
-
"parentSha": "0b8bb4847e740a0165ee2f9c660778a886e1edd6",
|
|
1102
|
-
"spec": "Implement incremental loading of contract comment threads and refactor contract tabs accordingly across backend, common, and web layers.\n\nBackend\n1) Add two API handlers to serve comment threads:\n- File: backend/api/src/get-comment-threads.ts\n - Implement getContractCommentThreads: APIHandler<'comment-threads'>. Accepts contractId, limit (default 50 on server; schema default 10), and page (default 0). Create SupabaseDirectClient and return getCommentThreads(pg, { contractId, limit, page }).\n - Implement getCommentThread: APIHandler<'comment-thread'>. Accepts contractId and commentId. Create SupabaseDirectClient and return getCommentThreadSupabase(pg, commentId, contractId).\n\n2) Register new routes:\n- File: backend/api/src/routes.ts\n - Import getContractCommentThreads and getCommentThread.\n - Add handlers mapping:\n - 'comment-threads': getContractCommentThreads\n - 'comment-thread': getCommentThread\n - Keep existing handlers intact and ensure imports are ordered/consistent.\n\n3) Add shared Supabase queries for threads:\n- File: backend/shared/src/supabase/contract-comments.ts\n - Implement getCommentThreads(pg, { contractId, limit, page }):\n - Query top-level (parent) comments for the contract where replyToCommentId is null and not deleted, ordered by created_time desc, limited with offset page*limit.\n - Query all replies whose replyToCommentId is in the set of parent comment IDs for that page, excluding deleted.\n - Convert rows with convertContractComment and return an object with { parentComments, replyComments }.\n - Implement getCommentThread(pg, commentId, contractId):\n - Fetch the specified comment within the contract. Determine the parentId: if the comment is a reply, use its replyToCommentId; otherwise use its own id.\n - Resolve parentComment (if the original was a reply fetch its parent; otherwise the same comment).\n - Fetch replyComments for that parent ordered by created_time asc, excluding deleted.\n - Fetch neighboring parent comments to provide context: those created after parentComment (newer parents) and up to a limited set of older parents (e.g., 3) around the parent’s created_time, excluding deleted.\n - If there are any nextParentComments (older parents), fetch nextReplyComments for those parents ordered by created_time asc, excluding deleted.\n - Return { parentComment, replyComments, parentComments, nextParentComments, nextReplyComments }.\n\nCommon\n4) Extend API schema with new endpoints:\n- File: common/src/api/schema.ts\n - Add 'comment-threads' (GET, public, not authed) with props: { contractId: string, limit: number (0..100, default 10), page: number (>=0, default 0) }. Returns { replyComments: ContractComment[]; parentComments: ContractComment[] }.\n - Add 'comment-thread' (GET, public, not authed) with props: { contractId: string; commentId: string }. Returns { parentComment: ContractComment | null; replyComments: ContractComment[]; parentComments: ContractComment[]; nextParentComments: ContractComment[]; nextReplyComments: ContractComment[] }.\n\n5) Fix pinned comments filter to treat JSON booleans as strings and exclude deleted using an OR condition:\n- File: common/src/supabase/comments.ts\n - In getPinnedComments, change .eq('data->>pinned', 'true') and replace the deleted filter with .or('data->>deleted.eq.false,data->>deleted.is.null').\n\nFrontend: hooks and components\n6) Add a hook to page through comment threads:\n- File: web/hooks/use-comments.ts\n - Implement useCommentThreads(contractId: string, limit: number, disabled: boolean) that manages { threads, loadMore, loading }.\n - On loadMore, call api('comment-threads', { contractId, limit, page }) and group replies by replyToCommentId to assemble an array of threads { parent, replies }, append to state, and increment page. Auto-load on mount when not disabled.\n\n7) Extract CommentsTabContent into its own module and update logic to use paged threads:\n- File: web/components/contract/comments-tab-content.tsx\n - Accept props: { staticContract, liveContract, comments, blockedUserIds, setCommentsLength?, replyTo?, clearReply?, className?, highlightCommentId?, pinnedComments, scrollToEnd? }.\n - Use useCommentThreads(staticContract.id, 10, !!highlightCommentId) to fetch threads lazily. Subscribe to new comments via useSubscribeNewComments.\n - When highlightCommentId is provided but not present locally, fetch api('comment-thread', { contractId, commentId }) and populate highlighted threads and surrounding context (parent, replies, nearby parent threads and their replies).\n - Merge static comments, subscribed new comments, paged threads, and highlighted threads, de-duplicate by id, and filter out blocked users.\n - Preserve existing sorting options (Best/Newest and Yes/No for binary) and pinned comments rendering. Update total length via setCommentsLength and render threads via ParentFeedComment and FeedCommentThread. Use VisibilityObserver to call loadMore on scroll. Show a LoadingIndicator when loading highlighted items or initial pages.\n\n8) Extract BetsTabContent into its own module:\n- File: web/components/contract/bets-tab-content.tsx\n - Accept props: { contract, bets, totalBets, setReplyToBet? }.\n - Preserve existing behavior: min-amount filter dropdown, listening for order updates, lazy loading more bets with api('bets', ...) when scrolled, rendering FeedBet, MultiNumericBetGroup, and FeedLiquidity entries, and showing skeleton loading rows.\n\n9) Update importing components to use the extracted modules:\n- File: web/components/contract/contract-tabs.tsx\n - Remove inline definitions of CommentsTabContent and BetsTabContent. Import and use the new components to preserve tab behavior, counts, and tracking.\n- File: web/components/comments/comments-button.tsx\n - Update import to use CommentsTabContent from web/components/contract/comments-tab-content.\n- File: web/components/contract/trades-button.tsx\n - Update import to use BetsTabContent from web/components/contract/bets-tab-content.\n\nAcceptance criteria\n- Visiting a contract page loads an initial page of parent comments and their replies via GET /v0/comment-threads, and automatically loads additional pages as the user scrolls down.\n- Sorting options and pinned comments continue to work; comments from blocked users are excluded.\n- Visiting a contract with a URL hash or state indicating a specific commentId results in that thread being shown even if it wasn’t in the first page, by fetching GET /v0/comment-thread and including contextual neighbors.\n- The pinned comments Supabase query returns only non-deleted pinned comments.\n- Bets tab content behaves as before, now sourced from the extracted component.\n- New endpoints are documented in the API schema and registered in backend routes.",
|
|
1103
|
-
"prompt": "Add lazy-loaded comment threads to the contract page. Create backend endpoints to fetch parent comments with their replies in pages, and an endpoint to fetch a specific comment’s thread and nearby context by commentId. Update the shared schema and routes. On the frontend, add a hook to load comment threads page-by-page and refactor the contract page to use it so comments load as users scroll, still supporting sorting, pinned comments, blocking, and highlighting a specific comment. Extract the comments and bets tab content into dedicated components and update imports accordingly. Also ensure pinned comments are filtered correctly for deleted items.",
|
|
1104
|
-
"supplementalFiles": [
|
|
1105
|
-
"backend/api/src/helpers/endpoint.ts",
|
|
1106
|
-
"backend/shared/src/supabase/init.ts",
|
|
1107
|
-
"web/lib/api/api.ts",
|
|
1108
|
-
"common/src/comment.ts",
|
|
1109
|
-
"web/components/comments/comment-thread.tsx",
|
|
1110
|
-
"web/components/comments/comment.tsx",
|
|
1111
|
-
"web/components/widgets/visibility-observer.tsx",
|
|
1112
|
-
"web/hooks/use-liquidity.ts",
|
|
1113
|
-
"web/components/feed/feed-bets.tsx",
|
|
1114
|
-
"web/components/feed/feed-liquidity.tsx"
|
|
1115
|
-
],
|
|
1116
|
-
"fileDiffs": [
|
|
1117
|
-
{
|
|
1118
|
-
"path": "backend/api/src/get-comment-threads.ts",
|
|
1119
|
-
"status": "added",
|
|
1120
|
-
"diff": "Index: backend/api/src/get-comment-threads.ts\n===================================================================\n--- backend/api/src/get-comment-threads.ts\t0b8bb48 (parent)\n+++ backend/api/src/get-comment-threads.ts\t0b36b52 (commit)\n@@ -0,0 +1,24 @@\n+import {\n+ getCommentThreads,\n+ getCommentThread as getCommentThreadSupabase,\n+} from 'shared/supabase/contract-comments'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { type APIHandler } from './helpers/endpoint'\n+\n+export const getContractCommentThreads: APIHandler<'comment-threads'> = async (\n+ props\n+) => {\n+ const { contractId, limit, page } = props\n+ const pg = createSupabaseDirectClient()\n+ return await getCommentThreads(pg, {\n+ contractId,\n+ limit: limit ?? 50,\n+ page: page ?? 0,\n+ })\n+}\n+\n+export const getCommentThread: APIHandler<'comment-thread'> = async (props) => {\n+ const { contractId, commentId } = props\n+ const pg = createSupabaseDirectClient()\n+ return await getCommentThreadSupabase(pg, commentId, contractId)\n+}\n"
|
|
1121
|
-
},
|
|
1122
|
-
{
|
|
1123
|
-
"path": "backend/api/src/routes.ts",
|
|
1124
|
-
"status": "modified",
|
|
1125
|
-
"diff": "Index: backend/api/src/routes.ts\n===================================================================\n--- backend/api/src/routes.ts\t0b8bb48 (parent)\n+++ backend/api/src/routes.ts\t0b36b52 (commit)\n@@ -1,182 +1,186 @@\n-import { updateMe } from './update-me'\n-import { placeBet } from './place-bet'\n-import { cancelBet } from './cancel-bet'\n-import { sellShares } from './sell-shares'\n-import { createMarket } from './create-market'\n-import { createComment } from './create-comment'\n-import { resolveMarket } from './resolve-market'\n-import { closeMarket } from './close-market'\n-import { getMe } from './get-me'\n-import { saveTwitchCredentials } from './save-twitch-credentials'\n-import { addLiquidity } from './add-liquidity'\n-import { removeLiquidity } from './remove-liquidity'\n-import { searchGroups, searchMyGroups } from './search-groups'\n-import { awardBounty } from './award-bounty'\n-import { addBounty } from './add-bounty'\n-import { createAnswerCPMM } from './create-answer-cpmm'\n-import { managram } from './managram'\n-import { setnews } from './set-news'\n-import { getDashboardFromSlug } from './get-dashboard-from-slug'\n-import { unresolve } from './unresolve'\n-import { updateMarket } from 'api/update-market'\n-import { type APIPath } from 'common/api/schema'\n-import { getMarkets } from 'api/markets'\n-import { hideComment } from './hide-comment'\n-import { pinComment } from './pin-comment'\n-import { getManagrams } from './get-managrams'\n-import { getGroups } from './get-groups'\n-import { getComments } from './get-comments'\n-import { getBetPointsBetween, getBets } from './get-bets'\n-import { getLiteUser, getUser } from './get-user'\n-import { getUsers } from './get-users'\n-import { getUserBalancesByIds, getUsersByIds } from './get-users-by-ids'\n-import { getMarket } from './get-market'\n-import { getMarketProb } from './get-market-prob'\n-import { getMarketProbs } from './get-market-probs'\n-import { getGroup } from './get-group'\n-import { getPositions } from './get-positions'\n-import { getLeagues } from './get-leagues'\n-import { getContract } from './get-contract'\n-import { getSingleAnswer } from './get-answer'\n-import { getContractAnswers } from './get-contract-answers'\n-import { addOrRemoveTopicFromContract } from './add-topic-to-market'\n-import { addOrRemoveTopicFromTopic } from './add-topic-to-topic'\n-import { searchUsers } from './search-users'\n-import { searchMarketsLite, searchMarketsFull } from './search-contracts'\n-import { post } from 'api/post'\n-import { fetchLinkPreview } from './fetch-link-preview'\n-import { type APIHandler } from './helpers/endpoint'\n-import { requestLoan } from 'api/request-loan'\n-import { getHeadlines, getPoliticsHeadlines } from './get-headlines'\n-import { getBoostAnalytics } from 'api/get-boost-analytics'\n-import { addOrRemoveReaction } from './reaction'\n-import { createManalink } from './create-manalink'\n-import { unlistAndCancelUserContracts } from './unlist-and-cancel-user-contracts'\n-import { getGroupsWithTopContracts } from 'api/get-topics-with-markets'\n-import { getBalanceChanges } from 'api/get-balance-changes'\n-import { placeMultiBet } from 'api/place-multi-bet'\n-import { getPartnerStats } from './get-partner-stats'\n-import { getSeenMarketIds } from 'api/get-seen-market-ids'\n-import { recordContractView } from 'api/record-contract-view'\n import { createPublicChatMessage } from 'api/create-public-chat-message'\n-import { getFollowedGroups } from './get-followed-groups'\n-import { getUniqueBetGroupCount } from 'api/get-unique-bet-groups'\n-import { deleteGroup } from './delete-group'\n-import { recordContractInteraction } from 'api/record-contract-interaction'\n-import { getUserPortfolio } from './get-user-portfolio'\n import { createuser } from 'api/create-user'\n-import { verifyPhoneNumber } from 'api/verify-phone-number'\n-import { requestOTP } from 'api/request-phone-otp'\n-import { multiSell } from 'api/multi-sell'\n-import { convertCashToMana } from './convert-cash-to-mana'\n-import { convertSpiceToMana } from './convert-sp-to-mana'\n-import { donate } from './donate'\n+import { getBalanceChanges } from 'api/get-balance-changes'\n+import { getBestComments } from 'api/get-best-comments'\n+import { getBoostAnalytics } from 'api/get-boost-analytics'\n import { getFeed } from 'api/get-feed'\n-import { getManaSupply } from './get-mana-supply'\n-import { getUserPortfolioHistory } from './get-user-portfolio-history'\n-import { deleteMe } from './delete-me'\n-import { updateModReport } from './update-mod-report'\n-import { getModReports } from './get-mod-reports'\n-import { searchContractPositions } from 'api/search-contract-positions'\n-import { blockUser, unblockUser } from './block-user'\n-import { blockGroup, unblockGroup } from './block-group'\n-import { blockMarket, unblockMarket } from './block-market'\n-import { getTxnSummaryStats } from 'api/get-txn-summary-stats'\n+import { getInterestingGroupsFromViews } from 'api/get-interesting-groups-from-views'\n import { getManaSummaryStats } from 'api/get-mana-summary-stats'\n-import { register } from 'api/gidx/register'\n-import { uploadDocument } from 'api/gidx/upload-document'\n-import { getVerificationStatus } from 'api/gidx/get-verification-status'\n-import { getCurrentPrivateUser } from './get-current-private-user'\n-import { updatePrivateUser } from './update-private-user'\n-import { setPushToken } from './push-token'\n-import { updateNotifSettings } from './update-notif-settings'\n-import { getVerificationDocuments } from 'api/gidx/get-verification-documents'\n-import { getMonitorStatus } from 'api/gidx/get-monitor-status'\n-import { getBestComments } from 'api/get-best-comments'\n-import { recordCommentView } from 'api/record-comment-view'\n+import { getNotifications } from 'api/get-notifications'\n import {\n getChannelMemberships,\n getChannelMessages,\n getLastSeenChannelTime,\n setChannelLastSeenTime,\n } from 'api/get-private-messages'\n-import { getNotifications } from 'api/get-notifications'\n-import { getCheckoutSession } from 'api/gidx/get-checkout-session'\n-import { completeCheckoutSession } from 'api/gidx/complete-checkout-session'\n-import { getContractTopics } from './get-contract-topics'\n-import { getRelatedMarkets } from './get-related-markets'\n-import { getRelatedMarketsByGroup } from './get-related-markets-by-group'\n-import { followContract } from './follow-contract'\n+import { getSeenMarketIds } from 'api/get-seen-market-ids'\n+import { getGroupsWithTopContracts } from 'api/get-topics-with-markets'\n+import { getTxnSummaryStats } from 'api/get-txn-summary-stats'\n+import { getUniqueBetGroupCount } from 'api/get-unique-bet-groups'\n import { getUserLimitOrdersWithContracts } from 'api/get-user-limit-orders-with-contracts'\n-import { getInterestingGroupsFromViews } from 'api/get-interesting-groups-from-views'\n import { completeCashoutSession } from 'api/gidx/complete-cashout-session'\n+import { completeCheckoutSession } from 'api/gidx/complete-checkout-session'\n+import { getCheckoutSession } from 'api/gidx/get-checkout-session'\n+import { getMonitorStatus } from 'api/gidx/get-monitor-status'\n+import { getVerificationDocuments } from 'api/gidx/get-verification-documents'\n+import { getVerificationStatus } from 'api/gidx/get-verification-status'\n+import { register } from 'api/gidx/register'\n+import { uploadDocument } from 'api/gidx/upload-document'\n+import { getMarkets } from 'api/markets'\n+import { multiSell } from 'api/multi-sell'\n+import { placeMultiBet } from 'api/place-multi-bet'\n+import { post } from 'api/post'\n+import { recordCommentView } from 'api/record-comment-view'\n+import { recordContractInteraction } from 'api/record-contract-interaction'\n+import { recordContractView } from 'api/record-contract-view'\n+import { requestLoan } from 'api/request-loan'\n+import { requestOTP } from 'api/request-phone-otp'\n+import { searchContractPositions } from 'api/search-contract-positions'\n+import { updateMarket } from 'api/update-market'\n+import { verifyPhoneNumber } from 'api/verify-phone-number'\n+import { type APIPath } from 'common/api/schema'\n+import { addBounty } from './add-bounty'\n+import { addLiquidity } from './add-liquidity'\n+import { addOrRemoveTopicFromContract } from './add-topic-to-market'\n+import { addOrRemoveTopicFromTopic } from './add-topic-to-topic'\n+import { awardBounty } from './award-bounty'\n+import { blockGroup, unblockGroup } from './block-group'\n+import { blockMarket, unblockMarket } from './block-market'\n+import { blockUser, unblockUser } from './block-user'\n+import { cancelBet } from './cancel-bet'\n+import { checkSportsEvent } from './check-sports-event'\n+import { closeMarket } from './close-market'\n+import { convertCashToMana } from './convert-cash-to-mana'\n+import { convertSpiceToMana } from './convert-sp-to-mana'\n+import { createAnswerCPMM } from './create-answer-cpmm'\n+import { createComment } from './create-comment'\n+import { createManalink } from './create-manalink'\n+import { createMarket } from './create-market'\n+import { deleteGroup } from './delete-group'\n+import { deleteMe } from './delete-me'\n+import { donate } from './donate'\n+import { fetchLinkPreview } from './fetch-link-preview'\n+import { followContract } from './follow-contract'\n+import { generateAIAnswers } from './generate-ai-answers'\n+import { generateAIDescription } from './generate-ai-description'\n+import { generateAIMarketSuggestions } from './generate-ai-market-suggestions'\n+import { getSingleAnswer } from './get-answer'\n+import { getBetPointsBetween, getBets } from './get-bets'\n import { getCashouts } from './get-cashouts'\n-import { getTxns } from './get-txns'\n-import { refreshAllClients } from './refresh-all-clients'\n-import { getLeaderboard } from './get-leaderboard'\n-import { toggleSystemTradingStatus } from './toggle-system-status'\n-import { completeCashoutRequest } from './gidx/complete-cashout-request'\n+import {\n+ getCommentThread,\n+ getContractCommentThreads,\n+} from './get-comment-threads'\n+import { getComments, getUserComments } from './get-comments'\n+import { getContract } from './get-contract'\n+import { getContractAnswers } from './get-contract-answers'\n+import { getContractTopics } from './get-contract-topics'\n+import { getCurrentPrivateUser } from './get-current-private-user'\n import { getDailyChangedMetricsAndContracts } from './get-daily-changed-metrics-and-contracts'\n+import { getDashboardFromSlug } from './get-dashboard-from-slug'\n+import { getFollowedGroups } from './get-followed-groups'\n+import { getGroup } from './get-group'\n+import { getGroups } from './get-groups'\n+import { getHeadlines, getPoliticsHeadlines } from './get-headlines'\n+import { getLeaderboard } from './get-leaderboard'\n+import { getLeagues } from './get-leagues'\n+import { getManaSupply } from './get-mana-supply'\n+import { getManagrams } from './get-managrams'\n+import { getMarket } from './get-market'\n+import { getMarketProb } from './get-market-prob'\n+import { getMarketProbs } from './get-market-probs'\n import { getMarketsByIds } from './get-markets'\n-import { getTopicTopics } from './get-topic-topics'\n-import { getTopicDashboards } from './get-topic-dashboards'\n-import { generateAIMarketSuggestions } from './generate-ai-market-suggestions'\n-import { generateAIDescription } from './generate-ai-description'\n-import { generateAIAnswers } from './generate-ai-answers'\n-import { getmonthlybets2024 } from './get-monthly-bets-2024'\n import { getmaxminprofit2024 } from './get-max-min-profit-2024'\n+import { getMe } from './get-me'\n+import { getModReports } from './get-mod-reports'\n+import { getmonthlybets2024 } from './get-monthly-bets-2024'\n import { getNextLoanAmount } from './get-next-loan-amount'\n-import { checkSportsEvent } from './check-sports-event'\n+import { getPartnerStats } from './get-partner-stats'\n+import { getPositions } from './get-positions'\n+import { getRelatedMarkets } from './get-related-markets'\n+import { getRelatedMarketsByGroup } from './get-related-markets-by-group'\n+import { getTopicDashboards } from './get-topic-dashboards'\n+import { getTopicTopics } from './get-topic-topics'\n+import { getTxns } from './get-txns'\n+import { getLiteUser, getUser } from './get-user'\n+import { getUserPortfolio } from './get-user-portfolio'\n+import { getUserPortfolioHistory } from './get-user-portfolio-history'\n+import { getUsers } from './get-users'\n+import { getUserBalancesByIds, getUsersByIds } from './get-users-by-ids'\n+import { completeCashoutRequest } from './gidx/complete-cashout-request'\n+import { type APIHandler } from './helpers/endpoint'\n+import { hideComment } from './hide-comment'\n+import { managram } from './managram'\n+import { pinComment } from './pin-comment'\n+import { placeBet } from './place-bet'\n+import { setPushToken } from './push-token'\n+import { addOrRemoveReaction } from './reaction'\n+import { refreshAllClients } from './refresh-all-clients'\n+import { removeLiquidity } from './remove-liquidity'\n+import { resolveMarket } from './resolve-market'\n+import { saveTwitchCredentials } from './save-twitch-credentials'\n+import { searchMarketsFull, searchMarketsLite } from './search-contracts'\n+import { searchGroups, searchMyGroups } from './search-groups'\n+import { searchUsers } from './search-users'\n+import { sellShares } from './sell-shares'\n+import { setnews } from './set-news'\n+import { toggleSystemTradingStatus } from './toggle-system-status'\n+import { unlistAndCancelUserContracts } from './unlist-and-cancel-user-contracts'\n+import { unresolve } from './unresolve'\n+import { updateMe } from './update-me'\n+import { updateModReport } from './update-mod-report'\n+import { updateNotifSettings } from './update-notif-settings'\n+import { updatePrivateUser } from './update-private-user'\n \n-import { createTask } from './create-task'\n-import { updateTask } from './update-task'\n import { createCategory } from './create-category'\n+import { createTask } from './create-task'\n import { getCategories } from './get-categories'\n-import { updateCategory } from './update-category'\n import { getTasks } from './get-tasks'\n+import { updateCategory } from './update-category'\n+import { updateTask } from './update-task'\n \n-import { getSiteActivity } from './get-site-activity'\n-import { isSportsInterested } from './is-sports-bettor'\n-import { getSportsGames } from './get-sports-games'\n-import { getMarketProps } from './get-market-props'\n-import { getUserContractMetricsWithContracts } from './get-user-contract-metrics-with-contracts'\n-import { validateiap } from './validate-iap'\n-import { getReactions } from './get-reactions'\n-import { markallnotificationsnew } from './mark-all-notifications-new'\n+import { createPost } from './create-post'\n+import { createPostComment } from './create-post-comment'\n+import { dismissUserReport } from './dismiss-user-report'\n+import { editPostComment, updatePostComment } from './edit-post-comment'\n+import { followPost } from './follow-post'\n import {\n- getContractOptionVoters,\n- getContractVoters,\n-} from './get-contract-voters'\n-import { purchaseContractBoost } from './purchase-boost'\n+ generateAIDateRanges,\n+ regenerateDateMidpoints,\n+} from './generate-ai-date-ranges'\n import {\n generateAINumericRanges,\n regenerateNumericMidpoints,\n } from './generate-ai-numeric-ranges'\n-import {\n- generateAIDateRanges,\n- regenerateDateMidpoints,\n-} from './generate-ai-date-ranges'\n-import { inferNumericUnit } from './infer-numeric-unit'\n import { generateConciseTitle } from './generate-concise-title'\n import { getCloseDateEndpoint } from './get-close-date'\n-import { referUser } from './refer-user'\n import {\n- saveMarketDraft,\n- getMarketDrafts,\n- deleteMarketDraft,\n-} from './market-drafts'\n+ getContractOptionVoters,\n+ getContractVoters,\n+} from './get-contract-voters'\n+import { getMarketProps } from './get-market-props'\n+import { getPosts } from './get-posts'\n+import { getReactions } from './get-reactions'\n import { getSeasonInfo } from './get-season-info'\n+import { getSiteActivity } from './get-site-activity'\n+import { getSportsGames } from './get-sports-games'\n+import { getUserContractMetricsWithContracts } from './get-user-contract-metrics-with-contracts'\n+import { getUserLastActiveTime } from './get-user-last-active-time'\n+import { inferNumericUnit } from './infer-numeric-unit'\n+import { isSportsInterested } from './is-sports-bettor'\n import { markNotificationRead } from './mark-all-notifications'\n-import { createPostComment } from './create-post-comment'\n-import { createPost } from './create-post'\n+import { markallnotificationsnew } from './mark-all-notifications-new'\n+import {\n+ deleteMarketDraft,\n+ getMarketDrafts,\n+ saveMarketDraft,\n+} from './market-drafts'\n+import { purchaseContractBoost } from './purchase-boost'\n+import { referUser } from './refer-user'\n import { updatePost } from './update-post'\n-import { getPosts } from './get-posts'\n-import { dismissUserReport } from './dismiss-user-report'\n-import { followPost } from './follow-post'\n-import { editPostComment, updatePostComment } from './edit-post-comment'\n-import { getUserComments } from './get-comments'\n-import { getUserLastActiveTime } from './get-user-last-active-time'\n+import { validateiap } from './validate-iap'\n+\n export const handlers: { [k in APIPath]: APIHandler<k> } = {\n 'refresh-all-clients': refreshAllClients,\n bet: placeBet,\n 'multi-bet': placeMultiBet,\n@@ -191,8 +195,10 @@\n 'get-channel-seen-time': getLastSeenChannelTime,\n 'set-channel-seen-time': setChannelLastSeenTime,\n 'get-contract': getContract,\n comment: createComment,\n+ 'comment-threads': getContractCommentThreads,\n+ 'comment-thread': getCommentThread,\n 'hide-comment': hideComment,\n 'pin-comment': pinComment,\n comments: getComments,\n market: createMarket,\n"
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
"path": "backend/shared/src/supabase/contract-comments.ts",
|
|
1129
|
-
"status": "modified",
|
|
1130
|
-
"diff": "Index: backend/shared/src/supabase/contract-comments.ts\n===================================================================\n--- backend/shared/src/supabase/contract-comments.ts\t0b8bb48 (parent)\n+++ backend/shared/src/supabase/contract-comments.ts\t0b36b52 (commit)\n@@ -1,9 +1,9 @@\n-import { convertContractComment } from 'common/supabase/comments'\n-import { SupabaseDirectClient } from 'shared/supabase/init'\n import { APIError } from 'common/api/utils'\n-import { millisToTs } from 'common/supabase/utils'\n import { ContractComment, PostComment } from 'common/comment'\n+import { convertContractComment } from 'common/supabase/comments'\n+import { millisToTs } from 'common/supabase/utils'\n+import { SupabaseDirectClient } from 'shared/supabase/init'\n \n export async function getCommentSafe(\n pg: SupabaseDirectClient,\n commentId: string\n@@ -26,8 +26,145 @@\n }\n return comment\n }\n \n+export async function getCommentThreads(\n+ pg: SupabaseDirectClient,\n+ filters: {\n+ contractId: string\n+ limit: number\n+ page: number\n+ }\n+) {\n+ const { contractId, limit, page } = filters\n+\n+ const allComments = await pg.map(\n+ `\n+ with parent_comments as (\n+ select cc.data, cc.likes, cc.comment_id from contract_comments cc\n+ where cc.contract_id = $1\n+ and (cc.data->>'replyToCommentId' is null)\n+ and (cc.data->>'deleted' is null or cc.data->>'deleted' = 'false')\n+ order by cc.created_time desc\n+ limit $2\n+ offset $3\n+ )\n+ select * from parent_comments\n+ union all\n+ select cc.data, cc.likes, cc.comment_id from contract_comments cc\n+ where cc.contract_id = $1\n+ and (cc.data->>'replyToCommentId' in (select comment_id from parent_comments))\n+ and (cc.data->>'deleted' is null or cc.data->>'deleted' = 'false')\n+ `,\n+ [contractId, limit, page * limit],\n+ convertContractComment\n+ )\n+\n+ const parentComments = allComments.filter((c) => !c.replyToCommentId)\n+ const replyComments = allComments.filter((c) => c.replyToCommentId)\n+ console.log('parentComments', parentComments.length)\n+ console.log('replyComments', replyComments.length)\n+\n+ return { parentComments, replyComments }\n+}\n+\n+export async function getCommentThread(\n+ pg: SupabaseDirectClient,\n+ commentId: string,\n+ contractId: string\n+) {\n+ const comment = await pg.oneOrNone(\n+ `\n+ select cc.data, cc.likes from contract_comments cc\n+ where cc.comment_id = $1 and cc.contract_id = $2\n+ `,\n+ [commentId, contractId],\n+ convertContractComment\n+ )\n+ if (!comment) {\n+ return {\n+ parentComment: null,\n+ replyComments: [],\n+ parentComments: [],\n+ nextParentComments: [],\n+ nextReplyComments: [],\n+ }\n+ }\n+\n+ const parentId = comment.replyToCommentId ?? comment.id\n+ const parentComment =\n+ comment.replyToCommentId && comment.replyToCommentId !== comment.id\n+ ? await pg.oneOrNone(\n+ `\n+ select cc.data, cc.likes from contract_comments cc\n+ where cc.comment_id = $1 and cc.contract_id = $2\n+ `,\n+ [parentId, contractId],\n+ convertContractComment\n+ )\n+ : comment\n+\n+ if (!parentComment) {\n+ return {\n+ parentComment: null,\n+ replyComments: [],\n+ parentComments: [],\n+ nextParentComments: [],\n+ nextReplyComments: [],\n+ }\n+ }\n+\n+ const results = await pg.multi(\n+ `\n+ select cc.data, cc.likes from contract_comments cc\n+ where cc.contract_id = $1\n+ and (cc.data->>'replyToCommentId' = $2)\n+ and (cc.data->>'deleted' is null or cc.data->>'deleted' = 'false')\n+ order by cc.created_time asc;\n+ select cc.data, cc.likes from contract_comments cc\n+ where cc.contract_id = $1\n+ and (cc.data->>'replyToCommentId' is null)\n+ and (cc.data->>'deleted' is null or cc.data->>'deleted' = 'false')\n+ and cc.created_time > $3\n+ order by cc.created_time desc;\n+ select cc.data, cc.likes from contract_comments cc\n+ where cc.contract_id = $1\n+ and (cc.data->>'replyToCommentId' is null)\n+ and (cc.data->>'deleted' is null or cc.data->>'deleted' = 'false')\n+ and cc.created_time < $3\n+ order by cc.created_time desc\n+ limit 3;\n+ `,\n+ [contractId, parentId, millisToTs(parentComment.createdTime)]\n+ )\n+ const replyComments = results[0].map(convertContractComment)\n+ const parentComments = results[1].map(convertContractComment)\n+ const nextParentComments = results[2].map(convertContractComment)\n+\n+ const nextReplyComments =\n+ nextParentComments.length > 0\n+ ? await pg.map(\n+ `\n+ select cc.data, cc.likes from contract_comments cc\n+ where cc.contract_id = $1\n+ and (cc.data->>'replyToCommentId' in ($2:csv))\n+ and (cc.data->>'deleted' is null or cc.data->>'deleted' = 'false')\n+ order by cc.created_time asc\n+ `,\n+ [contractId, nextParentComments.map((c) => c.id)],\n+ convertContractComment\n+ )\n+ : []\n+\n+ return {\n+ parentComment,\n+ replyComments,\n+ parentComments,\n+ nextParentComments,\n+ nextReplyComments,\n+ }\n+}\n+\n export async function getCommentsDirect(\n pg: SupabaseDirectClient,\n filters: {\n userId?: string\n"
|
|
1131
|
-
},
|
|
1132
|
-
{
|
|
1133
|
-
"path": "common/src/api/schema.ts",
|
|
1134
|
-
"status": "modified",
|
|
1135
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\t0b8bb48 (parent)\n+++ common/src/api/schema.ts\t0b36b52 (commit)\n@@ -951,8 +951,43 @@\n order: z.enum(['shares', 'profit']).optional(),\n })\n .strict(),\n },\n+ 'comment-threads': {\n+ method: 'GET',\n+ visibility: 'public',\n+ authed: false,\n+ props: z\n+ .object({\n+ contractId: z.string(),\n+ limit: z.coerce.number().gte(0).lte(100).default(10),\n+ page: z.coerce.number().gte(0).default(0),\n+ })\n+ .strict(),\n+ cache: DEFAULT_CACHE_STRATEGY,\n+ returns: {} as {\n+ replyComments: ContractComment[]\n+ parentComments: ContractComment[]\n+ },\n+ },\n+ 'comment-thread': {\n+ method: 'GET',\n+ visibility: 'public',\n+ authed: false,\n+ props: z\n+ .object({\n+ contractId: z.string(),\n+ commentId: z.string(),\n+ })\n+ .strict(),\n+ returns: {} as {\n+ parentComment: ContractComment | null\n+ replyComments: ContractComment[]\n+ parentComments: ContractComment[]\n+ nextParentComments: ContractComment[]\n+ nextReplyComments: ContractComment[]\n+ },\n+ },\n me: {\n method: 'GET',\n visibility: 'public',\n authed: true,\n"
|
|
1136
|
-
},
|
|
1137
|
-
{
|
|
1138
|
-
"path": "common/src/supabase/comments.ts",
|
|
1139
|
-
"status": "modified",
|
|
1140
|
-
"diff": "Index: common/src/supabase/comments.ts\n===================================================================\n--- common/src/supabase/comments.ts\t0b8bb48 (parent)\n+++ common/src/supabase/comments.ts\t0b36b52 (commit)\n@@ -60,10 +60,10 @@\n db\n .from('contract_comments')\n .select('data')\n .eq('contract_id', contractId)\n- .eq('data->>pinned', true)\n- .not('data->>deleted', 'eq', 'true')\n+ .eq('data->>pinned', 'true')\n+ .or('data->>deleted.eq.false,data->>deleted.is.null')\n .order('created_time', { ascending: false })\n )\n \n const pinnedComments = data.map((c) => c.data as ContractComment)\n"
|
|
1141
|
-
},
|
|
1142
|
-
{
|
|
1143
|
-
"path": "web/components/comments/comments-button.tsx",
|
|
1144
|
-
"status": "modified",
|
|
1145
|
-
"diff": "Index: web/components/comments/comments-button.tsx\n===================================================================\n--- web/components/comments/comments-button.tsx\t0b8bb48 (parent)\n+++ web/components/comments/comments-button.tsx\t0b36b52 (commit)\n@@ -3,9 +3,8 @@\n import clsx from 'clsx'\n import { Contract } from 'common/contract'\n import { Modal, SCROLLABLE_MODAL_CLASS } from '../layout/modal'\n import { Col } from '../layout/col'\n-import { CommentsTabContent } from '../contract/contract-tabs'\n import { usePrivateUser } from 'web/hooks/use-user'\n import { track } from 'web/lib/service/analytics'\n import { Tooltip } from '../widgets/tooltip'\n import { User } from 'common/user'\n@@ -14,8 +13,9 @@\n useNumContractComments,\n } from 'web/hooks/use-comments'\n import { Button } from 'web/components/buttons/button'\n import { Row } from '../layout/row'\n+import { CommentsTabContent } from 'web/components/contract/comments-tab-content'\n \n export function CommentsButton(props: {\n contract: Contract\n user: User | null | undefined\n"
|
|
1146
|
-
},
|
|
1147
|
-
{
|
|
1148
|
-
"path": "web/components/contract/bets-tab-content.tsx",
|
|
1149
|
-
"status": "added",
|
|
1150
|
-
"diff": "Index: web/components/contract/bets-tab-content.tsx\n===================================================================\n--- web/components/contract/bets-tab-content.tsx\t0b8bb48 (parent)\n+++ web/components/contract/bets-tab-content.tsx\t0b36b52 (commit)\n@@ -0,0 +1,234 @@\n+import { memo, useEffect, useRef, useState } from 'react'\n+import { Contract, CPMMNumericContract, MarketContract } from 'common/contract'\n+import { Bet } from 'common/bet'\n+import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n+import { listenToOrderUpdates } from 'client-common/hooks/use-bets'\n+import { groupBy, minBy, sortBy, uniqBy } from 'lodash'\n+import { useLiquidity } from 'web/hooks/use-liquidity'\n+import {\n+ DEV_HOUSE_LIQUIDITY_PROVIDER_ID,\n+ HOUSE_LIQUIDITY_PROVIDER_ID,\n+} from 'common/antes'\n+import { useEvent } from 'client-common/hooks/use-event'\n+import { api } from 'web/lib/api/api'\n+import { Row } from 'web/components/layout/row'\n+import DropdownMenu from 'web/components/widgets/dropdown-menu'\n+import generateFilterDropdownItems from 'web/components/search/search-dropdown-helpers'\n+import { track } from 'web/lib/service/analytics'\n+import { ChevronDownIcon } from '@heroicons/react/solid'\n+import { Col } from 'web/components/layout/col'\n+import { FeedBet } from 'web/components/feed/feed-bets'\n+import { MultiNumericBetGroup } from 'web/components/feed/feed-multi-numeric-bet-group'\n+import { FeedLiquidity } from 'web/components/feed/feed-liquidity'\n+import { LoadMoreUntilNotVisible } from 'web/components/widgets/visibility-observer'\n+\n+export const BetsTabContent = memo(function BetsTabContent(props: {\n+ contract: Contract\n+ bets: Bet[]\n+ totalBets: number\n+ setReplyToBet?: (bet: Bet) => void\n+}) {\n+ const { contract, setReplyToBet, totalBets } = props\n+ const { outcomeType } = contract\n+ const [olderBets, setOlderBets] = useState<Bet[]>([])\n+\n+ const [minAmountFilterIndex, setMinAmountFilterIndex] =\n+ usePersistentInMemoryState(0, `bet-amount-filter-${contract.id}`)\n+ const isNumber = outcomeType === 'NUMBER'\n+\n+ // Min amount filter options\n+ const minAmountOptions = [\n+ { label: 'Any amount', value: undefined },\n+ { label: 'M$100+', value: 100 },\n+ { label: 'M$1,000+', value: 1000 },\n+ { label: 'M$10,000+', value: 10000 },\n+ ]\n+ const selectedMinAmount = minAmountOptions[minAmountFilterIndex].value\n+\n+ // Filter initial bets on client side, server will filter olderBets\n+ const filteredInitialBets = selectedMinAmount\n+ ? props.bets.filter((bet) => Math.abs(bet.amount) >= selectedMinAmount)\n+ : props.bets\n+\n+ const bets = [...filteredInitialBets, ...olderBets]\n+ listenToOrderUpdates(contract.id, setOlderBets, true)\n+\n+ const oldestBet = minBy(bets, (b) => b.createdTime)\n+\n+ const lps = useLiquidity(contract.id) ?? []\n+ const visibleLps = lps.filter(\n+ (l) =>\n+ !l.isAnte &&\n+ l.userId !== HOUSE_LIQUIDITY_PROVIDER_ID &&\n+ l.userId !== DEV_HOUSE_LIQUIDITY_PROVIDER_ID &&\n+ l.amount > 0 &&\n+ !minAmountFilterIndex\n+ )\n+ const betsByBetGroupId = isNumber\n+ ? groupBy(bets, (bet) => bet.betGroupId ?? bet.id)\n+ : {}\n+ const groupedBets = Object.values(betsByBetGroupId)\n+\n+ const items = [\n+ ...(isNumber\n+ ? groupedBets.map((bets) => ({\n+ type: 'betGroup' as const,\n+ id: 'bets-tab-' + bets[0].betGroupId,\n+ bets,\n+ }))\n+ : bets.map((bet) => ({\n+ type: 'bet' as const,\n+ id: 'bets-tab-' + bet.id + '-' + 'false',\n+ bet,\n+ }))),\n+ ...visibleLps.map((lp) => ({\n+ type: 'liquidity' as const,\n+ id: lp.id,\n+ lp,\n+ })),\n+ ]\n+\n+ const totalItems = totalBets + visibleLps.length\n+ const totalLoadedItems = bets.length + visibleLps.length\n+\n+ const shouldLoadMore = totalLoadedItems < totalItems\n+ const [now] = useState(Date.now())\n+ const oldestBetTime = oldestBet?.createdTime ?? now\n+\n+ const loadMore = useEvent(async () => {\n+ if (!shouldLoadMore) return false\n+\n+ try {\n+ const newBets = await api('bets', {\n+ contractId: contract.id,\n+ beforeTime: oldestBetTime,\n+ limit: 50,\n+ filterRedemptions: !isNumber,\n+ includeZeroShareRedemptions: isNumber,\n+ minAmount: selectedMinAmount,\n+ })\n+\n+ if (newBets.length > 0) {\n+ setOlderBets((bets) => uniqBy([...bets, ...newBets], (b) => b.id))\n+ return true\n+ }\n+ return false\n+ } catch (err) {\n+ console.error(err)\n+ return false\n+ }\n+ })\n+ useEffect(() => {\n+ setOlderBets([])\n+ loadMore()\n+ }, [selectedMinAmount])\n+\n+ const allItems = sortBy(items, (item) =>\n+ item.type === 'bet'\n+ ? -item.bet.createdTime\n+ : item.type === 'liquidity'\n+ ? -item.lp.createdTime\n+ : item.type === 'betGroup'\n+ ? -item.bets[0].createdTime\n+ : undefined\n+ )\n+\n+ const scrollRef = useRef<HTMLDivElement>(null)\n+ const isCashContract = contract.token === 'CASH'\n+\n+ // Determine how many loading rows to show\n+ const numLoadingRows = shouldLoadMore\n+ ? Math.min(10, Math.max(0, totalBets - allItems.length))\n+ : 0\n+\n+ return (\n+ <>\n+ <div ref={scrollRef} />\n+\n+ {/* Minimum bet amount filter */}\n+ <Row className=\"mb-2\">\n+ <Row className=\"items-center gap-1\">\n+ <span className=\"text-ink-500 text-sm\">Min amount:</span>\n+ <DropdownMenu\n+ items={generateFilterDropdownItems(\n+ minAmountOptions.map((option, i) => ({\n+ label: option.label,\n+ value: i.toString(),\n+ })),\n+ (value: string) => {\n+ const newIndex = parseInt(value)\n+ setMinAmountFilterIndex(newIndex)\n+ setOlderBets([]) // Clear older bets to refetch with new filter\n+ track('change-bet-amount-filter', {\n+ contractSlug: contract.slug,\n+ contractName: contract.question,\n+ minAmount: minAmountOptions[newIndex].value,\n+ })\n+ }\n+ )}\n+ buttonContent={\n+ <Row className=\"text-ink-700 w-28 items-center text-sm\">\n+ <span className=\"whitespace-nowrap\">\n+ {minAmountOptions[minAmountFilterIndex].label}\n+ </span>\n+ <ChevronDownIcon className=\"h-4 w-4\" />\n+ </Row>\n+ }\n+ menuWidth={'w-36'}\n+ selectedItemName={minAmountOptions[minAmountFilterIndex].label}\n+ closeOnClick\n+ />\n+ </Row>\n+ </Row>\n+\n+ <Col className=\"mb-4 items-start gap-7\">\n+ {allItems.map((item) =>\n+ item.type === 'bet' ? (\n+ <FeedBet\n+ onReply={setReplyToBet}\n+ key={item.id}\n+ contract={contract as MarketContract}\n+ bet={item.bet}\n+ />\n+ ) : item.type === 'betGroup' ? (\n+ <MultiNumericBetGroup\n+ key={item.id}\n+ contract={contract as CPMMNumericContract}\n+ bets={item.bets}\n+ />\n+ ) : (\n+ <div\n+ key={item.id}\n+ className=\"-ml-2 rounded-full bg-gradient-to-r from-pink-300/50 via-purple-300/50 to-indigo-300/50 p-2\"\n+ >\n+ <FeedLiquidity\n+ liquidity={item.lp}\n+ isCashContract={isCashContract}\n+ />\n+ </div>\n+ )\n+ )}\n+ {/* Render skeleton loading rows */}\n+ {shouldLoadMore &&\n+ !minAmountFilterIndex &&\n+ Array(numLoadingRows)\n+ .fill(0)\n+ .map((_, i) => <LoadingBetRow key={`loading-${i}`} />)}\n+ </Col>\n+\n+ <LoadMoreUntilNotVisible loadMore={loadMore} />\n+ </>\n+ )\n+})\n+\n+function LoadingBetRow() {\n+ return (\n+ <div className=\"flex w-full animate-pulse gap-3 rounded-md \">\n+ {/* Avatar skeleton */}\n+ <div className=\"h-10 w-10 rounded-full bg-gray-500\" />\n+ <Col className=\"flex-1 justify-center gap-1.5\">\n+ <div className=\"h-4 w-1/2 rounded bg-gray-500\" />\n+ </Col>\n+ </div>\n+ )\n+}\n"
|
|
1151
|
-
},
|
|
1152
|
-
{
|
|
1153
|
-
"path": "web/components/contract/comments-tab-content.tsx",
|
|
1154
|
-
"status": "added",
|
|
1155
|
-
"diff": "Index: web/components/contract/comments-tab-content.tsx\n===================================================================\n--- web/components/contract/comments-tab-content.tsx\t0b8bb48 (parent)\n+++ web/components/contract/comments-tab-content.tsx\t0b36b52 (commit)\n@@ -0,0 +1,418 @@\n+import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'\n+import { useContractBets } from 'client-common/hooks/use-bets'\n+import { useSubscribeNewComments } from 'client-common/hooks/use-comments'\n+import { useEvent } from 'client-common/hooks/use-event'\n+import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n+import clsx from 'clsx'\n+import { Answer } from 'common/answer'\n+import { Bet } from 'common/bet'\n+import { ContractComment } from 'common/comment'\n+import { Contract } from 'common/contract'\n+import { TRADE_TERM } from 'common/envs/constants'\n+import { buildArray } from 'common/util/array'\n+import { MINUTE_MS } from 'common/util/time'\n+import { groupBy, keyBy, mapValues, sortBy, sumBy, uniqBy } from 'lodash'\n+import { memo, useEffect, useMemo, useReducer, useRef, useState } from 'react'\n+import { Button } from 'web/components/buttons/button'\n+import { ParentFeedComment } from 'web/components/comments/comment'\n+import { ContractCommentInput } from 'web/components/comments/comment-input'\n+import { FeedCommentThread } from 'web/components/comments/comment-thread'\n+import { Col } from 'web/components/layout/col'\n+import { Row } from 'web/components/layout/row'\n+import generateFilterDropdownItems from 'web/components/search/search-dropdown-helpers'\n+import DropdownMenu from 'web/components/widgets/dropdown-menu'\n+import { LoadingIndicator } from 'web/components/widgets/loading-indicator'\n+import { Tooltip } from 'web/components/widgets/tooltip'\n+import { VisibilityObserver } from 'web/components/widgets/visibility-observer'\n+import { useCommentThreads } from 'web/hooks/use-comments'\n+import { useIsPageVisible } from 'web/hooks/use-page-visible'\n+import { useUser } from 'web/hooks/use-user'\n+import { api } from 'web/lib/api/api'\n+import { track } from 'web/lib/service/analytics'\n+\n+export const CommentsTabContent = memo(function CommentsTabContent(props: {\n+ staticContract: Contract // contains the comments\n+ liveContract: Contract // you trade on this\n+ comments: ContractComment[]\n+ blockedUserIds: string[]\n+ setCommentsLength?: (length: number) => void\n+ replyTo?: Answer | Bet\n+ clearReply?: () => void\n+ className?: string\n+ highlightCommentId?: string\n+ pinnedComments: ContractComment[]\n+ scrollToEnd?: boolean\n+}) {\n+ const {\n+ staticContract,\n+ liveContract,\n+ comments: staticComments,\n+ blockedUserIds,\n+ setCommentsLength,\n+ replyTo,\n+ clearReply,\n+ className,\n+ highlightCommentId,\n+ pinnedComments: staticPinnedComments,\n+ scrollToEnd,\n+ } = props\n+ const user = useUser()\n+\n+ const { threads, loadMore, loading } = useCommentThreads(\n+ staticContract.id,\n+ 10,\n+ !!highlightCommentId\n+ )\n+ const [highlightedThreads, setHighlightedThreads] = useState<\n+ { parent: ContractComment; replies: ContractComment[] }[]\n+ >([])\n+ const [isLoadingHighlighted, setIsLoadingHighlighted] = useState(false)\n+\n+ const bets = useContractBets(\n+ staticContract.id,\n+ {\n+ commentRepliesOnly: true,\n+ },\n+ useIsPageVisible,\n+ (params) => api('bets', params)\n+ )\n+\n+ const newComments = useSubscribeNewComments(staticContract.id)\n+\n+ const allComments = useMemo(() => {\n+ const dynamicComments = threads.flatMap((t) => [t.parent, ...t.replies])\n+ const highlightedComments = highlightedThreads.flatMap((t) => [\n+ t.parent,\n+ ...t.replies,\n+ ])\n+ return uniqBy(\n+ [\n+ ...(newComments ?? []),\n+ ...staticComments,\n+ ...dynamicComments,\n+ ...highlightedComments,\n+ ],\n+ 'id'\n+ ).filter((c) => !blockedUserIds.includes(c.userId))\n+ }, [newComments, staticComments, threads, highlightedThreads, blockedUserIds])\n+\n+ const commentExistsLocally = useMemo(\n+ () => allComments.some((c) => c.id === highlightCommentId),\n+ [allComments, highlightCommentId]\n+ )\n+\n+ const isLoadingHighlightedComment =\n+ !!highlightCommentId &&\n+ !commentExistsLocally &&\n+ (loading || isLoadingHighlighted)\n+\n+ useEffect(() => {\n+ if (highlightCommentId && !commentExistsLocally && !loading) {\n+ setIsLoadingHighlighted(true)\n+ api('comment-thread', {\n+ contractId: staticContract.id,\n+ commentId: highlightCommentId,\n+ }).then((res) => {\n+ const {\n+ parentComment,\n+ replyComments,\n+ parentComments,\n+ nextParentComments,\n+ nextReplyComments,\n+ } = res\n+ if (parentComment) {\n+ const newThreads = [\n+ { parent: parentComment, replies: replyComments },\n+ ...parentComments.map((p) => ({ parent: p, replies: [] })),\n+ ]\n+ if (nextParentComments) {\n+ const repliesByParent = groupBy(\n+ nextReplyComments,\n+ 'replyToCommentId'\n+ )\n+ nextParentComments.forEach((p) => {\n+ newThreads.push({\n+ parent: p,\n+ replies: repliesByParent[p.id] ?? [],\n+ })\n+ })\n+ }\n+ setHighlightedThreads(newThreads)\n+ }\n+ setIsLoadingHighlighted(false)\n+ })\n+ }\n+ }, [highlightCommentId, commentExistsLocally, loading])\n+\n+ const isBinary = staticContract.outcomeType === 'BINARY'\n+ const isBountiedQuestion = staticContract.outcomeType == 'BOUNTIED_QUESTION'\n+ const bestFirst =\n+ isBountiedQuestion &&\n+ (!user || user.id !== staticContract.creatorId) &&\n+ !staticContract.isAutoBounty\n+\n+ const sorts = buildArray(\n+ bestFirst ? 'Best' : 'Newest',\n+ bestFirst ? 'Newest' : 'Best',\n+ isBinary && `Yes bets`,\n+ isBinary && 'No bets'\n+ )\n+\n+ const [sortIndex, setSortIndex] = usePersistentInMemoryState(\n+ 0,\n+ `comments-sort-${staticContract.id}`\n+ )\n+ const sort = sorts[sortIndex]\n+\n+ const sortTooltip =\n+ sort === 'Best'\n+ ? isBountiedQuestion\n+ ? 'Highest bounty, then most likes'\n+ : 'Most likes first'\n+ : null\n+\n+ // replied to answers/comments are NOT newest, otherwise newest first\n+ const isReply = (c: ContractComment) => c.replyToCommentId !== undefined\n+\n+ const strictlySortedComments = sortBy(allComments, [\n+ sort === 'Best'\n+ ? (c) =>\n+ isReply(c)\n+ ? c.createdTime\n+ : // For your own recent comments, show first.\n+ c.createdTime > Date.now() - 10 * MINUTE_MS && c.userId === user?.id\n+ ? -Infinity\n+ : c.hidden\n+ ? Infinity\n+ : -(\n+ (c.bountyAwarded ?? 0) * 1000 +\n+ (c.likes ?? 0) -\n+ (c.dislikes ?? 0)\n+ )\n+ : sort === 'Yes bets'\n+ ? (c: ContractComment) => -(c.betReplyAmountsByOutcome?.['YES'] ?? 0)\n+ : sort === 'No bets'\n+ ? (c: ContractComment) => -(c.betReplyAmountsByOutcome?.['NO'] ?? 0)\n+ : // Newest\n+ (c) => c,\n+ (c) => (isReply(c) ? c.createdTime : c.hidden ? Infinity : -c.createdTime),\n+ ])\n+\n+ const commentsByParent = groupBy(\n+ strictlySortedComments,\n+ (c) => c.replyToCommentId ?? '_'\n+ )\n+\n+ const commentById = keyBy(allComments, 'id')\n+\n+ // lump comments on load/sort to prevent jumping\n+ const [frozenCommentIds, refreezeIds] = useReducer(\n+ () => strictlySortedComments.map((c) => c.id),\n+ strictlySortedComments.map((c) => c.id)\n+ )\n+ useEffect(() => {\n+ if (user) refreezeIds()\n+ }, [user?.id])\n+\n+ const firstOldCommentIndex = strictlySortedComments.findIndex((c) =>\n+ frozenCommentIds.includes(c.id)\n+ )\n+\n+ const sortedComments = [\n+ ...strictlySortedComments.slice(0, firstOldCommentIndex),\n+ // Lump the original comments in a contiguous chunk so they don't jump around.\n+ ...frozenCommentIds.map((id) => commentById[id]).filter(Boolean),\n+ ...strictlySortedComments\n+ .slice(firstOldCommentIndex)\n+ .filter((c) => !frozenCommentIds.includes(c.id)),\n+ ]\n+\n+ const parentComments = sortedComments.filter(\n+ (c) => c.replyToCommentId === undefined\n+ )\n+\n+ const childrensBounties = isBountiedQuestion\n+ ? mapValues(commentsByParent, (comments) =>\n+ sumBy(comments, (c) => c?.bountyAwarded ?? 0)\n+ )\n+ : {}\n+\n+ useEffect(() => {\n+ setCommentsLength?.(allComments.length)\n+ }, [allComments.length])\n+\n+ const pinnedComments = uniqBy(\n+ staticPinnedComments.concat(\n+ allComments.filter((comment) => comment.pinned)\n+ ),\n+ 'id'\n+ )\n+ const onVisibilityUpdated = useEvent((visible: boolean) => {\n+ if (visible && !loading) loadMore()\n+ })\n+\n+ const endOfMessagesRef = useRef<any>(null)\n+\n+ useEffect(() => {\n+ if (endOfMessagesRef && scrollToEnd)\n+ endOfMessagesRef.current?.scrollIntoView({\n+ behavior: 'auto',\n+ block: 'start',\n+ })\n+ }, [endOfMessagesRef])\n+\n+ const [expandGptSummary, setExpandGptSummary] = usePersistentInMemoryState(\n+ false,\n+ `expand-gpt-summary-${staticContract.id}`\n+ )\n+\n+ function getSortLabel(sort: string) {\n+ if (sort == 'Yes bets') return `Yes ${TRADE_TERM}s`\n+ if (sort == 'No bets') return `No ${TRADE_TERM}s`\n+ return sort\n+ }\n+\n+ return (\n+ <Col className={clsx(className, scrollToEnd && 'flex-col-reverse')}>\n+ <div ref={endOfMessagesRef} />\n+ <ContractCommentInput\n+ autoFocus={false}\n+ replyTo={replyTo}\n+ className=\"mb-4 mr-px mt-px\"\n+ playContract={staticContract}\n+ clearReply={clearReply}\n+ trackingLocation={'contract page'}\n+ commentTypes={['comment', 'repost']}\n+ />\n+\n+ {staticContract.gptCommentSummary && (\n+ <Button\n+ className=\"mb-2 rounded-md bg-teal-100 p-4\"\n+ size=\"xs\"\n+ color=\"none\"\n+ onClick={() => setExpandGptSummary((e) => !e)}\n+ >\n+ <Col className=\"gap-2 p-2\">\n+ <Row className=\"items-center gap-2\">\n+ {expandGptSummary ? (\n+ <ChevronUpIcon className=\"h-5 w-5\" />\n+ ) : (\n+ <ChevronDownIcon className=\"h-5 w-5\" />\n+ )}\n+ <div className=\"text-lg\">Comments summary</div>\n+ </Row>\n+ <div\n+ className={clsx(\n+ 'whitespace-pre-line text-left text-sm',\n+ !expandGptSummary && 'line-clamp-3'\n+ )}\n+ >\n+ {staticContract.gptCommentSummary}\n+ </div>\n+ </Col>\n+ </Button>\n+ )}\n+\n+ {allComments.length > 0 && (\n+ <Row className=\"justify-end\">\n+ <Tooltip text={sortTooltip}>\n+ <Row className=\"items-center gap-1\">\n+ <span className=\"text-ink-400 text-sm\">Sort by:</span>\n+ <DropdownMenu\n+ items={generateFilterDropdownItems(\n+ sorts.map((s, i) => ({\n+ label: getSortLabel(s),\n+ value: i + '',\n+ })),\n+ (value: string) => {\n+ const i = parseInt(value)\n+ setSortIndex(i)\n+ console.log(i)\n+ refreezeIds()\n+ track('change-comments-sort', {\n+ contractSlug: staticContract.slug,\n+ contractName: staticContract.question,\n+ totalComments: allComments.length,\n+ totalUniqueTraders: staticContract.uniqueBettorCount,\n+ })\n+ }\n+ )}\n+ buttonContent={\n+ <Row className=\"text-ink-600 w-20 items-center text-sm\">\n+ <span className=\"whitespace-nowrap\">\n+ {getSortLabel(sort)}\n+ </span>\n+ <ChevronDownIcon className=\"h-4 w-4\" />\n+ </Row>\n+ }\n+ menuWidth={'w-28'}\n+ selectedItemName={sort}\n+ closeOnClick\n+ />\n+ </Row>\n+ </Tooltip>\n+ </Row>\n+ )}\n+\n+ {pinnedComments.map((comment) => (\n+ <div key={comment.id} className={'pt-3'}>\n+ <ParentFeedComment\n+ comment={comment}\n+ playContract={staticContract}\n+ liveContract={liveContract}\n+ trackingLocation={'contract page'}\n+ seeReplies={false}\n+ numReplies={0}\n+ isPinned\n+ />\n+ </div>\n+ ))}\n+\n+ {isLoadingHighlightedComment ? (\n+ <Col className=\"h-32 items-center justify-center\">\n+ <LoadingIndicator />\n+ </Col>\n+ ) : (\n+ parentComments.map((parent) => (\n+ <FeedCommentThread\n+ key={parent.id}\n+ playContract={staticContract}\n+ liveContract={liveContract}\n+ parentComment={parent}\n+ threadComments={commentsByParent[parent.id] ?? []}\n+ trackingLocation={'contract page'}\n+ idInUrl={highlightCommentId}\n+ showReplies={\n+ !isBountiedQuestion ||\n+ (!!user && user.id === staticContract.creatorId)\n+ }\n+ childrenBountyTotal={\n+ staticContract.outcomeType == 'BOUNTIED_QUESTION'\n+ ? childrensBounties[parent.id]\n+ : undefined\n+ }\n+ bets={bets?.filter(\n+ (b: Bet) =>\n+ b.replyToCommentId &&\n+ [parent]\n+ .concat(commentsByParent[parent.id] ?? [])\n+ .map((c) => c.id)\n+ .includes(b.replyToCommentId)\n+ )}\n+ />\n+ ))\n+ )}\n+ <div className=\"relative w-full\">\n+ <VisibilityObserver\n+ onVisibilityUpdated={onVisibilityUpdated}\n+ className=\"pointer-events-none absolute bottom-0 h-[75vh]\"\n+ />\n+ </div>\n+ {loading && (\n+ <Col className=\"h-32 items-center justify-center\">\n+ <LoadingIndicator />\n+ </Col>\n+ )}\n+ </Col>\n+ )\n+})\n"
|
|
1156
|
-
},
|
|
1157
|
-
{
|
|
1158
|
-
"path": "web/components/contract/contract-tabs.tsx",
|
|
1159
|
-
"status": "modified",
|
|
1160
|
-
"diff": "Index: web/components/contract/contract-tabs.tsx\n===================================================================\n--- web/components/contract/contract-tabs.tsx\t0b8bb48 (parent)\n+++ web/components/contract/contract-tabs.tsx\t0b36b52 (commit)\n@@ -1,68 +1,19 @@\n-import {\n- groupBy,\n- keyBy,\n- minBy,\n- mapValues,\n- sortBy,\n- sumBy,\n- uniqBy,\n- maxBy,\n-} from 'lodash'\n-import { memo, useEffect, useMemo, useReducer, useRef, useState } from 'react'\n-import clsx from 'clsx'\n-import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'\n+import { useState } from 'react'\n \n import { Answer } from 'common/answer'\n-import {\n- DEV_HOUSE_LIQUIDITY_PROVIDER_ID,\n- HOUSE_LIQUIDITY_PROVIDER_ID,\n-} from 'common/antes'\n import { Bet } from 'common/bet'\n import { ContractComment } from 'common/comment'\n-import {\n- BinaryContract,\n- Contract,\n- CPMMNumericContract,\n- MarketContract,\n-} from 'common/contract'\n+import { BinaryContract, Contract } from 'common/contract'\n import { buildArray } from 'common/util/array'\n-import { shortFormatNumber, maybePluralize } from 'common/util/format'\n-import { MINUTE_MS } from 'common/util/time'\n+import { maybePluralize, shortFormatNumber } from 'common/util/format'\n+import { BetsTabContent } from 'web/components/contract/bets-tab-content'\n+import { CommentsTabContent } from 'web/components/contract/comments-tab-content'\n import { UserPositionsTable } from 'web/components/contract/user-positions-table'\n-import { LoadingIndicator } from 'web/components/widgets/loading-indicator'\n-import { Tooltip } from 'web/components/widgets/tooltip'\n-import {\n- VisibilityObserver,\n- LoadMoreUntilNotVisible,\n-} from 'web/components/widgets/visibility-observer'\n-import { useEvent } from 'client-common/hooks/use-event'\n-import { useLiquidity } from 'web/hooks/use-liquidity'\n-import { useUser } from 'web/hooks/use-user'\n+import { useHashInUrlPageRouter } from 'web/hooks/use-hash-in-url-page-router'\n import { track } from 'web/lib/service/analytics'\n-import { FeedBet } from '../feed/feed-bets'\n-import { FeedCommentThread } from '../comments/comment-thread'\n-import { ContractCommentInput } from '../comments/comment-input'\n-import { FeedLiquidity } from '../feed/feed-liquidity'\n import { Col } from '../layout/col'\n-import { Row } from '../layout/row'\n import { ControlledTabs } from '../layout/tabs'\n-import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n-import { useSubscribeNewComments } from 'client-common/hooks/use-comments'\n-import { ParentFeedComment } from '../comments/comment'\n-import { useHashInUrlPageRouter } from 'web/hooks/use-hash-in-url-page-router'\n-import { MultiNumericBetGroup } from 'web/components/feed/feed-multi-numeric-bet-group'\n-import { Button } from '../buttons/button'\n-import DropdownMenu from '../widgets/dropdown-menu'\n-import generateFilterDropdownItems from '../search/search-dropdown-helpers'\n-import { useAPIGetter } from 'web/hooks/use-api-getter'\n-import { api } from 'web/lib/api/api'\n-import { TRADE_TERM } from 'common/envs/constants'\n-import {\n- listenToOrderUpdates,\n- useContractBets,\n-} from 'client-common/hooks/use-bets'\n-import { useIsPageVisible } from 'web/hooks/use-page-visible'\n \n export function ContractTabs(props: {\n staticContract: Contract\n liveContract: Contract\n@@ -175,580 +126,4 @@\n )}\n />\n )\n }\n-\n-const LOAD_MORE = 10\n-export const CommentsTabContent = memo(function CommentsTabContent(props: {\n- staticContract: Contract // contains the comments\n- liveContract: Contract // you trade on this\n- comments: ContractComment[]\n- blockedUserIds: string[]\n- setCommentsLength?: (length: number) => void\n- replyTo?: Answer | Bet\n- clearReply?: () => void\n- className?: string\n- highlightCommentId?: string\n- pinnedComments: ContractComment[]\n- scrollToEnd?: boolean\n-}) {\n- const {\n- staticContract,\n- liveContract,\n- blockedUserIds,\n- setCommentsLength,\n- replyTo,\n- clearReply,\n- className,\n- highlightCommentId,\n- scrollToEnd,\n- } = props\n- const user = useUser()\n-\n- // Load all comments once\n- const { data: fetchedComments, loading: commentsLoading } = useAPIGetter(\n- 'comments',\n- {\n- contractId: staticContract.id,\n- },\n- undefined,\n- 'comments-' + staticContract.id\n- )\n-\n- const bets = useContractBets(\n- staticContract.id,\n- {\n- commentRepliesOnly: true,\n- },\n- useIsPageVisible,\n- (params) => api('bets', params)\n- )\n- const latestCommentTime = useMemo(\n- () => maxBy(fetchedComments, 'createdTime')?.createdTime,\n- [fetchedComments?.length]\n- )\n-\n- const isPageVisible = useIsPageVisible()\n- const { data: newFetchedComments, loading: newCommentsLoading } =\n- useAPIGetter(\n- 'comments',\n- {\n- contractId: staticContract.id,\n- afterTime: latestCommentTime,\n- },\n- undefined,\n- 'new-comments-' + staticContract.id,\n- isPageVisible\n- )\n-\n- // Listen for new comments\n- const newComments = useSubscribeNewComments(staticContract.id)\n- const comments = uniqBy(\n- [\n- ...(newComments ?? []),\n- ...(newFetchedComments ?? []),\n- ...(fetchedComments ?? []),\n- ...props.comments,\n- ],\n- 'id'\n- ).filter((c) => !blockedUserIds.includes(c.userId))\n-\n- const commentExistsLocally = comments.some((c) => c.id === highlightCommentId)\n- const isLoadingHighlightedComment =\n- highlightCommentId &&\n- !commentExistsLocally &&\n- (commentsLoading || newCommentsLoading)\n-\n- const [parentCommentsToRender, setParentCommentsToRender] = useState(\n- props.comments.filter((c) => !c.replyToCommentId).length\n- )\n-\n- const isBinary = staticContract.outcomeType === 'BINARY'\n- const isBountiedQuestion = staticContract.outcomeType == 'BOUNTIED_QUESTION'\n- const bestFirst =\n- isBountiedQuestion &&\n- (!user || user.id !== staticContract.creatorId) &&\n- !staticContract.isAutoBounty\n-\n- const sorts = buildArray(\n- bestFirst ? 'Best' : 'Newest',\n- bestFirst ? 'Newest' : 'Best',\n- isBinary && `Yes bets`,\n- isBinary && 'No bets'\n- )\n-\n- const [sortIndex, setSortIndex] = usePersistentInMemoryState(\n- 0,\n- `comments-sort-${staticContract.id}`\n- )\n- const sort = sorts[sortIndex]\n-\n- const sortTooltip =\n- sort === 'Best'\n- ? isBountiedQuestion\n- ? 'Highest bounty, then most likes'\n- : 'Most likes first'\n- : null\n-\n- // replied to answers/comments are NOT newest, otherwise newest first\n- const isReply = (c: ContractComment) => c.replyToCommentId !== undefined\n-\n- const strictlySortedComments = sortBy(comments, [\n- sort === 'Best'\n- ? (c) =>\n- isReply(c)\n- ? c.createdTime\n- : // For your own recent comments, show first.\n- c.createdTime > Date.now() - 10 * MINUTE_MS && c.userId === user?.id\n- ? -Infinity\n- : c.hidden\n- ? Infinity\n- : -(\n- (c.bountyAwarded ?? 0) * 1000 +\n- (c.likes ?? 0) -\n- (c.dislikes ?? 0)\n- )\n- : sort === 'Yes bets'\n- ? (c: ContractComment) => -(c.betReplyAmountsByOutcome?.['YES'] ?? 0)\n- : sort === 'No bets'\n- ? (c: ContractComment) => -(c.betReplyAmountsByOutcome?.['NO'] ?? 0)\n- : // Newest\n- (c) => c,\n- (c) => (isReply(c) ? c.createdTime : c.hidden ? Infinity : -c.createdTime),\n- ])\n-\n- const commentsByParent = groupBy(\n- strictlySortedComments,\n- (c) => c.replyToCommentId ?? '_'\n- )\n-\n- const commentById = keyBy(comments, 'id')\n-\n- // lump comments on load/sort to prevent jumping\n- const [frozenCommentIds, refreezeIds] = useReducer(\n- () => strictlySortedComments.map((c) => c.id),\n- strictlySortedComments.map((c) => c.id)\n- )\n- useEffect(() => {\n- if (user) refreezeIds()\n- }, [user?.id])\n-\n- const firstOldCommentIndex = strictlySortedComments.findIndex((c) =>\n- frozenCommentIds.includes(c.id)\n- )\n-\n- const sortedComments = [\n- ...strictlySortedComments.slice(0, firstOldCommentIndex),\n- // Lump the original comments in a contiguous chunk so they don't jump around.\n- ...frozenCommentIds.map((id) => commentById[id]).filter(Boolean),\n- ...strictlySortedComments\n- .slice(firstOldCommentIndex)\n- .filter((c) => !frozenCommentIds.includes(c.id)),\n- ]\n-\n- const parentComments = sortedComments.filter(\n- (c) => c.replyToCommentId === undefined\n- )\n-\n- const childrensBounties = isBountiedQuestion\n- ? mapValues(commentsByParent, (comments) =>\n- sumBy(comments, (c) => c?.bountyAwarded ?? 0)\n- )\n- : {}\n-\n- const visibleCommentIds = useMemo(\n- () =>\n- parentComments\n- .slice(0, parentCommentsToRender)\n- .map((c) => [c.id, ...(commentsByParent[c.id] ?? []).map((c) => c.id)])\n- .flat(),\n- [comments.length]\n- )\n-\n- useEffect(() => {\n- if (highlightCommentId) {\n- const currentlyVisible = visibleCommentIds.includes(highlightCommentId)\n- if (!currentlyVisible) setParentCommentsToRender(comments.length)\n- }\n- setCommentsLength?.(comments.length)\n- }, [highlightCommentId, comments.length])\n-\n- const loadMore = () => setParentCommentsToRender((prev) => prev + LOAD_MORE)\n- const pinnedComments = uniqBy(\n- props.pinnedComments.concat(comments.filter((comment) => comment.pinned)),\n- 'id'\n- )\n- const onVisibilityUpdated = useEvent((visible: boolean) => {\n- if (visible) loadMore()\n- })\n-\n- const endOfMessagesRef = useRef<any>(null)\n-\n- useEffect(() => {\n- if (endOfMessagesRef && scrollToEnd)\n- endOfMessagesRef.current?.scrollIntoView({\n- behavior: 'auto',\n- block: 'start',\n- })\n- }, [endOfMessagesRef])\n-\n- const [expandGptSummary, setExpandGptSummary] = usePersistentInMemoryState(\n- false,\n- `expand-gpt-summary-${staticContract.id}`\n- )\n-\n- function getSortLabel(sort: string) {\n- if (sort == 'Yes bets') return `Yes ${TRADE_TERM}s`\n- if (sort == 'No bets') return `No ${TRADE_TERM}s`\n- return sort\n- }\n-\n- return (\n- <Col className={clsx(className, scrollToEnd && 'flex-col-reverse')}>\n- <div ref={endOfMessagesRef} />\n- <ContractCommentInput\n- autoFocus={false}\n- replyTo={replyTo}\n- className=\"mb-4 mr-px mt-px\"\n- playContract={staticContract}\n- clearReply={clearReply}\n- trackingLocation={'contract page'}\n- commentTypes={['comment', 'repost']}\n- />\n-\n- {staticContract.gptCommentSummary && (\n- <Button\n- className=\"mb-2 rounded-md bg-teal-100 p-4\"\n- size=\"xs\"\n- color=\"none\"\n- onClick={() => setExpandGptSummary((e) => !e)}\n- >\n- <Col className=\"gap-2 p-2\">\n- <Row className=\"items-center gap-2\">\n- {expandGptSummary ? (\n- <ChevronUpIcon className=\"h-5 w-5\" />\n- ) : (\n- <ChevronDownIcon className=\"h-5 w-5\" />\n- )}\n- <div className=\"text-lg\">Comments summary</div>\n- </Row>\n- <div\n- className={clsx(\n- 'whitespace-pre-line text-left text-sm',\n- !expandGptSummary && 'line-clamp-3'\n- )}\n- >\n- {staticContract.gptCommentSummary}\n- </div>\n- </Col>\n- </Button>\n- )}\n-\n- {comments.length > 0 && (\n- <Row className=\"justify-end\">\n- <Tooltip text={sortTooltip}>\n- <Row className=\"items-center gap-1\">\n- <span className=\"text-ink-400 text-sm\">Sort by:</span>\n- <DropdownMenu\n- items={generateFilterDropdownItems(\n- sorts.map((s, i) => ({\n- label: getSortLabel(s),\n- value: i + '',\n- })),\n- (value: string) => {\n- const i = parseInt(value)\n- setSortIndex(i)\n- console.log(i)\n- refreezeIds()\n- track('change-comments-sort', {\n- contractSlug: staticContract.slug,\n- contractName: staticContract.question,\n- totalComments: comments.length,\n- totalUniqueTraders: staticContract.uniqueBettorCount,\n- })\n- }\n- )}\n- buttonContent={\n- <Row className=\"text-ink-600 w-20 items-center text-sm\">\n- <span className=\"whitespace-nowrap\">\n- {getSortLabel(sort)}\n- </span>\n- <ChevronDownIcon className=\"h-4 w-4\" />\n- </Row>\n- }\n- menuWidth={'w-28'}\n- selectedItemName={sort}\n- closeOnClick\n- />\n- </Row>\n- </Tooltip>\n- </Row>\n- )}\n-\n- {pinnedComments.map((comment) => (\n- <div key={comment.id} className={'pt-3'}>\n- <ParentFeedComment\n- comment={comment}\n- playContract={staticContract}\n- liveContract={liveContract}\n- trackingLocation={'contract page'}\n- seeReplies={false}\n- numReplies={0}\n- isPinned\n- />\n- </div>\n- ))}\n-\n- {isLoadingHighlightedComment ? (\n- <Col className=\"h-32 items-center justify-center\">\n- <LoadingIndicator />\n- </Col>\n- ) : (\n- parentComments.slice(0, parentCommentsToRender).map((parent) => (\n- <FeedCommentThread\n- key={parent.id}\n- playContract={staticContract}\n- liveContract={liveContract}\n- parentComment={parent}\n- threadComments={commentsByParent[parent.id] ?? []}\n- trackingLocation={'contract page'}\n- idInUrl={highlightCommentId}\n- showReplies={\n- !isBountiedQuestion ||\n- (!!user && user.id === staticContract.creatorId)\n- }\n- childrenBountyTotal={\n- staticContract.outcomeType == 'BOUNTIED_QUESTION'\n- ? childrensBounties[parent.id]\n- : undefined\n- }\n- bets={bets?.filter(\n- (b) =>\n- b.replyToCommentId &&\n- [parent]\n- .concat(commentsByParent[parent.id] ?? [])\n- .map((c) => c.id)\n- .includes(b.replyToCommentId)\n- )}\n- />\n- ))\n- )}\n- <div className=\"relative w-full\">\n- <VisibilityObserver\n- onVisibilityUpdated={onVisibilityUpdated}\n- className=\"pointer-events-none absolute bottom-0 h-[75vh]\"\n- />\n- </div>\n- </Col>\n- )\n-})\n-\n-export const BetsTabContent = memo(function BetsTabContent(props: {\n- contract: Contract\n- bets: Bet[]\n- totalBets: number\n- setReplyToBet?: (bet: Bet) => void\n-}) {\n- const { contract, setReplyToBet, totalBets } = props\n- const { outcomeType } = contract\n- const [olderBets, setOlderBets] = useState<Bet[]>([])\n-\n- const [minAmountFilterIndex, setMinAmountFilterIndex] =\n- usePersistentInMemoryState(0, `bet-amount-filter-${contract.id}`)\n- const isNumber = outcomeType === 'NUMBER'\n-\n- // Min amount filter options\n- const minAmountOptions = [\n- { label: 'Any amount', value: undefined },\n- { label: 'M$100+', value: 100 },\n- { label: 'M$1,000+', value: 1000 },\n- { label: 'M$10,000+', value: 10000 },\n- ]\n- const selectedMinAmount = minAmountOptions[minAmountFilterIndex].value\n-\n- // Filter initial bets on client side, server will filter olderBets\n- const filteredInitialBets = selectedMinAmount\n- ? props.bets.filter((bet) => Math.abs(bet.amount) >= selectedMinAmount)\n- : props.bets\n-\n- const bets = [...filteredInitialBets, ...olderBets]\n- listenToOrderUpdates(contract.id, setOlderBets, true)\n-\n- const oldestBet = minBy(bets, (b) => b.createdTime)\n-\n- const lps = useLiquidity(contract.id) ?? []\n- const visibleLps = lps.filter(\n- (l) =>\n- !l.isAnte &&\n- l.userId !== HOUSE_LIQUIDITY_PROVIDER_ID &&\n- l.userId !== DEV_HOUSE_LIQUIDITY_PROVIDER_ID &&\n- l.amount > 0 &&\n- !minAmountFilterIndex\n- )\n- const betsByBetGroupId = isNumber\n- ? groupBy(bets, (bet) => bet.betGroupId ?? bet.id)\n- : {}\n- const groupedBets = Object.values(betsByBetGroupId)\n-\n- const items = [\n- ...(isNumber\n- ? groupedBets.map((bets) => ({\n- type: 'betGroup' as const,\n- id: 'bets-tab-' + bets[0].betGroupId,\n- bets,\n- }))\n- : bets.map((bet) => ({\n- type: 'bet' as const,\n- id: 'bets-tab-' + bet.id + '-' + 'false',\n- bet,\n- }))),\n- ...visibleLps.map((lp) => ({\n- type: 'liquidity' as const,\n- id: lp.id,\n- lp,\n- })),\n- ]\n-\n- const totalItems = totalBets + visibleLps.length\n- const totalLoadedItems = bets.length + visibleLps.length\n-\n- const shouldLoadMore = totalLoadedItems < totalItems\n- const [now] = useState(Date.now())\n- const oldestBetTime = oldestBet?.createdTime ?? now\n-\n- const loadMore = useEvent(async () => {\n- if (!shouldLoadMore) return false\n-\n- try {\n- const newBets = await api('bets', {\n- contractId: contract.id,\n- beforeTime: oldestBetTime,\n- limit: 50,\n- filterRedemptions: !isNumber,\n- includeZeroShareRedemptions: isNumber,\n- minAmount: selectedMinAmount,\n- })\n-\n- if (newBets.length > 0) {\n- setOlderBets((bets) => uniqBy([...bets, ...newBets], (b) => b.id))\n- return true\n- }\n- return false\n- } catch (err) {\n- console.error(err)\n- return false\n- }\n- })\n- useEffect(() => {\n- setOlderBets([])\n- loadMore()\n- }, [selectedMinAmount])\n-\n- const allItems = sortBy(items, (item) =>\n- item.type === 'bet'\n- ? -item.bet.createdTime\n- : item.type === 'liquidity'\n- ? -item.lp.createdTime\n- : item.type === 'betGroup'\n- ? -item.bets[0].createdTime\n- : undefined\n- )\n-\n- const scrollRef = useRef<HTMLDivElement>(null)\n- const isCashContract = contract.token === 'CASH'\n-\n- // Determine how many loading rows to show\n- const numLoadingRows = shouldLoadMore\n- ? Math.min(10, Math.max(0, totalBets - allItems.length))\n- : 0\n-\n- return (\n- <>\n- <div ref={scrollRef} />\n-\n- {/* Minimum bet amount filter */}\n- <Row className=\"mb-2\">\n- <Row className=\"items-center gap-1\">\n- <span className=\"text-ink-500 text-sm\">Min amount:</span>\n- <DropdownMenu\n- items={generateFilterDropdownItems(\n- minAmountOptions.map((option, i) => ({\n- label: option.label,\n- value: i.toString(),\n- })),\n- (value: string) => {\n- const newIndex = parseInt(value)\n- setMinAmountFilterIndex(newIndex)\n- setOlderBets([]) // Clear older bets to refetch with new filter\n- track('change-bet-amount-filter', {\n- contractSlug: contract.slug,\n- contractName: contract.question,\n- minAmount: minAmountOptions[newIndex].value,\n- })\n- }\n- )}\n- buttonContent={\n- <Row className=\"text-ink-700 w-28 items-center text-sm\">\n- <span className=\"whitespace-nowrap\">\n- {minAmountOptions[minAmountFilterIndex].label}\n- </span>\n- <ChevronDownIcon className=\"h-4 w-4\" />\n- </Row>\n- }\n- menuWidth={'w-36'}\n- selectedItemName={minAmountOptions[minAmountFilterIndex].label}\n- closeOnClick\n- />\n- </Row>\n- </Row>\n-\n- <Col className=\"mb-4 items-start gap-7\">\n- {allItems.map((item) =>\n- item.type === 'bet' ? (\n- <FeedBet\n- onReply={setReplyToBet}\n- key={item.id}\n- contract={contract as MarketContract}\n- bet={item.bet}\n- />\n- ) : item.type === 'betGroup' ? (\n- <MultiNumericBetGroup\n- key={item.id}\n- contract={contract as CPMMNumericContract}\n- bets={item.bets}\n- />\n- ) : (\n- <div\n- key={item.id}\n- className=\"-ml-2 rounded-full bg-gradient-to-r from-pink-300/50 via-purple-300/50 to-indigo-300/50 p-2\"\n- >\n- <FeedLiquidity\n- liquidity={item.lp}\n- isCashContract={isCashContract}\n- />\n- </div>\n- )\n- )}\n- {/* Render skeleton loading rows */}\n- {shouldLoadMore &&\n- !minAmountFilterIndex &&\n- Array(numLoadingRows)\n- .fill(0)\n- .map((_, i) => <LoadingBetRow key={`loading-${i}`} />)}\n- </Col>\n-\n- <LoadMoreUntilNotVisible loadMore={loadMore} />\n- </>\n- )\n-})\n-\n-function LoadingBetRow() {\n- return (\n- <div className=\"flex w-full animate-pulse gap-3 rounded-md \">\n- {/* Avatar skeleton */}\n- <div className=\"h-10 w-10 rounded-full bg-gray-500\" />\n- <Col className=\"flex-1 justify-center gap-1.5\">\n- <div className=\"h-4 w-1/2 rounded bg-gray-500\" />\n- </Col>\n- </div>\n- )\n-}\n"
|
|
1161
|
-
},
|
|
1162
|
-
{
|
|
1163
|
-
"path": "web/components/contract/trades-button.tsx",
|
|
1164
|
-
"status": "modified",
|
|
1165
|
-
"diff": "Index: web/components/contract/trades-button.tsx\n===================================================================\n--- web/components/contract/trades-button.tsx\t0b8bb48 (parent)\n+++ web/components/contract/trades-button.tsx\t0b36b52 (commit)\n@@ -6,9 +6,8 @@\n import { MODAL_CLASS, Modal, SCROLLABLE_MODAL_CLASS } from '../layout/modal'\n import { Row } from '../layout/row'\n import { LoadingIndicator } from '../widgets/loading-indicator'\n import { Tooltip } from '../widgets/tooltip'\n-import { BetsTabContent } from './contract-tabs'\n import { UserPositionsTable } from 'web/components/contract/user-positions-table'\n import { UncontrolledTabs } from 'web/components/layout/tabs'\n import { Col } from 'web/components/layout/col'\n import { Avatar } from '../widgets/avatar'\n@@ -23,8 +22,9 @@\n import { shortenNumber } from 'common/util/formatNumber'\n import { useBetsOnce } from 'client-common/hooks/use-bets'\n import { api } from 'web/lib/api/api'\n import { useAPIGetter } from 'web/hooks/use-api-getter'\n+import { BetsTabContent } from 'web/components/contract/bets-tab-content'\n \n export function TradesButton(props: {\n contract: Contract\n answer?: Answer\n"
|
|
1166
|
-
},
|
|
1167
|
-
{
|
|
1168
|
-
"path": "web/hooks/use-comments.ts",
|
|
1169
|
-
"status": "modified",
|
|
1170
|
-
"diff": "Index: web/hooks/use-comments.ts\n===================================================================\n--- web/hooks/use-comments.ts\t0b8bb48 (parent)\n+++ web/hooks/use-comments.ts\t0b36b52 (commit)\n@@ -1,17 +1,17 @@\n+import { useApiSubscription } from 'client-common/hooks/use-api-subscription'\n+import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n import { ContractComment } from 'common/comment'\n+import { convertContractComment } from 'common/supabase/comments'\n+import { groupBy, sortBy, uniqBy } from 'lodash'\n import { useEffect, useState } from 'react'\n-import { sortBy, uniqBy } from 'lodash'\n+import { api } from 'web/lib/api/api'\n import {\n getAllCommentRows,\n getComment,\n getCommentThread,\n getNumContractComments,\n } from 'web/lib/supabase/comments'\n-import { convertContractComment } from 'common/supabase/comments'\n-import { api } from 'web/lib/api/api'\n-import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n-import { useApiSubscription } from 'client-common/hooks/use-api-subscription'\n \n export function useNumContractComments(contractId: string) {\n const [numComments, setNumComments] = useState<number>(0)\n \n@@ -98,4 +98,41 @@\n }, [limit])\n \n return comments\n }\n+\n+export const useCommentThreads = (\n+ contractId: string,\n+ limit: number,\n+ disabled: boolean\n+) => {\n+ const [threads, setThreads] = useState<\n+ { parent: ContractComment; replies: ContractComment[] }[]\n+ >([])\n+ const [page, setPage] = useState(0)\n+ const [loading, setLoading] = useState(false)\n+\n+ const loadMore = async () => {\n+ if (loading) return\n+ setLoading(true)\n+ const { parentComments, replyComments } = await api('comment-threads', {\n+ contractId,\n+ limit,\n+ page,\n+ })\n+ const repliesByParent = groupBy(replyComments, 'replyToCommentId')\n+ const newThreads = parentComments.map((p) => ({\n+ parent: p,\n+ replies: repliesByParent[p.id] ?? [],\n+ }))\n+ setThreads((t) => [...t, ...newThreads])\n+ setPage((p) => p + 1)\n+ setLoading(false)\n+ }\n+\n+ useEffect(() => {\n+ if (disabled) return\n+ loadMore()\n+ }, [contractId, disabled])\n+\n+ return { threads, loadMore, loading }\n+}\n"
|
|
1171
|
-
}
|
|
1172
|
-
]
|
|
1173
|
-
},
|
|
1174
|
-
{
|
|
1175
|
-
"id": "paginate-positions",
|
|
1176
|
-
"sha": "83e025d7d7bda27e15a4663fc924d1ad7bd77c81",
|
|
1177
|
-
"parentSha": "4f61d9bccd1a66d02a43a4eb1fec5b4ca00ec74b",
|
|
1178
|
-
"spec": "Implement incremental, offset-based loading for the user positions table and the underlying Supabase query.\n\nChanges required:\n\n1) common/src/supabase/contract-metrics.ts\n- Update getOrderedContractMetricRowsForContractId to support paging by offset in addition to limit.\n - Add a new trailing parameter: offset: number = 0.\n - Replace prior limit-only usage with range(offset, offset + limit - 1) on both query halves (the two queries for the two sides/partitions).\n - Maintain existing filters/orderings:\n - If answerId is provided, filter eq('answer_id', answerId); else is('answer_id', null).\n - If order === 'shares', query1 filters has_yes_shares and orders by total_shares_yes descending; query2 filters has_no_shares and orders by total_shares_no descending.\n - Else (order === 'profit'), query1 orders profit desc and gt('profit', 0); query2 orders profit asc and lt('profit', 0).\n - Execute with run(query1) and run(query2); return concatenated rows.\n\n2) web/components/contract/user-positions-table.tsx\n- Implement incremental loading using the updated backend function.\n - Add constants: ROWS_PER_CALL = 50 (per side per request), USER_TABLE_PAGE_SIZE = 20 (UI page size).\n - State: maintain separate metric arrays and paging state per sort type:\n - contractMetricsOrderedByProfit: ContractMetric[] | undefined\n - contractMetricsOrderedByShares: ContractMetric[] | undefined\n - nextProfitOffset: number\n - nextSharesOffset: number\n - hasMoreProfit: boolean\n - hasMoreShares: boolean\n - Compute answers array as empty unless viewing a cpmm-multi-1 contract without a preselected answer (then use contract.answers). If an answer is provided via props, treat as a single-answer view.\n - Update updateContractMetrics(newSortBy, answerIdForShares, loadMore=false):\n - Guard against overlapping loads; if loadMore is requested but already loading or no more results for that sort, return. Allow full refresh when changing sort type even if currently loading.\n - On a fresh load (loadMore=false): reset page to 0; clear the corresponding metrics array; reset offsets (0) and hasMore=true for the relevant sort type.\n - Determine offsetToUse: loadMore ? nextProfitOffset/nextSharesOffset : 0.\n - Call getOrderedContractMetricRowsForContractId(contractId, db, (newSortBy==='profit' ? undefined : answerIdForShares), newSortBy, ROWS_PER_CALL, offsetToUse).\n - Convert rows via convertContractMetricRows and append to the appropriate metrics array with uniqBy dedupe keyed by userId + (answerId ?? '') + contractId.\n - Advance the corresponding next offset by ROWS_PER_CALL.\n - Update hasMore flags based on rows.length (if loadMore and 0 rows, set hasMore to false; on fresh load set hasMore to rows.length > 0).\n - Manage loading state to only be true during the fetch.\n - Trigger initial load in a useEffect once on mount, calling updateContractMetrics(sortBy, currentAnswerId, false).\n - Counts: keep existing calls to getContractMetricsCount for YES and NO for the current answerId; keep dependencies [currentAnswerId, contractId].\n - Fetch all answer position counts only for multi-choice contracts that have answers; add dependencies [contract.id, contract.mechanism, answers, setTotalPositions].\n - Filtering for display:\n - shares sort: filter by currentAnswerId (answer match or answerId null).\n - profit sort: display the accumulated profit metrics returned (already restricted to summary rows by the backend when order is profit); do not re-filter by !answerId.\n - Add a useEffect to auto-load more when the currently visible page needs more items per side:\n - Partition the currently accumulated metrics for the active sort:\n - profit: partition by profit >= 0 (left: Profit, right: Loss).\n - shares: partition by hasYesShares (left: YES, right: NO).\n - Compute requiredItemsForPageEnd = (page + 1) * USER_TABLE_PAGE_SIZE.\n - If either side has fewer than requiredItemsForPageEnd and hasMore for the current sort is true and not loading, call updateContractMetrics with loadMore=true to fetch the next chunk.\n - On sort toggles or answer selection changes, call updateContractMetrics(newSort, selectedAnswerId, false). Do not manually set page; the function resets it.\n - Update BinaryUserPositionsTable props: replace loadingPositions with pageSize and pass USER_TABLE_PAGE_SIZE. For loading prop, pass true only while loading the first page for the active sort (i.e., when the corresponding next offset is 0).\n - Inside BinaryUserPositionsTable:\n - Use largestColumnLength = Math.max(totalYesPositions, totalNoPositions) to drive Pagination totalItems.\n - Replace prior minHeight logic: reserve space using placeholders computed from the larger of current YES/NO totals; number of placeholders equals min(max(totalYesPositions, totalNoPositions), pageSize), or pageSize if totals are zero.\n - Update LoadingResults to accept placeholderCount prop and render that many placeholder rows per column.\n\nBehavioral expectations:\n- When the page requires more rows in either column (YES/NO or Profit/Loss), the component fetches the next chunk (ROWS_PER_CALL per side) using the new offset and appends deduped rows.\n- Initial loading spinners and placeholders appear only for the first page. Subsequent loads happen seamlessly without collapsing the table height.\n- Pagination reflects the known total per side (max of YES/NO counts), not just the currently loaded rows.\n- Sorting and answer changes reset paging and loaded state appropriately.",
|
|
1179
|
-
"prompt": "Implement paginated, offset-based loading for the user positions table so it can progressively load and display all available positions. Add an offset parameter to the Supabase query that fetches user contract metrics and use range-based paging. In the UI, keep separate offsets and has-more flags per sort type (profit vs shares), prefetch additional data as the user pages so both columns stay filled, and only show the loading state on the initial page. Update the table to reserve space using placeholders based on the known counts for each side, and adjust the pagination to use the total count rather than loaded items. Ensure changing sort or selected answer resets paging and data appropriately.",
|
|
1180
|
-
"supplementalFiles": [
|
|
1181
|
-
"common/src/supabase/utils.ts",
|
|
1182
|
-
"common/src/contract-metric.ts",
|
|
1183
|
-
"web/components/widgets/pagination.tsx",
|
|
1184
|
-
"web/lib/supabase/db.ts",
|
|
1185
|
-
"web/hooks/use-saved-contract-metrics.ts"
|
|
1186
|
-
],
|
|
1187
|
-
"fileDiffs": [
|
|
1188
|
-
{
|
|
1189
|
-
"path": "common/src/supabase/contract-metrics.ts",
|
|
1190
|
-
"status": "modified",
|
|
1191
|
-
"diff": "Index: common/src/supabase/contract-metrics.ts\n===================================================================\n--- common/src/supabase/contract-metrics.ts\t4f61d9b (parent)\n+++ common/src/supabase/contract-metrics.ts\t83e025d (commit)\n@@ -155,40 +155,49 @@\n contractId: string,\n db: SupabaseClient,\n answerId?: string,\n order: 'profit' | 'shares' = 'profit',\n- limit: number = 50\n+ limit: number = 50,\n+ offset: number = 0\n ) {\n- const q1 = db\n+ let query1 = db\n .from('user_contract_metrics')\n .select('*')\n .eq('contract_id', contractId)\n- .limit(limit)\n- const q2 = db\n+ let query2 = db\n .from('user_contract_metrics')\n .select('*')\n .eq('contract_id', contractId)\n- .limit(limit)\n \n if (answerId) {\n- q1.eq('answer_id', answerId)\n- q2.eq('answer_id', answerId)\n+ query1.eq('answer_id', answerId)\n+ query2.eq('answer_id', answerId)\n } else {\n- q1.is('answer_id', null)\n- q2.is('answer_id', null)\n+ query1.is('answer_id', null)\n+ query2.is('answer_id', null)\n }\n \n if (order === 'shares') {\n- q1.eq(`has_yes_shares`, true).order(`total_shares_yes`, {\n+ query1.eq('has_yes_shares', true).order('total_shares_yes', {\n ascending: false,\n })\n- q2.eq(`has_no_shares`, true).order(`total_shares_no`, { ascending: false })\n+ query2\n+ .eq('has_no_shares', true)\n+ .order('total_shares_no', { ascending: false })\n } else {\n- q1.order(`profit`, { ascending: false, nullsFirst: false }).gt(`profit`, 0)\n- q2.order(`profit`, { ascending: true, nullsFirst: false }).lt(`profit`, 0)\n+ query1\n+ .order('profit', { ascending: false, nullsFirst: false })\n+ .gt('profit', 0)\n+ query2\n+ .order('profit', { ascending: true, nullsFirst: false })\n+ .lt('profit', 0)\n }\n- const { data: q1Data } = await run(q1)\n- const { data: q2Data } = await run(q2)\n+\n+ query1 = query1.range(offset, offset + limit - 1)\n+ query2 = query2.range(offset, offset + limit - 1)\n+\n+ const { data: q1Data } = await run(query1)\n+ const { data: q2Data } = await run(query2)\n return q1Data.concat(q2Data)\n }\n \n export async function getContractIdsWithMetrics(\n"
|
|
1192
|
-
},
|
|
1193
|
-
{
|
|
1194
|
-
"path": "web/components/contract/user-positions-table.tsx",
|
|
1195
|
-
"status": "modified",
|
|
1196
|
-
"diff": "Index: web/components/contract/user-positions-table.tsx\n===================================================================\n--- web/components/contract/user-positions-table.tsx\t4f61d9b (parent)\n+++ web/components/contract/user-positions-table.tsx\t83e025d (commit)\n@@ -42,8 +42,11 @@\n import { UserHovercard } from '../user/user-hovercard'\n import { Select } from '../widgets/select'\n import { getPseudonym } from '../charts/contract/choice'\n \n+const ROWS_PER_CALL = 50 // Number of items to fetch per backend call, per side\n+const USER_TABLE_PAGE_SIZE = 20 // Number of items displayed per page in the UI\n+\n export const UserPositionsTable = memo(\n function UserPositionsTableContent(props: {\n contract: BinaryContract | CPMMMultiContract\n setTotalPositions?: (totalPositions: number) => void\n@@ -55,114 +58,230 @@\n const { contract, setTotalPositions, answerDetails } = props\n const answer = answerDetails?.answer\n const contractId = contract.id\n \n- useEffect(() => {\n- updateContractMetrics(sortBy, currentAnswerId)\n- }, [])\n-\n const [contractMetricsOrderedByProfit, setContractMetricsOrderedByProfit] =\n useState<ContractMetric[] | undefined>()\n+ const [nextProfitOffset, setNextProfitOffset] = useState(0)\n+ const [hasMoreProfit, setHasMoreProfit] = useState(true)\n+\n const [contractMetricsOrderedByShares, setContractMetricsOrderedByShares] =\n useState<ContractMetric[] | undefined>(undefined)\n+ const [nextSharesOffset, setNextSharesOffset] = useState(0)\n+ const [hasMoreShares, setHasMoreShares] = useState(true)\n+\n const [metricsCountsByAnswerId, setMetricsCountsByAnswerId] = useState<{\n [key: string]: number\n }>(\n answerDetails\n ? { [answerDetails.answer.id]: answerDetails.totalPositions }\n : {}\n )\n- const answers = answer\n- ? [answer]\n- : contract.mechanism === 'cpmm-multi-1'\n- ? contract.answers\n- : []\n+ const answers =\n+ answer || contract.mechanism !== 'cpmm-multi-1' ? [] : contract.answers\n \n const [currentAnswerId, setCurrentAnswerId] = useState<string | undefined>(\n answers.length > 0\n ? getMainBinaryMCAnswer(contract)?.id ??\n first(orderBy(answers, 'totalLiquidity', 'desc'))?.id\n : undefined\n )\n const [page, setPage] = useState(0)\n- const [loading, setLoading] = useState(true)\n+ const [loading, setLoading] = useState(false)\n const [totalYesPositions, setTotalYesPositions] = useState(0)\n const [totalNoPositions, setTotalNoPositions] = useState(0)\n const [sortBy, setSortBy] = useState<'profit' | 'shares'>(\n contract.isResolved ? 'profit' : 'shares'\n )\n \n const updateContractMetrics = async (\n- sortBy: 'profit' | 'shares',\n- answerId?: string\n+ newSortBy: 'profit' | 'shares',\n+ answerIdForShares?: string,\n+ loadMore = false\n ) => {\n+ const currentLoading = loading\n+ const currentHasMore =\n+ newSortBy === 'profit' ? hasMoreProfit : hasMoreShares\n+\n+ if (loadMore && (currentLoading || !currentHasMore)) {\n+ if (!currentHasMore && !currentLoading) setLoading(false) // If no more, ensure loading is false\n+ return\n+ }\n+ // If a full refresh is requested while loading something else, allow it if it's not for the same sort type or not a loadMore operation.\n+ // This condition might need refinement based on desired UX for rapid changes.\n+ if (currentLoading && !loadMore && newSortBy !== sortBy) {\n+ // Allow if changing sort type\n+ } else if (currentLoading) {\n+ return\n+ }\n+\n setLoading(true)\n+\n+ const offsetToUse = loadMore\n+ ? newSortBy === 'profit'\n+ ? nextProfitOffset\n+ : nextSharesOffset\n+ : 0\n+\n+ if (!loadMore) {\n+ setPage(0) // Reset page on new sort/filter\n+ if (newSortBy === 'profit') {\n+ setContractMetricsOrderedByProfit(undefined)\n+ setNextProfitOffset(0)\n+ setHasMoreProfit(true)\n+ } else {\n+ setContractMetricsOrderedByShares(undefined)\n+ setNextSharesOffset(0)\n+ setHasMoreShares(true)\n+ }\n+ }\n const rows = await getOrderedContractMetricRowsForContractId(\n contractId,\n db,\n- sortBy === 'profit' ? undefined : answerId,\n- sortBy\n+ newSortBy === 'profit' ? undefined : answerIdForShares,\n+ newSortBy,\n+ ROWS_PER_CALL,\n+ offsetToUse\n )\n \n- if (sortBy === 'profit') {\n+ const newMetrics = convertContractMetricRows(rows)\n+\n+ if (newSortBy === 'profit') {\n setContractMetricsOrderedByProfit((prev) =>\n uniqBy(\n- convertContractMetricRows(rows).concat(prev ?? []),\n- (cm) => cm.userId + cm.answerId + cm.contractId\n+ (loadMore && prev ? prev : []).concat(newMetrics),\n+ (cm) => cm.userId + (cm.answerId ?? '') + cm.contractId\n )\n )\n+ setNextProfitOffset(offsetToUse + ROWS_PER_CALL)\n+ if (loadMore) {\n+ if (rows.length === 0) setHasMoreProfit(false)\n+ } else {\n+ setHasMoreProfit(rows.length > 0)\n+ }\n } else {\n setContractMetricsOrderedByShares((prev) =>\n uniqBy(\n- convertContractMetricRows(rows).concat(prev ?? []),\n- (cm) => cm.userId + cm.answerId + cm.contractId\n+ (loadMore && prev ? prev : []).concat(newMetrics),\n+ (cm) => cm.userId + (cm.answerId ?? '') + cm.contractId\n )\n )\n+ setNextSharesOffset(offsetToUse + ROWS_PER_CALL)\n+ if (loadMore) {\n+ if (rows.length === 0) setHasMoreShares(false)\n+ } else {\n+ setHasMoreShares(rows.length > 0)\n+ }\n }\n-\n setLoading(false)\n }\n \n useEffect(() => {\n+ // Initial load\n+ updateContractMetrics(sortBy, currentAnswerId, false)\n+ }, []) // sortBy and currentAnswerId are stable initially from props/useState\n+\n+ // Fetch total counts for YES/NO labels (independent of paged positions)\n+ useEffect(() => {\n getContractMetricsCount(contractId, db, 'yes', currentAnswerId).then(\n setTotalYesPositions\n )\n getContractMetricsCount(contractId, db, 'no', currentAnswerId).then(\n setTotalNoPositions\n )\n }, [currentAnswerId, contractId])\n \n+ // Fetch total positions for all answers (for multi-choice carousel/select)\n const getAllAnswerPositionCounts = async () => {\n if (!setTotalPositions) return\n const count = await getContractMetricsCount(contractId, db)\n setTotalPositions(count)\n- if (contract.mechanism == 'cpmm-1') return\n+ if (contract.mechanism === 'cpmm-1' || answers.length === 0) return\n const allCounts = Object.fromEntries(\n await Promise.all(\n- answers.map(async (answer) => {\n- const count = await getContractMetricsCount(\n+ answers.map(async (ans) => {\n+ const ansCount = await getContractMetricsCount(\n contractId,\n db,\n undefined,\n- answer.id\n+ ans.id\n )\n- return [answer.id, count]\n+ return [ans.id, ansCount]\n })\n )\n )\n setMetricsCountsByAnswerId(allCounts)\n }\n useEffect(() => {\n getAllAnswerPositionCounts()\n- }, [])\n+ }, [contract.id, contract.mechanism, answers, setTotalPositions]) // Added dependencies\n \n const positionsToDisplay = contractMetricsOrderedByShares?.filter((cm) =>\n currentAnswerId ? cm.answerId === currentAnswerId : !cm.answerId\n )\n- const profitPositionsToDisplay = contractMetricsOrderedByProfit?.filter(\n- (cm) => !cm.answerId\n- )\n+ const profitPositionsToDisplay = contractMetricsOrderedByProfit // Already filtered by backend for !cm.answerId effectively when sortBy is profit\n \n+ // Effect to load more data when user scrolls near the end of the list\n+ useEffect(() => {\n+ if (loading) return\n+\n+ const currentHasMore = sortBy === 'profit' ? hasMoreProfit : hasMoreShares\n+ if (!currentHasMore) return\n+\n+ const metricsForPartition =\n+ sortBy === 'profit'\n+ ? profitPositionsToDisplay ?? []\n+ : positionsToDisplay ?? []\n+\n+ if (\n+ metricsForPartition.length === 0 &&\n+ (sortBy === 'profit' ? nextProfitOffset : nextSharesOffset) === 0\n+ ) {\n+ // Initial load might still be in progress or returned nothing, wait for it\n+ return\n+ }\n+\n+ let tempLeftLength = 0\n+ let tempRightLength = 0\n+\n+ if (sortBy === 'profit') {\n+ const [left, right] = partition(\n+ metricsForPartition,\n+ (cm) => cm.profit >= 0\n+ )\n+ tempLeftLength = left.length\n+ tempRightLength = right.length\n+ } else {\n+ // 'shares'\n+ const [left, right] = partition(\n+ metricsForPartition,\n+ (cm) => cm.hasYesShares\n+ )\n+ tempLeftLength = left.length\n+ tempRightLength = right.length\n+ }\n+\n+ const requiredItemsForPageEnd = (page + 1) * USER_TABLE_PAGE_SIZE\n+ const shouldLoadMore =\n+ requiredItemsForPageEnd > tempLeftLength ||\n+ requiredItemsForPageEnd > tempRightLength\n+\n+ if (shouldLoadMore && currentHasMore && !loading) {\n+ updateContractMetrics(sortBy, currentAnswerId, true /* loadMore */)\n+ }\n+ }, [\n+ page,\n+ sortBy,\n+ profitPositionsToDisplay,\n+ positionsToDisplay,\n+ hasMoreProfit,\n+ hasMoreShares,\n+ loading,\n+ currentAnswerId,\n+ nextProfitOffset,\n+ nextSharesOffset,\n+ ])\n+\n if (contract.mechanism === 'cpmm-1' || isBinaryMulti(contract)) {\n return (\n <Col className={'w-full'}>\n <Row className={'mb-2 items-center justify-end gap-2'}>\n@@ -170,24 +289,26 @@\n sort={sortBy === 'profit' ? 'profit' : 'position'}\n onSortClick={() => {\n const newSort = sortBy === 'shares' ? 'profit' : 'shares'\n setSortBy(newSort)\n- setPage(0)\n- updateContractMetrics(newSort, currentAnswerId)\n+ updateContractMetrics(newSort, currentAnswerId, false)\n }}\n />\n </Row>\n <BinaryUserPositionsTable\n- loading={loading}\n+ loading={\n+ loading &&\n+ (sortBy === 'profit' ? nextProfitOffset : nextSharesOffset) === 0\n+ }\n contract={contract}\n positionsByShares={positionsToDisplay}\n positionsByProfit={profitPositionsToDisplay}\n totalYesPositions={totalYesPositions}\n totalNoPositions={totalNoPositions}\n sortBy={sortBy}\n page={page}\n setPage={setPage}\n- loadingPositions={Math.max(totalYesPositions, totalNoPositions)}\n+ pageSize={USER_TABLE_PAGE_SIZE}\n />\n </Col>\n )\n } else if (contract.mechanism === 'cpmm-multi-1') {\n@@ -204,9 +325,9 @@\n key={answer.id}\n selected={answer.id === currentAnswerId}\n onSelect={() => {\n setCurrentAnswerId(answer.id)\n- updateContractMetrics(sortBy, answer.id)\n+ updateContractMetrics(sortBy, answer.id, false)\n }}\n >\n <Row className={'gap-1'}>\n <span className={'max-w-[60px] truncate text-ellipsis'}>\n@@ -230,9 +351,9 @@\n className=\"h-9 w-full max-w-sm\"\n value={currentAnswerId}\n onChange={(e) => {\n setCurrentAnswerId(e.target.value)\n- updateContractMetrics(sortBy, e.target.value)\n+ updateContractMetrics(sortBy, e.target.value, false)\n }}\n >\n {orderBy(\n answers,\n@@ -259,29 +380,26 @@\n sort={sortBy === 'profit' ? 'profit' : 'position'}\n onSortClick={() => {\n const newSort = sortBy === 'shares' ? 'profit' : 'shares'\n setSortBy(newSort)\n- setPage(0)\n- updateContractMetrics(newSort, currentAnswerId)\n+ updateContractMetrics(newSort, currentAnswerId, false)\n }}\n />\n </Row>\n <BinaryUserPositionsTable\n- loading={loading}\n+ loading={\n+ loading &&\n+ (sortBy === 'profit' ? nextProfitOffset : nextSharesOffset) === 0\n+ }\n contract={contract}\n positionsByShares={positionsToDisplay}\n positionsByProfit={profitPositionsToDisplay}\n totalYesPositions={totalYesPositions}\n totalNoPositions={totalNoPositions}\n sortBy={sortBy}\n page={page}\n setPage={setPage}\n- loadingPositions={\n- currentAnswerId && metricsCountsByAnswerId[currentAnswerId]\n- ? // Just an approximation, could be more asymmetric\n- metricsCountsByAnswerId[currentAnswerId] / 2\n- : Math.max(totalYesPositions, totalNoPositions)\n- }\n+ pageSize={USER_TABLE_PAGE_SIZE}\n />\n </Col>\n )\n }\n@@ -299,9 +417,9 @@\n sortBy: 'profit' | 'shares'\n page: number\n setPage: (page: number) => void\n loading: boolean\n- loadingPositions: number\n+ pageSize: number\n }) {\n const {\n contract,\n totalYesPositions,\n@@ -311,11 +429,10 @@\n sortBy,\n page,\n setPage,\n loading,\n- loadingPositions,\n+ pageSize,\n } = props\n- const pageSize = 20\n const currentUser = useUser()\n const followedUsers = useFollows(currentUser?.id)\n const isCashContract = contract.token === 'CASH'\n \n@@ -335,12 +452,9 @@\n (cm) => (sortBy === 'profit' ? -cm.profit : cm.totalShares['NO']),\n 'desc'\n ).slice(page * pageSize, (page + 1) * pageSize)\n \n- const largestColumnLength =\n- leftColumnPositions.length > rightColumnPositions.length\n- ? leftColumnPositions.length\n- : rightColumnPositions.length\n+ const largestColumnLength = Math.max(totalYesPositions, totalNoPositions)\n \n const isBinary =\n contract.outcomeType === 'BINARY' || contract.mechanism === 'cpmm-multi-1'\n const isStonk = contract.outcomeType === 'STONK'\n@@ -401,22 +515,25 @@\n </span>\n )\n }\n \n+ // Calculate placeholders based on totalYes/No positions for the current view.\n+ const maxSidePositions = Math.max(totalYesPositions, totalNoPositions)\n+ const numPlaceholders =\n+ maxSidePositions > 0 ? Math.min(maxSidePositions, pageSize) : pageSize\n+ const itemsToReserveSpaceFor =\n+ maxSidePositions > 0 ? Math.min(maxSidePositions, pageSize) : pageSize\n+\n return (\n <>\n <Col\n className={clsx('w-full')}\n- // To avoid the table height jumping when loading\n style={{\n- minHeight: `${\n- 80 +\n- (loadingPositions > pageSize ? pageSize : loadingPositions) * 57\n- }px`,\n+ minHeight: `${80 + itemsToReserveSpaceFor * 57}px`,\n }}\n >\n {loading ? (\n- <LoadingResults />\n+ <LoadingResults placeholderCount={numPlaceholders} />\n ) : (\n <Row className={'gap-1'}>\n <Col className={'w-1/2'}>\n <Row className={'justify-between p-2'}>\n@@ -596,20 +713,20 @@\n </Col>\n </Row>\n )\n })\n-const LoadingResults = () => {\n+const LoadingResults = ({ placeholderCount }: { placeholderCount: number }) => {\n return (\n <Row className={'gap-1'}>\n <Col className={'w-1/2'}>\n- <LoadingPositionsRows />\n- <LoadingPositionsRows />\n- <LoadingPositionsRows />\n+ {Array.from({ length: placeholderCount }).map((_, i) => (\n+ <LoadingPositionsRows key={`left-loading-${i}`} />\n+ ))}\n </Col>\n <Col className={'w-1/2'}>\n- <LoadingPositionsRows />\n- <LoadingPositionsRows />\n- <LoadingPositionsRows />\n+ {Array.from({ length: placeholderCount }).map((_, i) => (\n+ <LoadingPositionsRows key={`right-loading-${i}`} />\n+ ))}\n </Col>\n </Row>\n )\n }\n"
|
|
1197
|
-
}
|
|
1198
|
-
]
|
|
1199
|
-
},
|
|
1200
|
-
{
|
|
1201
|
-
"id": "reinstate-referrals",
|
|
1202
|
-
"sha": "05cd89045485dbab7fc76388b18e9c38cb2ea455",
|
|
1203
|
-
"parentSha": "2c9045be73e11906b1aba7e6a3bcd5fa904af2d4",
|
|
1204
|
-
"spec": "Implement an end-to-end referral program with share-link attribution and a new backend endpoint.\n\nBackend API\n- Add a new authed POST endpoint 'refer-user' in backend/api/src/refer-user.ts and register it in backend/api/src/routes.ts and common/src/api/schema.ts.\n - Props: { referredByUsername: string; contractId?: string }.\n - Behavior:\n - Look up the referring user by username; reject if not found or banned from posting.\n - Get the authed user as the new user; reject if missing.\n - If contractId is provided, fetch the contract; reject if not found.\n - In a SERIAL transaction:\n - Re-fetch the new user; ensure they have no existing referral details.\n - Ensure the new user account is no older than MINUTES_ALLOWED_TO_REFER.\n - Ensure there is no existing referral txn to the referring user for this referred user (category 'REFERRAL', data.referredUserId == newUser.id), otherwise reject.\n - Create a bank-to-user M$ transaction to the referrer for REFERRAL_AMOUNT with data { referredUserId, referredContractId? }.\n - Update the new user with { referredByUserId, referredByContractId? }.\n - After commit, create a referral notification to the referrer with the bonus amount and (if present) the referring contract.\n - Return { success: true }.\n\nNotifications\n- Update createReferralNotification signature and payload (backend/shared/src/create-notification.ts):\n - New signature: (toUserId: string, referredUser: User, bonusAmount: string, referredByContract?: Contract).\n - Set Notification.reason to 'user_joined_to_bet_on_your_market' if referredByContract.creatorId === toUserId; otherwise 'you_referred_user'.\n - Populate source fields: sourceContractId, sourceContractTitle (question), sourceContractSlug, sourceContractCreatorUsername, and set sourceText to bonusAmount.\n\nRemove legacy referral flow\n- Delete backend/shared/src/distribute-referral-bonus.ts and remove all imports/usages, including from backend/api/src/gidx/complete-checkout-session.ts.\n- Remove REFERRAL_AMOUNT_CASH and REFERRAL_MIN_PURCHASE_DOLLARS from common/src/economy.ts and any code that relies on them (e.g., email copy in backend/shared/src/emails.ts).\n\nShare link utilities\n- Update common/src/util/share.ts:\n - getShareUrl(contract, username?: string) to append referral query when username is provided.\n - getTopicShareUrl(groupSlug, username?: string) to include referral query and use the /browse/{slug} path.\n - Add referralQuery(username) which encodes the username into a short referral parameter '?r=' + base64(username) (strip '='), falling back to '?referrer=' + username.\n - Remove getReferralCodeFromUser.\n\nClient referral capture and apply\n- Add a new hook web/hooks/use-save-referral.ts:\n - Reads URL params: prefer 'r' (base64-decoded to username), fallback 'referrer'.\n - Writes referral info to local storage if the viewer is not signed in (user === null). Accept an optional defaultReferrerUsername and contractId for implicit attribution (e.g., market creator).\n\n- Extend web/lib/firebase/users.ts:\n - Implement writeReferralInfo(defaultReferrerUsername, { contractId?, explicitReferrer? }) to set/override cached referral username and optional contract id in local storage, with explicit referrer taking priority.\n - Implement canSetReferrer(user) to gate referral assignment to new, humanish users without existing referrer and within MINUTES_ALLOWED_TO_REFER.\n - Implement setCachedReferralInfoForUser(user) to call API 'refer-user' with cached details; mark completion in local storage; no-op on failure.\n\nIntegrate referral capture\n- Integrate useSaveReferral(user, options?) on high-traffic pages and views to capture referrals and default attributions:\n - Contract pages: default referrer is contract.creatorUsername, include contractId.\n - Pages such as dashboard (and variants), home, explore, browse, AI forecast, profile pages.\n - After onboarding welcome (or when skipping), call setCachedReferralInfoForUser(user) to apply the referral promptly.\n\nShare/tweet buttons and QR\n- Update all share link invocations to pass the current user’s username to getShareUrl/getTopicShareUrl so outbound links carry referral.\n- Update tweet helper functions (web/components/buttons/tweet-button.tsx):\n - getPositionTweet and getWinningTweet accept username and embed it in the shared URL.\n- Update share QR and copy-link components to use the referral-aware URLs.\n\nNavigation and referrals page\n- Add a sidebar navigation link to '/referrals'.\n- Implement the referrals page showing the personal referral link (domain + referralQuery(username)), QR code, and a brief explainer of the bonus amount.\n\nContract/topic share URLs\n- Ensure all instances of getShareUrl and getTopicShareUrl are updated to new signatures and paths across contract headers, creator panels, embeds, buttons, and topic title components.\n\nAnalytics\n- Track a public event when a referral is set on the backend (refer-user) including referredByUserId and optional referredByContractId.\n\nValidation and types\n- Ensure common/src/notification.ts supports the reasons used: 'you_referred_user' and 'user_joined_to_bet_on_your_market', and accepts the new source fields set in createReferralNotification.\n- Ensure API schema addition for 'refer-user' matches the handler’s expectations.\n\nAcceptance criteria\n- Visiting a shared link with '?r=<base64 username>' or '?referrer=<username>' while signed out caches the referrer and optional contract id.\n- Creating a new humanish account within the allowed time window automatically triggers a referral award to the referrer (M$ REFERRAL_AMOUNT) and sets referredBy fields on the new user; referrer receives an in-app notification, with market-aware reason when applicable.\n- All share links and tweets include the referral query when available.\n- Old purchase-based referral bonus logic is removed and no longer triggers any referral payouts.\n- The /referrals page shows a working personal link and QR code with the encoded referral query.",
|
|
1205
|
-
"prompt": "Reintroduce a full referral program: allow users to share links that append a short referral code, capture that referral code client-side for signed-out visitors, and upon signup attribute and reward the correct referrer. Add a public API to finalize the referral attribution and send a notification. Update site-wide share links and tweets to include referral info. Provide a simple referrals page and nav entry. Remove the previous purchase-triggered referral bonus and any unused constants or email content tied to it. Ensure attribution only applies to newly created, human-like accounts within a short time window.",
|
|
1206
|
-
"supplementalFiles": [
|
|
1207
|
-
"common/src/user.ts",
|
|
1208
|
-
"common/src/notification.ts",
|
|
1209
|
-
"backend/shared/src/supabase/init.ts",
|
|
1210
|
-
"web/lib/api/api.ts",
|
|
1211
|
-
"web/hooks/use-defined-search-params.ts",
|
|
1212
|
-
"web/lib/util/local.ts",
|
|
1213
|
-
"common/src/util/clean-username.ts"
|
|
1214
|
-
],
|
|
1215
|
-
"fileDiffs": [
|
|
1216
|
-
{
|
|
1217
|
-
"path": "backend/api/src/gidx/complete-checkout-session.ts",
|
|
1218
|
-
"status": "modified",
|
|
1219
|
-
"diff": "Index: backend/api/src/gidx/complete-checkout-session.ts\n===================================================================\n--- backend/api/src/gidx/complete-checkout-session.ts\t2c9045b (parent)\n+++ backend/api/src/gidx/complete-checkout-session.ts\t05cd890 (commit)\n@@ -1,27 +1,22 @@\n import { APIError, APIHandler } from 'api/helpers/endpoint'\n-import {\n- PaymentAmount,\n- REFERRAL_MIN_PURCHASE_DOLLARS,\n- WEB_PRICES,\n-} from 'common/economy'\n+import { PaymentAmount, WEB_PRICES } from 'common/economy'\n import { SWEEP_PRODUCTION_ENABLED } from 'common/envs/constants'\n import {\n CompleteSessionDirectCashierResponse,\n ProcessSessionCode,\n } from 'common/gidx/gidx'\n import { User } from 'common/user'\n import { getIp, track, trackPublicEvent } from 'shared/analytics'\n-import { distributeReferralBonusIfNoneGiven } from 'shared/distribute-referral-bonus'\n import {\n getGIDXStandardParams,\n getLocalServerIP,\n getUserRegistrationRequirements,\n GIDX_BASE_URL,\n } from 'shared/gidx/helpers'\n import { log } from 'shared/monitoring/log'\n import { createSupabaseDirectClient } from 'shared/supabase/init'\n-import { getReferrerInfo, updateUser } from 'shared/supabase/users'\n+import { updateUser } from 'shared/supabase/users'\n import { runTxnInBetQueue } from 'shared/txn/run-txn'\n import { getUser, LOCAL_DEV } from 'shared/utils'\n \n const ENDPOINT = GIDX_BASE_URL + '/v3.0/api/DirectCashier/CompleteSession'\n@@ -247,11 +242,8 @@\n category: 'CASH_BONUS',\n description: `Free sweepcash bonus for purchasing mana.`,\n } as const\n \n- const referrerInfo = user.usedReferralCode\n- ? await getReferrerInfo(pg, user.referredByUserId)\n- : undefined\n await pg.tx(async (tx) => {\n await runTxnInBetQueue(tx, manaPurchaseTxn)\n if (isSweepsVerified) {\n await runTxnInBetQueue(tx, cashBonusTxn)\n@@ -259,16 +251,8 @@\n await updateUser(tx, userId, {\n purchasedMana: true,\n purchasedSweepcash: isSweepsVerified,\n })\n- if (referrerInfo && paidInCents / 100 >= REFERRAL_MIN_PURCHASE_DOLLARS) {\n- await distributeReferralBonusIfNoneGiven(\n- tx,\n- user,\n- referrerInfo.id,\n- referrerInfo.sweepsVerified\n- )\n- }\n })\n \n await trackPublicEvent(user.id, 'M$ purchase', {\n amount: amount.mana,\n"
|
|
1220
|
-
},
|
|
1221
|
-
{
|
|
1222
|
-
"path": "backend/api/src/refer-user.ts",
|
|
1223
|
-
"status": "added",
|
|
1224
|
-
"diff": "Index: backend/api/src/refer-user.ts\n===================================================================\n--- backend/api/src/refer-user.ts\t2c9045b (parent)\n+++ backend/api/src/refer-user.ts\t05cd890 (commit)\n@@ -0,0 +1,131 @@\n+import { APIError, APIHandler } from 'api/helpers/endpoint'\n+import { MINUTES_ALLOWED_TO_REFER } from 'common/user'\n+import { Contract } from 'common/contract'\n+import { createSupabaseDirectClient, SERIAL_MODE } from 'shared/supabase/init'\n+import { REFERRAL_AMOUNT } from 'common/economy'\n+import { createReferralNotification } from 'shared/create-notification'\n+import { convertUser } from 'common/supabase/users'\n+import { first } from 'lodash'\n+import { log, getContractSupabase, getUser } from 'shared/utils'\n+import { runTxnFromBank } from 'shared/txn/run-txn'\n+import { MINUTE_MS } from 'common/util/time'\n+import { removeUndefinedProps } from 'common/util/object'\n+import { trackPublicEvent } from 'shared/analytics'\n+import { updateUser } from 'shared/supabase/users'\n+\n+export const referUser: APIHandler<'refer-user'> = async (props, auth) => {\n+ const { referredByUsername, contractId } = props\n+\n+ const pg = createSupabaseDirectClient()\n+ const referredByUser = first(\n+ await pg.map(\n+ `select * from users where username = $1`,\n+ [referredByUsername],\n+ (row) => convertUser(row)\n+ )\n+ )\n+ if (!referredByUser) {\n+ throw new APIError(404, `User ${referredByUsername} not found`)\n+ }\n+ if (referredByUser.isBannedFromPosting) {\n+ throw new APIError(\n+ 404,\n+ `User ${referredByUsername} is banned from posting, not eligible for referral bonus`\n+ )\n+ }\n+ const newUser = await getUser(auth.uid)\n+ if (!newUser) {\n+ throw new APIError(401, `User ${auth.uid} not found`)\n+ }\n+ let referredByContract: Contract | undefined\n+ if (contractId) {\n+ referredByContract = await getContractSupabase(contractId)\n+ if (!referredByContract) {\n+ throw new APIError(404, `Contract ${contractId} not found`)\n+ }\n+ log(`referredByContract: ${referredByContract.slug}`)\n+ }\n+ await handleReferral(newUser.id, referredByUser.id, referredByContract)\n+ await trackPublicEvent(newUser.id, 'Referral', {\n+ referredByUserId: referredByUser.id,\n+ referredByContractId: contractId,\n+ })\n+\n+ return { success: true }\n+}\n+\n+async function handleReferral(\n+ newUserId: string,\n+ referredByUserId: string,\n+ referredByContract?: Contract\n+) {\n+ const pg = createSupabaseDirectClient()\n+ log(`referredByUserId: ${referredByUserId}`)\n+ const { txn, user } = await pg.tx({ mode: SERIAL_MODE }, async (tx) => {\n+ const newUser = await getUser(newUserId, tx)\n+ if (!newUser) throw new APIError(500, `User ${newUserId} not found`)\n+\n+ if (newUser.referredByUserId || newUser.referredByContractId) {\n+ throw new APIError(400, `User ${newUser.id} already has referral details`)\n+ }\n+ if (\n+ newUser.createdTime <\n+ Date.now() - MINUTES_ALLOWED_TO_REFER * MINUTE_MS\n+ ) {\n+ throw new APIError(400, `User ${newUser.id} is too old to be referred`)\n+ }\n+\n+ const txns = await tx.one(\n+ `select count(*) from txns where to_id = $1\n+ and category = 'REFERRAL'\n+ and data->>'referredUserId' = $2`,\n+ [referredByUserId, newUser.id],\n+ (row) => row.count\n+ )\n+\n+ // If the referring user already has a referral txn due to referring this user, halt\n+ // TODO: store in data instead\n+ if (txns > 0) {\n+ throw new APIError(\n+ 404,\n+ 'Existing referral bonus found with matching details'\n+ )\n+ }\n+ log('creating referral txns')\n+\n+ // if they're updating their referredId, create a txn for both\n+ const txnData = {\n+ fromType: 'BANK',\n+ toId: referredByUserId,\n+ toType: 'USER',\n+ amount: REFERRAL_AMOUNT,\n+ token: 'M$',\n+ category: 'REFERRAL',\n+ description: `Referred new user id: ${newUser.id} for ${REFERRAL_AMOUNT}`,\n+ data: removeUndefinedProps({\n+ referredUserId: newUser.id,\n+ referredContractId: referredByContract?.id,\n+ }),\n+ } as const\n+\n+ const txn = await runTxnFromBank(tx, txnData)\n+\n+ await updateUser(\n+ tx,\n+ newUserId,\n+ removeUndefinedProps({\n+ referredByUserId,\n+ referredByContractId: referredByContract?.id,\n+ })\n+ )\n+\n+ return { txn, user: newUser }\n+ })\n+\n+ await createReferralNotification(\n+ referredByUserId,\n+ user,\n+ txn.amount.toString(),\n+ referredByContract\n+ )\n+}\n"
|
|
1225
|
-
},
|
|
1226
|
-
{
|
|
1227
|
-
"path": "backend/api/src/routes.ts",
|
|
1228
|
-
"status": "modified",
|
|
1229
|
-
"diff": "Index: backend/api/src/routes.ts\n===================================================================\n--- backend/api/src/routes.ts\t2c9045b (parent)\n+++ backend/api/src/routes.ts\t05cd890 (commit)\n@@ -173,9 +173,9 @@\n } from './generate-ai-date-ranges'\n import { inferNumericUnit } from './infer-numeric-unit'\n import { generateConciseTitle } from './generate-concise-title'\n import { getCloseDateEndpoint } from './get-close-date'\n-\n+import { referUser } from './refer-user'\n export const handlers: { [k in APIPath]: APIHandler<k> } = {\n 'refresh-all-clients': refreshAllClients,\n bet: placeBet,\n 'multi-bet': placeMultiBet,\n@@ -360,5 +360,6 @@\n 'generate-ai-date-ranges': generateAIDateRanges,\n 'regenerate-date-midpoints': regenerateDateMidpoints,\n 'generate-concise-title': generateConciseTitle,\n 'get-close-date': getCloseDateEndpoint,\n+ 'refer-user': referUser,\n }\n"
|
|
1230
|
-
},
|
|
1231
|
-
{
|
|
1232
|
-
"path": "backend/shared/src/create-notification.ts",
|
|
1233
|
-
"status": "modified",
|
|
1234
|
-
"diff": "Index: backend/shared/src/create-notification.ts\n===================================================================\n--- backend/shared/src/create-notification.ts\t2c9045b (parent)\n+++ backend/shared/src/create-notification.ts\t05cd890 (commit)\n@@ -10,9 +10,8 @@\n NOTIFICATION_DESCRIPTIONS,\n notification_reason_types,\n NotificationReason,\n PaymentCompletedData,\n- ReferralData,\n ReviewNotificationData,\n UniqueBettorData,\n } from 'common/notification'\n import {\n@@ -690,9 +689,10 @@\n \n export const createReferralNotification = async (\n toUserId: string,\n referredUser: User,\n- bonusAmounts: ReferralData\n+ bonusAmount: string,\n+ referredByContract?: Contract\n ) => {\n const privateUser = await getPrivateUser(toUserId)\n if (!privateUser) return\n const { sendToBrowser } = getNotificationDestinationsForUser(\n@@ -703,19 +703,29 @@\n \n const notification: Notification = {\n id: referredUser.id + '-signup-referral-bonus',\n userId: toUserId,\n- reason: 'you_referred_user',\n+ reason:\n+ referredByContract?.creatorId === toUserId\n+ ? 'user_joined_to_bet_on_your_market'\n+ : 'you_referred_user',\n createdTime: Date.now(),\n isSeen: false,\n sourceId: referredUser.id,\n sourceType: 'user',\n+ sourceContractId: referredByContract?.id,\n+\n sourceUpdateType: 'updated',\n sourceUserName: referredUser.name,\n sourceUserUsername: referredUser.username,\n sourceUserAvatarUrl: referredUser.avatarUrl,\n- sourceText: '',\n- data: bonusAmounts,\n+ sourceText: bonusAmount,\n+ // Only pass the contract referral details if they weren't referred to a group\n+ sourceContractCreatorUsername: referredByContract?.creatorUsername,\n+ sourceContractTitle: referredByContract?.question,\n+ sourceContractSlug: referredByContract?.slug,\n+ sourceSlug: referredByContract?.slug,\n+ sourceTitle: referredByContract?.question,\n }\n const pg = createSupabaseDirectClient()\n await insertNotificationToSupabase(notification, pg)\n // TODO send email notification\n"
|
|
1235
|
-
},
|
|
1236
|
-
{
|
|
1237
|
-
"path": "backend/shared/src/distribute-referral-bonus.ts",
|
|
1238
|
-
"status": "deleted",
|
|
1239
|
-
"diff": "Index: backend/shared/src/distribute-referral-bonus.ts\n===================================================================\n--- backend/shared/src/distribute-referral-bonus.ts\t2c9045b (parent)\n+++ backend/shared/src/distribute-referral-bonus.ts\t05cd890 (commit)\n@@ -1,75 +0,0 @@\n-import { SupabaseTransaction } from 'shared/supabase/init'\n-import { runTxnFromBank } from 'shared/txn/run-txn'\n-import { REFERRAL_AMOUNT, REFERRAL_AMOUNT_CASH } from 'common/economy'\n-import { createReferralNotification } from './create-notification'\n-import { User } from 'common/user'\n-\n-export async function distributeReferralBonusIfNoneGiven(\n- tx: SupabaseTransaction,\n- user: User,\n- referrerId: string,\n- referrerSweepsVerified: boolean | undefined\n-) {\n- const userId = user.id\n- const previousReferralTxn = await tx.oneOrNone(\n- `select 1 from txns where to_id = $1\n- and category = 'REFERRAL'\n- and token = 'M$'\n- and data->'data'->>'referredById' = $2`,\n- [userId, referrerId]\n- )\n- if (previousReferralTxn) {\n- return\n- }\n- await runTxnFromBank(tx, {\n- category: 'REFERRAL',\n- token: 'M$',\n- amount: REFERRAL_AMOUNT,\n- fromType: 'BANK',\n- toType: 'USER',\n- toId: userId,\n- data: {\n- referredById: referrerId,\n- },\n- })\n- await runTxnFromBank(tx, {\n- category: 'REFERRAL',\n- token: 'CASH',\n- amount: REFERRAL_AMOUNT_CASH,\n- fromType: 'BANK',\n- toType: 'USER',\n- toId: userId,\n- data: {\n- referredById: referrerId,\n- },\n- })\n-\n- await runTxnFromBank(tx, {\n- category: 'REFERRAL',\n- token: 'M$',\n- amount: REFERRAL_AMOUNT,\n- fromType: 'BANK',\n- toType: 'USER',\n- toId: referrerId,\n- data: {\n- referredId: userId,\n- },\n- })\n- if (referrerSweepsVerified) {\n- await runTxnFromBank(tx, {\n- category: 'REFERRAL',\n- token: 'CASH',\n- amount: REFERRAL_AMOUNT_CASH,\n- fromType: 'BANK',\n- toType: 'USER',\n- toId: referrerId,\n- data: {\n- referredId: userId,\n- },\n- })\n- }\n- await createReferralNotification(referrerId, user, {\n- manaAmount: REFERRAL_AMOUNT,\n- cashAmount: referrerSweepsVerified ? REFERRAL_AMOUNT_CASH : 0,\n- })\n-}\n"
|
|
1240
|
-
},
|
|
1241
|
-
{
|
|
1242
|
-
"path": "backend/shared/src/emails.ts",
|
|
1243
|
-
"status": "modified",
|
|
1244
|
-
"diff": "Index: backend/shared/src/emails.ts\n===================================================================\n--- backend/shared/src/emails.ts\t2c9045b (parent)\n+++ backend/shared/src/emails.ts\t05cd890 (commit)\n@@ -8,9 +8,8 @@\n MultiContract,\n renderResolution,\n } from 'common/contract'\n import { PrivateUser, User } from 'common/user'\n-import { getReferralCodeFromUser } from 'common/util/share'\n import {\n formatLargeNumber,\n formatMoney,\n formatSweepies,\n@@ -34,9 +33,8 @@\n createSupabaseDirectClient,\n } from 'shared/supabase/init'\n import { getLoverRow } from 'common/love/lover'\n import { HOUR_MS } from 'common/util/time'\n-import { REFERRAL_AMOUNT, REFERRAL_AMOUNT_CASH } from 'common/economy'\n \n export type PerContractInvestmentsData = {\n questionTitle: string\n questionUrl: string\n@@ -540,14 +538,8 @@\n 'interesting-markets',\n {\n name: firstName,\n unsubscribeUrl,\n- referralCode: getReferralCodeFromUser(privateUser.id),\n- referralManaAmount: formatMoney(REFERRAL_AMOUNT).replace(\n- ENV_CONFIG.moneyMoniker,\n- ''\n- ),\n- referralCashAmount: REFERRAL_AMOUNT_CASH.toFixed(0),\n question1Title: contractsToSend[0].question,\n question1Link: contractUrl(contractsToSend[0]),\n question1ImgSrc: imageSourceUrl(contractsToSend[0]),\n question2Title: contractsToSend[1].question,\n"
|
|
1245
|
-
},
|
|
1246
|
-
{
|
|
1247
|
-
"path": "common/src/api/schema.ts",
|
|
1248
|
-
"status": "modified",
|
|
1249
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\t2c9045b (parent)\n+++ common/src/api/schema.ts\t05cd890 (commit)\n@@ -2302,8 +2302,20 @@\n })\n .strict(),\n returns: {} as { closeTime: number },\n },\n+ 'refer-user': {\n+ method: 'POST',\n+ visibility: 'public',\n+ authed: true,\n+ props: z\n+ .object({\n+ referredByUsername: z.string(),\n+ contractId: z.string().optional(),\n+ })\n+ .strict(),\n+ returns: {} as { success: boolean },\n+ },\n } as const)\n \n export type APIPath = keyof typeof API\n export type APISchema<N extends APIPath> = (typeof API)[N]\n"
|
|
1250
|
-
},
|
|
1251
|
-
{
|
|
1252
|
-
"path": "common/src/economy.ts",
|
|
1253
|
-
"status": "modified",
|
|
1254
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\t2c9045b (parent)\n+++ common/src/economy.ts\t05cd890 (commit)\n@@ -46,10 +46,8 @@\n export const SUS_STARTING_BALANCE = 10\n export const PHONE_VERIFICATION_BONUS = 1000\n \n export const REFERRAL_AMOUNT = 1_000\n-export const REFERRAL_AMOUNT_CASH = 10.0\n-export const REFERRAL_MIN_PURCHASE_DOLLARS = 20\n \n const uniqueBettorBonusAmounts = [3, 10, 15, 20]\n export const getUniqueBettorBonusAmount = (\n liquidity: number,\n"
|
|
1255
|
-
},
|
|
1256
|
-
{
|
|
1257
|
-
"path": "common/src/util/share.ts",
|
|
1258
|
-
"status": "modified",
|
|
1259
|
-
"diff": "Index: common/src/util/share.ts\n===================================================================\n--- common/src/util/share.ts\t2c9045b (parent)\n+++ common/src/util/share.ts\t05cd890 (commit)\n@@ -1,11 +1,23 @@\n import { Contract } from 'common/contract'\n import { ENV_CONFIG } from 'common/envs/constants'\n \n-export const getShareUrl = (contract: Contract) =>\n- `https://${ENV_CONFIG.domain}/${contract.creatorUsername}/${contract.slug}`\n+export const getShareUrl = (contract: Contract, username: string | undefined) =>\n+ `https://${ENV_CONFIG.domain}/${contract.creatorUsername}/${contract.slug}${\n+ username ? referralQuery(username) : ''\n+ }`\n \n-export const getTopicShareUrl = (groupSlug: string) =>\n- `https://${ENV_CONFIG.domain}/topic/${groupSlug}`\n+export const getTopicShareUrl = (\n+ groupSlug: string,\n+ username: string | undefined\n+) =>\n+ `https://${ENV_CONFIG.domain}/browse/${groupSlug}${\n+ username ? referralQuery(username) : ''\n+ }`\n \n-export const getReferralCodeFromUser = (userId: string | undefined) =>\n- (userId?.slice(0, 5) ?? '').toUpperCase().replace(/0/g, '#')\n+export const referralQuery = (username: string) => {\n+ try {\n+ return '?r=' + btoa(username).replace(/=/g, '')\n+ } catch (e) {\n+ return '?referrer=' + username\n+ }\n+}\n"
|
|
1260
|
-
},
|
|
1261
|
-
{
|
|
1262
|
-
"path": "web/components/ai-forecast.tsx",
|
|
1263
|
-
"status": "modified",
|
|
1264
|
-
"diff": "Index: web/components/ai-forecast.tsx\n===================================================================\n--- web/components/ai-forecast.tsx\t2c9045b (parent)\n+++ web/components/ai-forecast.tsx\t05cd890 (commit)\n@@ -25,8 +25,10 @@\n import { LiaKiwiBirdSolid } from 'react-icons/lia'\n import TooltipComponent from 'web/components/widgets/tooltip'\n import { SizedBinaryChart } from 'web/components/charts/contract/binary'\n import { getBetPoints } from 'common/bets'\n+import { useUser } from 'web/hooks/use-user'\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n \n // Shared background pattern for all cards\n const BG_PATTERN_LIGHT =\n \"bg-[url(\\\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23000000' fill-opacity='0.02' fill-rule='evenodd'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/svg%3E\\\")]\"\n@@ -488,9 +490,9 @@\n () => contracts.find((c) => c.id === marketId),\n [contracts, marketId]\n )\n \n- // Always call hooks unconditionally\n+ // eslint-disable-next-line react-hooks/rules-of-hooks\n const liveContract = contract ? useLiveContract(contract) : null\n \n // Get the expected value if it's a numeric contract\n const numericValue =\n@@ -1011,8 +1013,9 @@\n })\n }, [cards, contracts])\n \n const contractsWithLiveData = contractsWithLive.map(({ card, contract }) => {\n+ // eslint-disable-next-line react-hooks/rules-of-hooks\n const liveContract = contract ? useLiveContract(contract) : null\n return { card, contract, liveContract }\n })\n \n@@ -1169,12 +1172,15 @@\n whenAgi,\n contracts = [],\n hideTitle,\n }: AIForecastProps) {\n+ // eslint-disable-next-line react-hooks/rules-of-hooks\n const liveWhenAgi = whenAgi && whenAgi.id ? useLiveContract(whenAgi) : null\n const expectedValueAGI = liveWhenAgi\n ? getNumberExpectedValue(liveWhenAgi)\n : 2030\n+ const user = useUser()\n+ useSaveReferral(user)\n const eventYear = Math.floor(expectedValueAGI)\n const eventMonth = Math.round((expectedValueAGI - eventYear) * 12)\n const expectedYear = new Date(eventYear, eventMonth, 1)\n \n"
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
"path": "web/components/bet/bet-summary.tsx",
|
|
1268
|
-
"status": "modified",
|
|
1269
|
-
"diff": "Index: web/components/bet/bet-summary.tsx\n===================================================================\n--- web/components/bet/bet-summary.tsx\t2c9045b (parent)\n+++ web/components/bet/bet-summary.tsx\t05cd890 (commit)\n@@ -20,8 +20,9 @@\n import { NoLabel, YesLabel } from '../outcome-label'\n import { ProfitBadge } from '../profit-badge'\n import { InfoTooltip } from '../widgets/info-tooltip'\n import { MoneyDisplay } from './money-display'\n+import { useUser } from 'web/hooks/use-user'\n \n export function UserBetsSummary(props: {\n contract: Contract\n initialMetrics?: ContractMetric\n@@ -77,8 +78,9 @@\n const isStonk = outcomeType === 'STONK'\n const mainBinaryMCAnswer = getMainBinaryMCAnswer(contract)\n const prob = contract.mechanism === 'cpmm-1' ? getProbability(contract) : 0\n const expectation = prob * yesWinnings + (1 - prob) * noWinnings\n+ const user = useUser()\n \n if (metrics.invested === 0 && metrics.profit === 0) return null\n \n const isCashContract = contract.token === 'CASH'\n@@ -244,9 +246,13 @@\n {areYourBets ? 'You' : 'They'} made{' '}\n <MoneyDisplay amount={profit} isCashContract={isCashContract} /> in\n profit!{' '}\n <TweetButton\n- tweetText={getWinningTweet(profit, contract)}\n+ tweetText={getWinningTweet(\n+ profit,\n+ contract,\n+ user?.username ?? ''\n+ )}\n className=\"ml-2\"\n />\n </div>\n </Row>\n"
|
|
1270
|
-
},
|
|
1271
|
-
{
|
|
1272
|
-
"path": "web/components/bet/sell-row.tsx",
|
|
1273
|
-
"status": "modified",
|
|
1274
|
-
"diff": "Index: web/components/bet/sell-row.tsx\n===================================================================\n--- web/components/bet/sell-row.tsx\t2c9045b (parent)\n+++ web/components/bet/sell-row.tsx\t05cd890 (commit)\n@@ -95,9 +95,10 @@\n <TweetButton\n tweetText={getPositionTweet(\n (sharesOutcome === 'NO' ? -1 : 1) * shares,\n metric.invested,\n- contract\n+ contract,\n+ user?.username ?? ''\n )}\n />\n )}\n </Row>\n"
|
|
1275
|
-
},
|
|
1276
|
-
{
|
|
1277
|
-
"path": "web/components/buttons/referrals-button.tsx",
|
|
1278
|
-
"status": "modified",
|
|
1279
|
-
"diff": "Index: web/components/buttons/referrals-button.tsx\n===================================================================\n--- web/components/buttons/referrals-button.tsx\t2c9045b (parent)\n+++ web/components/buttons/referrals-button.tsx\t05cd890 (commit)\n@@ -2,9 +2,8 @@\n import { DisplayUser } from 'common/api/user-types'\n import { REFERRAL_AMOUNT } from 'common/economy'\n import { getReferralCount } from 'common/supabase/referrals'\n import { User } from 'common/user'\n-import { getReferralCodeFromUser } from 'common/util/share'\n import { useEffect, useState } from 'react'\n import { CopyLinkRow } from 'web/components/buttons/copy-link-button'\n import { Row } from 'web/components/layout/row'\n import { Avatar } from 'web/components/widgets/avatar'\n@@ -17,8 +16,10 @@\n import { getReferrals } from 'web/lib/supabase/referrals'\n import { Col } from '../layout/col'\n import { CASH_COLOR } from '../portfolio/portfolio-graph'\n import { Subtitle } from '../widgets/subtitle'\n+import { ENV_CONFIG } from 'common/envs/constants'\n+import { referralQuery } from 'common/util/share'\n \n export const useReferralCount = (user: User) => {\n const [referralCount, setReferralCount] = useState(0)\n useEffect(() => {\n@@ -41,9 +42,11 @@\n const currentUser = useUser()\n const isYou = currentUser?.id === user.id\n \n const referredByUser = useDisplayUserById(user.referredByUserId)\n- const url = getReferralCodeFromUser(currentUser?.id)\n+ const url = `https://${ENV_CONFIG.domain}${referralQuery(\n+ user?.username ?? ''\n+ )}`\n \n return (\n <Col className=\"bg-canvas-0 rounded p-6\">\n {isYou && (\n"
|
|
1280
|
-
},
|
|
1281
|
-
{
|
|
1282
|
-
"path": "web/components/buttons/share-qr-button.tsx",
|
|
1283
|
-
"status": "modified",
|
|
1284
|
-
"diff": "Index: web/components/buttons/share-qr-button.tsx\n===================================================================\n--- web/components/buttons/share-qr-button.tsx\t2c9045b (parent)\n+++ web/components/buttons/share-qr-button.tsx\t05cd890 (commit)\n@@ -4,16 +4,17 @@\n import { useState } from 'react'\n import { Modal } from '../layout/modal'\n import { QRCode } from '../widgets/qr-code'\n import { Button } from './button'\n+import { useUser } from 'web/hooks/use-user'\n \n export function ShareQRButton(props: {\n contract: Contract\n className?: string\n }) {\n const { contract, className } = props\n- const shareUrl = getShareUrl(contract)\n-\n+ const user = useUser()\n+ const shareUrl = getShareUrl(contract, user?.username)\n const [open, setOpen] = useState(false)\n \n return (\n <>\n"
|
|
1285
|
-
},
|
|
1286
|
-
{
|
|
1287
|
-
"path": "web/components/buttons/tweet-button.tsx",
|
|
1288
|
-
"status": "modified",
|
|
1289
|
-
"diff": "Index: web/components/buttons/tweet-button.tsx\n===================================================================\n--- web/components/buttons/tweet-button.tsx\t2c9045b (parent)\n+++ web/components/buttons/tweet-button.tsx\t05cd890 (commit)\n@@ -41,21 +41,30 @@\n \n export const getPositionTweet = (\n position: number,\n invested: number,\n- contract: Contract\n+ contract: Contract,\n+ username: string\n ) => {\n const p = invested / Math.abs(position)\n const prob = formatPercent(position > 0 ? p : 1 - p)\n const side = position > 0 ? 'greater' : 'less'\n \n return `I'm predicting there's a ${side} than ${prob} chance. ${getShareUrl(\n- contract\n+ contract,\n+ username\n )}`\n }\n \n-export const getWinningTweet = (profit: number, contract: Contract) => {\n+export const getWinningTweet = (\n+ profit: number,\n+ contract: Contract,\n+ username: string\n+) => {\n const isCashContract = contract.token === 'CASH'\n return `I made ${isCashContract ? SWEEPIES_MONIKER : 'M$'}${formatMoneyNumber(\n profit\n- )} in profit trading on\\n'${contract.question}'! ${getShareUrl(contract)}`\n+ )} in profit trading on\\n'${contract.question}'! ${getShareUrl(\n+ contract,\n+ username\n+ )}`\n }\n"
|
|
1290
|
-
},
|
|
1291
|
-
{
|
|
1292
|
-
"path": "web/components/contract/contract-page.tsx",
|
|
1293
|
-
"status": "modified",
|
|
1294
|
-
"diff": "Index: web/components/contract/contract-page.tsx\n===================================================================\n--- web/components/contract/contract-page.tsx\t2c9045b (parent)\n+++ web/components/contract/contract-page.tsx\t05cd890 (commit)\n@@ -75,8 +75,9 @@\n import { api } from 'web/lib/api/api'\n import { shouldHideGraph } from 'common/contract-params'\n import { CreatorSharePanel, NonCreatorSharePanel } from './creator-share-panel'\n import { FollowMarketButton } from '../buttons/follow-market-button'\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n \n export function ContractPageContent(props: ContractParams) {\n const {\n comments,\n@@ -103,9 +104,12 @@\n \n const liveContract =\n !isPlay && liveCashContract ? liveCashContract : livePlayContract\n const user = useUser()\n-\n+ useSaveReferral(user, {\n+ defaultReferrerUsername: props.contract.creatorUsername,\n+ contractId: props.contract.id,\n+ })\n // Read and set play state from the query\n useEffect(() => {\n if (!router.isReady) return\n const playQuery = router.query.play\n"
|
|
1295
|
-
},
|
|
1296
|
-
{
|
|
1297
|
-
"path": "web/components/contract/creator-share-panel.tsx",
|
|
1298
|
-
"status": "modified",
|
|
1299
|
-
"diff": "Index: web/components/contract/creator-share-panel.tsx\n===================================================================\n--- web/components/contract/creator-share-panel.tsx\t2c9045b (parent)\n+++ web/components/contract/creator-share-panel.tsx\t05cd890 (commit)\n@@ -16,8 +16,9 @@\n import { getUniqueBettorBonusAmount } from 'common/economy'\n import { AddBoostButton } from './add-boost-button'\n import { BoostAnalytics } from './boost-analytics'\n import { Col } from '../layout/col'\n+import { useUser } from 'web/hooks/use-user'\n \n export function CreatorSharePanel(props: { contract: Contract }) {\n const { contract } = props\n return (\n@@ -29,9 +30,12 @@\n )}\n <AddBoostButton contract={contract} />\n <ShareLinkButton contract={contract} />\n <TweetButton\n- tweetText={'I created a question. ' + getShareUrl(contract)}\n+ tweetText={\n+ 'I created a question. ' +\n+ getShareUrl(contract, contract.creatorUsername)\n+ }\n className=\"hidden sm:flex\"\n />\n {contract.outcomeType == 'BOUNTIED_QUESTION' && (\n <CancelBountyButton contract={contract} />\n@@ -72,9 +76,9 @@\n <AddBoostButton contract={contract} />\n {children}\n <ShareLinkButton contract={contract} />\n <TweetButton\n- tweetText={getShareUrl(contract)}\n+ tweetText={getShareUrl(contract, contract.creatorUsername)}\n className=\"hidden sm:flex\"\n />\n </Row>\n )\n@@ -82,9 +86,10 @@\n \n const ShareLinkButton = (props: { contract: Contract; className?: string }) => {\n const { contract, className } = props\n const { isNative } = useNativeInfo()\n- const url = getShareUrl(contract)\n+ const user = useUser()\n+ const url = getShareUrl(contract, user?.username)\n \n const onClick = () => {\n if (!url) return\n copyToClipboard(url)\n"
|
|
1300
|
-
},
|
|
1301
|
-
{
|
|
1302
|
-
"path": "web/components/contract/header-actions.tsx",
|
|
1303
|
-
"status": "modified",
|
|
1304
|
-
"diff": "Index: web/components/contract/header-actions.tsx\n===================================================================\n--- web/components/contract/header-actions.tsx\t2c9045b (parent)\n+++ web/components/contract/header-actions.tsx\t05cd890 (commit)\n@@ -278,9 +278,9 @@\n />\n )}\n <FollowMarketIconButton contract={currentContract} user={user} />\n <CopyLinkOrShareButton\n- url={getShareUrl(currentContract)}\n+ url={getShareUrl(currentContract, user?.username)}\n tooltip=\"Copy question share link\"\n className=\"text-ink-500 hover:text-ink-600\"\n size=\"xs\"\n eventTrackingName=\"copy market link\"\n"
|
|
1305
|
-
},
|
|
1306
|
-
{
|
|
1307
|
-
"path": "web/components/dashboard/dashboard-page.tsx",
|
|
1308
|
-
"status": "modified",
|
|
1309
|
-
"diff": "Index: web/components/dashboard/dashboard-page.tsx\n===================================================================\n--- web/components/dashboard/dashboard-page.tsx\t2c9045b (parent)\n+++ web/components/dashboard/dashboard-page.tsx\t05cd890 (commit)\n@@ -30,8 +30,10 @@\n import { type Contract } from 'common/contract'\n import { UserHovercard } from '../user/user-hovercard'\n import clsx from 'clsx'\n import { useSaveCampaign } from 'web/hooks/use-save-campaign'\n+import { referralQuery } from 'common/util/share'\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n \n export type DashboardEndpoints = 'news' | 'politics' | 'ai'\n \n export function DashboardPage(props: {\n@@ -45,8 +47,9 @@\n endpoint: DashboardEndpoints\n className?: string\n }) {\n const user = useUser()\n+ useSaveReferral(user)\n useSaveCampaign()\n \n const router = useRouter()\n const pathName = usePathname() ?? ''\n@@ -137,9 +140,11 @@\n <Title className=\"!mb-0 \">{dashboard.title}</Title>\n \n <div className=\"flex items-center\">\n <CopyLinkOrShareButton\n- url={`https://${ENV_CONFIG.domain}/${endpoint}/${slug}`}\n+ url={`https://${ENV_CONFIG.domain}/${endpoint}/${slug}${\n+ user?.username ? referralQuery(user.username) : ''\n+ }`}\n eventTrackingName=\"copy dashboard link\"\n tooltip=\"Share\"\n />\n \n"
|
|
1310
|
-
},
|
|
1311
|
-
{
|
|
1312
|
-
"path": "web/components/dashboard/horizontal-dashboard.tsx",
|
|
1313
|
-
"status": "modified",
|
|
1314
|
-
"diff": "Index: web/components/dashboard/horizontal-dashboard.tsx\n===================================================================\n--- web/components/dashboard/horizontal-dashboard.tsx\t2c9045b (parent)\n+++ web/components/dashboard/horizontal-dashboard.tsx\t05cd890 (commit)\n@@ -17,16 +17,20 @@\n } from '../news/dashboard-news-item'\n import { Carousel } from '../widgets/carousel'\n import { DashboardText } from './dashboard-text-card'\n import { HorizontalDashboardCard } from './horizontal-dashboard-card'\n+import { useUser } from 'web/hooks/use-user'\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n \n export function HorizontalDashboard(props: {\n initialDashboard: Dashboard\n previews: LinkPreviews\n initialContracts: Contract[]\n slug: string\n }) {\n const { initialDashboard, slug, previews, initialContracts } = props\n+ const user = useUser()\n+ useSaveReferral(user)\n const fetchedDashboard = useDashboardFromSlug(slug)\n const [dashboard, setDashboard] = useState<Dashboard>(initialDashboard)\n \n // Update the dashboard state if a new fetchedDashboard becomes available\n"
|
|
1315
|
-
},
|
|
1316
|
-
{
|
|
1317
|
-
"path": "web/components/dashboard/politics-dashboard-page.tsx",
|
|
1318
|
-
"status": "modified",
|
|
1319
|
-
"diff": "Index: web/components/dashboard/politics-dashboard-page.tsx\n===================================================================\n--- web/components/dashboard/politics-dashboard-page.tsx\t2c9045b (parent)\n+++ web/components/dashboard/politics-dashboard-page.tsx\t05cd890 (commit)\n@@ -27,9 +27,9 @@\n import { Headline } from 'common/news'\n import { type Contract } from 'common/contract'\n import clsx from 'clsx'\n import { DashboardEndpoints } from 'web/components/dashboard/dashboard-page'\n-\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n export function PoliticsDashboardPage(props: {\n initialDashboard: Dashboard\n previews: LinkPreviews\n initialContracts: Contract[]\n@@ -40,8 +40,9 @@\n endpoint: DashboardEndpoints\n className?: string\n }) {\n const user = useUser()\n+ useSaveReferral(user)\n const router = useRouter()\n const pathName = usePathname() ?? ''\n \n const {\n"
|
|
1320
|
-
},
|
|
1321
|
-
{
|
|
1322
|
-
"path": "web/components/nav/sidebar.tsx",
|
|
1323
|
-
"status": "modified",
|
|
1324
|
-
"diff": "Index: web/components/nav/sidebar.tsx\n===================================================================\n--- web/components/nav/sidebar.tsx\t2c9045b (parent)\n+++ web/components/nav/sidebar.tsx\t05cd890 (commit)\n@@ -7,8 +7,9 @@\n LoginIcon,\n SearchIcon,\n GlobeAltIcon,\n ChatIcon,\n+ StarIcon,\n } from '@heroicons/react/outline'\n // import { PiTelevisionSimple } from 'react-icons/pi'\n import TrophyIcon from 'web/lib/icons/trophy-icon.svg'\n import clsx from 'clsx'\n@@ -144,8 +145,13 @@\n // href: '/tv',\n // icon: PiTelevisionSimple,\n // },\n {\n+ name: 'Share with friends',\n+ href: '/referrals',\n+ icon: StarIcon,\n+ },\n+ {\n name: 'Notifications',\n href: `/notifications`,\n icon: NotificationsIcon,\n },\n@@ -171,8 +177,10 @@\n const { isAdminOrMod } = options\n \n return buildArray<NavItem>(\n { name: 'Leagues', href: '/leagues', icon: TrophyIcon },\n+ { name: 'Share with friends', href: '/referrals', icon: StarIcon }, // remove this and I will beat you — SG\n+\n // { name: 'TV', href: '/tv', icon: PiTelevisionSimple },\n isAdminOrMod && {\n name: 'Reports',\n href: '/reports',\n"
|
|
1325
|
-
},
|
|
1326
|
-
{
|
|
1327
|
-
"path": "web/components/onboarding/welcome.tsx",
|
|
1328
|
-
"status": "modified",
|
|
1329
|
-
"diff": "Index: web/components/onboarding/welcome.tsx\n===================================================================\n--- web/components/onboarding/welcome.tsx\t2c9045b (parent)\n+++ web/components/onboarding/welcome.tsx\t05cd890 (commit)\n@@ -33,8 +33,9 @@\n import { getSavedContractVisitsLocally } from 'web/hooks/use-save-visits'\n import { capitalize } from 'lodash'\n import { TRADE_TERM } from 'common/envs/constants'\n import { convertGroup } from 'common/supabase/groups'\n+import { setCachedReferralInfoForUser } from 'web/lib/firebase/users'\n \n export const DEFAULT_FOR_YOU = false\n const SHOW_TOPICS = false\n \n@@ -61,11 +62,15 @@\n setPage(page)\n }\n \n useEffect(() => {\n+ if (!user) return\n if (shouldShowWelcomeModal) {\n track('welcome screen: landed')\n setOpen(true)\n+ } else {\n+ // Wait until after they've had the opportunity to change their name\n+ setCachedReferralInfoForUser(user)\n }\n }, [shouldShowWelcomeModal])\n \n const [userInterestedTopics, setUserInterestedTopics] = useState<Group[]>([])\n"
|
|
1330
|
-
},
|
|
1331
|
-
{
|
|
1332
|
-
"path": "web/components/topics/questions-topic-title.tsx",
|
|
1333
|
-
"status": "modified",
|
|
1334
|
-
"diff": "Index: web/components/topics/questions-topic-title.tsx\n===================================================================\n--- web/components/topics/questions-topic-title.tsx\t2c9045b (parent)\n+++ web/components/topics/questions-topic-title.tsx\t05cd890 (commit)\n@@ -14,9 +14,8 @@\n // import { TopicDropdown } from 'web/components/topics/topic-dropdown'\n import { useIsMobile } from 'web/hooks/use-is-mobile'\n import { TOPIC_IDS_YOU_CANT_FOLLOW } from 'common/supabase/groups'\n import { getTopicShareUrl } from 'common/util/share'\n-import { useRouter } from 'next/router'\n import { useUser } from 'web/hooks/use-user'\n \n export const QuestionsTopicTitle = forwardRef(\n (props: { topic: Group; addAbout: () => void }, ref: Ref<HTMLDivElement>) => {\n@@ -25,9 +24,8 @@\n const [showAddContract, setShowAddContract] = useState(false)\n const [loading, setLoading] = useState(false)\n const user = useUser()\n const isMobile = useIsMobile()\n- const router = useRouter()\n \n return (\n <Row\n className={\n@@ -39,9 +37,9 @@\n {topic.name}\n </h1>\n <Row>\n <CopyLinkOrShareButton\n- url={getTopicShareUrl(topic.slug)}\n+ url={getTopicShareUrl(topic.slug, user?.username)}\n className={'gap-1 whitespace-nowrap'}\n eventTrackingName={'copy questions page link'}\n size={isMobile ? 'sm' : 'md'}\n >\n"
|
|
1335
|
-
},
|
|
1336
|
-
{
|
|
1337
|
-
"path": "web/hooks/use-save-referral.ts",
|
|
1338
|
-
"status": "added",
|
|
1339
|
-
"diff": "Index: web/hooks/use-save-referral.ts\n===================================================================\n--- web/hooks/use-save-referral.ts\t2c9045b (parent)\n+++ web/hooks/use-save-referral.ts\t05cd890 (commit)\n@@ -0,0 +1,34 @@\n+import { cleanUsername } from 'common/util/clean-username'\n+import { useEffect } from 'react'\n+\n+import { User, writeReferralInfo } from 'web/lib/firebase/users'\n+import { useDefinedSearchParams } from 'web/hooks/use-defined-search-params'\n+\n+export const useSaveReferral = (\n+ user: User | null | undefined,\n+ options?: {\n+ defaultReferrerUsername?: string\n+ contractId?: string\n+ }\n+) => {\n+ const { searchParams } = useDefinedSearchParams()\n+\n+ useEffect(() => {\n+ const referrer = searchParams.get('r')\n+ ? decodeBase64(searchParams.get('r') as string)\n+ : (searchParams.get('referrer') as string)\n+\n+ const referrerOrDefault = referrer || options?.defaultReferrerUsername\n+\n+ if (user === null && referrerOrDefault) {\n+ writeReferralInfo(referrerOrDefault, {\n+ contractId: options?.contractId,\n+ explicitReferrer: referrer,\n+ })\n+ }\n+ }, [user, searchParams, JSON.stringify(options)])\n+}\n+\n+const decodeBase64 = (base64: string) => {\n+ return Buffer.from(cleanUsername(base64), 'base64').toString()\n+}\n"
|
|
1340
|
-
},
|
|
1341
|
-
{
|
|
1342
|
-
"path": "web/lib/firebase/users.ts",
|
|
1343
|
-
"status": "modified",
|
|
1344
|
-
"diff": "Index: web/lib/firebase/users.ts\n===================================================================\n--- web/lib/firebase/users.ts\t2c9045b (parent)\n+++ web/lib/firebase/users.ts\t05cd890 (commit)\n@@ -1,6 +1,11 @@\n import { Contract } from 'common/contract'\n-import { PrivateUser, User } from 'common/user'\n+import {\n+ humanish,\n+ MINUTES_ALLOWED_TO_REFER,\n+ PrivateUser,\n+ User,\n+} from 'common/user'\n import dayjs from 'dayjs'\n import utc from 'dayjs/plugin/utc'\n import {\n GoogleAuthProvider,\n@@ -11,15 +16,80 @@\n import { nativeSignOut } from 'web/lib/native/native-messages'\n import { postMessageToNative } from 'web/lib/native/post-message'\n import { getFirebaseAuth } from './auth'\n dayjs.extend(utc)\n+import { safeLocalStorage } from '../util/local'\n+import { api } from '../api/api'\n+import { removeUndefinedProps } from 'common/util/object'\n \n export type { User }\n \n export const auth = getFirebaseAuth()\n export const CACHED_REFERRAL_USERNAME_KEY = 'CACHED_REFERRAL_KEY'\n const CACHED_REFERRAL_CONTRACT_ID_KEY = 'CACHED_REFERRAL_CONTRACT_KEY'\n \n+// Scenarios:\n+// 1. User is referred by another user to homepage, group page, market page etc. explicitly via referrer= query param\n+// 2. User lands on a market or group without a referrer, we attribute the market/group creator\n+// Explicit referrers take priority over the implicit ones, (e.g. they're overwritten)\n+export function writeReferralInfo(\n+ defaultReferrerUsername: string,\n+ otherOptions?: {\n+ contractId?: string\n+ explicitReferrer?: string\n+ }\n+) {\n+ const local = safeLocalStorage\n+ const cachedReferralUser = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)\n+ const { contractId, explicitReferrer } = otherOptions || {}\n+\n+ // Write the first referral username we see.\n+ if (!cachedReferralUser) {\n+ local?.setItem(\n+ CACHED_REFERRAL_USERNAME_KEY,\n+ explicitReferrer || defaultReferrerUsername\n+ )\n+ if (contractId) local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)\n+ }\n+\n+ // Overwrite all referral info if we see an explicit referrer.\n+ if (explicitReferrer) {\n+ local?.setItem(CACHED_REFERRAL_USERNAME_KEY, explicitReferrer)\n+ if (!contractId) local?.removeItem(CACHED_REFERRAL_CONTRACT_ID_KEY)\n+ else local?.setItem(CACHED_REFERRAL_CONTRACT_ID_KEY, contractId)\n+ }\n+}\n+\n+export async function setCachedReferralInfoForUser(user: User) {\n+ if (!canSetReferrer(user)) return\n+\n+ const local = safeLocalStorage\n+ const cachedReferralUsername = local?.getItem(CACHED_REFERRAL_USERNAME_KEY)\n+ const cachedReferralContractId = local?.getItem(\n+ CACHED_REFERRAL_CONTRACT_ID_KEY\n+ )\n+ const referralComplete = local?.getItem('referral-complete') == 'true'\n+ if (!cachedReferralUsername || referralComplete) return\n+ console.log(\n+ `User created in last ${MINUTES_ALLOWED_TO_REFER} minutes, trying to set referral`\n+ )\n+ // get user via username\n+ api(\n+ 'refer-user',\n+ removeUndefinedProps({\n+ referredByUsername: cachedReferralUsername,\n+ contractId: cachedReferralContractId ?? undefined,\n+ })\n+ )\n+ .then((resp) => {\n+ console.log('referral resp', resp)\n+ local?.setItem('referral-complete', 'true')\n+ })\n+ .catch((err) => {\n+ console.log('error setting referral details', err)\n+ })\n+}\n+\n export async function firebaseLogin() {\n if (getIsNative()) {\n // Post the message back to expo\n postMessageToNative('loginClicked', {})\n@@ -70,4 +140,12 @@\n blockedByUserIds?.includes(contract.creatorId) ||\n blockedUserIds?.includes(contract.creatorId)\n )\n }\n+\n+export const canSetReferrer = (user: User) => {\n+ if (user.referredByUserId) return false\n+ if (!humanish(user)) return false\n+ const now = dayjs().utc()\n+ const userCreatedTime = dayjs(user.createdTime)\n+ return now.diff(userCreatedTime, 'minute') < MINUTES_ALLOWED_TO_REFER\n+}\n"
|
|
1345
|
-
},
|
|
1346
|
-
{
|
|
1347
|
-
"path": "web/pages/[username]/index.tsx",
|
|
1348
|
-
"status": "modified",
|
|
1349
|
-
"diff": "Index: web/pages/[username]/index.tsx\n===================================================================\n--- web/pages/[username]/index.tsx\t2c9045b (parent)\n+++ web/pages/[username]/index.tsx\t05cd890 (commit)\n@@ -54,8 +54,9 @@\n import { useHeaderIsStuck } from 'web/hooks/use-header-is-stuck'\n import { useIsMobile } from 'web/hooks/use-is-mobile'\n import { useLeagueInfo } from 'web/hooks/use-leagues'\n import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n import { usePrivateUser, useUser, useWebsocketUser } from 'web/hooks/use-user'\n import { User } from 'web/lib/firebase/users'\n import TrophyIcon from 'web/lib/icons/trophy-icon.svg'\n import { db } from 'web/lib/supabase/db'\n@@ -164,9 +165,11 @@\n const user = useWebsocketUser(props.user.id) ?? props.user\n const isMobile = useIsMobile()\n const router = useRouter()\n const currentUser = useUser()\n-\n+ useSaveReferral(currentUser, {\n+ defaultReferrerUsername: user.username,\n+ })\n const isCurrentUser = user.id === currentUser?.id\n const [expandProfileInfo, setExpandProfileInfo] = usePersistentLocalState(\n false,\n 'expand-profile-info'\n"
|
|
1350
|
-
},
|
|
1351
|
-
{
|
|
1352
|
-
"path": "web/pages/browse/index.tsx",
|
|
1353
|
-
"status": "modified",
|
|
1354
|
-
"diff": "Index: web/pages/browse/index.tsx\n===================================================================\n--- web/pages/browse/index.tsx\t2c9045b (parent)\n+++ web/pages/browse/index.tsx\t05cd890 (commit)\n@@ -14,11 +14,12 @@\n import { useIsMobile } from 'web/hooks/use-is-mobile'\n import { usePrivateUser, useUser } from 'web/hooks/use-user'\n import { ManifoldLogo } from 'web/components/nav/manifold-logo'\n import { DEFAULT_FOR_YOU, Welcome } from 'web/components/onboarding/welcome'\n-\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n export default function BrowsePage() {\n const user = useUser()\n+ useSaveReferral(user)\n \n return (\n <Page trackPageView={'questions page'} className=\"lg:px-4\">\n {/* only show logo on mobile, since there's no sidebar */}\n"
|
|
1355
|
-
},
|
|
1356
|
-
{
|
|
1357
|
-
"path": "web/pages/embed/[username]/[contractSlug].tsx",
|
|
1358
|
-
"status": "modified",
|
|
1359
|
-
"diff": "Index: web/pages/embed/[username]/[contractSlug].tsx\n===================================================================\n--- web/pages/embed/[username]/[contractSlug].tsx\t2c9045b (parent)\n+++ web/pages/embed/[username]/[contractSlug].tsx\t05cd890 (commit)\n@@ -53,8 +53,9 @@\n import { MultiNumericContractChart } from 'web/components/charts/contract/multi-numeric'\n import { MultiDateContractChart } from 'web/components/charts/contract/multi-date'\n import { pointsToBase64 } from 'common/util/og'\n import { mapValues } from 'lodash'\n+import { useUser } from 'web/hooks/use-user'\n type Points = HistoryPoint<any>[]\n \n async function getHistoryData(contract: Contract) {\n switch (contract.outcomeType) {\n@@ -286,11 +287,11 @@\n const isMultiNumeric = outcomeType === 'MULTI_NUMERIC'\n const isDate = outcomeType === 'DATE'\n \n const href = `https://${DOMAIN}${twombaContractPath(contract)}`\n+ const user = useUser()\n+ const shareUrl = getShareUrl(contract, user?.username)\n \n- const shareUrl = getShareUrl(contract)\n-\n const showMultiChart = isMulti && !!props.multiPoints\n \n return (\n <Col className=\"bg-canvas-0 h-[100vh] w-full gap-1 px-6 py-4\">\n"
|
|
1360
|
-
},
|
|
1361
|
-
{
|
|
1362
|
-
"path": "web/pages/explore/index.tsx",
|
|
1363
|
-
"status": "modified",
|
|
1364
|
-
"diff": "Index: web/pages/explore/index.tsx\n===================================================================\n--- web/pages/explore/index.tsx\t2c9045b (parent)\n+++ web/pages/explore/index.tsx\t05cd890 (commit)\n@@ -36,8 +36,9 @@\n import DropdownMenu from 'web/components/widgets/dropdown-menu'\n import { ACTIVITY_TYPES } from '../activity'\n import { DropdownPill } from 'web/components/search/filter-pills'\n import { TopicPillSelector } from 'web/components/topics/topic-selector'\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n const isProd = ENV === 'PROD'\n const NFL_ID = 'TNQwmbE5p6dnKx2e6Qlp'\n const NBA_ID = 'i0v3cXwuxmO9fpcInVYb'\n const EPL_ID = '5gsW3dPR3ySBRZCodrgm'\n@@ -48,8 +49,10 @@\n const ALL_IDS = [NFL_ID, SPORTS_ID, EPL_ID, NBA_ID, MLB_ID].join(',')\n export function ExploreContent(props: { render: boolean }) {\n const { render } = props\n const user = useUser()\n+ useSaveReferral(user)\n+\n const [isSportsInterested, setIsSportsInterested] = usePersistentLocalState<\n boolean | undefined\n >(undefined, 'is-sports-interested')\n const getIsSportsInterested = async () => {\n"
|
|
1365
|
-
},
|
|
1366
|
-
{
|
|
1367
|
-
"path": "web/pages/home/index.tsx",
|
|
1368
|
-
"status": "modified",
|
|
1369
|
-
"diff": "Index: web/pages/home/index.tsx\n===================================================================\n--- web/pages/home/index.tsx\t2c9045b (parent)\n+++ web/pages/home/index.tsx\t05cd890 (commit)\n@@ -16,11 +16,12 @@\n import { Welcome } from 'web/components/onboarding/welcome'\n import { LiveGeneratedFeed } from 'web/components/feed/live-generated-feed'\n import { ExploreContent } from '../explore'\n import { useBanner } from 'web/components/nav/banner'\n-\n+import { useSaveReferral } from 'web/hooks/use-save-referral'\n export default function Home() {\n const user = useUser()\n+ useSaveReferral(user)\n useRedirectIfSignedOut()\n const [showManifestBanner, hideManifestBanner] = useBanner('manifest-2025')\n \n return (\n"
|
|
1370
|
-
},
|
|
1371
|
-
{
|
|
1372
|
-
"path": "web/pages/referrals.tsx",
|
|
1373
|
-
"status": "modified",
|
|
1374
|
-
"diff": "Index: web/pages/referrals.tsx\n===================================================================\n--- web/pages/referrals.tsx\t2c9045b (parent)\n+++ web/pages/referrals.tsx\t05cd890 (commit)\n@@ -1,42 +1,69 @@\n import { Col } from 'web/components/layout/col'\n import { SEO } from 'web/components/SEO'\n+import { Title } from 'web/components/widgets/title'\n import { useUser } from 'web/hooks/use-user'\n import { Page } from 'web/components/layout/page'\n import { redirectIfLoggedOut } from 'web/lib/firebase/server-auth'\n-\n-import { getReferralCodeFromUser } from 'common/util/share'\n-\n+import { CopyLinkRow } from 'web/components/buttons/copy-link-button'\n+import { ENV_CONFIG } from 'common/envs/constants'\n+import { InfoBox } from 'web/components/widgets/info-box'\n+import { QRCode } from 'web/components/widgets/qr-code'\n+import { REFERRAL_AMOUNT } from 'common/economy'\n+import { formatMoney } from 'common/util/format'\n+import clsx from 'clsx'\n+import { TokenNumber } from 'web/components/widgets/token-number'\n+import { referralQuery } from 'common/util/share'\n export const getServerSideProps = redirectIfLoggedOut('/')\n \n export default function ReferralsPage() {\n const user = useUser()\n- const isSweepstakesVerified = user?.sweepstakesVerified\n \n- const code = getReferralCodeFromUser(user?.id)\n+ const url = `https://${ENV_CONFIG.domain}${referralQuery(\n+ user?.username ?? ''\n+ )}`\n+\n return (\n <Page trackPageView={'referrals'}>\n <SEO\n title=\"Refer a friend\"\n- description={`Invite new users to Manifold!`}\n+ description={`Invite new users to Manifold and get ${formatMoney(\n+ REFERRAL_AMOUNT\n+ )} if they sign up and place a trade!`}\n url=\"/referrals\"\n />\n \n- <Col className=\"mx-auto max-w-2xl items-center\">\n- <Col className=\"bg-canvas-0 rounded-lg p-8 shadow-lg\">\n- <h1 className=\"mb-6 text-center text-3xl font-bold\">\n- Refer a Friend\n- </h1>\n-\n+ <Col className=\"items-center\">\n+ <Col className=\"bg-canvas-0 h-full rounded p-4 py-8 sm:p-8 sm:shadow-md\">\n+ <Title>Refer a friend</Title>\n <img\n- className=\"mx-auto mb-8 block\"\n+ className=\"mb-6 block -scale-x-100 self-center\"\n src=\"/logo-flapping-with-money.gif\"\n width={200}\n height={200}\n- alt=\"Animated logo\"\n+ alt=\"\"\n />\n \n- <div className=\"text-center text-xl\">Coming soon...</div>\n+ <div className={'mb-4'}>\n+ Invite new users to Manifold and get{' '}\n+ <TokenNumber\n+ coinType={'MANA'}\n+ amount={REFERRAL_AMOUNT}\n+ className={clsx('font-bold')}\n+ isInline\n+ />{' '}\n+ if they sign up and place a trade!\n+ </div>\n+\n+ <CopyLinkRow url={url} eventTrackingName=\"copy referral link\" />\n+\n+ <QRCode url={url} className=\"mt-4 self-center\" />\n+\n+ <InfoBox\n+ title=\"FYI\"\n+ className=\"mt-4\"\n+ text=\"You can also earn the referral bonus using the share link to any question or group!\"\n+ />\n </Col>\n </Col>\n </Page>\n )\n"
|
|
1375
|
-
}
|
|
1376
|
-
]
|
|
1377
|
-
},
|
|
1378
|
-
{
|
|
1379
|
-
"id": "remove-play-query",
|
|
1380
|
-
"sha": "a3aeeb19676f966215b5eafb24ef52ac9db2fad1",
|
|
1381
|
-
"parentSha": "5d7a0b8db6835cae42d0a07de4f38c853583ae12",
|
|
1382
|
-
"spec": "Implement the removal of sweepstakes/play mode and the play URL query from contract-related pages and routing. Make the following changes:\n\n1) URL/path generation\n- Remove the twombaContractPath function and its usage. Replace all references to twombaContractPath with contractPath.\n- After creating a contract, navigate to contractPath(newContract) instead of any play-based path.\n- Update embed URLs: src should be https://{DOMAIN}/embed + contractPath(contract).\n- Update SEO canonical/og URL generation for contract pages to use contractPath(contract).\n\n2) Contract page behavior (single contract, no play toggle)\n- Eliminate play/sweeps toggle state and behavior on the main contract page:\n - Remove useSweepstakes(), query.play parsing, router.replace() calls updating play, and isPlay state.\n - Do not compute or fetch a sibling cash contract. Remove the cash: CashType prop from ContractParams and all dependent code paths (bet data, totals, points, metrics switching, etc.). Use only the live state of the contract provided in props.\n - For data hooks (bets, points, totals), initialize them from the liveContract (useLiveContract(props.contract)) only.\n - For top traders/metrics, use the values from props.topContractMetrics as provided for the single active contract.\n - For header and title components, pass the single live contract (no play/current split props).\n - Description rendering: If the contract.token is 'CASH', do not render the ContractDescription editor/content. Instead, show a short notice that links to the parent (mana) contract using the slug with '--cash' removed. For non-CASH contracts, render ContractDescription as usual.\n - Always pass props.totalPositions as-is (do not switch based on play/cash).\n\n3) Contract info dialog and header actions\n- Update ContractInfoDialog to accept a single contract prop. Remove playContract/statsContract and setIsPlay props. Use the single contract for history, share, and embed buttons.\n- In the Stats section, replace the SweepsToggle component with a simple link. If contract.token === 'CASH', link to /{creatorUsername}/{slug-without--cash}; otherwise, link to /{creatorUsername}/{slug}--cash. Display the link text reflecting whether the current contract is the cash variant.\n- Update HeaderActions to accept a single contract prop. Remove setIsPlay, playContract/currentContract parameters, and all code branching on them. Use contract for follow/watch, duplicate, block/unblock, mark uninteresting, add liquidity, dropdown actions, and reporting.\n\n4) Contract page routing (SSR/SSG) and embed page\n- In pages/[username]/[contractSlug].tsx:\n - Do not redirect CASH contracts to a play=false URL. Remove any CASH_SUFFIX logic and removal of username from getStaticProps if not needed.\n - Do not fetch sibling or cash contracts. Load only the main contract by slug, then populate and return its ContractParams.\n - In NonPrivateContractPage, remove Sweepstakes logic and any cashPoints or cashContract props. If rendering in an iframe, render the embed page with the single contract and its points.\n - Use ContractSEO with the single contract.\n- In pages/embed/[username]/[contractSlug].tsx:\n - Remove cash/sibling contract fetching and all play query logic. Do not branch on isCash.\n - Load only the contract, its points, and optional multiPoints; pass them to the embed view.\n - Set ContractSEO to the single contract.\n - In ContractSmolView, set the href to https://{DOMAIN}{contractPath(contract)}.\n\n5) Middleware: remove play query and constrain API proxying\n- Update web/middleware.ts so that:\n - On any matched request, if the URL has a play query parameter, remove it and redirect (308) to the same URL without play.\n - Only run API proxy logic for paths under /api/. For non-API requests, continue the request without proxying.\n - Update config.matcher to include:\n - '/api/v0/:path*' for the API proxy\n - '/([^/]+)/([^/]+)' to catch contract pages (username/slug)\n - '/embed/([^/]+)/([^/]+)' to catch embed pages\n\n6) Cleanup and types\n- Remove CashType and the cash field from ContractParams and from any exports. Remove imports and references to CASH_SUFFIX, useSweepstakes, SweepsToggle, and useRouter in the affected components.\n- Ensure all updated components compile with the simplified props and imported symbols.\n\n7) Validation and references\n- Verify no remaining references to twombaContractPath exist in the codebase.\n- Verify no components on these pages rely on router.query.play; all navigation to cash vs mana contracts must be done by slug-based routes only.\n- Confirm analytics/tracking calls still use the correct contract.id and slug.",
|
|
1383
|
-
"prompt": "Refactor the contract and embed experience to remove the old play/sweepstakes toggle and stop using a play URL query. Consolidate the pages to show a single live contract instance, eliminate sibling/cash contract toggling logic, and shift any navigation between cash and mana variants to simple slug-based links. Update all share, embed, and SEO URLs to use a single contract path builder. Adjust middleware so that any incoming play query is stripped by redirect and the API proxy only runs for API routes. Remove now-unused types and imports related to play/sweeps in these pages. Ensure contract pages, embed pages, and share/embed controls work correctly without the play query or sweepstakes toggle.",
|
|
1384
|
-
"supplementalFiles": [
|
|
1385
|
-
"web/components/sweepstakes-provider.tsx",
|
|
1386
|
-
"web/components/sweeps/sweeps-toggle.tsx",
|
|
1387
|
-
"web/hooks/use-saved-contract-metrics.ts",
|
|
1388
|
-
"web/components/contract/contract-description.tsx",
|
|
1389
|
-
"common/src/contract-seo.ts",
|
|
1390
|
-
"web/lib/supabase/contracts.ts"
|
|
1391
|
-
],
|
|
1392
|
-
"fileDiffs": [
|
|
1393
|
-
{
|
|
1394
|
-
"path": "common/src/contract.ts",
|
|
1395
|
-
"status": "modified",
|
|
1396
|
-
"diff": "Index: common/src/contract.ts\n===================================================================\n--- common/src/contract.ts\t5d7a0b8 (parent)\n+++ common/src/contract.ts\ta3aeeb1 (commit)\n@@ -6,9 +6,9 @@\n import { Answer } from './answer'\n import { getLiquidity } from './calculate-cpmm'\n import { ContractComment } from './comment'\n import { ContractMetric } from './contract-metric'\n-import { CASH_SUFFIX, ENV_CONFIG } from './envs/constants'\n+import { ENV_CONFIG } from './envs/constants'\n import { Fees } from './fees'\n import { PollOption } from './poll-option'\n import { formatMoney, formatPercent } from './util/format'\n import { MultiBase64Points } from './chart'\n@@ -453,20 +453,8 @@\n }) {\n return `/${contract.creatorUsername}/${contract.slug}`\n }\n \n-export function twombaContractPath(contract: {\n- creatorUsername: string\n- slug: string\n- token?: ContractToken\n-}) {\n- const isCashContract = contract.token == 'CASH'\n- const cleanedSlug = contract.slug.replace(new RegExp(`${CASH_SUFFIX}$`), '')\n- return `/${contract.creatorUsername}/${cleanedSlug}${\n- isCashContract ? '?play=false' : '?play=true'\n- }`\n-}\n-\n export type CashType = {\n contract: Contract\n lastBetTime?: number\n pointsString: string\n@@ -488,9 +476,8 @@\n chartAnnotations: ChartAnnotation[]\n topics: Topic[]\n dashboards: { slug: string; title: string }[]\n pinnedComments: ContractComment[]\n- cash?: CashType\n }\n \n export type MaybeAuthedContractParams =\n | {\n"
|
|
1397
|
-
},
|
|
1398
|
-
{
|
|
1399
|
-
"path": "web/components/buttons/share-embed-button.tsx",
|
|
1400
|
-
"status": "modified",
|
|
1401
|
-
"diff": "Index: web/components/buttons/share-embed-button.tsx\n===================================================================\n--- web/components/buttons/share-embed-button.tsx\t5d7a0b8 (parent)\n+++ web/components/buttons/share-embed-button.tsx\ta3aeeb1 (commit)\n@@ -1,17 +1,17 @@\n import { CodeIcon } from '@heroicons/react/outline'\n import toast from 'react-hot-toast'\n \n-import { Contract, contractPath, twombaContractPath } from 'common/contract'\n+import { Contract, contractPath } from 'common/contract'\n import { DOMAIN } from 'common/envs/constants'\n import { copyToClipboard } from 'web/lib/util/copy'\n import { track } from 'web/lib/service/analytics'\n import { Button } from './button'\n import clsx from 'clsx'\n \n export function embedContractCode(contract: Contract) {\n const title = contract.question\n- const src = `https://${DOMAIN}/embed${twombaContractPath(contract)}`\n+ const src = `https://${DOMAIN}/embed${contractPath(contract)}`\n return `<iframe src=\"${src}\" title=\"${title}\" frameborder=\"0\" style=\"position: relative; left:50%; transform: translateX(-50%); width:90%; height:18rem; max-width: 35rem;\"></iframe>`\n }\n \n export function ShareEmbedButton(props: {\n"
|
|
1402
|
-
},
|
|
1403
|
-
{
|
|
1404
|
-
"path": "web/components/contract/contract-info-dialog.tsx",
|
|
1405
|
-
"status": "modified",
|
|
1406
|
-
"diff": "Index: web/components/contract/contract-info-dialog.tsx\n===================================================================\n--- web/components/contract/contract-info-dialog.tsx\t5d7a0b8 (parent)\n+++ web/components/contract/contract-info-dialog.tsx\ta3aeeb1 (commit)\n@@ -27,16 +27,16 @@\n import { InfoTooltip } from '../widgets/info-tooltip'\n import ShortToggle from '../widgets/short-toggle'\n import { Table } from '../widgets/table'\n import { ContractHistoryButton } from './contract-edit-history-button'\n-import { SweepsToggle } from '../sweeps/sweeps-toggle'\n import { useSweepstakes } from '../sweepstakes-provider'\n+import Link from 'next/link'\n+import { linkClass } from '../widgets/site-link'\n export const Stats = (props: {\n contract: Contract\n- setIsPlay: (isPlay: boolean) => void\n user?: User | null | undefined\n }) => {\n- const { contract, user, setIsPlay } = props\n+ const { contract, user } = props\n const { creatorId } = contract\n const shouldAnswersSumToOne =\n contract.mechanism === 'cpmm-multi-1'\n ? contract.shouldAnswersSumToOne\n@@ -337,26 +337,21 @@\n )}\n {sweepsEnabled && !isNonBetPollOrBountiedQuestion && (\n <tr>\n <td>Sweepstakes</td>\n- <td>\n- <SweepsToggle\n- sweepsEnabled={sweepsEnabled}\n- isPlay={isPlay}\n- onClick={() => {\n- if (prefersPlay && isPlay) {\n- setPrefersPlay(false)\n- setIsPlay(false)\n- } else if (!prefersPlay && !isPlay) {\n- setPrefersPlay(true)\n- setIsPlay(true)\n- } else if (prefersPlay && !isPlay) {\n- setIsPlay(true)\n- } else if (!prefersPlay && isPlay) {\n- setIsPlay(false)\n- }\n- }}\n- />\n+ <td className={linkClass}>\n+ <Link\n+ href={\n+ contract.token === 'CASH'\n+ ? `/${contract.creatorUsername}/${contract.slug.replace(\n+ '--cash',\n+ ''\n+ )}`\n+ : `/${contract.creatorUsername}/${contract.slug}--cash`\n+ }\n+ >\n+ {contract.token === 'CASH' ? 'True' : 'False'}\n+ </Link>\n </td>\n </tr>\n )}\n \n@@ -534,16 +529,14 @@\n )\n }\n \n export function ContractInfoDialog(props: {\n- playContract: Contract\n- statsContract: Contract\n+ contract: Contract\n user: User | null | undefined\n- setIsPlay: (isPlay: boolean) => void\n open: boolean\n setOpen: (open: boolean) => void\n }) {\n- const { playContract, statsContract, user, open, setOpen, setIsPlay } = props\n+ const { contract, user, open, setOpen } = props\n const isAdmin = useAdmin()\n const isTrusted = useTrusted()\n \n return (\n@@ -551,21 +544,21 @@\n open={open}\n setOpen={setOpen}\n className=\"bg-canvas-0 flex flex-col gap-4 rounded p-6\"\n >\n- <Stats contract={statsContract} user={user} setIsPlay={setIsPlay} />\n+ <Stats contract={contract} user={user} />\n \n {!!user && (\n <>\n <Row className=\"my-2 flex-wrap gap-2\">\n- <ContractHistoryButton contract={playContract} />\n- <ShareQRButton contract={playContract} />\n- <ShareIRLButton contract={playContract} />\n- <ShareEmbedButton contract={statsContract} />\n+ <ContractHistoryButton contract={contract} />\n+ <ShareQRButton contract={contract} />\n+ <ShareIRLButton contract={contract} />\n+ <ShareEmbedButton contract={contract} />\n </Row>\n <Row className=\"flex-wrap gap-2\">\n {isAdmin || isTrusted ? (\n- <SuperBanControl userId={playContract.creatorId} />\n+ <SuperBanControl userId={contract.creatorId} />\n ) : null}\n </Row>\n </>\n )}\n"
|
|
1407
|
-
},
|
|
1408
|
-
{
|
|
1409
|
-
"path": "web/components/contract/contract-page.tsx",
|
|
1410
|
-
"status": "modified",
|
|
1411
|
-
"diff": "Index: web/components/contract/contract-page.tsx\n===================================================================\n--- web/components/contract/contract-page.tsx\t5d7a0b8 (parent)\n+++ web/components/contract/contract-page.tsx\ta3aeeb1 (commit)\n@@ -56,20 +56,15 @@\n import { useRelatedMarkets } from 'web/hooks/use-related-contracts'\n import { useReview } from 'web/hooks/use-review'\n import { useSaveCampaign } from 'web/hooks/use-save-campaign'\n import { useSaveContractVisitsLocally } from 'web/hooks/use-save-visits'\n-import {\n- useSavedContractMetrics,\n- useTopContractMetrics,\n-} from 'web/hooks/use-saved-contract-metrics'\n+import { useSavedContractMetrics } from 'web/hooks/use-saved-contract-metrics'\n import { useTracking } from 'web/hooks/use-tracking'\n import { usePrivateUser, useUser } from 'web/hooks/use-user'\n import { track } from 'web/lib/service/analytics'\n import { scrollIntoViewCentered } from 'web/lib/util/scroll'\n import { SpiceCoin } from 'web/public/custom-components/spiceCoin'\n import { YourTrades } from 'web/pages/[username]/[contractSlug]'\n-import { useSweepstakes } from '../sweepstakes-provider'\n-import { useRouter } from 'next/router'\n import { precacheAnswers } from 'web/hooks/use-answers'\n import { useIsPageVisible } from 'web/hooks/use-page-visible'\n import { api } from 'web/lib/api/api'\n import { shouldHideGraph } from 'common/contract-params'\n@@ -77,8 +72,9 @@\n import { FollowMarketButton } from '../buttons/follow-market-button'\n import { useSaveReferral } from 'web/hooks/use-save-referral'\n import { base64toPoints } from 'common/edge/og'\n import { useDisplayUserById } from 'web/hooks/use-user-supabase'\n+import Link from 'next/link'\n \n export function ContractPageContent(props: ContractParams) {\n const {\n comments,\n@@ -88,79 +84,20 @@\n chartAnnotations,\n topics,\n dashboards,\n pinnedComments,\n- cash,\n } = props\n \n- // sync query state with context\n- const { prefersPlay } = useSweepstakes()\n- const router = useRouter()\n- const livePlayContract = useLiveContract(props.contract)\n- const sweepsIsPossible = !!livePlayContract.siblingContractId\n- const [isPlay, setIsPlay] = useState<boolean | undefined>(prefersPlay)\n- const liveCashContract = props.cash\n- ? // eslint-disable-next-line react-hooks/rules-of-hooks\n- useLiveContract(props.cash.contract)\n- : null\n-\n- const liveContract =\n- !isPlay && liveCashContract ? liveCashContract : livePlayContract\n+ // Just use the contract that was navigated to directly\n+ const liveContract = useLiveContract(props.contract)\n const user = useUser()\n useSaveReferral(user, {\n defaultReferrerUsername: props.contract.creatorUsername,\n contractId: props.contract.id,\n })\n- // Read and set play state from the query\n- useEffect(() => {\n- if (!router.isReady) return\n- const playQuery = router.query.play\n- const queryIndicatesSweeps = playQuery === 'false'\n- const queryIndicatesPlay =\n- playQuery === 'true' ||\n- (playQuery === undefined &&\n- !sweepsIsPossible &&\n- prefersPlay === undefined)\n- if (queryIndicatesSweeps) {\n- if (sweepsIsPossible && isPlay) {\n- setIsPlay(false)\n- } else if (!sweepsIsPossible && !isPlay) {\n- setIsPlay(true)\n- }\n- } else if (queryIndicatesPlay && !isPlay) {\n- setIsPlay(true)\n- }\n- }, [isPlay, router.query, prefersPlay])\n \n- const setIsPlayAndQuery = (isPlay: boolean) => {\n- setIsPlay(isPlay)\n- setPlayStateInQuery(isPlay)\n- }\n-\n- const setPlayStateInQuery = (play: boolean) => {\n- const newQuery = { ...router.query, play: play.toString() }\n-\n- if (JSON.stringify(newQuery) !== JSON.stringify(router.query)) {\n- router.replace(\n- {\n- query: newQuery,\n- hash: router.asPath.split('#')[1],\n- },\n- undefined,\n- { shallow: true }\n- )\n- }\n- }\n-\n const myContractMetrics = useSavedContractMetrics(liveContract)\n- const topContractMetrics = useTopContractMetrics({\n- playContract: livePlayContract,\n- cashContract: liveCashContract,\n- prefersPlay: isPlay ?? false,\n- // TODO: do we really need this? leaderboards are below the fold. If we do, should add for cash as well\n- defaultTopManaTraders: props.topContractMetrics,\n- defaultTopCashTraders: [],\n- })\n+ const topContractMetrics = props.topContractMetrics\n \n const privateUser = usePrivateUser()\n const blockedUserIds = privateUser?.blockedUserIds ?? []\n \n@@ -181,41 +118,25 @@\n useEffect(() => {\n if ('answers' in props.contract) {\n precacheAnswers(props.contract.answers)\n }\n- if (props.cash?.contract && 'answers' in props.cash.contract) {\n- precacheAnswers(props.cash.contract.answers)\n- }\n }, [])\n \n- const playBetData = useBetData({\n- contractId: props.contract.id,\n- outcomeType: props.contract.outcomeType,\n+ const { bets, totalBets, yourNewBets, betPoints } = useBetData({\n+ contractId: liveContract.id,\n+ outcomeType: liveContract.outcomeType,\n userId: user?.id,\n lastBetTime: props.lastBetTime,\n totalBets: props.totalBets,\n- pointsString,\n- multiPointsString,\n+ pointsString: pointsString,\n+ multiPointsString: multiPointsString,\n })\n \n- const cashBetData = useBetData({\n- contractId: cash?.contract.id ?? '_',\n- outcomeType: cash?.contract.outcomeType,\n- userId: user?.id,\n- lastBetTime: cash?.lastBetTime,\n- totalBets: cash?.totalBets ?? 0,\n- pointsString: cash?.pointsString,\n- multiPointsString: cash?.multiPointsString,\n- })\n-\n- const { bets, totalBets, yourNewBets, betPoints } =\n- cash && !isPlay ? cashBetData : playBetData\n-\n const { isResolved, outcomeType, resolution, closeTime, creatorId } =\n liveContract\n- const { coverImageUrl } = livePlayContract\n+ const { coverImageUrl } = liveContract\n \n- const description = livePlayContract.description\n+ const description = liveContract.description\n \n const isAdmin = useAdmin()\n const isMod = useTrusted()\n const isCreator = creatorId === user?.id\n@@ -299,9 +220,9 @@\n }}\n priority\n />\n <ChangeBannerButton\n- contract={livePlayContract}\n+ contract={liveContract}\n className=\"absolute right-4 top-12\"\n />\n </div>\n )}\n@@ -336,11 +257,9 @@\n )}\n </Row>\n {(headerStuck || !coverImageUrl) && (\n <HeaderActions\n- setIsPlay={setIsPlayAndQuery}\n- playContract={livePlayContract}\n- currentContract={liveContract}\n+ contract={liveContract}\n initialHideGraph={initialHideGraph}\n hideGraph={hideGraph}\n setHideGraph={setHideGraph}\n />\n@@ -353,11 +272,9 @@\n <div>\n <BackButton className=\"pr-8\" />\n </div>\n <HeaderActions\n- setIsPlay={setIsPlayAndQuery}\n- playContract={livePlayContract}\n- currentContract={liveContract}\n+ contract={liveContract}\n initialHideGraph={initialHideGraph}\n hideGraph={hideGraph}\n setHideGraph={setHideGraph}\n />\n@@ -373,9 +290,9 @@\n isLarge\n className=\"mr-1\"\n />\n <EditableQuestionTitle\n- contract={livePlayContract}\n+ contract={liveContract}\n canEdit={isAdmin || isCreator || isMod}\n />\n </div>\n </Col>\n@@ -495,14 +412,27 @@\n {showResolver && <Spacer h={4} />}\n <CreatorSharePanel contract={liveContract} />\n </>\n )}\n- <ContractDescription\n- contractId={props.contract.id}\n- creatorId={props.contract.creatorId}\n- isSweeps={isCashContract}\n- description={description}\n- />\n+ {liveContract.token === 'CASH' ? (\n+ <span className=\"bg-canvas-50 rounded-md p-4\">\n+ See parent question for description and comments:{' '}\n+ <Link\n+ href={`/${\n+ liveContract.creatorUsername\n+ }/${liveContract.slug.replace('--cash', '')}`}\n+ >\n+ {liveContract.question}\n+ </Link>\n+ </span>\n+ ) : (\n+ <ContractDescription\n+ contractId={props.contract.id}\n+ creatorId={props.contract.creatorId}\n+ isSweeps={isCashContract}\n+ description={description}\n+ />\n+ )}\n <Row className=\"mb-4 items-center gap-2\">\n <MarketTopics\n contract={props.contract}\n dashboards={dashboards}\n@@ -552,11 +482,9 @@\n liveContract={liveContract}\n bets={bets}\n totalBets={totalBets}\n comments={comments}\n- totalPositions={\n- !isPlay && cash ? cash.totalPositions : props.totalPositions\n- }\n+ totalPositions={props.totalPositions}\n replyTo={replyTo}\n setReplyTo={setReplyTo}\n blockedUserIds={blockedUserIds}\n activeIndex={activeTabIndex}\n"
|
|
1412
|
-
},
|
|
1413
|
-
{
|
|
1414
|
-
"path": "web/components/contract/contract-seo.tsx",
|
|
1415
|
-
"status": "modified",
|
|
1416
|
-
"diff": "Index: web/components/contract/contract-seo.tsx\n===================================================================\n--- web/components/contract/contract-seo.tsx\t5d7a0b8 (parent)\n+++ web/components/contract/contract-seo.tsx\ta3aeeb1 (commit)\n@@ -1,5 +1,5 @@\n-import { Contract, twombaContractPath } from 'common/contract'\n+import { Contract, contractPath } from 'common/contract'\n import { getSeoDescription, getContractOGProps } from 'common/contract-seo'\n import { removeUndefinedProps } from 'common/util/object'\n import { SEO } from '../SEO'\n \n@@ -20,9 +20,9 @@\n return (\n <SEO\n title={question}\n description={seoDesc}\n- url={twombaContractPath(contract)}\n+ url={contractPath(contract)}\n ogProps={{ props: ogCardProps, endpoint: 'market' }}\n shouldIgnore={contract.visibility !== 'public'}\n />\n )\n"
|
|
1417
|
-
},
|
|
1418
|
-
{
|
|
1419
|
-
"path": "web/components/contract/header-actions.tsx",
|
|
1420
|
-
"status": "modified",
|
|
1421
|
-
"diff": "Index: web/components/contract/header-actions.tsx\n===================================================================\n--- web/components/contract/header-actions.tsx\t5d7a0b8 (parent)\n+++ web/components/contract/header-actions.tsx\ta3aeeb1 (commit)\n@@ -36,70 +36,60 @@\n import { ChangeBannerButton } from './change-banner-button'\n import { GoGraph } from 'react-icons/go'\n \n export function HeaderActions(props: {\n- playContract: Contract\n- setIsPlay: (isPlay: boolean) => void\n- currentContract: Contract\n+ contract: Contract\n initialHideGraph: boolean\n hideGraph: boolean\n setHideGraph: (hideGraph: boolean) => void\n }) {\n- const {\n- playContract,\n- currentContract,\n- initialHideGraph,\n- hideGraph,\n- setHideGraph,\n- setIsPlay,\n- } = props\n+ const { contract, initialHideGraph, hideGraph, setHideGraph } = props\n const user = useUser()\n const privateUser = usePrivateUser()\n- const isCreator = user?.id === playContract.creatorId\n+ const isCreator = user?.id === contract.creatorId\n \n const [detailsOpen, setDetailsOpen] = useState(false)\n const [repostOpen, setRepostOpen] = useState(false)\n const [reportOpen, setReportOpen] = useState(false)\n const [liquidityOpen, setLiquidityOpen] = useState(false)\n \n- const duplicateHref = duplicateContractHref(playContract)\n+ const duplicateHref = duplicateContractHref(contract)\n \n const isBlocked =\n- privateUser && privateUser.blockedContractIds?.includes(playContract.id)\n+ privateUser && privateUser.blockedContractIds?.includes(contract.id)\n \n const onBlock = async () => {\n await toast.promise(\n- api('market/:contractId/block', { contractId: playContract.id }),\n+ api('market/:contractId/block', { contractId: contract.id }),\n {\n loading: 'Blocking...',\n success: `You'll no longer see this question in your feed nor search.`,\n error: 'Error blocking user',\n }\n )\n }\n const onUnblock = async () => {\n- await api('market/:contractId/unblock', { contractId: playContract.id })\n+ await api('market/:contractId/unblock', { contractId: contract.id })\n }\n \n const markUninteresting = async () => {\n await updateUserDisinterestEmbedding({\n- contractId: playContract.id,\n- creatorId: playContract.creatorId,\n+ contractId: contract.id,\n+ creatorId: contract.creatorId,\n })\n toast(`We won't show you content like that again`, {\n icon: <TiVolumeMute className={'h-5 w-5 text-teal-500'} />,\n })\n }\n \n const contractOpenAndPublic =\n- !currentContract.isResolved &&\n- (currentContract.closeTime ?? Infinity) > Date.now() &&\n- currentContract.visibility == 'public'\n+ !contract.isResolved &&\n+ (contract.closeTime ?? Infinity) > Date.now() &&\n+ contract.visibility == 'public'\n \n const addLiquidityEnabled =\n user &&\n- (currentContract.mechanism == 'cpmm-1' ||\n- currentContract.mechanism == 'cpmm-multi-1') &&\n+ (contract.mechanism == 'cpmm-1' || contract.mechanism == 'cpmm-multi-1') &&\n contractOpenAndPublic\n \n const [following, setFollowing] = useState<boolean>()\n const [followingOpen, setFollowingOpen] = useState(false)\n@@ -107,9 +97,9 @@\n if (!user?.id) return\n db.from('contract_follows')\n .select('contract_id')\n .eq('follow_id', user.id)\n- .eq('contract_id', currentContract.id)\n+ .eq('contract_id', contract.id)\n .then((res) => {\n setFollowing((res.data?.length ?? 0) > 0)\n })\n }, [user?.id, followingOpen])\n@@ -131,12 +121,12 @@\n {\n name: following ? 'Unwatch' : 'Watch',\n onClick: async () => {\n if (following) {\n- await unfollowMarket(playContract.id, playContract.slug)\n+ await unfollowMarket(contract.id, contract.slug)\n setFollowing(false)\n } else {\n- await followMarket(playContract.id, playContract.slug)\n+ await followMarket(contract.id, contract.slug)\n setFollowing(true)\n }\n if (!user.hasSeenContractFollowModal) {\n await api('me/update', { hasSeenContractFollowModal: true })\n@@ -241,42 +231,37 @@\n ]\n \n return (\n <Row className=\"mr-4 shrink-0 items-center [&>*]:flex\">\n- {!playContract.coverImageUrl && isCreator && (\n- <ChangeBannerButton\n- contract={playContract}\n- className=\"ml-3 first:ml-0\"\n- />\n+ {!contract.coverImageUrl && isCreator && (\n+ <ChangeBannerButton contract={contract} className=\"ml-3 first:ml-0\" />\n )}\n- <FollowMarketIconButton contract={currentContract} user={user} />\n+ <FollowMarketIconButton contract={contract} user={user} />\n <CopyLinkOrShareButton\n- url={getShareUrl(currentContract, user?.username)}\n+ url={getShareUrl(contract, user?.username)}\n tooltip=\"Copy question share link\"\n className=\"text-ink-500 hover:text-ink-600\"\n size=\"xs\"\n eventTrackingName=\"copy market link\"\n- trackingInfo={{ contractId: currentContract.id }}\n+ trackingInfo={{ contractId: contract.id }}\n />\n <DropdownMenu items={dropdownItems} />\n <ContractInfoDialog\n- playContract={playContract}\n- statsContract={currentContract}\n+ contract={contract}\n user={user}\n- setIsPlay={setIsPlay}\n open={detailsOpen}\n setOpen={setDetailsOpen}\n />\n {repostOpen && (\n <RepostModal\n- playContract={playContract}\n+ playContract={contract}\n open={repostOpen}\n setOpen={setRepostOpen}\n />\n )}\n {addLiquidityEnabled && (\n <AddLiquidityModal\n- contract={currentContract}\n+ contract={contract}\n isOpen={liquidityOpen}\n setOpen={setLiquidityOpen}\n />\n )}\n@@ -284,11 +269,11 @@\n isModalOpen={reportOpen}\n label={'contract'}\n setIsModalOpen={setReportOpen}\n report={{\n- contentId: playContract.id,\n+ contentId: contract.id,\n contentType: 'contract',\n- contentOwnerId: playContract.creatorId,\n+ contentOwnerId: contract.creatorId,\n }}\n />\n <FollowMarketModal\n open={followingOpen}\n"
|
|
1422
|
-
},
|
|
1423
|
-
{
|
|
1424
|
-
"path": "web/components/new-contract/contract-params-form.tsx",
|
|
1425
|
-
"status": "modified",
|
|
1426
|
-
"diff": "Index: web/components/new-contract/contract-params-form.tsx\n===================================================================\n--- web/components/new-contract/contract-params-form.tsx\t5d7a0b8 (parent)\n+++ web/components/new-contract/contract-params-form.tsx\ta3aeeb1 (commit)\n@@ -11,11 +11,11 @@\n MAX_DESCRIPTION_LENGTH,\n MAX_QUESTION_LENGTH,\n NUMBER_BUCKETS_MAX,\n NON_BETTING_OUTCOMES,\n- twombaContractPath,\n Visibility,\n PollVoterVisibility,\n+ contractPath,\n } from 'common/contract'\n import {\n getAnte,\n getUniqueBettorBonusAmount,\n@@ -650,9 +650,9 @@\n })\n \n // Await to clear form data from localstorage after navigate, since market is created.\n // Don't clear before navigate, because looks like a bug.\n- const path = twombaContractPath(newContract)\n+ const path = contractPath(newContract)\n await router.push(path)\n resetProperties()\n } catch (e) {\n console.error('error creating contract', e)\n"
|
|
1427
|
-
},
|
|
1428
|
-
{
|
|
1429
|
-
"path": "web/middleware.ts",
|
|
1430
|
-
"status": "modified",
|
|
1431
|
-
"diff": "Index: web/middleware.ts\n===================================================================\n--- web/middleware.ts\t5d7a0b8 (parent)\n+++ web/middleware.ts\ta3aeeb1 (commit)\n@@ -1,24 +1,46 @@\n import { NextResponse, type NextRequest } from 'next/server'\n import { PROD_CONFIG } from 'common/envs/prod'\n \n export async function middleware(req: NextRequest) {\n- const path = req.nextUrl.pathname.replace('/api/', '')\n+ const url = req.nextUrl\n \n- if (pathsToSkip.includes(path)) {\n- return NextResponse.next()\n+ // Handle play parameter removal for all requests\n+ if (url.searchParams.has('play')) {\n+ url.searchParams.delete('play')\n+ return NextResponse.redirect(url, 308)\n }\n \n- return new Response('Permanent Redirect', {\n- status: 308,\n- headers: {\n- location: getProxiedRequestUrl(req, path),\n- },\n- })\n+ // Only run API proxy logic for API requests\n+ if (url.pathname.startsWith('/api/')) {\n+ const path = req.nextUrl.pathname.replace('/api/', '')\n+\n+ if (pathsToSkip.includes(path)) {\n+ return NextResponse.next()\n+ }\n+\n+ return new Response('Permanent Redirect', {\n+ status: 308,\n+ headers: {\n+ location: getProxiedRequestUrl(req, path),\n+ },\n+ })\n+ }\n+\n+ // For non-API requests, just continue normally\n+ return NextResponse.next()\n }\n \n export const config = {\n- matcher: ['/api/v0/:path*'],\n+ matcher: [\n+ // API proxy\n+ '/api/v0/:path*',\n+ // Contract pages - be specific about the format\n+ // This matches /username/contract-slug but not / or /browse etc\n+ '/([^/]+)/([^/]+)',\n+ // Embed pages\n+ '/embed/([^/]+)/([^/]+)',\n+ ],\n }\n \n const pathsToSkip = ['v0/deployment-id', 'v0/revalidate']\n \n"
|
|
1432
|
-
},
|
|
1433
|
-
{
|
|
1434
|
-
"path": "web/pages/[username]/[contractSlug].tsx",
|
|
1435
|
-
"status": "modified",
|
|
1436
|
-
"diff": "Index: web/pages/[username]/[contractSlug].tsx\n===================================================================\n--- web/pages/[username]/[contractSlug].tsx\t5d7a0b8 (parent)\n+++ web/pages/[username]/[contractSlug].tsx\ta3aeeb1 (commit)\n@@ -7,19 +7,17 @@\n } from 'common/contract'\n import { ContractMetric } from 'common/contract-metric'\n import { getContractParams } from 'common/contract-params'\n import { base64toPoints } from 'common/edge/og'\n-import { CASH_SUFFIX } from 'common/envs/constants'\n-import { getContract, getContractFromSlug } from 'common/supabase/contracts'\n+import { getContractFromSlug } from 'common/supabase/contracts'\n import { removeUndefinedProps } from 'common/util/object'\n-import { pick, sortBy, uniqBy } from 'lodash'\n+import { sortBy, uniqBy } from 'lodash'\n import { ContractBetsTable } from 'web/components/bet/contract-bets-table'\n import { YourOrders } from 'web/components/bet/order-book'\n import { ContractPageContent } from 'web/components/contract/contract-page'\n import { ContractSEO } from 'web/components/contract/contract-seo'\n import { Col } from 'web/components/layout/col'\n import { Page } from 'web/components/layout/page'\n-import { useSweepstakes } from 'web/components/sweepstakes-provider'\n import { Title } from 'web/components/widgets/title'\n import { useIsIframe } from 'web/hooks/use-is-iframe'\n import { useIsPageVisible } from 'web/hooks/use-page-visible'\n import { useUser } from 'web/hooks/use-user'\n@@ -30,9 +28,9 @@\n \n export async function getStaticProps(ctx: {\n params: { username: string; contractSlug: string }\n }) {\n- const { username, contractSlug } = ctx.params\n+ const { contractSlug } = ctx.params\n const adminDb = await initSupabaseAdmin()\n const contract = await getContractFromSlug(adminDb, contractSlug)\n \n if (!contract) {\n@@ -50,44 +48,14 @@\n },\n }\n }\n \n- if (contract.token === 'CASH') {\n- const manaContract = contract.siblingContractId\n- ? await getContract(adminDb, contract.siblingContractId)\n- : null\n- const slug = manaContract?.slug ?? contractSlug.replace(CASH_SUFFIX, '')\n-\n- return {\n- redirect: {\n- destination: `/${username}/${slug}?play=false`,\n- permanent: false,\n- },\n- }\n- }\n-\n const props = await getContractParams(contract, adminDb)\n \n- // Fetch sibling contract if it exists\n- let cash = undefined\n- if (contract.siblingContractId) {\n- const cashContract = await getContract(adminDb, contract.siblingContractId)\n- if (cashContract) {\n- const params = await getContractParams(cashContract, adminDb)\n- cash = pick(params, [\n- 'contract',\n- 'lastBetTime',\n- 'pointsString',\n- 'multiPointsString',\n- 'totalPositions',\n- 'totalBets',\n- ])\n- }\n- }\n return {\n props: {\n state: 'authed',\n- params: removeUndefinedProps({ ...props, cash }),\n+ params: removeUndefinedProps(props),\n },\n }\n }\n \n@@ -109,39 +77,23 @@\n return <NonPrivateContractPage contractParams={props.params} />\n }\n \n function NonPrivateContractPage(props: { contractParams: ContractParams }) {\n- const { contract, pointsString, cash } = props.contractParams\n- const { prefersPlay } = useSweepstakes()\n+ const { contract, pointsString } = props.contractParams\n \n const points = pointsString ? base64toPoints(pointsString) : []\n- const cashPoints = cash\n- ? cash.pointsString\n- ? base64toPoints(cash.pointsString)\n- : []\n- : null\n \n const inIframe = useIsIframe()\n if (!contract) {\n return <Custom404 customText=\"Unable to fetch question\" />\n }\n if (inIframe) {\n- return (\n- <ContractEmbedPage\n- contract={contract}\n- points={points}\n- cashContract={cash ? cash.contract : null}\n- cashPoints={cashPoints}\n- />\n- )\n+ return <ContractEmbedPage contract={contract} points={points} />\n }\n \n return (\n <Page trackPageView={false} className=\"xl:col-span-10\">\n- <ContractSEO\n- contract={!prefersPlay && cash?.contract ? cash.contract : contract}\n- points={pointsString}\n- />\n+ <ContractSEO contract={contract} points={pointsString} />\n <ContractPageContent key={contract.id} {...props.contractParams} />\n </Page>\n )\n }\n"
|
|
1437
|
-
},
|
|
1438
|
-
{
|
|
1439
|
-
"path": "web/pages/embed/[username]/[contractSlug].tsx",
|
|
1440
|
-
"status": "modified",
|
|
1441
|
-
"diff": "Index: web/pages/embed/[username]/[contractSlug].tsx\n===================================================================\n--- web/pages/embed/[username]/[contractSlug].tsx\t5d7a0b8 (parent)\n+++ web/pages/embed/[username]/[contractSlug].tsx\ta3aeeb1 (commit)\n@@ -7,15 +7,15 @@\n } from 'common/chart'\n import {\n CPMMMultiContract,\n Contract,\n+ contractPath,\n getMainBinaryMCAnswer,\n isBinaryMulti,\n- twombaContractPath,\n } from 'common/contract'\n import { getMultiBetPoints, getSingleBetPoints } from 'common/contract-params'\n import { DOMAIN, TRADE_TERM } from 'common/envs/constants'\n-import { getContract, getContractFromSlug } from 'common/supabase/contracts'\n+import { getContractFromSlug } from 'common/supabase/contracts'\n import { formatMoney } from 'common/util/format'\n import { getShareUrl } from 'common/util/share'\n import Image from 'next/image'\n import { useRouter } from 'next/router'\n@@ -82,16 +82,8 @@\n }\n \n const points = await getHistoryData(contract)\n \n- let cashContract = null\n- let cashPoints = null\n- if (contract.siblingContractId) {\n- cashContract = await getContract(db, contract.siblingContractId)\n- if (cashContract) {\n- cashPoints = await getHistoryData(cashContract)\n- }\n- }\n let multiPoints = null\n if (\n contract.outcomeType === 'MULTI_NUMERIC' ||\n contract.outcomeType === 'DATE'\n@@ -108,9 +100,9 @@\n pointsToBase64(v)\n )\n }\n return {\n- props: { contract, points, multiPoints, cashContract, cashPoints },\n+ props: { contract, points, multiPoints },\n }\n }\n \n export async function getStaticPaths() {\n@@ -120,34 +112,24 @@\n export default function ContractEmbedPage(props: {\n contract: Contract\n points: Points | null\n multiPoints?: MultiBase64Points | null\n- cashContract: Contract | null\n- cashPoints: Points | null\n }) {\n const [showQRCode, setShowQRCode] = useState(false)\n- const [isCash, setIsCash] = useState(false)\n- const { cashContract, points, cashPoints } = props\n+ const { points } = props\n const multiPoints = props.multiPoints\n ? unserializeBase64Multi(props.multiPoints)\n : null\n \n const contract = useLiveContract(props.contract)\n- const liveCashContract = cashContract\n- ? // eslint-disable-next-line react-hooks/rules-of-hooks\n- useLiveContract(cashContract)\n- : null\n \n const router = useRouter()\n \n useEffect(() => {\n if (router.isReady) {\n if (router.query.qr !== undefined) {\n setShowQRCode(true)\n }\n- if (router.query.play !== undefined) {\n- setIsCash(router.query.play === 'false')\n- }\n }\n }, [router.isReady, router.query])\n \n useEffect(() => {\n@@ -159,19 +141,19 @@\n hostname: window.location.hostname,\n })\n }, [contract?.creatorId, contract?.id, contract?.slug])\n \n- if (!contract || (isCash && !liveCashContract)) {\n+ if (!contract) {\n return <Custom404 />\n }\n \n return (\n <>\n <NoSEO />\n- <ContractSEO contract={isCash ? liveCashContract! : contract} />\n+ <ContractSEO contract={contract} />\n <ContractSmolView\n- contract={isCash ? liveCashContract! : contract}\n- points={isCash ? cashPoints : points}\n+ contract={contract}\n+ points={points}\n multiPoints={multiPoints}\n showQRCode={showQRCode}\n />\n </>\n@@ -286,9 +268,9 @@\n const isPoll = outcomeType === 'POLL'\n const isMultiNumeric = outcomeType === 'MULTI_NUMERIC'\n const isDate = outcomeType === 'DATE'\n \n- const href = `https://${DOMAIN}${twombaContractPath(contract)}`\n+ const href = `https://${DOMAIN}${contractPath(contract)}`\n const user = useUser()\n const shareUrl = getShareUrl(contract, user?.username)\n \n const showMultiChart = isMulti && !!props.multiPoints\n"
|
|
1442
|
-
}
|
|
1443
|
-
]
|
|
1444
|
-
},
|
|
1445
|
-
{
|
|
1446
|
-
"id": "remove-tiers",
|
|
1447
|
-
"sha": "13133e30c40d2de635158bd58519bac0b473f069",
|
|
1448
|
-
"parentSha": "262f7ebf215c6315a321fc7ea83574d64c21b2d1",
|
|
1449
|
-
"spec": "Implement full removal of legacy market tiers and adopt numeric liquidity tiers throughout backend, common, and web layers.\n\nBack-end API and shared changes:\n1) Remove tier mutation on liquidity changes:\n- backend/api/src/add-liquidity.ts: Stop importing getTierFromLiquidity from common/tier and remove marketTier update in updateContract. Only increment subsidyPool and totalLiquidity.\n- backend/api/src/remove-liquidity.ts: Same—remove getTierFromLiquidity import and marketTier updates; only decrement subsidyPool and totalLiquidity.\n\n2) Update answer creation cost to be liquidity-derived:\n- backend/api/src/create-answer-cpmm.ts:\n - Replace import of getTieredAnswerCost and getTierFromLiquidity with getAnswerCostFromLiquidity from common/tier.\n - Compute answerCost as getAnswerCostFromLiquidity(contract.totalLiquidity, contract.answers.length) instead of tier-based cost.\n\n3) Update market creation to use numeric liquidity tier:\n- backend/api/src/create-market.ts:\n - Remove import and any usage of getTieredCost from common/tier.\n - Accept a new liquidityTier: number parameter from validateMarketBody and remove marketTier from the request/contract creation flow.\n - Remove extraLiquidity from per-type create schemas (binary, pseudo-numeric, multi) and instead accept optional extraLiquidity at the top-level create props.\n - Compute ante using getAnte(outcomeType, numAnswers, liquidityTier). Compute totalMarketCost as ante + (extraLiquidity ?? 0). Enforce ante >= 1; enforce closeTime in future as before. Do not compute marketTier-based costs.\n - Pass liquidityTier through to new contract creation data as needed; do not include marketTier anywhere.\n\n4) Remove tier-based filtering from activity and search:\n- backend/api/src/get-site-activity.ts: Remove SQL filter and tier != 'play' condition; keep visibility/public and other filters.\n- backend/api/src/search-contracts.ts: Remove TierParamsType import; remove marketTier filter param from search props, and do not pass marketTier to shared SQL builder. Ensure search continues to work without tier filters.\n- backend/shared/src/supabase/search-contracts.ts:\n - Remove imports MarketTierType, TierParamsType, tiers and any type usage.\n - Remove marketTier parameters from getForYouSQL, basicSearchSQL, getSearchContractSQL, getSearchContractWhereSQL. Delete tier-based SQL where clauses. Adjust function signatures and invocations accordingly.\n- backend/shared/src/topic-interests.ts: Remove contracts.tier != 'play' minimum-quality filter.\n- backend/shared/src/weekly-markets-emails.ts: Stop passing marketTier in getForYouMarkets call.\n\n5) Remove tier-related scripts and SQL:\n- Delete backend/scripts/decrease-deposits-for-created-answers.ts.\n- Delete backend/scripts/shift-tiers/backfill-tier-column.sql and get-tier-from-liquidity-function.sql.\n\nCommon (shared) package changes:\n6) API search types and market types:\n- common/src/api/market-search-types.ts: Remove marketTier zod param from searchProps.\n- common/src/api/market-types.ts:\n - Remove MarketTierType and tiers imports.\n - Remove marketTier from LiteMarket type and from toLiteMarket serialization.\n - Remove extraLiquidity from per-type create schemas (createBinarySchema, createNumericSchema, createMultiSchema).\n - In createMarketProps, add extraLiquidity?: number (min 1) and liquidityTier: number().min(0). Remove marketTier enum.\n\n7) Contract type model:\n- common/src/contract.ts: Remove optional marketTier?: MarketTierType field and any related tier imports.\n\n8) Economy and tier helpers:\n- common/src/economy.ts:\n - Import answerCostTiers and liquidityTiers from common/tier.\n - Redefine getAnte(outcomeType, numAnswers, liquidityTier):\n - If outcomeType === 'POLL', return 10.\n - If outcomeType === 'MULTIPLE_CHOICE', determine tierIndex by the first liquidityTiers element >= liquidityTier; ante is max(numAnswers * answerCostTiers[tierIndex], liquidityTier) when numAnswers is provided; otherwise liquidityTiers[tierIndex].\n - For all other predictive types, return liquidityTier.\n - Keep MINIMUM_BOUNTY = 1000; remove FIXED_ANTE/MIN_ANSWER_COST-based tier logic.\n\n9) Tier utilities:\n- common/src/tier.ts:\n - Replace legacy tiers array and types with:\n - export const liquidityTiers = [100, 1_000, 10_000, 100_000] as const.\n - export const answerCostTiers = [25, 100, 1000, 10000] as const.\n - export function getTierFromLiquidity(liquidity: number): number that returns the index of the first liquidityTiers element >= liquidity.\n - export function getAnswerCostFromLiquidity(liquidity: number, numAnswers: number): number that returns answerCostTiers[index], where index is determined by first liquidityTiers >= (liquidity / numAnswers).\n - Remove MarketTierType, TierParamsType, tiers, getTieredCost, getTieredAnswerCost, and old getTierFromLiquidity(contract, liquidity) functions.\n\nFrontend (web) changes:\n10) Answers panel cost display:\n- web/components/answers/create-answer-panel.tsx: Replace getTieredAnswerCost/marketTier usage with getAnswerCostFromLiquidity(contract.totalLiquidity, contract.answers.length) when displaying add-answer cost.\n\n11) Bet panel quick-add sizing:\n- web/components/bet/bet-panel.tsx: Replace marketTier resolution with liquidityTier index using getTierFromLiquidity(contract.totalLiquidity). Set quickAddButtonSize to 'small' when liquidityTier === 0, or when cpmm-multi-1 and liquidityTier === 1 and !shouldAnswersSumToOne, mirroring previous logic but using numeric tiers.\n\n12) Remove upgrade-tier UI:\n- Delete web/components/contract/upgrade-tier-button.tsx and any references to it.\n\n13) New contract form and cost section:\n- web/components/new-contract/contract-params-form.tsx:\n - Remove MarketTierType state and persistence; introduce liquidityTier: number state defaulting to liquidityTiers[0].\n - Compute ante with getAnte(outcomeType, numAnswers, liquidityTier). Validate ante <= balance and other existing validations.\n - Pass liquidityTier (not marketTier) in create request body; adjust button label cost to display ante.\n - Update props passed to CostSection to use liquidityTier and setLiquidityTier, and pass numAnswers so it can compute cost.\n- web/components/new-contract/cost-section.tsx:\n - Remove tier icons/components and MarketTierType types.\n - Accept props: liquidityTier: number, setLiquidityTier(tier: number), outcomeType, numAnswers.\n - Compute ante via getAnte(outcomeType, numAnswers, liquidityTier).\n - Render selectable liquidity options using liquidityTiers; highlight current selection; display amount = liquidityTier numeric value for each option.\n - Show AddFundsModal when ante > balance.\n\n14) Search UI and filters:\n- web/components/search.tsx: Remove market tier query param (MARKET_TIER_KEY) wiring, default values, and passing in fetch calls.\n- web/components/search/contract-filters.tsx and web/components/search/filter-pills.tsx: Remove tier filter UI and logic (TierDropdownPill, toggleTier, DEFAULT_TIER, related imports and pills). Ensure remaining filters (sort, for you, sweepstakes, etc.) function unchanged.\n\n15) Liquidity tooltip:\n- web/components/tiers/liquidity-tooltip.tsx: Remove MarketTierType imports and TierIcon/getPresentedTierName exports; keep tooltip focused on liquidity amount and explanatory text.\n\n16) Amount input adjustments:\n- web/components/widgets/amount-input.tsx: Replace marketTier?: MarketTierType prop with liquidityTier?: number. Adjust smallAmounts condition to use liquidityTier === 0.\n\n17) Admin sports market creation:\n- web/lib/admin/create-sports-markets.ts: Import liquidityTiers and include liquidityTier: liquidityTiers[1] in create calls so sports markets default to the second numeric tier.\n\nData and cleanup:\n18) Remove any database references to contracts.tier in search/filters; do not rely on a stored marketTier field in contract data. Leave existing stored marketTier values unused.\n\n19) Ensure type and schema updates compile across packages; update imports accordingly. Remove dead imports (MarketTierType, TierParamsType, tiers, getTieredCost, getTieredAnswerCost) wherever referenced.\n\nAcceptance criteria:\n- Creating a market requires liquidityTier and computes ante/total cost without using discrete tiers; form shows numeric liquidity options and correctly enforces balance.\n- Adding/removing liquidity no longer updates any marketTier on the contract.\n- Creating answers shows add-answer cost based on current liquidity/answers, not tiers.\n- Search and site activity endpoints and UI no longer accept or display tier filters; results remain correct without tier-based filtering.\n- Admin sports market creator sets liquidityTier and succeeds.\n- Code compiles with no references to MarketTierType, TierParamsType, tiers constant, getTieredCost, or getTieredAnswerCost in live code.\n- Obsolete scripts and UI components related to tiers are removed.",
|
|
1450
|
-
"prompt": "Migrate the app away from discrete market tiers (play/plus/premium/crystal) to numeric liquidity tiers and liquidity-derived costs. Update the market creation flow to accept a numeric liquidityTier and compute the ante and total cost from it. Remove all tier-based filtering and UI from search and site activity. Adjust answer-creation cost to depend on current market liquidity and number of answers. Remove any code that updates or depends on a marketTier field when liquidity changes. Replace tier selection UI with simple numeric liquidity choices, ensure validation uses the new ante, and keep existing behavior for sweepstakes and other filters unchanged. Remove obsolete scripts and components tied to tiers. The final system should compile and run with no references to the old tier enums or tier-based cost helpers.",
|
|
1451
|
-
"supplementalFiles": [
|
|
1452
|
-
"web/public/custom-components/tiers.tsx",
|
|
1453
|
-
"web/components/new-contract/create-contract-types.tsx"
|
|
1454
|
-
],
|
|
1455
|
-
"fileDiffs": [
|
|
1456
|
-
{
|
|
1457
|
-
"path": "backend/api/src/add-liquidity.ts",
|
|
1458
|
-
"status": "modified",
|
|
1459
|
-
"diff": "Index: backend/api/src/add-liquidity.ts\n===================================================================\n--- backend/api/src/add-liquidity.ts\t262f7eb (parent)\n+++ backend/api/src/add-liquidity.ts\t13133e3 (commit)\n@@ -6,9 +6,8 @@\n import { getContract, getUser } from 'shared/utils'\n import { onCreateLiquidityProvision } from './on-update-liquidity-provision'\n import { insertLiquidity } from 'shared/supabase/liquidity'\n import { convertLiquidity } from 'common/supabase/liquidity'\n-import { getTierFromLiquidity } from 'common/tier'\n import { FieldVal } from 'shared/supabase/utils'\n import { updateContract } from 'shared/supabase/contracts'\n \n export const addLiquidity: APIHandler<\n@@ -67,12 +66,8 @@\n \n await updateContract(tx, contractId, {\n subsidyPool: FieldVal.increment(subsidyAmount),\n totalLiquidity: FieldVal.increment(subsidyAmount),\n- marketTier: getTierFromLiquidity(\n- contract,\n- contract.totalLiquidity + subsidyAmount\n- ),\n })\n \n return {\n result: liquidity,\n"
|
|
1460
|
-
},
|
|
1461
|
-
{
|
|
1462
|
-
"path": "backend/api/src/create-answer-cpmm.ts",
|
|
1463
|
-
"status": "modified",
|
|
1464
|
-
"diff": "Index: backend/api/src/create-answer-cpmm.ts\n===================================================================\n--- backend/api/src/create-answer-cpmm.ts\t262f7eb (parent)\n+++ backend/api/src/create-answer-cpmm.ts\t13133e3 (commit)\n@@ -35,9 +35,9 @@\n insertAnswer,\n updateAnswer,\n updateAnswers,\n } from 'shared/supabase/answers'\n-import { getTieredAnswerCost, getTierFromLiquidity } from 'common/tier'\n+import { getAnswerCostFromLiquidity } from 'common/tier'\n import { updateContract } from 'shared/supabase/contracts'\n import { FieldVal } from 'shared/supabase/utils'\n import { runTransactionWithRetries } from 'shared/transact-with-retries'\n import { Bet, getNewBetId, LimitBet, maker } from 'common/bet'\n@@ -104,10 +104,11 @@\n creatorId: string\n ) => {\n const { shouldAnswersSumToOne } = contract\n \n- const answerCost = getTieredAnswerCost(\n- getTierFromLiquidity(contract, contract.totalLiquidity)\n+ const answerCost = getAnswerCostFromLiquidity(\n+ contract.totalLiquidity,\n+ contract.answers.length\n )\n \n const { newAnswer, user } = await runTransactionWithRetries(\n async (pgTrans) => {\n"
|
|
1465
|
-
},
|
|
1466
|
-
{
|
|
1467
|
-
"path": "backend/api/src/create-market.ts",
|
|
1468
|
-
"status": "modified",
|
|
1469
|
-
"diff": "Index: backend/api/src/create-market.ts\n===================================================================\n--- backend/api/src/create-market.ts\t262f7eb (parent)\n+++ backend/api/src/create-market.ts\t13133e3 (commit)\n@@ -57,9 +57,8 @@\n import { generateAntes } from 'shared/create-contract-helpers'\n import { betsQueue } from 'shared/helpers/fn-queue'\n import { convertUser } from 'common/supabase/users'\n import { camelCase, first } from 'lodash'\n-import { getTieredCost } from 'common/tier'\n \n type Body = ValidatedAPIParams<'market'>\n \n export const createMarket: APIHandler<'market'> = async (body, auth) => {\n@@ -118,16 +117,16 @@\n shouldAnswersSumToOne,\n totalBounty,\n isAutoBounty,\n visibility,\n- marketTier,\n idempotencyKey,\n sportsStartTimestamp,\n sportsEventId,\n sportsLeague,\n answerShortTexts,\n answerImageUrls,\n takerAPIOrdersDisabled,\n+ liquidityTier,\n } = validateMarketBody(body)\n \n const userId = auth.uid\n \n@@ -135,13 +134,15 @@\n \n const hasOtherAnswer = addAnswersMode !== 'DISABLED' && shouldAnswersSumToOne\n const numAnswers = (answers?.length ?? 0) + (hasOtherAnswer ? 1 : 0)\n \n- const unmodifiedAnte =\n- (totalBounty ?? getAnte(outcomeType, numAnswers)) + (extraLiquidity ?? 0)\n+ const ante =\n+ outcomeType === 'BOUNTIED_QUESTION' && totalBounty\n+ ? totalBounty\n+ : getAnte(outcomeType, numAnswers, liquidityTier)\n+ const totalMarketCost = ante + (extraLiquidity ?? 0)\n+ if (ante < 1) throw new APIError(400, 'Ante must be at least 1')\n \n- if (unmodifiedAnte < 1) throw new APIError(400, 'Ante must be at least 1')\n-\n const closeTime = await getCloseTimestamp(\n closeTimeRaw,\n question,\n outcomeType,\n@@ -150,16 +151,8 @@\n \n if (closeTime && closeTime < Date.now())\n throw new APIError(400, 'Question must close in the future')\n \n- const totalMarketCost = marketTier\n- ? getTieredCost(unmodifiedAnte, marketTier, outcomeType)\n- : unmodifiedAnte\n- const ante =\n- outcomeType === 'MULTIPLE_CHOICE' && !shouldAnswersSumToOne\n- ? totalMarketCost\n- : Math.min(unmodifiedAnte, totalMarketCost)\n-\n const duplicateSubmissionUrl = await getDuplicateSubmissionUrl(\n idempotencyKey,\n pg\n )\n@@ -218,9 +211,8 @@\n answerImageUrls,\n addAnswersMode,\n shouldAnswersSumToOne,\n isAutoBounty,\n- marketTier,\n token: 'MANA',\n sportsStartTimestamp,\n sportsEventId,\n sportsLeague,\n@@ -330,9 +322,10 @@\n groupIds,\n visibility = 'public',\n isTwitchContract,\n utcOffset,\n- marketTier,\n+ extraLiquidity,\n+ liquidityTier,\n idempotencyKey,\n sportsStartTimestamp,\n sportsEventId,\n sportsLeague,\n@@ -354,15 +347,14 @@\n answerImageUrls: string[] | undefined,\n addAnswersMode: add_answers_mode | undefined,\n shouldAnswersSumToOne: boolean | undefined,\n totalBounty: number | undefined,\n- isAutoBounty: boolean | undefined,\n- extraLiquidity: number | undefined\n+ isAutoBounty: boolean | undefined\n \n if (outcomeType === 'PSEUDO_NUMERIC') {\n const parsed = validateMarketType(outcomeType, createNumericSchema, body)\n \n- ;({ min, max, isLogScale, extraLiquidity } = parsed)\n+ ;({ min, max, isLogScale } = parsed)\n const { initialValue } = parsed\n \n if (max - min <= 0.01)\n throw new APIError(400, 'Max must be greater than min by more than 0.01')\n@@ -385,9 +377,9 @@\n \n if (outcomeType === 'BINARY') {\n const parsed = validateMarketType(outcomeType, createBinarySchema, body)\n \n- ;({ initialProb, extraLiquidity } = parsed)\n+ ;({ initialProb } = parsed)\n }\n if (outcomeType === 'NUMBER') {\n if (!MULTI_NUMERIC_CREATION_ENABLED)\n throw new APIError(\n@@ -420,9 +412,8 @@\n answerShortTexts,\n answerImageUrls,\n addAnswersMode,\n shouldAnswersSumToOne,\n- extraLiquidity,\n } = validateMarketType(outcomeType, createMultiSchema, body))\n if (answers.length < 2 && addAnswersMode === 'DISABLED')\n throw new APIError(\n 400,\n@@ -463,9 +454,9 @@\n addAnswersMode,\n shouldAnswersSumToOne,\n totalBounty,\n isAutoBounty,\n- marketTier,\n+ liquidityTier,\n idempotencyKey,\n sportsStartTimestamp,\n sportsEventId,\n sportsLeague,\n"
|
|
1470
|
-
},
|
|
1471
|
-
{
|
|
1472
|
-
"path": "backend/api/src/get-site-activity.ts",
|
|
1473
|
-
"status": "modified",
|
|
1474
|
-
"diff": "Index: backend/api/src/get-site-activity.ts\n===================================================================\n--- backend/api/src/get-site-activity.ts\t262f7eb (parent)\n+++ backend/api/src/get-site-activity.ts\t13133e3 (commit)\n@@ -10,9 +10,14 @@\n // todo: personalization based on followed users & topics\n export const getSiteActivity: APIHandler<'get-site-activity'> = async (\n props\n ) => {\n- const { limit, offset = 0, blockedGroupSlugs = [], blockedContractIds = [] } = props\n+ const {\n+ limit,\n+ offset = 0,\n+ blockedGroupSlugs = [],\n+ blockedContractIds = [],\n+ } = props\n log('getSiteActivity called', { limit })\n \n const blockedUserIds = [\n 'FDWTsTHFytZz96xmcKzf7S5asYL2', // yunabot (does a lot of manual trades)\n@@ -67,9 +72,8 @@\n ),\n pg.manyOrNone(\n `select * from contracts\n where visibility = 'public'\n- and tier != 'play'\n and creator_id != all($1)\n and id != all($2)\n and not exists (\n select 1 from group_contracts gc\n"
|
|
1475
|
-
},
|
|
1476
|
-
{
|
|
1477
|
-
"path": "backend/api/src/remove-liquidity.ts",
|
|
1478
|
-
"status": "modified",
|
|
1479
|
-
"diff": "Index: backend/api/src/remove-liquidity.ts\n===================================================================\n--- backend/api/src/remove-liquidity.ts\t262f7eb (parent)\n+++ backend/api/src/remove-liquidity.ts\t13133e3 (commit)\n@@ -1,6 +1,5 @@\n import { removeCpmmLiquidity } from 'common/calculate-cpmm'\n-import { getTierFromLiquidity } from 'common/tier'\n import { formatMoneyWithDecimals } from 'common/util/format'\n import { runTransactionWithRetries } from 'shared/transact-with-retries'\n import { updateContract } from 'shared/supabase/contracts'\n import { FieldVal } from 'shared/supabase/utils'\n@@ -66,12 +65,8 @@\n \n await updateContract(pgTrans, contractId, {\n subsidyPool: FieldVal.increment(-takeFromPending),\n totalLiquidity: FieldVal.increment(-totalAmount),\n- marketTier: getTierFromLiquidity(\n- contract,\n- contract.totalLiquidity - totalAmount\n- ),\n })\n \n await runTxnInBetQueue(pgTrans, {\n fromType: 'CONTRACT',\n"
|
|
1480
|
-
},
|
|
1481
|
-
{
|
|
1482
|
-
"path": "backend/api/src/search-contracts.ts",
|
|
1483
|
-
"status": "modified",
|
|
1484
|
-
"diff": "Index: backend/api/src/search-contracts.ts\n===================================================================\n--- backend/api/src/search-contracts.ts\t262f7eb (parent)\n+++ backend/api/src/search-contracts.ts\t13133e3 (commit)\n@@ -13,9 +13,8 @@\n import { convertContract } from 'common/supabase/contracts'\n import { log } from 'shared/utils'\n import { toLiteMarket } from 'common/api/market-types'\n import { searchProps } from 'common/api/market-search-types'\n-import { TierParamsType } from 'common/tier'\n \n export const searchMarketsLite: APIHandler<'search-markets'> = async (\n props,\n auth\n@@ -44,9 +43,8 @@\n limit,\n topicSlug: possibleTopicSlug,\n forYou,\n creatorId,\n- marketTier,\n token,\n gids: groupIds,\n } = props\n const isPrizeMarket =\n@@ -81,9 +79,8 @@\n limit,\n offset,\n sort,\n isPrizeMarket,\n- marketTier as TierParamsType,\n token,\n undefined,\n creatorId\n ),\n@@ -99,9 +96,8 @@\n offset,\n sort,\n isPrizeMarket,\n token,\n- marketTier: marketTier as TierParamsType,\n })\n return await pg.map(forYouSql, [term], (r) => convertContract(r))\n }\n } else if (isRecent && !term && userId) {\n@@ -136,9 +132,8 @@\n searchType,\n isPrizeMarket,\n token,\n groupIds,\n- marketTier: marketTier as TierParamsType,\n })\n )\n .join(';')\n \n"
|
|
1485
|
-
},
|
|
1486
|
-
{
|
|
1487
|
-
"path": "backend/scripts/decrease-deposits-for-created-answers.ts",
|
|
1488
|
-
"status": "deleted",
|
|
1489
|
-
"diff": "Index: backend/scripts/decrease-deposits-for-created-answers.ts\n===================================================================\n--- backend/scripts/decrease-deposits-for-created-answers.ts\t262f7eb (parent)\n+++ backend/scripts/decrease-deposits-for-created-answers.ts\t13133e3 (commit)\n@@ -1,32 +0,0 @@\n-import { FieldValue } from 'firebase-admin/firestore'\n-import { runScript } from './run-script'\n-\n-import { getTieredAnswerCost } from 'common/tier'\n-\n-if (require.main === module) {\n- runScript(async ({ pg, firestore }) => {\n- const createdAnswers = await pg.manyOrNone(`\n- select answers.* from answers\n- join contracts on answers.contract_id = contracts.id\n- where answers.created_time > (contracts.created_time + interval '5 seconds')\n- `)\n-\n- console.log('createdAnswers', createdAnswers)\n-\n- const answerCost = getTieredAnswerCost(undefined)\n- for (const answer of createdAnswers) {\n- console.log(\n- 'decrease deposits for user',\n- answer.user_id,\n- 'by',\n- answerCost\n- )\n- await firestore\n- .collection('users')\n- .doc(answer.user_id)\n- .update({\n- totalDeposits: FieldValue.increment(-answerCost),\n- })\n- }\n- })\n-}\n"
|
|
1490
|
-
},
|
|
1491
|
-
{
|
|
1492
|
-
"path": "backend/scripts/shift-tiers/backfill-tier-column.sql",
|
|
1493
|
-
"status": "deleted",
|
|
1494
|
-
"diff": "Index: backend/scripts/shift-tiers/backfill-tier-column.sql\n===================================================================\n--- backend/scripts/shift-tiers/backfill-tier-column.sql\t262f7eb (parent)\n+++ backend/scripts/shift-tiers/backfill-tier-column.sql\t13133e3 (commit)\n@@ -1,53 +0,0 @@\n--- Create a function to update contracts in batches\n-drop function if exists update_contract_tiers_efficient (batch_size integer, start_time timestamp);\n-\n-create\n-or replace function update_contract_tiers_efficient (batch_size integer, start_time timestamp) returns table (\n- updated_count integer,\n- last_updated_time timestamp\n-) as $$\n-DECLARE\n- batch_updated INTEGER;\n- last_updated_timestamp TIMESTAMP;\n-BEGIN\n- WITH to_update AS (\n- SELECT \n- id, \n- created_time,\n- get_tier_from_liquidity_efficient(\n- outcome_type, \n- CASE WHEN data ? 'answers' THEN jsonb_array_length(data->'answers') ELSE NULL END,\n- COALESCE((data->>'totalLiquidity')::NUMERIC, 0)\n- ) AS new_tier\n- FROM contracts\n- -- WHERE created_time > start_time\n- WHERE token = 'CASH' -- Added condition\n- AND tier IS NULL -- Added condition\n- AND (data->>'totalLiquidity' IS NOT NULL)\n- ORDER BY created_time\n- LIMIT batch_size\n- )\n- UPDATE contracts c\n- SET data = c.data || jsonb_build_object('marketTier', tu.new_tier)\n- FROM to_update tu\n- WHERE c.id = tu.id\n- AND (c.tier IS DISTINCT FROM tu.new_tier OR c.tier IS NULL);\n-\n- GET DIAGNOSTICS batch_updated = ROW_COUNT;\n- \n- IF batch_updated > 0 THEN\n- SELECT created_time INTO last_updated_timestamp\n- FROM contracts\n- WHERE created_time > start_time\n- ORDER BY created_time\n- LIMIT 1 OFFSET (batch_size - 1);\n- ELSE\n- last_updated_timestamp := start_time;\n- END IF;\n-\n- RETURN QUERY SELECT batch_updated, last_updated_timestamp;\n-END;\n-$$ language plpgsql;\n-\n--- Commit the transaction to save the functions\n-commit;\n"
|
|
1495
|
-
},
|
|
1496
|
-
{
|
|
1497
|
-
"path": "backend/scripts/shift-tiers/get-tier-from-liquidity-function.sql",
|
|
1498
|
-
"status": "deleted",
|
|
1499
|
-
"diff": "Index: backend/scripts/shift-tiers/get-tier-from-liquidity-function.sql\n===================================================================\n--- backend/scripts/shift-tiers/get-tier-from-liquidity-function.sql\t262f7eb (parent)\n+++ backend/scripts/shift-tiers/get-tier-from-liquidity-function.sql\t13133e3 (commit)\n@@ -1,48 +0,0 @@\n--- Create a more efficient function to get the tier from liquidity\n-create\n-or replace function get_tier_from_liquidity_efficient (\n- outcome_type text,\n- num_answers integer,\n- liquidity numeric\n-) returns text as $$\n-DECLARE\n- ante NUMERIC;\n- tier_liquidity NUMERIC;\n- fixed_ante CONSTANT NUMERIC := 1000;\n- multiple_choice_minimum_cost CONSTANT NUMERIC := 1000;\n-BEGIN\n- IF outcome_type IN ('POLL', 'BOUNTIED_QUESTION') THEN\n- RETURN NULL;\n- END IF;\n-\n- ante := CASE outcome_type\n- WHEN 'BINARY' THEN fixed_ante\n- WHEN 'MULTIPLE_CHOICE' THEN GREATEST(fixed_ante / 10 * COALESCE(num_answers, 0), multiple_choice_minimum_cost)\n- WHEN 'FREE_RESPONSE' THEN fixed_ante / 10\n- WHEN 'PSEUDO_NUMERIC' THEN fixed_ante * 2.5\n- WHEN 'STONK' THEN fixed_ante\n- WHEN 'NUMBER' THEN fixed_ante * 10\n- ELSE fixed_ante\n- END;\n-\n- -- Special handling for NUMBER type\n-IF outcome_type = 'NUMBER' THEN\n- IF liquidity >= (ante * 10) THEN \n- RETURN 'crystal';\n- ELSIF liquidity >= ante THEN \n- RETURN 'premium';\n- ELSIF liquidity >= (ante / 10) THEN \n- RETURN 'plus';\n- ELSE \n- RETURN 'play';\n- END IF;\n-END IF;\n-\n- -- For other types\n- IF liquidity >= ante * 100 THEN RETURN 'crystal';\n- ELSIF liquidity >= ante * 10 THEN RETURN 'premium';\n- ELSIF liquidity >= ante THEN RETURN 'plus';\n- ELSE RETURN 'play';\n- END IF;\n-END;\n-$$ language plpgsql immutable;\n"
|
|
1500
|
-
},
|
|
1501
|
-
{
|
|
1502
|
-
"path": "backend/shared/src/supabase/search-contracts.ts",
|
|
1503
|
-
"status": "modified",
|
|
1504
|
-
"diff": "Index: backend/shared/src/supabase/search-contracts.ts\n===================================================================\n--- backend/shared/src/supabase/search-contracts.ts\t262f7eb (parent)\n+++ backend/shared/src/supabase/search-contracts.ts\t13133e3 (commit)\n@@ -23,9 +23,8 @@\n } from 'shared/topic-interests'\n import { contractColumnsToSelect, log } from 'shared/utils'\n import { PrivateUser } from 'common/user'\n import { GROUP_SCORE_PRIOR } from 'common/feed'\n-import { MarketTierType, TierParamsType, tiers } from 'common/tier'\n import { tsToMillis } from 'common/supabase/utils'\n \n const DEFAULT_THRESHOLD = 1000\n type TokenInputType = 'CASH' | 'MANA' | 'ALL' | 'CASH_AND_MANA'\n@@ -40,9 +39,8 @@\n offset: number\n sort: 'score' | 'freshness-score'\n isPrizeMarket: boolean\n token: TokenInputType\n- marketTier: TierParamsType\n privateUser?: PrivateUser\n threshold?: number\n }) {\n const {\n@@ -51,9 +49,8 @@\n limit,\n offset,\n sort,\n isPrizeMarket,\n- marketTier,\n privateUser,\n threshold = DEFAULT_THRESHOLD,\n token,\n } = items\n@@ -89,9 +86,8 @@\n limit,\n offset,\n sort,\n isPrizeMarket,\n- marketTier,\n token,\n privateUser\n )\n }\n@@ -127,9 +123,8 @@\n contractType,\n uid: userId,\n hideStonks: true,\n isPrizeMarket,\n- marketTier,\n token,\n }),\n offset <= threshold / 2 &&\n sort === 'score' &&\n@@ -165,9 +160,8 @@\n limit: number,\n offset: number,\n sort: 'score' | 'freshness-score',\n isPrizeMarket: boolean,\n- marketTier: TierParamsType,\n token: TokenInputType,\n privateUser?: PrivateUser,\n creatorId?: string\n ) => {\n@@ -181,9 +175,8 @@\n contractType,\n uid: userId,\n hideStonks: true,\n isPrizeMarket,\n- marketTier,\n token,\n creatorId,\n }),\n privateUserBlocksSql(privateUser),\n@@ -210,9 +203,8 @@\n uid?: string\n isForYou?: boolean\n searchType: SearchTypes\n isPrizeMarket?: boolean\n- marketTier: TierParamsType\n token: TokenInputType\n groupIds?: string\n }) {\n const {\n@@ -316,9 +308,8 @@\n hideStonks?: boolean\n hideLove?: boolean\n isPrizeMarket?: boolean\n token: TokenInputType\n- marketTier: TierParamsType\n }) {\n const {\n filter,\n sort,\n@@ -327,9 +318,8 @@\n uid,\n hideStonks,\n hideLove,\n isPrizeMarket,\n- marketTier,\n token,\n } = args\n type FilterSQL = Record<string, string>\n const filterSQL: FilterSQL = {\n@@ -372,19 +362,8 @@\n : token === 'CASH_AND_MANA'\n ? `data->>'siblingContractId' is not null`\n : ''\n \n- const tierFilters = tiers\n- .map((tier: MarketTierType, index) =>\n- marketTier[index] === '1' ? `tier = '${tier}'` : ''\n- )\n- .filter(Boolean)\n-\n- const combinedTierFilter =\n- tierFilters.length > 1\n- ? `(${tierFilters.join(' OR ')})`\n- : tierFilters[0] ?? ''\n-\n return [\n where(filterSQL[filter]),\n where(stonkFilter),\n where(loveFilter, [[PROD_MANIFOLD_LOVE_GROUP_SLUG]]),\n@@ -394,9 +373,8 @@\n where(creatorFilter),\n where(deletedFilter),\n where(isPrizeMarketFilter),\n where(tokenFilter),\n- where(combinedTierFilter),\n ]\n }\n \n type SortFields = Record<\n"
|
|
1505
|
-
},
|
|
1506
|
-
{
|
|
1507
|
-
"path": "backend/shared/src/topic-interests.ts",
|
|
1508
|
-
"status": "modified",
|
|
1509
|
-
"diff": "Index: backend/shared/src/topic-interests.ts\n===================================================================\n--- backend/shared/src/topic-interests.ts\t262f7eb (parent)\n+++ backend/shared/src/topic-interests.ts\t13133e3 (commit)\n@@ -116,9 +116,8 @@\n buildArray(\n where(`contracts.close_time > now()`),\n where(`contracts.outcome_type != 'STONK'`),\n where(`contracts.outcome_type != 'BOUNTIED_QUESTION'`),\n- where(`contracts.tier != 'play'`), // filtering by liquidity takes too long\n where(`contracts.visibility = 'public'`),\n where(`contracts.unique_bettor_count > 1`)\n )\n \n"
|
|
1510
|
-
},
|
|
1511
|
-
{
|
|
1512
|
-
"path": "backend/shared/src/weekly-markets-emails.ts",
|
|
1513
|
-
"status": "modified",
|
|
1514
|
-
"diff": "Index: backend/shared/src/weekly-markets-emails.ts\n===================================================================\n--- backend/shared/src/weekly-markets-emails.ts\t262f7eb (parent)\n+++ backend/shared/src/weekly-markets-emails.ts\t13133e3 (commit)\n@@ -96,9 +96,8 @@\n limit: limit * 2,\n offset: 0,\n sort: 'score',\n isPrizeMarket: false,\n- marketTier: '00000',\n privateUser,\n token: 'ALL',\n threshold: 200,\n })\n"
|
|
1515
|
-
},
|
|
1516
|
-
{
|
|
1517
|
-
"path": "common/src/api/market-search-types.ts",
|
|
1518
|
-
"status": "modified",
|
|
1519
|
-
"diff": "Index: common/src/api/market-search-types.ts\n===================================================================\n--- common/src/api/market-search-types.ts\t262f7eb (parent)\n+++ common/src/api/market-search-types.ts\t13133e3 (commit)\n@@ -62,12 +62,8 @@\n z.literal('1'),\n z.literal('0'),\n ])\n .default('0'),\n- marketTier: z\n- .string()\n- .regex(/^[01]{5}$/)\n- .default('00000'),\n token: z\n .union([\n z.literal('MANA'),\n z.literal('CASH'),\n"
|
|
1520
|
-
},
|
|
1521
|
-
{
|
|
1522
|
-
"path": "common/src/api/market-types.ts",
|
|
1523
|
-
"status": "modified",
|
|
1524
|
-
"diff": "Index: common/src/api/market-types.ts\n===================================================================\n--- common/src/api/market-types.ts\t262f7eb (parent)\n+++ common/src/api/market-types.ts\t13133e3 (commit)\n@@ -12,9 +12,8 @@\n import { MINIMUM_BOUNTY } from 'common/economy'\n import { DOMAIN } from 'common/envs/constants'\n import { MAX_ID_LENGTH } from 'common/group'\n import { getMappedValue } from 'common/pseudo-numeric'\n-import { MarketTierType, tiers } from 'common/tier'\n import { removeUndefinedProps } from 'common/util/object'\n import { richTextToString } from 'common/util/parse'\n import { z } from 'zod'\n import { coerceBoolean, contentSchema } from './zod-types'\n@@ -58,9 +57,8 @@\n \n uniqueBettorCount: number\n lastUpdatedTime?: number\n lastBetTime?: number\n- marketTier?: MarketTierType\n sportsStartTimestamp?: string\n sportsEventId?: string\n sportsLeague?: string\n }\n@@ -120,9 +118,8 @@\n loverUserId1,\n loverUserId2,\n matchCreatorId,\n isLove,\n- marketTier,\n token,\n siblingContractId,\n } = contract\n \n@@ -171,9 +168,8 @@\n lastUpdatedTime,\n lastBetTime,\n lastCommentTime,\n ...numericValues,\n- marketTier,\n token,\n siblingContractId,\n \n // Manifold love props.\n@@ -262,18 +258,16 @@\n \n export const createBinarySchema = z.object({\n outcomeType: z.enum(['BINARY', 'STONK']),\n initialProb: z.number().min(1).max(99).optional(),\n- extraLiquidity: z.number().min(1).optional(),\n })\n \n export const createNumericSchema = z.object({\n outcomeType: z.enum(['PSEUDO_NUMERIC']),\n min: z.number().safe(),\n max: z.number().safe(),\n initialValue: z.number().safe(),\n isLogScale: z.boolean().optional(),\n- extraLiquidity: z.number().min(1).optional(),\n })\n \n export const createMultiSchema = z.object({\n outcomeType: z.enum(['MULTIPLE_CHOICE']),\n@@ -289,9 +283,8 @@\n addAnswersMode: z\n .enum(['DISABLED', 'ONLY_CREATOR', 'ANYONE'])\n .default('DISABLED'),\n shouldAnswersSumToOne: z.boolean().optional(),\n- extraLiquidity: z.number().min(1).optional(),\n })\n export const createMultiNumericSchema = z.object({\n outcomeType: z.enum(['NUMBER']),\n min: z.number().safe(),\n@@ -329,9 +322,10 @@\n groupIds: z.array(z.string().min(1).max(MAX_ID_LENGTH)).optional(),\n visibility: z.enum(VISIBILITIES).default('public').optional(),\n isTwitchContract: z.boolean().optional(),\n utcOffset: z.number().optional(),\n- marketTier: z.enum(tiers).optional(),\n+ extraLiquidity: z.number().min(1).optional(),\n+ liquidityTier: z.number().min(0),\n idempotencyKey: z.string().regex(randomStringRegex).length(10).optional(),\n sportsStartTimestamp: z.string().optional(),\n sportsEventId: z.string().optional(),\n sportsLeague: z.string().optional(),\n"
|
|
1525
|
-
},
|
|
1526
|
-
{
|
|
1527
|
-
"path": "common/src/contract.ts",
|
|
1528
|
-
"status": "modified",
|
|
1529
|
-
"diff": "Index: common/src/contract.ts\n===================================================================\n--- common/src/contract.ts\t262f7eb (parent)\n+++ common/src/contract.ts\t13133e3 (commit)\n@@ -10,9 +10,8 @@\n import { CASH_SUFFIX, ENV_CONFIG } from './envs/constants'\n import { Fees } from './fees'\n import { PollOption } from './poll-option'\n import { formatMoney, formatPercent } from './util/format'\n-import { MarketTierType } from './tier'\n \n /************************************************\n \n supabase status: columns exist for\n@@ -91,10 +90,8 @@\n isRanked?: boolean\n \n gptCommentSummary?: string\n \n- marketTier?: MarketTierType\n-\n token: ContractToken\n siblingContractId?: string\n \n /** @deprecated - no longer used */\n"
|
|
1530
|
-
},
|
|
1531
|
-
{
|
|
1532
|
-
"path": "common/src/economy.ts",
|
|
1533
|
-
"status": "modified",
|
|
1534
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\t262f7eb (parent)\n+++ common/src/economy.ts\t13133e3 (commit)\n@@ -1,36 +1,29 @@\n import { OutcomeType } from 'common/contract'\n+import { answerCostTiers, liquidityTiers } from './tier'\n \n export const DEFAULT_CASH_ANTE = 50\n-\n-export const FIXED_ANTE = 1000\n-export const BASE_ANSWER_COST = FIXED_ANTE / 10\n-export const MIN_ANSWER_COST = 25\n-const ANTES = {\n- BINARY: FIXED_ANTE,\n- MULTIPLE_CHOICE: BASE_ANSWER_COST, // Amount per answer.\n- FREE_RESPONSE: BASE_ANSWER_COST, // Amount per answer.\n- PSEUDO_NUMERIC: FIXED_ANTE * 2.5,\n- STONK: FIXED_ANTE,\n- BOUNTIED_QUESTION: 0,\n- POLL: FIXED_ANTE / 10,\n- NUMBER: FIXED_ANTE * 10,\n-}\n-\n export const MINIMUM_BOUNTY = 1000\n-export const MULTIPLE_CHOICE_MINIMUM_COST = 1000\n \n export const getAnte = (\n outcomeType: OutcomeType,\n- numAnswers: number | undefined\n+ numAnswers: number | undefined,\n+ liquidityTier: number // this could be 100, 1000, 10000, 100000 (aka tiers)\n ) => {\n- const ante = ANTES[outcomeType as keyof typeof ANTES] ?? FIXED_ANTE\n+ if (outcomeType === 'POLL') {\n+ return 10\n+ }\n \n if (outcomeType === 'MULTIPLE_CHOICE') {\n- return Math.max(ante * (numAnswers ?? 0), MULTIPLE_CHOICE_MINIMUM_COST)\n+ const tierIndex =\n+ liquidityTiers.findIndex((tier) => tier >= liquidityTier) ??\n+ liquidityTiers[liquidityTiers.length - 1]\n+ return numAnswers\n+ ? Math.max(numAnswers * answerCostTiers[tierIndex], liquidityTier)\n+ : liquidityTiers[tierIndex]\n }\n \n- return ante\n+ return liquidityTier\n }\n \n /* Sweeps bonuses */\n export const KYC_VERIFICATION_BONUS_CASH = 3\n"
|
|
1535
|
-
},
|
|
1536
|
-
{
|
|
1537
|
-
"path": "common/src/new-contract.ts",
|
|
1538
|
-
"status": "modified",
|
|
1539
|
-
"diff": "Index: common/src/new-contract.ts\n===================================================================\n--- common/src/new-contract.ts\t262f7eb (parent)\n+++ common/src/new-contract.ts\t13133e3 (commit)\n@@ -31,9 +31,8 @@\n | 'description'\n | 'closeTime'\n | 'visibility'\n | 'isTwitchContract'\n- | 'marketTier'\n | 'token'\n | 'takerAPIOrdersDisabled'\n | 'siblingContractId'\n | 'coverImageUrl'\n@@ -83,9 +82,8 @@\n addAnswersMode,\n shouldAnswersSumToOne,\n coverImageUrl,\n isAutoBounty,\n- marketTier,\n token,\n sportsStartTimestamp,\n sportsEventId,\n sportsLeague,\n@@ -160,9 +158,8 @@\n platformFee: 0,\n },\n \n isTwitchContract,\n- marketTier,\n token,\n \n sportsStartTimestamp,\n sportsEventId,\n"
|
|
1540
|
-
},
|
|
1541
|
-
{
|
|
1542
|
-
"path": "common/src/tier.ts",
|
|
1543
|
-
"status": "modified",
|
|
1544
|
-
"diff": "Index: common/src/tier.ts\n===================================================================\n--- common/src/tier.ts\t262f7eb (parent)\n+++ common/src/tier.ts\t13133e3 (commit)\n@@ -1,74 +1,17 @@\n-import {\n- CREATEABLE_NON_PREDICTIVE_OUTCOME_TYPES,\n- MarketContract,\n- OutcomeType,\n-} from './contract'\n-import { BASE_ANSWER_COST, getAnte, MIN_ANSWER_COST } from './economy'\n+export const liquidityTiers = [100, 1_000, 10_000, 100_000] as const\n+export const answerCostTiers = [25, 100, 1000, 10000] as const\n \n-// Array of tiers in order\n-export const tiers = ['play', 'plus', 'premium', 'crystal'] as const\n-\n export type BinaryDigit = '0' | '1'\n \n-export type TierParamsType =\n- `${BinaryDigit}${BinaryDigit}${BinaryDigit}${BinaryDigit}${BinaryDigit}`\n-\n-// Derive the MarketTierType from the array\n-export type MarketTierType = (typeof tiers)[number]\n-\n-export const getTieredAnswerCost = (marketTier: MarketTierType | undefined) => {\n- return marketTier\n- ? Math.max(\n- BASE_ANSWER_COST * 10 ** (tiers.indexOf(marketTier) - 1),\n- MIN_ANSWER_COST\n- )\n- : BASE_ANSWER_COST\n+export function getTierFromLiquidity(liquidity: number): number {\n+ return liquidityTiers.findIndex((tier) => tier >= liquidity)\n }\n \n-export const getTieredCost = (\n- baseCost: number,\n- tier: MarketTierType | undefined,\n- outcomeType: OutcomeType\n-) => {\n- if (CREATEABLE_NON_PREDICTIVE_OUTCOME_TYPES.includes(outcomeType)) {\n- return baseCost\n- }\n-\n- const tieredCost = tier\n- ? baseCost * 10 ** (tiers.indexOf(tier) - 1)\n- : baseCost\n-\n- if (outcomeType == 'NUMBER' && tier != 'play') {\n- return tieredCost / 10\n- }\n-\n- return tieredCost\n+export function getAnswerCostFromLiquidity(\n+ liquidity: number,\n+ numAnswers: number\n+): number {\n+ return answerCostTiers[\n+ liquidityTiers.findIndex((tier) => tier >= liquidity / numAnswers)\n+ ]\n }\n-\n-// For multi & mumeric markets this can only get the current tier, not back-calculate\n-export function getTierFromLiquidity(\n- contract: MarketContract,\n- liquidity: number\n-): MarketTierType {\n- const { outcomeType } = contract\n-\n- let numAnswers = undefined\n- if ('answers' in contract) {\n- numAnswers = contract.answers.length\n- }\n-\n- const ante = getAnte(outcomeType, numAnswers)\n-\n- // Iterate through the tiers from highest to lowest\n- for (let i = tiers.length - 1; i >= 0; i--) {\n- const tier = tiers[i]\n- const tierLiquidity = getTieredCost(ante, tier, outcomeType)\n-\n- // Return the first tier where the liquidity is greater or equal to the tier's requirement\n- if (liquidity >= tierLiquidity) {\n- return tier as MarketTierType\n- }\n- }\n- // Default to the lowest tier if none of the conditions are met\n- return 'play'\n-}\n"
|
|
1545
|
-
},
|
|
1546
|
-
{
|
|
1547
|
-
"path": "web/components/answers/create-answer-panel.tsx",
|
|
1548
|
-
"status": "modified",
|
|
1549
|
-
"diff": "Index: web/components/answers/create-answer-panel.tsx\n===================================================================\n--- web/components/answers/create-answer-panel.tsx\t262f7eb (parent)\n+++ web/components/answers/create-answer-panel.tsx\t13133e3 (commit)\n@@ -1,9 +1,9 @@\n import { ChevronDownIcon, XCircleIcon } from '@heroicons/react/solid'\n import clsx from 'clsx'\n import { MultiSort } from 'common/answer'\n import { MultiContract, SORTS } from 'common/contract'\n-import { getTieredAnswerCost, getTierFromLiquidity } from 'common/tier'\n+import { getAnswerCostFromLiquidity } from 'common/tier'\n import { useLayoutEffect, useRef, useState } from 'react'\n import { FaSearch, FaSearchPlus } from 'react-icons/fa'\n import { api } from 'web/lib/api/api'\n import { withTracking } from 'web/lib/service/analytics'\n@@ -142,14 +142,11 @@\n >\n <span className=\"font-semibold\">Add</span>\n <span className=\"text-ink-200 dark:text-ink-800 ml-1\">\n <MoneyDisplay\n- amount={getTieredAnswerCost(\n- contract.marketTier ??\n- getTierFromLiquidity(\n- contract,\n- contract.totalLiquidity\n- )\n+ amount={getAnswerCostFromLiquidity(\n+ contract.totalLiquidity,\n+ contract.answers.length\n )}\n isCashContract={isCashContract}\n />\n </span>\n"
|
|
1550
|
-
},
|
|
1551
|
-
{
|
|
1552
|
-
"path": "web/components/bet/bet-panel.tsx",
|
|
1553
|
-
"status": "modified",
|
|
1554
|
-
"diff": "Index: web/components/bet/bet-panel.tsx\n===================================================================\n--- web/components/bet/bet-panel.tsx\t262f7eb (parent)\n+++ web/components/bet/bet-panel.tsx\t13133e3 (commit)\n@@ -236,11 +236,9 @@\n } = props\n \n const user = useUser()\n const privateUser = usePrivateUser()\n- const marketTier =\n- contract.marketTier ??\n- getTierFromLiquidity(contract, contract.totalLiquidity)\n+ const liquidityTier = getTierFromLiquidity(contract.totalLiquidity)\n \n const { unfilledBets: allUnfilledBets, balanceByUserId } =\n useUnfilledBetsAndBalanceByUserId(\n contract.id,\n@@ -264,11 +262,11 @@\n : undefined\n const isCashContract = contract.token === 'CASH'\n \n const quickAddButtonSize =\n- marketTier === 'play' ||\n+ liquidityTier === 0 ||\n (contract.mechanism === 'cpmm-multi-1' &&\n- contract.marketTier === 'plus' &&\n+ liquidityTier === 1 &&\n !contract.shouldAnswersSumToOne)\n ? 'small'\n : undefined\n \n"
|
|
1555
|
-
},
|
|
1556
|
-
{
|
|
1557
|
-
"path": "web/components/contract/upgrade-tier-button.tsx",
|
|
1558
|
-
"status": "deleted",
|
|
1559
|
-
"diff": "Index: web/components/contract/upgrade-tier-button.tsx\n===================================================================\n--- web/components/contract/upgrade-tier-button.tsx\t262f7eb (parent)\n+++ web/components/contract/upgrade-tier-button.tsx\t13133e3 (commit)\n@@ -1,260 +0,0 @@\n-import clsx from 'clsx'\n-import { CreateableOutcomeType, MarketContract } from 'common/contract'\n-import {\n- MarketTierType,\n- getTierFromLiquidity,\n- tiers,\n- getTieredCost,\n-} from 'common/tier'\n-import { ReactNode, useState } from 'react'\n-import { CrystalTier } from 'web/public/custom-components/tiers'\n-import { Button } from '../buttons/button'\n-import { Col } from '../layout/col'\n-import { Modal } from '../layout/modal'\n-import { Title } from '../widgets/title'\n-import { AddLiquidityControl } from './liquidity-modal'\n-import { getAnte } from 'common/economy'\n-import { TokenNumber } from '../widgets/token-number'\n-import { TierIcon, getPresentedTierName } from '../tiers/liquidity-tooltip'\n-import { api } from 'web/lib/api/api'\n-import { useUser } from 'web/hooks/use-user'\n-import { ENV_CONFIG } from 'common/envs/constants'\n-import { AddFundsModal } from '../add-funds-modal'\n-import { track } from 'web/lib/service/analytics'\n-import toast from 'react-hot-toast'\n-\n-export function UpgradeTierButton(props: {\n- contract: MarketContract\n- className?: string\n-}) {\n- const { contract, className } = props\n- const [open, setOpen] = useState(false)\n-\n- const disabled =\n- contract.isResolved ||\n- (contract.closeTime ?? Infinity) < Date.now() ||\n- contract.visibility !== 'public'\n-\n- if (disabled) return <></>\n-\n- const alreadyHighestTier =\n- contract.marketTier === 'crystal' ||\n- getTierFromLiquidity(contract, contract.totalLiquidity) === 'crystal'\n-\n- return (\n- <Button\n- onClick={() => setOpen(true)}\n- size=\"lg\"\n- color=\"indigo-outline\"\n- className={clsx(className, 'group')}\n- >\n- <CrystalTier className=\"mr-2 h-5 w-5\" />\n- {alreadyHighestTier ? 'Add liquidity' : 'Upgrade'}\n- <AddLiquidityDialogue\n- contract={contract}\n- isOpen={open}\n- setOpen={setOpen}\n- />\n- </Button>\n- )\n-}\n-\n-export function AddLiquidityDialogue(props: {\n- contract: MarketContract\n- isOpen: boolean\n- setOpen: (open: boolean) => void\n-}) {\n- const { contract, isOpen, setOpen } = props\n- const { outcomeType } = contract\n-\n- let numAnswers = undefined\n- if ('answers' in contract) {\n- numAnswers = contract.answers.length\n- }\n- const ante = getAnte(outcomeType, numAnswers)\n-\n- const currentTier =\n- contract.marketTier ??\n- getTierFromLiquidity(contract, contract.totalLiquidity)\n- const currentTierIndex = tiers.indexOf(currentTier)\n- const alreadyHighestTier = currentTier === 'crystal'\n-\n- const [amount, setAmount] = useState<number | undefined>(\n- alreadyHighestTier\n- ? 1000\n- : getTieredCost(ante, tiers[currentTierIndex + 1], outcomeType) -\n- contract.totalLiquidity\n- )\n-\n- return (\n- <Modal open={isOpen} setOpen={setOpen} size=\"sm\">\n- <Col className=\"bg-canvas-0 gap-2.5 rounded p-4 pb-8 sm:gap-4\">\n- <Title className=\"!mb-2\">\n- {alreadyHighestTier ? 'Add liquidity' : 'Upgrade Tier'}\n- </Title>\n- {alreadyHighestTier ? (\n- <AddLiquidityControl\n- contract={contract}\n- amount={amount}\n- setAmount={setAmount}\n- />\n- ) : (\n- <UpgradeTierContent\n- currentTierIndex={currentTierIndex}\n- contract={contract}\n- ante={ante}\n- amount={amount}\n- setAmount={setAmount}\n- setOpen={setOpen}\n- />\n- )}\n- </Col>\n- </Modal>\n- )\n-}\n-\n-function UpgradeTierContent(props: {\n- currentTierIndex: number\n- contract: MarketContract\n- ante: number\n- amount: number | undefined\n- setAmount: (amount: number | undefined) => void\n- setOpen: (open: boolean) => void\n-}) {\n- const { currentTierIndex, contract, ante, amount, setAmount, setOpen } = props\n- const { outcomeType, id: contractId, slug } = contract\n- const totalOptions = tiers.length - 1 - currentTierIndex\n-\n- const [error, setError] = useState<string | undefined>(undefined)\n- const [isLoading, setIsLoading] = useState(false)\n-\n- const submit = async () => {\n- if (!amount) return\n-\n- setIsLoading(true)\n-\n- try {\n- await api('market/:contractId/add-liquidity', {\n- amount,\n- contractId,\n- })\n- setError(undefined)\n- track('upgrade market', { amount, contractId, slug })\n- setOpen(false)\n- toast('Your market has been upgraded!', {\n- icon: '🚀',\n- })\n- } catch (e) {\n- setError('Server error')\n- } finally {\n- setIsLoading(false)\n- }\n- }\n-\n- const user = useUser()\n- const [fundsModalOpen, setFundsModalOpen] = useState(false)\n-\n- const notEnoughFunds = !!user && !!amount && user.balance < amount\n-\n- return (\n- <>\n- <div\n- className={clsx(\n- 'w-full gap-2',\n- totalOptions > 2 ? 'grid grid-cols-2' : 'flex flex-row'\n- )}\n- >\n- {tiers.slice(currentTierIndex + 1).map((tier) => (\n- <UpgradeTier\n- key={tier}\n- contract={contract}\n- baseCost={ante}\n- icon={<TierIcon tier={tier} />}\n- tier={tier}\n- outcomeType={outcomeType}\n- currentAmount={amount}\n- onClick={(upgradeCost) => setAmount(upgradeCost)}\n- />\n- ))}\n- </div>\n- <Button\n- disabled={isLoading || !!error || !amount || notEnoughFunds}\n- onClick={submit}\n- loading={isLoading}\n- >\n- Upgrade{' '}\n- {amount &&\n- `to ${getTierFromLiquidity(\n- contract,\n- amount + contract.totalLiquidity\n- )}`}\n- </Button>\n- {notEnoughFunds && (\n- <div className=\"mb-2 mr-auto mt-2 self-center whitespace-nowrap text-xs font-medium tracking-wide\">\n- <span className=\"text-scarlet-500 mr-2\">Insufficient balance</span>\n- <Button\n- size=\"xs\"\n- color=\"green\"\n- onClick={() => setFundsModalOpen(true)}\n- >\n- Get {ENV_CONFIG.moneyMoniker}\n- </Button>\n- <AddFundsModal open={fundsModalOpen} setOpen={setFundsModalOpen} />\n- </div>\n- )}\n- </>\n- )\n-}\n-\n-function UpgradeTier(props: {\n- contract: MarketContract\n- baseCost: number\n- icon: ReactNode\n- tier: MarketTierType\n- outcomeType: CreateableOutcomeType\n- onClick: (upgradeCost: number) => void\n- currentAmount?: number\n-}) {\n- const {\n- contract,\n- baseCost,\n- icon,\n- tier,\n- outcomeType,\n- onClick,\n- currentAmount,\n- } = props\n-\n- const additionalAmount =\n- getTieredCost(baseCost, tier, outcomeType) - contract.totalLiquidity\n- return (\n- <Col\n- className={clsx(\n- currentAmount == additionalAmount\n- ? tier == 'play'\n- ? 'outline-ink-500'\n- : tier == 'plus'\n- ? 'outline-blue-500'\n- : tier == 'premium'\n- ? 'outline-purple-400'\n- : 'outline-pink-500'\n- : tier == 'play'\n- ? 'hover:outline-ink-500/50 opacity-50 outline-transparent'\n- : tier == 'plus'\n- ? 'opacity-50 outline-transparent hover:outline-purple-500/50'\n- : tier == 'premium'\n- ? 'opacity-50 outline-transparent hover:outline-fuchsia-400/50'\n- : 'opacity-50 outline-transparent hover:outline-pink-500/50',\n- 'bg-canvas-50 w-full cursor-pointer select-none items-center rounded px-4 py-2 outline transition-colors'\n- )}\n- onClick={() => onClick(additionalAmount)}\n- >\n- <div className=\"text-4xl sm:text-5xl\">{icon}</div>\n- <div className=\"text-ink-600\">{getPresentedTierName(tier)}</div>\n- <TokenNumber\n- className=\"text-xl font-semibold\"\n- amount={additionalAmount}\n- />\n- </Col>\n- )\n-}\n"
|
|
1560
|
-
},
|
|
1561
|
-
{
|
|
1562
|
-
"path": "web/components/new-contract/contract-params-form.tsx",
|
|
1563
|
-
"status": "modified",
|
|
1564
|
-
"diff": "Index: web/components/new-contract/contract-params-form.tsx\n===================================================================\n--- web/components/new-contract/contract-params-form.tsx\t262f7eb (parent)\n+++ web/components/new-contract/contract-params-form.tsx\t13133e3 (commit)\n@@ -5,9 +5,8 @@\n \n import {\n add_answers_mode,\n Contract,\n- CREATEABLE_NON_PREDICTIVE_OUTCOME_TYPES,\n CreateableOutcomeType,\n MAX_DESCRIPTION_LENGTH,\n MAX_QUESTION_LENGTH,\n MULTI_NUMERIC_BUCKETS_MAX,\n@@ -65,29 +64,25 @@\n import { PseudoNumericRangeSection } from 'web/components/new-contract/pseudo-numeric-range-section'\n import { SimilarContractsSection } from 'web/components/new-contract/similar-contracts-section'\n import { MultiNumericRangeSection } from 'web/components/new-contract/multi-numeric-range-section'\n import { getMultiNumericAnswerBucketRangeNames } from 'common/multi-numeric'\n-import { getTieredCost, MarketTierType } from 'common/tier'\n import { randomString } from 'common/util/random'\n import { formatWithToken } from 'common/util/format'\n import { BiUndo } from 'react-icons/bi'\n+import { liquidityTiers } from 'common/tier'\n \n export function ContractParamsForm(props: {\n creator: User\n outcomeType: CreateableOutcomeType\n params?: Partial<NewQuestionParams>\n }) {\n const { creator, params, outcomeType } = props\n- const DEFAULT_TIER = CREATEABLE_NON_PREDICTIVE_OUTCOME_TYPES.includes(\n- outcomeType\n+\n+ const [liquidityTier, setLiquidityTier] = usePersistentLocalState<number>(\n+ liquidityTiers[0],\n+ 'liquidity-tier'\n )\n- ? undefined\n- : 'play'\n \n- const [marketTier, setMarketTier] = usePersistentLocalState<\n- MarketTierType | undefined\n- >(DEFAULT_TIER, 'market-tier')\n-\n const paramsKey =\n (params?.q ?? '') +\n (params?.groupSlugs?.join('') ?? '') +\n (params?.groupIds?.join('') ?? '')\n@@ -232,10 +227,8 @@\n const dismissedSimilarContractsKey = 'dismissed-similar-contracts'\n const [dismissedSimilarContractTitles, setDismissedSimilarContractTitles] =\n usePersistentInMemoryState<string[]>([], dismissedSimilarContractsKey)\n \n- const ante = getAnte(outcomeType, numAnswers)\n-\n const timeInMs = params?.closeTime ? Number(params.closeTime) : undefined\n const initDate = (timeInMs ? dayjs(timeInMs) : dayjs().add(7, 'day')).format(\n 'YYYY-MM-DD'\n )\n@@ -271,13 +264,8 @@\n )\n \n const { balance } = creator\n \n- const anteOrBounty =\n- outcomeType === 'BOUNTIED_QUESTION'\n- ? bountyAmount ?? defaultBountyAmount\n- : ante\n-\n const closeTime = closeDate\n ? dayjs(`${closeDate}T${closeHoursMinutes}`).valueOf()\n : undefined\n \n@@ -326,20 +314,17 @@\n // closeTime must be in the future\n !shouldHaveCloseDate || (closeTime ?? Infinity) > Date.now()\n \n const isValidTopics = selectedGroups.length <= MAX_GROUPS_PER_MARKET\n-\n+ const ante = getAnte(outcomeType, numAnswers, liquidityTier)\n const numberOfBuckets = getMultiNumericAnswerBucketRangeNames(\n min ?? 0,\n max ?? 0,\n precision && precision > 0 ? precision : 1\n ).length\n const isValid =\n isValidQuestion &&\n- ante !== undefined &&\n- ante !== null &&\n- // Disabled while it doesn't account for play tier (ante is 10x actual play cost)\n- // ante <= balance &&\n+ ante <= balance &&\n isValidDate &&\n isValidTopics &&\n (outcomeType !== 'PSEUDO_NUMERIC' ||\n (min !== undefined &&\n@@ -452,9 +437,9 @@\n totalBounty: bountyAmount,\n isAutoBounty:\n outcomeType === 'BOUNTIED_QUESTION' ? isAutoBounty : undefined,\n precision,\n- marketTier,\n+ liquidityTier,\n idempotencyKey,\n sportsStartTimestamp: params?.sportsStartTimestamp,\n sportsEventId: params?.sportsEventId,\n sportsLeague: params?.sportsLeague,\n@@ -748,12 +733,12 @@\n />\n </Row>\n <CostSection\n balance={balance}\n- baseCost={anteOrBounty}\n+ numAnswers={numAnswers}\n outcomeType={outcomeType}\n- marketTier={marketTier}\n- setMarketTier={setMarketTier}\n+ liquidityTier={liquidityTier}\n+ setLiquidityTier={setLiquidityTier}\n />\n {errorText && <span className={'text-error'}>{errorText}</span>}\n <Button\n className=\"w-full\"\n@@ -772,9 +757,9 @@\n }}\n >\n {submitState === 'EDITING'\n ? `Create question for ${formatWithToken({\n- amount: getTieredCost(anteOrBounty, marketTier, outcomeType),\n+ amount: ante,\n short: true,\n token: 'M$',\n })}`\n : submitState === 'LOADING'\n"
|
|
1565
|
-
},
|
|
1566
|
-
{
|
|
1567
|
-
"path": "web/components/new-contract/cost-section.tsx",
|
|
1568
|
-
"status": "modified",
|
|
1569
|
-
"diff": "Index: web/components/new-contract/cost-section.tsx\n===================================================================\n--- web/components/new-contract/cost-section.tsx\t262f7eb (parent)\n+++ web/components/new-contract/cost-section.tsx\t13133e3 (commit)\n@@ -1,70 +1,32 @@\n import {\n CREATEABLE_NON_PREDICTIVE_OUTCOME_TYPES,\n CreateableOutcomeType,\n } from 'common/contract'\n-import { ReactNode, useState } from 'react'\n+import { useState } from 'react'\n import { Col } from 'web/components/layout/col'\n \n import clsx from 'clsx'\n import { ENV_CONFIG } from 'common/envs/constants'\n import { AddFundsModal } from 'web/components/add-funds-modal'\n import { Button } from 'web/components/buttons/button'\n-import {\n- CrystalTier,\n- PlayTier,\n- PlusTier,\n- PremiumTier,\n-} from 'web/public/custom-components/tiers'\n+\n import { TokenNumber } from '../widgets/token-number'\n-import { getTieredCost, MarketTierType } from 'common/tier'\n-import { getPresentedTierName } from '../tiers/liquidity-tooltip'\n-import { ManaCoin } from 'web/public/custom-components/manaCoin'\n-import { getContractTypeFromValue } from './create-contract-types'\n-import { InfoTooltip } from '../widgets/info-tooltip'\n-import { capitalize } from 'lodash'\n+import { liquidityTiers } from 'common/tier'\n+import { getAnte } from 'common/economy'\n \n-type TIER_TYPE = { name: MarketTierType; icon: ReactNode }\n-\n-export const TIERS: TIER_TYPE[] = [\n- { name: 'play', icon: <PlayTier /> },\n- {\n- name: 'plus',\n- icon: <PlusTier />,\n- },\n- {\n- name: 'premium',\n- icon: <PremiumTier />,\n- },\n- {\n- name: 'crystal',\n- icon: <CrystalTier />,\n- },\n-] as const\n-\n-const TIER_EXCLUSIONS: Partial<\n- Record<CreateableOutcomeType, MarketTierType[]>\n-> = {\n- NUMBER: ['play'],\n-}\n-\n-const isTierDisabled = (\n- outcomeType: CreateableOutcomeType,\n- tier: MarketTierType\n-) => {\n- return TIER_EXCLUSIONS[outcomeType]?.includes(tier)\n-}\n-\n export const CostSection = (props: {\n balance: number\n outcomeType: CreateableOutcomeType\n- baseCost: number\n- marketTier: MarketTierType | undefined\n- setMarketTier: (tier: MarketTierType) => void\n+ liquidityTier: number\n+ setLiquidityTier: (tier: number) => void\n+ numAnswers: number | undefined\n }) => {\n- const { balance, outcomeType, baseCost, marketTier, setMarketTier } = props\n+ const { balance, outcomeType, liquidityTier, setLiquidityTier, numAnswers } =\n+ props\n+ const ante = getAnte(outcomeType, numAnswers, liquidityTier)\n+\n const [fundsModalOpen, setFundsModalOpen] = useState(false)\n- const currentCost = getTieredCost(baseCost, marketTier, outcomeType)\n return (\n <Col className=\"items-start px-1\">\n <label className=\"mb-1 gap-2\">\n <span>\n@@ -74,15 +36,13 @@\n </span>\n </label>\n \n <PriceSection\n- baseCost={baseCost}\n- outcomeType={outcomeType}\n- currentTier={marketTier}\n- setMarketTier={setMarketTier}\n+ liquidityTier={liquidityTier}\n+ setLiquidityTier={setLiquidityTier}\n />\n \n- {currentCost > balance && (\n+ {ante > balance && (\n <div className=\"mb-2 mr-auto mt-2 self-center whitespace-nowrap text-xs font-medium tracking-wide\">\n <span className=\"text-scarlet-500 mr-2\">Insufficient balance</span>\n <Button\n size=\"xs\"\n@@ -98,120 +58,73 @@\n )\n }\n \n function PriceSection(props: {\n- baseCost: number\n- outcomeType: CreateableOutcomeType\n- currentTier: MarketTierType | undefined\n- setMarketTier: (tier: MarketTierType) => void\n+ liquidityTier: number\n+ setLiquidityTier: (tier: number) => void\n }) {\n- const { baseCost, outcomeType, currentTier, setMarketTier } = props\n+ const { liquidityTier, setLiquidityTier } = props\n \n- if (!currentTier) {\n- return <TokenNumber amount={getTieredCost(baseCost, 'play', outcomeType)} />\n- }\n return (\n <Col className=\"w-full gap-2\">\n <div className=\"text-ink-600 text-sm\">\n More liquidity attracts more traders but has a higher cost.\n </div>\n <div className=\"grid w-full grid-cols-2 gap-2 sm:grid-cols-4\">\n- {TIERS.map((tier: TIER_TYPE) => {\n- return (\n- <Tier\n- key={tier.name}\n- baseCost={baseCost}\n- tier={tier.name}\n- outcomeType={outcomeType}\n- currentTier={currentTier}\n- setMarketTier={setMarketTier}\n- isTierDisabled={isTierDisabled(outcomeType, tier.name)}\n- />\n- )\n- })}\n+ {liquidityTiers.map((tier) => (\n+ <Tier\n+ key={tier}\n+ currentTier={liquidityTier}\n+ liquidityTier={tier}\n+ setLiquidityTier={setLiquidityTier}\n+ />\n+ ))}\n </div>\n </Col>\n )\n }\n \n function Tier(props: {\n- baseCost: number\n- tier: MarketTierType\n- outcomeType: CreateableOutcomeType\n- currentTier: MarketTierType\n- setMarketTier: (tier: MarketTierType) => void\n+ currentTier: number\n+ liquidityTier: number\n+ setLiquidityTier: (tier: number) => void\n isTierDisabled?: boolean\n }) {\n- const {\n- baseCost,\n- tier,\n- outcomeType,\n- currentTier,\n- setMarketTier,\n- isTierDisabled,\n- } = props\n+ const { currentTier, liquidityTier, setLiquidityTier, isTierDisabled } = props\n \n- const questionType = capitalize(getContractTypeFromValue(outcomeType, 'name'))\n- const tierName = getPresentedTierName(tier)\n+ const tierIndex = liquidityTiers.findIndex((tier) => tier === liquidityTier)\n \n- if (isTierDisabled) {\n- return (\n- <div\n- className={clsx(\n- 'bg-canvas-50 w-full select-none items-baseline rounded py-2 pl-2 pr-4 transition-colors',\n- 'flex flex-row justify-start gap-3 sm:flex-col sm:items-center sm:justify-between sm:gap-0'\n- )}\n- >\n- <div className=\"text-ink-500 flex flex-col items-center gap-1 text-sm font-bold sm:flex-row sm:items-start\">\n- <div>Disabled</div>\n- <InfoTooltip\n- text={`The ${questionType} question type does not work with the ${tierName} tier because it requires more liquidity.`}\n- />\n- </div>\n- <Col className=\"sm:items-center\">\n- <div className=\"text-ink-400\">{tierName}</div>\n- <div\n- className=\"text-xl opacity-50\"\n- style={{ filter: 'saturate(0%)' }}\n- >\n- <ManaCoin />\n- </div>\n- </Col>\n- </div>\n- )\n- }\n-\n return (\n <div\n className={clsx(\n- currentTier == tier\n- ? tier == 'play'\n+ currentTier == liquidityTier\n+ ? tierIndex == 0\n ? 'outline-ink-500'\n- : tier == 'plus'\n+ : tierIndex == 1\n ? 'outline-blue-500'\n- : tier == 'premium'\n+ : tierIndex == 2\n ? 'outline-purple-400'\n : 'outline-pink-500'\n- : tier == 'play'\n+ : tierIndex == 0\n ? 'hover:outline-ink-500/50 opacity-50 outline-transparent'\n- : tier == 'plus'\n+ : tierIndex == 1\n ? 'opacity-50 outline-transparent hover:outline-purple-500/50'\n- : tier == 'premium'\n+ : tierIndex == 2\n ? 'opacity-50 outline-transparent hover:outline-fuchsia-400/50'\n : 'opacity-50 outline-transparent hover:outline-pink-500/50',\n 'bg-canvas-50 cursor-pointer ',\n 'flex w-full select-none flex-row items-center gap-2 rounded px-4 py-2 outline transition-colors sm:flex-col sm:gap-0'\n )}\n onClick={() => {\n if (!isTierDisabled) {\n- setMarketTier(tier)\n+ setLiquidityTier(liquidityTier)\n }\n }}\n >\n <Col className=\"sm:items-center\">\n <TokenNumber\n className=\"text-xl font-semibold\"\n- amount={getTieredCost(baseCost, tier, outcomeType)}\n+ amount={liquidityTier}\n numberType=\"short\"\n />\n </Col>\n </div>\n"
|
|
1570
|
-
},
|
|
1571
|
-
{
|
|
1572
|
-
"path": "web/components/search.tsx",
|
|
1573
|
-
"status": "modified",
|
|
1574
|
-
"diff": "Index: web/components/search.tsx\n===================================================================\n--- web/components/search.tsx\t262f7eb (parent)\n+++ web/components/search.tsx\t13133e3 (commit)\n@@ -29,9 +29,9 @@\n import { ContractFilters } from './search/contract-filters'\n import { UserResults } from './search/user-results'\n import { BrowseTopicPills } from './topics/browse-topic-pills'\n import { LoadMoreUntilNotVisible } from 'web/components/widgets/visibility-observer'\n-import { BinaryDigit, TierParamsType } from 'common/tier'\n+import { BinaryDigit } from 'common/tier'\n import { useIsMobile } from 'web/hooks/use-is-mobile'\n import { Spacer } from './layout/spacer'\n import { useSweepstakes } from './sweepstakes-provider'\n import { FilterPill } from './search/filter-pills'\n@@ -140,9 +140,8 @@\n [CONTRACT_TYPE_KEY]: ContractTypeType\n [SEARCH_TYPE_KEY]: SearchType\n [PRIZE_MARKET_KEY]: BinaryDigit\n [FOR_YOU_KEY]: BinaryDigit\n- [MARKET_TIER_KEY]: TierParamsType\n [TOPIC_FILTER_KEY]: string\n [SWEEPIES_KEY]: '0' | '1' | '2'\n [GROUP_IDS_KEY]: string\n }\n@@ -599,9 +598,8 @@\n f: filter,\n ct: contractType,\n p: isPrizeMarketString,\n fy: forYou,\n- mt: marketTier,\n tf: topicSlug,\n sw: sweepState,\n gids,\n } = searchParams\n@@ -631,9 +629,8 @@\n limit: CONTRACTS_PER_SEARCH_PAGE,\n topicSlug: topicSlug !== '' ? topicSlug : undefined,\n creatorId: additionalFilter?.creatorId,\n isPrizeMarket: isPrizeMarketString,\n- marketTier,\n forYou,\n token:\n sweepState === '2'\n ? 'ALL'\n@@ -742,9 +739,8 @@\n defaultPrizeMarket?: '1' | '0'\n defaultSweepies?: '2' | '1' | '0'\n defaultForYou?: '1' | '0'\n useUrlParams?: boolean\n- defaultMarketTier?: TierParamsType\n defaultTopicFilter?: string\n }) => {\n const {\n persistPrefix,\n@@ -754,9 +750,8 @@\n defaultSearchType,\n useUrlParams,\n defaultPrizeMarket,\n defaultForYou,\n- defaultMarketTier,\n defaultTopicFilter,\n defaultSweepies,\n } = props\n \n@@ -767,9 +762,8 @@\n [CONTRACT_TYPE_KEY]: defaultContractType ?? 'ALL',\n [SEARCH_TYPE_KEY]: defaultSearchType,\n [PRIZE_MARKET_KEY]: defaultPrizeMarket ?? '0',\n [FOR_YOU_KEY]: defaultForYou ?? '0',\n- [MARKET_TIER_KEY]: defaultMarketTier ?? DEFAULT_TIER,\n [TOPIC_FILTER_KEY]: defaultTopicFilter ?? '',\n [SWEEPIES_KEY]: defaultSweepies ?? '0',\n [GROUP_IDS_KEY]: '',\n }\n"
|
|
1575
|
-
},
|
|
1576
|
-
{
|
|
1577
|
-
"path": "web/components/search/contract-filters.tsx",
|
|
1578
|
-
"status": "modified",
|
|
1579
|
-
"diff": "Index: web/components/search/contract-filters.tsx\n===================================================================\n--- web/components/search/contract-filters.tsx\t262f7eb (parent)\n+++ web/components/search/contract-filters.tsx\t13133e3 (commit)\n@@ -2,20 +2,15 @@\n import { track } from 'web/lib/service/analytics'\n import { Col } from '../layout/col'\n import { getLabelFromValue } from './search-dropdown-helpers'\n \n-import { MarketTierType, TierParamsType, tiers } from 'common/tier'\n import { useState } from 'react'\n import { FaSortAmountDownAlt } from 'react-icons/fa'\n import { FaFileContract, FaFilter, FaSliders } from 'react-icons/fa6'\n import { IconButton } from 'web/components/buttons/button'\n import { Carousel } from 'web/components/widgets/carousel'\n import { SweepiesCoin } from 'web/public/custom-components/sweepiesCoin'\n-import {\n- CrystalTier,\n- PlusTier,\n- PremiumTier,\n-} from 'web/public/custom-components/tiers'\n+\n import { MODAL_CLASS, Modal } from '../layout/modal'\n import { Row } from '../layout/row'\n import {\n BOUNTY_MARKET_SORTS,\n@@ -27,9 +22,8 @@\n DEFAULT_FILTER,\n DEFAULT_FILTERS,\n DEFAULT_SORT,\n DEFAULT_SORTS,\n- DEFAULT_TIER,\n FILTERS,\n FOR_YOU_KEY,\n Filter,\n GROUP_IDS_KEY,\n@@ -48,9 +42,8 @@\n AdditionalFilterPill,\n FilterDropdownPill,\n FilterPill,\n minimalistIndigoSelectedClass,\n- TierDropdownPill,\n unselectedClass,\n } from './filter-pills'\n import { useUser } from 'web/hooks/use-user'\n \n@@ -63,15 +56,9 @@\n }) {\n const { className, params, updateParams, hideSweepsToggle, topicSlug } = props\n const user = useUser()\n \n- const {\n- s: sort,\n- f: filter,\n- ct: contractType,\n- mt: currentTiers,\n- sw: isSweepiesString,\n- } = params\n+ const { s: sort, f: filter, ct: contractType, sw: isSweepiesString } = params\n const isSweeps = isSweepiesString === '1'\n const { setPrefersPlay } = useSweepstakes()\n \n const selectFilter = (selection: Filter) => {\n@@ -135,17 +122,8 @@\n contractType !== DEFAULT_CONTRACT_TYPE\n \n const forYou = params[FOR_YOU_KEY] === '1' && !params[GROUP_IDS_KEY]\n \n- const toggleTier = (tier: MarketTierType) => {\n- const tierIndex = tiers.indexOf(tier)\n- if (tierIndex >= 0 && tierIndex < currentTiers.length) {\n- const tiersArray = currentTiers.split('')\n- tiersArray[tierIndex] = tiersArray[tierIndex] === '0' ? '1' : '0'\n- updateParams({ mt: tiersArray.join('') as TierParamsType })\n- }\n- }\n-\n return (\n <Col className={clsx('mb-1 mt-2 items-stretch gap-1 ', className)}>\n <Carousel fadeEdges labelsParentClassName=\"gap-1 items-center\">\n {isSweeps && !hideSweepsToggle && (\n@@ -247,32 +225,8 @@\n >\n For you\n </FilterPill>\n )}\n- {!hideFilter && currentTiers !== DEFAULT_TIER && (\n- <AdditionalFilterPill\n- type=\"filter\"\n- onXClick={() =>\n- updateParams({ mt: DEFAULT_TIER as TierParamsType })\n- }\n- >\n- <Row className=\"items-center py-[3px]\">\n- {currentTiers.split('').map((tier, index) => {\n- if (tier === '1') {\n- if (tiers[index] == 'plus') {\n- return <PlusTier key={index} />\n- }\n- if (tiers[index] == 'premium') {\n- return <PremiumTier key={index} />\n- }\n- if (tiers[index] == 'crystal') {\n- return <CrystalTier key={index} />\n- }\n- }\n- })}\n- </Row>\n- </AdditionalFilterPill>\n- )}\n {nonDefaultSort && (\n <AdditionalFilterPill\n type=\"sort\"\n onXClick={() => selectSort(DEFAULT_SORT)}\n@@ -330,9 +284,8 @@\n selectFilter={selectFilter}\n selectSort={selectSort}\n selectContractType={selectContractType}\n toggleSweepies={toggleSweepies}\n- toggleTier={toggleTier}\n hideFilter={hideFilter}\n />\n </Col>\n )\n@@ -345,9 +298,8 @@\n selectFilter: (selection: Filter) => void\n selectSort: (selection: Sort) => void\n selectContractType: (selection: ContractTypeType) => void\n toggleSweepies: () => void\n- toggleTier: (tier: MarketTierType) => void\n hideFilter: boolean\n }) {\n const {\n open,\n@@ -356,18 +308,11 @@\n selectFilter,\n selectContractType,\n selectSort,\n toggleSweepies,\n- toggleTier,\n hideFilter,\n } = props\n- const {\n- s: sort,\n- f: filter,\n- ct: contractType,\n- sw: isSweepiesString,\n- mt: currentTiers,\n- } = params\n+ const { s: sort, f: filter, ct: contractType, sw: isSweepiesString } = params\n \n const sortItems =\n contractType == 'BOUNTIED_QUESTION'\n ? BOUNTY_MARKET_SORTS\n@@ -398,12 +343,8 @@\n />\n Sweepstakes\n </Row>\n </FilterPill>\n- <TierDropdownPill\n- toggleTier={toggleTier}\n- currentTiers={currentTiers}\n- />\n {!hideFilter &&\n FILTERS.map(({ label: filterLabel, value: filterValue }) => (\n <FilterPill\n key={filterValue}\n"
|
|
1580
|
-
},
|
|
1581
|
-
{
|
|
1582
|
-
"path": "web/components/search/filter-pills.tsx",
|
|
1583
|
-
"status": "modified",
|
|
1584
|
-
"diff": "Index: web/components/search/filter-pills.tsx\n===================================================================\n--- web/components/search/filter-pills.tsx\t262f7eb (parent)\n+++ web/components/search/filter-pills.tsx\t13133e3 (commit)\n@@ -1,15 +1,9 @@\n import { ChevronDownIcon, XCircleIcon } from '@heroicons/react/solid'\n import clsx from 'clsx'\n-import { MarketTierType, TierParamsType, tiers } from 'common/tier'\n import { ReactNode } from 'react'\n-import {\n- CrystalTier,\n- PlusTier,\n- PremiumTier,\n-} from 'web/public/custom-components/tiers'\n+\n import { Row } from '../layout/row'\n-import CheckedDropdownMenu from '../widgets/checked-dropdown'\n import {\n FILTERS,\n FOR_YOU_KEY,\n Filter,\n@@ -90,81 +84,8 @@\n </Row>\n )\n }\n \n-export function TierDropdownPill(props: {\n- toggleTier: (tier: MarketTierType) => void\n- currentTiers: TierParamsType\n-}) {\n- const { toggleTier, currentTiers } = props\n- return (\n- <CheckedDropdownMenu\n- withinOverflowContainer\n- items={[\n- {\n- name: 'Crystal',\n- content: (\n- <Row className=\"items-center text-sm\">\n- <CrystalTier />\n- <div className=\"bg-gradient-to-r from-pink-700 to-pink-500 bg-clip-text text-transparent dark:from-pink-400 dark:to-pink-300\">\n- Crystal\n- </div>\n- </Row>\n- ),\n- onToggle: () => toggleTier('crystal'),\n- checked: currentTiers[tiers.indexOf('crystal')] == '1',\n- },\n- {\n- name: 'Premium',\n- content: (\n- <Row className=\"items-center text-sm text-purple-600 dark:text-purple-400\">\n- <PremiumTier />\n- Premium\n- </Row>\n- ),\n- onToggle: () => toggleTier('premium'),\n- checked: currentTiers[tiers.indexOf('premium')] == '1',\n- },\n- {\n- name: 'Plus',\n- content: (\n- <Row className=\"items-center text-sm font-semibold text-blue-600 dark:text-blue-500\">\n- <PlusTier />\n- Plus\n- </Row>\n- ),\n- onToggle: () => toggleTier('plus'),\n- checked: currentTiers[tiers.indexOf('plus')] == '1',\n- },\n- ]}\n- buttonContent={(open) => (\n- <DropdownPill\n- open={open}\n- color={currentTiers.includes('1') ? 'indigo' : 'light-gray'}\n- >\n- <Row className=\"h-5 items-center\">\n- {currentTiers === '00000'\n- ? 'Tiers'\n- : currentTiers.split('').map((tier, index) => {\n- if (tier === '1') {\n- if (tiers[index] == 'plus') {\n- return <PlusTier key={index} />\n- }\n- if (tiers[index] == 'premium') {\n- return <PremiumTier key={index} />\n- }\n- if (tiers[index] == 'crystal') {\n- return <CrystalTier key={index} />\n- }\n- }\n- })}\n- </Row>\n- </DropdownPill>\n- )}\n- />\n- )\n-}\n-\n export function FilterDropdownPill(props: {\n selectFilter: (selection: Filter) => void\n currentFilter: Filter\n }) {\n"
|
|
1585
|
-
},
|
|
1586
|
-
{
|
|
1587
|
-
"path": "web/components/tiers/liquidity-tooltip.tsx",
|
|
1588
|
-
"status": "modified",
|
|
1589
|
-
"diff": "Index: web/components/tiers/liquidity-tooltip.tsx\n===================================================================\n--- web/components/tiers/liquidity-tooltip.tsx\t262f7eb (parent)\n+++ web/components/tiers/liquidity-tooltip.tsx\t13133e3 (commit)\n@@ -1,16 +1,9 @@\n import { Placement } from '@floating-ui/react'\n import clsx from 'clsx'\n import { Contract } from 'common/contract'\n-import { MarketTierType } from 'common/tier'\n import { formatWithToken, shortFormatNumber } from 'common/util/format'\n-import { capitalize } from 'lodash'\n-import {\n- CrystalTier,\n- PlayTier,\n- PlusTier,\n- PremiumTier,\n-} from 'web/public/custom-components/tiers'\n+\n import { Tooltip } from '../widgets/tooltip'\n import { GiWaterDrop } from 'react-icons/gi'\n \n export function LiquidityTooltip(props: {\n@@ -41,30 +34,4 @@\n {shortFormatNumber(amount)}\n </Tooltip>\n )\n }\n-\n-export function getPresentedTierName(tier: MarketTierType) {\n- if (tier == 'play') {\n- return 'Basic'\n- }\n-\n- return capitalize(tier)\n-}\n-\n-export function TierIcon(props: { tier: MarketTierType; className?: string }) {\n- const { tier, className } = props\n- if (tier == 'play') {\n- return <PlayTier className={className} />\n- }\n-\n- if (tier == 'plus') {\n- return <PlusTier className={className} />\n- }\n- if (tier == 'premium') {\n- return <PremiumTier className={className} />\n- }\n- if (tier == 'crystal') {\n- return <CrystalTier className={className} />\n- }\n- return <></>\n-}\n"
|
|
1590
|
-
},
|
|
1591
|
-
{
|
|
1592
|
-
"path": "web/components/widgets/amount-input.tsx",
|
|
1593
|
-
"status": "modified",
|
|
1594
|
-
"diff": "Index: web/components/widgets/amount-input.tsx\n===================================================================\n--- web/components/widgets/amount-input.tsx\t262f7eb (parent)\n+++ web/components/widgets/amount-input.tsx\t13133e3 (commit)\n@@ -5,9 +5,8 @@\n PHONE_VERIFICATION_BONUS,\n SWEEPS_MIN_BET,\n } from 'common/economy'\n import { ENV_CONFIG } from 'common/envs/constants'\n-import { MarketTierType } from 'common/tier'\n import { humanish, User } from 'common/user'\n import {\n formatMoney,\n formatWithToken,\n@@ -161,17 +160,17 @@\n disregardUserBalance?: boolean\n quickButtonAmountSize?: 'large' | 'small'\n disableQuickButtons?: boolean\n token?: InputTokenType\n- marketTier?: MarketTierType | undefined\n+ liquidityTier?: number | undefined\n sliderColor?: keyof typeof sliderColors\n }) {\n const {\n amount,\n onChange,\n error,\n setError,\n- marketTier,\n+ liquidityTier,\n disabled,\n binaryOutcome,\n showBalance,\n showSlider,\n@@ -297,9 +296,9 @@\n amount={amount}\n onAmountChange={onChange}\n binaryOutcome={binaryOutcome}\n disabled={disabled}\n- smallAmounts={!hasLotsOfMoney || marketTier === 'play'}\n+ smallAmounts={!hasLotsOfMoney || liquidityTier === 0}\n token={token}\n sliderColor={sliderColor}\n />\n )}\n"
|
|
1595
|
-
},
|
|
1596
|
-
{
|
|
1597
|
-
"path": "web/lib/admin/create-sports-markets.ts",
|
|
1598
|
-
"status": "modified",
|
|
1599
|
-
"diff": "Index: web/lib/admin/create-sports-markets.ts\n===================================================================\n--- web/lib/admin/create-sports-markets.ts\t262f7eb (parent)\n+++ web/lib/admin/create-sports-markets.ts\t13133e3 (commit)\n@@ -2,8 +2,9 @@\n import { SportsGames } from 'common/sports-info'\n import { buildArray } from 'common/util/array'\n import { APIParams } from 'common/api/schema'\n import { ENV } from 'common/envs/constants'\n+import { liquidityTiers } from 'common/tier'\n \n export const handleCreateSportsMarkets = async (\n setIsLoading: (loading: boolean) => void,\n setIsFinished: (finished: boolean) => void\n@@ -83,8 +84,9 @@\n answers,\n answerShortTexts,\n answerImageUrls,\n visibility: 'public',\n+ liquidityTier: liquidityTiers[1],\n addAnswersMode: 'DISABLED',\n shouldAnswersSumToOne: true,\n sportsStartTimestamp: sportsGames.strTimestamp,\n sportsEventId: sportsGames.idEvent,\n"
|
|
1600
|
-
}
|
|
1601
|
-
]
|
|
1602
|
-
},
|
|
1603
|
-
{
|
|
1604
|
-
"id": "simplify-trades-table",
|
|
1605
|
-
"sha": "702a5b83248af6fd73abf118c05411fad29e4f55",
|
|
1606
|
-
"parentSha": "80a4d9486ee05ab71963558a8ed7f88f2d2e2c73",
|
|
1607
|
-
"spec": "Implement a simplified User Bets (Trades) experience, update expected value/date calculations for resolved multi-answer markets, harden click interactions in paginated, expandable lists, and adjust resolved text color fallback.\n\n1) Expected value/date for resolved multi-answer markets\n- Files: common/src/multi-numeric.ts, common/src/multi-date.ts\n- getExpectedValue(contract, initialOnly?) and getExpectedDate(contract, initialOnly?) must:\n - Read contract.isResolved in addition to answers and shouldAnswersSumToOne.\n - When initialOnly is true, continue using getInitialAnswerProbability for each answer.\n - When initialOnly is false:\n - If contract.isResolved is true, use each Answer.prob directly to compute expected value/date.\n - Otherwise, use getAnswerProbability(contract, answer.id) as before.\n- Keep all existing logic for shouldAnswersSumToOne weighting and threshold-style handling.\n\n2) ContractBetsTable: defaultExpanded behavior and click isolations\n- File: web/components/bet/contract-bets-table.tsx\n- Add an optional prop defaultExpanded?: boolean (default false).\n- Initialize the expanded state from defaultExpanded (not hard-coded false).\n- Ensure the \"Show more\" button’s onClick stops propagation (e.stopPropagation()) so it doesn’t trigger parent row clicks.\n- This change must be backward compatible with all existing usages.\n\n3) LimitOrdersTable cash icon rendering\n- File: web/components/bet/limit-orders-table.tsx\n- In the contract header link where the SweepiesCoin appears when contract.token === 'CASH', render <SweepiesCoin /> without absolute/top classes. Remove the absolute inset/top positioning wrapper.\n\n4) UserBetsTable: refactor UI to simplified list with dropdown filters and sorting\n- File: web/components/bet/user-bets-table.tsx\n- Replace the carousel and complex column grid with:\n - A sticky top controls bar (only for non-limit-orders view) including:\n - Search input (unchanged behavior).\n - A Filter dropdown with options: All, Open, Sold, Closed, Resolved, Limit Orders.\n - When Limit Orders is selected, set showLimitOrders = true and set a default bets filter (e.g., 'open').\n - When any other filter is selected, set showLimitOrders = false and apply the chosen bets filter.\n - When showLimitOrders is true (and if viewing your own bets), show a second dropdown for limit order status: Open (active), Expired, Filled, Cancelled. Toggling updates the includeExpired/includeFilled/includeCancelled props used by LimitOrdersTable.\n - When showLimitOrders is false, show a Sort dropdown with the options below.\n - Persist the sort selection in memory (usePersistentInMemoryState). Define sortOption as an object { field: BetSort; direction: 'asc' | 'desc' } with initial { field: 'newest', direction: 'desc' }.\n - Sort options (labels map to field and direction):\n - Newest (newest, desc)\n - Oldest (newest, asc)\n - Highest Value (value, desc)\n - Highest Position (position, desc)\n - Highest Profit (profit, desc)\n - Lowest Profit (profit, asc)\n - Highest 1d Change (day, desc)\n - Lowest 1d Change (day, asc)\n - Highest 1w Change (week, desc)\n - Lowest 1w Change (week, asc)\n - Closing Soon (closeTime, asc)\n - Only show the Sweepcash toggle PillButton when hasSweeps is true and the filter is 'resolved'.\n- Limit orders view:\n - When showLimitOrders is true, render LimitOrdersTable with filter set to \"all\" (ignore the bets filter), and includeExpired/filled/cancelled controlled by the limit order status dropdown.\n- Bets view list rendering:\n - Change BetsTable prop types from CPMMContract[] to MarketContract[].\n - Remove header row, editable columns, modal, and NumberCell/Header components entirely.\n - Accept sortOption from parent (remove internal sort state) and sort using the supplied option. Fix profitPercent sorting to use metricsByContractId[c.id].profitPercent (not profit).\n - Render each contract as a single row with:\n - Left: question link (stopping propagation), with SweepiesCoin tooltip for CASH markets; below/adjacent, a status chip:\n - If resolved and outcomeType is MULTIPLE_CHOICE: show resolution as MULTI if multiple winners/MKT; CANCEL via BinaryOutcomeLabel(\"CANCEL\"); else show the winning answer with MultiOutcomeLabel; otherwise use ContractStatusLabel for non-multi types.\n - If not resolved and not MULTIPLE_CHOICE: show ContractStatusLabel.\n - Show creator name, and additionally:\n - If sorting by closeTime: show the close/resolution date (MM/DD[/YY]).\n - If sorting by newest: show time since last bet via RelativeTimestamp.\n - Right: values based on sort selection:\n - Position: sum of totalShares (positive formatting) with token (CASH vs M$).\n - Otherwise show payout value (positive formatting) with token.\n - For day/week sort: display profit for that window and a small percentage pill using from.day.profitPercent or from.week.profitPercent (with teal styling if positive).\n - Otherwise show overall profit and a percentage pill with profitPercent.\n - A chevron icon indicating expandable state; rotate when expanded.\n - Clicking a row toggles expansion for that contract (expandedIds). Links inside the row must stop propagation.\n - In the expanded area, render ContractBetsTable with defaultExpanded enabled to show full bets, passing isYourBets, contractMetric, and paginate.\n\n5) Resolved text color fallback\n- File: web/components/contract/text-color.ts\n- When contract.resolution is set but not found in the OUTCOME_TO_COLOR_TEXT mapping, return 'text-blue-600 dark:text-blue-200' as the fallback class (not the previous 'text-primary-200').\n\n6) Pagination: stop event propagation when changing pages\n- File: web/components/widgets/pagination.tsx\n- Change PaginationArrow onClick signature to accept an optional React.MouseEvent.\n- In Pagination, update both Prev and Next handlers to stop propagation (e.stopPropagation()) before changing page.\n- In PageNumbers, update the button onClick to stop propagation before calling setPage.\n- Ensure these changes prevent parent row onClick (expand/collapse) from triggering when interacting with pagination controls.\n\n7) User profile header actions layout\n- File: web/pages/[username]/index.tsx\n- For the current user in the header actions row:\n - Replace the existing pair of AddFundsButton and RedeemSweepsButtons with a single AddFundsButton (className: \"mr-2 w-full whitespace-nowrap px-8 lg:hidden\").\n - Move RedeemSweepsButtons to a separate rendering block for current user only, with className: \"m-2 w-48\" and remove the previous mobile-only row that combined both buttons.\n\n8) Imports and cleanup\n- Update imports in user-bets-table.tsx to include Headless UI Menu components (Menu, MenuButton, MenuItem, MenuItems) and ChevronDownIcon. Remove unused imports/components removed as part of the simplification (carousel, dropdown-menu, modal, icon buttons for headers, etc.).\n- Update uses of BinaryOutcomeLabel, MultiOutcomeLabel, ContractStatusLabel, RelativeTimestamp per the new layout.\n- Ensure links and interactive elements within rows call e.stopPropagation() to avoid toggling expansion.\n\nAcceptance criteria:\n- In Trades tab of a user profile, the control bar shows a search input and dropdowns (Filter, optional Limit Order Status in limit orders view, and Sort in non-limit view). Switching filters updates the view appropriately; selecting Limit Orders shows the limit orders list and ignores the standard bets filters.\n- In non-limit orders view, the list is a simplified row layout with question, status/resolution, creator, optional time/date, and right-aligned values with token formatting and change badges, and an expand chevron. Clicking the row expands details; pagination inside expanded bets or its “show more” button does not accidentally toggle expansion.\n- Default expansion of bets in the expanded section is on (via defaultExpanded).\n- For resolved multi-answer markets, expected value/date functions use the final Answer.prob values (reflected wherever these functions are used).\n- The resolved text color fallback appears blue (‘text-blue-600’ in light and ‘dark:text-blue-200’ in dark) if the resolution key isn’t in the map.\n- SweepiesCoin renders without absolute positioning in limit orders table titles.\n- Prev/Next and page number clicks no longer bubble to parent row onClick handlers.",
|
|
1608
|
-
"prompt": "Revamp the user trades view and tighten interactions while ensuring resolved multi-answer market stats display correctly.\n\n- Add a simplified Trades list UI: a sticky control bar with a search box, a Filter dropdown (All/Open/Sold/Closed/Resolved/Limit Orders), a Limit Order Status dropdown (Open/Expired/Filled/Cancelled) visible only when in the limit orders view, and a Sort dropdown for non-limit-views. Persist the sort setting and apply it to the bets list.\n- Replace the old column-grid table with a single-row-per-contract layout showing the question, status/resolution, creator, and context (date/time by sort), and on the right the key numeric figure (position or value) and change metrics (profit or day/week profit) with % badges. Rows are expandable; the expand chevron reflects state. Links and controls inside rows must not toggle expansion.\n- When viewing Limit Orders, feed a neutral filter to the table and control the included statuses via the dropdown.\n- Make the expanded bets section default to expanded.\n- Fix expected value and expected date calculations for resolved multi-answer contracts by using the answers’ stored probabilities post-resolution.\n- Use a blue fallback text color for resolved status when no mapped color is available.\n- Ensure pagination clicks and “show more” do not bubble and inadvertently toggle row expansion.\n- Adjust the current user’s profile header actions: show only the Add Funds button there; render Redeem Sweeps separately below with simpler layout.\n\nKeep prop types consistent, remove unused imports/components from the old UI, and maintain backward compatibility for existing ContractBetsTable callers.",
|
|
1609
|
-
"supplementalFiles": [
|
|
1610
|
-
"common/src/contract.ts",
|
|
1611
|
-
"common/src/answer.ts",
|
|
1612
|
-
"web/components/outcome-label.tsx",
|
|
1613
|
-
"web/components/contract/contracts-table.tsx",
|
|
1614
|
-
"web/components/widgets/tooltip.tsx",
|
|
1615
|
-
"web/components/add-funds-modal.tsx"
|
|
1616
|
-
],
|
|
1617
|
-
"fileDiffs": [
|
|
1618
|
-
{
|
|
1619
|
-
"path": "common/src/multi-date.ts",
|
|
1620
|
-
"status": "modified",
|
|
1621
|
-
"diff": "Index: common/src/multi-date.ts\n===================================================================\n--- common/src/multi-date.ts\t80a4d94 (parent)\n+++ common/src/multi-date.ts\t702a5b8 (commit)\n@@ -9,14 +9,16 @@\n export function getExpectedDate(\n contract: MultiDateContract,\n initialOnly?: boolean\n ) {\n- const { answers, shouldAnswersSumToOne } = contract\n+ const { answers, shouldAnswersSumToOne, isResolved } = contract\n \n const answerProbabilities = filterDefined(\n answers.map((a) =>\n initialOnly\n ? getInitialAnswerProbability(contract, a)\n+ : isResolved\n+ ? a.prob\n : getAnswerProbability(contract, a.id)\n )\n )\n const answerMidpoints = answers.map((a) => a.midpoint!)\n"
|
|
1622
|
-
},
|
|
1623
|
-
{
|
|
1624
|
-
"path": "common/src/multi-numeric.ts",
|
|
1625
|
-
"status": "modified",
|
|
1626
|
-
"diff": "Index: common/src/multi-numeric.ts\n===================================================================\n--- common/src/multi-numeric.ts\t80a4d94 (parent)\n+++ common/src/multi-numeric.ts\t702a5b8 (commit)\n@@ -9,14 +9,16 @@\n export function getExpectedValue(\n contract: MultiNumericContract,\n initialOnly?: boolean\n ) {\n- const { answers, shouldAnswersSumToOne } = contract\n+ const { answers, shouldAnswersSumToOne, isResolved } = contract\n \n const answerProbabilities = filterDefined(\n answers.map((a) =>\n initialOnly\n ? getInitialAnswerProbability(contract, a)\n+ : isResolved\n+ ? a.prob\n : getAnswerProbability(contract, a.id)\n )\n )\n const answerMidpoints = answers.map((a) => a.midpoint!)\n"
|
|
1627
|
-
},
|
|
1628
|
-
{
|
|
1629
|
-
"path": "web/components/bet/contract-bets-table.tsx",
|
|
1630
|
-
"status": "modified",
|
|
1631
|
-
"diff": "Index: web/components/bet/contract-bets-table.tsx\n===================================================================\n--- web/components/bet/contract-bets-table.tsx\t80a4d94 (parent)\n+++ web/components/bet/contract-bets-table.tsx\t702a5b8 (commit)\n@@ -35,15 +35,17 @@\n isYourBets: boolean\n contractMetric: ContractMetric\n hideRedemptionAndLoanMessages?: boolean\n paginate?: boolean\n+ defaultExpanded?: boolean\n }) {\n const {\n contract,\n isYourBets,\n hideRedemptionAndLoanMessages,\n paginate,\n contractMetric,\n+ defaultExpanded = false,\n } = props\n const { isResolved, mechanism, outcomeType } = contract\n \n const bets = sortBy(\n@@ -79,9 +81,9 @@\n \n const [page, setPage] = useState(0)\n const unexpandedBetsPerPage = 2\n const betsPerPage = 5\n- const [expanded, setExpanded] = useState(false)\n+ const [expanded, setExpanded] = useState(defaultExpanded)\n \n const displayedBets = expanded\n ? normalBets.slice(page * betsPerPage, (page + 1) * betsPerPage)\n : normalBets.slice(0, unexpandedBetsPerPage)\n@@ -132,9 +134,12 @@\n <button\n className={\n 'hover:bg-canvas-100 text-primary-700 mb-1 rounded-md p-2 text-sm'\n }\n- onClick={() => setExpanded(true)}\n+ onClick={(e) => {\n+ e.stopPropagation()\n+ setExpanded(true)\n+ }}\n >\n Show {normalBets.length - unexpandedBetsPerPage} more {TRADE_TERM}s\n </button>\n )}\n"
|
|
1632
|
-
},
|
|
1633
|
-
{
|
|
1634
|
-
"path": "web/components/bet/limit-orders-table.tsx",
|
|
1635
|
-
"status": "modified",
|
|
1636
|
-
"diff": "Index: web/components/bet/limit-orders-table.tsx\n===================================================================\n--- web/components/bet/limit-orders-table.tsx\t80a4d94 (parent)\n+++ web/components/bet/limit-orders-table.tsx\t702a5b8 (commit)\n@@ -357,11 +357,9 @@\n className={''}\n />\n </span>\n <span className=\"text-ink-600 line-clamp-1\">\n- {contract.token == 'CASH' && (\n- <SweepiesCoin className=\"absolute inset-0 top-[0.2em]\" />\n- )}\n+ {contract.token == 'CASH' && <SweepiesCoin />}\n {contract.question}\n </span>\n </Link>\n </Row>\n"
|
|
1637
|
-
},
|
|
1638
|
-
{
|
|
1639
|
-
"path": "web/components/bet/user-bets-table.tsx",
|
|
1640
|
-
"status": "modified",
|
|
1641
|
-
"diff": "Index: web/components/bet/user-bets-table.tsx\n===================================================================\n--- web/components/bet/user-bets-table.tsx\t80a4d94 (parent)\n+++ web/components/bet/user-bets-table.tsx\t702a5b8 (commit)\n@@ -1,6 +1,5 @@\n 'use client'\n-import { ChevronUpIcon } from '@heroicons/react/solid'\n import clsx from 'clsx'\n import { LimitBet } from 'common/bet'\n import { getContractBetNullMetrics } from 'common/calculate'\n import {\n@@ -9,55 +8,45 @@\n CPMMContract,\n MarketContract,\n } from 'common/contract'\n import { ContractMetric } from 'common/contract-metric'\n-import { ENV_CONFIG, SWEEPIES_MARKET_TOOLTIP } from 'common/envs/constants'\n+import { SWEEPIES_MARKET_TOOLTIP } from 'common/envs/constants'\n import { buildArray } from 'common/util/array'\n-import {\n- formatWithToken,\n- maybePluralize,\n- shortFormatNumber,\n- SWEEPIES_MONIKER,\n-} from 'common/util/format'\n+import { formatWithToken } from 'common/util/format'\n import { searchInAny } from 'common/util/parse'\n import { Dictionary, sortBy, sum, uniqBy, mapValues } from 'lodash'\n import Link from 'next/link'\n-import { ReactNode, useEffect, useMemo, useState } from 'react'\n-import { BiCaretDown, BiCaretUp } from 'react-icons/bi'\n-import { BsThreeDotsVertical } from 'react-icons/bs'\n+import { useEffect, useMemo, useState } from 'react'\n import { BetsSummary } from 'web/components/bet/bet-summary'\n import { ContractBetsTable } from 'web/components/bet/contract-bets-table'\n import { OrderTable } from 'web/components/bet/order-book'\n-import { Button, IconButton } from 'web/components/buttons/button'\n import { PillButton } from 'web/components/buttons/pill-button'\n-import { ContractStatusLabel } from 'web/components/contract/contracts-table'\n-import { OutcomeLabel } from 'web/components/outcome-label'\n-import { RelativeTimestamp } from 'web/components/relative-timestamp'\n-import { Avatar } from 'web/components/widgets/avatar'\n-import { Carousel } from 'web/components/widgets/carousel'\n import { Input } from 'web/components/widgets/input'\n import { Pagination } from 'web/components/widgets/pagination'\n import { useContractBets } from 'client-common/hooks/use-bets'\n import { useEvent } from 'client-common/hooks/use-event'\n-import { useIsMobile } from 'web/hooks/use-is-mobile'\n import { usePersistentInMemoryState } from 'client-common/hooks/use-persistent-in-memory-state'\n import { usePersistentLocalState } from 'web/hooks/use-persistent-local-state'\n import { usePersistentQueryState } from 'web/hooks/use-persistent-query-state'\n import { useIsAuthorized, useUser } from 'web/hooks/use-user'\n import { api } from 'web/lib/api/api'\n import { User } from 'web/lib/firebase/users'\n import { SweepiesCoin } from 'web/public/custom-components/sweepiesCoin'\n-import DropdownMenu from '../widgets/dropdown-menu'\n-import { Modal, MODAL_CLASS } from '../layout/modal'\n import { useSweepstakes } from '../sweepstakes-provider'\n import { LoadingIndicator } from '../widgets/loading-indicator'\n import { linkClass } from '../widgets/site-link'\n import { Tooltip } from '../widgets/tooltip'\n import { floatingEqual } from 'common/util/math'\n import { Col } from '../layout/col'\n import { Row } from '../layout/row'\n import { useIsPageVisible } from 'web/hooks/use-page-visible'\n-import { LimitOrdersTable, LimitOrdersToggle } from './limit-orders-table'\n+import { LimitOrdersTable } from './limit-orders-table'\n+import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react'\n+import { ChevronDownIcon } from '@heroicons/react/solid'\n+import { ContractStatusLabel } from '../contract/contracts-table'\n+import { BinaryOutcomeLabel, MultiOutcomeLabel } from '../outcome-label'\n+import { RelativeTimestamp } from '../relative-timestamp'\n+\n type BetSort =\n | 'newest'\n | 'profit'\n | 'closeTime'\n@@ -96,15 +85,16 @@\n const [showLimitOrders, setShowLimitOrders] = usePersistentLocalState(\n false,\n 'show-limit-orders-view'\n )\n+ type LimitOrderFilter = 'active' | 'filled' | 'expired' | 'cancelled'\n // Add state for showing different order types\n- const [orderFilter, setOrderFilter] = usePersistentInMemoryState<\n- 'active' | 'filled' | 'expired' | 'cancelled'\n- >('active', 'limit-orders-filter')\n- const updateOrderFilter = (\n- newFilter: 'active' | 'filled' | 'expired' | 'cancelled'\n- ) => {\n+ const [orderFilter, setOrderFilter] =\n+ usePersistentInMemoryState<LimitOrderFilter>(\n+ 'active',\n+ 'limit-orders-filter'\n+ )\n+ const updateOrderFilter = (newFilter: LimitOrderFilter) => {\n setOrderFilter(orderFilter === newFilter ? 'active' : newFilter)\n }\n \n const getMetrics = useEvent(() =>\n@@ -148,10 +138,19 @@\n const [page, setPage] = usePersistentInMemoryState(0, 'portfolio-page')\n \n const [query, setQuery] = usePersistentQueryState('b', '')\n \n- const onSetFilter = (f: BetFilter) => {\n- setFilter(f)\n+ const onSetFilter = (f: BetFilter | 'limit_orders') => {\n+ if (f === 'limit_orders') {\n+ setShowLimitOrders(true)\n+ // When toggling limit orders on, set a default filter but it won't be used\n+ // while limit orders view is active\n+ setFilter('open')\n+ return\n+ }\n+ // When selecting any other filter, turn off limit orders view\n+ setShowLimitOrders(false)\n+ setFilter(f as BetFilter)\n setPage(0)\n }\n \n const toggleTokenFilter = () => {\n@@ -211,82 +210,205 @@\n })\n : []\n \n const hasSweeps = contracts?.some((c) => c.token === 'CASH')\n+\n+ // Define sort options for the dropdown\n+ const [sortOption, setSortOption] = usePersistentInMemoryState<{\n+ field: BetSort\n+ direction: 'asc' | 'desc'\n+ }>({ field: 'newest', direction: 'desc' }, 'bets-list-sort')\n+\n+ const sortOptions: {\n+ label: string\n+ field: BetSort\n+ direction: 'asc' | 'desc'\n+ }[] = [\n+ { label: 'Newest', field: 'newest', direction: 'desc' },\n+ { label: 'Oldest', field: 'newest', direction: 'asc' },\n+ { label: 'Highest Value', field: 'value', direction: 'desc' },\n+ { label: 'Highest Position', field: 'position', direction: 'desc' },\n+ { label: 'Highest Profit', field: 'profit', direction: 'desc' },\n+ { label: 'Lowest Profit', field: 'profit', direction: 'asc' },\n+ { label: 'Highest 1d Change', field: 'day', direction: 'desc' },\n+ { label: 'Lowest 1d Change', field: 'day', direction: 'asc' },\n+ { label: 'Highest 1w Change', field: 'week', direction: 'desc' },\n+ { label: 'Lowest 1w Change', field: 'week', direction: 'asc' },\n+ { label: 'Closing Soon', field: 'closeTime', direction: 'asc' },\n+ ]\n+\n+ const filterOptions = [\n+ { label: 'All', value: 'all' },\n+ { label: 'Open', value: 'open' },\n+ { label: 'Sold', value: 'sold' },\n+ { label: 'Closed', value: 'closed' },\n+ { label: 'Resolved', value: 'resolved' },\n+ { label: 'Limit Orders', value: 'limit_orders' },\n+ ]\n+\n+ const limitOrderFilterOptions: {\n+ label: string\n+ value: LimitOrderFilter\n+ }[] = [\n+ { label: 'Open', value: 'active' },\n+ { label: 'Expired', value: 'expired' },\n+ { label: 'Filled', value: 'filled' },\n+ { label: 'Cancelled', value: 'cancelled' },\n+ ]\n+\n+ const onSelectSortOption = (option: {\n+ field: BetSort\n+ direction: 'asc' | 'desc'\n+ }) => {\n+ setSortOption(option)\n+ setPage(0)\n+ }\n+\n return (\n- <Col>\n- <div className=\"flex flex-wrap justify-between gap-4 max-sm:flex-col\">\n+ <Col className=\"relative\">\n+ <div\n+ className={clsx(\n+ 'flex flex-wrap justify-between gap-4 max-sm:flex-col',\n+ !showLimitOrders && 'bg-canvas-0 sticky top-0 z-10 pt-1'\n+ )}\n+ >\n <Col className=\"w-full gap-2\">\n- <Row className=\"items-center gap-1 sm:gap-2\">\n+ <Col\n+ className={\n+ 'items-end gap-2 sm:flex-row sm:items-center sm:justify-between'\n+ }\n+ >\n <Input\n placeholder={isYou ? 'Search your bets' : 'Search bets'}\n className={'w-full min-w-[30px]'}\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n />\n- {isYou && (\n- <LimitOrdersToggle\n- showLimitOrders={showLimitOrders}\n- setShowLimitOrders={setShowLimitOrders}\n- />\n- )}\n- </Row>\n- <Carousel labelsParentClassName={'gap-1'}>\n- {showLimitOrders && isYou && (\n+ <Row className=\"h-full gap-2\">\n+ {/* Filter Dropdown */}\n+ <Col className=\"relative\">\n+ <Menu>\n+ <MenuButton className=\"bg-canvas-0 border-ink-200 hover:bg-canvas-50 inline-flex h-full w-32 items-center justify-between rounded-md border px-3 py-1.5 text-sm shadow-sm\">\n+ <span>\n+ {showLimitOrders\n+ ? 'Limit Orders'\n+ : filterOptions.find((opt) => opt.value === filter)\n+ ?.label || 'Filter'}\n+ </span>\n+ <ChevronDownIcon className=\"ml-2 h-4 w-4\" />\n+ </MenuButton>\n+ <MenuItems className=\"bg-canvas-0 border-ink-200 absolute right-0 z-20 mt-1 w-48 overflow-hidden rounded-md border shadow-lg\">\n+ {filterOptions.map((option) => (\n+ <MenuItem key={option.value}>\n+ {({ focus }) => (\n+ <button\n+ className={clsx(\n+ 'w-full px-4 py-2 text-left text-sm',\n+ focus ? 'bg-primary-50' : '',\n+ option.value === 'limit_orders' && showLimitOrders\n+ ? 'bg-primary-100'\n+ : ''\n+ )}\n+ onClick={() =>\n+ onSetFilter(\n+ option.value as BetFilter | 'limit_orders'\n+ )\n+ }\n+ >\n+ {option.label}\n+ </button>\n+ )}\n+ </MenuItem>\n+ ))}\n+ </MenuItems>\n+ </Menu>\n+ </Col>\n+\n+ {/* Limit Order Status Dropdown - Only visible when showLimitOrders is true */}\n+ {showLimitOrders && isYou && (\n+ <Col className=\"relative\">\n+ <Menu>\n+ <MenuButton className=\"bg-canvas-0 border-ink-200 hover:bg-canvas-50 inline-flex h-full w-28 items-center justify-between rounded-md border px-3 py-1.5 text-sm shadow-sm\">\n+ <span>\n+ {limitOrderFilterOptions.find(\n+ (opt) => opt.value === orderFilter\n+ )?.label || 'Open'}\n+ </span>\n+ <ChevronDownIcon className=\"ml-2 h-4 w-4\" />\n+ </MenuButton>\n+ <MenuItems className=\"bg-canvas-0 border-ink-200 absolute right-0 z-20 mt-1 w-48 overflow-hidden rounded-md border shadow-lg\">\n+ {limitOrderFilterOptions.map((option) => (\n+ <MenuItem key={option.value}>\n+ {({ focus }) => (\n+ <button\n+ className={clsx(\n+ 'w-full px-4 py-2 text-left text-sm',\n+ focus ? 'bg-primary-50' : '',\n+ orderFilter === option.value\n+ ? 'bg-primary-100'\n+ : ''\n+ )}\n+ onClick={() => updateOrderFilter(option.value)}\n+ >\n+ {option.label}\n+ </button>\n+ )}\n+ </MenuItem>\n+ ))}\n+ </MenuItems>\n+ </Menu>\n+ </Col>\n+ )}\n+\n+ {!showLimitOrders && (\n+ <Col className=\"relative\">\n+ <Menu>\n+ <MenuButton className=\"bg-canvas-0 border-ink-200 hover:bg-canvas-50 inline-flex h-full w-44 items-center justify-between rounded-md border px-3 py-1.5 text-sm shadow-sm\">\n+ <span>\n+ {sortOptions.find(\n+ (opt) =>\n+ opt.field === sortOption.field &&\n+ opt.direction === sortOption.direction\n+ )?.label || 'Sort'}\n+ </span>\n+ <ChevronDownIcon className=\"ml-2 h-4 w-4\" />\n+ </MenuButton>\n+ <MenuItems className=\"bg-canvas-0 border-ink-200 absolute right-0 z-10 mt-1 w-48 overflow-hidden rounded-md border shadow-lg\">\n+ {sortOptions.map((option) => (\n+ <MenuItem key={`${option.field}-${option.direction}`}>\n+ {({ focus }) => (\n+ <button\n+ className={clsx(\n+ 'w-full px-4 py-2 text-left text-sm',\n+ focus ? 'bg-primary-50' : '',\n+ sortOption.field === option.field &&\n+ sortOption.direction === option.direction\n+ ? 'bg-primary-100'\n+ : ''\n+ )}\n+ onClick={() => onSelectSortOption(option)}\n+ >\n+ {option.label}\n+ </button>\n+ )}\n+ </MenuItem>\n+ ))}\n+ </MenuItems>\n+ </Menu>\n+ </Col>\n+ )}\n+ </Row>\n+ </Col>\n+ {hasSweeps && filter === 'resolved' && (\n+ <Row>\n <PillButton\n- selected={orderFilter === 'expired'}\n- onSelect={() => updateOrderFilter('expired')}\n- >\n- Expired\n- </PillButton>\n- )}\n- {showLimitOrders && isYou && (\n- <PillButton\n- selected={orderFilter === 'filled'}\n- onSelect={() => updateOrderFilter('filled')}\n- >\n- Filled\n- </PillButton>\n- )}\n- {showLimitOrders && isYou && (\n- <PillButton\n- selected={orderFilter === 'cancelled'}\n- onSelect={() => updateOrderFilter('cancelled')}\n- >\n- Cancelled\n- </PillButton>\n- )}\n- {showLimitOrders && isYou && (\n- <span className=\"text-ink-500 \">|</span>\n- )}\n- {(['all', 'open', 'sold', 'closed', 'resolved'] as BetFilter[]).map(\n- (f) => (\n- <PillButton\n- key={f}\n- selected={filter === f}\n- onSelect={() => onSetFilter(f)}\n- >\n- {f === 'all'\n- ? 'All'\n- : f === 'sold'\n- ? 'Sold'\n- : f === 'closed'\n- ? 'Closed'\n- : f === 'resolved'\n- ? 'Resolved'\n- : 'Open'}\n- </PillButton>\n- )\n- )}\n- {hasSweeps && (\n- <PillButton\n selected={!(prefersPlay ?? false)}\n onSelect={toggleTokenFilter}\n >\n Sweepcash\n </PillButton>\n- )}\n- </Carousel>\n+ </Row>\n+ )}\n </Col>\n </div>\n \n {!loaded ? (\n@@ -304,20 +426,21 @@\n isYourBets={isYou}\n includeExpired={orderFilter === 'expired'}\n includeFilled={orderFilter === 'filled'}\n includeCancelled={orderFilter === 'cancelled'}\n- filter={filter}\n+ filter=\"all\" // Use a default filter since filters don't matter for limit orders view\n className=\"mt-2\"\n />\n ) : (\n <BetsTable\n- contracts={filteredContracts as CPMMContract[]}\n+ contracts={filteredContracts as MarketContract[]}\n metricsByContractId={nullableMetricsByContract}\n page={page}\n user={user}\n setPage={setPage}\n filter={filter}\n signedInUser={signedInUser}\n+ sortOption={sortOption}\n />\n )}\n </Col>\n )\n@@ -336,73 +459,33 @@\n )\n }\n \n function BetsTable(props: {\n- contracts: CPMMContract[]\n+ contracts: MarketContract[]\n metricsByContractId: { [key: string]: ContractMetric }\n page: number\n setPage: (page: number) => void\n filter: BetFilter\n user: User\n signedInUser: User | null | undefined\n+ sortOption: { field: BetSort; direction: 'asc' | 'desc' }\n }) {\n- const { metricsByContractId, page, setPage, filter, user, signedInUser } =\n- props\n+ const {\n+ metricsByContractId,\n+ page,\n+ setPage,\n+ filter,\n+ user,\n+ signedInUser,\n+ sortOption,\n+ } = props\n const areYourBets = user.id === signedInUser?.id\n- const [sort, setSort] = usePersistentInMemoryState<{\n- field: BetSort\n- direction: 'asc' | 'desc'\n- }>({ field: 'newest', direction: 'desc' }, 'bets-list-sort')\n- const onSetSort = (field: BetSort) => {\n- if (sort.field === field) {\n- setSort((prevSort) => ({\n- ...prevSort,\n- direction: prevSort.direction === 'asc' ? 'desc' : 'asc',\n- }))\n- } else {\n- setSort({ field, direction: 'desc' })\n- }\n- setPage(0)\n- }\n \n- type ColumnHeader = {\n- sort: BetSort\n- label: string\n- enabled: boolean\n- altSort?: BetSort\n- placeholder?: boolean\n- }\n-\n- const [columns, setColumns] = usePersistentLocalState<ColumnHeader[]>(\n- [\n- { sort: 'newest', label: 'Time', enabled: true },\n- { sort: 'value', label: 'Value', enabled: true },\n- { sort: 'position', label: 'Position', enabled: false },\n- { sort: 'profit', label: 'Profit', enabled: true },\n- { sort: 'day', label: '1d', enabled: true },\n- { sort: 'week', label: '1w', enabled: true },\n- { sort: 'closeTime', label: 'Close', enabled: true },\n- ],\n- 'user-bet-table-columns'\n- )\n- const isEnabled = (sort: BetSort) =>\n- !isMobile || columns.find((col) => col.sort === sort)?.enabled\n-\n- const handleColumnToggle = (sort: BetSort) => {\n- setColumns((prevConfigs) =>\n- prevConfigs.map((config) =>\n- config.sort === sort ? { ...config, enabled: !config.enabled } : config\n- )\n- )\n- }\n- const enabledColumnsCount = columns.filter((c) => c.enabled).length\n- const [modalOpen, setModalOpen] = useState(false)\n-\n // Most of these are descending sorts by default.\n const SORTS: Record<BetSort, (c: Contract) => number> = {\n position: (c) => -sum(Object.values(metricsByContractId[c.id].totalShares)),\n profit: (c) => -metricsByContractId[c.id].profit,\n- profitPercent: (c) => -metricsByContractId[c.id].profit,\n+ profitPercent: (c) => -metricsByContractId[c.id].profitPercent,\n value: (c) => -metricsByContractId[c.id].payout,\n newest: (c) => -(metricsByContractId[c.id].lastBetTime ?? 0),\n probChangeDay: (c) => {\n if (c.mechanism === 'cpmm-1') {\n@@ -416,349 +499,272 @@\n // This is in fact the intuitive sort direction.\n (filter === 'open' ? -1 : 1) *\n (c.resolutionTime ?? c.closeTime ?? Infinity),\n }\n+\n+ const sortFunction = SORTS[sortOption.field]\n const contracts =\n- sort.direction === 'desc'\n- ? sortBy(props.contracts, SORTS[sort.field])\n- : sortBy(props.contracts, SORTS[sort.field]).reverse()\n+ sortOption.direction === 'desc'\n+ ? sortBy(props.contracts, sortFunction)\n+ : sortBy(props.contracts, sortFunction).reverse()\n+\n const rowsPerSection = 50\n const currentSlice = page * rowsPerSection\n- const isMobile = useIsMobile(600)\n \n- const dataColumns: {\n- header: ColumnHeader\n- span: number\n- renderCell: (c: Contract) => ReactNode\n- headerBuddy?: ReactNode\n- }[] = buildArray([\n- {\n- header: columns[0],\n- span: isMobile ? 2 : 1,\n- renderCell: (c: Contract) => (\n- <Row className={'justify-start'}>\n- <RelativeTimestamp\n- time={metricsByContractId[c.id].lastBetTime}\n- shortened\n- className=\"text-ink-500 -ml-1\"\n- />\n- </Row>\n- ),\n- },\n- {\n- header: columns[1],\n- span: isMobile ? 3 : 2,\n- renderCell: (c: Contract) => {\n- const maxOutcome = metricsByContractId[c.id].maxSharesOutcome\n- const showOutcome = maxOutcome && c.outcomeType === 'BINARY'\n- return (\n- <Row className={clsx('justify-end gap-1')}>\n- {showOutcome && isMobile && (\n- <OutcomeLabel\n- contract={c}\n- outcome={maxOutcome}\n- truncate={'short'}\n- />\n- )}\n- <Col className={'sm:min-w-[50px]'}>\n- {Math.floor(metricsByContractId[c.id].payout) <= 0 ? (\n- <span className=\"text-ink-500 text-right\">-</span>\n- ) : (\n- <NumberCell\n- num={metricsByContractId[c.id].payout}\n- isCashContract={c.token === 'CASH'}\n- />\n- )}\n- </Col>\n- </Row>\n- )\n- },\n- },\n-\n- {\n- header: columns[2],\n- span: 3,\n- renderCell: (c: Contract) => {\n- const maxOutcome = metricsByContractId[c.id].maxSharesOutcome\n- const showOutcome = maxOutcome && c.outcomeType === 'BINARY'\n- const totalShares = sum(\n- Object.values(metricsByContractId[c.id].totalShares)\n- )\n- return (\n- <Row className={clsx('justify-end gap-1')}>\n- {showOutcome &&\n- ((isMobile && !isEnabled('value')) || !isMobile) && (\n- <OutcomeLabel\n- contract={c}\n- outcome={maxOutcome}\n- truncate={'short'}\n- />\n- )}\n- <Col className={'sm:min-w-[50px]'}>\n- {Math.floor(totalShares) <= 0 ? (\n- <span className=\"text-ink-500 text-right\">-</span>\n- ) : (\n- <NumberCell\n- num={totalShares}\n- isCashContract={c.token === 'CASH'}\n- />\n- )}\n- </Col>\n- </Row>\n- )\n- },\n- headerBuddy: isMobile && <div className=\"-mr-2\" />,\n- },\n- {\n- header: { ...columns[3] },\n- span: isMobile ? 3 : 2,\n- renderCell: (c: Contract) => {\n- const cm = metricsByContractId[c.id]\n- return (\n- <Row className={'justify-end gap-1'}>\n- {Math.floor(cm.profit) === 0 ? (\n- <span className=\"text-ink-500 text-right\">-</span>\n- ) : (\n- <NumberCell\n- num={cm.profit}\n- change={true}\n- isCashContract={c.token === 'CASH'}\n- />\n- )}\n- </Row>\n- )\n- },\n- },\n- {\n- header: columns[4],\n- span: isMobile ? 3 : 2,\n- renderCell: (c: Contract) => {\n- const cm = metricsByContractId[c.id]\n- return (\n- <Row className={'justify-end gap-1'}>\n- {Math.floor(cm.from?.day.profit ?? 0) === 0 ? (\n- <span className=\"text-ink-500 text-right\">-</span>\n- ) : (\n- <NumberCell\n- num={cm.from?.day.profit ?? 0}\n- change={true}\n- isCashContract={c.token === 'CASH'}\n- />\n- )}\n- </Row>\n- )\n- },\n- },\n- {\n- header: columns[5],\n- span: isMobile ? 3 : 2,\n- renderCell: (c: Contract) => {\n- const cm = metricsByContractId[c.id]\n- return (\n- <Row className={'justify-end gap-1'}>\n- {Math.floor(cm.from?.week.profit ?? 0) === 0 ? (\n- <span className=\"text-ink-500 text-right\">-</span>\n- ) : (\n- <NumberCell\n- num={cm.from?.week.profit ?? 0}\n- change={true}\n- isCashContract={c.token === 'CASH'}\n- />\n- )}\n- </Row>\n- )\n- },\n- },\n- {\n- header: columns[6],\n- span: isMobile ? 3 : 2,\n- renderCell: (c: Contract) => {\n- const closeTime = c.resolutionTime ?? c.closeTime\n- const date = new Date(closeTime ?? Infinity)\n- const isThisYear = new Date().getFullYear() === date.getFullYear()\n- const dateString = date.toLocaleDateString('en-US', {\n- month: '2-digit',\n- day: '2-digit',\n- year: isThisYear ? undefined : '2-digit',\n- })\n- return (\n- <Row className={'justify-end'}>\n- <span className={'text-ink-800'}>\n- {closeTime ? dateString : 'N/A'}\n- </span>\n- </Row>\n- )\n- },\n- },\n- ])\n- const getColSpan = (i: number) =>\n- i === 4\n- ? 'col-span-4'\n- : i === 3\n- ? 'col-span-3'\n- : i === 2\n- ? 'col-span-2'\n- : 'col-span-1'\n-\n const [expandedIds, setExpandedIds] = useState<string[]>([])\n-\n- const setNewExpandedId = async (id: string) => {\n+ const setNewExpandedId = (id: string) => {\n setExpandedIds((oldIds) =>\n oldIds.includes(id)\n ? oldIds.filter((oldId) => oldId !== id)\n : [...oldIds, id]\n )\n }\n \n- const MAX_SHOWN_MOBILE = 5\n- const columnsToDisplay = dataColumns\n- .filter((c) => isEnabled(c.header.sort))\n- .slice(0, isMobile ? MAX_SHOWN_MOBILE : 10)\n-\n return (\n <Col className=\"mb-4 flex-1 gap-4\">\n <Col className={'w-full'}>\n- <div\n- className={clsx(\n- 'grid-cols-15 bg-canvas-0 sticky z-10 grid w-full py-2 pr-1',\n- isMobile ? 'top-12' : 'top-0' // Sets it below sticky user profile header on mobile\n- )}\n- >\n- {columnsToDisplay.map((col) =>\n- col.header.placeholder ? (\n- <span key={col.header.label} className=\"text-sm\" />\n- ) : (\n- <span\n- key={col.header.sort}\n- className={clsx(\n- getColSpan(col.span),\n- 'relative flex justify-end first:justify-start'\n- )}\n- >\n- <Header\n- onClick={() =>\n- onSetSort(\n- (col.header.altSort && sort.field === col.header.altSort\n- ? col.header.altSort\n- : col.header.sort) as BetSort\n- )\n- }\n- up={\n- sort.field === col.header.sort ||\n- sort.field === col.header.altSort\n- ? sort.direction === 'asc'\n- : undefined\n- }\n- className=\"text-sm font-semibold\"\n- >\n- {col.header.label}\n- </Header>\n- {col.headerBuddy ? col.headerBuddy : null}\n- </span>\n- )\n- )}\n- {isMobile && (\n- <div className=\"absolute bottom-1.5 right-0\">\n- <DropdownMenu\n- menuWidth=\"w-32\"\n- items={[\n- {\n- name: 'Edit Columns',\n- onClick: () => setModalOpen(true),\n- },\n- ]}\n- buttonContent={<BsThreeDotsVertical className=\"h-4\" />}\n- />\n- </div>\n- )}\n- </div>\n {contracts\n .slice(currentSlice, currentSlice + rowsPerSection)\n .map((contract) => {\n+ const metric = metricsByContractId[contract.id]\n+ const closeDate = contract.resolutionTime ?? contract.closeTime\n+ const date = closeDate ? new Date(closeDate) : null\n+ const isThisYear = date\n+ ? new Date().getFullYear() === date.getFullYear()\n+ : false\n+ const dateString = date\n+ ? date.toLocaleDateString('en-US', {\n+ month: '2-digit',\n+ day: '2-digit',\n+ year: isThisYear ? undefined : '2-digit',\n+ })\n+ : 'N/A'\n+ const resolvedAnswer =\n+ contract.mechanism === 'cpmm-multi-1'\n+ ? contract.answers.find(\n+ (a) =>\n+ a.id === contract.resolution ||\n+ (contract.resolutions?.[a.id] ?? 0) >= 99\n+ )\n+ : undefined\n+\n return (\n <Row\n key={contract.id + 'bets-table-row'}\n className={\n- 'border-ink-200 hover:bg-canvas-50 cursor-pointer border-b py-2'\n+ 'border-ink-200 hover:bg-canvas-50 cursor-pointer border-b py-3'\n }\n+ onClick={() => setNewExpandedId(contract.id)}\n >\n- <Col className={'w-full gap-3 sm:gap-2'}>\n- {/* Contract title*/}\n- <Row className={'justify-between'}>\n- <Link\n- href={contractPath(contract)}\n- className={clsx(\n- linkClass,\n- 'line-clamp-2 flex items-center pr-2 sm:pr-1'\n- )}\n- onClick={(e) => e.stopPropagation()}\n- >\n- <span className=\"mr-2 inline-flex items-center\">\n- <Avatar\n- avatarUrl={contract.creatorAvatarUrl}\n- size={'2xs'}\n- className={''}\n- />\n- </span>\n- <span className=\"text-ink-600 line-clamp-1\">\n+ <Col className=\"w-full px-2\">\n+ <Row className=\"justify-between\">\n+ {/* Left side - Question, probability and creator */}\n+ <Col className=\"w-3/4\">\n+ <Link\n+ href={contractPath(contract)}\n+ className={clsx(\n+ linkClass,\n+ 'line-clamp-2 text-lg font-medium'\n+ )}\n+ onClick={(e) => e.stopPropagation()}\n+ >\n {contract.token == 'CASH' && (\n- <span>\n- <Tooltip\n- text={SWEEPIES_MARKET_TOOLTIP}\n- className=\" relative mr-0.5 inline-flex h-[1em] w-[1.1em] items-baseline\"\n+ <Tooltip\n+ text={SWEEPIES_MARKET_TOOLTIP}\n+ className=\"relative mr-0.5 inline-flex h-[1em] w-[1.1em] items-baseline\"\n+ >\n+ <SweepiesCoin className=\"absolute inset-0 top-[0.2em]\" />\n+ </Tooltip>\n+ )}\n+ {contract.question}\n+ </Link>\n+ <Row className=\"mt-1 items-center\">\n+ {contract.isResolved ? (\n+ <span className=\"text-ink-800 mr-1 text-sm\">\n+ {contract.outcomeType === 'MULTIPLE_CHOICE' ? (\n+ Object.values(contract.resolutions ?? {}).filter(\n+ (r) => r > 1\n+ ).length > 1 || contract.resolution === 'MKT' ? (\n+ <span>MULTI</span>\n+ ) : contract.resolution === 'CANCEL' ? (\n+ <BinaryOutcomeLabel outcome=\"CANCEL\" />\n+ ) : resolvedAnswer ? (\n+ <MultiOutcomeLabel\n+ answer={resolvedAnswer}\n+ resolution={contract.resolution ?? ''}\n+ truncate=\"none\"\n+ answerClassName={\n+ 'font-bold text-base-400 !break-normal'\n+ }\n+ />\n+ ) : null\n+ ) : (\n+ <ContractStatusLabel contract={contract} />\n+ )}\n+ <span className=\"text-ink-500 ml-1 text-sm\">•</span>\n+ </span>\n+ ) : contract.outcomeType !== 'MULTIPLE_CHOICE' ? (\n+ <span className=\"text-ink-800 mr-1 text-sm\">\n+ <ContractStatusLabel contract={contract} />\n+ <span className=\"text-ink-500 ml-1 text-sm\">•</span>\n+ </span>\n+ ) : null}\n+ <Row className=\"items-center gap-1\">\n+ {/* <Avatar\n+ avatarUrl={contract.creatorAvatarUrl}\n+ username={contract.creatorUsername}\n+ size={'xs'}\n+ /> */}\n+ <span className=\"text-ink-500 text-sm\">\n+ {contract.creatorName}\n+ </span>\n+\n+ {sortOption.field === 'closeTime' ? (\n+ <span className=\"text-ink-500 text-sm\">\n+ • {dateString}\n+ </span>\n+ ) : sortOption.field === 'newest' ? (\n+ <span className=\"text-ink-500 text-sm\">\n+ •\n+ <RelativeTimestamp\n+ time={metric.lastBetTime}\n+ className=\"text-ink-500 text-sm\"\n+ shortened\n+ />\n+ </span>\n+ ) : null}\n+ </Row>\n+ </Row>\n+ </Col>\n+\n+ {/* Right side - Value and profit */}\n+ <Row className=\"items-start gap-2\">\n+ <Col className=\"text-right\">\n+ {/* Display different values based on sort selection */}\n+ {sortOption.field === 'position' ? (\n+ <span className=\"text-ink-900 text-lg font-medium\">\n+ {formatWithToken({\n+ amount: sum(Object.values(metric.totalShares)),\n+ token: contract.token === 'CASH' ? 'CASH' : 'M$',\n+ }).replace('-', '')}\n+ </span>\n+ ) : (\n+ <span className=\"text-ink-900 text-lg font-medium\">\n+ {formatWithToken({\n+ amount: metric.payout,\n+ token: contract.token === 'CASH' ? 'CASH' : 'M$',\n+ }).replace('-', '')}\n+ </span>\n+ )}\n+\n+ {sortOption.field === 'day' ? (\n+ <span\n+ className={clsx(\n+ 'text-sm font-medium',\n+ (metric.from?.day.profit ?? 0) > 0\n+ ? 'text-teal-500'\n+ : 'text-ink-500'\n+ )}\n+ >\n+ {formatWithToken({\n+ amount: metric.from?.day.profit ?? 0,\n+ token: contract.token === 'CASH' ? 'CASH' : 'M$',\n+ }).replace('-', '')}\n+ <span\n+ className={clsx(\n+ 'ml-1 rounded-full px-1.5 py-0.5 text-xs',\n+ (metric.from?.day.profitPercent ?? 0) > 0\n+ ? 'bg-teal-100 text-teal-800'\n+ : 'bg-canvas-50 text-ink-600'\n+ )}\n >\n- <SweepiesCoin className=\"absolute inset-0 top-[0.2em]\" />\n- </Tooltip>\n+ {(metric.from?.day.profitPercent ?? 0) > 0\n+ ? '+'\n+ : ''}\n+ {(metric.from?.day.profitPercent ?? 0).toFixed(0)}\n+ %\n+ </span>\n </span>\n+ ) : sortOption.field === 'week' ? (\n+ <span\n+ className={clsx(\n+ 'text-sm font-medium',\n+ (metric.from?.week.profit ?? 0) > 0\n+ ? 'text-teal-500'\n+ : 'text-ink-500'\n+ )}\n+ >\n+ {formatWithToken({\n+ amount: metric.from?.week.profit ?? 0,\n+ token: contract.token === 'CASH' ? 'CASH' : 'M$',\n+ }).replace('-', '')}\n+ <span\n+ className={clsx(\n+ 'ml-1 rounded-full px-1.5 py-0.5 text-xs',\n+ (metric.from?.week.profitPercent ?? 0) > 0\n+ ? 'bg-teal-100 text-teal-800'\n+ : 'bg-canvas-50 text-ink-600'\n+ )}\n+ >\n+ {(metric.from?.week.profitPercent ?? 0) > 0\n+ ? '+'\n+ : ''}\n+ {(metric.from?.week.profitPercent ?? 0).toFixed(\n+ 0\n+ )}\n+ %\n+ </span>\n+ </span>\n+ ) : (\n+ <span\n+ className={clsx(\n+ 'text-sm font-medium',\n+ metric.profit > 0\n+ ? 'text-teal-500'\n+ : 'text-ink-500'\n+ )}\n+ >\n+ {formatWithToken({\n+ amount: metric.profit,\n+ token: contract.token === 'CASH' ? 'CASH' : 'M$',\n+ }).replace('-', '')}\n+ <span\n+ className={clsx(\n+ 'ml-1 rounded-full px-1.5 py-0.5 text-xs',\n+ metric.profitPercent > 0\n+ ? 'bg-teal-100 text-teal-800'\n+ : 'bg-canvas-50 text-ink-600'\n+ )}\n+ >\n+ {metric.profitPercent > 0 ? '+' : ''}\n+ {metric.profitPercent.toFixed(0)}%\n+ </span>\n+ </span>\n )}\n- {contract.question}\n- </span>\n- <span className={' ml-1 flex min-w-[40px] justify-end'}>\n- <ContractStatusLabel\n- className={\n- '!text-ink-600 whitespace-nowrap font-semibold'\n- }\n- contract={contract}\n+ </Col>\n+ <Col className=\"flex items-start pt-1\">\n+ <ChevronDownIcon\n+ className={clsx(\n+ 'text-ink-500 h-4 w-4 transition-transform',\n+ expandedIds.includes(contract.id)\n+ ? 'rotate-180'\n+ : ''\n+ )}\n />\n- </span>\n- </Link>\n- <IconButton\n- size={'2xs'}\n- onClick={() => setNewExpandedId(contract.id)}\n- >\n- {expandedIds.includes(contract.id) ? (\n- <ChevronUpIcon className=\"h-4\" />\n- ) : (\n- <ChevronUpIcon className=\"h-4 rotate-180\" />\n- )}\n- </IconButton>\n+ </Col>\n+ </Row>\n </Row>\n \n- {/* Contract Metrics details*/}\n- <div\n- className={'grid-cols-15 grid w-full'}\n- onClick={() => setNewExpandedId(contract.id)}\n- >\n- {columnsToDisplay.map((c) => (\n- <div\n- className={clsx(getColSpan(c.span))}\n- key={c.header.sort + contract.id + 'row'}\n- >\n- {c.renderCell(contract)}\n- </div>\n- ))}\n- </div>\n- <Col className={'-mt-2 w-full'}>\n- {expandedIds.includes(contract.id) && (\n- <ExpandedBetRow\n- contract={contract}\n- user={user}\n- signedInUser={signedInUser}\n- contractMetric={metricsByContractId[contract.id]}\n- areYourBets={areYourBets}\n- />\n- )}\n- </Col>\n+ {/* Expanded View */}\n+ {expandedIds.includes(contract.id) && (\n+ <ExpandedBetRow\n+ contract={contract}\n+ user={user}\n+ signedInUser={signedInUser}\n+ contractMetric={metricsByContractId[contract.id]}\n+ areYourBets={areYourBets}\n+ />\n+ )}\n </Col>\n </Row>\n )\n })}\n@@ -769,41 +775,8 @@\n pageSize={rowsPerSection}\n totalItems={contracts.length}\n setPage={setPage}\n />\n- <Modal setOpen={setModalOpen} open={modalOpen}>\n- <div className={MODAL_CLASS}>\n- <span className=\"mb-4 text-lg font-bold\">\n- {enabledColumnsCount >= MAX_SHOWN_MOBILE\n- ? `Unselect ${\n- MAX_SHOWN_MOBILE + 1 - enabledColumnsCount\n- } column to select another`\n- : `Select ${\n- MAX_SHOWN_MOBILE - enabledColumnsCount\n- } more ${maybePluralize(\n- 'column',\n- MAX_SHOWN_MOBILE - enabledColumnsCount\n- )} (max 5)`}\n- </span>\n- {columns.map((col) => (\n- <div key={col.sort}>\n- <div className=\"mb-2 flex items-center\">\n- <input\n- type=\"checkbox\"\n- disabled={\n- !col.enabled &&\n- columns.filter((c) => c.enabled).length >= MAX_SHOWN_MOBILE\n- }\n- checked={col.enabled}\n- onChange={() => handleColumnToggle(col.sort)}\n- />\n- <label className=\"ml-2\">{col.label}</label>\n- </div>\n- </div>\n- ))}\n- <Button onClick={() => setModalOpen(false)}>Done</Button>\n- </div>\n- </Modal>\n </Col>\n )\n }\n \n@@ -873,69 +846,14 @@\n bets={bets}\n isYourBets={areYourBets}\n contractMetric={contractMetric}\n paginate\n+ defaultExpanded\n />\n </Col>\n )\n }\n \n-const NumberCell = (props: {\n- num: number\n- change?: boolean\n- isCashContract: boolean\n-}) => {\n- const { num, change, isCashContract } = props\n- const formattedNum =\n- num < 1000 && num > -1000\n- ? formatWithToken({ amount: num, token: isCashContract ? 'CASH' : 'M$' })\n- : isCashContract\n- ? SWEEPIES_MONIKER + shortFormatNumber(num)\n- : ENV_CONFIG.moneyMoniker + shortFormatNumber(num)\n- return (\n- <Row className=\"items-start justify-end \">\n- {change &&\n- formattedNum !==\n- formatWithToken({\n- amount: 0,\n- token: isCashContract ? 'CASH' : 'M$',\n- }) ? (\n- num > 0 ? (\n- <span className=\"text-teal-500\">{formattedNum}</span>\n- ) : (\n- <span className=\"text-ink-500\">{formattedNum}</span>\n- )\n- ) : (\n- <span className=\"text-ink-800\">{formattedNum}</span>\n- )}\n- </Row>\n- )\n-}\n-\n-const Header = (props: {\n- children: ReactNode\n- onClick?: () => void\n- up?: boolean\n- className?: string\n-}) => {\n- const { onClick, up, className, children } = props\n- return (\n- <Row\n- className={clsx(className, 'cursor-pointer items-center')}\n- onClick={onClick}\n- >\n- {up != undefined ? (\n- up ? (\n- <BiCaretUp className=\" h-4\" />\n- ) : (\n- <BiCaretDown className=\" h-4\" />\n- )\n- ) : null}\n- <span>{children}</span>\n- </Row>\n- )\n-}\n-\n export function LoadingMetricRow() {\n return (\n <div className=\"animate-pulse py-4\">\n <Row className=\"mb-2 items-center gap-2\">\n"
|
|
1642
|
-
},
|
|
1643
|
-
{
|
|
1644
|
-
"path": "web/components/contract/text-color.ts",
|
|
1645
|
-
"status": "modified",
|
|
1646
|
-
"diff": "Index: web/components/contract/text-color.ts\n===================================================================\n--- web/components/contract/text-color.ts\t80a4d94 (parent)\n+++ web/components/contract/text-color.ts\t702a5b8 (commit)\n@@ -13,9 +13,12 @@\n }) {\n const { resolution } = contract\n \n if (resolution) {\n- return OUTCOME_TO_COLOR_TEXT[resolution as resolution] ?? 'text-primary-200'\n+ return (\n+ OUTCOME_TO_COLOR_TEXT[resolution as resolution] ??\n+ 'text-blue-600 dark:text-blue-200'\n+ )\n }\n if ((contract.closeTime ?? Infinity) < Date.now()) {\n return 'text-ink-600'\n }\n"
|
|
1647
|
-
},
|
|
1648
|
-
{
|
|
1649
|
-
"path": "web/components/widgets/pagination.tsx",
|
|
1650
|
-
"status": "modified",
|
|
1651
|
-
"diff": "Index: web/components/widgets/pagination.tsx\n===================================================================\n--- web/components/widgets/pagination.tsx\t80a4d94 (parent)\n+++ web/components/widgets/pagination.tsx\t702a5b8 (commit)\n@@ -88,9 +88,12 @@\n aria-label=\"Pagination\"\n >\n <Row className=\"mx-auto gap-4\">\n <PaginationArrow\n- onClick={() => onClick(page - 1)}\n+ onClick={(e) => {\n+ e?.stopPropagation()\n+ onClick(page - 1)\n+ }}\n disabled={page <= 0}\n nextOrPrev=\"prev\"\n />\n <Row className=\"gap-2\">\n@@ -107,9 +110,12 @@\n />\n ))}\n </Row>\n <PaginationArrow\n- onClick={() => onClick(page + 1)}\n+ onClick={(e) => {\n+ e?.stopPropagation()\n+ onClick(page + 1)\n+ }}\n disabled={page >= maxPage}\n nextOrPrev=\"next\"\n />\n </Row>\n@@ -117,9 +123,9 @@\n )\n }\n \n export function PaginationArrow(props: {\n- onClick: () => void\n+ onClick: (e?: React.MouseEvent) => void\n disabled: boolean\n nextOrPrev: 'next' | 'prev'\n }) {\n const { onClick, disabled, nextOrPrev } = props\n@@ -153,9 +159,12 @@\n return <div className=\"text-ink-400 select-none\">{PAGE_ELLIPSES}</div>\n }\n return (\n <button\n- onClick={() => setPage(pageNumber)}\n+ onClick={(e) => {\n+ e.stopPropagation()\n+ setPage(pageNumber)\n+ }}\n className={clsx(\n 'select-none rounded-lg px-2',\n page === pageNumber\n ? 'bg-primary-100 text-primary-700'\n"
|
|
1652
|
-
},
|
|
1653
|
-
{
|
|
1654
|
-
"path": "web/pages/[username]/index.tsx",
|
|
1655
|
-
"status": "modified",
|
|
1656
|
-
"diff": "Index: web/pages/[username]/index.tsx\n===================================================================\n--- web/pages/[username]/index.tsx\t80a4d94 (parent)\n+++ web/pages/[username]/index.tsx\t702a5b8 (commit)\n@@ -312,19 +312,12 @@\n )}\n \n <Row className={'items-center gap-1 sm:gap-2'}>\n {isCurrentUser ? (\n- <Row className=\"my-2 hidden items-center gap-2 px-4 sm:flex\">\n- <AddFundsButton\n- userId={user.id}\n- className=\"whitespace-nowra w-full lg:hidden\"\n- />\n- <RedeemSweepsButtons\n- user={user}\n- className=\"shrink-0\"\n- redeemableCash={user.cashBalance}\n- />\n- </Row>\n+ <AddFundsButton\n+ userId={user.id}\n+ className=\"mr-2 w-full whitespace-nowrap px-8 lg:hidden\"\n+ />\n ) : (\n <>\n <SendMessageButton toUser={user} currentUser={currentUser} />\n <FollowButton userId={user.id} />\n@@ -351,19 +344,13 @@\n </Col>\n )}\n \n {isCurrentUser && (\n- <Row className=\"my-2 w-full items-center gap-2 px-4 sm:hidden\">\n- <AddFundsButton\n- userId={user.id}\n- className=\"w-1/2 whitespace-nowrap\"\n- />\n- <RedeemSweepsButtons\n- user={user}\n- className=\"w-1/2\"\n- redeemableCash={user.cashBalance}\n- />\n- </Row>\n+ <RedeemSweepsButtons\n+ user={user}\n+ className=\"m-2 w-48\"\n+ redeemableCash={user.cashBalance}\n+ />\n )}\n \n <Col className=\"mx-4\">\n <QueryUncontrolledTabs\n"
|
|
1657
|
-
}
|
|
1658
|
-
]
|
|
1659
|
-
},
|
|
1660
|
-
{
|
|
1661
|
-
"id": "slim-mcp-endpoints",
|
|
1662
|
-
"sha": "862d30eaefa9392cf0167293bd2b90606ee0fd36",
|
|
1663
|
-
"parentSha": "e8feb41e1b933fa9fcb8baa132b63399708c0643",
|
|
1664
|
-
"spec": "Implement slimmer market payloads for Model Context Protocol (MCP) endpoints and supporting utilities.\n\nRequirements:\n\n1) Add an ultra-lightweight market shape and transformer\n- In common/src/api/market-types.ts:\n - Introduce UltraLiteMarket type containing: id, url, creatorId, creatorUsername, creatorName, createdTime (ISO string), question, outcomeType, probability? (number), liquidityTier? (string label), answers? (array of { id, text, probability }), value? (for pseudo-numeric), volume (rounded integer), volume24Hours (rounded integer), isResolved, resolution?, resolutionTime? (ISO string), uniqueBettorCount.\n - Implement toUltraLiteMarket(liteMarket: LiteMarket): UltraLiteMarket that derives liquidityTier by mapping the tier index from getTierIndexFromLiquidityAndAnswers(totalLiquidity, answers?.length ?? 0) to labels: 0 = \"low\", 1 = \"medium\", 2 = \"high\", >2 = \"very high\"; return 'n/a' if tier is undefined. Convert createdTime and resolutionTime to ISO strings and round volume fields.\n\n2) Change toLiteMarket signature to accept options\n- In common/src/api/market-types.ts, change toLiteMarket(contract, includeLiteAnswers?: boolean) to toLiteMarket(contract, props: { includeLiteAnswers?: boolean } = { includeLiteAnswers: false }). Preserve existing behavior of including minimal answer info only when includeLiteAnswers is true.\n- Update all call sites to pass an options object instead of a boolean. Example: toLiteMarket(contract, { includeLiteAnswers: true }) and toLiteMarket(contract) for default behavior.\n\n3) Slim and format MCP tool responses\n- In backend/api/src/mcp.ts:\n - For the search-markets tool, request lite markets with minimal answers, convert each LiteMarket to UltraLiteMarket, then format probability and answers’ probabilities as human-readable percent strings (e.g., \"57% chance\"). Return the JSON stringified array of these slimmed maps.\n - For the get-market tool, fetch the contract by id using a direct PG client and getContract utility, convert to LiteMarket with lite answers included, then to UltraLiteMarket. Add a description string field (ensure rich text descriptions are converted to plain text). Format probabilities as percent strings as above. Return the JSON stringified single market map.\n - Ensure errors are surfaced as MCP tool execution errors with a useful message.\n\n4) Optimize get-market fetching and attach answers\n- In backend/api/src/get-market.ts:\n - Query contracts and answers in a single multi-statement SQL call using contractColumnsToSelect and where clause by id or slug.\n - Convert the contract row using convertContract, and answers using convertAnswer; attach answers if the contract type supports them.\n - Return toLiteMarket(...) when lite flag is true, else toFullMarket(...).\n\n5) Ensure search endpoints use new toLiteMarket signature\n- In backend/api/src/search-contracts.ts: pass an options object to toLiteMarket(c, { includeLiteAnswers }) instead of a boolean.\n\n6) Tier/bonus consistency for multi-answer markets\n- In common/src/tier.ts: modify getTierIndexFromLiquidityAndAnswers so that if numAnswers is falsy, it falls back to getTierIndexFromLiquidity(liquidity) instead of dividing by zero.\n- In common/src/economy.ts: update getUniqueBettorBonusAmount to always use getTierIndexFromLiquidityAndAnswers(liquidity, numAnswers), relying on the fallback behavior above; remove any separate single-answer branch.\n\n7) Imports and utilities\n- Ensure imports reflect new usage: use APIError, first, createSupabaseDirectClient, contractColumnsToSelect, convertAnswer, convertContract, removeUndefinedProps, and richTextToString where appropriate.\n\nObservable outcomes:\n- MCP search-markets and get-market tools return slimmed JSON with percent-formatted probability fields and minimal answer data, plus a string description for get-market.\n- get-market endpoint fetches contract and answers in one round-trip and includes answers in the contract object where appropriate.\n- All toLiteMarket callers compile by using the new options object signature.\n- Tier-based labels and unique bettor bonus calculations properly handle markets with and without answers.\n",
|
|
1665
|
-
"prompt": "We want to significantly reduce the amount of data returned by our MCP tools for markets while keeping them useful. Create an ultra-light market representation and use it for MCP search and single-market tools. Show probabilities as human-readable percentages, include minimal answer info with their chances, and provide a plain-text description for single-market responses. Also make the lite market transformer accept an options object and ensure search endpoints and other callers use the new signature. Optimize market fetching to grab the contract and its answers together and attach answers when relevant. Finally, make the liquidity tier and unique-bettor bonus logic consistent for both single- and multi-answer markets.",
|
|
1666
|
-
"supplementalFiles": [
|
|
1667
|
-
"backend/shared/src/utils.ts",
|
|
1668
|
-
"common/src/supabase/contracts.ts",
|
|
1669
|
-
"common/src/api/schema.ts"
|
|
1670
|
-
],
|
|
1671
|
-
"fileDiffs": [
|
|
1672
|
-
{
|
|
1673
|
-
"path": "backend/api/src/get-market.ts",
|
|
1674
|
-
"status": "modified",
|
|
1675
|
-
"diff": "Index: backend/api/src/get-market.ts\n===================================================================\n--- backend/api/src/get-market.ts\te8feb41 (parent)\n+++ backend/api/src/get-market.ts\t862d30e (commit)\n@@ -1,19 +1,30 @@\n-import { createSupabaseDirectClient } from 'shared/supabase/init'\n-import { APIError } from 'common/api/utils'\n import { toFullMarket, toLiteMarket } from 'common/api/market-types'\n-import { convertContract } from 'common/supabase/contracts'\n+import { APIError } from 'common/api/utils'\n+import { convertAnswer, convertContract } from 'common/supabase/contracts'\n+import { first } from 'lodash'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { contractColumnsToSelect } from 'shared/utils'\n \n export const getMarket = async (\n props: ({ id: string } | { slug: string }) & { lite?: boolean }\n ) => {\n const pg = createSupabaseDirectClient()\n- const contract = await pg.oneOrNone(\n- `select * from contracts\n- where ${'id' in props ? 'id' : 'slug'} = $1`,\n- ['id' in props ? props.id : props.slug],\n- (r) => (r ? convertContract(r) : null)\n+ const whereClause = 'id' in props ? 'id' : 'slug'\n+ const whereValue = 'id' in props ? props.id : props.slug\n+\n+ const res = await pg.multi(\n+ `select ${contractColumnsToSelect} from contracts where ${whereClause} = $1 limit 1;\n+ select * from answers where contract_id = (select id from contracts where ${whereClause} = $1) order by index;`,\n+ [whereValue]\n )\n+\n+ const contract = first(res[0].map(convertContract))\n if (!contract) throw new APIError(404, 'Contract not found')\n \n+ const answers = res[1].map(convertAnswer)\n+ if (contract && 'answers' in contract) {\n+ contract.answers = answers\n+ }\n+\n return props.lite ? toLiteMarket(contract) : toFullMarket(contract)\n }\n"
|
|
1676
|
-
},
|
|
1677
|
-
{
|
|
1678
|
-
"path": "backend/api/src/mcp.ts",
|
|
1679
|
-
"status": "modified",
|
|
1680
|
-
"diff": "Index: backend/api/src/mcp.ts\n===================================================================\n--- backend/api/src/mcp.ts\te8feb41 (parent)\n+++ backend/api/src/mcp.ts\t862d30e (commit)\n@@ -6,15 +6,24 @@\n ErrorCode,\n ListToolsRequestSchema,\n McpError,\n } from '@modelcontextprotocol/sdk/types.js'\n+import {\n+ LiteMarket,\n+ toLiteMarket,\n+ toUltraLiteMarket,\n+ UltraLiteMarket,\n+} from 'common/api/market-types'\n import { API } from 'common/api/schema'\n+import { APIError } from 'common/api/utils'\n import { NON_POINTS_BETS_LIMIT } from 'common/supabase/bets'\n+import { removeUndefinedProps } from 'common/util/object'\n+import { richTextToString } from 'common/util/parse'\n import { Request, Response } from 'express'\n-import { log, metrics } from 'shared/utils'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { getContract, log, metrics } from 'shared/utils'\n import { z } from 'zod'\n import { getBets } from './get-bets'\n-import { getMarket } from './get-market'\n import { getUser } from './get-user'\n import { searchMarketsLite } from './search-contracts'\n import { searchUsers } from './search-users'\n \n@@ -219,8 +228,22 @@\n // Handle tool execution\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params\n metrics.inc('mcp/request_count', { name })\n+\n+ const chancifyUltraLiteMarket = (market: UltraLiteMarket) => {\n+ return removeUndefinedProps({\n+ ...market,\n+ probability: market.probability\n+ ? `${Math.round(market.probability * 100)}% chance`\n+ : undefined,\n+ answers: market.answers?.map((answer) => ({\n+ text: answer.text,\n+ probability: `${Math.round(answer.probability * 100)}% chance`,\n+ })),\n+ })\n+ }\n+\n try {\n switch (name) {\n case 'search-markets': {\n const params = API['search-markets'].props.parse(args)\n@@ -240,18 +263,22 @@\n includeLiteAnswers: true,\n }\n \n try {\n- const markets = await searchMarketsLite(\n+ const markets = (await searchMarketsLite(\n searchParams,\n undefined, // auth not required for this endpoint\n {} as Request // minimal request object since it's not used\n- )\n+ )) as LiteMarket[]\n+\n+ const marketsWithProbabilities = markets\n+ .map(toUltraLiteMarket)\n+ .map(chancifyUltraLiteMarket)\n return {\n content: [\n {\n type: 'text',\n- text: JSON.stringify(markets, null, 2),\n+ text: JSON.stringify(marketsWithProbabilities, null, 2),\n },\n ],\n }\n } catch (error: any) {\n@@ -265,14 +292,26 @@\n case 'get-market': {\n const { id } = API['market/:id'].props.parse(args)\n \n try {\n- const market = await getMarket({ id })\n+ const pg = createSupabaseDirectClient()\n+ const contract = await getContract(pg, id)\n+ if (!contract) throw new APIError(404, 'Contract not found')\n+ const market = toLiteMarket(contract, {\n+ includeLiteAnswers: true,\n+ })\n+ const marketWithProbability = removeUndefinedProps({\n+ ...chancifyUltraLiteMarket(toUltraLiteMarket(market)),\n+ description:\n+ typeof contract.description === 'string'\n+ ? contract.description\n+ : richTextToString(contract.description),\n+ })\n return {\n content: [\n {\n type: 'text',\n- text: JSON.stringify(market, null, 2),\n+ text: JSON.stringify(marketWithProbability, null, 2),\n },\n ],\n }\n } catch (error: any) {\n"
|
|
1681
|
-
},
|
|
1682
|
-
{
|
|
1683
|
-
"path": "backend/api/src/search-contracts.ts",
|
|
1684
|
-
"status": "modified",
|
|
1685
|
-
"diff": "Index: backend/api/src/search-contracts.ts\n===================================================================\n--- backend/api/src/search-contracts.ts\te8feb41 (parent)\n+++ backend/api/src/search-contracts.ts\t862d30e (commit)\n@@ -24,9 +24,9 @@\n auth\n ) => {\n const { includeLiteAnswers } = props\n const contracts = await search(props, auth?.uid)\n- return contracts.map((c) => toLiteMarket(c, includeLiteAnswers))\n+ return contracts.map((c) => toLiteMarket(c, { includeLiteAnswers }))\n }\n \n export const searchMarketsFull: APIHandler<'search-markets-full'> = async (\n props,\n"
|
|
1686
|
-
},
|
|
1687
|
-
{
|
|
1688
|
-
"path": "common/src/api/market-types.ts",
|
|
1689
|
-
"status": "modified",
|
|
1690
|
-
"diff": "Index: common/src/api/market-types.ts\n===================================================================\n--- common/src/api/market-types.ts\te8feb41 (parent)\n+++ common/src/api/market-types.ts\t862d30e (commit)\n@@ -12,9 +12,12 @@\n import { DOMAIN } from 'common/envs/constants'\n import { MAX_ID_LENGTH } from 'common/group'\n import { MAX_MULTI_NUMERIC_ANSWERS } from 'common/multi-numeric'\n import { getMappedValue } from 'common/pseudo-numeric'\n-import { liquidityTiers } from 'common/tier'\n+import {\n+ getTierIndexFromLiquidityAndAnswers,\n+ liquidityTiers,\n+} from 'common/tier'\n import { removeUndefinedProps } from 'common/util/object'\n import { richTextToString } from 'common/util/parse'\n import { randomStringRegex } from 'common/util/random'\n import { z } from 'zod'\n@@ -42,8 +45,13 @@\n pool?: { [outcome: string]: number }\n probability?: number\n p?: number\n totalLiquidity?: number\n+ answers?: {\n+ id: string\n+ text: string\n+ probability: number\n+ }[]\n // For pseudo-numeric\n value?: number\n min?: number\n max?: number\n@@ -93,9 +101,9 @@\n }\n \n export function toLiteMarket(\n contract: Contract,\n- includeLiteAnswers?: boolean\n+ props: { includeLiteAnswers?: boolean } = { includeLiteAnswers: false }\n ): LiteMarket {\n const {\n id,\n creatorId,\n@@ -125,8 +133,9 @@\n isLove,\n token,\n siblingContractId,\n } = contract\n+ const { includeLiteAnswers } = props\n \n const { p, totalLiquidity } = contract as any\n \n const probability =\n@@ -247,8 +256,99 @@\n : richTextToString(description),\n }\n }\n \n+export type UltraLiteMarket = {\n+ // Unique identifier for this market\n+ id: string\n+\n+ // Attributes about the creator\n+ creatorId: string\n+ creatorUsername: string\n+ creatorName: string\n+ createdTime: string\n+\n+ question: string\n+ url: string\n+ outcomeType: string\n+\n+ probability?: number\n+ liquidityTier?: string\n+ answers?: {\n+ id: string\n+ text: string\n+ probability: number\n+ }[]\n+ // For pseudo-numeric\n+ value?: number\n+\n+ volume: number\n+ volume24Hours: number\n+\n+ isResolved: boolean\n+ resolution?: string\n+ resolutionTime?: string\n+\n+ uniqueBettorCount: number\n+}\n+export function toUltraLiteMarket(liteMarket: LiteMarket): UltraLiteMarket {\n+ const {\n+ id,\n+ creatorId,\n+ creatorName,\n+ creatorUsername,\n+ answers,\n+ question,\n+ probability,\n+ totalLiquidity,\n+ outcomeType,\n+ volume,\n+ volume24Hours,\n+ isResolved,\n+ resolution,\n+ resolutionTime,\n+ uniqueBettorCount,\n+ url,\n+ createdTime,\n+ } = liteMarket\n+ const tier = totalLiquidity\n+ ? getTierIndexFromLiquidityAndAnswers(totalLiquidity, answers?.length ?? 0)\n+ : undefined\n+ let liquidityTier = 'n/a'\n+ if (tier !== undefined) {\n+ if (tier === 0) {\n+ liquidityTier = 'low'\n+ } else if (tier === 1) {\n+ liquidityTier = 'medium'\n+ } else if (tier === 2) {\n+ liquidityTier = 'high'\n+ } else if (tier > 2) {\n+ liquidityTier = 'very high'\n+ }\n+ }\n+ return {\n+ id,\n+ url,\n+ creatorId,\n+ creatorName,\n+ creatorUsername,\n+ answers,\n+ question,\n+ probability,\n+ liquidityTier,\n+ outcomeType,\n+ volume: Math.round(volume),\n+ volume24Hours: Math.round(volume24Hours),\n+ isResolved,\n+ resolution,\n+ resolutionTime: resolutionTime\n+ ? new Date(resolutionTime).toISOString()\n+ : undefined,\n+ createdTime: new Date(createdTime).toISOString(),\n+ uniqueBettorCount,\n+ }\n+}\n+\n function augmentAnswerWithProbability(\n contract: MultiContract,\n answer: Answer\n ): ApiAnswer {\n"
|
|
1691
|
-
},
|
|
1692
|
-
{
|
|
1693
|
-
"path": "common/src/economy.ts",
|
|
1694
|
-
"status": "modified",
|
|
1695
|
-
"diff": "Index: common/src/economy.ts\n===================================================================\n--- common/src/economy.ts\te8feb41 (parent)\n+++ common/src/economy.ts\t862d30e (commit)\n@@ -1,10 +1,10 @@\n import { OutcomeType } from 'common/contract'\n import {\n answerCostTiers,\n- liquidityTiers,\n getTierIndexFromLiquidity,\n getTierIndexFromLiquidityAndAnswers,\n+ liquidityTiers,\n } from './tier'\n \n export const DEFAULT_CASH_ANTE = 50\n export const MINIMUM_BOUNTY = 1000\n@@ -49,11 +49,8 @@\n export const getUniqueBettorBonusAmount = (\n liquidity: number,\n numAnswers: number\n ) => {\n- if (!numAnswers) {\n- return uniqueBettorBonusAmounts[getTierIndexFromLiquidity(liquidity)]\n- }\n return uniqueBettorBonusAmounts[\n getTierIndexFromLiquidityAndAnswers(liquidity, numAnswers)\n ]\n }\n"
|
|
1696
|
-
},
|
|
1697
|
-
{
|
|
1698
|
-
"path": "common/src/tier.ts",
|
|
1699
|
-
"status": "modified",
|
|
1700
|
-
"diff": "Index: common/src/tier.ts\n===================================================================\n--- common/src/tier.ts\te8feb41 (parent)\n+++ common/src/tier.ts\t862d30e (commit)\n@@ -24,8 +24,11 @@\n export function getTierIndexFromLiquidityAndAnswers(\n liquidity: number,\n numAnswers: number\n ): number {\n+ if (!numAnswers) {\n+ return getTierIndexFromLiquidity(liquidity)\n+ }\n const liquidityPerAnswer = liquidity / numAnswers\n for (\n let tierIndex = answerCostTiers.length - 1;\n tierIndex >= 0;\n"
|
|
1701
|
-
}
|
|
1702
|
-
]
|
|
1703
|
-
},
|
|
1704
|
-
{
|
|
1705
|
-
"id": "unify-ai-prompts",
|
|
1706
|
-
"sha": "e922ecefbc977826b7b16c1bc760e8da44387d65",
|
|
1707
|
-
"parentSha": "4e46d468c063596742b3db5140e276c7e8df9bab",
|
|
1708
|
-
"spec": "Implement a unified AI prompting interface and migrate existing endpoints to use it.\n\nScope\n- Backend shared helpers (new abstraction and OpenAI utils update)\n- Backend API endpoints that generate AI-based content\n- Scheduler job using AI summarization\n- Shared helper for AI-driven close-date inference\n- Client components for midpoint regeneration guard\n- Dependency upgrade for OpenAI SDK\n\nRequirements\n1) Add a provider-agnostic AI prompt facade\n- Create backend/shared/src/helpers/prompt-ai.ts exporting:\n - aiModels: a merged constant of model identifiers from existing provider helpers (Claude, Gemini) plus OpenAI model keys, e.g. { gpt5, gpt5mini, sonnet3, sonnet4, haiku, flash, ... }.\n - promptAI(prompt, options): a single function that routes to the appropriate provider based on model string prefix, supports:\n - system (optional system prompt)\n - webSearch (boolean)\n - reasoning (optional effort: low | medium | high)\n - parseAsJson flag: if true, parse the returned string to JSON using existing robust parser (parseAIResponseAsJson from gemini.ts); else return raw string.\n - Provider selection rule: model name starting with \"claude\" -> Claude; starting with \"gemini\" -> Gemini; otherwise -> OpenAI.\n\n2) Upgrade OpenAI SDK and move to Responses API\n- Bump backend/api/package.json dependency \"openai\" to ^5.12.2.\n- Update backend/shared/src/helpers/openai-utils.ts to:\n - Export models map for OpenAI: { gpt5: 'gpt-5', gpt5mini: 'gpt-5-mini' }.\n - Implement promptOpenAI using OpenAI.responses.create with:\n - input: user prompt\n - instructions: system (if provided)\n - tools/tool_choice when webSearch is true (use the web_search_preview_2025_03_11 tool with high search context size)\n - reasoning effort support\n - text.verbosity low\n - Return output_text as the string result and log it.\n - Expose helper to do web-search+parse JSON via existing parser (or rely on promptAI with parseAsJson: true and webSearch: true).\n\n3) Migrate AI endpoints and helpers to promptAI\nReplace provider-specific calls with promptAI across:\n- backend/api/src/generate-ai-answers.ts\n - Use promptAI with aiModels.gpt5, webSearch: true, parseAsJson: true for answers/addAnswersMode JSON.\n- backend/api/src/generate-ai-date-ranges.ts\n - Use promptAI with aiModels.gpt5mini for bucket and threshold ranges, parseAsJson: true.\n - Use promptAI with aiModels.sonnet3 and parseAsJson: true for midpoint regeneration.\n- backend/api/src/generate-ai-description.ts\n - Use promptAI with aiModels.gpt5 and webSearch: true for markdown description.\n- backend/api/src/generate-ai-market-suggestions.ts\n - Use promptAI with aiModels.gpt5, webSearch: true, parseAsJson: true for AIGeneratedMarket[] results.\n- backend/api/src/generate-ai-numeric-ranges.ts\n - Use promptAI with aiModels.gpt5mini, parseAsJson: true for thresholds and buckets.\n - For midpoint regeneration, use aiModels.gpt5mini with parseAsJson: true. Throw APIError 400 if all provided ranges are empty strings.\n- backend/api/src/generate-concise-title.ts\n - Use promptAI with aiModels.sonnet3 and system prompt for concise title generation.\n- backend/api/src/get-best-comments.ts\n - Replace Gemini usage with promptAI using aiModels.flash for both batched and final selection prompts.\n- backend/api/src/get-related-markets.ts\n - Replace Gemini call and manual parsing with promptAI using aiModels.flash and parseAsJson: true to filter IDs.\n- backend/api/src/infer-numeric-unit.ts\n - Replace Gemini call with promptAI using aiModels.flash, system prompt, parseAsJson: true to get { unit }.\n- backend/api/src/on-create-comment-on-contract.ts\n - Replace Claude/Gemini calls with promptAI:\n - Clarification detection: aiModels.gpt5, parseAsJson: true to get { isClarification, description }.\n - Needs-response check: aiModels.gpt5mini, parseAsJson: true to get { needsResponse, reason }.\n- backend/scheduler/src/jobs/auto-award-bounty.ts\n - Replace OpenAI call with promptAI using aiModels.gpt5 for comment summary.\n- backend/shared/src/helpers/ai-close-date.ts\n - Replace Gemini with promptAI using aiModels.sonnet3 to return a concise date string.\n- backend/shared/src/supabase/contracts.ts\n - Replace Gemini parse flow with promptAI using aiModels.flash, system prompt, parseAsJson true; return boolean for isSelfReferential with safe default.\n\n4) Preserve API behavior and telemetry\n- Preserve existing API signature and return types for each endpoint.\n- Retain analytics via track() calls and rate limiting via rateLimitByUser where already present.\n- Log relevant AI responses for debugging as currently done.\n\n5) Frontend guardrail adjustments\n- In web/components/new-contract/multi-numeric-date-section.tsx and multi-numeric-range-section.tsx:\n - Before calling midpoint regeneration endpoints, early return (and optionally console.log) if all answers are empty strings.\n\n6) Miscellaneous\n- Add native/.gitignore entry for Manifold.app/.\n- Ensure yarn.lock reflects the OpenAI upgrade.\n\nAcceptance Criteria\n- All replaced endpoints compile and pass type checks using promptAI.\n- Structured responses are parsed as JSON when requested and validated by existing server-side logic; errors are surfaced via APIError as before.\n- OpenAI calls use Responses API and optional web search tool; Claude/Gemini usage remains supported via promptAI where selected.\n- Existing telemetry and rate limiting remain in effect.\n- Frontend midpoint regeneration does not call server when all ranges are empty.\n- No regressions in other AI helpers; Claude/Gemini helper files remain intact and continue to expose their APIs.\n",
|
|
1709
|
-
"prompt": "Unify our AI calling pattern behind a single provider-agnostic interface and upgrade OpenAI usage. Create a shared prompt function that selects OpenAI, Claude, or Gemini by model, can optionally run web search, supports reasoning, and can return either raw text or parsed JSON. Migrate the AI endpoints and shared helpers that generate descriptions, answers, suggestions, ranges, midpoints, units, close dates, best comments, related markets, and bounty summaries to use this interface while preserving their inputs/outputs and telemetry. Upgrade the OpenAI SDK to its latest major version and use the Responses API, including web search where needed. Add light client-side guardrails to skip midpoint regeneration when no ranges are provided. Keep existing rate limits, tracking, and error handling intact.",
|
|
1710
|
-
"supplementalFiles": [
|
|
1711
|
-
"backend/shared/src/helpers/claude.ts",
|
|
1712
|
-
"backend/shared/src/helpers/gemini.ts",
|
|
1713
|
-
"backend/shared/src/helpers/perplexity.ts",
|
|
1714
|
-
"backend/api/src/helpers/endpoint.ts",
|
|
1715
|
-
"backend/api/src/helpers/rate-limit.ts",
|
|
1716
|
-
"backend/shared/src/analytics.ts",
|
|
1717
|
-
"backend/shared/src/helpers/embeddings.ts",
|
|
1718
|
-
"common/src/ai-creation-prompts.ts",
|
|
1719
|
-
"common/src/api/schema.ts"
|
|
1720
|
-
],
|
|
1721
|
-
"fileDiffs": [
|
|
1722
|
-
{
|
|
1723
|
-
"path": "backend/api/package.json",
|
|
1724
|
-
"status": "modified",
|
|
1725
|
-
"diff": "Index: backend/api/package.json\n===================================================================\n--- backend/api/package.json\t4e46d46 (parent)\n+++ backend/api/package.json\te922ece (commit)\n@@ -54,9 +54,9 @@\n \"link-preview-js\": \"3.0.4\",\n \"lodash\": \"4.17.21\",\n \"mailgun-js\": \"0.22.0\",\n \"marked\": \"4.1.1\",\n- \"openai\": \"4.92.1\",\n+ \"openai\": \"5.12.2\",\n \"pg-promise\": \"11.10.0\",\n \"sharp\": \"0.32.6\",\n \"string-similarity\": \"4.0.4\",\n \"stripe\": \"8.194.0\",\n"
|
|
1726
|
-
},
|
|
1727
|
-
{
|
|
1728
|
-
"path": "backend/api/src/generate-ai-answers.ts",
|
|
1729
|
-
"status": "modified",
|
|
1730
|
-
"diff": "Index: backend/api/src/generate-ai-answers.ts\n===================================================================\n--- backend/api/src/generate-ai-answers.ts\t4e46d46 (parent)\n+++ backend/api/src/generate-ai-answers.ts\te922ece (commit)\n@@ -1,14 +1,14 @@\n-import { APIError, APIHandler } from './helpers/endpoint'\n-import { track } from 'shared/analytics'\n-import { promptOpenAIWebSearchParseJson } from 'shared/helpers/openai-utils'\n import {\n addAnswersModeDescription,\n multiChoiceOutcomeTypeDescriptions,\n } from 'common/ai-creation-prompts'\n+import { HOUR_MS } from 'common/util/time'\n+import { track } from 'shared/analytics'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { log } from 'shared/utils'\n+import { APIError, APIHandler } from './helpers/endpoint'\n import { rateLimitByUser } from './helpers/rate-limit'\n-import { HOUR_MS } from 'common/util/time'\n \n export const generateAIAnswers: APIHandler<'generate-ai-answers'> =\n rateLimitByUser(\n async (props, auth) => {\n@@ -51,12 +51,16 @@\n \"addAnswersMode\": \"ANYONE\"\n }\n `\n \n- const result = await promptOpenAIWebSearchParseJson<{\n+ const result = await promptAI<{\n answers: string[]\n addAnswersMode: 'DISABLED' | 'ONLY_CREATOR' | 'ANYONE'\n- }>(userPrompt)\n+ }>(userPrompt, {\n+ model: aiModels.gpt5,\n+ webSearch: true,\n+ parseAsJson: true,\n+ })\n log('GPT-4.1 response', result)\n \n track(auth.uid, 'generate-ai-answers', {\n question: question.substring(0, 100),\n"
|
|
1731
|
-
},
|
|
1732
|
-
{
|
|
1733
|
-
"path": "backend/api/src/generate-ai-date-ranges.ts",
|
|
1734
|
-
"status": "modified",
|
|
1735
|
-
"diff": "Index: backend/api/src/generate-ai-date-ranges.ts\n===================================================================\n--- backend/api/src/generate-ai-date-ranges.ts\t4e46d46 (parent)\n+++ backend/api/src/generate-ai-date-ranges.ts\te922ece (commit)\n@@ -1,7 +1,7 @@\n import { HOUR_MS } from 'common/util/time'\n import { track } from 'shared/analytics'\n-import { models, promptClaudeParsingJson } from 'shared/helpers/claude'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { log } from 'shared/utils'\n import {\n assertMidpointsAreAscending,\n assertMidpointsAreUnique,\n@@ -125,15 +125,17 @@\n const thresholdSystemPrompt = baseDateSystemPrompt('thresholds')\n \n // Generate both bucket and threshold ranges in parallel\n const [buckets, thresholds] = await Promise.all([\n- promptClaudeParsingJson<DateRangeResponse>(prompt, {\n- model: models.sonnet3,\n+ promptAI<DateRangeResponse>(prompt, {\n+ model: aiModels.gpt5mini,\n system: bucketSystemPrompt,\n+ parseAsJson: true,\n }),\n- promptClaudeParsingJson<DateRangeResponse>(prompt, {\n- model: models.sonnet3,\n+ promptAI<DateRangeResponse>(prompt, {\n+ model: aiModels.gpt5mini,\n system: thresholdSystemPrompt,\n+ parseAsJson: true,\n }),\n ])\n \n // Process bucket results\n@@ -188,10 +190,11 @@\n Return ONLY an array of midpoint dates, one for each range, without any other text or formatting.\n DO NOT return the answer array, JUST THE MIDPOINTS.\n `\n \n- const result = await promptClaudeParsingJson<string[]>(prompt, {\n- model: models.sonnet3,\n+ const result = await promptAI<string[]>(prompt, {\n+ model: aiModels.sonnet3,\n+ parseAsJson: true,\n })\n log('claudeResponse', result)\n \n track(auth.uid, 'regenerate-date-midpoints', {\n"
|
|
1736
|
-
},
|
|
1737
|
-
{
|
|
1738
|
-
"path": "backend/api/src/generate-ai-description.ts",
|
|
1739
|
-
"status": "modified",
|
|
1740
|
-
"diff": "Index: backend/api/src/generate-ai-description.ts\n===================================================================\n--- backend/api/src/generate-ai-description.ts\t4e46d46 (parent)\n+++ backend/api/src/generate-ai-description.ts\te922ece (commit)\n@@ -1,15 +1,15 @@\n-import { APIError, APIHandler } from './helpers/endpoint'\n-import { log } from 'shared/utils'\n-import { track } from 'shared/analytics'\n-import { anythingToRichText } from 'shared/tiptap'\n-import { promptOpenAIWithWebSearch } from 'shared/helpers/openai-utils'\n import {\n addAnswersModeDescription,\n outcomeTypeDescriptions,\n resolutionCriteriaPrompt,\n } from 'common/ai-creation-prompts'\n import { HOUR_MS } from 'common/util/time'\n+import { track } from 'shared/analytics'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n+import { anythingToRichText } from 'shared/tiptap'\n+import { log } from 'shared/utils'\n+import { APIError, APIHandler } from './helpers/endpoint'\n import { rateLimitByUser } from './helpers/rate-limit'\n \n export const generateAIDescription: APIHandler<'generate-ai-description'> =\n rateLimitByUser(\n@@ -83,9 +83,12 @@\n ${userQuestionAndDescription}\n \n Only return the markdown description, nothing else.\n `\n- const gptResponse = await promptOpenAIWithWebSearch(prompt)\n+ const gptResponse = await promptAI(prompt, {\n+ model: aiModels.gpt5,\n+ webSearch: true,\n+ })\n \n track(auth.uid, 'generate-ai-description', {\n question: question.substring(0, 100),\n hasExistingDescription: !!description,\n"
|
|
1741
|
-
},
|
|
1742
|
-
{
|
|
1743
|
-
"path": "backend/api/src/generate-ai-market-suggestions.ts",
|
|
1744
|
-
"status": "modified",
|
|
1745
|
-
"diff": "Index: backend/api/src/generate-ai-market-suggestions.ts\n===================================================================\n--- backend/api/src/generate-ai-market-suggestions.ts\t4e46d46 (parent)\n+++ backend/api/src/generate-ai-market-suggestions.ts\te922ece (commit)\n@@ -1,14 +1,14 @@\n-import { APIError, APIHandler, AuthedUser } from './helpers/endpoint'\n-import { AIGeneratedMarket } from 'common/contract'\n-import { log } from 'shared/utils'\n import { formattingPrompt, guidelinesPrompt } from 'common/ai-creation-prompts'\n-import { anythingToRichText } from 'shared/tiptap'\n+import { APIParams } from 'common/api/schema'\n+import { AIGeneratedMarket } from 'common/contract'\n+import { HOUR_MS } from 'common/util/time'\n import { track } from 'shared/analytics'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n+import { anythingToRichText } from 'shared/tiptap'\n+import { log } from 'shared/utils'\n+import { APIError, APIHandler, AuthedUser } from './helpers/endpoint'\n import { rateLimitByUser } from './helpers/rate-limit'\n-import { HOUR_MS } from 'common/util/time'\n-import { promptOpenAIWebSearchParseJson } from 'shared/helpers/openai-utils'\n-import { APIParams } from 'common/api/schema'\n \n export const generateSuggestions = async (\n props: APIParams<'generate-ai-market-suggestions'>,\n auth: AuthedUser\n@@ -36,11 +36,13 @@\n \n ONLY return a valid JSON array of market objects and do NOT include any other text.\n `\n \n- const response = await promptOpenAIWebSearchParseJson<AIGeneratedMarket[]>(\n- combinedPrompt\n- )\n+ const response = await promptAI<AIGeneratedMarket[]>(combinedPrompt, {\n+ model: aiModels.gpt5,\n+ webSearch: true,\n+ parseAsJson: true,\n+ })\n \n // Parse the JSON response\n let parsedMarkets: AIGeneratedMarket[] = []\n try {\n"
|
|
1746
|
-
},
|
|
1747
|
-
{
|
|
1748
|
-
"path": "backend/api/src/generate-ai-numeric-ranges.ts",
|
|
1749
|
-
"status": "modified",
|
|
1750
|
-
"diff": "Index: backend/api/src/generate-ai-numeric-ranges.ts\n===================================================================\n--- backend/api/src/generate-ai-numeric-ranges.ts\t4e46d46 (parent)\n+++ backend/api/src/generate-ai-numeric-ranges.ts\te922ece (commit)\n@@ -1,7 +1,7 @@\n import { HOUR_MS } from 'common/util/time'\n import { track } from 'shared/analytics'\n-import { models, promptClaudeParsingJson } from 'shared/helpers/claude'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { log } from 'shared/utils'\n import { APIError, APIHandler } from './helpers/endpoint'\n import { rateLimitByUser } from './helpers/rate-limit'\n \n@@ -45,15 +45,17 @@\n \n const bucketSystemPrompt = baseSystemPrompt('bucket')\n \n const [thresholds, buckets] = await Promise.all([\n- promptClaudeParsingJson<RangeResponse>(prompt, {\n- model: models.haiku,\n+ promptAI<RangeResponse>(prompt, {\n+ model: aiModels.gpt5mini,\n system: thresholdSystemPrompt,\n+ parseAsJson: true,\n }),\n- promptClaudeParsingJson<RangeResponse>(prompt, {\n- model: models.haiku,\n+ promptAI<RangeResponse>(prompt, {\n+ model: aiModels.gpt5mini,\n system: bucketSystemPrompt,\n+ parseAsJson: true,\n }),\n ])\n \n log('thresholds', thresholds)\n@@ -83,8 +85,12 @@\n export const regenerateNumericMidpoints: APIHandler<'regenerate-numeric-midpoints'> =\n rateLimitByUser(\n async (props, auth) => {\n const { question, description, answers, min, max, unit, tab } = props\n+ log('midpoints from answers', { answers })\n+ if (answers.every((answer) => answer.trim() === '')) {\n+ throw new APIError(400, 'No ranges provided')\n+ }\n const prompt = `${userPrompt(\n question,\n min,\n max,\n@@ -99,10 +105,11 @@\n ${tab === 'thresholds' ? thresholdExamples : bucketExamples}\n \n Return ONLY an array of midpoint numbers, one for each range, in the same order as the ranges, without any other text or formatting.`\n \n- const result = await promptClaudeParsingJson<number[]>(prompt, {\n- model: models.haiku,\n+ const result = await promptAI<number[]>(prompt, {\n+ model: aiModels.gpt5mini,\n+ parseAsJson: true,\n })\n log('claudeResponse', result)\n \n track(auth.uid, 'regenerate-numeric-midpoints', {\n"
|
|
1751
|
-
},
|
|
1752
|
-
{
|
|
1753
|
-
"path": "backend/api/src/generate-concise-title.ts",
|
|
1754
|
-
"status": "modified",
|
|
1755
|
-
"diff": "Index: backend/api/src/generate-concise-title.ts\n===================================================================\n--- backend/api/src/generate-concise-title.ts\t4e46d46 (parent)\n+++ backend/api/src/generate-concise-title.ts\te922ece (commit)\n@@ -1,9 +1,8 @@\n import { HOUR_MS } from 'common/util/time'\n-import { promptClaude } from 'shared/helpers/claude'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { APIHandler } from './helpers/endpoint'\n import { rateLimitByUser } from './helpers/rate-limit'\n-// import { models, promptGemini } from 'shared/helpers/gemini'\n \n export const generateConciseTitle: APIHandler<'generate-concise-title'> =\n rateLimitByUser(\n async (props) => {\n@@ -40,15 +39,12 @@\n \n const prompt = `Question: \"${question}\"\n Your concise version, without any other text or commentary:`\n \n- const response = await promptClaude(prompt, {\n+ const response = await promptAI(prompt, {\n+ model: aiModels.sonnet3,\n system,\n })\n- // const response = await promptGemini(prompt, {\n- // model: models.flash,\n- // system,\n- // })\n \n let trimmedResponse = response.trim()\n if (trimmedResponse.startsWith('\"') && trimmedResponse.endsWith('\"')) {\n trimmedResponse = trimmedResponse.slice(1, -1)\n"
|
|
1756
|
-
},
|
|
1757
|
-
{
|
|
1758
|
-
"path": "backend/api/src/get-best-comments.ts",
|
|
1759
|
-
"status": "modified",
|
|
1760
|
-
"diff": "Index: backend/api/src/get-best-comments.ts\n===================================================================\n--- backend/api/src/get-best-comments.ts\t4e46d46 (parent)\n+++ backend/api/src/get-best-comments.ts\te922ece (commit)\n@@ -1,17 +1,17 @@\n import { APIHandler } from 'api/helpers/endpoint'\n+import { ContractComment } from 'common/comment'\n+import { convertContractComment } from 'common/supabase/comments'\n+import { parseJsonContentToText } from 'common/util/parse'\n+import { uniq } from 'lodash'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n+import { getContractsDirect } from 'shared/supabase/contracts'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n import {\n buildUserInterestsCache,\n userIdsToAverageTopicConversionScores,\n } from 'shared/topic-interests'\n-import { createSupabaseDirectClient } from 'shared/supabase/init'\n-import { convertContractComment } from 'common/supabase/comments'\n-import { parseJsonContentToText } from 'common/util/parse'\n-import { getContractsDirect } from 'shared/supabase/contracts'\n-import { uniq } from 'lodash'\n-import { ContractComment } from 'common/comment'\n import { rateLimitByUser } from './helpers/rate-limit'\n-import { promptGemini } from 'shared/helpers/gemini'\n \n export const getBestComments: APIHandler<'get-best-comments'> = rateLimitByUser(\n async (props, auth) => {\n const { limit, ignoreContractIds } = props\n@@ -151,9 +151,11 @@\n conveyed. Do NOT pick any comments that don't meet the minimum quality bar. \n Only return to me the comment ID, (ie don't say here is my top comment, just give me the ID)\n `\n \n- const batchMsgContent = await promptGemini(batchPrompt)\n+ const batchMsgContent = await promptAI(batchPrompt, {\n+ model: aiModels.flash,\n+ })\n const batchCommentIds = batchMsgContent\n ? batchMsgContent\n .split(',')\n .map((s: string) => s.trim())\n@@ -194,9 +196,9 @@\n respond with the highest quality comments you see.\n So, what are the highest quality ~${limit} comment IDs separated by commas, in order\n of descending quality? (ie don't say here are my top comments, just give me the IDs)\n `\n- const chosenComments = await promptGemini(prompt)\n+ const chosenComments = await promptAI(prompt, { model: aiModels.flash })\n const commentIds = chosenComments\n ? chosenComments\n .split(',')\n .map((s: string) => s.trim())\n"
|
|
1761
|
-
},
|
|
1762
|
-
{
|
|
1763
|
-
"path": "backend/api/src/get-related-markets.ts",
|
|
1764
|
-
"status": "modified",
|
|
1765
|
-
"diff": "Index: backend/api/src/get-related-markets.ts\n===================================================================\n--- backend/api/src/get-related-markets.ts\t4e46d46 (parent)\n+++ backend/api/src/get-related-markets.ts\te922ece (commit)\n@@ -9,9 +9,9 @@\n \n import { APIHandler } from 'api/helpers/endpoint'\n import { orderBy } from 'lodash'\n import { TOPIC_SIMILARITY_THRESHOLD } from 'shared/helpers/embeddings'\n-import { parseAIResponseAsJson, promptGemini } from 'shared/helpers/gemini'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n \n type cacheType = {\n marketIdsFromEmbeddings: string[]\n lastUpdated: number\n@@ -66,10 +66,12 @@\n \n Return a JSON array containing ONLY the IDs of markets to KEEP (those that are different enough).\n `\n \n- const response = await promptGemini(prompt)\n- const marketsToKeep = parseAIResponseAsJson(response)\n+ const marketsToKeep = await promptAI<string[]>(prompt, {\n+ model: aiModels.flash,\n+ parseAsJson: true,\n+ })\n \n if (Array.isArray(marketsToKeep) && marketsToKeep.length > 0) {\n marketsFromEmbeddings = marketsFromEmbeddings.filter((market) =>\n marketsToKeep.includes(market.id)\n"
|
|
1766
|
-
},
|
|
1767
|
-
{
|
|
1768
|
-
"path": "backend/api/src/infer-numeric-unit.ts",
|
|
1769
|
-
"status": "modified",
|
|
1770
|
-
"diff": "Index: backend/api/src/infer-numeric-unit.ts\n===================================================================\n--- backend/api/src/infer-numeric-unit.ts\t4e46d46 (parent)\n+++ backend/api/src/infer-numeric-unit.ts\te922ece (commit)\n@@ -1,7 +1,7 @@\n import { HOUR_MS } from 'common/util/time'\n import { track } from 'shared/analytics'\n-import { parseAIResponseAsJson, promptGemini } from 'shared/helpers/gemini'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { log } from 'shared/utils'\n import { APIError, APIHandler } from './helpers/endpoint'\n import { rateLimitByUser } from './helpers/rate-limit'\n \n@@ -30,10 +30,13 @@\n ? `Description: ${description}`\n : ''\n }\n `\n- const response = await promptGemini(prompt, { system: systemPrompt })\n- const result = parseAIResponseAsJson(response)\n+ const result = await promptAI<{ unit: string }>(prompt, {\n+ model: aiModels.flash,\n+ system: systemPrompt,\n+ parseAsJson: true,\n+ })\n log.info('Inferred unit:', { result })\n \n track(auth.uid, 'infer-numeric-unit', {\n question,\n"
|
|
1771
|
-
},
|
|
1772
|
-
{
|
|
1773
|
-
"path": "backend/api/src/on-create-comment-on-contract.ts",
|
|
1774
|
-
"status": "modified",
|
|
1775
|
-
"diff": "Index: backend/api/src/on-create-comment-on-contract.ts\n===================================================================\n--- backend/api/src/on-create-comment-on-contract.ts\t4e46d46 (parent)\n+++ backend/api/src/on-create-comment-on-contract.ts\te922ece (commit)\n@@ -16,10 +16,9 @@\n import {\n createAIDescriptionUpdateNotification,\n replied_users_info,\n } from 'shared/create-notification'\n-import { models, promptClaude } from 'shared/helpers/claude'\n-import { parseAIResponseAsJson, promptGemini } from 'shared/helpers/gemini'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { createCommentOnContractNotification } from 'shared/notifications/create-new-contract-comment-notif'\n import { getAnswer } from 'shared/supabase/answers'\n import { getCommentsDirect } from 'shared/supabase/contract-comments'\n import { updateContract } from 'shared/supabase/contracts'\n@@ -334,24 +333,22 @@\n NOTE: If the creator explicitly states that their comment is not a clarification, such as saying \"these comments are not a clarification,\" then you must not treat it as clarifying or changing the resolution criteria. In that case, return {\"isClarification\": false, \"description\": \"\"}.\n Only return the raw JSON object without any markdown code blocks, backticks, additional formatting, or anything else.`\n \n try {\n- const response = await promptClaude(prompt, {\n- model: models.sonnet4,\n+ const clarification = await promptAI<ClarificationResponse>(prompt, {\n+ model: aiModels.gpt5,\n+ parseAsJson: true,\n })\n log('Clarification response:', {\n question: contract.question,\n contractId: contract.id,\n slug: contract.slug,\n- response,\n+ clarification,\n })\n- if (!response) {\n+ if (!clarification) {\n log.error('No response from ai clarification')\n return\n }\n- const clarification = parseAIResponseAsJson(\n- response\n- ) as ClarificationResponse\n \n if (clarification.isClarification && clarification.description) {\n const dateParts = new Date()\n .toLocaleDateString('en-US', {\n@@ -461,11 +458,13 @@\n \n Only return the JSON object, no other text.`\n \n try {\n- const response = await promptGemini(prompt)\n- const result = parseAIResponseAsJson(response)\n- return result as { needsResponse: boolean; reason: string }\n+ const result = await promptAI<{ needsResponse: boolean; reason: string }>(\n+ prompt,\n+ { model: aiModels.gpt5mini, parseAsJson: true }\n+ )\n+ return result\n } catch (error) {\n log.error(`Error checking if comment needs response: ${error}`)\n // Default to false if there's an error\n return { needsResponse: false, reason: '' }\n"
|
|
1776
|
-
},
|
|
1777
|
-
{
|
|
1778
|
-
"path": "backend/scheduler/src/jobs/auto-award-bounty.ts",
|
|
1779
|
-
"status": "modified",
|
|
1780
|
-
"diff": "Index: backend/scheduler/src/jobs/auto-award-bounty.ts\n===================================================================\n--- backend/scheduler/src/jobs/auto-award-bounty.ts\t4e46d46 (parent)\n+++ backend/scheduler/src/jobs/auto-award-bounty.ts\te922ece (commit)\n@@ -1,12 +1,12 @@\n-import { sortBy, sumBy } from 'lodash'\n-import { BountiedQuestionContract } from 'common/contract'\n import { getAutoBountyPayoutPerHour } from 'common/bounty'\n-import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { BountiedQuestionContract } from 'common/contract'\n+import { sortBy, sumBy } from 'lodash'\n import { awardBounty } from 'shared/bounty'\n-import { promptOpenAI } from 'shared/helpers/openai-utils'\n-import { log, revalidateContractStaticProps } from 'shared/utils'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { updateContract } from 'shared/supabase/contracts'\n+import { createSupabaseDirectClient } from 'shared/supabase/init'\n+import { log, revalidateContractStaticProps } from 'shared/utils'\n \n export const autoAwardBounty = async () => {\n const pg = createSupabaseDirectClient()\n \n@@ -78,9 +78,9 @@\n \n The following comments have been submitted:\n \n ` + sortedComments.map((c) => `${c.likes} likes:\\n${c.content}`).join('\\n\\n')\n- const resultMessage = await promptOpenAI(prompt, 'o4-mini')\n+ const resultMessage = await promptAI(prompt, { model: aiModels.gpt5 })\n if (resultMessage) {\n await updateContract(pg, contract.id, {\n gptCommentSummary: resultMessage,\n })\n"
|
|
1781
|
-
},
|
|
1782
|
-
{
|
|
1783
|
-
"path": "backend/shared/src/helpers/ai-close-date.ts",
|
|
1784
|
-
"status": "modified",
|
|
1785
|
-
"diff": "Index: backend/shared/src/helpers/ai-close-date.ts\n===================================================================\n--- backend/shared/src/helpers/ai-close-date.ts\t4e46d46 (parent)\n+++ backend/shared/src/helpers/ai-close-date.ts\te922ece (commit)\n@@ -1,8 +1,8 @@\n import * as dayjs from 'dayjs'\n import * as utc from 'dayjs/plugin/utc'\n import { log } from 'shared/utils'\n-import { promptGemini } from './gemini'\n+import { aiModels, promptAI } from './prompt-ai'\n dayjs.extend(utc)\n \n export const getCloseDate = async (question: string, utcOffset?: number) => {\n const now = dayjs.utc().format('M/D/YYYY h:mm a')\n@@ -33,9 +33,9 @@\n \n Question: ${question}\n Now: ${now}\n End date:`\n- response = await promptGemini(prompt)\n+ response = await promptAI(prompt, { model: aiModels.sonnet3 })\n } catch (e: any) {\n log.error('Error generating close date', { e })\n return undefined\n }\n"
|
|
1786
|
-
},
|
|
1787
|
-
{
|
|
1788
|
-
"path": "backend/shared/src/helpers/openai-utils.ts",
|
|
1789
|
-
"status": "modified",
|
|
1790
|
-
"diff": "Index: backend/shared/src/helpers/openai-utils.ts\n===================================================================\n--- backend/shared/src/helpers/openai-utils.ts\t4e46d46 (parent)\n+++ backend/shared/src/helpers/openai-utils.ts\te922ece (commit)\n@@ -1,12 +1,16 @@\n-import OpenAI from 'openai'\n+import { APIError } from 'common/api/utils'\n+// import { buildArray } from 'common/util/array'\n import * as dayjs from 'dayjs'\n-import { log } from 'shared/utils'\n import * as utc from 'dayjs/plugin/utc'\n-import { APIError } from 'common/api/utils'\n-import { buildArray } from 'common/util/array'\n+import OpenAI from 'openai'\n+import { log } from 'shared/utils'\n+import { parseAIResponseAsJson } from './gemini'\n dayjs.extend(utc)\n-export type MODELS = 'o3-mini' | 'gpt-4o' | 'gpt-4.1-2025-04-14' | 'o4-mini'\n+export const models = {\n+ gpt5: 'gpt-5',\n+ gpt5mini: 'gpt-5-mini',\n+} as const\n \n export const generateEmbeddings = async (question: string) => {\n const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n let response\n@@ -45,66 +49,63 @@\n .then((res) => res.data[0].url)\n .catch((err) => (console.log(err), undefined))\n }\n \n+const defaultOptions: {\n+ system?: string\n+ model: (typeof models)[keyof typeof models]\n+ reasoning?: { effort: 'low' | 'medium' | 'high' }\n+ webSearch?: boolean\n+} = {\n+ model: models.gpt5,\n+}\n+\n export const promptOpenAI = async (\n prompt: string,\n- model: MODELS,\n- options: { system?: string } = {}\n+ options: typeof defaultOptions = defaultOptions\n ) => {\n- const { system } = options\n const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n \n- const result = await openai.chat.completions\n+ const { system, model, reasoning, webSearch } = options\n+\n+ const result = await openai.responses\n .create({\n model,\n- messages: buildArray(\n- system && {\n- role: 'system',\n- content: system,\n- },\n- { role: 'user', content: prompt }\n- ),\n+ input: prompt,\n+ ...(system ? { instructions: system } : {}),\n+ ...(webSearch\n+ ? {\n+ tools: [\n+ {\n+ type: 'web_search_preview_2025_03_11',\n+ search_context_size: 'high',\n+ },\n+ ],\n+ tool_choice: 'auto',\n+ }\n+ : {}),\n+ ...(reasoning ? { reasoning: { effort: reasoning.effort } } : {}),\n+ text: {\n+ // @ts-expect-error - verbosity is not typed\n+ verbosity: 'low',\n+ },\n })\n .catch((err) => (console.log(err), undefined))\n \n if (!result) throw new APIError(500, 'No result from OpenAI')\n \n- const message = result.choices[0].message.content\n- log('GPT4 returned message:', message)\n+ const message = (result as any).output_text as string | undefined\n+ log('OpenAI Responses returned message:', message)\n if (!message) throw new APIError(500, 'No result from OpenAI')\n return message\n }\n \n-export const promptOpenAIWithWebSearch = async (\n+export const promptOpenAIParsingAsJson = async (\n prompt: string,\n- model: MODELS = 'gpt-4.1-2025-04-14'\n+ options: typeof defaultOptions = defaultOptions\n ) => {\n- const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n-\n- try {\n- const result = await openai.responses.create({\n- model,\n- input: prompt,\n- tools: [{ type: 'web_search_preview', search_context_size: 'high' }], // Provide the tool definition\n- tool_choice: 'required',\n- })\n-\n- const message = result.output_text\n- log('OpenAI with tools returned message:', message)\n- if (!message) throw new Error('No message returned from OpenAI') // Changed to Error\n-\n- // Return the entire message object (could contain content or tool_calls)\n- return message\n- } catch (err: any) {\n- log.error('Error calling OpenAI with tools:', err?.message ?? err)\n- // Propagate the error or return a specific error indicator\n- // Throwing an APIError might be suitable depending on usage context\n- throw new APIError(\n- 500,\n- `OpenAI API error: ${err?.message ?? 'Unknown error'}`\n- )\n- }\n+ const res = await promptOpenAI(prompt, options)\n+ return parseAIResponseAsJson(res)\n }\n \n export const removeJsonTicksFromResponse = (response: string): string => {\n // Remove markdown code block formatting if present\n@@ -118,40 +119,11 @@\n // If no markdown formatting found, return the original response\n return response.trim()\n }\n \n-// Helper function to ensure the response is valid JSON, adapted from claude.ts\n-export const parseOpenAIResponseAsJson = (response: string): any => {\n- const cleanedResponse = removeJsonTicksFromResponse(response)\n-\n- try {\n- // Try to parse as is\n- return JSON.parse(cleanedResponse)\n- } catch (error) {\n- // If parsing fails, try to handle common issues\n-\n- // Check if it's an array wrapped in extra text\n- const arrayStart = cleanedResponse.indexOf('[')\n- const arrayEnd = cleanedResponse.lastIndexOf(']')\n-\n- if (arrayStart !== -1 && arrayEnd !== -1 && arrayEnd > arrayStart) {\n- const potentialArray = cleanedResponse.substring(arrayStart, arrayEnd + 1)\n- try {\n- return JSON.parse(potentialArray)\n- } catch (e) {\n- // If still fails, throw the original error\n- throw error\n- }\n- }\n-\n- // If we can't fix it, throw the original error\n- throw error\n- }\n-}\n-\n export const promptOpenAIWebSearchParseJson = async <T>(\n prompt: string,\n- model: MODELS = 'gpt-4.1-2025-04-14'\n+ options: typeof defaultOptions = defaultOptions\n ): Promise<T> => {\n- const response = await promptOpenAIWithWebSearch(prompt, model)\n- return parseOpenAIResponseAsJson(response)\n+ const response = await promptOpenAI(prompt, { ...options, webSearch: true })\n+ return parseAIResponseAsJson(response)\n }\n"
|
|
1791
|
-
},
|
|
1792
|
-
{
|
|
1793
|
-
"path": "backend/shared/src/helpers/prompt-ai.ts",
|
|
1794
|
-
"status": "added",
|
|
1795
|
-
"diff": "Index: backend/shared/src/helpers/prompt-ai.ts\n===================================================================\n--- backend/shared/src/helpers/prompt-ai.ts\t4e46d46 (parent)\n+++ backend/shared/src/helpers/prompt-ai.ts\te922ece (commit)\n@@ -1,1 +1,74 @@\n-[NEW FILE]\n\\n+import { models as claudeModels, promptClaude } from './claude'\n+import {\n+ models as geminiModels,\n+ parseAIResponseAsJson,\n+ promptGemini,\n+} from './gemini'\n+import { models as openaiModels, promptOpenAI } from './openai-utils'\n+\n+type ReasoningEffort = 'low' | 'medium' | 'high'\n+\n+export const aiModels = {\n+ ...openaiModels,\n+ ...claudeModels,\n+ ...geminiModels,\n+} as const\n+\n+export type PromptAIOptionsBase = {\n+ model: (typeof aiModels)[keyof typeof aiModels]\n+ system?: string\n+ webSearch?: boolean\n+ reasoning?: { effort: ReasoningEffort }\n+}\n+\n+export type PromptAIJsonOptions = PromptAIOptionsBase & { parseAsJson: true }\n+export type PromptAIStringOptions = PromptAIOptionsBase & {\n+ parseAsJson?: false\n+}\n+\n+function getProviderFromModel(\n+ model: (typeof aiModels)[keyof typeof aiModels]\n+): 'openai' | 'claude' | 'gemini' {\n+ const lower = model.toLowerCase()\n+ if (lower.startsWith('claude')) return 'claude'\n+ if (lower.startsWith('gemini')) return 'gemini'\n+ return 'openai'\n+}\n+\n+export async function promptAI<T>(\n+ prompt: string,\n+ options: PromptAIJsonOptions\n+): Promise<T>\n+export async function promptAI(\n+ prompt: string,\n+ options: PromptAIStringOptions\n+): Promise<string>\n+export async function promptAI<T = unknown>(\n+ prompt: string,\n+ options: (PromptAIJsonOptions | PromptAIStringOptions) & {\n+ model: (typeof aiModels)[keyof typeof aiModels]\n+ }\n+): Promise<string | T> {\n+ const { model, system, webSearch, reasoning } = options\n+ const provider = getProviderFromModel(model)\n+\n+ let rawResponse: string\n+\n+ if (provider === 'openai') {\n+ rawResponse = await promptOpenAI(prompt, {\n+ model: model as any,\n+ system,\n+ reasoning,\n+ webSearch,\n+ })\n+ } else if (provider === 'claude') {\n+ rawResponse = await promptClaude(prompt, { model: model as any, system })\n+ } else {\n+ rawResponse = await promptGemini(prompt, { model: model as any, system })\n+ }\n+\n+ if ('parseAsJson' in options && options.parseAsJson) {\n+ return parseAIResponseAsJson(rawResponse) as T\n+ }\n+ return rawResponse\n+}\n"
|
|
1796
|
-
},
|
|
1797
|
-
{
|
|
1798
|
-
"path": "backend/shared/src/supabase/contracts.ts",
|
|
1799
|
-
"status": "modified",
|
|
1800
|
-
"diff": "Index: backend/shared/src/supabase/contracts.ts\n===================================================================\n--- backend/shared/src/supabase/contracts.ts\t4e46d46 (parent)\n+++ backend/shared/src/supabase/contracts.ts\te922ece (commit)\n@@ -7,10 +7,10 @@\n import { isAdminId } from 'common/envs/constants'\n import { convertAnswer, convertContract } from 'common/supabase/contracts'\n import { Tables } from 'common/supabase/utils'\n import { camelCase, mapValues, sortBy } from 'lodash'\n-import { parseAIResponseAsJson, promptGemini } from 'shared/helpers/gemini'\n import { generateEmbeddings } from 'shared/helpers/openai-utils'\n+import { aiModels, promptAI } from 'shared/helpers/prompt-ai'\n import { SupabaseDirectClient } from 'shared/supabase/init'\n import { contractColumnsToSelect, log } from 'shared/utils'\n import { broadcastUpdatedContract } from 'shared/websockets/helpers'\n import { DataUpdate, update, updateData } from './utils'\n@@ -172,13 +172,17 @@\n \"isSelfReferential\": boolean,\n }`\n \n try {\n- const response = await promptGemini(contract.question, {\n- system: systemPrompt,\n- })\n- const result = parseAIResponseAsJson(response)\n- return result.isSelfReferential\n+ const result = await promptAI<{ isSelfReferential?: boolean }>(\n+ contract.question,\n+ {\n+ model: aiModels.flash,\n+ system: systemPrompt,\n+ parseAsJson: true,\n+ }\n+ )\n+ return !!result.isSelfReferential\n } catch (error) {\n log.error('Error checking for self-referential market:', {\n error: error instanceof Error ? error.message : String(error),\n })\n"
|
|
1801
|
-
},
|
|
1802
|
-
{
|
|
1803
|
-
"path": "native/.gitignore",
|
|
1804
|
-
"status": "modified",
|
|
1805
|
-
"diff": "Index: native/.gitignore\n===================================================================\n--- native/.gitignore\t4e46d46 (parent)\n+++ native/.gitignore\te922ece (commit)\n@@ -22,4 +22,5 @@\n .DS_Store\n # simulator build\n *.tar.gz\n .env.local\n+Manifold.app/\n"
|
|
1806
|
-
},
|
|
1807
|
-
{
|
|
1808
|
-
"path": "web/components/new-contract/multi-numeric-date-section.tsx",
|
|
1809
|
-
"status": "modified",
|
|
1810
|
-
"diff": "Index: web/components/new-contract/multi-numeric-date-section.tsx\n===================================================================\n--- web/components/new-contract/multi-numeric-date-section.tsx\t4e46d46 (parent)\n+++ web/components/new-contract/multi-numeric-date-section.tsx\te922ece (commit)\n@@ -160,8 +160,12 @@\n max: string,\n tab: 'thresholds' | 'buckets'\n ) => {\n setRegenerateError('')\n+ if (answers.every((answer) => answer.trim() === '')) {\n+ console.log('no answers to regenerate midpoints for')\n+ return\n+ }\n try {\n // Call regenerate-date-midpoints without tab parameter\n const result = await api('regenerate-date-midpoints', {\n question,\n"
|
|
1811
|
-
},
|
|
1812
|
-
{
|
|
1813
|
-
"path": "web/components/new-contract/multi-numeric-range-section.tsx",
|
|
1814
|
-
"status": "modified",
|
|
1815
|
-
"diff": "Index: web/components/new-contract/multi-numeric-range-section.tsx\n===================================================================\n--- web/components/new-contract/multi-numeric-range-section.tsx\t4e46d46 (parent)\n+++ web/components/new-contract/multi-numeric-range-section.tsx\te922ece (commit)\n@@ -171,8 +171,12 @@\n max: number | undefined,\n tab: 'thresholds' | 'buckets'\n ) => {\n setRegenerateError('')\n+ if (answers.every((answer) => answer.trim() === '')) {\n+ console.log('no answers to regenerate midpoints for')\n+ return\n+ }\n // Only regenerate midpoints if we have min and max\n if (min === undefined || max === undefined) return\n \n try {\n"
|
|
1816
|
-
},
|
|
1817
|
-
{
|
|
1818
|
-
"path": "yarn.lock",
|
|
1819
|
-
"status": "modified",
|
|
1820
|
-
"diff": "Index: yarn.lock\n===================================================================\n--- yarn.lock\t4e46d46 (parent)\n+++ yarn.lock\te922ece (commit)\n@@ -6541,13 +6541,8 @@\n version \"1.0.2\"\n resolved \"https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee\"\n integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==\n \n-base-64@^0.1.0:\n- version \"0.1.0\"\n- resolved \"https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb\"\n- integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==\n-\n base64-js@0.0.8:\n version \"0.0.8\"\n resolved \"https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978\"\n integrity sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==\n@@ -6838,13 +6833,8 @@\n version \"1.0.2\"\n resolved \"https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf\"\n integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==\n \n-charenc@0.0.2:\n- version \"0.0.2\"\n- resolved \"https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667\"\n- integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==\n-\n cheerio-select@^2.1.0:\n version \"2.1.0\"\n resolved \"https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4\"\n integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==\n@@ -7219,13 +7209,8 @@\n path-key \"^3.1.0\"\n shebang-command \"^2.0.0\"\n which \"^2.0.1\"\n \n-crypt@0.0.2:\n- version \"0.0.2\"\n- resolved \"https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b\"\n- integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==\n-\n css-background-parser@^0.1.0:\n version \"0.1.0\"\n resolved \"https://registry.yarnpkg.com/css-background-parser/-/css-background-parser-0.1.0.tgz#48a17f7fe6d4d4f1bca3177ddf16c5617950741b\"\n integrity sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==\n@@ -7629,16 +7614,8 @@\n version \"4.0.2\"\n resolved \"https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d\"\n integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==\n \n-digest-fetch@^1.3.0:\n- version \"1.3.0\"\n- resolved \"https://registry.yarnpkg.com/digest-fetch/-/digest-fetch-1.3.0.tgz#898e69264d00012a23cf26e8a3e40320143fc661\"\n- integrity sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==\n- dependencies:\n- base-64 \"^0.1.0\"\n- md5 \"^2.3.0\"\n-\n dir-glob@^3.0.1:\n version \"3.0.1\"\n resolved \"https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f\"\n integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==\n@@ -9734,13 +9711,8 @@\n dependencies:\n call-bind \"^1.0.2\"\n has-tostringtag \"^1.0.0\"\n \n-is-buffer@~1.1.6:\n- version \"1.1.6\"\n- resolved \"https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be\"\n- integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==\n-\n is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:\n version \"1.2.7\"\n resolved \"https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055\"\n integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==\n@@ -10926,17 +10898,8 @@\n version \"1.1.0\"\n resolved \"https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9\"\n integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==\n \n-md5@^2.3.0:\n- version \"2.3.0\"\n- resolved \"https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f\"\n- integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==\n- dependencies:\n- charenc \"0.0.2\"\n- crypt \"0.0.2\"\n- is-buffer \"~1.1.6\"\n-\n mdn-data@2.0.28:\n version \"2.0.28\"\n resolved \"https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba\"\n integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==\n@@ -11479,23 +11442,8 @@\n integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==\n dependencies:\n mimic-fn \"^2.1.0\"\n \n-openai@4.16.1:\n- version \"4.16.1\"\n- resolved \"https://registry.yarnpkg.com/openai/-/openai-4.16.1.tgz#6377682ad2af805affd1b401958fb6eb92a87d61\"\n- integrity sha512-Gr+uqUN1ICSk6VhrX64E+zL7skjI1TgPr/XUN+ZQuNLLOvx15+XZulx/lSW4wFEAQzgjBDlMBbBeikguGIjiMg==\n- dependencies:\n- \"@types/node\" \"^18.11.18\"\n- \"@types/node-fetch\" \"^2.6.4\"\n- abort-controller \"^3.0.0\"\n- agentkeepalive \"^4.2.1\"\n- digest-fetch \"^1.3.0\"\n- form-data-encoder \"1.7.2\"\n- formdata-node \"^4.3.2\"\n- node-fetch \"^2.6.7\"\n- web-streams-polyfill \"^3.2.1\"\n-\n openai@4.92.1:\n version \"4.92.1\"\n resolved \"https://registry.yarnpkg.com/openai/-/openai-4.92.1.tgz#554e1aed4a9072acf4491ee3ef725a35871276b0\"\n integrity sha512-rFjyiQF/eHXIuzyoT2qkCY/xmI+zyq9xlMZmOEFkSsyGhc8tpNaf7rW25m5uTddnk6B5gRfRX640onMhAQyTww==\n@@ -11507,8 +11455,13 @@\n form-data-encoder \"1.7.2\"\n formdata-node \"^4.3.2\"\n node-fetch \"^2.6.7\"\n \n+openai@5.12.2:\n+ version \"5.12.2\"\n+ resolved \"https://registry.yarnpkg.com/openai/-/openai-5.12.2.tgz#512ab6b80eb8414837436e208f1b951442b97761\"\n+ integrity sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==\n+\n optionator@^0.8.1:\n version \"0.8.3\"\n resolved \"https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495\"\n integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==\n"
|
|
1821
|
-
}
|
|
1822
|
-
]
|
|
1823
|
-
},
|
|
1824
|
-
{
|
|
1825
|
-
"id": "update-native-notifs",
|
|
1826
|
-
"sha": "7d171eed10432a4622cc1028bbf541a1cab96bcd",
|
|
1827
|
-
"parentSha": "74465a3153ad1c23ba79ed5d8d03e6afb84de773",
|
|
1828
|
-
"spec": "Implement a native notifications and deep link refactor with Android notification icon updates, plus minor splash/auth UI changes and version bumps across the native app.\n\nRequired changes:\n\n1) Centralize and simplify notifications in native/App.tsx\n- Reorganize imports (group external libs, then internal). Ensure imports include: Clipboard from @react-native-clipboard/clipboard, ExpoClipboard, expo-constants/dev-client/linking/notifications/status-bar/web-browser/store-review, Firebase User type, platform/react-native essentials, SafeAreaProvider/SafeAreaView, WebView, and app/auth/ENV from ./init. Ensure EXTERNAL_REDIRECTS and isAdminId are from common/envs/constants.\n- Initialize Expo Notifications handler BEFORE the App component: call Notifications.setNotificationHandler with shouldPlaySound:true, shouldSetBadge:false, shouldShowBanner:true, shouldShowList:true.\n- Remove the NativeEventEmitter/LinkingManager workaround and the addNotificationResponseReceivedListener subscription ref; handle notification responses solely via Notifications.useLastNotificationResponse() inside an effect that:\n - Checks for a valid notification with DEFAULT_ACTION_IDENTIFIER, logs reason, forwards it via handlePushNotification, then calls Notifications.clearLastNotificationResponseAsync().\n- Modify handlePushNotification to:\n - Extract Notification payload, log the reason only.\n - If webview loaded and listening, send a single communicateWithWebview('notification', notification) (remove multiple delayed sends).\n - Always setEndpointWithNativeQuery to navigate to the resolved URL.\n - Remove reading/writing of lastNotificationIds; delete any persistence logic for it.\n- On app mount, call clearData('lastNotificationIds') to purge the deprecated key, and signInUserFromStorage().\n- Replace Linking.useURL/NativeEventEmitter hack with Linking.useLinkingURL() (or the equivalent expo hook) in a const linkedUrl, and keep a single effect to parse and route deep links via Linking.parse(linkedUrl). Remove Linking.getInitialURL logic and any eventEmitter.emit('url', 'blank') cache-clearing behavior.\n- Adjust fullyLoaded to include listeningToNative.current: fullyLoaded = hasLoadedWebView && fbUser && isConnected && listeningToNative.current.\n- Update container styles so:\n - display is always 'flex'.\n - backgroundColor is '#4337C9' until fullyLoaded, then switch to the dynamic backgroundColor state.\n- Render layout as:\n - <SafeAreaProvider>\n - <SafeAreaView style={styles.container} edges={[\"top\",\"bottom\",\"left\",\"right\"]}>\n - <StatusBar style={theme === 'dark' ? 'light' : 'dark'} animated={true} />\n - <SplashAuth ... />\n - <CustomWebview ... display={!!fullyLoaded} />\n - </SafeAreaView>\n - Remove the extra fragment wrapper and the inner SafeAreaView used previously.\n- Reduce overly chatty logs: comment out the verbose logs in communicateWithWebview and when signing in fb user from webview cache.\n\n2) Update CustomWebview component (native/components/custom-webview.tsx)\n- Add a new boolean prop display.\n- Wrap the WebView content inside a root <View> that sets style to { flex: display ? 1 : 0, height: display ? 'auto' : 0 }.\n- On Android’s ScrollView, also control visibility with style={{ display: display ? 'flex' : 'none' }}.\n- Pass display from App.tsx based on fullyLoaded.\n\n3) Tweak AuthPage styles (native/components/auth-page.tsx)\n- In AuthPageStyles.container, add display: 'flex'.\n- Reduce flappy marginTop from 180 to 150.\n- In card style, add minHeight: 180 to match height.\n\n4) Android notification icon, color, and version\n- In native/android/app/build.gradle, bump versionCode from 65 to 66 and versionName to 2.0.66.\n- In native/android/app/src/main/AndroidManifest.xml, add meta-data entries:\n - com.google.firebase.messaging.default_notification_channel_id = \"default\"\n - com.google.firebase.messaging.default_notification_color = @color/notification_icon_color\n - expo.modules.notifications.default_notification_color = @color/notification_icon_color\n - Keep existing icon meta-data for firebase and expo pointing to @drawable/notification_icon.\n- In native/android/app/src/main/res/values/colors.xml, add <color name=\"notification_icon_color\">#4337C9</color>.\n- Replace notification icon PNGs under drawable-hdpi, mdpi, xhdpi, xxhdpi, xxxhdpi with updated monochrome assets suited for notification status bar use.\n\n5) Expo config and assets\n- In native/app.json:\n - Bump version to 2.0.66, android.versionCode to 66, and ios.buildNumber to 1.0.66.\n - Under the expo-notifications plugin config, update:\n - icon to \"./assets/manifold_white_transparent.png\" (replace logo-96 reference),\n - color to \"#4337C9\",\n - defaultChannel to \"default\".\n- Remove native/assets/logo-96.png from use (delete the file if no longer needed elsewhere) and add the new notification icon source asset manifold_white_transparent.png under native/assets.\n\n6) iOS version bump\n- In native/ios/Manifold/Info.plist, bump CFBundleShortVersionString to 2.0.66 and CFBundleVersion to 1.0.66.\n\nAcceptance criteria:\n- Opening a push notification navigates to the correct in-app route, whether the app is foreground/background/cold-start, without duplicate handling or reliance on stored lastNotificationIds.\n- Deep links received while the app is running or on startup are parsed and routed correctly without the previous NativeEventEmitter/getInitialURL workaround.\n- The app shows a purple splash/background until the WebView is ready and listening; the WebView only appears when fullyLoaded (including the listeningToNative flag) using the new display prop.\n- Android notifications use the new monochrome icon and purple accent, and the default notification channel is set.\n- App versions reflect 2.0.66 / 1.0.66 as appropriate across Android/iOS/Expo config.",
|
|
1829
|
-
"prompt": "Refactor the native app’s notification and deep link handling to be simpler and more reliable, and update Android notification appearance with a monochrome icon and accent color. Make the WebView only appear once the app is fully ready to receive messages to avoid flicker. Also bump native app versions for Android and iOS.\n\nSpecifically:\n- Use Expo’s notification handler and last notification response hook to process taps from any app state, without persisting or de-duplicating notifications.\n- Switch deep link handling to the built-in linking hook and remove any event emitter or initial URL hacks.\n- Keep the purple splash/background visible until the WebView is truly ready, and hide the WebView until then via a display prop.\n- Update the Android notification icon and color, set a default channel, and reflect these via both the manifest and the Expo notifications plugin.\n- Bump app versions for Android and iOS accordingly.",
|
|
1830
|
-
"supplementalFiles": [
|
|
1831
|
-
"native/init.ts",
|
|
1832
|
-
"native/components/splash-auth.tsx",
|
|
1833
|
-
"native/components/logger.tsx",
|
|
1834
|
-
"native/lib/auth.ts",
|
|
1835
|
-
"native/android/app/src/main/java/com/markets/manifold/MainActivity.kt",
|
|
1836
|
-
"native/ios/Manifold/AppDelegate.swift"
|
|
1837
|
-
],
|
|
1838
|
-
"fileDiffs": [
|
|
1839
|
-
{
|
|
1840
|
-
"path": "native/App.tsx",
|
|
1841
|
-
"status": "modified",
|
|
1842
|
-
"diff": "Index: native/App.tsx\n===================================================================\n--- native/App.tsx\t74465a3 (parent)\n+++ native/App.tsx\t7d171ee (commit)\n@@ -1,25 +1,8 @@\n-import Clipboard from '@react-native-clipboard/clipboard'\n-import { EXTERNAL_REDIRECTS, isAdminId } from 'common/envs/constants'\n-import * as ExpoClipboard from 'expo-clipboard'\n-import 'expo-dev-client'\n-import * as Notifications from 'expo-notifications'\n-import * as WebBrowser from 'expo-web-browser'\n-import { User as FirebaseUser } from 'firebase/auth'\n-import React, { useCallback, useEffect, useRef, useState } from 'react'\n-import {\n- BackHandler,\n- NativeEventEmitter,\n- Platform,\n- Share,\n- StyleSheet,\n-} from 'react-native'\n-import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'\n-import WebView from 'react-native-webview'\n-import { app, auth, ENV } from './init'\n-// @ts-ignore\n import { ReadexPro_400Regular, useFonts } from '@expo-google-fonts/readex-pro'\n+import Clipboard from '@react-native-clipboard/clipboard'\n import * as Sentry from '@sentry/react-native'\n+import { EXTERNAL_REDIRECTS, isAdminId } from 'common/envs/constants'\n import { setFirebaseUserViaJson } from 'common/firebase-auth'\n import {\n MesageTypeMap,\n nativeToWebMessage,\n@@ -30,18 +13,25 @@\n import { getSourceUrl, Notification } from 'common/notification'\n import { CustomWebview } from 'components/custom-webview'\n import { log } from 'components/logger'\n import { SplashAuth } from 'components/splash-auth'\n+import * as ExpoClipboard from 'expo-clipboard'\n import Constants from 'expo-constants'\n+import 'expo-dev-client'\n import * as Linking from 'expo-linking'\n-import { MaybeNotificationResponse, Subscription } from 'expo-notifications'\n+import * as Notifications from 'expo-notifications'\n import { StatusBar } from 'expo-status-bar'\n import * as StoreReview from 'expo-store-review'\n+import * as WebBrowser from 'expo-web-browser'\n+import { User as FirebaseUser } from 'firebase/auth'\n import { clearData, getData, storeData } from 'lib/auth'\n import { checkLocationPermission, getLocation } from 'lib/location'\n import { useIsConnected } from 'lib/use-is-connected'\n-// @ts-ignore\n-import * as LinkingManager from 'react-native/Libraries/Linking/NativeLinkingManager'\n+import React, { useCallback, useEffect, useRef, useState } from 'react'\n+import { BackHandler, Platform, Share, StyleSheet } from 'react-native'\n+import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context'\n+import WebView from 'react-native-webview'\n+import { app, auth, ENV } from './init'\n \n Sentry.init({\n dsn: 'https://2353d2023dad4bc192d293c8ce13b9a1@o4504040581496832.ingest.us.sentry.io/4504040585494528',\n debug: ENV === 'DEV',\n@@ -54,13 +44,22 @@\n // const BASE_URI = 'http://192.168.1.229:3000/'\n \n const BASE_URI =\n ENV === 'DEV' ? 'https://dev.manifold.markets/' : 'https://manifold.markets/'\n-const isIOS = Platform.OS === 'ios'\n+\n+// Set up notification handler before component\n+Notifications.setNotificationHandler({\n+ handleNotification: async () => ({\n+ shouldPlaySound: true,\n+ shouldSetBadge: false,\n+ shouldShowBanner: true,\n+ shouldShowList: true,\n+ }),\n+})\n+\n const App = () => {\n // Init\n const webview = useRef<WebView>(null)\n- const notificationResponseListener = useRef<Subscription>()\n useFonts({ ReadexPro_400Regular })\n \n // This tracks if the webview has loaded its first page\n const [hasLoadedWebView, setHasLoadedWebView] = useState(false)\n@@ -84,8 +83,9 @@\n }\n \n useEffect(() => {\n signInUserFromStorage()\n+ clearData('lastNotificationIds') // no longer used, clear them from local storage\n }, [])\n \n // Sends the saved user to the web client to make the log in process faster\n const sendWebviewAuthInfo = (user: FirebaseUser) => {\n@@ -107,12 +107,9 @@\n params.set('nativePlatform', Platform.OS)\n url.search = params.toString()\n return url.toString()\n })\n- const linkedUrl = Linking.useURL()\n- const eventEmitter = new NativeEventEmitter(\n- isIOS ? LinkingManager.default : null\n- )\n+ const linkedUrl = Linking.useLinkingURL()\n \n // UI\n const [backgroundColor, setBackgroundColor] = useState('rgba(255,255,255,1)')\n const [theme, setTheme] = useState<'dark' | 'light'>('light')\n@@ -152,94 +149,49 @@\n // Perhaps this isn't current if the webview is killed for memory collection? Not sure\n const notification = response.notification.request.content\n .data as Notification\n if (notification == undefined) return\n- const lastNotificationIds = await getData<string[]>('lastNotificationIds')\n- if (\n- lastNotificationIds?.length &&\n- lastNotificationIds.some((id) => id === notification.id)\n- ) {\n- log('skipping lastNotificationResponse', notification.id)\n- return\n- }\n- log('handling notification', notification)\n+ log('handling notification', notification.reason)\n \n // Resolve the destination URL from the notification.\n const destination = getSourceUrl(notification)\n \n // If the webview is already loaded and listening, forward the message so\n // the web client can mark the notification as seen, etc.\n if (hasLoadedWebView && listeningToNative.current) {\n- // Send multiple times in case the client JavaScript isn\\'t ready yet\n- // (mirrors the logic in sendWebviewAuthInfo).\n- const timeouts = [0, 200, 800]\n- timeouts.forEach((timeout) =>\n- setTimeout(\n- () => communicateWithWebview('notification', notification),\n- timeout\n- )\n- )\n+ communicateWithWebview('notification', notification)\n }\n \n // Always set the URL so that, even if the message is missed, the webview\n // navigates to the correct page when it becomes active.\n setEndpointWithNativeQuery(destination)\n-\n- storeData('lastNotificationIds', [\n- ...(lastNotificationIds || []),\n- notification.id,\n- ])\n }\n \n useEffect(() => {\n- // This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded)\n- notificationResponseListener.current =\n- Notifications.addNotificationResponseReceivedListener((response) => {\n- log('notification response', response)\n- handlePushNotification(response)\n- })\n-\n- return () => {\n- if (notificationResponseListener.current)\n- notificationResponseListener.current.remove()\n- }\n- }, [])\n-\n- useEffect(() => {\n- Linking.getInitialURL().then((url) => {\n- log('Initial url:', url, '- has loaded webview:', hasLoadedWebView)\n- if (url) setUrlWithNativeQuery(url)\n- })\n const backHandler = BackHandler.addEventListener(\n 'hardwareBackPress',\n handleBackButtonPress\n )\n return () => backHandler.remove()\n }, [])\n \n- const handleLastNotificationResponse = async (\n- lastNotif: MaybeNotificationResponse\n- ) => {\n+ const lastNotifResponse = Notifications.useLastNotificationResponse()\n+ useEffect(() => {\n if (\n- lastNotif &&\n- lastNotif.notification.request.content.data &&\n- lastNotif.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER\n+ lastNotifResponse &&\n+ lastNotifResponse.notification.request.content.data &&\n+ lastNotifResponse.actionIdentifier ===\n+ Notifications.DEFAULT_ACTION_IDENTIFIER\n ) {\n log(\n 'processing lastNotificationResponse',\n- lastNotif.notification.request.content.data\n+ lastNotifResponse.notification.request.content.data.reason\n )\n- handlePushNotification(lastNotif)\n- // Clearing the last notification response doesn't seem to persist across app restarts, so we store the id\n+ handlePushNotification(lastNotifResponse)\n Notifications.clearLastNotificationResponseAsync()\n }\n- }\n+ }, [lastNotifResponse])\n \n- const lastNotificationResponse = Notifications.useLastNotificationResponse()\n- useEffect(() => {\n- handleLastNotificationResponse(lastNotificationResponse)\n- }, [lastNotificationResponse])\n-\n // Handle deep links\n useEffect(() => {\n if (!linkedUrl || linkedUrl === 'blank') return\n const { hostname, path } = Linking.parse(linkedUrl)\n@@ -256,14 +208,8 @@\n )\n if (hasLoadedWebView && listeningToNative.current)\n communicateWithWebview('link', { url })\n else setEndpointWithNativeQuery(url)\n- // If we don't clear the url, we'll reopen previously opened links\n- const clearUrlCacheEvent = {\n- hostname: 'manifold.markets',\n- url: 'blank',\n- }\n- eventEmitter.emit('url', clearUrlCacheEvent)\n }\n }, [linkedUrl])\n \n const handleBackButtonPress = () => {\n@@ -383,9 +329,9 @@\n try {\n const fbUserAndPrivateUser = JSON.parse(payload)\n if (fbUserAndPrivateUser && fbUserAndPrivateUser.fbUser) {\n const fbUser = fbUserAndPrivateUser.fbUser as FirebaseUser\n- log('Signing in fb user from webview cache')\n+ // log('Signing in fb user from webview cache')\n // We don't actually use the firebase auth for anything right now, but in case we do in the future...\n await setFirebaseUserViaJson(fbUser, app)\n await storeData('user', fbUser)\n }\n@@ -457,14 +403,14 @@\n const communicateWithWebview = <T extends nativeToWebMessageType>(\n type: T,\n data: MesageTypeMap[T]\n ) => {\n- log(\n- 'Sending message to webview:',\n- type,\n- 'is listening:',\n- listeningToNative.current\n- )\n+ // log(\n+ // 'Sending message to webview:',\n+ // type,\n+ // 'is listening:',\n+ // listeningToNative.current\n+ // )\n webview.current?.postMessage(\n JSON.stringify({\n type,\n data,\n@@ -480,16 +426,17 @@\n webview.current?.reload()\n }\n \n const isConnected = useIsConnected()\n- const fullyLoaded = hasLoadedWebView && fbUser && isConnected\n+ const fullyLoaded =\n+ hasLoadedWebView && fbUser && isConnected && listeningToNative.current\n const styles = StyleSheet.create({\n container: {\n- display: fullyLoaded ? 'flex' : 'none',\n+ display: 'flex',\n flex: 1,\n justifyContent: 'center',\n overflow: 'hidden',\n- backgroundColor: backgroundColor,\n+ backgroundColor: fullyLoaded ? backgroundColor : '#4337C9',\n },\n })\n \n const handleExternalLink = useCallback(\n@@ -506,36 +453,35 @@\n [baseUri]\n )\n \n return (\n- <>\n- <SafeAreaProvider>\n+ <SafeAreaProvider>\n+ <SafeAreaView\n+ style={styles.container}\n+ edges={['top', 'bottom', 'left', 'right']}\n+ >\n+ <StatusBar\n+ animated={true}\n+ style={theme === 'dark' ? 'light' : 'dark'}\n+ hidden={false}\n+ />\n <SplashAuth\n webview={webview}\n hasLoadedWebView={hasLoadedWebView}\n fbUser={fbUser}\n isConnected={isConnected}\n />\n- <SafeAreaView\n- style={styles.container}\n- edges={['top', 'bottom', 'left', 'right']}\n- >\n- <StatusBar\n- animated={true}\n- style={theme === 'dark' ? 'light' : 'dark'}\n- hidden={false}\n- />\n- <CustomWebview\n- urlToLoad={urlToLoad}\n- webview={webview}\n- resetWebView={resetWebView}\n- setHasLoadedWebView={setHasLoadedWebView}\n- handleMessageFromWebview={handleMessageFromWebview}\n- handleExternalLink={handleExternalLink}\n- />\n- </SafeAreaView>\n- </SafeAreaProvider>\n+ <CustomWebview\n+ display={!!fullyLoaded}\n+ urlToLoad={urlToLoad}\n+ webview={webview}\n+ resetWebView={resetWebView}\n+ setHasLoadedWebView={setHasLoadedWebView}\n+ handleMessageFromWebview={handleMessageFromWebview}\n+ handleExternalLink={handleExternalLink}\n+ />\n+ </SafeAreaView>\n {/*<ExportLogsButton />*/}\n- </>\n+ </SafeAreaProvider>\n )\n }\n export default Sentry.wrap(App)\n"
|
|
1843
|
-
},
|
|
1844
|
-
{
|
|
1845
|
-
"path": "native/android/app/build.gradle",
|
|
1846
|
-
"status": "modified",
|
|
1847
|
-
"diff": "Index: native/android/app/build.gradle\n===================================================================\n--- native/android/app/build.gradle\t74465a3 (parent)\n+++ native/android/app/build.gradle\t7d171ee (commit)\n@@ -93,10 +93,10 @@\n defaultConfig {\n applicationId 'com.markets.manifold'\n minSdkVersion rootProject.ext.minSdkVersion\n targetSdkVersion rootProject.ext.targetSdkVersion\n- versionCode 65\n- versionName \"2.0.65\"\n+ versionCode 66\n+ versionName \"2.0.66\"\n }\n signingConfigs {\n debug {\n storeFile file('debug.keystore')\n"
|
|
1848
|
-
},
|
|
1849
|
-
{
|
|
1850
|
-
"path": "native/android/app/src/main/AndroidManifest.xml",
|
|
1851
|
-
"status": "modified",
|
|
1852
|
-
"diff": "Index: native/android/app/src/main/AndroidManifest.xml\n===================================================================\n--- native/android/app/src/main/AndroidManifest.xml\t74465a3 (parent)\n+++ native/android/app/src/main/AndroidManifest.xml\t7d171ee (commit)\n@@ -13,9 +13,12 @@\n <data android:scheme=\"https\"/>\n </intent>\n </queries>\n <application android:name=\".MainApplication\" android:label=\"@string/app_name\" android:icon=\"@mipmap/ic_launcher\" android:roundIcon=\"@mipmap/ic_launcher_round\" android:allowBackup=\"true\" android:theme=\"@style/AppTheme\" android:supportsRtl=\"true\">\n+ <meta-data android:name=\"com.google.firebase.messaging.default_notification_channel_id\" android:value=\"default\"/>\n+ <meta-data android:name=\"com.google.firebase.messaging.default_notification_color\" android:resource=\"@color/notification_icon_color\"/>\n <meta-data android:name=\"com.google.firebase.messaging.default_notification_icon\" android:resource=\"@drawable/notification_icon\"/>\n+ <meta-data android:name=\"expo.modules.notifications.default_notification_color\" android:resource=\"@color/notification_icon_color\"/>\n <meta-data android:name=\"expo.modules.notifications.default_notification_icon\" android:resource=\"@drawable/notification_icon\"/>\n <meta-data android:name=\"expo.modules.updates.ENABLED\" android:value=\"true\"/>\n <meta-data android:name=\"expo.modules.updates.EXPO_RUNTIME_VERSION\" android:value=\"@string/expo_runtime_version\"/>\n <meta-data android:name=\"expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH\" android:value=\"ALWAYS\"/>\n"
|
|
1853
|
-
},
|
|
1854
|
-
{
|
|
1855
|
-
"path": "native/android/app/src/main/res/drawable-hdpi/notification_icon.png",
|
|
1856
|
-
"status": "modified",
|
|
1857
|
-
"diff": "Index: native/android/app/src/main/res/drawable-hdpi/notification_icon.png\n===================================================================\n--- native/android/app/src/main/res/drawable-hdpi/notification_icon.png\t74465a3 (parent)\n+++ native/android/app/src/main/res/drawable-hdpi/notification_icon.png\t7d171ee (commit)\n@@ -1,8 +1,7 @@\n �PNG\r\n \u001a\n-\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000$\u0000\u0000\u0000$\b\u0006\u0000\u0000\u0000�\u0000��\u0000\u0000\u0006�IDATx\u0001��\tl��\u0001��������\u0007���e�Q�\u0010*b��-(D@\u000eMd�\u0002�\u0014��Y�eL\u001a�\u0000+�\u0012��l�f�8\u0006;�:�Є�i`Ӂ��\u0010\u0018��k����}W�{�㷭B��\u001e�d\t���|�a��\u0004�H@\u0001��)@p�\u0002\u0004�\u0004�\u0018H\u0000�˄�\u0003\u001a\t)n.��\u0014\u0003)$�\u0018�-Fr���\f\u0002l��Br\u0013��:X��\u0000e�I�\"ۆ�f�����.B��$\teBW���\b)�>3�\u0014��/K\u0003�����-p��+I\u0012�l�gR&{�\\JF�\u0013!HH\b���\f��\u0012���\u0017o�\u0013���\u0010 �dѳ���3�,�\u0013!\u0018@�@��4V�h\b�CR�q�\b�&I$\u0016�Y�}������(�E\u001eP$���\\\u0017[^\u001b����|r��sm1�b\u0000�\u0017\b\u0001���a��%�X�DG{���\u0010�6�`+��������\u001c;\u001c��x��q\u001e��\\Ű`�\u0003Y�|�h��a7��8��u\u001eM�*����A>\u0017�~6���-D�,�&ض�\r�ò�b\fE?ӆG����\u0003��\r��3Q\n-\u0007{���K�\u0000�Gcu�0�-.d�����D1L�Ѫ\u0000�H.2��b�Hrr��XYĪuCp�4\\\u001e�\u0017˛\u001892�9s�P6�\u0013��S���N'��Ɩ\u0018��\u000e\u0006�I��V0�$��\u001dw\u0010\tZ�Xr���(���k�9�R\u0004��\\�Y����i=k��7�G,^�=�\f�$�ظ��m�\u001e���(5�=L��͉�a:��84�t\u00198�\u0002g�F��d�S�̙��K\u001b���G\u000f\u000e\u001dJJR��wq��.�MB:�hPS\u0013Ķ\u0001\u0005�1E8b���h;\u0003]�qV���W+G�zU=����/*[q�\u0014�B6]~��w�3kv.�a�h�Iz�\u0006��XL1ga\u001e����0lt��$_`�|N�- \u001a�Hqk\b\u0001\bhm�e˦�ؼe8YY\u001a�\u001f\u0007Q�>\u0016\u0010\b��)\u001fƑ�=�]UO8h`� \u0004df;���A���\u0003]')�$\u0014\u0010�XH��\u0014`\u0001\u0002��P\u0017{K\\L����\u0000! f\u000b��\u000e����s+NS{\"�.�g���,\u001f�>즧�@�IJ��\u000bAo��������7�8v4Ⱥ\r�(�=�����p\b��;I�\u0010�d\u0000�[�O���\u001b��\u001c\\�$\t�\u000b\u0002\u0001\u0003O�ƕ�\u0012�oma���l�����u�W^jƲl�b\u0000\u0005<85�Ϛ����\u0010\u0002�\u0000!HH'\t�A�y\u000b_�\u0003e�O\b�St�_;�\u000b\u0017\f6�o��(\u000eIBF\u001c\u001e��G��\u0000��N�;d\u0012\u000e[�A\u000bð���8�\u0002��>:IH\tg;LF�z0\r�\u001a\u0018&��\f��\u000f���N֯�ǡ�\u0014��\u0012l�~R��{\u0007������3��|��:��\u001a.����\u0010@yY=5ǂ(\u0005:�\b\b��d{\u001d�\u0014��\u001a�,-ࡩ�l��LMu\u0000]�R��l\u0018qg:�'�(�>�^�S�����6\u000e�ŏ\u0010�\tQK0��LV�\rf�[�TU��%}t�!\u0012�H\u001f����d���\u0004#\u0016�\u0017��P�D��0wQ>\u0013'f����\b\u0014B@Q���b7\u001f\u001f�FJ\u0010\u0002,\u0004KPȴiٔ�m�Tu\u0010�A?�d\u0014�!��B\u000fo��\u000ev��,���\u001d]��2ۂ鳼̜�e��SH�P�i\u000b�y�\u000f�\u00181\u001b� ��M��a��7���'\u0010��t0�$\t��'`r�B�X�F\n-��:1,P6\b\u0001J�7\u001e�d��\u0002V.?M,h�\u0014 #�������\u001d\u0004L��ͫ����}����\u0001,\u001b��t�P\n-:�\u0006��<Ɉ�)L��I��#8�f��\u0001?��\u000bo��e�\r��U���q.1-��H\u000e\u001f\u001d�&\u001aR�\\?��ǧQ�����^�FR:� \u0005 \u0014Mua\u001aj�l�h���4�L˦ra\u001e�\\7\u0007�ډ��$\u001a\u0003��>\u000eM2�Q/�w�a��c��\n-����H�Fj\\�VR��\u0005��\u0010�r\b\u0002~�#\u001f\u0005x��N>�\n-0t���\u0017\u0017��CY(!�_0�R\u0006\u000f��2vL\u001a��Z����H��\u000eJ�\u0006)\u0005���4�\f�OC�<�;KӘ2=��~�\u0010�K��!u���Ɛ\u001a�M�+�mp9\u0001����\u0000G�\u0002(\u000bF�K��x\u0004ݭ�\u001a7D�&�m�\u001d�\u0003���h.P�\u001b&�?P����\u0016#��Hn1�\u0006����r���\u0000\u0000\u0000\u0000IEND�B`�\n\\n+\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000$\u0000\u0000\u0000$\b\u0006\u0000\u0000\u0000�\u0000��\u0000\u0000\u0004BIDATx\u0001��}�\u0016\u0004\u001d\u0007����Y��|#\u000e�ȍ\u0013G�ZF\r�.7�iJ�\"\bu�\"{A#�U˭hmY\rl�^j\u000e�\u001a*��\u0005\u0011.Z,�ڢh�\u0002)\u0018Ċ���o�u[����A�����9眡r\u0006�܈��\u0003U\u0015gAÙY�\u0005��,ij��0\u0015\u0007�*Ɛ�\u0002܀c�\u000e�\u0005Mý\u0017+�Q<dl�\u0017۰\u0010뜆$��\u0002_��CFh\u0018��\u0006܋\t�F�YF���[�\u0011�p]��N!�U؉�p\\\u0007\r����Z�\u001f��Z<��\n+�MƵX��8���\"ɸ$��\u000e�\u0003;��9\u001d4�L�\u001a|\u0005O��؎��`�K��\u0019���ުz\t[0/I\u0019!�E�\u001a>��x\u000e[����э�`/��\u0014|\u0006G��$\r�-�#�c\u0003��B-�tc\u0007�1\u0007�q%v\u0018E\u0003�1\u0007�`\u0006������\u001d��UI�\u0019��\u001bӱՐ�ڇ>�4(I%Y���Uu\u00187♪z�(�U�\u0013-���۠�z!���1~�d5���8j���$O�X�\u000fbcUſ-�zchj�,f\u001bRU���\u000b��4\u0016�����c-��\u0004̭���$�\u0000��achh.-�j7>����ة�E�\u0010��SU�\r7\u000f��\u001f���]/��{\u0014��}h\u001a�d<��CX�uUuR�$��x���i\fM���F���$�0\u0003�J�\u0018�㛸\u0012o���:��7�#:H2\u0011���2B�W�7�ZUǍ�d\u00026�ϸ\n+?��U�7�Hr\u001b��\u0007]��7`\u0016f�2��mM��q\f]8�]\u001fv�\u001e|\u001c\u000fT�Kƶ\b��\u0007\u0017�\u001f��\u001e���q\u0018K�ڽ��1\u0005\u0007�Hr\u001e��5���v;�$��F|\u001f��\u001f���I&�Q\u001cŒ��k�\bUu\"�_ХE���!\u001cĵU�'-�L��:a�\u001e\u001c���\u001a0$�d<���Ϊ�7���^t\u0019�����9<����_�$��\u0016����\u0016bCU\r\u0018�d\n+\u001e�>�]U��4u�I��^܊�����E�W�;X�g�Hr\u001e��\n+C�\\���U��\u0016M���\u0007?�\u0000���#FH2\u000e�p\f��j�po�>\u001c0(I7������\u000e\u001a:{\u0016sq\u0012שׁ#:{\u0007��Ҫ\u001a�\"Ia!6UՉ$3�\t��)�h�쇸\u000f���$��\u001eOVU�AI�a%C�]�\u001e|:�\fl�\u001a���\u0013FQƐ�0\u001d��&L�Nl�2l�\u0017�*FH�V܃;�\u0018VW�\u0017�B9MI^���\u0005��:��z<��U5`H����\u001a_Ǘ��S(��$\r\\�\u001e,�L�\u001c?�\u0006�a/&a)�TՀ�P$�`>\u0016`\u0016^�������/��(I\u0003�b\u000e�ckU\r8���\u0001����p��\u0003\u0000\u0000\u0000\u0000IEND�B`�\n\\n"
|
|
1858
|
-
},
|
|
1859
|
-
{
|
|
1860
|
-
"path": "native/android/app/src/main/res/drawable-mdpi/notification_icon.png",
|
|
1861
|
-
"status": "modified",
|
|
1862
|
-
"diff": "Index: native/android/app/src/main/res/drawable-mdpi/notification_icon.png\n===================================================================\n--- native/android/app/src/main/res/drawable-mdpi/notification_icon.png\t74465a3 (parent)\n+++ native/android/app/src/main/res/drawable-mdpi/notification_icon.png\t7d171ee (commit)\n@@ -1,6 +1,6 @@\n �PNG\r\n \u001a\n-\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0018\u0000\u0000\u0000\u0018\b\u0006\u0000\u0000\u0000�w=�\u0000\u0000\u0003�IDATx\u0001��}L�e\u0000��������\u000e\u001088\b����lӢ6WmNr�\u000bY�\u001a���X��i+)�����e��tnV�\u001f����67�fc��2�\u0002�-^�%\u0004��\u001f��\t�08��\u000f>\u001f�q]�Ò\u0011Z\u0001�%c�,1�\"\u0019âH\u0016!\u0010�P�#�\u0010�5I\u0014�\u0006�a\u001e!`��\u0000k\n-��\u0010GC�2�l�Y���'��U����C\u001bA��\u001e\u0012\u0012a8h�G\n-^}-�\u0003\u0007sК\u0019�۴\u0003�\u000edcG\f�\u000b\u0013Y���\u0010�(����\u0003tuk\u0012�%ӄ���\u0018��Z��\u0007|�|z\u0013)�!��h��r\u0006��,�m�����\n-\u0013�[\u0019ˤ1\u001b�ܜ��+\u0003��\u0018#-��$=\u000e\u001b��P�7����8���� �I&��@�p��A�\u000b\u0012\u001fkq�X\u000b/���?�&����Q,ihk\u001d%�>/^����yHK�^�_<�x\"�|Տ��C1�цK\u0017;1\u0006�2c(���8��U�TV�1�os��\u0003c���({�.g��\u0001�ϴ��w\u0018\u0003lz�O��F,�\u001c�یaJp�!&F!%���9�A\u001b+W��\u0005\u0014\u0015'��gs�����0����x~�a\b%\r�\u0014Q�\u0011\u0007�O�`Jwg��O��֑\u001cR��|}���k��$S\u001c\r%����j�\u0018�QD\u0019\u001d��<\u0012c�2�@b�\"%���U-��l,Ɍ�L\u000f�-INq1\u001c\u0012�AM0��\u0014X\u0016(�\f�\u001a\u0012�\u0004�c��I^/Ϧ�5��7\u001b\u0011���\u0018P\u0016\b\u0001;J���v�\r�I��)��<�}(������7��\u000e�\u001eAF���ogq�l\u0007�M!\u0004�h�U��/?\r�g]\b��'+���`\tx� ���t^y����\b�\u0014Q����U�Ȥ�&,4Ӷ=\u0013�\u0018h�\u001eBY��$��\u001e���{2��S\u001c*o\u0004m�&�⊁\u0013��p}�C��P�3��$\u0017��K$77�/.taI\u0018���qt��Qu�~��\u001bᓏ�@\u001bfS�Ag{�K\u0017:\tG ���ϦR�%�/kzI\t����)��g,��8�ˇ�[\u0018\u001e��$�(\u0016`\fx\\��8B[�\b�;X�6���{X��&+�˕���s�\b\u0016��\u000bƀ�\u0005M�!n�\u001e\"\u0012�@���~\u001b!�O�E2\u001a\\n�5`#$�K��$K�_W�T\u0015���\"\u0000\u0000\u0000\u0000IEND�B`�\n\\n+\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000\u0018\u0000\u0000\u0000\u0018\b\u0006\u0000\u0000\u0000�w=�\u0000\u0000\u0002ZIDATx\u0001��o��\u0003\u001c\u0007���w��+s\\�?\u0007Zi<�\u0014%���l�kg�h\u000f\u0016jm\n++���< ��E������4]K�\u0003\u001eL�Y���xt�(�+2o�گ~����C^/�\u001dI�&\u0019�H-��+��\"�t$Y�d�\u0005$\u0019�0��\u0017�,IRz�4���\u0010��\\�l\u001b�\u0007K�\fX@�[p\u001c�z��\\��ϰ7ɐ~w�\u0003|�[�HRI��Q|]U�z�p\u0010-܇��\u000b�%)�$c�����\u001e&t$\u0019�;�\u0003o�C�\u0005�b\n+�U5��x�\u0019��]���\n+W'�$+p\b/V��X���iW�1\u001dI�\u001aU�;ɞ$SX��t}��\f\u001b�j6� .�����үt=�)�Tկ\u001aI�0�9LTլ3V�#\u000bh;���=�F\u001cH2�A��g�jZ��xR�$����H�\u000f�W՜�$W�m��\u0007��[=�\f�\bv�F\\�a\\�O���`\f'4���\u0019G���~s�U8\u000f+�9��$�Ǧ�~�p\tN$\u0019��x���i$)���ú&���Nj$ٌk���N�����In�\u000el���\u001e�i\u001dIڸ��Nj$y\fW`kUE���)<�opgU��#�\r�\tw�Z��5�lDžU��\u001e-���\u001c\u00061�dX#�Ex\u0006\u000fVUtMb��0TU��S\u0016�d\u0004�\u0006\u0003X��UuPG�\u0001\u001c�1�PUO[@�\u001bI.�Z��\b��},�\u0001쬪\u0017�CY�$�c5��f쪪�����\u0013\u0004P�Qp��'\u0000\u0000\u0000\u0000IEND�B`�\n\\n"
|
|
1863
|
-
},
|
|
1864
|
-
{
|
|
1865
|
-
"path": "native/android/app/src/main/res/drawable-xhdpi/notification_icon.png",
|
|
1866
|
-
"status": "modified",
|
|
1867
|
-
"diff": "Index: native/android/app/src/main/res/drawable-xhdpi/notification_icon.png\n===================================================================\n--- native/android/app/src/main/res/drawable-xhdpi/notification_icon.png\t74465a3 (parent)\n+++ native/android/app/src/main/res/drawable-xhdpi/notification_icon.png\t7d171ee (commit)\n@@ -1,8 +1,14 @@\n �PNG\r\n \u001a\n-\u0000\u0000\u0000\rIHDR\u0000\u0000\u00000\u0000\u0000\u00000\b\u0006\u0000\u0000\u0000W\u0002��\u0000\u0000\u000b\u0000IDATx\u0001��\ttU���������_�K^\u0012�&�Pv�{۩ӱ�tl];\u0015\u001c�!Ppa逶�X<V=�B�*EkkK���푶.u\u0019O�N=\u001d�\"�\u0010V\u0013\b\u0001����{�F(B\f�he���#�^�f\t��4 �\u0006��4 \u001c�9J\u0000\r\b�i@8N\u0003Bg�ㄎ4 ��\u0006\u0004,S�\t��4Gi:�t�9Jӑ�#��i:Ӝ�ր��\u0012����S�p�\u001eN��)� \u0011�-@8c\u0014g�R`��qsol�3Fq�\u00143��몹���D*�0L�LPt�\b\u001f�\b��|kR\r�����F�O�0@)NI�\u0005Qpp�&��t�2��s�\t�\\��6�\u001d��*j>�LASY�\"��T\u0014�a\u0018B� <��\b���K:�钆�\u0016͍�{��8�\rIF�룭M�]�\tٸf��Z֬\u001f�ԙ5����Q��R��k֬\u001dΠ�A����5���\u00148-�L\u00188��\u0011��yt�\u0001Z�\u0015���K\u000eMw\b���'\u001b>��\u001b�#\u0002O>~�p��d\u0014'!\u0002����\u000e%\u0012qѲ/K*Y��\u0017\u000e���N:�0��j��\u0002�M����P\u0019u�˯P�S\u0012�TN3��r�q\f��I\"^�~s���y���d\u0014\u001f!\u0002��f�}�\u00186\"�7&�s�p��6%x��V�Yq6~�\u001bӤ3\u0001�\u0012&^Q���\u000e\u00120a��\u001c~��p��2��1L!��;n\u001f�\u0003ˆ��\u000b1~�d'ማe\u000f�P�[�pR�\u0013\t$���\u0016���D���[ho.�Jڄ#&w.x��i�|f8\u0019��0�\u0013\u0019\u0006|�\u001f�x�&\u001bs\b�OH�\u0016p\u001cM����\u0012 _�p�͆ߏ�K�\\ɂy\r̟���o�f߾,��J�ؚSQ� ���\u001b{3yr-{vgY��\u0001��e,�]P��ob�`���\u0014��5k��Hh��C{Z4u�z����dQ,h�\u001f�H���\u0019�Ŷ4�(\u0003�1�5S�Y�\u001f��ŊL��.[�N2b��sέ����\u0013)\u0017��\u0014c���J����d2%|~E\"a��C���x=\u0006�%��\u000e���J���e�\u0006�n�a���\u001e�\f\r�be+5�\u0014ZC��9x�����$\u000ek�\u0006� �(V�\u001fƜ�g���̸�\u0001\u000f6�I�ԩ5d�%^�\u0018�+&c�4��\u0012��-��6����\b��߬��CD\\`���b��m<�f$��חU����0��\u001aZ�g��N�pH\u0010��KxW�����\u001c���s\u0017����!\u0014\n-6S����;�\u0004+�\u000f�Q|�j��y+�\rZsZ&'py��h|!�\u0013\u0010\u00040\\BS���L\u0011�\n-h�-MCC�{��ɝw\r�@k�_?\u001d�K�<�p3�pL�Bx��\u0019ο(�\u0017a�����Z^}�\u0010�����\u001f\u0002e�֠-�lR\u0014��X��\u0001�\u0001�mN��#lK�\u0001\u0011�P\u0002�˥�z\u0014��\u0003\u0002.7<�\\�>�<,�e ���1\f��\r1\\e`[\u001ca\u001aB��\u001c���߽>�HE\u0019wݹ���:F8(�\u0016 \u0002��������6b1�p@�IW\u0004�Y\u001b�T�P\u0014���(�[X�|\u001f�\u0007x�0��?�p�t�&$\u0002\u0002JA[���k|(%��6WN�D1[\"�\u0017\u001c�\u000f\u0019\u0006\\xQ9Ѩ��\u000f�'\u001aQX%MW\u0014]�\u001a2\u0019\u001b\u0011p|\u0006\"�˅۾���;�\f\u001f\u0019@\u0019�2�0!Y\u0014~�j\bs�\u000e��O�c�\u0015�h���t�ao������ak�-�sX%Mw(��5���\u0007<\u0001\u0003�\u000e�\u0003�r�6�\u0001���G�\r�P�C�Z\u001f��a,��\u0004�;���+�\u0012�eщY&�\u0019�e�\u0010?\\�B��\u0006�e�\u0005�hlll[��\u0019ąNl\u000b����)\r�yf$�7�`�� o���ysw\u00110\u001d�nAkN�-�p�ݵ������$����2�/hJ%\u0007��@kN�*A.됈�\u001800�������D+\u0005�攔\u0001յ..�\\%K��M�B!\u0002\b�Ak�\u001a��q\u001c:1�nw(\u0016m�FL�ٚ\u0013)\u0005��_�`��A\u001c�\u0015�|�{\u001c�[�\",86��Iìi�(%T�LƟ\u0017�-i��X��6N�F;\u000e٢&֦�\u0004!�\u0017�1��,\u0014�65g��,P�#\f\u0013�)��{\u0007�˪�ͯZY����\u001f\\n��\f�\u001a��\\C,�����x<\u0006en\u0003�K���5X��Ur(\u0014m\u001e[���~\u001f�\u0018�n�D���TV�\u0014S\u001aOH�\u0016�o\u000f��p6������x�8A�`[t��jl\u000b�!�Dځ�]]�˥�|�f�Z\u001c\u0002\u0001\b�\u0014eJa\u0004\r<!���\u0016��\t��\u0013%^%�R�8\u001ca�M٬M$b���:��|S\r7�ܟƆ\u0014j\u000bnm\u0013�\n-ڡ�TNsü>TU)�,j�< \u001cӞ��ͬ�?_i#�t��\bJ�\u0006\u001c�C<fsK]���^�v�\u0001�]�>�\u00108\u000e\u001f2�\u0006AȤ-\u0002A�2S��C\u00193���V7�f�~�U\n-ۦ�L\u0011���Ŵ�}��FB>�\u0018�\u0005�?\u001b����'Vo��\\aY\u001a�\u0001;��\u0004�X��l*+�X��F^y��P@pl:0醠�T�f��\u0010/�i,���v�fv���\b����l\u0001&^^��s�bك�y��v�nᘖ���\u001e��~K���娩V(\u0005�I�U�FYp�@���\\6a\u0013R�\bz\u0005��Ĥ\u001b�A!~آ2��p,Ϭ�z\u001a�K��\u0012D�U\u0002\u0011��\u001cq��An_<�'~��3k\u000f�w\u000bǘ&�\u0018��3#˩���\rD ���\u001e\u001e�\u0017.��������\u0004C�-��1���%t�,�M�r��4\u0003\u0006y�nj\u001f���\u0010���\u0002�\u000e8\u0004C�R�\f�&�e�φ���C�|���W8Q<��eQ?<>Ŋ��X\u000e����\u0017�GЧ��[goc�sm\u0004|��p:�I7h\r�\\�e�5s�=͜=�ǿL�2A?\u0016\u0010[�\u0013��e\u001b��W\u0002�c��_\r���$X��=�\u0002��|�0!\\n�\u000f�Tr��{�8¤�\u001af�П��MpӍ;�\u001b\u000e�2Ak�d�M�\r>� \u0002��<�,i���\u0018<�ϴ)Qn��,�\u001e�R���4\u000f,�M{\u000e��8¶\u0000�|\u0012&Ϫ&_px�OqV�\u001fƈ�!\u001e}���?j%T%86 t���\u0005oj>!Q`\u0018B찃�\u0012��\n-0��(�]\u0010��5hܖ�u�x��8�-6�}\u0014���/���)K��>JE�����{W��\u001b4\u001f�e�w�\u000eX��<(|��)��9)ܞ=\f\u001f\u001bd��*��h\u0000��\u0015[�$X����^�P�Ũ�a^{��;n�M�\u000f�2�||&��RQ\u0013)\u0017D�eg�y�N\u0012�(F�\u001bdʌ*�g\u0010n�A�h���{x�6ʃ�e\u0001�'br\u0006h\rVI\u0013\t\u000b��46$�=-Aأ\u0018��r\u001a\u001b��cEB~A;�]L�0��HX0L͖w�h���0�\"\u0002�\r\u000e�.E\u000f���\u0014=���S�p�\u000bR�v�ޱ�t\u0000\u0000\u0000\u0000IEND�B`�\n\\n+\u0000\u0000\u0000\rIHDR\u0000\u0000\u00000\u0000\u0000\u00000\b\u0006\u0000\u0000\u0000W\u0002��\u0000\u0000\u0005�IDATx\u0001��Ah\u001d�\u001d\u0007���x�\u001c2��\rr蘰\u001e\n++�Be�e�Ce)������B\u000b�U��AK\u0015+lء\"�\u000e\u0005\u0005\u001d\n+-�bJ#\n+\u0016,,c\u0001;\u0014�A��\u000ez�!�\b=��Ã�\u0002�`M�{I�Z\u0018���[�n�m�\u001cI�r\u001f4�cIF0�\u0003{\t�A�=����0��$-߳�\u0015�4�CUնv�h�\u0011|�q��.%ن�U��C��^�\u0001\u001c�\tw� �V�E�a�]H2��M|�1}4�\"�q\u001c�\u0015\u001dǒ\u001c�\u0006I\u0006�\u0018��8��$��@�\u0007�\u000f\u001c�\r���$\u0007�\"^�_q\u001d��v�\u0011�\u001b� N��\u0000ƭQ�1|�!,�BU-�aY��x\u001b�WճX�\u0006<�K8�d����rU]�����2vZE�f�\u0013�\u0004_�Q��U4�l�y\\�_��a��!��\u0012>J2��$�؉s��\u001c~�d@\u000fI��\t��OQ��U4�\u0011\u0006�\u0005��\u0013\u001cӱ��\u0016�(6�|��ۍc\u0000�}�4\u00061��$#�\u0012[�hU���ژ�lU-ZE\u0003O�\u0010\u001e���\u0011~��eYU}��\u0018��n�\u0007sUu�-��\n+�b�\n+I���0�\u0007��eI6b\u0004g�A��ft�IJ����B���J�uU�aY�\u0016�pTwg�$ͪj'ـw1�70YUm�څ6f�ACoװ�-��$��T�1\u001d�:�u�1Z\u0018M�\u0015�c;�U�SU��]\u0013���Ek���\"���)��$��\u001f�U5�����y���p\u0003\u000fU��VH��\b�Z��A�\n+U��n��S��\u001e�\f`\u001e�0�������ƌ5j�m\u0001-]T�u<�\u0001\u001d\u001f�\"�&|��x��\u001e��%���lU-Z�����c=T�U�F\u001b/Y!�8��\u0006�����G�\u0016Fq�\u001dh�m\u0011-}T�l�'�n�WՉ$M���q\u0001���Ս��G�\r؆m����\"\u0006�\fT�\r=T�{I6��$�؏a�PU'��\u001e�Vբ��\f`\u000b�1��a�o}��ۼ�!\\��q�\u0002ob\u0001;��5J��(^Kr\u0000��9�`\u0000m\\��q\t��\u001cv4���c��=�a������Ό��c8�_�_x\u000e\u000f�\u0007U�\u0010��\u0017s�QUKM�-�\u0018�C�\r8�1��g������e<�KUu�\n+Iv�\f.aGU-Y��CU]O�FK\u0017I��\u0003�q\u0017��0��UuA\u0017Iv�\ffp�������<����0�p\u0005�T�U]$i�<�YU't7�&�u�d\u0017�`\u0006����\u0016M�-`���\f�M\u001c�{x��n��\u0004����`��歐�0^�\f�UU�\n+M�}��eI6�\u0003l¡�zO\u001fI��i���fu�d\u0003Fq�\n+I�`\n+�q��ںh�o\u0011?I�\u0017oc\u0001\u000fW�e}$\u0019�)\\�\t��D\u0013�n��\b�p��&����5��\bfp����#I\u0013��ľ�j�m\u0002sU5�$��U���I�h�o\u0001M��5\u001e���؎ǫj^\u000fI\u00061�snJ2�Wq��&�AS�`\u0001��{<�d\u001e�0]Usn�d\u0014�㕪���q\f`ڲ$S8���ek��GU�1��$M�b/��\u000fI\u0016�!��W8�K8nu\u0013����$S8�ɪ:�\u000e���d\u0004�0��h��I�UU7��d\u0010��\u000b�)\u000eb��N�C�\u001eH�\u0015��\u0007����8���Zr�$\u0007p\n+_`+&��\rw��cI6a/&�\u001570�s����$�1�6�Uմ�T�GI\u001e�8&0�6�0�&�Uմ�A�O�\fa'�`\u0018��jںu���_�/V[A�_�c�\u0000\u0000\u0000\u0000IEND�B`�\n\\n"
|
|
1868
|
-
},
|
|
1869
|
-
{
|
|
1870
|
-
"path": "native/android/app/src/main/res/drawable-xxhdpi/notification_icon.png",
|
|
1871
|
-
"status": "modified",
|
|
1872
|
-
"diff": "Index: native/android/app/src/main/res/drawable-xxhdpi/notification_icon.png\n===================================================================\n--- native/android/app/src/main/res/drawable-xxhdpi/notification_icon.png\t74465a3 (parent)\n+++ native/android/app/src/main/res/drawable-xxhdpi/notification_icon.png\t7d171ee (commit)\n@@ -1,30 +1,25 @@\n �PNG\r\n \u001a\n-\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000H\u0000\u0000\u0000H\b\u0006\u0000\u0000\u0000U��G\u0000\u0000\u0015�IDATx\u0001��\u0007��u���������9gz��I!\u0001B�ATJ$!�\"��\\a\u0015DA�¢����\u0004\u0010\u0002RteWD\bj (뢏��M@@A\u0004�HBKH�I&�眙���[2�'�I&�\t\n-�y���'f�T\u0004\u0014\u0010vO\u0001at\u0014\u0010@\u0001\u0001\u0014\u0010ޥ�\u0000\n-\b����^\n-\b�R�%�L\u0001\u0001\u0014\u0010v���^\n-\b� \"J�\u0016\b�02\u0005��SvO\u0018�\u0002��)\"��\u0002) (�v*`+�){��?�=SFG\u0019=et�w��\u000e�~#2�7\"�~#2�7\"�~#2�7\"�~#2|�\u0018\u000bD��0|�\u0018\u000bri����0|H�\u000ehQ�wv95�A��C��! \u0002}��ܿ��څS9�35�Y0�\u000f��\u0003�\n-�\u0003��\u0016'�\\�eA��0��`\u0007\u0004U>P�\u000f�1P\u0004f|4��)Q|\u001fjjB�Q\u000bcx߉�\b�0*�10\u0006l\u001b,�}f�Pt�y''��lD �\bP���<E����mC&�l�T�9E�3�^\u0012�\\��z���\u001b}İOT�\"\u0018`�a\t\n-y���\u0002ѨŌ\u0019A�\u0019E\f�@\u0010ֶ��8<����ȄI!|UD\u0018�a/\u0018\u0003�<\u001c|\\�g_>�%��P�\u00180��mÆ�>_���ں\b/���WW�\b�-\u000e>\"L�&Ų�}a\fx%H�\f���o�{ \u0017\\��I�lݬ\u0018�\u0011ٌ�10�V�\u001d\u001a�k�1~B���(}�\u001e���\u00048%\u0010a�<\u0017\u0012-�c>RI��|y\u001f'�\u0010CDhi\t3�\"�1�l��W\u000en\rsޗ\u001a8aN\u0015������\u0013O\f�2��{��0\n-\"�K)�\u0015\u00167�8�\t\u0013�\u0010\u0011�a��l#�>��um>��0ZƂ�\u0001��s���Ghk���K9:��p\u001c���0�;���`۰y��Qǖ����5�B\u0006UX�j��7�\u0004B�*#�\u0019\u0005߅��!�[�JSs���\u0019jk�\u0004��`�p�խ�m-�\u0015CD�\u0004��#߃���Skp\u001c�'\u001e�2����\n-d�.��a&%\r����*�\"\u0006J��s\u0003�|[\u001d�Ϩ\u0007�m\u001d9jj��r\u001e\u000f��\u0017+�\u0014\u000b \f#\u0010\u00011\u0010��|eA\u000b�\u000e���\u000b�<�t\u000bC\u0019���\"/�؋�)_�b<�fDq� T�P��)Q�ψ��[�g\u0006\t��͍.�!�x<Ȅ�l�\u0019E�Q1\u0002�\u001e��!½���ɳ\u001b�\\e��-��� �\b��ΰau��\u0001\u0011��0\u0002c�c��U㘏V�ё㊋7���\"�+��<�@\u0017�����\u0013�|eA\u000bY�\b\u0004\u0005\u0011v˲�6p�\u0004\u0015�\u0001���!ֽY \u0014\u0012\n-E�t�!\u0014��m\n-�ʱG\"\u0010\f\t����Ϋ�\u001bwM��C����s�5����^�\u001ePF hX���δ�\u0015`T\f�#З5���\t|��*��j���\\KY��d}J%%\u00184�ʢ\u0005\u001bX��\u0010�\u001d�d���l�S\\e����l>1�\u0012Dx�?�(��@����Qb��\t\u0011����\u000e�R\t��\u0016\u000f�h\u0012W-�D]}����6>s�\u001b<�h?_�|\u0015\u0013Z�hk���\u001f�\u0018� �2*�]�\u0003��\u000b�^���Sjغ%��~�I�d�Y\u001fM2��Jb1\u001bc\t����¢�7�yS�\u0003�%Y����+x�*�a�ж����\u001244Dho����Y\u001a�\u0005���\u0012ж����4Ԇ\t*��\u0013\u001106�\r)G\u001d�`��'qđ��t\u0017��6�R�%Qa8�\u0013�l���\u001e\n-�b\f�2*6���M>�]��9��\u0011\f\u001a�Q�/^�L,f\u0011�X\u0004�\u0006�2d�\u001c��\u0010�\n-\u001b����v\u0016-n���tn+��;��h1���\b�\u0012���s��UX���_����\tnIq�7�*�8>--!�+\r���������v�\u0005�h�O�Q^\u0011�����vk;�����\f:�हq��ctu�ypy���\u0006�\b\b�b�g���m�i�����-�B��\u0013\b\b���P�c��\u001c��J\u001c>+I��&\u0014��=HF�ٕ)\u0016�\u0006��:�/\\܂S���}=ī\u0005�\u0003\u0011\u0018�U���2�\u001fX�֭9�yw?\u0013�[\u0014\u000b�*�@gw�Bޣi\\�d�E:�aY��� ��IS#|si33\u000f�`p���\u000fo��j'\u0012\u0013���AE�p�Y��*O=ه�*aE\u0000a�l�����\t��\u0014��O����5E�w\u0015I�\u001c�~���14��r�$���$\u001e�0\n-�\u000fu�����xhJ\u0007����\u000b/i�/�Y�6Cy��\n-\u001a��ͮd�W^Lc\\\u0017�\u0005U@@<�\r���>�\u001d�q�=�\u0000\u0014�r��5\\|Q=\r�Qֿ=������c)j�\r(�>\u0014J0iz��3b���X�X\u001a_���+6N!\u0010�L�㛋��>HH�lA\u0000˅`\u0004��R(xXF�L\u001a\u0002F\u0001�s�����\u0007�i�\u0010b��Z��i\u001c\u000b���@W\u0011U�o\n-r��r��\u001c�Z�&\u001c\u0000�\u0001\u0011�\u0005-p�.�CCC���\u0000Vw�lA��\u0016�]��ig�\u0011�ؼ�f��.܄햨,\u0013�\u0007U�\u0003\u0002=�駗�,\u000f���^�^�#^\u0001��\u0015�.\u0018[�U\t�j\bE��\u0005!\u000b�\u0010 0��t�����:\u000b+ �0�)*X>�-n�_�r��8w��J�\"���1ǔ��\u0014��C�[�!\u0012\u0000\u0011v�\u00020�ﱵ�@(l\u0018_\u0013��B�\t\u001e|d:�>��Ԁ��wn�Sg��<�\u0012�\b�*��W\u0012�Ü0�\u0012U��G;ɋ\"�=�]\u0010\u0001��\f\u0013\u0010a\u0007\u0001rF\u0019*z\u0018\u0011�\t\u000b\u0014\u0010@A\f�@��s�\rm�N�0yJ�%KƳ��\r�rZ�`����tty�4\u0018�c\u0007\u0011�K�l�R$\u00100L:$Ju���\u00176RV\u0016`��!n�~3�����RpJ�0a�e��v�\u001b�TPU\u001db��A�}:˸&���lvG�%\u0005D _�\u0010\u0003���w�\n-��.U(�\b����Wo��[�yh97/�Ȥ�e��%��i��*���D\u0005:�\u0015�,�Yg�\u0010\b\u0018�\u0011V>��-K:ps\u000ee���\u001e\u0002�\u001eTVZ\u001cb5��<��\u001e�\"\u0002ʘ\u0018��2,�q\u0011\u0011�q\u001bw\b�\u0001�\u001d|\u001fB6t���OWn���ȑ�*�'\u0002<�����.�\u0010��\u000e\"�+D\u0012�D�b�h�&�r�?W���7b\u0017\u001d�\u0006|��\u0018\u0003�z|��t\u0015u�\u0011־5�C����\u001b|�11�-aX>�!\u0002��!�\u000b\"��^�\u0010�\bkW\r���m�u}<Oi�\\�,\"�\u0002�0ˆ��\u0012-����&��|\u000b������s�\u0015o�be��)\u0006_�-Dž�*�ٳ�(\u0016=�z2E��p\u001de�l���*d�\u001eۅÆ�-�(���\u0010�\t�/�c��1N�_�9�5��\u0014��.e\t!\u0010\u0014�vyL=0���[�6�\f�\u0012\u001eX�����`ԣ�\\�\\vK\u0015�.L>���Ӣ���x�4V\b\\\u0007D\u0018\u0013�1��P\u0018t�.\u001c�0\u0001\u0010�n��䉆\u0005״�x�����z�\u0014\u0016/�@GW\tw\u0010.8�����L$b���C,�w\u000b��|�\u0003&X8%Pe�\u0014\b��|\n->wA5�D��>���^+P[/�2f6c\u0010�����<%\u00142ؖ ��\u0000żR\u001d���蠲2�G�����5����q��\r�:�\u0012�\u0016~��N�~����E��78%e;Uv�2P�+��G9hF�b�㿞�%X�>�\u0019�@\u0004ry\u001f��\t\u0004\r�\u0012P��\u0016(�\\���\"\u001c7��C\u000e�SS\u001bbh�a��\u000e���\u001ej�J�Zp\u001dFŲ��\r\u001eW-���<Ȫ?���\u0017�L�d�:�\u0013���\n-vP\u0018L�8�O(h\bX�*{$\u0002\u0001\u0003�{\u001c֯�2�0�UA~��\u0001n����7�k�(\u0015@�Q\u0011�L\u0016�8$�q'Ԓ<p_\u0017��-D\u0004�R�S�\u001f\u0005�\u001d�0�\u001d�nٌ�� ��q\u001c\u001f;`��\f����[v\u0000����\u000f��K\u001b�ȱ�\f\r9<��\u001e���\u000e�\u0005��zC1���WR\u0003�9��\"\u001a���+�<�2K؆�n\u000f1 \u0002\b�\u0010V!\u001a�@T��`Y`,P\u0005�c'6c`,��PJE�HĦ�ޢ�\u001b\u0010@y\u000f\u00110\u0016�������3�6��\u0018����������i\u0002F�G\u0005�\u0003\u0011FM\u0015\\\u0017\u001a\u001bmf�T�*�ׇ���\td2.��G.�\u001dr�f=2\u0019�\\ʥ����|R}\u001e�[��!�pThm1�%�1\u0010\u0003�\u0019%_p�'\u0002ԴXlnW\u0002!�Wv0\u0006r9�\b\u0007����;��|���\u0015},�y\u0013ۺ=�*\u0005�\u0001�g�Y\u0016��p�)�L�\u001c�XtI&\u0003|lv\u0005�%\u0018#\u0018#\u0018\u0003\"�v��>���>Ţ���<��.���M ,����\u0018�@\u000e�P�,���P�C \f(�\u0001\u0001����8��K.i��i\tz{�,_���Xڋ\u0018��J�sA��X6�|\u000fT�%;(��p��\u0015��\u0016\u000f-k��g\t�lj+-*�,\u0012�\u0016�\n-��2�p�\"\u0016�\b\u0004�h̦�1JY\u0019�@]]\u0004|�/��B�ò\f�r�\u0012�\u0014\u0010p<\u0018\u001cR���\u001a���\u0016��A:;s\\�`\u0003���\u001c�q0��K���M>�\u0007��\r�b'\"P�(G\u001d\u0017��q��\u0012?�����x\u000eX\u0002\"�zPJC�@�\u0016�A��\u00196_�����(��˿����[I\u0006؉�\u0018\b�\u0010�f\\�\u0011�+l�E�\u0013�PP��a���ff\u001eVA&���O:�ᆭT���\n-pK���\u00140��\n-7��Lcc�;o�Bv��\u0018��X�v��\u001d��\u0011��<��[X��e�D\u000b�\u00061`\f\bBY%$|�\u001d��\u0012\\~]\u000b\r\rQ�6�x��\u000e�.룵���@���\u0019+�l�C\u0004�q�|Q)d�#�\\uC\u000b�MQ�������y�\u0014\u0015Q�\f�%@�%;\b�C�%���w�j�s[�p�b0�!\n-\"\f\u0013�Ԑ��c\"L�^N_o��\u001f\u001a`�x\u000b�e�z�`l%;\u0004�m8�\u001a>wA\u0013���믥Y��h{5ǸZ�))�b3\u0006\n-X@.�]$j(��K�h`��Z��\u0001֬N�pa;���D#�\u0001U@�%; �k��:�=��`а����__�*\u0001�� \u0006�i�yU�B�_?7H�\"�r�/)�\u0014� �oV\u001a&\u0007����9��������ɭ�m�8.�Z���-�1�@\u0018\u001c�خ�9�/��@�LM2�v���6�����F \u0014\u0002U@�=�m}>_����}�D�Y�����C\u000f\u000e0e��ux�b\u000e�\u001f\u0014��S�\u0018H�x��^$�8%�\t�\u0006�����,�v\"e�\u0000�]E�~�FV�\"M�8�+����f,\u0014\"!\u0018\u001a��=e����k���'O?���BP��\u0019�e�`\u000f�;����4�|������\u0007��:�P*�\b;��/p�\tI�kB��R��k��\u0002��m�@�R�\u0018�/��O�\u0011\fY��~��N\u0007�Vg\u00197�� �\u001eٌ�\u0002�ra0塀1��y��Z���2tu���P�h��B���\u000f\"���� ?�\u001czt��j\u000bUU!~��V���EE\f\u001c\u0007D�A\u0015Ba!�7�<��P�b�^��>͵B $ttyL�\u0012b��V�\u001f\u001cǶ�\u0007�o��{�\t�S�4�.�f�6_����\n-��й��7>���,nsȡ\u0015�qV-s��(�2���GW�G�\u0006�\u0005;\u0000�0�\u0018(栱9ķ�FyE���͕_�ĸ:��\u0013;\u0000���9��`�\u0019�tlͱhA\u001b�uBуR^8��\u001an�}\n-�-\u0011�~+˝__�=���\\'��-�f,\u0014\u0014\u0018\u0018t�k�V�j��:��\u0013>\u001e���ʘ1����U�>W��?\f�ܳ��{5˖�\"�A��\u0012|\u000b\u0012�!�]<�X,�3O���[8`���[�\u0019��OVc���\u001e\u001c�LQ\b�l�Y��q'V\u0011\fZ<��.��N'��.2u��TT��f\u001f��\u0010\t\t����_H��g�����˘=��\u0003\u000fJ2��Z>v|\u0015�\u0001�W^J��K)^��\u0010=y��ۛ�qX��V\u000f��om!\u0010��\\v�\u0018��Q>~|��S\u0013tw����i�\"Lm�r��f\u000e��$�sY��6n����\u0004�k\u0004�\u0004\bcb�/\u0014|\u0005ˀe\u000bV\u0004\u0012���L���3���9\tf�*g�!q�R��gՑN���\\��c��\u0014������\bąH\u0000PP\u0005�\u0001a��A@�3��\u001e\u0011X�����\u0012W.l��3\u001b��-V�!͝��ycU����RAQ\u001f\u0010��jm�h\u0011�J\u0018�>�\u000e��Bs�A�����+/�y�\u0014o��\u0003�%����� F�\\��� �r�L�g�ϥ�_\t\u0005�P�a�\u000f�\u0003��D���\u0006ry�g�Ϲ��pƙ�x����\u001e\u0016.�LwO��\u001a�)�\b�ʷZ�/Z��H\u0004P�}\u0010��\u0010���!���\u0005�z2��GR��<��RU\u001df��\u0004G\u001f�`��\n-�N���\u0010z;\u001d^]�\u0012�d�A=8�j�9��|�e��2\u000e;��L��\u001b����{]�>���{ �����+R\u0005�\u0018\u0016�A�D\u0019 .�����\u001b�9rf�\u0013�s�Q�L?0��9��;��L���U\u0003<�d��Wf�T\ts�UaYB}C�R���UinZ���\u001b\u001d\u001a�\r�\"�>�+�Ĭ���!U\u0010\u0001�\u0006\u0011(�`ۀOe��4!�Q3\u0013||N�ISbTׄq\u001c��\u0002�=%\u000e>$�e\tٌˏ\u001f���\u0007�\u0018�rIT\t���M\u0001��oL�a��0;\b-�\u0006|��Z�\u0017�{����R\u001d\rr�W�\u001c}L\u0005\u0013&��2.�*\f\u000e����\r<�� \u00151�%\u0005����\u0003�\n-(�\u0002\u0006�\u0004�m��\"�^�I���#�\fq���$\u0013\u0001���ޭ\u000eu�\u0004����CD\u0004|\u000fJ��B�x�\b�or��ovb#ē�l\u0012J%\u0010����J��\u0018\u0016�\u0014�D\u0010����A��\t��\u000f�\u000f�\u0007ð߈\f��Ȱ߈\f��Ȱ߈\f��Ȱ߈\f���\u0000;J+\u001e��'q\u0000\u0000\u0000\u0000IEND�B`�\n\\n+\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000H\u0000\u0000\u0000H\b\u0006\u0000\u0000\u0000U��G\u0000\u0000\n+�IDATx\u0001��\u000b��\u0005�\u0000�����[��\u000e\u000b�%�'\r��[ϵ�\u0004��f�3\u000e��U�ըgؕ\u00167r\bX�z��\u001e��1�i��hK磻\u001eF����Erʭ\u0011�\bA7��\u0003|ϙ\u001f3����?�\u00063���aÆ\r\u001b6lذ���.��'�Ә\u0017\u0011O�\r5�E2�\u0005\u0017c\u001a�̛7������Lɮs :\u0015&b��Pɮ3\u0005�\u0015Z�a7T�\u000bdf\u0003��V�\u0016���\u001ca7S֏�,��\n+ߏ�͆�x\u001c���\u0016��\u000f�\u0005�v#%}��\u0006Lǵ�\u0016�3��И��(nT�@�wIf�f�i�٤\u000e%}�8��x��\u0002|� e�\u0018����Èx\u001e]h�_\u001bb�٘���]܅�ԡd\u00072�\u0013\u000b1�[ڰ03�\fN'�b\r\u001eWX�p�!��{�\u001c܌\t\n+ϨC�;df\u001b.E\u001b\u001eE���cNf6�\t�وN쇕X��\u0014�h�̱�@f��?�\u00124+��\u000fԡd;�9\u0001��G�\u00043�\u000b�`\u0011V�opYf�~\u0007`\u0012\u001apkDT\u0015Vc\t\u001a�i�����ى\u001f�lT�QaQD��C�6�ٌ���u�\u0015\u0011��<*�%.�\u0016|\u0002�_\u001bڱ\u001e��-\u001b�\f����\u0015;!3��Y�\u0013�\u001c��\u0011o�K�Jޔ��8\u001f'�5̉�G\u0015�\n+\r�\u000fW�\u0011���i�3\r\u0015<\u0018\u0011=�����)֡\u0015mꔙM�\u0004\u000bp\u0010�\u000b�a%Z�\u001aϩS93��l�D\u000f\u0016���\u001c�=��\u0018�+1\u0012��3��\u0007#b�~df\u0013NC\u0015w�C��YL�љ���j\u0000�وN��1x\u0019W�\"�\u001eW#�(^P�\u0012>��Є-���p;��L�\n+\u001a#b=��wЊ98����\u0019+��;DD\u000f\u001eA\u0013\u000e��js\u0016n��X�Y�\u0013\u0011�\u0018��x\u001d�E�Fu*a.F)4��8\u000eG�\u0003\u0018�Ќ&o����'�A'.��&����ct۱�P8\u0012\u0007�GfV2�\",���\u0005>�oFD�B\u0007Z�\u0016O�\t%\\��p\u0015�ř��C�/>�Є\u0011������_�\u0018ܒ���@f\u001e��X��\"b�\u001d���X�6t�Cf\u001e�{p\t\u001ap\r>\u0012\u0011K#��M�ل��\u0005OD�2;�\u001c\u0011��Gf�(�F��-�\u0002\\�\u0013�lf^\u0011\u0011=�n2އUX����Ѹ�v2���\u0000�X���+\"6x�\u0016���b;�d\u0000\u0011QUhD��DD\u0015��JTp\u001eN���\u001c�#�'��7��\bzp|f6�&3+�\u0007܀v��9�9\"6�C�Ё^t�Ie�Y�v��\u001d\"b\u0013���\u0003q\u0016���\u0017�HDlE+&c+�\u0019\u0011[���\u0004>�\u00133�\u001e�\u001fWb\u001azq\u000b�\u0012\u0011��v\u0012���\u0011��N*�MU��o\u000bp\u001f\u001a�\u0010�\n+�a\u001cV�I\u0003{\t˱\u0015�p\u001c��iX����ֿ�،�\u0018��ڬVhշ��\fO�\u0010���V��2\u0016GD�\u0000\"b\u0013��U\u001c��0\u0019+�\u0005,��\u001e���CЊ��3�PV��\n+#�!\"\u0012?����=L�\u001d���X�\u0006�Y�\u000bx\u0015mh@\u0017�5js\u0006\u0012?D�A(�M�\u0006\u0010\u0011��%�\u001aS\u0014�a��L�l�b=�\u0015\u001f\u0011k� 3Ga\u0012^�O#b�A(�M�B��<��b\u000eF�\u0019�d\u0000�9\rW���⑈xM�:�>��2�TV��\n+��AD�f�5�\u0003s�)<�;�@f��\u0002|\u0016����xZ\u001d2s\u0004&a\fe\u0006��6�\n+�j\u0014\u0011���:\u000e�t���_F�R���v,�q؊��ƈX�~-�\u0010\u001a��N��\u0012�Q-�M\u000f��@u���̜�ј�K3�X��\r8\u0016�p$�c!���\u001e;�\u0005\u001d�E�:df\u0005�8\u0018G�\rݡ\u0006�y\u0018��8�'\"�ꐙ��\u000e\u0013q\u001f��\u0014��&|\u001f\u0017E�R�����B�\u001d\u0011�{��l�>�\u0007���q0\u000e�_x������b=ơ\u0015��!\"�g�\u001c܃\u0013Њ\u000e��n̍�U\u0006o*6�;ޔ�e쏃ъ6�)�a\u001c��P�(l�\u0012�-�M\u0015=\n+\u0015;�\u0007�\u0015\u0017�\u0003��/�5\"�\u001a��<\u0004��\"���y8\f���\u0018�\n+J\n+�x\u0018��$���\n+?�\u0005x��6��[��X�\u000e�ٌO�<lƳ�\u0011\u0011K\r�3Ѐ1�R���x\t/`\u0005\u001e��X\u0015\u0011��,�P\u0016<����5�TV�*z\u0014F�Cf\u001e�/�\u0013H,�Xn�d�(L�f��*��*t�\u0019tGD��df#�b.Z�\u0004\u0017D�\u001a۔զ\u0007�\u0014ޯF�ن;юF|\u0015��EC�\u0003�ï1\u000bK�JD��߱�\u0012\u0007b9.���SV����Cb�\u0001d�H����\u000f+1'\"�5�2s\u0004&a\f���\"��\u001f�وcq\u000f\u001a�\u0018gG�\u001a�PV���E�~df3��S�\u001b�a!��AfN@\u0007\u001e���\f�\u0005\u001fB\u0003\u001e���~df#N�B4�~̊�5v��v�C\u0015\u0015}��\t�\n+NP�\u0019�#b�\u001adf\u0013n�\u0007�E�n`-�P�2�c�\u0010\u0007b1f�y}(��+=3sLD��6�ل�1\u000f�X��𭈨�Af��q\u0014���js2��\u0015\u0011k�!3+8\u0011��\u0001�qJD��GY�zPE\u0003\u001am���1\u0003_�^�!�`IDlV��lĩ����u�R��\n+��!3+�\ff�\u0001�cVD�\u001a@Y�֣\u0017\u0015����l�B\u001c���.\\\u0016\u0011+��\u0010�ľ�\u001a�\"�j\u0000�ن���R}���\u0018�;1\u000fϫAY�^��\u0007Fe�Gq\u001b�b\r������Cf6�z\u001c��࢈�U�Sр\u001f�y\u0015��5���\u00111S\u001d�j�\u0001o`$�F;Z�\u0004.��{�)3+�\":�\n+\u0017ED�\u001adf\u0005S�\tK��df\u0005��l��\u0016,P��\u001aE�\u0006�����\u0003p\u001f���ꔙ\r8\u0011ga#��OԮ\u001dmX��\u0011����\u0018�۰ \"^Q����C ЋE(a/�W�I��\u0016|\r�GDU\r2�\u0001�q\u0000\u001e�\u0013���Q��ϡ\u0007�F�L;��>��\u0000���;�\u001a�e��X�e\u0011�I?2�\u0019sІ�X\u0018\u0011U��\u000fG�\u0011\u000fDD՛2s\u0014fc\u00066�&\\a\u0010���0~�6L��hC\u001bN�Z<������c�a\n+�1'\"֫�Xt*ty�\\�@\u0005��\u001d\u0011�� �A��\u0006��T��\u0016�`����7�\b���q:��\u0012.ĝ\u0011�E\u001d2��X���8!3Ga.>�W0?\"�5\u0004�\u0006!\"�`\u0005Vd��@\u0007�\u0012\u001d� .�+X�_�h�\u001b�(\"���I\n+�3s\f��\u0019x\u0005\u000bp�!\u0012�\u0005�ق\u0016L�'p8\u001a�H�p\u0007��Qf��sX��Ņ8\u0006\u0015��m\u0011�c��?��l��p\u001a�c,�TX�.t�Y���^}�̋�\u0000�!Љ\u0017�Ո��\u0010\u000bD�و\t8\u0002G�\bL@#����=,�3\u0011��v2��o�8lF\t�1\u0017�F�&C,�\"��'�b<N��Ќ���$\u0016�+\"6f�Q�\u001d�\u0014zq\n+\u001e���wA�Mdf\u0005�8\r\u0013т\u0016�^t��c0\u0002������.\n+���,a\f:p4\u000eF\u0007���9���\u0011Q�.\n+����\u0013�\u0016����\u000fg�ረ\u001a6lذaÆ\r\u001b6lW�?x�\n+��\u0016�F\u0000\u0000\u0000\u0000IEND�B`�\n\\n"
|
|
1873
|
-
},
|
|
1874
|
-
{
|
|
1875
|
-
"path": "native/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png",
|
|
1876
|
-
"status": "modified",
|
|
1877
|
-
"diff": "Index: native/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png\n===================================================================\n--- native/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png\t74465a3 (parent)\n+++ native/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png\t7d171ee (commit)\n@@ -1,52 +1,12 @@\n �PNG\r\n \u001a\n-\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000`\u0000\u0000\u0000`\b\u0006\u0000\u0000\u0000�w8\u0000\u0000#SIDATx\u0001��\t��u�������~��oN\u0018��F<QDE\"\u0019\u000e\u000fd4�ʵ��mW�\u000eS�l��r7m�,��03�R+@<8<R<SSD\u0011��af�������~���,\u0016Q�1b\u001f<�R?��,��M�w$\n-* \n-*t\u0013\u0005\u0015ޑ(��(�T@�n*t\u0013��\n-��\n-oK�n*�F\u0014T�&�.Tx[��\u0002��B7QP\u0001QP�M��\u0002��\u0002��D�\u0002��T��f\\���&�\u0002��\u0014\u0010vR@\u0000a�\u0014\u0010v��\u0000�_\t%��\u0000\n-\u0018vR@؝��\u0001\u0014\u0010�J\u0001�Nʮ\u0004\u0010@\u0001\u0001\u0014\u0010v��a'\u0005��\f��aϔ�\f�( Ơ��*�tSvRޚ�+��)�S�����Wʞ);)�R�L�I�Iyk��])�L�j\u0005Q�\u0002�AoJ7\u0015�A�����p�~e8h�2\u001c�_\u0019\u000eگ\f\u0007�W���+�A���\u0000�\n-(\u0007,�\u0001L\u0015\"1��@�\u0003��\u0000�\n-�(�l\u000eȦ\u0015�\u0003U\u000e8�\u0003�*�\u001e4��\u001c:)Bu?��\u0016�q9�\u0018\u000e0j!�\u0010�=\u0011p�\u0017*���\u0013��Oƒ�\u0014�\"�p@1\u001ch\u0004\u0010hD9�C\u0003\u0010�ڡ�L�^��?\u0007����\u0001�p\u0000Q�D���\u0011�+���nx\u0019�Jh��(kP\\\u000fP\u000e\u0018�\u0003�\b�\u0003k��2�/�\bA��\u0006\u000f�\u0003�1��ߕZP�{�ҋ�\u0001UP�}�z���r��Q�8���տ��* �]��\u0017\u0001�\u0013T��Wl\u0000\b{��\u000bT!\u001a\u0013\u001c\u0017\"1�\u0018P�w)�����\u000f}����\b��1B��O�C�\u001d�9�qx)�Bi�к�r�#E\u001e}�H6�\u0018\u0007P���>R\u000b�\u0012aգ>�Q<�\u0003ǻ8\u000e\u0004\u0001��+\u0014\b\u0002e(S�\u0012jk��Ғg��r�I�C&{lZ�SV-\u0014\u000b B�S\u0005ǁx�0I��Ύ�ud�>_��z�]\u0016/*�b�\u0007�PV)��h���X�ƍ�sߢa�|*@\f�.�B\u0015bqa�c\u0001\u0017~��ڡ�����Ƌi#\u0014O��\u001c\u001ba�\n-��\t�\u0007U��\bVa��<7�ԗ[n=��Vr�ľL��d��\u0001^D�[���\u0006PVi���ȷ�Y�ŗ�f�R��\u0018��\u001f�e�\u0013E⥂*�N���:,�N�1Bh�\u001f�hޞ'了��b��b\\z�*�@Y���\u0011��&e�c����)-�\b���y��.ƌs\b|eo\u0019�\u0003\u001b@�BX���\u0015_Nr�\u0015c0F�}��2c� ~u�\u0000�-*��\u0010l�{�\n-ф�mm�yg�\u00197����\rin�a��\u0016\u001fk�Р�1z\b�C-x\u0011�ƅ��\n-���\u0012�?~\u0018���C\u0015|�\u0012z�\u001d�97K�A\u0006��^sy����J���>�*�9�h�!�\u0006U��w�p�m�s�W[9cV��V�1�k\"�y��5\u0001W�g5ɤG�ɥ̈́6o(���Lz��\u001f'\u0014� \u0006Ty�n��B�f�ӯ����A4�Y��\u0019B���\u001a\n-\u0005����a5.�\u000f� �^1�\u000b6����nY��\r\u0011�u�8JK=�EKss��\bo���F��ϖ�la��r�Z�5�@&�\u001c5�a��Մ�����w�L\u001a��rY�Ύ\u0002��>Q��\u001d\n-9�\u0018�3�`\\(�\u0010�/)2�������9��<��J��,\"t[�r+?�Y\u0017u�;�\u0014\u0011��a/Y\u000b�Ra�ƀ�#\r7�2���(�y��C�\u001b\t��\u0001/��\n-\n-���7�=���.��\u0003b\t�Z�\u0015�\u0013�?�s���\f\u001e\\Jh��6�7ǐ�.��\u0014�ښ'T^\u0011a�T�t�b\u001c�\u0013�\u0010+\u0011Taޢ<7\\W�/�<�C�W\u0012��\u0003~t�*��s�\b��`�vF`(\u0016\u0001�]1�\u0005�\u0010/\u0011R�\u0016�\u000b?�c\u001c�\u0007$\b=�d\u0013gt=�#��Q��~��?�i\u001b���\u00187�t,�\u001b��S�1Pe�X\u000bƁF��j\b���\u0005͌Ā�\u0006�i{�Pi�Lj1\u0011��[�\u0011�\rU\u0010�d��᥀��[\u001eyh\u0004_�l,e�\u0011B\u001b7�����UY�\u001fZIh��\u000en�V\u0007���䳊\b��\u001d��\u0017�BNy�E�\u001d��a�$��k�a�\u001a BW:�G݈\u0018\u001f<i5k�t\u0012\u001aV��7O��g�8��8�#U�D�q}�?_�`��rB�֦���\u0014�����Jh��\u001c!/b\u0018Z\u0017e}F\u0011��\u0011�\u0001x\u0011!�\u0010�-*P��8�W\u001f�\u0007�\r�ǒE[�s�r�^�哟\u0019L�%���\u000e\u0018\u0003\"�k���\n-�K�%O�<��(Ǝ� ��vΟ��i\r\u001e�\u0011�ۋ�H&]@����in�\u0012�pt\u001f\u0016>0��\u001e�\u0013/\u0015Ty[�\u0010K\bϭ�i8�\u000f��K�G���\u0006\u0001�P��q}\u000eU�\r\u001e\u0012'�\"\u0002(�H-��\u000b]��\u0005\u000f\u0017���\u0001\\��C\u0019<��P{[���k\u00053f�Í�\u0017/*c��\n-B۶e���6�Mt)\u0016��°\u0007��z�E��K�,}|\u0004GM�C����\u001d��W�|��Wd-\u0001ٌE�nјC9�M��\\}�J�Y��̓\u0007q�/\u00062q��J���Q$\n-��ʔ#<�>��Ў�\u001cw����\t\u000eż��0�ΰvU�L�'4`@��*({�\u0016\u001c\u0007�\u0015\u001e*RVex���LJ\u0013�8�^^��'�^�\u0015W��(ϯ\u000b�}F_b1�ГK�yj�O�R���'.{ \u0002�'��H��\u000f�1��~�������-���\u001a��%��7JUu���(���PR�Ё2�p�\u001f�<Cm�*.��8D��es�+�����\u001e�m�1�J!\u0012\u0015\u0016.-r�M}��?A�\u0017v���\"��GȤ\u0015-(\u00155�ח\u0015H�\n-�����\u001b�\u0016C�+�[�\u0001�K� P�-�s�5U���FPY\u0019%T,Z�x�F>r�\u0016�?�e�L�-�\u0002�̈ṟ}\buv\u0016��f&\u001f��(��[P\u000b�R�G��s�\u0010f�<\bk\u0015\u0011(-���qx���V\t%\u0012.�t�r��\u0011��k�\f\u001a����\u001f�\b|���lڰ�E?�2��%ݡ\u0018�7�\u0001ߧ�ԓ�\u0012��\u0003\u001e����Z@�Z��\n-\u000f=��֚��\u0004\u0015\u0015\u0011��%������_) ��\u0014ֽ\u0014�ڤ<x\u001d�N\u0019D��[���5��M)fO�P,(A\u0000K���ug?��#�^xn\u0007��?Ϝ\u0019\u0011Rm�qxO\\�\u0017U()\u0013\u001e~�ȍ߭⬳�\u00122F\b%J\\z\u0004����tt\u0014Hu\u0016�\u001b�$\u001au��\u001d@\u0010���2gz��_���\u0003cL�1�x���\u001cˆ5/Ӽ�R�ϐ�(���qa��\u0001_���ѣ�\t�^�ɍ7��=-B�KA@-8\u000eoP���;\u0004J�\u001e#�EXz��Z�/\u0002\u0002�B4&D�0q�>?��}c$uuIz,}��K�i=훕��\u0011R��@>�L\u0018�p�\u0007j\b\u0015�\u0001��v;\u0013j\u001d�\u0019�8�g.o!\b`\u0000B�\u000e�L�O$jH��tv\u0014hi��ؘc�\u001c\u001b7�ٴ���\u000b\n-�\r\u0014\u001e|�H�Q�X��\u0005�\u0002\n-��r�\u0007�̘��\u0017�\u0012�#���\u0013㦟��~�+��\u000b�(�E�\u0001D��K�\u0002���/^�\u0010z��f@\bY\u000bƀ\bXK�m[���Q��\u0011Q~���\u0001#\u001c�EE\u0003()\u0013:[-\u000f>�s�O���#\u0016w\b��En�u\u001d\u0017����<\u0006�\u0016:[\u00151\u0010/\u0011�/)r�M}\u001800A�Wڸ��]�vR�\\F�\u0017.��\b五\u0011':�sS�\rk�ѧ�˫/�y�q�MXv\u0012F!�?�0�\u0010C<!\u0014��P4�0\b�Z\u0005\u0001�`\u00038i��'N^łg�\f�-e��2�~v\u0014\u0013�]�i'E\b|�� ݮ�:1�Q\u0013�\bmo���\u001f�1�x�l�b\f;\t\u0004\u0001\fưq}�\u001eCjc�@q\u001c\u0010�d���\"�1\u0013\u001c�}f4\u0013��K��W��\u001f_[�\u001d��3#B.�dҊq�\u0018(\u0016 \u0006�Ϩ!�V���&\u0006b�\r�� \u0006�\u0005�����s\u0005\u0016��EW��)\u000e�O��0#���\u001e#?�\u0012/\u0015�\u0003[�\u0004\u0014\n-�P4j��\u0015��N\u0002���(\u0015�\b\\z�kt�\u0017\b\u001d3�\u000f\u000f�7��\u001eɓ(\u0015�\t��|�*��c��}f\u0007O����\u0005U�$\u0002���\u001f.�[�'��\t\r\u0018\u0018\u0007�h\\p=����g�*�\u0007\u000fc�}\tY�̟��1c_��̙\u001e!ծ\u0004>��͍\b�\u001e���2F�.'�vm��_���S\u001c\n-ye_\u0019�@\u0004�y�;�a�T�DR\b��թt�+]�J>�\u0004>x�кI)�\u0003B��Cy?C\u0010�\b�D �R\u0006�qxrA�k�y�b�\u0012:������\u0003���@4&�\u0011&O�C(�����-\u001c]��\u0017\u0015\u0011va-$�\f�_-�N\u0017\t��\u001b�2�ƍ\u0001���̟[˿_s\b}��\t57g����p�&N��Q3�!ծ\u0018ÛԂ��j,�\u001b�a�\u0010z�&��D�BPd���\r\u0011(�\u0014\u0005�7\b��\boR\u0005��\u0006�\\. \u0014�\f�U���\u0001�����8�nW&�t���\u0014�kW��/�!�\u000b���X���Z��ת�\u001b�$���v~��\f��\u0014!�Uv�\u0010�Ë�\u0007�����7NUu�W;\u0003�\u001b\u0013��{�2jT9=�{���.Z���\u0003\u001afF��T�\u0005��W\n-n\u0004�6\u0005\\��\u0004�\u001f^E�i{��o\u0007ӏ�Ȥ\u00151�3�;\u0011\u0010\u0001���`\foPr����\u0019�+\r�\u001c��\u000b�@g�r��\b�|��?ܳ��1�%_\u001e���q�|�\n-�5��Ë��\"\u0018\u0007�e7��B#�\u001d-yB��o3��v(�F�\u0013��\u0002n��\u001a&Nz\u001d\u001b(�Իt�*� �.�B<!<�*��sk��\u001dBO<��ӯ��\u0013B\u0010�+\f�H\u0001\u0011�e�\u0001!�3��\u001b�\u0019E�݈@�C9�>�Ygo扥�\t�b\u000e?��x��PEh˖.n�����]r]�\b�S0\u0006�\b\u001b7f\t%\u0012.\u001f��0��\"�6�O�^擟n���\u0011\u0012IC�S1\u000e�S�Ġ��r�\u0007\"\u001csl\u001fB��\u0002w�����\\rY�\u0018z�a_) t�d\u0002B�cH�9�v(�\u0000�nT!�Uf��Q?e\r��h'Է&NEe�гO��J[@,.�Evc-Db��@\u0016�l6 ���X�h\u000bS떳t^��\u0019\u00112)�XP�a��Q��|��t\u001f�����~\u0007wߛ���`\u0003z�a_\t�t�t��\u001cWH�9��@���N\u0004\u0002\u001f�\u0011&\u001e���s_g{c�\u001e�t�����i\u0013=2i�8��\u0006�,\u0017ښ,s\u0017\u0015��wC9����\n-�c��(p��`Ƭ����P{�Cg��N�\u0003�.��j�)S�\u0012��\u0003���ĸ\u0012�Z@�5�^�J����\u001e�I�6\u0014#��D �U*j\f-�,W]����\"�X�a���y��EJ��\r�\n-b��J���H�Z�����s�\u0012�:���e���2���6\u001afF\b�\u0014���\u0014����S>����!���^[���~�f�$�\\FA�5.�H\u0000U� t�\u0003z����\u00011�-c��C\u00199��;��ֽ�U�8\u0004�5|�_F�eS��\\�b�L��\u0016��\\(\u0016�y�\n-\\��U|�_FPQ\u0011!�\u0017-��g#\u001f=w\u000b'�w9��#զ�\u00011�#�`\f4\u0003'�֏�*<�`;5\b��\u0016��k\\�����\u0007H�|z��:�\"\u0002\n-\b{f\\Hu(������3�v\r���H<�p�UcX��e^}�Ș�.�<V$�\u000e�\u0017�1}�@zlۚ���7�8��\bA\u0000�\u000e�qA�w�\n-����\t�+��d��rB�ק���;8z�G6���W\u0019z�Z(CH�\u0002�UB%%.!\u0011ޙ�1�jW\u001afF��3�<x�fB��\u0011��i,���]�s|�8Z{(�g\u000e����\u001a9��e̿����\u0011�Y��S�\u0003��5/\u0002+�Ẇ�Ë��\r�\u0018@�u.�H\u0000k���BgG@\u0010(�\b��C7\u0001����թ���(����瞍q��>\f\u001e\\�\u000fo\u001f�3O�r��É�\u001cB]�\"��u\u001d\u0017��i�z\f\u001c)t�)ƀ\b{O!\u0016\u0017��\u000e��q\u000e?��P��,���\u000eN��R�+\"�:þ\u0012P\u000b�2Hu\u0004��%\u0014O8tS@�;\u0002j�/*3�����5����БGU�ًG\u0011�9�V���g/X��_haΌ\b�\u0007��b\f����\u0002��X_JJ=BO=���/�$�\u0005\u001b�0�\u0002U�ą�vK�h\t�b\u000e!U\u0010��\u0002�+�K�gW�Y�X\u000b!߷��U��c^a�_�̙��թ\u0004E0���\u0018Hw(��z\u001cw|_B�T�{~���\u0004>��\b;)��*��*��*��+.�@\u0015�(t�Z�EK(\u0016s� �\u0000\u0010ޙ�\u0002�\n-a�ʒE#�6}\u0000�ຆ��\u001c?�q5��S>�*��\u0015c\u0000�=Q\u000b�����\"��Y?��\u0011z��\u001d�qO�3O��I[�qA\u0004Ā\b;)��*X\u000bj�Z�ZP\u000bjA\u0015İG.�@\u0015\\O�h�\u0014\n-�P4�P�\u0010\u0004�\boK\u0015\u001c\u0017�%¼E\u0005.</�\u0019ɰ�$=��s3���:V<\u001fpƬ\b]�J�+ưo\u0004l\u0000�\u0011N��!T(X���\u0000\u001cV?糼Ţ(�\u0013�@5Bi\u001f!Q)D\u0013\u0010�\n-n\u0004\\O\b�2ʞ��\u0012ǁ�uJ!\u001f\u0010�F\u001c��1�E\u0010\u0003jyKj!�\u00142ie��\u0002?����>QG<�\u0012��\u0002~���|�ӍL��r�4��6E\u0004İO�B\"),}��W��d�$� \u0002�u\u0014_�̒�\u0005�r\u0001�l@&��\n-�J���>�T@�ӧ�#��#��5��Ų��ҲҲ\u0006�\u0005�Nr�\u0013�^b\u001c�9��r\u0001�H�PQcȦ-ф\u0010\u0004��&U\u0010��*��\u000b�T�\u0019�|b\u0014ǟPC�\r�S��5k��/�8�>B>�dR�\u0018z�q@\u0015�QN�ݏ�\bx��oM�����b�\u0012��\u001fX\n-yK6�\u0013�ʟ���uWn��\u0010�\r@�]��\u00121�\u0003%�\u000b\by������b���\u000f\b;�\u0005/\n-�'�]��+_H��+F2`@�\u001eK\u0016m�gm �_h�\u0019!ݡ��\u0018zM4&�y��˟K2fl\u0005�m[�X�z��\n-�h�!\u001ew�F\u001d\"Q�H��y\u0006�5\u0018#�p]��\u001a�J��$���\u000eZ\u001a\u0003\u0012�B\u0010�\u000b�^\"�\u001b�l6 �z��\n-C.�H�\u0001\u0014\u0014\u0014(-\u0017vl�<�r���9�3Ϫ��\f���\u0002?��\u001a.�r\u0007�N��8B�]\u0011\u0001\u0011ޒ*8\u000eDb\u0002\n-ł�� ��\u0012\u0003��,�sV\r�g\b�y�&.����D�D\u0019Zi�Sg��1TV;�W\u001a�+\\��\u001d�I��r��r� ��\u000e-��ê�}��\u001aBzd\u001b+\u001e)Rw��_d7.�D�n�L@�u\re�\u000e�FŌ\u0002�`\\(M\n-\u000f,.r�t�ew��ë���V���Z�[R�aF���R,(\"왂1 \u0006�{�H��\u000e%eB\u0010�\bo�8��d�HC����&�ys\u0017���3f�\u0010�P,(�\u000fżҸ!`�+>��JW\u00016��\u0000M���ۥ}��e}P\u0005�5\u0014��;�X�������\\|\u001fTٍK/�d|B�+$�\u001cR>�b\t�Ze��<��v5�|�p**���E���o䜏m�C]N���P\u0004\u0010a�\u0014�\u0003ƁG��<x�Pjj�\\{�z\u001a7\u0006�K\u0004kٍ\r �\u0014\u0016?Y��\u001c@2�\u0011Z�X\u0013/m\u000b�\u001d��թ8.\u0018\u0003���(\u0015�\u0006d\u0004 pB��e}@�f�_��~�@zl���w���\u001f�$�i'E�g\u0015\u001b�\b�q�e]]\u0001!c�ҤC#J����SE:�+�\u001e�cƬA�غ��\u001b����ޔⴓ\"�E%ݮ���) \u0010+\u0011�]R��uL�>�����⬳73gz�T�b\f�p<Ȧ���]N�ܗP{[�_����\u0013\\�iE\fX\u000bX�?�Z�D!\u001a\u0017�7υ�%��#�\u001b^F��\u001em�_\u001b�\u0013d��\u0019\u0011�\u001d\n-\u0002\"�%�ޢ�A�J��(+w\u0019����\u0002\r���իG2�.I�'�n�s�ѾI9cV�T�E\u0015���\u0014H�\u000b�\u0016���=�L�>�P:]�?u0��!�SDZ()\u0013�/.��\u001f���_�П�ma�#\u0005fO��I+\"��\u0006�H\n-���pi���Q\r\u001f���x�%��U䶟��s_lf��\u001eј�٦\u0018����K\u0014�\u0000�t@�hT�L�_�:��?:�x�%�N\u0017��\u0017���f�'y\f\u001c%t�ZĀ\b�H-�U\u0019�.�s�O\u0007p�Y��|�r�w^�\u001f��=�#�VD�A>���pR}\r�\\6��w7s� ��\u0007\u0011�J\u0001��*a�\"��a�'������zU\u0007�\\��_ݝ�aF�lFɦ\u0015���\\zQ\u0015B:\u0015У�*�C�G3m�@z�z��k�Z�\u001d�d�3#B6�dҊ\u0018��\r��Z��0�\r�Us��#�q�\u000fV�o��03B�M\u0011�n\"\u0011�G}>wu9�G$\t��J\u001b������\"�2J\u000f\u001b@,!8.�[T�+_H���G2``��*<p�f�>}#�\u0019�L��jWĀ\u0018��K/Q�R �\n-�V1F�u� <��c��M�ް�c�\f\r3\"��\u00151 �^�\u0016�*�{\u0017\u0016���r>��ш��W���/�p��\b\u001dm�1�F\u0015\u0010،��5�\b�*\u000f,�N��� \u00026��r��ɲ�e�;�\u0018ȇ�\u0019��\u0019Bmmyn��\u001a��F+�| �\u0018!ծ\u0018�wť�(��\b�b�\u0012�98�\u0010ڴ1���\u001a�K�\u000f�\u0012�\u0006�I[�\u0003ւ�\u0006�mY\u000b�ra�b�O2��_\u001f��\u0019B��n�\u0013��J��(�v�\u0018ޒ����\u0001\u0017���V\u0012Z�6�u��`�\u0007=�i%$\u0006�ʄ�\u0016\u0017�4���\u0017�r�Q��X�R+_��\u001a�|�HÌ(��\u0012\u0004�qx�\\z�\u0002�2�h�\u0018G�[;v���q��1�7O5��#\u001dʪ\fb �U�\u0005\u0010\u0001\u0011vc\u0003H$�ͫ\u0002�<�����RR�\u0011z��m4�����Q�:\u0015��d\u0003��\t/l\b��G�\u0011�:��,�N\n-0\u0006|\u001fJ�\u0004\u0014�-��oT��*J��-�{#\u001f9w\u000b'�w9�ޣ�M1\u000e����bA��gX��\"\u000f�\u0013N�Kuu�БGUs�Q��ؑ�K+;y��V\u001e��b��\u0005�\b'\u001c�RV)\u0014\u000b��)� B7U��\bmM�xR��O�QU\u001d#���[8�~-�N���(ւ\b�Q�h\u001cZ�Y�<9ʄ��\t56f��\u0007�L?�%�Rʫ�-k\u0002����;o(��>\u0004\u0011�5n�p������\u0014��E\b|Hw(�a�8�\u0007�jz�\u0005\u0005b%·���c��ёB\tH�\u001dJJ<\u0012\t��CJ8�>�|z\u0015sN/a�(x}y��O\u0014�oP��\u001a�%��\n-(�\u0011�XP������1�.I�\u0015�L=r%S�w\t�>��G�Ra��E��[��pL5���o�\u001f�s�!.ј0I�i�\u0011�;��O�A�nO?��\u0005g����\n-L��M+~\u0011�a\u001f���������� �i�\u0018\u0007\u0005�#�-�����-4�t�X\u0012\t�D�%Q�Q;���S�r�\u0019U�>5N�p��\u0017�<�l��z�z�����K����1�?����\riΩ���\f�\u0012C!\u000f\"��A.\u0003}�˾^G�,BGG�o}\u001d\u0003*\f�\u0011\u0016<\\�\u001b���e��\u0004�\\6�[��p�FF�\u0019\u0006�qHw(\" �>\u0012\u0001�]�\u0007��\u0012���z\u000fUHwXn����he��\b��Y�q�+\u0019=��ʪ(5����3��\u0001|�3\u0019^]��\u0013�����i�~-�c\u001aő\u0013�\t�h��\u000bW�D��ҐI+��my\u0011��'����>\f\u001cTB�/��`��\u0005�\u001e��9ˣ\u000f\u000fg�I\u0003�a}�k�^�O~�aΌ\b���I+ơW��\u000fD���O)\u0018��\u0011S]�v!ݡ|�+;PZ�3=¬9�\u001c{\\%�F�S^\u0011a��\u0004\u0003\u0006$��1��?������GU\u0013�ޘ�\u0013\u001f^��'\u0002&��I+ѸP,(jA\f�Q\u000b��\u00013f�#T(X\u0016�k&O�ĩ%\\z�\b�Ԗ���[���\r$�\tsfD��PB���\\�O\u0002(�\u0000\n-����0��#Բ�r�%-@\u000b\u001f>-�ʙ8�����H&#\f\u001d�d�0\b\u0002�q���\"�OK�,����$\u0011�<¡��A\u0015�]J��1��*D\u0013���\\��2F�,#���V���V�\u000f�3�h�!��,���Y˥��0sr\u0004�\u0013Rm�qx߸��\b\u0018\u0007T��S\u0011\u0003eՆ��\fj�qS��^Ԍ����\u0019cƩ\u0015\u001csl%Ç'))�\b�\u001eS�eW��ښ�+�u�ԓ�,����%\u0005�\b�NrIV\b�\u0002\u0014��\u0006�y�\u001a��~8�P,Z����c�����v�q�j�^P�aF�l���R���J�'=��\u00030\u0006\"1�\u0018h�jyz�\u000f\b\u0017|,F��\n-��XŰ�R�q���Ҝ�\u0015\u001d<�D\u001b\u000b~��c/�\f�P7ɡ�ڰm}�a�F��-�\u0013�9�hɑ�\u0005\f\u001a\\B�Z��y�8�C��4ڡ_�!ݩ��~R\u0010\u0001���I�(� TA-Db\u0010�\u000b�\"l_\u001f��z�\u0000\u0017~<���+8��Jj�&��\u001c����YV�����Z��wi^z- ���\u0007F0��AX�\u0018#�hi��\u001bWs͵\u001d�:5�*�2�qx�)���I��g�0���q��\b�\u000b6����gW�8\b��\u0004'M�䈣*��-%\u0012u�\n-��\u0019^~��g�n��/���2��[\\�\u0010z�\u0016.�h-+�\u001c0i�KWJ\t\u0002\u0010��AA\u0004lN�'=�\u001c \u001c\u0007�q�Z��J��M\u0001\u00031�sQ����\u001cqd%����y�\u001e������\bŢ巿Y�'.�Ɣ#]ʫ\f])�\u0018��\u0014D��\\\u000e A\u0000�NE\u0004�\u001c�0�\b�\\VYrW���(�p�p�\u0017K�:���\u000e�dР\u0012\u001cW\u0010\u0011B[�tqݷVsӏӜ^\u001f�XP�:\u0015�߸\u001c`��[.��\u0000Dž!�8\fs\u001d����\u001d\u0019��^���-|��%L�`%�\u001fQɚ5).>k=�v�aF���b-\u0018����\u0000%B7\u001b@!Pȁ\u0002��\u0019F�\u001c�\u0019�Wק���N\u000e)��k\u001d�)\u0013\u001cJ\u000eu�lS�\u0003\"�w.�W\b\bP�C>�8\u001e\u001c1��H\u0003ٴR\u0017s�}%ۥ\u0018�\u0018.�Lj�8�\u00162iE\u0004\u001c\u0007�9E-\u0018�?\u0014���D�\u0016\u0004�$��1\u001c�_\u0019\u000eگ\f\u0007�W���+�A�����p�~e8h�2\u001c�_\u0019\u000eگ\f\u0007�W�\u000f�|\u00032�þ�\u0000\u0000\u0000\u0000IEND�B`�\n\\n+\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000`\u0000\u0000\u0000`\b\u0006\u0000\u0000\u0000�w8\u0000\u0000\n+�IDATx\u0001���qۈz\u0006��j�?H\u0005��`�\n+�X��\n+LV\u0010�\u0002�\u0015ȩ�t\u0005�*��\u0002�Vp�\n+�\u000e��\f<���K\")���9�={��ٳgϞ={��ٳgϾ��7��=\u0016XWU�\u0017q�o I�\u0016\r^����{87�4~\u0011'~rI\u001a�|��8���|��/����ܧ��\"N�Ē4�LFlM�$�_��\u0007J�`c������̶�\u001d+�s���N<@�\u0006w���.I�靛���ެ�\u0003%钼N�8����Ak���\u0013J�@g�����7i�t~�$�+\\;��oH���O����t:����쟾�$�$�qi6:���Hr��/{�d�i�4{g֛u��$gx����9��/H�¥���]'i\u001dQ�\u0005Z�]U\r>��\u0011��\"I�%y�;4>����\u0001N|F�\u00156fo�jkv�ޤ�]���tf�|���'��Ir�W>�w�\u0013��ŵٶ�^�\f&gXc0ip��q\u001c/Ͷ>uo�y\u0002IZ�Ǚ�[�f�\u001c�ğ$iq��侪�f�\u000f�j�\u0012�I��\u0003%Y�5\u0019�j�/�j��d��uDI.�\u001e\u000b�\u0011\u0017x��dWU�\u0003�� I�\r\u001a�\u0001\u0017� ISU#�\u0018M�$�\u000e��f�|�;�3G��I���ـeU����\u001d��?$ip��d�eU�>��Y�\u000fU5���2���:�ޗ�f/\u001d(I�;�̶XV�`rnv�\b�\u001f�ܠ3��\u000e��?��\u0002\u000b�eU�� �k�2{QU�GH���PU/|E��hM���v��d�k4&#��j�$\r��d���t\u0004'I����5n�\n+���\u0019\u0016f�?���ؚ�%i=�K�w��Yg\u000fI6ؠ1�aYU[\u001f��zGr�K�i|�\n+�I�M���uf[�vo��#$Y$y��Y�\u0017U5�Թ٭#9�\u001b��\u001e�x�7x�%�xQ��WTՈ%v&-n<@�\u0016\u000b���F�PU\u0003v&m��\u0007H��=Z�������_$iЙ�U�;�Ӫz��\u0006����\u001a�\\�\u000e\rΒl�j���������49��W$y�Wf#.��ޗuf�#:�\bU5z��\u001a�6[%Y��ά�p���}A�&�\u001d^��㿪��ם��:�\u0013�k|EU��2�$�|F�\u000e�I_U�\u0007��\u0001;�.I�/����83{[U˪\u001a}E�\u0006��XU�#:�x�&�o���ؚm��>unv��z�Ο$��\u001d\u001a�\u0011\u0017Uu�a:�ޑ�xbU�ƽI��$��u&#z���ٹ?$i����l����=ܹ٭#;�x�\u000f�,<�\u0005\u0006�\u0005�|��Cc�W�葪��h�%i�\u001e�ٶ�^T���t&cU�������l�\u0001�j�\u001a�I�dc�����z��X��XW��#%��zO��wRU\u0003.�VI�љ�U��߭O\rXV��~��n=�\u0013��3k=BU�cmvi��S�\u0005^�X�eU\r�י�U�{\u0002�\u001eog�x���&�\r�>vk\u000fI:lИ]U�[\u0007Hҡ1�=�\u0013?@U]���{���\u001a7hLvxQUo\u001d����\u00139�x��?�o���.I�\u0001�4I����\u001e/�jp\u001c��XU�'r⑪jt\u0004U5�\u0002�I�kߐ�\f�Fk����U5:�$\u001d\u001a��\u0013:q��\u0001�j�%F�U�k_��5�И�XV�k�unv�Ȓ4IΒ4��s�3�\u000eTUC�+lL.�����>H�`��l�EU�\u001c_g2VU�@I���7�hM�S?���&Y���&�PUC�\u00167X����+O I�Ƥ�HIZ��\r-�|���~F\u001f$YT���u�`er����l�UUm=�s�[_�d�\u0016-��\u0016��\u0019�>���3Y`�8�ТE�Wf\u0003�U5xZ��XU�\u000f�4hq���b��v\u0018Й�XV�p�'RUc�+���\u0016WU5zBI:4&C�K��\u0016�o\u001bq��\u001e�ɝوeU\r�pj?;�\u0016�� �\n+�>��������\fg��\u001e\u0003~�PU;���\u001dZ�\u0011˪\u001a|pj?;��\u0011$�ƥ�\u000e\u0017U5�~:_6`��\u0018�j�\u0015I\u0016�Ak2bYU�?9u��p�$\u000bܠ5뱮��w��Cc�À�1TսGH��\u000e�ɈeU\r���\u001e��>�\u000fZ{Jr�\u001b4fo������MUm�!I�;4&#�U5��S?H��xe6⢪��\u0018�Yo\u000fIZܡ1\u0019����\u0017��߈\u0006\u000b����\r��\u0006,�j�\u0003$�И�U5z�$-�И\fXV��+N�o�\u0019\u0016\u001e(I�\u001b,��VՕ=%iq���.�����#%iq��d���F�p�;Ir��X�����+{J��\u0006g蒜�Og�{�$-�И\fXV��\u0001N�o�$mU\r>#I�\r:�\u0001\u0017U�s�\r\u0016&CU�{�$\u001d\u001a���F\u000f��\f7hL\u0006,�j�@����f��H�b��l���\u001a\u001d �%:�\u0011k�97��@IVؘ\rXV��\u0011N=�$+\\�1\u0019qUU[\u0007J���UU\r�ә�\u001e �\n+\u001b�\u0001˪\u001a=ҩ�\rfg��A�\rVf;\\T��@I\u001alИl�jk\u000fI�И�U5��$+l�z��j��S�\u001b�E�\u0005nК�XW��8�њ�pe�f��!�\n+\u001b�mU�\u001d��q�G�\u000e\u001b4fWU�֑$YaevQU��uf��H���l[Uk\u0007:����7[�\u0006�ɈeU�u$IZ\\�]U�`OIZ,L��\u001a}A�\u00156f۪Z;�S�ј�㢪FG���\u0006�I_Uo\u001d�٭/Hr�k�mU�\u001dɩÌh�F\\U�踮њ�v�ά�\u0019I6X�m�j�N\u001d�\u001e�Y��Iv��\u0006\u0007H���좪F\u0007H�ba�W��/�l�2�V�ڑ�:�\u001a��%Z�\u0005.q�dD�۪�=B�\u0016�fWU58�K�[�d��ٛ�z�\t�:@U�x��I\u0016�p�3�\u0006+����q[U�o۠1�����z�d��ٺ���Hy\u0002I\u001at8G��zܢ��џ$�ƥ�\u000e/�jt�$-ޛ�Uu�$\u001b���U����\u0013KҠ�9:_��\u0016=�pc���\u0006G��\u001a�&���C�\rVf��zb�;K��\u001c\u001d\u001a�7�1ySU�\u001dI�ca�&�X���j�;(?P�\u000e�����\u0001��W��\u0001��xo�c�;�f���N�O\"I����y\u0003��\u0006���\u001a�&Wx��l]U[�Q�\t%i�\u0012\u001d\u0016>o�\u001e�j�\u0000I���Ɉ�dĺ�z�Y��%Y��K�>o�\u001e�WU�3��x�S#�U5�\u0001��H�\u0005:�D��F�����\u0007I�q�c#�U5�A��T�\u0005�p�����;^aa6bYU�\u001f��\u0002�4�p�η�XV��\u0007+��$\r�p�\u000e���XV��'P~qI:��È��\u001a<{��ٳgϞ={��ٳgϞ={���?���Wsy\fn\u0000\u0000\u0000\u0000IEND�B`�\n\\n"
|
|
1878
|
-
},
|
|
1879
|
-
{
|
|
1880
|
-
"path": "native/android/app/src/main/res/values/colors.xml",
|
|
1881
|
-
"status": "modified",
|
|
1882
|
-
"diff": "Index: native/android/app/src/main/res/values/colors.xml\n===================================================================\n--- native/android/app/src/main/res/values/colors.xml\t74465a3 (parent)\n+++ native/android/app/src/main/res/values/colors.xml\t7d171ee (commit)\n@@ -2,5 +2,6 @@\n <color name=\"splashscreen_background\">#4337C9</color>\n <color name=\"iconBackground\">#4337C9</color>\n <color name=\"colorPrimary\">#023c69</color>\n <color name=\"colorPrimaryDark\">#4337C9</color>\n+ <color name=\"notification_icon_color\">#4337C9</color>\n </resources>\n\\n"
|
|
1883
|
-
},
|
|
1884
|
-
{
|
|
1885
|
-
"path": "native/app.json",
|
|
1886
|
-
"status": "modified",
|
|
1887
|
-
"diff": "Index: native/app.json\n===================================================================\n--- native/app.json\t74465a3 (parent)\n+++ native/app.json\t7d171ee (commit)\n@@ -5,9 +5,9 @@\n \"owner\": \"iansp\",\n \"scheme\": \"com.markets.manifold\",\n \"newArchEnabled\": false,\n \"jsEngine\": \"hermes\",\n- \"version\": \"2.0.65\",\n+ \"version\": \"2.0.66\",\n \"orientation\": \"portrait\",\n \"icon\": \"./assets/logo.png\",\n \"userInterfaceStyle\": \"light\",\n \"plugins\": [\n@@ -15,9 +15,11 @@\n \"expo-font\",\n [\n \"expo-notifications\",\n {\n- \"icon\": \"./assets/logo-96.png\"\n+ \"icon\": \"./assets/manifold_white_transparent.png\",\n+ \"color\": \"#4337C9\",\n+ \"defaultChannel\": \"default\"\n }\n ],\n [\n \"@sentry/react-native/expo\",\n@@ -63,9 +65,9 @@\n \"foregroundImage\": \"./assets/adaptive-icon.png\",\n \"backgroundColor\": \"#4337C9\"\n },\n \"package\": \"com.markets.manifold\",\n- \"versionCode\": 65\n+ \"versionCode\": 66\n },\n \"ios\": {\n \"infoPlist\": {\n \"NSCameraUsageDescription\": \"Pictures can be attached to the content you create.\",\n@@ -77,8 +79,8 @@\n \"associatedDomains\": [\n \"applinks:manifold.markets\",\n \"webcredentials:manifold.markets\"\n ],\n- \"buildNumber\": \"1.0.65\"\n+ \"buildNumber\": \"1.0.66\"\n }\n }\n }\n"
|
|
1888
|
-
},
|
|
1889
|
-
{
|
|
1890
|
-
"path": "native/assets/logo-96.png",
|
|
1891
|
-
"status": "deleted",
|
|
1892
|
-
"diff": "Index: native/assets/logo-96.png\n===================================================================\n--- native/assets/logo-96.png\t74465a3 (parent)\n+++ native/assets/logo-96.png\t7d171ee (commit)\n@@ -1,49 +1,1 @@\n-�PNG\r\n-\u001a\n-\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000`\u0000\u0000\u0000`\b\u0006\u0000\u0000\u0000�w8\u0000\u0000\u0000\u0001sRGB\u0000��\u001c�\u0000\u0000\u0000�eXIfMM\u0000*\u0000\u0000\u0000\b\u0000\u0005\u0001\u0012\u0000\u0003\u0000\u0000\u0000\u0001\u0000\u0001\u0000\u0000\u0001\u001a\u0000\u0005\u0000\u0000\u0000\u0001\u0000\u0000\u0000J\u0001\u001b\u0000\u0005\u0000\u0000\u0000\u0001\u0000\u0000\u0000R\u0001(\u0000\u0003\u0000\u0000\u0000\u0001\u0000\u0002\u0000\u0000�i\u0000\u0004\u0000\u0000\u0000\u0001\u0000\u0000\u0000Z\u0000\u0000\u0000\u0000\u0000\u0000\u0000�\u0000\u0000\u0000\u0001\u0000\u0000\u0000�\u0000\u0000\u0000\u0001\u0000\u0003�\u0001\u0000\u0003\u0000\u0000\u0000\u0001\u0000\u0001\u0000\u0000�\u0002\u0000\u0004\u0000\u0000\u0000\u0001\u0000\u0000\u0000`�\u0003\u0000\u0004\u0000\u0000\u0000\u0001\u0000\u0000\u0000`\u0000\u0000\u0000\u0000�e)�\u0000\u0000\u0000\tpHYs\u0000\u0000\u0017\u0012\u0000\u0000\u0017\u0012\u0001g��R\u0000\u0000\u0001YiTXtXML:com.adobe.xmp\u0000\u0000\u0000\u0000\u0000<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"XMP Core 6.0.0\">\n- <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n- <rdf:Description rdf:about=\"\"\n- xmlns:tiff=\"http://ns.adobe.com/tiff/1.0/\">\n- <tiff:Orientation>1</tiff:Orientation>\n- </rdf:Description>\n- </rdf:RDF>\n-</x:xmpmeta>\n-\u0019^�\u0007\u0000\u0000#�IDATx\u0001�]\txT��~�MBV\u0012¾\u0006\u0002\t\n-hQ\u0004\u0004�\b\b\b\n-\u0016�պ`��j�[�ߥu����Jŵ�\u0016w�ʦl���\n-�\u0002aO l!d_�63���ܹ�\u0010�LH\"�?9�̝�{�Y��;�y��\u0001c�~Y\u0006\u0004TÔ�\u0000�Zߧꪞ}U]�����zv;�w]}��O�����o�}߮g?oh����U�����ݖ�wͫ�s}��ou@`@@���\u0000\u0004�Y�D{�6\u001e���V�j��v��t��o\u001b���<��TWW\u0015�}믣�v{v}�vj��-k��k�k_}��{�6���qլS�}����\u0018MO���v�\u0011\\�j���x��ݣ��s�NͿ������ݳ��u����~��~cߩ����W���;v,���\u0000.(w0o�,���{-C\u0001\u000f���Z���V��@+\u0003��T\u000b�ke@\u000b\u0011��f[\u0019�/�Z�^+\u0003Z���6��\u0000)�B�Z\u0019�B����V\u0006�K�\u0016��ʀ\u0016\"��͞�\f0�\u0014�\u0000�R�G�wR3@�w�\u0005 (�^���\t'-\u0003\f�C��\u0003.��T#�$e�I�\u0000\u0011_\u0004�Lsc�P\u0007�u\fBaN5��Z<��Iǀj7�&<\u0000�ֺp�-m�ʛ����dD�\u0006�YE��I��=�\u0018`��$r&c\u0003�~��\u0010�G�H�\u001a\u0017�}\u001b\\\\\u0019\u0001'�~pR1@�'<\"\u0000�>u�?E!�w4�m���{�\"�L�j�7n�\u0013SQ'\u0015\u0003�^\u0002���pcҔ�\b\f\f��e1�[�6�����+@jQ���4�eO�#��3�zߑt�f�q��P�vz�1u;u\u0012\u0003<����\u0001�3�!@`����j�]\u001cD#�o\u0016\u0006h am\u0002�vW#0(\u0000\u001a��\u0003i�\r�}h��R�x�����q\u0018��U��.>\u0014\u0003�\u0005���h������tƱ�?DF\u0007��\u001e\u00176\u0010�����\u0003��&�th\u0004\u0013�����Jv}�IJϝ�xU�\u0019\\s\u0013A\u0013���I�\u001a9���C~^\u0005Rw\u0015��QQ!8eD\b��,8�R\fP��d�����J$\rv es|��/�X �*)��\u0010�&1@��&��hU%n�\u0019�}�\u0006��彰�\u000b\u0017\u0014m\u000en�@\f\u0015��P?Za;V�p�_�!ԣ���||�m���&<\u0018��\u000e\u001c��F\bWJK\u0014��\rA\u0000\u0017:�_�gg���\fĩ\u0003bq��\u00187%\n-�6�������I�e\u0016�.\u0010�U�\u000f��?�3�b��xyi5Λ����9P\\P�tU�\t\u0007S����;a�����\u000b?�B�D+�)88\u0010={�!\u001dE\u0018tܳ��mF�)PQ1�?�;\u0011�;\u0010kV�ňQ\u001d�/\u0014\u0014T��u�H�\u001f\u0004��\u0003��\u001c�\n-\u0010��\u0006`9���Q���$C|��m���\t]��k��`y��gt��\u0003�YM�\u000f��;����/۠����ʾ�%��L1rr�f��ͮ�<9f��\f�������F\u001b�\u00158��~k\u0004\u0016~>�\u0010_cӜU��:\u0017o�/C���� ���\u001a\u000f\u001a-+L�2j�\u0015N\\��p��P2BC�\u0014Y$�\u001a��\u0015W�ơ�\n-�uo\u001e�M\bEa��L��5�S\u001by\b�φ4\u0017��[;H�[�m�\u0007�V���i��H��$\u0003ؗ=\u0016S��\u001f�yDR�\u000e���v'��\u0015S/���X\u0013�\r�9WV���w�0�C0\u0019b��/\u0000i\u0014I$�\u0011Q\u0001سɅ�S\u001dx����\fAU�\u001b��L1e��������\u001b\"�iY\u00154\u00111��E\u001b�\u0011:�~�-\bC��3�k��n\u001e��\u000bƎM�(*�4�����\u0004��H�x�m�MR��4��|��U�\u0010�-)�q�%�\f���\u000e�y皲9\u000fϽT��AA(/m����\u0001\"� ��}.tJ\f����#.��H�\u0005����\u0015��{E�\u000b��gpZXX\u0010\u001e�k\u0012N\u001d\u001b��t�AK�e�6Ԕ/����\u0018t�fm�)���ޢrt'\u0003>�ޅ<�!���\u000e�\u001f\u001d��B�as��\u001f\u0006�q���Y��\u0002O>\u001e�W�\u001e�S<�O�{n�.\n-\\9\u0019`�[<�0�0ﳊ����>�\u0018�Ai�/.p��\f��7��Sg!_��uY��҉�-�+u��[\u0007��g���v��0��d\u0014탑�P�iU��%B��3nB\u0007Ӟt���HT��Q\u0013��r�,2\"\u0004}�\u001c�O��k�.�1i�ʩ��L=�эOW��mw&\u001b�C\u001dh߹��o���� \u001f�KK-��G\n-я��(k����\u0006\u0019���Pе�w~��\u001b\u001f%\u0011mD�]l�:\u0007SG��\u0003�%�O���'\f?\u001f���i\u0016F�\u0010���%b�WUt\u00193��t�_\u0007�����p$'ǘ���.��'��\t��S�Ȱ\u0018\u0010� \u0012J\bE�\u0011\u0012����W��J�\n-:ʞ\u0011p\u0018{i\u001b�H\u001d�����S\t�xy\u0006.\u001c�����p���|�\u001fF\u0001����U��\n-\r|�w�\"���?�\u001aX��/���5M��^���KŹSCЛ뮠@��*QQ��\u0003pǍ;�{��3�lI/|�i�Qej����\"���:=�v��\u0017V�ʆ�m(�Z�\u001d�Ͼ�r朗O�Y�h�\u000f��6�?�\u0016����J�1�3�xj\u0000\u0004�U\n-�+��߷a��=\b����\u001b��/٢��CG0��|�;$�\u0018`�F~��\u0000C|\u0002���\u0000,[S�5����\u0006Ǜ�w�( #R�%߉�\u0016T�9ƨ�\u0011AP��P��\u0018.��;�x�\u001d(+#4`9obW�y�\u000b\u0016��$�\n-�wS��\u0017Ѫ\u001duZ\b�8��|ss�1wN\u001eF\u000f\u000e2�\u000e!��\t�ؽ�\u0002G�X}t�lAQ��>���Z��ӟ��Bt\\ ��&\u0019���7\u001c\u000ek�nޔ��~�\twߓ�K��b#�\u000eS����6\u0015!�/�\u0011�yb\u0011�f#?ꄡZN�Hb�-I����ё�U���\u001c���t@��p�o\u001f��v���\r%\u0003$y\u0001��d����C�蹗��G�.���F*\u0005O\u000f\u001e(�=.��\u0016���Z\u0010\u000b)��0��Y�����o��&\u0017��VႱ\u000e���i���\u0010��DB�ŕ��\bF|�0� �e\fյ\u000bH�hO�kC\u001b��\u000f��w�c� �\t�}0o\u001f.�� ��\u0012�\u000b9$���Cq�Y�\u0010\u0016\u0015U��ײ1b@0ʩ���\u0000��\u0012�|�i\u0015���H�������\u0007�{��oǪ�\u0012N��JIQ5=�\u000e���y4�v\u001b�@m�rG?�ߛ��/��?\u0011RM�\"�-�V\u0019=���\b},Y���p��aW62[��\u0013�\u000bu\"��\u0012\t�zN�Y���o�Ui\u001a��ؿ$v��SV5�~�@뺫y�����D>i��bL9�aT�\u001c�k69���\u001d\u0011C'��\f��~TA��ڗ\u0010����\u0017k|�@\u0005i�\u0011t4�\"�g�#\u000e��Ӽ\"T!�\u000e��ن�$���\n-\u00079���\n-��r\u001bbq�T\u0012X��ƅ�\u001c�rF\u0006>^���|\u001e������1��>��+T\tvQl7c�\u000b��\u0014�~���WN��ϖ q�G�ظ�jm�DBY\u0016\u0014�����߁�ÖW��Cb�ԩ�\u0018���B���\u0001�\u0010��LL\u0019�\u0019��*5n\u0014�\u001fZ-B7��\u0007a�9\u0016\u0012��0�{�0\u0006��������|k]\u0001�8�}��u�\b�LGh �x�1xr�33�))�e\u0015؟^����ӻ\u0004`�W�\u001b�8�PT\r�\u0005A\"i\"��<\u0014���C�|���>\u0016<M���-��&(����&� ��?��?��':�dd�'�|�RK�m�a�\u0015�2,CP08�O(��_��}�WT�\u0010\u0016۔P\u0015�\u001a_J��+/v�e�N��n���h�+{p�-�\u0018;,\u0004]��.W<����1&Uع��\n-�l�dz/�b�\u0018G�ԏ��\u0003\u0006HEȚ�32\b�\f��mB|�`l��\u0002[8��t�Y%\u0000}I�N�\u0005��)�&P.ݩ�M�+�\u0019�$I�m\u0011v̐\u0010\\5q\u0017\u0016�\u000fE�\u001e���'\u001as��\u001dr�\u000e3\u0019�m���Ё7a����\u0015t9�y\u0004o����éZ86��˶%,ݨ��ҽG\u0018r)�Z\u001d�OT\\\u0000�Ӊ���{�W�؟��T_���O�\u001b�ʍ:�>��H��O\u0015�lm�c�[�_M��hQ\u0016�4�����{��\n-Rmq^\u001d����~]��ϖ\u001a�vʨ \\@u2�z\\�Ե��5،4����P��v=�\u0000�\u001e��V\u0012���n\u0010U�\u001d7mGa��>8sH<�|��l�z���O��¯f�qs�\u0010���r\rڐC�W���bZ����i\u0015^�չ�\u0015�T}Eф�o�/\u001a�-\u001d�%��c��}HJފ�\u001b��������خJ0m�]����h����ݴC�z�\u0010�H\u000bͩ��V\u0006�QM��j�=}0}i�S-��\"J��\u0016q��*��\u0001�]����\u0003�8�8�\u0006b:\u0012��O�����jtK\n-º�UD\u001e��dk�L<�\u001b^}��\tp�2ӭ\r96b��6\u0004a\u0017}��3�qR���Y�Z�(�\u00102uk�ُt?���h�?�6Y��IB���\u001f>���\n-[�o��_�ұ�\u001f�F��C� �6��,�a��T��)S;z-�OWf!����D���:\u0019��5YY�Z�N\n-��O������{Z\u0001{����=\f��0�Dq\u0012&J���zR/C�\u000b��3���s��G�jFo<��8�����)�\u0019\u000f���mk\u0001^��\u0011t�%\u0006x�\u001f��.BIה/]\u0006\t�`��\"\u0017z%�`��Sq���^\u0002~�>\u001b�Lڌ�-2\u001b�§R;\u001a���M�\u001f��3\u0000te8\u0006\r�T�\u001cp/�+\u0017ㆇ\u00185%M���p\u0013$��:\u001fB\u001eө\u0018`Z9�\u0000��\u0018\u001aZ�T��]�\u00174Q��i\f���l�?o�y*�����Q������y]۟��F(����\rק9\u000b\t\u0011nfR*ss,$$���[]��K\u0003з���$\u001c����!Cwr?�ƙ���hm��mS�A�p�.\u0017.���w�^�y�qK�Vws��\u0019�@/\u001c���ee֨�ɀ�@\u0003�j2@��^1=�\u0017�\r%�=��k\u000e�^da��֩��W����\u000b1z\u0004�g]n^�\u0000Hm��g!!�!�^�\u000b��\u0016fߛ^��n܌k��4{XxT��Q��z3\u0002~�=\u0007���,�����\u001a^�2���`�@����W]ٯ\u001eϵ�\f08۳:�\u001c�0 (\u0010Q���z\u0006*.�(�<Md��\u0010�\u001d�f�\bUiߡ\r�ҪVY�e\u000e�\u001dtcs3�M�hE��h��28�\t[���Zj���\u00071:!\u0005k\u0016�\u001b�}�\u0004��y(����;q�u�^��ƍ��K\u0017x,-o!��*�\f��.D|\u000f�e3���)\u0006��RکZj��Y\u0005���z�0���wBp�.��/=�EG��okJ���\u0018m>%U��y�����e�$\n-@!�4��MN�t$�\bB\u000f\u0006K��h��\u001f�yJq\u00195��\u0015�`^\u0016�G��\u001d�B��4ԍ�y�\u0019��x\u0006TZzT4\"���ϑ��u\u000eX�H�@���=n�w�\u000e�\u0017��V�h��1�dC��^m�3:���&���ԉ���M�1���\u0006}��\u0014:Ѯ�\u0013��{��F��J��d��\u001fS8\u000fY��}��o�nkl\u0015=�����J�g�Q+�����G�\u0019`�k\u0018@��OL 2�>\u001e\u000e�!� ]Z�� qH\u0010^{�\f�|l�q�)�z��\u0012q��X�B�JK\r�2�;��O�-\u0016��;\b\u0003\u0006Z(�IC�ݷ�1�m��pa\"m\u0015�i�\u0018qCt�櫶esO�l9\u001f��%�\u000f\u001b����\u0015�PC�x�\u0003K�\u0011�ZU�\u0001�b���b���O��D_\u0012)��Qu\u0015�_ͦ<!\u0004\u000f�o\u0001��H�o�K4>���K����e\u0015��w߲���\u001dX�,\u0001����m�P�\u0011<�X*�A�].\u0002�\u00149��W�W��r-_T�$��u�L\u000f��J�\u0006>�υ8���\u000e�4$P�4]�&�\u0000�.��&�K�]�����\u001aV\u0011\u0003\u001a,��$O��r���>�^�\u0003�5y\u001f\u001f����v\u0001xoa9Ι�\u0006�щ�K|9�&\u000fۄ������g&0O\u0019������~����\u0000PG\u0003�u��\u0015�qH�T��g.vc~^��\u0000�If}D{:�\n-]F}���p�\u0000\u0016U�\u0012𧰞,l9�&Mދ�\u001br�[ݺE��9�xᙎxCXFZ)7��f��f����K6Z\u0011�/��cb}!���.��U\u001b\u001a^V�\f��<�K\u001fV�q;4�M��\u0006�Mf�\b�\u0015\u0010\u0016-5��&*��\u0019 ��+9���r\u0018Ok��ˏ����\u000e7���\u001b�ڵ�\u00107��D\u000ff�q���c�h�3#�+U������S}�1�/�e\u001b8*�e�\u0000�\u0019?�g�?�^\u0012�\u0004\u00152k����a;#�?x��\u001b�`��|�S+�\u0002h�\u0002;\u0003M�mт�藴\u0005;��\u0013m|�Y5��\u00185Qw�u>�{�/ƞ\u0015�a�-�YB����rLD�v�yW\u0000\u0007�y������\u0019����\u001c�\u000f4\u0010eN(�͗\u0001\u000e���\u001cV�&��\u0014�MOq!+��FT\"�\u001d��LT�(�1�gf��G\u000bi���Re����z\u001dt-���\n-���ф45�ohx�1�\b#za\\Yn��(\u0006h\u0013��\u0011\u0002\"jK\u0002b��F\u0000�\u0011�fc��Dž9�.i��\u001ed��f�\u0001��O\rR*@\u0010S���+���p�W�뵯7d����6f\u001fO��4\u0018�\u0015rE\u001f��{�B�JH�q�c�z\"^��.��ŠT\u0010R�v\"�����}��\u0017\u001a���\u001dߍ�g����P�l�)�3O4Q�/f�,\fP'\n-~��9�\u000eefA|���*��4�Vt_�n�o�\n-g?�\u0001W\\�@g�549��y3��qF\u000f\u000eƙ�Z\u001b�-�����=��5�T��GcM���X���{�2)�m<�\u001a��\u001cʼ8Bc���I��ۅ�\"�\u0001\u001f\u0002 \u0005y�ȒY:œ��m�Y�`FӀ������7긯�1ل^�4C��p˴|m�%\u0010�\u0005{�J\u0007����@�[�\u0017�϶�P��D���ix��R�\t���|9ͅ�e\u0019k\u001c\u0005��IS,�K��7W>)��'�^�H��Z�P���i���`���\u001cF|wˇ��|K�1@DQ\u0018�f�qI��\u0005��6�q��f�_��=CKt��\n-��(�~w\":{�\u001d5@e��v�^Du\n-0�^��aZ=��wb�|W�'�n���\u0010�Ș'�*�\u0014�v\u00163�\"ĄV�`�8��L\u000e\n-���H>,��o�����\u0019�w����L��\b�tc7\u001f\u0003�x\u0018��qIǴ\rD�$�+��f�g2�r\u000f��vs\u0015]\u0007]q���)�\n-U�~!\rwݓk���9�\u000fyԵ��1R��\u001by;��RW}�P\u0012���n�{z\u0007CX����<�ɘo(�z�H\u0014�\u001a�\\�q�C,\u001dt�sĴ\r��=�)�\u00063U%�{��'w\"�[D+�f�g�\u001e��*$\f�=���\f�'�vIS\"4�#��(��.�z��M}������B��I�\u0003=�&M^i��ݾ\u001b\u001f���T&A�z\\��\u0012�\u0004�F,b*�I��S�L\u0016�oH�<�����'�ҩa��\u0019V�݁\u0003���\u0002n�\u0011�d3RQ�̽.�o�\u001e�t�RF\u0007\u000f�9R�Y��\n-��\u001d���x�JE|������3\u0014�1��4��\u0001�!����\u0000�a;EPIQrI��!�Y\u0004�\u0016��`�\u001d~{co.s������>\\r�A��L�����\riQ5H|\u0012R����ߥ\u001f�D�\u000e�x��t\u0013\u000f\u0016�\u0012$�Y�z��XW�\u000f\u001e��=��fu�I��A\u0006\n-e\t���\u001a�\u0012\u0006\u0002H�>\u001a\u0014�̿\u000f2q�Sv5�|��5���\u000e0%�GR���\u0012㗒kD}�6�fg�풖��KZ��Q��m!�V���\t��w��$�:w�ci^'��`ō%��\u0016J�\b\u0011F\"/�Y�\"�6�E�ˮ.5�6%���5�j\u0010\r]9�F�\u001a��GX���p�|)�y�L}�8�\f�\f���m�ޢ\n-\u0003��{�\u000f��t\u0003x��U����t�Hte�h��8k#�^ih�v�\r_E\u0010�$�f\u0017�G�\u0007}2�\u0012�xLIN4_�+\u0014y�'\u0013Mǘ���\u000e� �ف�SV�\"�V|^w/�\u0015�Y�Y���F�Ts�\u0006zRz?�wu���С��v6����Ou����a�j�^%���~�\u0003��=�K|�1�}j'S)����@$��ಈ__i�\u0015 �(\u0002[�\u0013\u0013Ph�\u0000*�W��\u0010GO�����L�W���Gf�\r\rA��V֚\b_�`�_�����BP����l⪣��I�\u0013�|��y��̅\u001a\u0004��K%t��نW9��\u0007s�qzW�p�1\u0006M�m(\u001e��䮶�\u0002\u0019�N�ʱ`��M�U���K��sˌԗQ���\u001a\f\u0000���[\u0001l,�#�K�.�q\u000e��~��>^�ˉ��k6\u001b�+�U�AF�?R�v%�\"������0�Z)e�<��.<���um�\u001a�W-\u0007����q�\u000f\u000f�03��*����KMΒ6^��/!+�B��\b,Z;�K|���\u0016\u001f�i��`��c����l�\u0000\u000e�p�X��l�\u0007(�X��.�\u0017�g��~��\u0010h$��K>�G�Նt�\u000e�/��v�]1�í�����n���t�Be+\u001f���� \u0014I�\u0001��L�£\u001d�XcUī�GW�\u000f�Gė�\u0013RZ��IDӅ��z�v�\u001a�����i��<�=�ط�K\u0011\u00104�4\u001b\u0003����\u000f)fB�2ޔ\u001fj�\u001bۿ�\u0004��{\u001a�z�\u0018��\u0014f&'\u0007�����F\\���ND��\u0016N����\t�=I�2W)�W]�A�-��s��e�w\u001dw�y㛮���ґ��\u001f*d.�\u0015�RU��h&�&\\\u001e:*�\u001c�8��p�l�>\u000f�ޚ�u��\f\\�\n-���\u001f�c�a_k�\u0013�Q�Z��\t����\u0003{v\u000b��\u0015��Bp�\u0005axI\u0005>��M\u001b�������E`-�ڊ�Q0�\u0000\u0013�N??\u0004�>�̈\u001b_bY��!�\u0018��\u0014�\u0018\t:��H����!��~iGc�����\u001fF1�Z1��#5%�|�\u0003m���A���=潷әݽ\r��n�\u001ck\u001d0�W��kl\t\u0018;��:�ݸ��\u0006�W\n-����G\u0017�=�=tBҷ��c\"|�.\u000fK\u0017\u0014��S��z;�LZ�T-B@���\u0011�Z�wE���нw�\u000e@\u0011�YE��df����\u001e�����Wm\b>*@߁��:v�#��L��h�fs\u001e�V9\u0007�\\شǍ�\u0017t��\u000b�{Ǒ��`3�H5)�:����\u0007)������3Cwy�� ���Q\u0013M5t��\u0018���iWD��Q�愣�[bH���0�\u000e�+�.���E�bM\u001e>�[L��4G��\r�O����*�\u0006ҠS\u0016��-n��q���J\u0015\u0019w�.\u00139����\u0015-��Y�R����\u000b���vu�I5T\u001cY*t\u0001Uί�������lh���,�:c\u000f\u000e�d��u��4��Q9jϷ4�\n-�mT�An�o78��QPs�(\f\u001f\u0019G�W\f�y8÷(��$ܵ��������&��y%�\u0019��ѯ�|�1\u0019�\u000f�����^tN\n-\"�j\"��\u0005+k�|�\u000fY�ʬ+a.�\u0007�\u0006�\u000bW�\u0012���|�Q%2��Ǭ��t{�6������\u001bs�0=�0�p�*�Q�\u0019\b�]\u0001-�\u0000-ymx�p�^R��N\u001e|� ��0I\u0017Ec؈Xs�(�s��&��~nM���dn\t%�\u0012�?�Q�t2UtR�_m6�\u0019�\u0013\u0013\u0007n\u0000yH�����ɧ�ͯ��!�\u001d\u0013���\u0019t;�l���=iDu��a\\�>�����\u0016\\6�\u00164�jCWޗ���e@�� ߾%��М�\u0016Y\u001a���x��\f�&���?�q�`:]\u0005\u0013.��Y�bM\u0016�T�\\��7v|\u0017:����_\t\u0005�U��x��)H�OV\u000e�l\u0019Zr\u000b\b��8�͖�}#\u0010|�\u0004�xҏk,^�M\u0013х!�#p�G}�Yp�g%ϲ]w\u001e]�\u001d�NI|%��4\u0013�M[�G�0�n� \u0012�]\u001b\\%�\u0015��\u001a\u0007�\u0014�\u0007��CnJ���9�x�\u0003�'�0}<\u0016�}�� s�\bU�^�/e\t�\u0016\u0016Va��(��R�#�`80\u0000��\u0016���֯�(�S��!\u0014�@�W0h�*'�7��-d�l����\u0014d\u001e�K\u000f���_{Q\u0006��ލ;���y\\��W4�\u0004���Pi\u0011\u0015�P���je�$��\u0016|]�_aQ�����0���D8��̓\u001av��ݮ~�c��B���a��\"svX�8��?��\u0014���<�l\bs��b��0y^\u001f{d\u001bƌ���}V�ڃ\u001e�;�?E W8m\u00022S�Fm��\u001eG\u0013�\u0014\r�\tw�\ta@m\u0003��j��5;����\u0013�\u001c$f\\\u0016��\u0013��!q\f�Gz]\u001av\u001b9<n�}\u001b��6\u001f��-�j�����K\u0018\u001a���E�Ct\u0019\u000f<ˁ=?��\u0014i\u000fQԮ+��Td\t+�e�/\u000e��o�б�uv��M��\tן\u001e\u00034\u0019�\f�\u0006\u001d�\u0010���\u0004\u001e&\u00017��\u0006��k�l�q\u0013�2�\u0012��S�7I�&�a��-\u0005�%��G����\f�3\u0018�lI\u001f�as�P��\\�f���Ls�-������a�s�i2�\u001e�\b!\\.��_1\u0011��:���X���d�\u0000���p�\u0019\u0017K+�-z�ȫb�v��2�6�G��Y��D�3\u0004���o�K/w\u001b��s�CyfM��![�n�\u0019�?m\u0006�5A�}�zd\u0004�����,�9�{ɍ�\u0018=��8=��r\u0012��\u0011�\u001d�����6r�\u00015�C��0�t�s��\u0016�[\u0002��5\u000f\r�'�\u0007�3�c\u001e�~\u0017a{�\u001a:�qp���S.����z�ύe�9�X�Js��Μ=�P�\u0017JL���kE����:?���e�M I��ԓ�ϖ�[�\u0003��P1#���_�\u001cA/g,\u0006�\u0016���b�4=\u001d\u0015\fw��oTH�mfZ-���'?\u0003~@.NI<11[�)E���`�O+������G��D��Śŝ��!4���C\u0006xfot>�\b������Ep9�4\u0006\u0015����}m|�2�e-�ںn�{��\u0001ܬ\u0005gM��k�6nn��\t\u0010�\u0018\n-��c���lcJ�Ҕ���o�\u0013��\u000b���H\n-�2�DR�}�2��\u0001'�\u0002'���\u0015�ʀ\u0013L�\u0013�}�\n-he�\t��\t�u\u0005�2�\u0004S�\u0004wߺ\u0002N0\u0003�\u000f��&\u001enXpZ\u0000\u0000\u0000\u0000IEND�B`�\n\\n+[DELETED]\n\\n"
|
|
1893
|
-
},
|
|
1894
|
-
{
|
|
1895
|
-
"path": "native/assets/manifold_white_transparent.png",
|
|
1896
|
-
"status": "added",
|
|
1897
|
-
"diff": "Index: native/assets/manifold_white_transparent.png\n===================================================================\n--- native/assets/manifold_white_transparent.png\t74465a3 (parent)\n+++ native/assets/manifold_white_transparent.png\t7d171ee (commit)\n@@ -1,1 +1,22 @@\n-[NEW FILE]\n\\n+�PNG\r\n+\u001a\n+\u0000\u0000\u0000\rIHDR\u0000\u0000\u0000`\u0000\u0000\u0000`\b\u0006\u0000\u0000\u0000�w8\u0000\u0000\u0000\u0004gAMA\u0000\u0000��\u000b�a\u0005\u0000\u0000\n+IiCCPsRGB IEC61966-2.1\u0000\u0000H��SwX��\u0016>��e\u000fVB��l�\u0000\"#�\b�\u0010Y�\u0010�\u0000a�\u0010\u0012@Ņ�\n+V\u0014\u0015\u0011�HUĂ�\n+H���(�gA��Z�U\\8�\u001fܧ�}z�����������y�\u000f�\u0011\u0012&��j\u00009R�<:�\u001f�OH�ɽ�\u0002\u0015H�\u0004 \u0010���g\u0005�\u0000\u0000�\u0003yx~t�?�\u0001�o\u0000\u0002\u0000p�.$\u0012�����P&W\u0000 �\u0000�\"\u0012�\u000b\u0001�R\u0000�.T�\u0014\u0000�\u0018\u0000�S�d\n+\u0000�\u0000\u0000ly|B\"\u0000�\r\u0000��I>\u0005\u0000ة��\u0017\u0000آ\u001c�\b\u0000�\u0001\u0000�(G$\u0002@�\u0000`U�R,\u0002��\u0000��@\".\u0004��\u0001�Y�2G\u0002��\u0005\u0000v�X�\u000f@`\u0000��B,�\u0000 8\u0002\u0000C\u001e\u0013�\u0003 L\u0003�0ҿ�_p��H\u0001\u0000�˕͗K�3\u0014���\u001aw����!��l�Ba\u0017)\u0010f\t�\"���#\u0013H�\u0003L�\f\u0000\u0000\u001a����8?������f�l��Ţ�k�o\">!�����\u0002\u0004\u0000\u0010N���_���\u0003p�\u0001�u�k�[\u0000�V\u0000h��]3�\t�Z\n+�z��y8�@\u001e��P�<\u001d\u001c\n+\u000b\u000b�%b��0�>�3�o��~��@\u001e��z�\u0000q�@������qanv�R���\u0004B1n��#�Dž��)��4�\\,\u0015��X��P\"M�y�R�D!ɕ�\u0012�2�\u001f��\t�w\r\u0000��O�N�\u0007��l�~�\u0001\u0002�\u000eX�v\u0000@~�-�\u001a\u000b�\u0000\u0010g42y�\u0000\u0000����@+\u0001\u0000͗��\u0000\u0000��\u0018\\��\u0017L�\b\u0000\u0000D��*�A\u0007\f�\u0014��\u000e��\u001d��\u0017\u0002a\u0006D@\f$�<\u0010B\u0006�\u001c\n+�\u0018�A\u0019T�:�\u0004��\u0003\u001a�\u0011��\u0010��18\r��\u0012\\��p\u0017\u0006`\u0018��\u0018��\t\u0004A�\b\u0013a!:�\u0011b��\"�\b\u0017��\u0004\"aH4��� �\u0014Q\"��r�\u0002�Bj�]H#�-r\u00149�\\@���� 2����G1���Q\u0003�\u0002u@��\u001f\u001a�Ơs�t4\u000f]���k�\u001a�\u001e=�����K�ut\u0000}��c��1\u000ef��a\\��E`�X\u001a&�\u0016c�X5V�5c\u001dX7v\u0015\u001b��a�\b$\u0002��\u0013�\b^�\u0010�l���GXLXC�%�#�\u0012�\bW\t��1�'\"��O�%z\u0012��xb:��XF�&�!\u001e!�%^'\u000e\u0013_�H$\u000eɒ�N\n+!%�2I\u000bIkH�H-�S�>�\u0010i�L&�m���\b��� ����\u000f�O�����\u0014:ň�L\t�$R��\u0012J5e?�\u0004��2B���Qͩ��\b��:�ZIm�vP/S��\u00134u�%͛\u0016Cˤ-��Кigi�h/�t�\t݃\u001eE�З�k�\u0007����w\f\r�\r��Hb(\u0019k\u0019{\u0019�\u0018�\u0019/�L�\u0005ӗ��T0�2\u001b�g�\u000f�oUX*�*|\u0015��\u0012�:�V�~��TUsU?�y�\u000bT�U\u000f�^V}�FU�P�\t�\u0016�թ\u001dU��6��RwR�P�Q_��_���c\r���F��H�Tc��\u0019�!\u0016�2e�XB�rV\u0003�,k�Mb[���Lv\u0005�\u001bv/{LSCs�f�f�f��q�\u0001\u000eƱ��9ٜJ�!�\r�{-\u0003-?-��j�f�~�7�zھ�b�r�\u0016����up�@�,��:m:�u\t�6�Q����u��>�c�y�\t���\u000e���G�m���\u0017�����\u001f704\b6�\u0019l18c�̐c�k�i�����\u0011�h���h��I�'�&�g�5x\u0017>f�o\u001cb�4�e�k<abi2ۤĤ��)͔k�f�Ѵ�t���,ܬج��9՜k�a�ټ�����E��J�6�ǖږ|�\u0005�M����V>VyV�VI�\\�,�m�WlP\u001bW�\f�:�˶�����v�m�\u0014�\u0014�)�)�Sn�1���\n+��\u0006�9�a�%�m��\u001d�\u001c\u0012\u001d�;t;|rtu�vlp���4éĩ��Wg\u001bg�s��5\u0017�K��\u0012�v�\u0017Sm���n�z˕�\u001a�ҵ������ܭ�m���=�}��M.�\u001b�]�=�A���X�q�㝧�����/^v^Y^��\u001eO��&��0m���[��{`:>=e���\u0003>�>\u0002�z�����\"�=�#~�~�~\u0007���;�������y�\u0016�N\u0005`\u0001�\u0001�\u0001��\u001a��\u0003k\u0003\u001f\u0004�\u0004�\u00075\u0005�\u0005�\u0006/\f>\u0015B\f\t\rY\u001fr�o�\u0017�\u001b�c3�g,��\u0015�\b�\u0015Z\u001b�0�&L\u001e�\u0011���\b�\u0010~o��L�̶\b��Gl��\u001fi\u0019�\u0017�}\u0014)*2�.�Q�Stqt�,֬�Y�g���;�j�rvg�jlRlc웸�����x��E�\u0012t\u0013$\t�����=��s\u0002�l�3��T�tc��ܢ�\u0017���˞w<Y5Y�|8��\u0012��?� BP/\u0018O�nM\u001d\u0013OE����Q���J<��V��8�;}C�h�OFu�3\tOR+y�\u0019��#�MVD�ެ��q�-9�����R\ri��+�0�(�Of++�\r�y�m�\u001b�����#�s��\u0015l�Lѣ�R�P\u000e\u0016L/�+x[\u0018[x�H�HZ�3�f���#\u000b�\u0016|���P���ظxY��\"�E�\u0016#�S\u0017w.1]R�dxi��}�h˲��P�XRU�jy��R�ҥ�C+�W4�����n��Z�c\u0015a�dU�j��[V*\u0017�_�p�����F���WN_�|�ym���J����H��n��Y��J�jA�І�\r�\u001b��\u001b_mJ�t�zj��ʹ���\u00035a5�[̶���6��z�]�V������&�ֿ�w{�\u000e�\u001d\u0015;��켵+xWk�E}�n��ݏ\u001ab\u001b���~ݸGwOŞ�{�{\u0007�E��jtolܯ���\tmR6�\u001eH:p囀oڛ�w�pZ*\u000e�A��'ߦ|{�P������ߙ��\b�Hy+�:�u�-�m�=���茣�\u001d^\u001dG���~�1�cu�5�W���(=��䂓�d���N?=ԙ�y�L��k]Q]�gCϞ?\u0017t�L�_�����]�p�\"�b�%�K�=�=G~p��H�[o�e���W<�t�M�;����j��s���.]�y����\u001b�n&�\u001c�%���v��\u0017w\n+�L�]z�x������\u0007�\u000f����e�m��`�`��Y\u000f�\u000e\t�����Ӈ��G�G�#F#���\u001f\u001f\u001b\r\u001a��dΓ᧲�\u0013��~V�y�s������K�X���\u000b��Ͽ�y��r�:�#�\u001f��y=�����}���ǽ\u001f�(�@�P���cǧ�O�>�|��/����-G8�\u0000\u0000\u0000 cHRM\u0000\u0000z&\u0000\u0000��\u0000\u0000�\u0000\u0000\u0000��\u0000\u0000u0\u0000\u0000�`\u0000\u0000:�\u0000\u0000\u0017p��Q<\u0000\u0000\u0000\tpHYs\u0000\u0000.#\u0000\u0000.#\u0001x�?v\u0000\u0000\b�IDATx��]��6\u0012}{u�\u000f�\u0001/\u0002�\u0011X\u001b��\u0011�\u001b�i#�\u001c\u0001�\u0001�\u0011p\u001c��\u0011P\u001b\u0001�\u0011�\u0017A�\u000fPV\u0013\u0004I\u0010\u0000Ei�Wյ\u001c|t7�\u0001-\u0002��~ \"��\u000e��ځ�;v\u00026�N���\t�\u0018;\u0001\u001bc'`c�\u0004l�����\u0013�1v\u00026�3\u0011P\u0003�\u0000��~$ų\u0010�\u0000H\u0000\u0002�O[:�\u001a�B�'��`�x\u0017x\u0006\u0002\u0004��*Sw�b%<\u0003\u0001�Q��Q��x\u0006\u0002\\�>�ۉ���\u0004\b�V@\u000b��Q��XB�\u0000Pu\"�p�\u0001Ş_\u0000��~~\u001fi��|D\u0010QM7�]�o�P��Mٕ]��`J\u0014\u0011�b����LC�+\u000f0c�\u001aV^�r�Q��Tq�IA%�\u001fz9�S��hC��W����\u000f+�w!�ّ\u001fYY\u001b�q\u0001�c�W�e5�!Y�`��J�]r ��l�\u0000]�Te�\b�\u000b:�\t�dL㨯R\u0005�SN#cw��H�RP\u000e�z��\u0005�W@\u0000��[Z\u0010\u0000�H�f���o�z���<\u001b\u00120c�y��5ڂ�\u0015I��V����\u000bZ�͈��\u001c�s+$��N7GA�W�\\��V��ՁL�y�*��yp�vu�@Xr�>4�7.�ʚ\u0014�x\n+\u00120iGt?_\u0000�8�x\u0004�\u001b�G��\u0004\u0014�\"rQ��=�ҏ��\u0010i�\n+\u0001\u0013\u0003>�\u000b�\u0018_�O�_�Xd3���\u0015\u000e�N�́���#fE��d\u0013�|W��H+\u0006D&��8T�N%�\t�b\"�\u0004\u0015���:G�Vp8\u000169DaiA���G����\"kNr�RM�I$����SG@Aa8:\u0014���KIྸ��r\\��%�g\"3�\\~�M\u0019h�I@(N\u000e���ތ\u001a�ק��l-\u0019|FÔSMحX;��֨| �\u0013�[�\u000b��Y\u001f4mW~�y/\u0006�����X\u00110�������q$�~����z\u0001�h���\u0000x����\u0000�/\u0000~\u001di/`.\u0004\u0000&\u0016���m\u001e\u000b\u0019\u0013l\u0016�gf�fmK\u000f�%k�/�X��D}h\u001a~�ْ/\u001c���t��<�NY\u0003�\u000b�fm�\u0002$�WM�\u00134<�={ڪX\u001f\u0015\u0010�U\b�=�\u001e�\u0001�9�ɚ\n+�4��+�\u0007\u001a\u001e�\u0015��\u0005�\u0003|KN��9�Ӿ�\u0006 g��\u0001>\u0015\u0013��I�i�,�Y�2���\t��44���\u0005Ǯ�\u0011��Wt��\u0015�Q���\u0002ױ���\b�\u000e�\u000eF����\u0015�2b0��4<H\u000b�}���mTBnE|cϙg�\u0016�g�Ό$n���8�\u001f3/�+{��W۟\u0003t�\u0011��p�k)\u0017���s�C/���\"n�.�.0{��@���E��\u0018G���ٲ<F�O�\"@�w�KӚ�M5c\u0012�\u0002�س\b��\u0002��3t�)��#Y�\u0017���\u0006��\u0015��k��Ilu3�\u000b���\u001a��@���\u001b��1~��\u0004�\u001f (\u0005\t���\u0011�OP������Klʹk�\tMAm\"�[�ӄ�߷i\u0007�\u00038��~A���X(�!�\u0000\bZ\u0001�fm�,���/&ڞ�Y�i� -DJfC��_t~�P\u0005g�`\n+�r+�����jSSܷ`S�;\u001b:��\u0003�7ƒ���(\u0004�ƿҔ�\u001e�F�A�;sIf\u0002\u0015�SˡC��������Α;��\t����f�ft���Lc�D�R��C��\u000b\u001a��\\Q�}�!j\u0016 ۷C7����r\f\r\rӧ��埫~�/G\u000b�G8[�/]y��}����#��`�G�\u0016f?���ҕ��6\u001f��\u000b�%9c�p��4\\�yB�sR�Od'�dr�\"w*�W�&k\u0015���7�,\u0002u�(пw�\u0006�O�$��\u00035Qw��\u000f�<�\ff�.��[��!E\n+�Wd�\f}G\u0001�������B�H�[��.�%ѿ1��\u0011|\u0000�)\b�2\f�q�a�9E�K�~bҞ��4H;\\�$�d\u0005^�:�Z_�A\u0013��_O\u0005?����fa?A�\rJ\u001d1�\u0014��/U��ώ)�8��ϲ\u0005}$������\n+s��\u0006�!a�m\u0015�\u001f�?z�� ��/�=\u001c��5|\u0016��?Z�^S�A���\u001b�KW3��ZK�f�Ub\u0006_2�r&H\u0015�QS�#���\u0019�C1\u001d�¾\u0007�\b>Q\\\n+��=��6\u0012fi*V�\u0002�<�\"l\u0003f�p��\"��\u0003\u0010�~r��\u001d���wb�\u001f\u001c�9\r_��\b{S�>F/��;{s�^з'1A�K�dՕ��\r�;H\u0013��ޗ\u0011�\u000eLO���\u000e~E\u0011op1���_\t�h��!c�6��\u000b�+�ho\u0007��\u001dO*\u0002\n+2+B[\u000e\u001ec\u001d�\t����0]sD&\u000f>E\u0012\u0000挶�Ӕ~W+-;��J���i�J�)!\u0001\u001cgJ��\u0015�Oms\u0001���'�\u001d��)�\u0016�@[�iZ�[���h(\r�\r�9���%z��\u000f�R�\u001b\r�\u0019����ҝB�d���6�e�L`79\u0001��\u0012�i\u001c�s^\u0005\u0006J3]�D\u0003/���Q_Zc8%���\u0000.Y\u0017�3�C�22jַJ�k��\n+���|v\u0011��\u0004p\u0011��\u0015M����A\u0000�gi3�&D�e��ӊ\u0004Ē��:�Пb$��es��ӝ\b�Eu��4\u000e^wJl�a�E'�e�.���\bXJFM�%K`O2�\u0015�/��-��\u0000\u0004��)h��YMq��\u0005�u���Od�XǢ��;A������W�o0�W~���!��=Z���|�ڿ\u000b���\u0012���\u0010�\u0013Ư\u0007��\u0004�\u000f�\u0007Q���X8Z��ٹ\u0007��\"%\u001bI\u001d\u001c��{�b�mh:{�)�\u0007\u0019��O\u0018�R��2~F?���r�wxf\u00028\u0004\f\tSdp�x��\u0003�\u0000\u000e���\u0010V}�\u0007\t>�>\t��p#���o\\O��@�C���\u000f�w�����\u0013�1v\u00026�N���\t�\u0018;\u0001\u001bc'`c�\u0004l�����\u0013�1�\u000f9\t�1=I\u0012\u0014\u0000\u0000\u0000\u000eeXIfMM\u0000*\u0000\u0000\u0000\b\u0000\u0000\u0000\u0000\u0000\u0000\u0000�S�\u0000\u0000\u0000\u0000IEND�B`�\n\\n"
|
|
1898
|
-
},
|
|
1899
|
-
{
|
|
1900
|
-
"path": "native/components/auth-page.tsx",
|
|
1901
|
-
"status": "modified",
|
|
1902
|
-
"diff": "Index: native/components/auth-page.tsx\n===================================================================\n--- native/components/auth-page.tsx\t74465a3 (parent)\n+++ native/components/auth-page.tsx\t7d171ee (commit)\n@@ -223,15 +223,16 @@\n \n export const AuthPageStyles = StyleSheet.create({\n container: {\n flex: 1,\n+ display: 'flex',\n justifyContent: 'center',\n backgroundColor: '#4337C9',\n },\n flappy: {\n height: 175,\n resizeMode: 'contain',\n- marginTop: 180,\n+ marginTop: 150,\n },\n googleButton: {\n backgroundColor: 'white',\n borderRadius: 5,\n@@ -259,8 +260,9 @@\n width: 300,\n paddingTop: 20,\n padding: 35,\n height: 180,\n+ minHeight: 180,\n alignItems: 'center',\n },\n modalView: {\n margin: 20,\n"
|
|
1903
|
-
},
|
|
1904
|
-
{
|
|
1905
|
-
"path": "native/components/custom-webview.tsx",
|
|
1906
|
-
"status": "modified",
|
|
1907
|
-
"diff": "Index: native/components/custom-webview.tsx\n===================================================================\n--- native/components/custom-webview.tsx\t74465a3 (parent)\n+++ native/components/custom-webview.tsx\t7d171ee (commit)\n@@ -33,16 +33,18 @@\n resetWebView: () => void\n setHasLoadedWebView: (loaded: boolean) => void\n handleMessageFromWebview: (m: any) => Promise<void>\n handleExternalLink: (url: string) => void\n+ display: boolean\n }) => {\n const {\n urlToLoad,\n webview,\n resetWebView,\n setHasLoadedWebView,\n handleMessageFromWebview,\n handleExternalLink,\n+ display,\n } = props\n \n const [refreshing, setRefreshing] = useState(false)\n const [refresherEnabled, setEnableRefresher] = useState(true)\n@@ -55,12 +57,14 @@\n } else if (refresherEnabled) {\n setEnableRefresher(false)\n }\n }\n+\n return (\n- <>\n+ <View style={{ flex: display ? 1 : 0, height: display ? 'auto' : 0 }}>\n {Platform.OS === 'android' ? (\n <ScrollView\n+ style={{ display: display ? 'flex' : 'none' }}\n contentContainerStyle={[styles.container, { position: 'relative' }]}\n refreshControl={\n <RefreshControl\n refreshing={refreshing}\n@@ -127,9 +131,9 @@\n }}\n />\n </View>\n )}\n- </>\n+ </View>\n )\n }\n \n const styles = StyleSheet.create({\n"
|
|
1908
|
-
},
|
|
1909
|
-
{
|
|
1910
|
-
"path": "native/ios/Manifold/Info.plist",
|
|
1911
|
-
"status": "modified",
|
|
1912
|
-
"diff": "Index: native/ios/Manifold/Info.plist\n===================================================================\n--- native/ios/Manifold/Info.plist\t74465a3 (parent)\n+++ native/ios/Manifold/Info.plist\t7d171ee (commit)\n@@ -20,9 +20,9 @@\n <string>$(PRODUCT_NAME)</string>\n <key>CFBundlePackageType</key>\n <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>\n <key>CFBundleShortVersionString</key>\n- <string>2.0.65</string>\n+ <string>2.0.66</string>\n <key>CFBundleSignature</key>\n <string>????</string>\n <key>CFBundleURLTypes</key>\n <array>\n@@ -40,9 +40,9 @@\n </array>\n </dict>\n </array>\n <key>CFBundleVersion</key>\n- <string>1.0.65</string>\n+ <string>1.0.66</string>\n <key>ITSAppUsesNonExemptEncryption</key>\n <false/>\n <key>LSMinimumSystemVersion</key>\n <string>12.0</string>\n"
|
|
1913
|
-
}
|
|
1914
|
-
]
|
|
1915
|
-
},
|
|
1916
|
-
{
|
|
1917
|
-
"id": "update-post-validation",
|
|
1918
|
-
"sha": "94c2f0dcd82ccba7138395612a8fa83712096c0e",
|
|
1919
|
-
"parentSha": "f5cfe579a7c2f5ceac6a467836c61ac97ae1ce70",
|
|
1920
|
-
"spec": "Implement improved post creation validation behavior and title length support:\n\n- Shared API schema\n - In common/src/api/schema.ts, update the 'create-post' endpoint props to allow a longer title by increasing the z.string().max value on title from 120 to 160.\n\n- Web client: create-post page\n - In web/pages/create-post.tsx, update imports to include APIError from the web API client module.\n - In the form submission logic (the function that calls api('create-post', ...)), change error handling to detect server-side validation failures. When an APIError is thrown with a message indicating request validation failure and includes a structured details array of { field: string; error: string }, display the first field-level error to the user in the existing error UI. For other APIError cases, surface the API-provided message. For non-APIError exceptions, log the error and display the existing generic error message.\n - Ensure that on success the existing behavior (clearing the editor content and any navigation/notifications) remains unchanged.\n\n- Assumptions/compatibility\n - The backend is already producing APIError instances for validation failures with message text that includes 'validating request' and a details array of field-level errors; the client-side change should consume this contract.\n - Do not alter backend behavior beyond the schema change; no route or handler changes are required for this task.\n\n- Acceptance criteria\n - The API allows creating a post with a title up to 160 characters; attempts with longer titles fail with a validation error.\n - When the API rejects the request due to validation (e.g., title too long), the create-post page displays a clear field-specific message derived from the APIError details (e.g., 'title: String must contain at most 160 character(s)').\n - Other API errors show their message; unknown errors show the existing generic fallback message.\n",
|
|
1921
|
-
"prompt": "Increase the maximum allowed title length for creating a post and improve the create-post page to display server-side validation errors clearly. Update the shared API schema so the title can be up to 160 characters, and modify the create-post page to catch API errors: if the server returns a validation error, show the relevant field error to the user; otherwise show the server message or a generic fallback. Keep existing success behavior the same.",
|
|
1922
|
-
"supplementalFiles": [
|
|
1923
|
-
"backend/api/src/create-post.ts",
|
|
1924
|
-
"backend/api/src/routes.ts",
|
|
1925
|
-
"backend/api/src/helpers/endpoint.ts",
|
|
1926
|
-
"web/lib/api/api.ts",
|
|
1927
|
-
"common/src/api/zod-types.ts",
|
|
1928
|
-
"common/src/top-level-post.ts",
|
|
1929
|
-
"common/src/api/utils.ts"
|
|
1930
|
-
],
|
|
1931
|
-
"fileDiffs": [
|
|
1932
|
-
{
|
|
1933
|
-
"path": "common/src/api/schema.ts",
|
|
1934
|
-
"status": "modified",
|
|
1935
|
-
"diff": "Index: common/src/api/schema.ts\n===================================================================\n--- common/src/api/schema.ts\tf5cfe57 (parent)\n+++ common/src/api/schema.ts\t94c2f0d (commit)\n@@ -2305,9 +2305,9 @@\n authed: true,\n returns: {} as { post: TopLevelPost },\n props: z\n .object({\n- title: z.string().min(1).max(120),\n+ title: z.string().min(1).max(160),\n content: contentSchema,\n isAnnouncement: z.boolean().optional(),\n visibility: z.enum(['public', 'unlisted']).optional(),\n isChangeLog: z.boolean().optional(),\n"
|
|
1936
|
-
},
|
|
1937
|
-
{
|
|
1938
|
-
"path": "web/pages/create-post.tsx",
|
|
1939
|
-
"status": "modified",
|
|
1940
|
-
"diff": "Index: web/pages/create-post.tsx\n===================================================================\n--- web/pages/create-post.tsx\tf5cfe57 (parent)\n+++ web/pages/create-post.tsx\t94c2f0d (commit)\n@@ -9,9 +9,9 @@\n import { useUser } from 'web/hooks/use-user'\n import { linkClass } from 'web/components/widgets/site-link'\n import clsx from 'clsx'\n import Link from 'next/link'\n-import { api } from 'web/lib/api/api'\n+import { api, APIError } from 'web/lib/api/api'\n import { Page } from 'web/components/layout/page'\n import { DAY_MS } from 'common/util/time'\n import ShortToggle from 'web/components/widgets/short-toggle'\n import { Row } from 'web/components/layout/row'\n@@ -82,10 +82,18 @@\n isChangeLog,\n }\n \n const result = await api('create-post', newPost).catch((e) => {\n- console.log(e)\n- setError('There was an error creating the post, please try again')\n+ if (e instanceof APIError) {\n+ if (e.message.includes('validating request')) {\n+ const details = e.details as { field: string; error: string }[]\n+ const detail = details[0]\n+ setError(`${detail.field}: ${detail.error}`)\n+ } else setError(e.message)\n+ } else {\n+ console.error(e)\n+ setError('There was an error creating the post, please try again')\n+ }\n return e\n })\n if (result.post) {\n editor.commands.clearContent(true)\n"
|
|
1941
|
-
}
|
|
1942
|
-
]
|
|
1943
|
-
}
|
|
1944
|
-
]
|
|
1945
|
-
}
|