Compare commits
104 Commits
Author | SHA1 | Date | |
---|---|---|---|
5c9378833e | |||
5fc9930265 | |||
685414e8fb | |||
fe4bba5169 | |||
e9796c545d | |||
fc84300712 | |||
73329df47b | |||
dc1e75d99d | |||
0a7d2d160a | |||
67e4b825db | |||
dfe1de5279 | |||
38a2fe8215 | |||
8ccb88558f | |||
31bfb357b3 | |||
611d553193 | |||
543ea966af | |||
f476d8d9ad | |||
3f1334c02c | |||
78dd1dce40 | |||
a457d6dd50 | |||
038b26ab7e | |||
a55f49d5a2 | |||
8a191bb694 | |||
b9460be02d | |||
fd7839932d | |||
1fa2aa1bdd | |||
d739def318 | |||
bf89e4b11d | |||
f5bf50456a | |||
d7843b52c3 | |||
370f332e64 | |||
c0a72b6b9c | |||
8c91993f78 | |||
|
2176c51d5f | ||
002a0e5e2f | |||
a873cc4497 | |||
bc09456bb7 | |||
fc9c222a44 | |||
83a01e6156 | |||
346e070d0e | |||
2518707d6a | |||
ba84377eae | |||
f0f4188758 | |||
a4b88e6023 | |||
e8f1839a95 | |||
63628c96fe | |||
0d84c26e08 | |||
1bf1cf9c83 | |||
ed0c6e0588 | |||
133db36d69 | |||
9766b49046 | |||
a78918da61 | |||
17ba3c1ce4 | |||
c48a80b0d2 | |||
aff30ac0c4 | |||
0cddbf69b0 | |||
e4a3f38ddf | |||
0e3cede352 | |||
aeece45756 | |||
3892db1157 | |||
69ab8f905c | |||
a751caf8fb | |||
494e8d74ad | |||
cb6f9239bf | |||
c9bfba8474 | |||
acc85dad63 | |||
|
f98958f27e | ||
|
7a5b7837f5 | ||
|
1a0e42147e | ||
|
00615c1e63 | ||
|
ceeeff9681 | ||
|
5bf24a58ef | ||
|
646397d8d4 | ||
|
2345b81350 | ||
|
de47956e29 | ||
|
c96627e2cf | ||
|
cc2257b626 | ||
|
c051f0dc7d | ||
|
c1330101e1 | ||
|
558c0964e4 | ||
|
6e25f8bcd2 | ||
|
69e5a6ece5 | ||
|
8fac309bf3 | ||
|
c3d144337b | ||
|
f5a3c16322 | ||
|
addf87b22a | ||
|
19779bca02 | ||
|
1e357a75b8 | ||
|
a6e6053c63 | ||
05c87e0e17 | |||
b60316e450 | |||
|
ab2efa8a88 | ||
|
31555615a9 | ||
|
9c162fab9d | ||
|
a80bc51d58 | ||
|
5c65cdf2f7 | ||
|
5fdea80f8b | ||
40c6f57c11 | |||
020f0c0c5e | |||
7236e9662f | |||
3afbb647da | |||
51bdfc915d | |||
|
d684d074f5 | ||
|
4d736f711d |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -31,6 +31,7 @@ yarn-error.log*
|
||||||
|
|
||||||
# yarn vs npm conflict
|
# yarn vs npm conflict
|
||||||
package-lock.json ## use yarn.lock instead
|
package-lock.json ## use yarn.lock instead
|
||||||
|
# yarn.lock
|
||||||
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
.netlify
|
.netlify
|
||||||
|
|
7
LICENSE.md
Normal file
7
LICENSE.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
Copyright 2023 Quantified Uncertainty Research Institute.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
40
README.md
40
README.md
|
@ -11,8 +11,8 @@ This repository includes the source code for both the website and the library th
|
||||||
### 1. Download this repository
|
### 1. Download this repository
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/QURIresearch/metaforecast
|
$ git clone https://github.com/quantified-uncertainty/metaforecast
|
||||||
$ cd metaforecasts
|
$ cd metaforecast
|
||||||
$ npm install
|
$ npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -26,17 +26,28 @@ See [./docs/configuration.md](./docs/configuration.md) for details.
|
||||||
|
|
||||||
### 3. Actually run
|
### 3. Actually run
|
||||||
|
|
||||||
`npm run cli` starts a local CLI which presents the user with choices. If you would like to skip that step, use the option name instead, e.g., `npm run cli wildeford`.
|
After installing and building (`npm run build`) the application, `npm run cli` starts a local CLI which presents the user with choices. If you would like to skip that step, use the option name instead, e.g., `npm run cli wildeford`.
|
||||||
|
|
||||||
![](./public/screenshot-cli.png)
|
![](./public/screenshot-cli.png)
|
||||||
|
|
||||||
`npm run next-dev` starts a Next.js dev server with the website on `http://localhost:3000`.
|
`npm run next-dev` starts a Next.js dev server with the website on `http://localhost:3000`.
|
||||||
|
|
||||||
|
So overall this would look like
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/quantified-uncertainty/metaforecast
|
||||||
|
$ cd metaforecast
|
||||||
|
$ npm install
|
||||||
|
$ npm run build
|
||||||
|
$ npm run cli
|
||||||
|
$ npm run next-dev
|
||||||
|
```
|
||||||
|
|
||||||
### 4. Example: download the metaforecasts database
|
### 4. Example: download the metaforecasts database
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/QURIresearch/metaforecast
|
$ git clone https://github.com/quantified-uncertainty/metaforecast
|
||||||
$ cd metaforecasts
|
$ cd metaforecast
|
||||||
$ npm install
|
$ npm install
|
||||||
$ node src/backend/manual/manualDownload.js
|
$ node src/backend/manual/manualDownload.js
|
||||||
```
|
```
|
||||||
|
@ -63,7 +74,7 @@ In general, if you want to integrate metaforecast into your service, we want to
|
||||||
|
|
||||||
## What are "stars" and how are they computed
|
## What are "stars" and how are they computed
|
||||||
|
|
||||||
Star ratings—e.g. ★★★☆☆—are an indicator of the quality of an aggregate forecast for a question. These ratings currently try to reflect my own best judgment and the best judgment of forecasting experts I've asked, based on our collective experience forecasting on these platforms. Thus, stars have a strong subjective component which could be formalized and refined in the future. You can see the code used to decide how many stars to assign [here](./src/backend/utils/stars.js).
|
Star ratings—e.g. ★★★☆☆—are an indicator of the quality of an aggregate forecast for a question. These ratings currently try to reflect my own best judgment and the best judgment of forecasting experts I've asked, based on our collective experience forecasting on these platforms. Thus, stars have a strong subjective component which could be formalized and refined in the future. You can see the code used to decide how many stars a forecast should get by looking at the function `calculateStars()` in the files for every platform [here](./src/backend/platforms).
|
||||||
|
|
||||||
With regards the quality, I am most uncertain about Smarkets, Hypermind, Ladbrokes and WilliamHill, as I haven't used them as much. Also note that, whatever other redeeming features they might have, prediction markets rarely go above 95% or below 5%.
|
With regards the quality, I am most uncertain about Smarkets, Hypermind, Ladbrokes and WilliamHill, as I haven't used them as much. Also note that, whatever other redeeming features they might have, prediction markets rarely go above 95% or below 5%.
|
||||||
|
|
||||||
|
@ -78,5 +89,22 @@ Overall, the services which we use are:
|
||||||
|
|
||||||
## Various notes
|
## Various notes
|
||||||
|
|
||||||
|
- This repository is released under the [MIT license](https://opensource.org/licenses/MIT). See `LICENSE.md`
|
||||||
- Commits follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
|
- Commits follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
|
||||||
- For elicit and metaculus, this library currently filters out questions with <10 predictions.
|
- For elicit and metaculus, this library currently filters out questions with <10 predictions.
|
||||||
|
- The database is updated once a day, at 3:00 AM UTC, with the command `ts-node -T src/backend/flow/doEverythingForScheduler.ts`. The frontpage is updated after that, at 6:00 AM UTC with the command `ts-node -T src/backend/index.ts frontpage`. It's possible that either of these two operations makes the webpage briefly go down.
|
||||||
|
|
||||||
|
## To do
|
||||||
|
|
||||||
|
- [x] Update Metaculus and Manifold Markets fetchers
|
||||||
|
- [x] Add markets from [Insight Prediction](https://insightprediction.com/).
|
||||||
|
- [ ] Use <https://news.manifold.markets/p/above-the-fold-midterms-special> to update stars calculation for Manifold.
|
||||||
|
- [ ] Add a few more snippets, with fetching individual questions, questions with histories, questions added within the last 24h to the /contrib folder (good first issue)
|
||||||
|
- [ ] Refactor code so that users can capture and push the question history chart to imgur (good first issue)
|
||||||
|
- [ ] Upgrade to [React 18](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html). This will require dealing with the workaround we used for [this issue](https://github.com/vercel/next.js/issues/36019#issuecomment-1103266481)
|
||||||
|
- [ ] Add database of resolutions
|
||||||
|
- [ ] Allow users to embed predictions in the EA Forum/LessWrong (in progress)
|
||||||
|
- [ ] Find a long-term mantainer for this project
|
||||||
|
- [ ] Allow users to record their own predictions
|
||||||
|
- [ ] Release snapshots (I think @niplav is working on this)
|
||||||
|
- [ ] ...
|
||||||
|
|
69680
package-lock.json
generated
69680
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
130
package.json
130
package.json
|
@ -24,93 +24,101 @@
|
||||||
"build": "prisma generate && next build",
|
"build": "prisma generate && next build",
|
||||||
"next-start": "next start",
|
"next-start": "next start",
|
||||||
"next-export": "next export",
|
"next-export": "next export",
|
||||||
"dbshell": ". .env && psql $DIGITALOCEAN_POSTGRES"
|
"dbshell": ". .env && psql $DIGITALOCEAN_POSTGRES",
|
||||||
|
"upgrade-interactive": "yarn upgrade-interactive --latest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/react-dom": "^0.7.0",
|
"@floating-ui/react-dom": "^0.7.2",
|
||||||
"@graphql-yoga/node": "^2.1.0",
|
"@graphql-yoga/plugin-response-cache": "^1.1.0",
|
||||||
"@pothos/core": "^3.5.1",
|
"@pothos/core": "^3.22.8",
|
||||||
"@pothos/plugin-prisma": "^3.4.0",
|
"@pothos/plugin-prisma": "^3.35.6",
|
||||||
"@pothos/plugin-relay": "^3.10.0",
|
"@pothos/plugin-relay": "^3.28.6",
|
||||||
"@prisma/client": "^3.11.1",
|
"@prisma/client": "^3.15.2",
|
||||||
"@quri/squiggle-lang": "^0.2.8",
|
"@quri/squiggle-lang": "^0.5.1",
|
||||||
"@tailwindcss/forms": "^0.4.0",
|
"@tailwindcss/forms": "^0.4.1",
|
||||||
"@tailwindcss/typography": "^0.5.1",
|
"@tailwindcss/typography": "^0.5.7",
|
||||||
"@types/chroma-js": "^2.1.3",
|
"@types/chroma-js": "^2.1.4",
|
||||||
"@types/dom-to-image": "^2.6.4",
|
"@types/dom-to-image": "^2.6.4",
|
||||||
"@types/google-spreadsheet": "^3.2.1",
|
"@types/google-spreadsheet": "^3.3.0",
|
||||||
"@types/jsdom": "^16.2.14",
|
"@types/jsdom": "^16.2.15",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.0",
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "<18.0.0",
|
||||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||||
"@types/textversionjs": "^1.1.1",
|
"@types/textversionjs": "^1.1.1",
|
||||||
"@types/tunnel": "^0.0.3",
|
"@types/tunnel": "^0.0.3",
|
||||||
"airtable": "^0.11.1",
|
"airtable": "^0.11.5",
|
||||||
"algoliasearch": "^4.10.3",
|
"ajv": "^8.11.0",
|
||||||
"autoprefixer": "^10.1.0",
|
"algoliasearch": "^4.14.2",
|
||||||
"axios": "^0.25.0",
|
"autoprefixer": "10.4.5",
|
||||||
|
"axios": "^1.2.0",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"critters": "^0.0.16",
|
"critters": "^0.0.16",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.29.3",
|
||||||
"dom-to-image": "^2.6.0",
|
"dom-to-image": "^2.6.0",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.3",
|
||||||
"fetch": "^1.1.0",
|
"fetch": "^1.1.0",
|
||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.6.2",
|
||||||
"google-spreadsheet": "^3.1.15",
|
"google-spreadsheet": "^3.3.0",
|
||||||
"graphql": "^16.3.0",
|
"graphql": "^16.6.0",
|
||||||
"graphql-request": "^4.0.0",
|
"graphql-request": "^5.0.0",
|
||||||
"html-to-image": "^1.7.0",
|
"graphql-yoga": "^3.0.0-next.10",
|
||||||
|
"html-to-image": "^1.10.8",
|
||||||
"https": "^1.0.0",
|
"https": "^1.0.0",
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"json2csv": "^5.0.5",
|
"json2csv": "^5.0.7",
|
||||||
"multiselect-react-dropdown": "^2.0.17",
|
"multiselect-react-dropdown": "^2.0.25",
|
||||||
"next": "12",
|
"next": "^12.3.1",
|
||||||
"next-plausible": "^3.1.6",
|
"next-plausible": "^3.6.3",
|
||||||
"next-urql": "^3.3.2",
|
"next-urql": "^3.3.3",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"open": "^7.3.1",
|
"open": "^7.4.2",
|
||||||
"papaparse": "^5.3.0",
|
"papaparse": "^5.3.2",
|
||||||
"pg": "^8.7.3",
|
"pg": "^8.8.0",
|
||||||
"postcss": "^8.2.1",
|
"postcss": "^8.4.18",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-preset-env": "^7.3.2",
|
"postcss-preset-env": "^7.8.2",
|
||||||
"prisma": "^3.11.1",
|
"prisma": "^3.15.2",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
|
"re-resizable": "^6.9.9",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-component-export-image": "^1.0.6",
|
"react-component-export-image": "^1.0.6",
|
||||||
"react-compound-slider": "^3.3.1",
|
"react-compound-slider": "^3.4.0",
|
||||||
"react-copy-to-clipboard": "^5.0.3",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-dropdown": "^1.9.2",
|
"react-dropdown": "^1.11.0",
|
||||||
"react-hook-form": "^7.27.0",
|
"react-hook-form": "^7.38.0",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.6.0",
|
||||||
"react-is": "^18.0.0",
|
"react-is": "^18.2.0",
|
||||||
"react-markdown": "^8.0.0",
|
"react-markdown": "^8.0.3",
|
||||||
"react-safe": "^1.3.0",
|
"react-safe": "^1.3.0",
|
||||||
"react-select": "^5.2.2",
|
"react-select": "^5.5.4",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"tabletojson": "^2.0.4",
|
"tabletojson": "^2.0.7",
|
||||||
"tailwindcss": "^3.0.22",
|
"tailwindcss": "^3.2.0",
|
||||||
"textversionjs": "^1.1.3",
|
"textversionjs": "^1.1.3",
|
||||||
"ts-node": "^10.7.0",
|
"ts-node": "^10.9.1",
|
||||||
"tunnel": "^0.0.6",
|
"tunnel": "^0.0.6",
|
||||||
"urql": "^2.2.0",
|
"urql": "^2.2.3",
|
||||||
"urql-custom-scalars-exchange": "^0.1.5",
|
"urql-custom-scalars-exchange": "^0.1.6",
|
||||||
"victory": "^36.3.2"
|
"victory": "^36.6.8"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "<18.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^2.6.2",
|
"@graphql-codegen/cli": "^2.13.7",
|
||||||
"@graphql-codegen/introspection": "^2.1.1",
|
"@graphql-codegen/introspection": "^2.2.1",
|
||||||
"@graphql-codegen/near-operation-file-preset": "^2.2.9",
|
"@graphql-codegen/near-operation-file-preset": "^2.4.3",
|
||||||
"@graphql-codegen/schema-ast": "^2.4.1",
|
"@graphql-codegen/schema-ast": "^2.5.1",
|
||||||
"@graphql-codegen/typed-document-node": "^2.2.8",
|
"@graphql-codegen/typed-document-node": "^2.3.5",
|
||||||
"@graphql-codegen/typescript": "^2.4.8",
|
"@graphql-codegen/typescript": "^2.7.5",
|
||||||
"@graphql-codegen/typescript-operations": "^2.3.5",
|
"@graphql-codegen/typescript-operations": "^2.5.5",
|
||||||
"@netlify/plugin-nextjs": "^4.2.4",
|
"@svgr/cli": "^6.5.0",
|
||||||
"@svgr/cli": "^6.2.1",
|
|
||||||
"@types/pg": "^8.6.5",
|
"@types/pg": "^8.6.5",
|
||||||
"netlify-cli": "^9.13.6"
|
"eslint": "^8.25.0",
|
||||||
|
"eslint-config-next": "^12.3.1",
|
||||||
|
"typescript": "4.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
-- questions
|
||||||
|
ALTER TABLE "questions"
|
||||||
|
ADD COLUMN "fetched" TIMESTAMP(6),
|
||||||
|
ADD COLUMN "first_seen" TIMESTAMP(6);
|
||||||
|
|
||||||
|
UPDATE "questions"
|
||||||
|
SET "fetched" = "timestamp", "first_seen" = "timestamp";
|
||||||
|
|
||||||
|
ALTER TABLE "questions"
|
||||||
|
ALTER COLUMN "fetched" SET NOT NULL,
|
||||||
|
ALTER COLUMN "first_seen" SET NOT NULL;
|
||||||
|
|
||||||
|
-- history
|
||||||
|
ALTER TABLE "history"
|
||||||
|
ADD COLUMN "fetched" TIMESTAMP(6);
|
||||||
|
|
||||||
|
UPDATE "history" SET "fetched" = "timestamp";
|
||||||
|
|
||||||
|
ALTER TABLE "history"
|
||||||
|
ALTER COLUMN "fetched" SET NOT NULL;
|
||||||
|
|
||||||
|
-- populate first_seen
|
||||||
|
UPDATE questions
|
||||||
|
SET "first_seen" = h.fs
|
||||||
|
FROM (
|
||||||
|
SELECT id, MIN(fetched) AS fs FROM history GROUP BY id
|
||||||
|
) as h
|
||||||
|
WHERE questions.id = h.id;
|
14
prisma/migrations/20220520195517_indices/migration.sql
Normal file
14
prisma/migrations/20220520195517_indices/migration.sql
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "history_platform_idx" ON "history"("platform");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "history_fetched_idx" ON "history"("fetched");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "questions_platform_idx" ON "questions"("platform");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "questions_fetched_idx" ON "questions"("fetched");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "questions_first_seen_idx" ON "questions"("first_seen");
|
|
@ -0,0 +1,12 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `timestamp` on the `history` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `timestamp` on the `questions` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "history" DROP COLUMN "timestamp";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "questions" DROP COLUMN "timestamp";
|
|
@ -0,0 +1,5 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "FrontpageId" DROP CONSTRAINT "FrontpageId_id_fkey";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FrontpageId" ADD CONSTRAINT "FrontpageId_id_fkey" FOREIGN KEY ("id") REFERENCES "questions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -24,24 +24,6 @@ model Dashboard {
|
||||||
@@map("dashboards")
|
@@map("dashboards")
|
||||||
}
|
}
|
||||||
|
|
||||||
model History {
|
|
||||||
id String
|
|
||||||
idref String?
|
|
||||||
question Question? @relation(fields: [idref], references: [id], onDelete: SetNull, onUpdate: Restrict)
|
|
||||||
title String
|
|
||||||
url String
|
|
||||||
platform String
|
|
||||||
description String
|
|
||||||
options Json
|
|
||||||
timestamp DateTime @db.Timestamp(6)
|
|
||||||
qualityindicators Json
|
|
||||||
extra Json
|
|
||||||
pk Int @id @default(autoincrement())
|
|
||||||
|
|
||||||
@@index([id])
|
|
||||||
@@map("history")
|
|
||||||
}
|
|
||||||
|
|
||||||
model Question {
|
model Question {
|
||||||
/// E.g. "fantasyscotus-580"
|
/// E.g. "fantasyscotus-580"
|
||||||
id String @id
|
id String @id
|
||||||
|
@ -68,7 +50,8 @@ model Question {
|
||||||
// }
|
// }
|
||||||
// ]
|
// ]
|
||||||
options Json
|
options Json
|
||||||
timestamp DateTime @db.Timestamp(6)
|
fetched DateTime @db.Timestamp(6)
|
||||||
|
firstSeen DateTime @map("first_seen") @db.Timestamp(6)
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// "numforecasts": 120,
|
// "numforecasts": 120,
|
||||||
|
@ -80,10 +63,33 @@ model Question {
|
||||||
onFrontpage FrontpageId?
|
onFrontpage FrontpageId?
|
||||||
history History[]
|
history History[]
|
||||||
|
|
||||||
|
@@index([platform])
|
||||||
|
@@index([fetched])
|
||||||
|
@@index([firstSeen])
|
||||||
@@map("questions")
|
@@map("questions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model History {
|
||||||
|
id String
|
||||||
|
idref String?
|
||||||
|
question Question? @relation(fields: [idref], references: [id], onDelete: SetNull, onUpdate: Restrict)
|
||||||
|
title String
|
||||||
|
url String
|
||||||
|
platform String
|
||||||
|
description String
|
||||||
|
options Json
|
||||||
|
fetched DateTime @db.Timestamp(6)
|
||||||
|
qualityindicators Json
|
||||||
|
extra Json
|
||||||
|
pk Int @id @default(autoincrement())
|
||||||
|
|
||||||
|
@@index([id])
|
||||||
|
@@index([platform])
|
||||||
|
@@index([fetched])
|
||||||
|
@@map("history")
|
||||||
|
}
|
||||||
|
|
||||||
model FrontpageId {
|
model FrontpageId {
|
||||||
question Question @relation(fields: [id], references: [id])
|
question Question @relation(fields: [id], references: [id], onDelete: Cascade)
|
||||||
id String @unique
|
id String @unique
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,14 @@ scalar Date
|
||||||
type History implements QuestionShape {
|
type History implements QuestionShape {
|
||||||
description: String!
|
description: String!
|
||||||
|
|
||||||
|
"""Last timestamp at which metaforecast fetched the question"""
|
||||||
|
fetched: Date!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Last timestamp at which metaforecast fetched the question, in ISO 8601 format
|
||||||
|
"""
|
||||||
|
fetchedStr: String!
|
||||||
|
|
||||||
"""History items are identified by their integer ids"""
|
"""History items are identified by their integer ids"""
|
||||||
id: ID!
|
id: ID!
|
||||||
options: [ProbabilityOption!]!
|
options: [ProbabilityOption!]!
|
||||||
|
@ -50,8 +58,8 @@ type History implements QuestionShape {
|
||||||
"""Unique string which identifies the question"""
|
"""Unique string which identifies the question"""
|
||||||
questionId: ID!
|
questionId: ID!
|
||||||
|
|
||||||
"""Timestamp at which metaforecast fetched the question"""
|
"""Last timestamp at which metaforecast fetched the question"""
|
||||||
timestamp: Date!
|
timestamp: Date! @deprecated(reason: "Renamed to `fetched`")
|
||||||
title: String!
|
title: String!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -112,15 +120,15 @@ type QualityIndicators {
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
"""Look up a single dashboard by its id"""
|
"""Look up a single dashboard by its id"""
|
||||||
dashboard(id: ID!): Dashboard!
|
dashboard(id: ID!): Dashboard
|
||||||
|
|
||||||
"""Get a list of questions that are currently on the frontpage"""
|
"""Get a list of questions that are currently on the frontpage"""
|
||||||
frontpage: [Question!]!
|
frontpage: [Question!]!
|
||||||
platforms: [Platform!]!
|
platforms: [Platform!]!
|
||||||
|
|
||||||
"""Look up a single question by its id"""
|
"""Look up a single question by its id"""
|
||||||
question(id: ID!): Question!
|
question(id: ID!): Question
|
||||||
questions(after: String, before: String, first: Int, last: Int): QueryQuestionsConnection!
|
questions(after: String, before: String, first: Int, last: Int, orderBy: QuestionsOrderBy): QueryQuestionsConnection!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Search for questions; uses Algolia instead of the primary metaforecast database
|
Search for questions; uses Algolia instead of the primary metaforecast database
|
||||||
|
@ -140,6 +148,22 @@ type QueryQuestionsConnectionEdge {
|
||||||
|
|
||||||
type Question implements QuestionShape {
|
type Question implements QuestionShape {
|
||||||
description: String!
|
description: String!
|
||||||
|
|
||||||
|
"""Last timestamp at which metaforecast fetched the question"""
|
||||||
|
fetched: Date!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Last timestamp at which metaforecast fetched the question, in ISO 8601 format
|
||||||
|
"""
|
||||||
|
fetchedStr: String!
|
||||||
|
|
||||||
|
"""First timestamp at which metaforecast fetched the question"""
|
||||||
|
firstSeen: Date!
|
||||||
|
|
||||||
|
"""
|
||||||
|
First timestamp at which metaforecast fetched the question, in ISO 8601 format
|
||||||
|
"""
|
||||||
|
firstSeenStr: String!
|
||||||
history: [History!]!
|
history: [History!]!
|
||||||
|
|
||||||
"""Unique string which identifies the question"""
|
"""Unique string which identifies the question"""
|
||||||
|
@ -148,8 +172,8 @@ type Question implements QuestionShape {
|
||||||
platform: Platform!
|
platform: Platform!
|
||||||
qualityIndicators: QualityIndicators!
|
qualityIndicators: QualityIndicators!
|
||||||
|
|
||||||
"""Timestamp at which metaforecast fetched the question"""
|
"""Last timestamp at which metaforecast fetched the question"""
|
||||||
timestamp: Date!
|
timestamp: Date! @deprecated(reason: "Renamed to `fetched`")
|
||||||
title: String!
|
title: String!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -161,12 +185,20 @@ type Question implements QuestionShape {
|
||||||
|
|
||||||
interface QuestionShape {
|
interface QuestionShape {
|
||||||
description: String!
|
description: String!
|
||||||
|
|
||||||
|
"""Last timestamp at which metaforecast fetched the question"""
|
||||||
|
fetched: Date!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Last timestamp at which metaforecast fetched the question, in ISO 8601 format
|
||||||
|
"""
|
||||||
|
fetchedStr: String!
|
||||||
options: [ProbabilityOption!]!
|
options: [ProbabilityOption!]!
|
||||||
platform: Platform!
|
platform: Platform!
|
||||||
qualityIndicators: QualityIndicators!
|
qualityIndicators: QualityIndicators!
|
||||||
|
|
||||||
"""Timestamp at which metaforecast fetched the question"""
|
"""Last timestamp at which metaforecast fetched the question"""
|
||||||
timestamp: Date!
|
timestamp: Date! @deprecated(reason: "Renamed to `fetched`")
|
||||||
title: String!
|
title: String!
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -175,6 +207,10 @@ interface QuestionShape {
|
||||||
url: String!
|
url: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum QuestionsOrderBy {
|
||||||
|
FIRST_SEEN_DESC
|
||||||
|
}
|
||||||
|
|
||||||
input SearchInput {
|
input SearchInput {
|
||||||
"""List of platform ids to filter by"""
|
"""List of platform ids to filter by"""
|
||||||
forecastingPlatforms: [String!]
|
forecastingPlatforms: [String!]
|
||||||
|
|
|
@ -22,7 +22,8 @@ let buildQuery = (endCursor) => `{
|
||||||
numForecasts
|
numForecasts
|
||||||
stars
|
stars
|
||||||
}
|
}
|
||||||
timestamp
|
firstSeenStr
|
||||||
|
fetchedStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pageInfo {
|
pageInfo {
|
91
scripts/download-first-seen-today.mjs
Normal file
91
scripts/download-first-seen-today.mjs
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/* Imports */
|
||||||
|
import fs from "fs";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
/* Definitions */
|
||||||
|
const VERBOSE = true;
|
||||||
|
let ISO_DATE_TODAY = new Date().toISOString().slice(0, 10);
|
||||||
|
let print = (message) => (VERBOSE ? console.log(message) : null);
|
||||||
|
let graphQLendpoint = "https://metaforecast.org/api/graphql";
|
||||||
|
let buildQuery = (endCursor) => `{
|
||||||
|
questions(first: 1000 ${
|
||||||
|
!!endCursor ? `after: "${endCursor}"` : ""
|
||||||
|
} orderBy: FIRST_SEEN_DESC) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
url
|
||||||
|
description
|
||||||
|
options {
|
||||||
|
name
|
||||||
|
probability
|
||||||
|
}
|
||||||
|
qualityIndicators {
|
||||||
|
numForecasts
|
||||||
|
stars
|
||||||
|
}
|
||||||
|
firstSeenStr
|
||||||
|
fetchedStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pageInfo {
|
||||||
|
endCursor
|
||||||
|
startCursor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/* Support functions */
|
||||||
|
let getSomeMetaforecastPredictions = async (query) => {
|
||||||
|
let response = await axios({
|
||||||
|
url: graphQLendpoint,
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
data: JSON.stringify({ query: query }),
|
||||||
|
})
|
||||||
|
.then((res) => res.data)
|
||||||
|
.then((res) => res.data); // not a typo
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
let save = (questions) => {
|
||||||
|
print("Saving Results");
|
||||||
|
fs.writeFileSync("forecasts-today.json", JSON.stringify(questions, null, 4));
|
||||||
|
};
|
||||||
|
|
||||||
|
let getNodes = (questions) => {
|
||||||
|
let edges = questions.edges;
|
||||||
|
let nodes = edges.map((edge) => edge.node);
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
// main
|
||||||
|
let getAllMetaforecastPredictions = async () => {
|
||||||
|
print("Fetching forecasts");
|
||||||
|
let results = [];
|
||||||
|
let firstQuery = await getSomeMetaforecastPredictions(buildQuery());
|
||||||
|
let nodes = getNodes(firstQuery.questions);
|
||||||
|
let nodesToday = nodes.filter(
|
||||||
|
(node) => node.firstSeenStr.slice(0, 10) == ISO_DATE_TODAY
|
||||||
|
);
|
||||||
|
results.push(...nodesToday);
|
||||||
|
let endCursor = firstQuery.questions.pageInfo.endCursor;
|
||||||
|
while (endCursor && nodesToday.length > 0) {
|
||||||
|
print("Cursor: " + endCursor);
|
||||||
|
let queryResults = await getSomeMetaforecastPredictions(
|
||||||
|
buildQuery(endCursor)
|
||||||
|
);
|
||||||
|
let nodes = getNodes(queryResults.questions);
|
||||||
|
nodesToday = nodes.filter(
|
||||||
|
(node) => node.firstSeenStr.slice(0, 10) == ISO_DATE_TODAY
|
||||||
|
);
|
||||||
|
results.push(...nodesToday);
|
||||||
|
endCursor = queryResults.questions.pageInfo.endCursor;
|
||||||
|
}
|
||||||
|
//results = results.map((result) => result.node);
|
||||||
|
save(results);
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
getAllMetaforecastPredictions();
|
8
src/Global.d.ts
vendored
Normal file
8
src/Global.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// Workaround related to: https://github.com/vercel/next.js/issues/29788
|
||||||
|
// https://github.com/vercel/next.js/issues/29788#issuecomment-1000595524
|
||||||
|
declare type StaticImageData = {
|
||||||
|
src: string;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
placeholder?: string;
|
||||||
|
};
|
|
@ -1,9 +1,12 @@
|
||||||
import { platforms } from "../platforms/registry";
|
import { getPlatforms } from "../platforms/registry";
|
||||||
import { executeJobByName } from "./jobs";
|
import { executeJobByName } from "./jobs";
|
||||||
|
|
||||||
/* Do everything */
|
/* Do everything */
|
||||||
export async function doEverything() {
|
export async function doEverything() {
|
||||||
let jobNames = [...platforms.map((platform) => platform.name), "algolia"];
|
let jobNames = [
|
||||||
|
...getPlatforms().map((platform) => platform.name),
|
||||||
|
"algolia",
|
||||||
|
];
|
||||||
|
|
||||||
console.log("");
|
console.log("");
|
||||||
console.log("");
|
console.log("");
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { doEverything } from "../flow/doEverything";
|
import { doEverything } from "../flow/doEverything";
|
||||||
import { rebuildFrontpage } from "../frontpage";
|
import { rebuildFrontpage } from "../frontpage";
|
||||||
import { processPlatform } from "../platforms";
|
import { processPlatform } from "../platforms";
|
||||||
import { platforms } from "../platforms/registry";
|
import { getPlatforms } from "../platforms/registry";
|
||||||
import { rebuildAlgoliaDatabase } from "../utils/algolia";
|
import { rebuildAlgoliaDatabase } from "../utils/algolia";
|
||||||
import { sleep } from "../utils/sleep";
|
import { sleep } from "../utils/sleep";
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ interface Job<ArgNames extends string = ""> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jobs: Job<string>[] = [
|
export const jobs: Job<string>[] = [
|
||||||
...platforms.map((platform) => ({
|
...getPlatforms().map((platform) => ({
|
||||||
name: platform.name,
|
name: platform.name,
|
||||||
message: `Download predictions from ${platform.name}`,
|
message: `Download predictions from ${platform.name}`,
|
||||||
...(platform.version === "v2" ? { args: platform.fetcherArgs } : {}),
|
...(platform.version === "v2" ? { args: platform.fetcherArgs } : {}),
|
||||||
|
|
|
@ -26,7 +26,7 @@ export async function rebuildFrontpage() {
|
||||||
AND questions.description != ''
|
AND questions.description != ''
|
||||||
AND JSONB_ARRAY_LENGTH(questions.options) > 0
|
AND JSONB_ARRAY_LENGTH(questions.options) > 0
|
||||||
GROUP BY questions.id
|
GROUP BY questions.id
|
||||||
HAVING COUNT(DISTINCT history.timestamp) >= 7
|
HAVING COUNT(DISTINCT history.fetched) >= 7
|
||||||
ORDER BY RANDOM() LIMIT 50
|
ORDER BY RANDOM() LIMIT 50
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd /home/loki/Documents/core/software/fresh/js/metaforecast/metaforecast-monorepo
|
cd /home/loki/Documents/core/software/fresh/js/metaforecast/metaforecast-monorepo
|
||||||
/home/loki/.nvm/versions/node/v17.5.0/bin/npm run cli 3 > ../last-superforecast-fetch.txt
|
|
||||||
|
/home/loki/.nvm/versions/node/v16.15.0/lib/node_modules/npm/bin/npm-cli.js run cli goodjudgment > ../last-superforecast-fetch.txt
|
||||||
|
|
|
@ -79,7 +79,7 @@ export const givewellopenphil: Platform = {
|
||||||
const dataWithDate = data.map((datum: any) => ({
|
const dataWithDate = data.map((datum: any) => ({
|
||||||
...datum,
|
...datum,
|
||||||
platform: platformName,
|
platform: platformName,
|
||||||
timestamp: new Date("2021-02-23"),
|
// timestamp: new Date("2021-02-23"),
|
||||||
}));
|
}));
|
||||||
return dataWithDate;
|
return dataWithDate;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
/* Imports */
|
/* Imports */
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Tabletojson } from "tabletojson";
|
import {Tabletojson} from "tabletojson";
|
||||||
|
|
||||||
import { average } from "../../utils";
|
import {average} from "../../utils";
|
||||||
import { hash } from "../utils/hash";
|
import {hash} from "../utils/hash";
|
||||||
import { FetchedQuestion, Platform } from "./";
|
import {FetchedQuestion, Platform} from "./";
|
||||||
|
import {FullQuestionOption} from "../../common/types";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "goodjudgment";
|
const platformName = "goodjudgment";
|
||||||
|
@ -30,32 +31,30 @@ export const goodjudgment: Platform = {
|
||||||
// hard-coded backup proxy
|
// hard-coded backup proxy
|
||||||
*/
|
*/
|
||||||
// proxy = {
|
// proxy = {
|
||||||
// ip: process.env.BACKUP_PROXY_IP,
|
// ip: process.env.BACKUP_PROXY_IP,
|
||||||
// port: process.env.BACKUP_PROXY_PORT,
|
// port: process.env.BACKUP_PROXY_PORT,
|
||||||
// };
|
// };
|
||||||
// // }
|
// // }
|
||||||
// let agent = tunnel.httpsOverHttp({
|
// let agent = tunnel.httpsOverHttp({
|
||||||
// proxy: {
|
// proxy: {
|
||||||
// host: proxy.ip,
|
// host: proxy.ip,
|
||||||
// port: proxy.port,
|
// port: proxy.port,
|
||||||
// },
|
// },
|
||||||
// });
|
// });
|
||||||
|
|
||||||
const content = await axios
|
const content = await axios.request({
|
||||||
.request({
|
url: "https://goodjudgment.io/superforecasts/",
|
||||||
url: "https://goodjudgment.io/superforecasts/",
|
method: "get",
|
||||||
method: "get",
|
headers: {
|
||||||
headers: {
|
"User-Agent": "Chrome"
|
||||||
"User-Agent": "Chrome",
|
},
|
||||||
},
|
// agent,
|
||||||
// agent,
|
// port: 80,
|
||||||
// port: 80,
|
}).then((query) => query.data);
|
||||||
})
|
|
||||||
.then((query) => query.data);
|
|
||||||
|
|
||||||
// Processing
|
// Processing
|
||||||
let results: FetchedQuestion[] = [];
|
let results: FetchedQuestion[] = [];
|
||||||
let jsonTable = Tabletojson.convert(content, { stripHtmlFromCells: false });
|
let jsonTable = Tabletojson.convert(content, {stripHtmlFromCells: false});
|
||||||
jsonTable.shift(); // deletes first element
|
jsonTable.shift(); // deletes first element
|
||||||
jsonTable.pop(); // deletes last element
|
jsonTable.pop(); // deletes last element
|
||||||
|
|
||||||
|
@ -63,38 +62,21 @@ export const goodjudgment: Platform = {
|
||||||
let title = table[0]["0"].split("\t\t\t").splice(3)[0];
|
let title = table[0]["0"].split("\t\t\t").splice(3)[0];
|
||||||
if (title != undefined) {
|
if (title != undefined) {
|
||||||
title = title.replaceAll("</a>", "");
|
title = title.replaceAll("</a>", "");
|
||||||
const id = `${platformName}-${hash(title)}`;
|
const id = `${platformName}-${
|
||||||
const description = table
|
hash(title)
|
||||||
.filter((row: any) => row["0"].includes("BACKGROUND:"))
|
}`;
|
||||||
.map((row: any) => row["0"])
|
const description = table.filter((row : any) => row["0"].includes("BACKGROUND:")).map((row : any) => row["0"]).map((text : any) => text.split("BACKGROUND:")[1].split("Examples of Superforecaster")[0].split("AT A GLANCE")[0].replaceAll("\n\n", "\n").split("\n").slice(3).join(" ").replaceAll(" ", "").replaceAll("<br> ", ""))[0];
|
||||||
.map((text: any) =>
|
const options = table.filter((row : any) => "4" in row).map((row : any) => ({
|
||||||
text
|
name: row["2"].split('<span class="qTitle">')[1].replace("</span>", ""),
|
||||||
.split("BACKGROUND:")[1]
|
probability: Number(row["3"].split("%")[0]) / 100,
|
||||||
.split("Examples of Superforecaster")[0]
|
type: "PROBABILITY"
|
||||||
.split("AT A GLANCE")[0]
|
}));
|
||||||
.replaceAll("\n\n", "\n")
|
let analysis = table.filter((row : any) => row[0] ? row[0].toLowerCase().includes("commentary") : false);
|
||||||
.split("\n")
|
|
||||||
.slice(3)
|
|
||||||
.join(" ")
|
|
||||||
.replaceAll(" ", "")
|
|
||||||
.replaceAll("<br> ", "")
|
|
||||||
)[0];
|
|
||||||
const options = table
|
|
||||||
.filter((row: any) => "4" in row)
|
|
||||||
.map((row: any) => ({
|
|
||||||
name: row["2"]
|
|
||||||
.split('<span class="qTitle">')[1]
|
|
||||||
.replace("</span>", ""),
|
|
||||||
probability: Number(row["3"].split("%")[0]) / 100,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
}));
|
|
||||||
let analysis = table.filter((row: any) =>
|
|
||||||
row[0] ? row[0].toLowerCase().includes("commentary") : false
|
|
||||||
);
|
|
||||||
// "Examples of Superforecaster Commentary" / Analysis
|
// "Examples of Superforecaster Commentary" / Analysis
|
||||||
// The following is necessary twice, because we want to check if there is an empty list, and then get the first element of the first element of the list.
|
// The following is necessary twice, because we want to check if there is an empty list, and then get the first element of the first element of the list.
|
||||||
analysis = analysis ? analysis[0] : "";
|
analysis = analysis ? analysis[0] : "";
|
||||||
analysis = analysis ? analysis[0] : ""; // not a duplicate
|
analysis = analysis ? analysis[0] : "";
|
||||||
|
// not a duplicate
|
||||||
// console.log(analysis)
|
// console.log(analysis)
|
||||||
let standardObj: FetchedQuestion = {
|
let standardObj: FetchedQuestion = {
|
||||||
id,
|
id,
|
||||||
|
@ -104,16 +86,14 @@ export const goodjudgment: Platform = {
|
||||||
options,
|
options,
|
||||||
qualityindicators: {},
|
qualityindicators: {},
|
||||||
extra: {
|
extra: {
|
||||||
superforecastercommentary: analysis || "",
|
superforecastercommentary: analysis || ""
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
results.push(standardObj);
|
results.push(standardObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log("Failing is not unexpected; see utils/pullSuperforecastsManually.sh/js");
|
||||||
"Failing is not unexpected; see utils/pullSuperforecastsManually.sh/js"
|
|
||||||
);
|
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
|
@ -121,8 +101,8 @@ export const goodjudgment: Platform = {
|
||||||
let nuno = () => 4;
|
let nuno = () => 4;
|
||||||
let eli = () => 4;
|
let eli = () => 4;
|
||||||
let misha = () => 3.5;
|
let misha = () => 3.5;
|
||||||
let starsDecimal = average([nuno()]); //, eli(), misha()])
|
let starsDecimal = average([nuno()]); // , eli(), misha()])
|
||||||
let starsInteger = Math.round(starsDecimal);
|
let starsInteger = Math.round(starsDecimal);
|
||||||
return starsInteger;
|
return starsInteger;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
/* Imports */
|
/* Imports */
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Tabletojson } from "tabletojson";
|
import {Tabletojson} from "tabletojson";
|
||||||
|
|
||||||
import { average } from "../../utils";
|
import {average} from "../../utils";
|
||||||
import { applyIfSecretExists } from "../utils/getSecrets";
|
import {applyIfSecretExists} from "../utils/getSecrets";
|
||||||
import { sleep } from "../utils/sleep";
|
import {sleep} from "../utils/sleep";
|
||||||
import toMarkdown from "../utils/toMarkdown";
|
import toMarkdown from "../utils/toMarkdown";
|
||||||
import { FetchedQuestion, Platform } from "./";
|
import {FetchedQuestion, Platform} from "./";
|
||||||
|
import {FullQuestionOption} from "../../common/types";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "goodjudgmentopen";
|
const platformName = "goodjudgmentopen";
|
||||||
|
@ -17,125 +18,98 @@ const annoyingPromptUrls = [
|
||||||
"https://www.gjopen.com/questions/1779-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters",
|
"https://www.gjopen.com/questions/1779-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters",
|
||||||
"https://www.gjopen.com/questions/2246-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters-2022-thread",
|
"https://www.gjopen.com/questions/2246-are-there-any-forecasting-tips-tricks-and-experiences-you-would-like-to-share-and-or-discuss-with-your-fellow-forecasters-2022-thread",
|
||||||
"https://www.gjopen.com/questions/2237-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen",
|
"https://www.gjopen.com/questions/2237-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen",
|
||||||
|
"https://www.gjopen.com/questions/2437-what-forecasting-questions-should-we-ask-what-questions-would-you-like-to-forecast-on-gjopen"
|
||||||
];
|
];
|
||||||
const DEBUG_MODE: "on" | "off" = "off"; // "on"
|
const DEBUG_MODE: "on" | "off" = "off"; // "on"
|
||||||
const id = () => 0;
|
const id = () => 0;
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
|
|
||||||
async function fetchPage(page: number, cookie: string) {
|
function cleanDescription(text : string) {
|
||||||
|
let md = toMarkdown(text);
|
||||||
|
let result = md.replaceAll("---", "-").replaceAll(" ", " ");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPage(page : number, cookie : string) {
|
||||||
const response: string = await axios({
|
const response: string = await axios({
|
||||||
url: htmlEndPoint + page,
|
url: htmlEndPoint + page,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: cookie,
|
Cookie: cookie
|
||||||
},
|
}
|
||||||
}).then((res) => res.data);
|
}).then((res) => res.data);
|
||||||
//console.log(response)
|
// console.log(response)
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchStats(questionUrl: string, cookie: string) {
|
async function fetchStats(questionUrl : string, cookie : string) {
|
||||||
let response: string = await axios({
|
let response: string = await axios({
|
||||||
url: questionUrl + "/stats",
|
url: questionUrl + "/stats",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
|
"Content-Type": "text/html",
|
||||||
Cookie: cookie,
|
Cookie: cookie,
|
||||||
Referer: questionUrl,
|
Referer: questionUrl
|
||||||
},
|
}
|
||||||
}).then((res) => res.data);
|
}).then((res) => res.data);
|
||||||
//console.log(response)
|
|
||||||
|
|
||||||
// Is binary?
|
if (response.includes("Sign up or sign in to forecast")) {
|
||||||
let isbinary = response.includes("binary?":true");
|
throw Error("Not logged in");
|
||||||
|
|
||||||
let options: FetchedQuestion["options"] = [];
|
|
||||||
if (isbinary) {
|
|
||||||
// Crowd percentage
|
|
||||||
let htmlElements = response.split("\n");
|
|
||||||
let h3Element = htmlElements.filter((str) => str.includes("<h3>"))[0];
|
|
||||||
// console.log(h3Element)
|
|
||||||
let crowdpercentage = h3Element.split(">")[1].split("<")[0];
|
|
||||||
let probability = Number(crowdpercentage.replace("%", "")) / 100;
|
|
||||||
options.push(
|
|
||||||
{
|
|
||||||
name: "Yes",
|
|
||||||
probability: probability,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No",
|
|
||||||
probability: +(1 - probability).toFixed(2), // avoids floating point shenanigans
|
|
||||||
type: "PROBABILITY",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let optionsHtmlElement = "<table" + response.split("tbody")[1] + "table>";
|
|
||||||
let tablesAsJson = Tabletojson.convert(optionsHtmlElement);
|
|
||||||
let firstTable = tablesAsJson[0];
|
|
||||||
options = firstTable.map((element: any) => ({
|
|
||||||
name: element["0"],
|
|
||||||
probability: Number(element["1"].replace("%", "")) / 100,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
}));
|
|
||||||
//console.log(optionsHtmlElement)
|
|
||||||
//console.log(options)
|
|
||||||
}
|
}
|
||||||
|
// Init
|
||||||
|
let options: FullQuestionOption[] = [];
|
||||||
|
|
||||||
// Description
|
// Parse the embedded json
|
||||||
let descriptionraw = response.split(
|
let htmlElements = response.split("\n");
|
||||||
`<div id="question-background" class="collapse smb">`
|
let jsonLines = htmlElements.filter((element) => element.includes("data-react-props"));
|
||||||
)[1];
|
let embeddedJsons = jsonLines.map((jsonLine, i) => {
|
||||||
let descriptionprocessed1 = descriptionraw.split(`</div>`)[0];
|
let innerJSONasHTML = jsonLine.split('data-react-props="')[1].split('"')[0];
|
||||||
let descriptionprocessed2 = toMarkdown(descriptionprocessed1);
|
let json = JSON.parse(innerJSONasHTML.replaceAll(""", '"'));
|
||||||
let descriptionprocessed3 = descriptionprocessed2
|
return json;
|
||||||
.split("\n")
|
});
|
||||||
.filter((string) => !string.includes("Confused? Check our"))
|
let firstEmbeddedJson = embeddedJsons[0];
|
||||||
.join("\n");
|
let title = firstEmbeddedJson.question.name;
|
||||||
let description = descriptionprocessed3;
|
let description = cleanDescription(firstEmbeddedJson.question.description);
|
||||||
|
let comments_count = firstEmbeddedJson.question.comments_count;
|
||||||
// Number of forecasts
|
let numforecasters = firstEmbeddedJson.question.predictors_count;
|
||||||
let numforecasts = response
|
let numforecasts = firstEmbeddedJson.question.prediction_sets_count;
|
||||||
.split("prediction_sets_count":")[1]
|
let questionType = firstEmbeddedJson.question.type;
|
||||||
.split(",")[0];
|
if (questionType.includes("Binary") || questionType.includes("NonExclusiveOpinionPoolQuestion") || questionType.includes("Forecast::Question") || ! questionType.includes("Forecast::MultiTimePeriodQuestion")) {
|
||||||
//console.log(numforecasts)
|
options = firstEmbeddedJson.question.answers.map((answer : any) => ({name: answer.name, probability: answer.normalized_probability, type: "PROBABILITY"}));
|
||||||
|
if (options.length == 1 && options[0].name == "Yes") {
|
||||||
// Number of predictors
|
let probabilityNo = options[0].probability > 1 ? 1 - options[0].probability / 100 : 1 - options[0].probability;
|
||||||
let numforecasters = response
|
options.push({name: "No", probability: probabilityNo, type: "PROBABILITY"});
|
||||||
.split("predictors_count":")[1]
|
}
|
||||||
.split(",")[0];
|
}
|
||||||
//console.log(numpredictors)
|
|
||||||
|
|
||||||
let result = {
|
let result = {
|
||||||
description,
|
description: description,
|
||||||
options,
|
options: options,
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: Number(numforecasts),
|
numforecasts: Number(numforecasts),
|
||||||
numforecasters: Number(numforecasters),
|
numforecasters: Number(numforecasters),
|
||||||
},
|
comments_count: Number(comments_count)
|
||||||
// this mismatches the code below, and needs to be fixed, but I'm doing typescript conversion and don't want to touch any logic for now
|
}
|
||||||
} as any;
|
};
|
||||||
|
// console.log(JSON.stringify(result, null, 4));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSignedIn(html: string) {
|
function isSignedIn(html : string) {
|
||||||
let isSignedInBool = !(
|
let isSignedInBool = !(html.includes("You need to sign in or sign up before continuing") || html.includes("Sign up"));
|
||||||
html.includes("You need to sign in or sign up before continuing") ||
|
|
||||||
html.includes("Sign up")
|
|
||||||
);
|
|
||||||
// console.log(html)
|
// console.log(html)
|
||||||
if (!isSignedInBool) {
|
if (! isSignedInBool) {
|
||||||
console.log("Error: Not signed in.");
|
console.log("Error: Not signed in.");
|
||||||
}
|
}
|
||||||
console.log(`is signed in? ${isSignedInBool ? "yes" : "no"}`);
|
console.log(`is signed in? ${
|
||||||
|
isSignedInBool ? "yes" : "no"
|
||||||
|
}`);
|
||||||
return isSignedInBool;
|
return isSignedInBool;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reachedEnd(html: string) {
|
function reachedEnd(html : string) {
|
||||||
let reachedEndBool = html.includes("No questions match your filter");
|
let reachedEndBool = html.includes("No questions match your filter");
|
||||||
if (reachedEndBool) {
|
if (reachedEndBool) { // console.log(html)
|
||||||
//console.log(html)
|
|
||||||
}
|
}
|
||||||
console.log(`Reached end? ${reachedEndBool}`);
|
console.log(`Reached end? ${reachedEndBool}`);
|
||||||
return reachedEndBool;
|
return reachedEndBool;
|
||||||
|
@ -143,14 +117,15 @@ function reachedEnd(html: string) {
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
|
|
||||||
async function goodjudgmentopen_inner(cookie: string) {
|
async function goodjudgmentopen_inner(cookie : string) {
|
||||||
let i = 1;
|
let i = 1;
|
||||||
let response = await fetchPage(i, cookie);
|
let response = await fetchPage(i, cookie);
|
||||||
|
|
||||||
let results = [];
|
let results = [];
|
||||||
let init = Date.now();
|
let init = Date.now();
|
||||||
// console.log("Downloading... This might take a couple of minutes. Results will be shown.")
|
// console.log("Downloading... This might take a couple of minutes. Results will be shown.")
|
||||||
while (!reachedEnd(response) && isSignedIn(response)) {
|
console.log("Page #1")
|
||||||
|
while (! reachedEnd(response) && isSignedIn(response)) {
|
||||||
let htmlLines = response.split("\n");
|
let htmlLines = response.split("\n");
|
||||||
DEBUG_MODE == "on" ? htmlLines.forEach((line) => console.log(line)) : id();
|
DEBUG_MODE == "on" ? htmlLines.forEach((line) => console.log(line)) : id();
|
||||||
let h5elements = htmlLines.filter((str) => str.includes("<h5> <a href="));
|
let h5elements = htmlLines.filter((str) => str.includes("<h5> <a href="));
|
||||||
|
@ -159,20 +134,19 @@ async function goodjudgmentopen_inner(cookie: string) {
|
||||||
for (let h5element of h5elements) {
|
for (let h5element of h5elements) {
|
||||||
let h5elementSplit = h5element.split('"><span>');
|
let h5elementSplit = h5element.split('"><span>');
|
||||||
let url = h5elementSplit[0].split('<a href="')[1];
|
let url = h5elementSplit[0].split('<a href="')[1];
|
||||||
if (!annoyingPromptUrls.includes(url)) {
|
if (! annoyingPromptUrls.includes(url)) {
|
||||||
let title = h5elementSplit[1].replace("</span></a></h5>", "");
|
let title = h5elementSplit[1].replace("</span></a></h5>", "");
|
||||||
await sleep(1000 + Math.random() * 1000); // don't be as noticeable
|
await sleep(1000 + Math.random() * 1000); // don't be as noticeable
|
||||||
try {
|
try {
|
||||||
let moreinfo = await fetchStats(url, cookie);
|
let moreinfo = await fetchStats(url, cookie);
|
||||||
if (moreinfo.isbinary) {
|
/*if (moreinfo.isbinary) {
|
||||||
if (!moreinfo.crowdpercentage) {
|
if (! moreinfo.crowdpercentage) { // then request again.
|
||||||
// then request again.
|
|
||||||
moreinfo = await fetchStats(url, cookie);
|
moreinfo = await fetchStats(url, cookie);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
let questionNumRegex = new RegExp("questions/([0-9]+)");
|
let questionNumRegex = new RegExp("questions/([0-9]+)");
|
||||||
const questionNumMatch = url.match(questionNumRegex);
|
const questionNumMatch = url.match(questionNumRegex);
|
||||||
if (!questionNumMatch) {
|
if (! questionNumMatch) {
|
||||||
throw new Error(`Couldn't find question num in ${url}`);
|
throw new Error(`Couldn't find question num in ${url}`);
|
||||||
}
|
}
|
||||||
let questionNum = questionNumMatch[1];
|
let questionNum = questionNumMatch[1];
|
||||||
|
@ -182,19 +156,19 @@ async function goodjudgmentopen_inner(cookie: string) {
|
||||||
title: title,
|
title: title,
|
||||||
url: url,
|
url: url,
|
||||||
platform: platformName,
|
platform: platformName,
|
||||||
...moreinfo,
|
... moreinfo
|
||||||
};
|
};
|
||||||
if (j % 30 == 0 || DEBUG_MODE == "on") {
|
if (j % 30 == 0 || DEBUG_MODE == "on") {
|
||||||
console.log(`Page #${i}`);
|
console.log(`Page #${i}`);
|
||||||
console.log(question);
|
console.log(question);
|
||||||
|
} else {
|
||||||
|
console.log(question.title)
|
||||||
}
|
}
|
||||||
// console.log(question)
|
// console.log(question)
|
||||||
results.push(question);
|
results.push(question);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
console.log(
|
console.log(`We encountered some error when fetching the URL: ${url}, so it won't appear on the final json`);
|
||||||
`We encountered some error when fetching the URL: ${url}, so it won't appear on the final json`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
j = j + 1;
|
j = j + 1;
|
||||||
|
@ -207,9 +181,7 @@ async function goodjudgmentopen_inner(cookie: string) {
|
||||||
response = await fetchPage(i, cookie);
|
response = await fetchPage(i, cookie);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
console.log(
|
console.log(`We encountered some error when fetching page #${i}, so it won't appear on the final json`);
|
||||||
`We encountered some error when fetching page #${i}, so it won't appear on the final json`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,9 +192,11 @@ async function goodjudgmentopen_inner(cookie: string) {
|
||||||
|
|
||||||
let end = Date.now();
|
let end = Date.now();
|
||||||
let difference = end - init;
|
let difference = end - init;
|
||||||
console.log(
|
console.log(`Took ${
|
||||||
`Took ${difference / 1000} seconds, or ${difference / (1000 * 60)} minutes.`
|
difference / 1000
|
||||||
);
|
} seconds, or ${
|
||||||
|
difference / (1000 * 60)
|
||||||
|
} minutes.`);
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
@ -234,23 +208,18 @@ export const goodjudgmentopen: Platform = {
|
||||||
version: "v1",
|
version: "v1",
|
||||||
async fetcher() {
|
async fetcher() {
|
||||||
let cookie = process.env.GOODJUDGMENTOPENCOOKIE;
|
let cookie = process.env.GOODJUDGMENTOPENCOOKIE;
|
||||||
return (await applyIfSecretExists(cookie, goodjudgmentopen_inner)) || null;
|
return(await applyIfSecretExists(cookie, goodjudgmentopen_inner)) || null;
|
||||||
},
|
},
|
||||||
calculateStars(data) {
|
calculateStars(data) {
|
||||||
let minProbability = Math.min(
|
let minProbability = Math.min(...data.options.map((option) => option.probability || 0));
|
||||||
...data.options.map((option) => option.probability || 0)
|
let maxProbability = Math.max(...data.options.map((option) => option.probability || 0));
|
||||||
);
|
|
||||||
let maxProbability = Math.max(
|
|
||||||
...data.options.map((option) => option.probability || 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
let nuno = () => ((data.qualityindicators.numforecasts || 0) > 100 ? 3 : 2);
|
let nuno = () => ((data.qualityindicators.numforecasts || 0) > 100 ? 3 : 2);
|
||||||
let eli = () => 3;
|
let eli = () => 3;
|
||||||
let misha = () =>
|
let misha = () => minProbability > 0.1 || maxProbability < 0.9 ? 3.1 : 2.5;
|
||||||
minProbability > 0.1 || maxProbability < 0.9 ? 3.1 : 2.5;
|
|
||||||
|
|
||||||
let starsDecimal = average([nuno(), eli(), misha()]);
|
let starsDecimal = average([nuno(), eli(), misha()]);
|
||||||
let starsInteger = Math.round(starsDecimal);
|
let starsInteger = Math.round(starsDecimal);
|
||||||
return starsInteger;
|
return starsInteger;
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,9 +2,8 @@ import axios from "axios";
|
||||||
|
|
||||||
import { Question } from "@prisma/client";
|
import { Question } from "@prisma/client";
|
||||||
|
|
||||||
import { prisma } from "../database/prisma";
|
import { AlgoliaQuestion, questionToAlgoliaQuestion } from "../utils/algolia";
|
||||||
import { AlgoliaQuestion } from "../utils/algolia";
|
import { FetchedQuestion, Platform, prepareQuestion, upsertSingleQuestion } from "./";
|
||||||
import { FetchedQuestion, Platform, prepareQuestion } from "./";
|
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const searchEndpoint =
|
const searchEndpoint =
|
||||||
|
@ -23,7 +22,7 @@ const modelToQuestion = (model: any): ReturnType<typeof prepareQuestion> => {
|
||||||
title: model.name,
|
title: model.name,
|
||||||
url: `https://www.getguesstimate.com/models/${model.id}`,
|
url: `https://www.getguesstimate.com/models/${model.id}`,
|
||||||
// timestamp,
|
// timestamp,
|
||||||
description,
|
description: description || "",
|
||||||
options: [],
|
options: [],
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
|
@ -55,10 +54,11 @@ async function search(query: string): Promise<AlgoliaQuestion[]> {
|
||||||
const models: any[] = response.data.hits;
|
const models: any[] = response.data.hits;
|
||||||
const mappedModels: AlgoliaQuestion[] = models.map((model) => {
|
const mappedModels: AlgoliaQuestion[] = models.map((model) => {
|
||||||
const q = modelToQuestion(model);
|
const q = modelToQuestion(model);
|
||||||
return {
|
return questionToAlgoliaQuestion({
|
||||||
...q,
|
...q,
|
||||||
timestamp: String(q.timestamp),
|
fetched: new Date(),
|
||||||
};
|
firstSeen: new Date(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// filter for duplicates. Surprisingly common.
|
// filter for duplicates. Surprisingly common.
|
||||||
|
@ -76,12 +76,8 @@ async function search(query: string): Promise<AlgoliaQuestion[]> {
|
||||||
|
|
||||||
const fetchQuestion = async (id: number): Promise<Question> => {
|
const fetchQuestion = async (id: number): Promise<Question> => {
|
||||||
const response = await axios({ url: `${apiEndpoint}/spaces/${id}` });
|
const response = await axios({ url: `${apiEndpoint}/spaces/${id}` });
|
||||||
let q = modelToQuestion(response.data);
|
const q = modelToQuestion(response.data);
|
||||||
return await prisma.question.upsert({
|
return await upsertSingleQuestion(q);
|
||||||
where: { id: q.id },
|
|
||||||
create: q,
|
|
||||||
update: q,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const guesstimate: Platform & {
|
export const guesstimate: Platform & {
|
||||||
|
@ -94,5 +90,5 @@ export const guesstimate: Platform & {
|
||||||
search,
|
search,
|
||||||
version: "v1",
|
version: "v1",
|
||||||
fetchQuestion,
|
fetchQuestion,
|
||||||
calculateStars: (q) => (q.description.length > 250 ? 2 : 1),
|
calculateStars: (q) => (q.description?.length > 250 ? 2 : 1),
|
||||||
};
|
};
|
||||||
|
|
|
@ -28,9 +28,13 @@ export interface QualityIndicators {
|
||||||
|
|
||||||
export type FetchedQuestion = Omit<
|
export type FetchedQuestion = Omit<
|
||||||
Question,
|
Question,
|
||||||
"extra" | "qualityindicators" | "timestamp" | "platform" | "options"
|
| "extra"
|
||||||
|
| "qualityindicators"
|
||||||
|
| "fetched"
|
||||||
|
| "firstSeen"
|
||||||
|
| "platform"
|
||||||
|
| "options"
|
||||||
> & {
|
> & {
|
||||||
timestamp?: Date;
|
|
||||||
extra?: object; // required in DB but annoying to return empty; also this is slightly stricter than Prisma's JsonValue
|
extra?: object; // required in DB but annoying to return empty; also this is slightly stricter than Prisma's JsonValue
|
||||||
options: QuestionOption[]; // stronger type than Prisma's JsonValue
|
options: QuestionOption[]; // stronger type than Prisma's JsonValue
|
||||||
qualityindicators: Omit<QualityIndicators, "stars">; // slightly stronger type than Prisma's JsonValue
|
qualityindicators: Omit<QualityIndicators, "stars">; // slightly stronger type than Prisma's JsonValue
|
||||||
|
@ -78,8 +82,9 @@ export type Platform<ArgNames extends string = ""> = {
|
||||||
// So here we build a new type which should be ok to use both in place of prisma's Question type and as an input to its update or create methods.
|
// So here we build a new type which should be ok to use both in place of prisma's Question type and as an input to its update or create methods.
|
||||||
type PreparedQuestion = Omit<
|
type PreparedQuestion = Omit<
|
||||||
Question,
|
Question,
|
||||||
"extra" | "qualityindicators" | "options"
|
"extra" | "qualityindicators" | "options" | "fetched" | "firstSeen"
|
||||||
> & {
|
> & {
|
||||||
|
fetched: Date;
|
||||||
extra: NonNullable<Question["extra"]>;
|
extra: NonNullable<Question["extra"]>;
|
||||||
qualityindicators: NonNullable<Question["qualityindicators"]>;
|
qualityindicators: NonNullable<Question["qualityindicators"]>;
|
||||||
options: NonNullable<Question["options"]>;
|
options: NonNullable<Question["options"]>;
|
||||||
|
@ -91,8 +96,8 @@ export const prepareQuestion = (
|
||||||
): PreparedQuestion => {
|
): PreparedQuestion => {
|
||||||
return {
|
return {
|
||||||
extra: {},
|
extra: {},
|
||||||
timestamp: new Date(),
|
|
||||||
...q,
|
...q,
|
||||||
|
fetched: new Date(),
|
||||||
platform: platform.name,
|
platform: platform.name,
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
...q.qualityindicators,
|
...q.qualityindicators,
|
||||||
|
@ -101,6 +106,20 @@ export const prepareQuestion = (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const upsertSingleQuestion = async (
|
||||||
|
q: PreparedQuestion
|
||||||
|
): Promise<Question> => {
|
||||||
|
return await prisma.question.upsert({
|
||||||
|
where: { id: q.id },
|
||||||
|
create: {
|
||||||
|
...q,
|
||||||
|
firstSeen: new Date(),
|
||||||
|
},
|
||||||
|
update: q,
|
||||||
|
});
|
||||||
|
// TODO - update history?
|
||||||
|
};
|
||||||
|
|
||||||
export const processPlatform = async <T extends string = "">(
|
export const processPlatform = async <T extends string = "">(
|
||||||
platform: Platform<T>,
|
platform: Platform<T>,
|
||||||
args?: { [k in T]: string }
|
args?: { [k in T]: string }
|
||||||
|
@ -144,9 +163,9 @@ export const processPlatform = async <T extends string = "">(
|
||||||
|
|
||||||
for (const q of fetchedQuestions.map((q) => prepareQuestion(q, platform))) {
|
for (const q of fetchedQuestions.map((q) => prepareQuestion(q, platform))) {
|
||||||
if (oldIdsSet.has(q.id)) {
|
if (oldIdsSet.has(q.id)) {
|
||||||
|
// TODO - check if question has changed for better performance
|
||||||
updatedQuestions.push(q);
|
updatedQuestions.push(q);
|
||||||
} else {
|
} else {
|
||||||
// TODO - check if question has changed for better performance
|
|
||||||
createdQuestions.push(q);
|
createdQuestions.push(q);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +173,10 @@ export const processPlatform = async <T extends string = "">(
|
||||||
const stats: { created?: number; updated?: number; deleted?: number } = {};
|
const stats: { created?: number; updated?: number; deleted?: number } = {};
|
||||||
|
|
||||||
await prisma.question.createMany({
|
await prisma.question.createMany({
|
||||||
data: createdQuestions,
|
data: createdQuestions.map((q) => ({
|
||||||
|
...q,
|
||||||
|
firstSeen: new Date(),
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
stats.created = createdQuestions.length;
|
stats.created = createdQuestions.length;
|
||||||
|
|
||||||
|
|
339
src/backend/platforms/insight.ts
Normal file
339
src/backend/platforms/insight.ts
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
/* Imports */
|
||||||
|
import {or} from "ajv/dist/compile/codegen";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import {FetchedQuestion, Platform} from ".";
|
||||||
|
import {QuestionOption} from "../../common/types";
|
||||||
|
import toMarkdown from "../utils/toMarkdown";
|
||||||
|
import { average } from "../../utils";
|
||||||
|
|
||||||
|
/* Definitions */
|
||||||
|
const platformName = "insight";
|
||||||
|
const marketsEnpoint = "https://insightprediction.com/api/markets?orderBy=is_resolved&sortedBy=asc";
|
||||||
|
const getMarketEndpoint = (id : number) => `https://insightprediction.com/api/markets/${id}`;
|
||||||
|
const SPORTS_CATEGORIES = [
|
||||||
|
'World Cup',
|
||||||
|
'MLB',
|
||||||
|
'Futures',
|
||||||
|
'Sports',
|
||||||
|
'EPL',
|
||||||
|
'Golf',
|
||||||
|
'NHL',
|
||||||
|
'College Football'
|
||||||
|
]
|
||||||
|
|
||||||
|
/* Support functions */
|
||||||
|
|
||||||
|
// Stubs
|
||||||
|
const excludeMarketFromTitle = (title : any) => {
|
||||||
|
if (!!title) {
|
||||||
|
return title.includes(" vs ") || title.includes(" Over: ") || title.includes("NFL") || title.includes("Will there be a first time winner") || title.includes("Premier League")
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActiveYesNoOrderBook = (orderbook : any) => {
|
||||||
|
if (!!orderbook) {
|
||||||
|
let yes = !!orderbook.yes && !!orderbook.yes.buy && Array.isArray(orderbook.yes.buy) && orderbook.yes.buy.length != 0 && !!orderbook.yes.buy[0].price && !!orderbook.yes.sell && Array.isArray(orderbook.yes.sell) && orderbook.yes.sell.length != 0 && !!orderbook.yes.sell[0].price
|
||||||
|
let no = !!orderbook.no && !!orderbook.no.buy && Array.isArray(orderbook.no.buy) && orderbook.no.buy.length != 0 && !!orderbook.no.buy[0].price && !!orderbook.no.sell && Array.isArray(orderbook.no.sell) && orderbook.no.sell.length != 0 && !!orderbook.no.sell[0].price
|
||||||
|
return yes && no
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isBinaryQuestion = (data : any) => Array.isArray(data) && data.length == 1
|
||||||
|
|
||||||
|
const geomMean = (a : number, b : number) => Math.sqrt(a * b)
|
||||||
|
|
||||||
|
const processRelativeUrls = (a : string) => a.replaceAll("] (/", "](http://insightprediction.com/").replaceAll("](/", "](http://insightprediction.com/")
|
||||||
|
|
||||||
|
const processDescriptionText = (text : any) => {
|
||||||
|
if (typeof text === 'string') {
|
||||||
|
return processRelativeUrls(toMarkdown(text))
|
||||||
|
} else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOrderbookPrize = (orderbook : any) => {
|
||||||
|
let yes_min_cents = orderbook.yes.buy[0].price
|
||||||
|
let yes_max_cents = orderbook.yes.sell[0].price
|
||||||
|
let yes_min = Number(yes_min_cents.slice(0, -1))
|
||||||
|
let yes_max = Number(yes_max_cents.slice(0, -1))
|
||||||
|
let yes_price_orderbook = geomMean(yes_min, yes_max)
|
||||||
|
return yes_price_orderbook
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAnswerProbability = (answer : any) => {
|
||||||
|
let orderbook = answer.orderbook
|
||||||
|
let latest_yes_price = answer.latest_yes_price
|
||||||
|
if (!! orderbook && hasActiveYesNoOrderBook(orderbook)) {
|
||||||
|
let yes_price_orderbook = getOrderbookPrize(orderbook)
|
||||||
|
let yes_probability = (latest_yes_price ? geomMean(latest_yes_price, yes_price_orderbook) : yes_price_orderbook) / 100
|
||||||
|
return yes_probability
|
||||||
|
} else if (!! latest_yes_price) {
|
||||||
|
return latest_yes_price / 100
|
||||||
|
} else {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetching
|
||||||
|
async function fetchPage(bearer: string, pageNum: number) {
|
||||||
|
let pageUrl = `${marketsEnpoint}&page=${pageNum}`
|
||||||
|
const response = await axios({
|
||||||
|
url: pageUrl, // &orderBy=is_resolved&sortedBy=desc`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${bearer}`
|
||||||
|
}
|
||||||
|
}).then((res) => res.data);
|
||||||
|
// console.log(response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMarket(bearer: string, marketId: number) {
|
||||||
|
const response = await axios({
|
||||||
|
url: getMarketEndpoint(marketId),
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${bearer}`
|
||||||
|
}
|
||||||
|
}).then((res) => res.data);
|
||||||
|
// console.log(response)
|
||||||
|
return response;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const processMarket = (market : any) => {
|
||||||
|
let options: FetchedQuestion["options"] = []
|
||||||
|
|
||||||
|
if (!!market && !!market.answer && !!market.answer.data) {
|
||||||
|
let data = market.answer.data
|
||||||
|
if (isBinaryQuestion(data)) { // Binary questions
|
||||||
|
let answer = data[0]
|
||||||
|
let probability = getAnswerProbability(answer)
|
||||||
|
if (probability != -1) {
|
||||||
|
options = [
|
||||||
|
{
|
||||||
|
name: "Yes",
|
||||||
|
probability: probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
}, {
|
||||||
|
name: "No",
|
||||||
|
probability: 1 - probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else { // non binary question
|
||||||
|
for (let answer of data) {
|
||||||
|
let probability = getAnswerProbability(answer)
|
||||||
|
if (probability != -1) {
|
||||||
|
let newOption: QuestionOption = ({
|
||||||
|
name: String(answer.title),
|
||||||
|
probability: probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
});
|
||||||
|
options.push(newOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!! options && Array.isArray(options) && options.length > 0) {
|
||||||
|
const id = `${platformName}-${
|
||||||
|
market.id
|
||||||
|
}`
|
||||||
|
const result: FetchedQuestion = {
|
||||||
|
id: id,
|
||||||
|
title: market.title,
|
||||||
|
url: market.url,
|
||||||
|
description: processDescriptionText(market.rules),
|
||||||
|
options,
|
||||||
|
qualityindicators: market.coin_id == "USD" ? (
|
||||||
|
{volume: market.volume}
|
||||||
|
) : ({})
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllMarkets(bearer: string) {
|
||||||
|
let pageNum = 1
|
||||||
|
let markets = []
|
||||||
|
let categories = []
|
||||||
|
let isEnd = false
|
||||||
|
while (! isEnd) {
|
||||||
|
if(pageNum % 20 == 0){
|
||||||
|
console.log(`Fetching page #${pageNum}`) // : ${pageUrl}
|
||||||
|
}
|
||||||
|
let page = await fetchPage(bearer, pageNum)
|
||||||
|
// console.log(JSON.stringify(page, null, 2))
|
||||||
|
let data = page.data
|
||||||
|
if (!! data && Array.isArray(data) && data.length > 0) {
|
||||||
|
let lastMarket = data[data.length - 1]
|
||||||
|
let isLastMarketResolved = lastMarket.is_resolved
|
||||||
|
if (isLastMarketResolved == true) {
|
||||||
|
isEnd = true
|
||||||
|
}
|
||||||
|
let newMarkets = data.filter(market => !market.is_resolved && !market.is_expired && ! excludeMarketFromTitle(market.title))
|
||||||
|
for (let initMarketData of newMarkets) {
|
||||||
|
let fullMarketDataResponse = await fetchMarket(bearer, initMarketData.id)
|
||||||
|
let fullMarketData = fullMarketDataResponse.data
|
||||||
|
let processedMarketData = processMarket(fullMarketData)
|
||||||
|
|
||||||
|
if (processedMarketData != null && ! SPORTS_CATEGORIES.includes(fullMarketData.category)) {
|
||||||
|
console.log(`- Adding: ${
|
||||||
|
fullMarketData.title
|
||||||
|
}`)
|
||||||
|
console.group()
|
||||||
|
console.log(fullMarketData)
|
||||||
|
console.log(JSON.stringify(processedMarketData, null, 2))
|
||||||
|
console.groupEnd()
|
||||||
|
|
||||||
|
markets.push(processedMarketData)
|
||||||
|
}
|
||||||
|
|
||||||
|
let category = fullMarketData.category
|
||||||
|
categories.push(category)
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isEnd = true
|
||||||
|
} pageNum = pageNum + 1
|
||||||
|
}
|
||||||
|
console.log(markets)
|
||||||
|
console.log(categories)
|
||||||
|
return markets
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
async function fetchQuestionStats(bearer : string, marketId : number) {
|
||||||
|
const response = await axios({
|
||||||
|
url: getMarketEndpoint(marketId),
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `Bearer ${bearer}`
|
||||||
|
}
|
||||||
|
}).then((res) => res.data);
|
||||||
|
// console.log(response)
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function fetchData(bearer : string) {
|
||||||
|
let pageNum = 1;
|
||||||
|
let reachedEnd = false;
|
||||||
|
let results = [];
|
||||||
|
while (! reachedEnd) {
|
||||||
|
let newPage = await fetchPage(bearer, pageNum);
|
||||||
|
let newPageData = newPage.data;
|
||||||
|
let marketsFromPage = []
|
||||||
|
for (let market of newPageData) {
|
||||||
|
let response = await fetchQuestionStats(bearer, market.id);
|
||||||
|
let marketData = response.data
|
||||||
|
let marketAnswer = marketData.answer.data
|
||||||
|
delete marketData.answer
|
||||||
|
// These are the options and their prices.
|
||||||
|
let marketOptions = marketAnswer.map(answer => {
|
||||||
|
return({name: answer.title, probability: answer.latest_yes_price, type: "PROBABILITY"})
|
||||||
|
})
|
||||||
|
marketsFromPage.push({
|
||||||
|
... marketData,
|
||||||
|
options: marketOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let finalObject = marketsFromPage
|
||||||
|
|
||||||
|
console.log(`Page = #${pageNum}`);
|
||||||
|
// console.log(newPageData)
|
||||||
|
console.dir(finalObject, {depth: null});
|
||||||
|
results.push(... finalObject);
|
||||||
|
|
||||||
|
let newPagination = newPage.meta.pagination;
|
||||||
|
if (newPagination.total_pages == pageNum) {
|
||||||
|
reachedEnd = true;
|
||||||
|
} else {
|
||||||
|
pageNum = pageNum + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processPredictions(predictions : any[]) {
|
||||||
|
let results = await predictions.map((prediction) => {
|
||||||
|
const id = `${platformName}-${
|
||||||
|
prediction.id
|
||||||
|
}`;
|
||||||
|
const probability = prediction.probability;
|
||||||
|
const options: FetchedQuestion["options"] = [
|
||||||
|
{
|
||||||
|
name: "Yes",
|
||||||
|
probability: probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
}, {
|
||||||
|
name: "No",
|
||||||
|
probability: 1 - probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const result: FetchedQuestion = {
|
||||||
|
id,
|
||||||
|
title: prediction.title,
|
||||||
|
url: "https://example.com",
|
||||||
|
description: prediction.description,
|
||||||
|
options,
|
||||||
|
qualityindicators: {
|
||||||
|
// other: prediction.otherx,
|
||||||
|
// indicators: prediction.indicatorx,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
return results; // resultsProcessed
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
/* Body */
|
||||||
|
export const insight: Platform = {
|
||||||
|
name: platformName,
|
||||||
|
label: "Insight Prediction",
|
||||||
|
color: "#ff0000",
|
||||||
|
version: "v1",
|
||||||
|
async fetcher() {
|
||||||
|
let bearer = process.env.INSIGHT_BEARER;
|
||||||
|
if (!! bearer) {
|
||||||
|
let data = await fetchAllMarkets(bearer);
|
||||||
|
return data
|
||||||
|
} else {
|
||||||
|
throw Error("No INSIGHT_BEARER available in environment")
|
||||||
|
}
|
||||||
|
// let results: FetchedQuestion[] = []; // await processPredictions(data); // somehow needed
|
||||||
|
// return results;
|
||||||
|
},
|
||||||
|
calculateStars(data) {
|
||||||
|
let nuno = () => {
|
||||||
|
if((data.qualityindicators.volume || 0) > 10000){
|
||||||
|
return 4
|
||||||
|
} else if((data.qualityindicators.volume || 0) > 1000){
|
||||||
|
return 3
|
||||||
|
} else{
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let eli = () => null;
|
||||||
|
let misha = () => null;
|
||||||
|
let starsDecimal = average([nuno()]); //, eli(data), misha(data)])
|
||||||
|
let starsInteger = Math.round(starsDecimal);
|
||||||
|
return starsInteger;
|
||||||
|
}
|
||||||
|
};
|
|
@ -6,12 +6,12 @@ import { FetchedQuestion, Platform } from "./";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "manifold";
|
const platformName = "manifold";
|
||||||
const endpoint = "https://manifold.markets/api/v0/markets";
|
const ENDPOINT = "https://manifold.markets/api/v0/markets";
|
||||||
// See https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5
|
// See https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
|
|
||||||
async function fetchData() {
|
async function fetchPage(endpoint: string) {
|
||||||
let response = await axios({
|
let response = await axios({
|
||||||
url: endpoint,
|
url: endpoint,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
@ -23,6 +23,31 @@ async function fetchData() {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchAllData(){
|
||||||
|
let endpoint = ENDPOINT
|
||||||
|
let end = false
|
||||||
|
let allData = []
|
||||||
|
let counter = 1
|
||||||
|
while(!end){
|
||||||
|
console.log(`Query #${counter}: ${endpoint}`)
|
||||||
|
let newData = await fetchPage(endpoint)
|
||||||
|
if(Array.isArray(newData)){
|
||||||
|
allData.push(...newData)
|
||||||
|
let hasReachedEnd = (newData.length == 0) || (newData[newData.length -1] == undefined) || (newData[newData.length -1].id == undefined)
|
||||||
|
if(!hasReachedEnd){
|
||||||
|
let lastId = newData[newData.length -1].id
|
||||||
|
endpoint = `${ENDPOINT}?before=${lastId}`
|
||||||
|
}else{
|
||||||
|
end = true
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
end = true
|
||||||
|
}
|
||||||
|
counter = counter +1
|
||||||
|
}
|
||||||
|
return allData
|
||||||
|
}
|
||||||
|
|
||||||
function showStatistics(results: FetchedQuestion[]) {
|
function showStatistics(results: FetchedQuestion[]) {
|
||||||
console.log(`Num unresolved markets: ${results.length}`);
|
console.log(`Num unresolved markets: ${results.length}`);
|
||||||
let sum = (arr: number[]) => arr.reduce((tally, a) => tally + a, 0);
|
let sum = (arr: number[]) => arr.reduce((tally, a) => tally + a, 0);
|
||||||
|
@ -63,11 +88,11 @@ function processPredictions(predictions: any[]): FetchedQuestion[] {
|
||||||
id: id,
|
id: id,
|
||||||
title: prediction.question,
|
title: prediction.question,
|
||||||
url: prediction.url,
|
url: prediction.url,
|
||||||
description: prediction.description,
|
description: prediction.description || "",
|
||||||
options,
|
options,
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
createdTime: prediction.createdTime,
|
createdTime: prediction.createdTime,
|
||||||
volume7Days: prediction.volume7Days,
|
// volume7Days: prediction.volume7Days, // deprecated.
|
||||||
volume24Hours: prediction.volume24Hours,
|
volume24Hours: prediction.volume24Hours,
|
||||||
pool: prediction.pool, // normally liquidity, but I don't actually want to show it.
|
pool: prediction.pool, // normally liquidity, but I don't actually want to show it.
|
||||||
},
|
},
|
||||||
|
@ -90,16 +115,16 @@ export const manifold: Platform = {
|
||||||
color: "#793466",
|
color: "#793466",
|
||||||
version: "v1",
|
version: "v1",
|
||||||
async fetcher() {
|
async fetcher() {
|
||||||
let data = await fetchData();
|
let data = await fetchAllData();
|
||||||
let results = processPredictions(data); // somehow needed
|
let results = processPredictions(data); // somehow needed
|
||||||
showStatistics(results);
|
showStatistics(results);
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
calculateStars(data) {
|
calculateStars(data) {
|
||||||
let nuno = () =>
|
let nuno = () =>
|
||||||
(data.qualityindicators.volume7Days || 0) > 250 ||
|
(data.qualityindicators.volume24Hours || 0) > 100 ||
|
||||||
((data.qualityindicators.pool || 0) > 500 &&
|
((data.qualityindicators.pool || 0) > 500 &&
|
||||||
(data.qualityindicators.volume7Days || 0) > 100)
|
(data.qualityindicators.volume24Hours || 0) > 50)
|
||||||
? 2
|
? 2
|
||||||
: 1;
|
: 1;
|
||||||
let eli = () => null;
|
let eli = () => null;
|
||||||
|
|
|
@ -1,205 +0,0 @@
|
||||||
/* Imports */
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { average } from "../../utils";
|
|
||||||
import { sleep } from "../utils/sleep";
|
|
||||||
import toMarkdown from "../utils/toMarkdown";
|
|
||||||
import { FetchedQuestion, Platform } from "./";
|
|
||||||
|
|
||||||
/* Definitions */
|
|
||||||
const platformName = "metaculus";
|
|
||||||
let now = new Date().toISOString();
|
|
||||||
let DEBUG_MODE = "off";
|
|
||||||
let SLEEP_TIME = 5000;
|
|
||||||
|
|
||||||
/* Support functions */
|
|
||||||
async function fetchMetaculusQuestions(next: string) {
|
|
||||||
// Numbers about a given address: how many, how much, at what price, etc.
|
|
||||||
let response;
|
|
||||||
let data;
|
|
||||||
try {
|
|
||||||
response = await axios({
|
|
||||||
url: next,
|
|
||||||
method: "GET",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
data = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error in async function fetchMetaculusQuestions(next)`);
|
|
||||||
console.log(error);
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
if (error.response?.headers["retry-after"]) {
|
|
||||||
const timeout = error.response.headers["retry-after"];
|
|
||||||
console.log(`Timeout: ${timeout}`);
|
|
||||||
await sleep(Number(timeout) * 1000 + SLEEP_TIME);
|
|
||||||
} else {
|
|
||||||
await sleep(SLEEP_TIME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
response = await axios({
|
|
||||||
url: next,
|
|
||||||
method: "GET",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
data = response.data;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return { results: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// console.log(response)
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchMetaculusQuestionDescription(slug: string) {
|
|
||||||
try {
|
|
||||||
let response = await axios({
|
|
||||||
method: "get",
|
|
||||||
url: "https://www.metaculus.com" + slug,
|
|
||||||
}).then((response) => response.data);
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error in: fetchMetaculusQuestionDescription`);
|
|
||||||
console.log(
|
|
||||||
`We encountered some error when attempting to fetch a metaculus page. Trying again`
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
axios.isAxiosError(error) &&
|
|
||||||
typeof error.response != "undefined" &&
|
|
||||||
typeof error.response.headers != "undefined" &&
|
|
||||||
typeof error.response.headers["retry-after"] != "undefined"
|
|
||||||
) {
|
|
||||||
const timeout = error.response.headers["retry-after"];
|
|
||||||
console.log(`Timeout: ${timeout}`);
|
|
||||||
await sleep(Number(timeout) * 1000 + SLEEP_TIME);
|
|
||||||
} else {
|
|
||||||
await sleep(SLEEP_TIME);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
let response = await axios({
|
|
||||||
method: "get",
|
|
||||||
url: "https://www.metaculus.com" + slug,
|
|
||||||
}).then((response) => response.data);
|
|
||||||
// console.log(response)
|
|
||||||
return response;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
`We encountered some error when attempting to fetch a metaculus page.`
|
|
||||||
);
|
|
||||||
console.log("Error", error);
|
|
||||||
throw "Giving up";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const metaculus: Platform = {
|
|
||||||
name: platformName,
|
|
||||||
label: "Metaculus",
|
|
||||||
color: "#006669",
|
|
||||||
version: "v1",
|
|
||||||
async fetcher() {
|
|
||||||
// let metaculusQuestionsInit = await fetchMetaculusQuestions(1)
|
|
||||||
// let numQueries = Math.round(Number(metaculusQuestionsInit.count) / 20)
|
|
||||||
// console.log(`Downloading... This might take a while. Total number of queries: ${numQueries}`)
|
|
||||||
// for (let i = 4; i <= numQueries; i++) { // change numQueries to 10 if one want to just test }
|
|
||||||
let all_questions = [];
|
|
||||||
let next = "https://www.metaculus.com/api2/questions/";
|
|
||||||
let i = 1;
|
|
||||||
while (next) {
|
|
||||||
if (i % 20 == 0) {
|
|
||||||
console.log("Sleeping for 500ms");
|
|
||||||
await sleep(SLEEP_TIME);
|
|
||||||
}
|
|
||||||
console.log(`\nQuery #${i}`);
|
|
||||||
let metaculusQuestions = await fetchMetaculusQuestions(next);
|
|
||||||
let results = metaculusQuestions.results;
|
|
||||||
let j = false;
|
|
||||||
for (let result of results) {
|
|
||||||
if (result.publish_time < now && now < result.resolve_time) {
|
|
||||||
await sleep(SLEEP_TIME / 2);
|
|
||||||
let questionPage = await fetchMetaculusQuestionDescription(
|
|
||||||
result.page_url
|
|
||||||
);
|
|
||||||
if (!questionPage.includes("A public prediction by")) {
|
|
||||||
// console.log(questionPage)
|
|
||||||
let descriptionraw = questionPage.split(
|
|
||||||
`<div class="content" ng-bind-html-compile="qctrl.question.description_html">`
|
|
||||||
)[1]; //.split(`<div class="question__content">`)[1]
|
|
||||||
let descriptionprocessed1 = descriptionraw.split("</div>")[0];
|
|
||||||
let descriptionprocessed2 = toMarkdown(descriptionprocessed1);
|
|
||||||
let description = descriptionprocessed2;
|
|
||||||
|
|
||||||
let isbinary = result.possibilities.type == "binary";
|
|
||||||
let options: FetchedQuestion["options"] = [];
|
|
||||||
if (isbinary) {
|
|
||||||
let probability = Number(result.community_prediction.full.q2);
|
|
||||||
options = [
|
|
||||||
{
|
|
||||||
name: "Yes",
|
|
||||||
probability: probability,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No",
|
|
||||||
probability: 1 - probability,
|
|
||||||
type: "PROBABILITY",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
let id = `${platformName}-${result.id}`;
|
|
||||||
let interestingInfo: FetchedQuestion = {
|
|
||||||
id,
|
|
||||||
title: result.title,
|
|
||||||
url: "https://www.metaculus.com" + result.page_url,
|
|
||||||
description,
|
|
||||||
options,
|
|
||||||
qualityindicators: {
|
|
||||||
numforecasts: Number(result.number_of_predictions),
|
|
||||||
},
|
|
||||||
extra: {
|
|
||||||
resolution_data: {
|
|
||||||
publish_time: result.publish_time,
|
|
||||||
resolution: result.resolution,
|
|
||||||
close_time: result.close_time,
|
|
||||||
resolve_time: result.resolve_time,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
//"status": result.status,
|
|
||||||
//"publish_time": result.publish_time,
|
|
||||||
//"close_time": result.close_time,
|
|
||||||
//"type": result.possibilities.type, // We want binary ones here.
|
|
||||||
//"last_activity_time": result.last_activity_time,
|
|
||||||
};
|
|
||||||
if (Number(result.number_of_predictions) >= 10) {
|
|
||||||
console.log(`- ${interestingInfo.title}`);
|
|
||||||
all_questions.push(interestingInfo);
|
|
||||||
if ((!j && i % 20 == 0) || DEBUG_MODE == "on") {
|
|
||||||
console.log(interestingInfo);
|
|
||||||
j = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log("- [Skipping public prediction]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
next = metaculusQuestions.next;
|
|
||||||
i = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return all_questions;
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateStars(data) {
|
|
||||||
const { numforecasts } = data.qualityindicators;
|
|
||||||
let nuno = () =>
|
|
||||||
(numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
|
|
||||||
let eli = () => 3;
|
|
||||||
let misha = () => 3;
|
|
||||||
let starsDecimal = average([nuno(), eli(), misha()]);
|
|
||||||
let starsInteger = Math.round(starsDecimal);
|
|
||||||
return starsInteger;
|
|
||||||
},
|
|
||||||
};
|
|
262
src/backend/platforms/metaculus/api.ts
Normal file
262
src/backend/platforms/metaculus/api.ts
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
import Ajv, { JTDDataType, ValidateFunction } from "ajv/dist/jtd";
|
||||||
|
import axios from "axios";
|
||||||
|
import { sleep } from "../../utils/sleep";
|
||||||
|
|
||||||
|
// Type examples:
|
||||||
|
// - group: https://www.metaculus.com/api2/questions/9866/
|
||||||
|
// - claim: https://www.metaculus.com/api2/questions/9668/
|
||||||
|
// - subquestion forecast: https://www.metaculus.com/api2/questions/10069/
|
||||||
|
// - basic forecast: https://www.metaculus.com/api2/questions/11005/
|
||||||
|
|
||||||
|
const RETRY_SLEEP_TIME = 5000;
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
id: {
|
||||||
|
type: "uint32",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const predictableProps = {
|
||||||
|
publish_time: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
close_time: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
resolve_time: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
resolution: {
|
||||||
|
type: "float64",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
possibilities: {
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
// Enum["binary", "continuous"], via https://github.com/quantified-uncertainty/metaforecast/pull/84#discussion_r878240875
|
||||||
|
// but metaculus might add new values in the future and we don't want the fetcher to break
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
number_of_predictions: {
|
||||||
|
type: "uint32",
|
||||||
|
},
|
||||||
|
community_prediction: {
|
||||||
|
properties: {
|
||||||
|
full: {
|
||||||
|
// q1/q2/q3 can be missing, e.g. https://www.metaculus.com/api2/questions/1633/
|
||||||
|
optionalProperties: {
|
||||||
|
q1: {
|
||||||
|
type: "float64",
|
||||||
|
},
|
||||||
|
q2: {
|
||||||
|
type: "float64",
|
||||||
|
},
|
||||||
|
q3: {
|
||||||
|
type: "float64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nullable: true,
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const pageProps = {
|
||||||
|
page_url: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
type: "uint32",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// these are missing in /api2/questions/ requests, and building two schemas is too much pain
|
||||||
|
const optionalPageProps = {
|
||||||
|
description: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
description_html: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const questionSchema = {
|
||||||
|
discriminator: "type",
|
||||||
|
mapping: {
|
||||||
|
forecast: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...pageProps,
|
||||||
|
...predictableProps,
|
||||||
|
},
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...pageProps,
|
||||||
|
},
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
sub_questions: {
|
||||||
|
elements: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...predictableProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
// we're not interested in claims currently (but we should be?)
|
||||||
|
claim: {
|
||||||
|
properties: {
|
||||||
|
...commonProps,
|
||||||
|
...pageProps,
|
||||||
|
},
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
discussion: {
|
||||||
|
optionalProperties: {
|
||||||
|
...optionalPageProps,
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const knownQuestionTypes = Object.keys(questionSchema.mapping);
|
||||||
|
|
||||||
|
const shallowMultipleQuestionsSchema = {
|
||||||
|
properties: {
|
||||||
|
results: {
|
||||||
|
elements: {
|
||||||
|
properties: {
|
||||||
|
type: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
type: "string",
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
additionalProperties: true,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ApiCommon = JTDDataType<{
|
||||||
|
properties: typeof commonProps;
|
||||||
|
}>;
|
||||||
|
export type ApiPredictable = JTDDataType<{
|
||||||
|
properties: typeof predictableProps;
|
||||||
|
}>;
|
||||||
|
export type ApiQuestion = JTDDataType<typeof questionSchema>;
|
||||||
|
|
||||||
|
type ApiShallowMultipleQuestions = JTDDataType<
|
||||||
|
typeof shallowMultipleQuestionsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ApiMultipleQuestions = {
|
||||||
|
results: ApiQuestion[];
|
||||||
|
next: ApiShallowMultipleQuestions["next"]; // Omit<ApiShallowMultipleQuestions, "results"> doesn't work correctly here
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateQuestion = new Ajv().compile<ApiQuestion>(questionSchema);
|
||||||
|
const validateShallowMultipleQuestions =
|
||||||
|
new Ajv().compile<ApiShallowMultipleQuestions>(
|
||||||
|
shallowMultipleQuestionsSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
async function fetchWithRetries<T = unknown>(url: string): Promise<T> {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<T>(url);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error while fetching ${url}`);
|
||||||
|
console.log(error);
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
if (error.response?.headers["retry-after"]) {
|
||||||
|
const timeout = error.response.headers["retry-after"];
|
||||||
|
console.log(`Timeout: ${timeout}`);
|
||||||
|
await sleep(Number(timeout) * 1000 + 1000);
|
||||||
|
} else {
|
||||||
|
await sleep(RETRY_SLEEP_TIME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await axios.get<T>(url);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAndValidate = async <T = unknown>(
|
||||||
|
url: string,
|
||||||
|
validator: ValidateFunction<T>
|
||||||
|
): Promise<T> => {
|
||||||
|
// console.log(url);
|
||||||
|
const data = await fetchWithRetries<object>(url);
|
||||||
|
if (validator(data)) {
|
||||||
|
return data;
|
||||||
|
}else{
|
||||||
|
console.log(data)
|
||||||
|
throw new Error(
|
||||||
|
`Response validation for url ${url} failed: ` +
|
||||||
|
JSON.stringify(validator.errors, null, 4)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchApiQuestions(
|
||||||
|
next: string
|
||||||
|
): Promise<ApiMultipleQuestions> {
|
||||||
|
const data = await fetchAndValidate(next, validateShallowMultipleQuestions);
|
||||||
|
|
||||||
|
const isDefined = <T>(argument: T | undefined): argument is T => {
|
||||||
|
return argument !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
results: data.results
|
||||||
|
.map((result) => {
|
||||||
|
if (!knownQuestionTypes.includes(result.type)) {
|
||||||
|
console.warn(`Unknown result type ${result.type}, skipping`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (!validateQuestion(result)) {
|
||||||
|
throw new Error(
|
||||||
|
`Response validation failed: ` +
|
||||||
|
JSON.stringify(validateQuestion.errors)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
})
|
||||||
|
.filter(isDefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchSingleApiQuestion(id: number): Promise<ApiQuestion> {
|
||||||
|
return await fetchAndValidate(
|
||||||
|
`https://www.metaculus.com/api2/questions/${id}/`,
|
||||||
|
validateQuestion
|
||||||
|
);
|
||||||
|
}
|
220
src/backend/platforms/metaculus/index.ts
Normal file
220
src/backend/platforms/metaculus/index.ts
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import Error from "next/error";
|
||||||
|
import {FetchedQuestion, Platform} from "..";
|
||||||
|
import {average} from "../../../utils";
|
||||||
|
import {sleep} from "../../utils/sleep";
|
||||||
|
import {
|
||||||
|
ApiCommon,
|
||||||
|
ApiMultipleQuestions,
|
||||||
|
ApiPredictable,
|
||||||
|
ApiQuestion,
|
||||||
|
fetchApiQuestions,
|
||||||
|
fetchSingleApiQuestion
|
||||||
|
} from "./api";
|
||||||
|
|
||||||
|
const platformName = "metaculus";
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const SLEEP_TIME = 1000;
|
||||||
|
|
||||||
|
async function apiQuestionToFetchedQuestions(apiQuestion: ApiQuestion): Promise<FetchedQuestion[]> {
|
||||||
|
// one item can expand:
|
||||||
|
// - to 0 questions if we don't want it;
|
||||||
|
// - to 1 question if it's a simple forecast
|
||||||
|
// - to multiple questions if it's a group (see https://github.com/quantified-uncertainty/metaforecast/pull/84 for details)
|
||||||
|
|
||||||
|
const skip = (q : ApiPredictable) : boolean => {
|
||||||
|
if (q.publish_time > now || now > q.resolve_time) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (q.number_of_predictions < 10) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildFetchedQuestion = (q : ApiPredictable & ApiCommon) : Omit < FetchedQuestion,
|
||||||
|
"url" | "description" | "title" > => {
|
||||||
|
const isBinary = q.possibilities.type === "binary";
|
||||||
|
let options: FetchedQuestion["options"] = [];
|
||||||
|
if (isBinary) {
|
||||||
|
const probability = q.community_prediction?.full.q2;
|
||||||
|
if (probability !== undefined) {
|
||||||
|
options = [
|
||||||
|
{
|
||||||
|
name: "Yes",
|
||||||
|
probability: probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
}, {
|
||||||
|
name: "No",
|
||||||
|
probability: 1 - probability,
|
||||||
|
type: "PROBABILITY"
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: `${platformName}-${
|
||||||
|
q.id
|
||||||
|
}`,
|
||||||
|
options,
|
||||||
|
qualityindicators: {
|
||||||
|
numforecasts: q.number_of_predictions
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
resolution_data: {
|
||||||
|
publish_time: apiQuestion.publish_time,
|
||||||
|
resolution: apiQuestion.resolution,
|
||||||
|
close_time: apiQuestion.close_time,
|
||||||
|
resolve_time: apiQuestion.resolve_time
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (apiQuestion.type === "group") {
|
||||||
|
await sleep(SLEEP_TIME);
|
||||||
|
let apiQuestionDetailsTemp
|
||||||
|
try{
|
||||||
|
apiQuestionDetailsTemp = await fetchSingleApiQuestion(apiQuestion.id);
|
||||||
|
}catch(error){
|
||||||
|
console.log(error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const apiQuestionDetails = apiQuestionDetailsTemp
|
||||||
|
if (apiQuestionDetails.type !== "group") {
|
||||||
|
console.log("Error: expected `group` type")
|
||||||
|
return [] //throw new Error("Expected `group` type"); // shouldn't happen, this is mostly for typescript
|
||||||
|
}else{
|
||||||
|
try{
|
||||||
|
let result = (apiQuestionDetails.sub_questions || []).filter((q) => ! skip(q)).map((sq) => {
|
||||||
|
const tmp = buildFetchedQuestion(sq);
|
||||||
|
return {
|
||||||
|
... tmp,
|
||||||
|
title: `${
|
||||||
|
apiQuestion.title
|
||||||
|
} (${
|
||||||
|
sq.title
|
||||||
|
})`,
|
||||||
|
description: apiQuestionDetails.description || "",
|
||||||
|
url: `https://www.metaculus.com${
|
||||||
|
apiQuestion.page_url
|
||||||
|
}?sub-question=${
|
||||||
|
sq.id
|
||||||
|
}`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result
|
||||||
|
}catch(error){
|
||||||
|
console.log(error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (apiQuestion.type === "forecast") {
|
||||||
|
if (apiQuestion.group) {
|
||||||
|
return []; // sub-question, should be handled on the group level
|
||||||
|
}
|
||||||
|
if (skip(apiQuestion)) {
|
||||||
|
console.log(`- [Skipping]: ${
|
||||||
|
apiQuestion.title
|
||||||
|
}`)
|
||||||
|
/*console.log(`Close time: ${
|
||||||
|
apiQuestion.close_time
|
||||||
|
}, resolve time: ${
|
||||||
|
apiQuestion.resolve_time
|
||||||
|
}`)*/
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
await sleep(SLEEP_TIME);
|
||||||
|
try{
|
||||||
|
const apiQuestionDetails = await fetchSingleApiQuestion(apiQuestion.id);
|
||||||
|
const tmp = buildFetchedQuestion(apiQuestion);
|
||||||
|
return [{
|
||||||
|
... tmp,
|
||||||
|
title: apiQuestion.title,
|
||||||
|
description: apiQuestionDetails.description || "",
|
||||||
|
url: "https://www.metaculus.com" + apiQuestion.page_url
|
||||||
|
},];
|
||||||
|
}catch(error){
|
||||||
|
console.log(error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (apiQuestion.type !== "claim") { // should never happen, since `discriminator` in JTD schema causes a strict runtime check
|
||||||
|
console.log(`Unknown metaculus question type: ${
|
||||||
|
(apiQuestion as any).type
|
||||||
|
}, skipping`);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const metaculus: Platform<"id" | "debug"> = {
|
||||||
|
name: platformName,
|
||||||
|
label: "Metaculus",
|
||||||
|
color: "#006669",
|
||||||
|
version: "v2",
|
||||||
|
fetcherArgs: [
|
||||||
|
"id", "debug"
|
||||||
|
],
|
||||||
|
async fetcher(opts) {
|
||||||
|
let allQuestions: FetchedQuestion[] = [];
|
||||||
|
|
||||||
|
if (opts.args ?. id) {
|
||||||
|
try{
|
||||||
|
console.log("Using optional id arg.")
|
||||||
|
const id = Number(opts.args.id);
|
||||||
|
const apiQuestion = await fetchSingleApiQuestion(id);
|
||||||
|
const questions = await apiQuestionToFetchedQuestions(apiQuestion);
|
||||||
|
console.log(questions);
|
||||||
|
return {questions, partial: true};
|
||||||
|
|
||||||
|
}catch(error){
|
||||||
|
console.log(error)
|
||||||
|
return {questions: [], partial: true};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let next: string | null = "https://www.metaculus.com/api2/questions/";
|
||||||
|
let i = 1;
|
||||||
|
while (next) {
|
||||||
|
console.log(`\nQuery #${i} - ${next}`);
|
||||||
|
|
||||||
|
await sleep(SLEEP_TIME);
|
||||||
|
const apiQuestions: ApiMultipleQuestions = await fetchApiQuestions(next);
|
||||||
|
const results = apiQuestions.results;
|
||||||
|
// console.log(results)
|
||||||
|
let j = false;
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const questions = await apiQuestionToFetchedQuestions(result);
|
||||||
|
// console.log(questions)
|
||||||
|
for (const question of questions) {
|
||||||
|
console.log(`- ${
|
||||||
|
question.title
|
||||||
|
}`);
|
||||||
|
if ((! j && i % 20 === 0) || opts.args ?. debug) {
|
||||||
|
console.log(question);
|
||||||
|
j = true;
|
||||||
|
}
|
||||||
|
allQuestions.push(question);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next = apiQuestions.next;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {questions: allQuestions, partial: false};
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateStars(data) {
|
||||||
|
const {numforecasts} = data.qualityindicators;
|
||||||
|
const nuno = () => (numforecasts || 0) > 300 ? 4 : (numforecasts || 0) > 100 ? 3 : 2;
|
||||||
|
const eli = () => 3;
|
||||||
|
const misha = () => 3;
|
||||||
|
const starsDecimal = average([nuno(), eli(), misha()]);
|
||||||
|
const starsInteger = Math.round(starsDecimal);
|
||||||
|
return starsInteger;
|
||||||
|
}
|
||||||
|
};
|
|
@ -7,6 +7,7 @@ import { goodjudgmentopen } from "./goodjudgmentopen";
|
||||||
import { guesstimate } from "./guesstimate";
|
import { guesstimate } from "./guesstimate";
|
||||||
import { Platform, PlatformConfig } from "./index";
|
import { Platform, PlatformConfig } from "./index";
|
||||||
import { infer } from "./infer";
|
import { infer } from "./infer";
|
||||||
|
import { insight } from "./insight";
|
||||||
import { kalshi } from "./kalshi";
|
import { kalshi } from "./kalshi";
|
||||||
import { manifold } from "./manifold";
|
import { manifold } from "./manifold";
|
||||||
import { metaculus } from "./metaculus";
|
import { metaculus } from "./metaculus";
|
||||||
|
@ -17,30 +18,44 @@ import { smarkets } from "./smarkets";
|
||||||
import { wildeford } from "./wildeford";
|
import { wildeford } from "./wildeford";
|
||||||
import { xrisk } from "./xrisk";
|
import { xrisk } from "./xrisk";
|
||||||
|
|
||||||
export const platforms: Platform<string>[] = [
|
// function instead of const array, this helps to fight circular dependencies
|
||||||
betfair,
|
export const getPlatforms = (): Platform<string>[] => {
|
||||||
fantasyscotus,
|
return [
|
||||||
foretold,
|
betfair,
|
||||||
givewellopenphil,
|
fantasyscotus,
|
||||||
goodjudgment,
|
foretold,
|
||||||
goodjudgmentopen,
|
givewellopenphil,
|
||||||
guesstimate,
|
goodjudgment,
|
||||||
infer,
|
goodjudgmentopen,
|
||||||
kalshi,
|
guesstimate,
|
||||||
manifold,
|
infer,
|
||||||
metaculus,
|
insight,
|
||||||
polymarket,
|
kalshi,
|
||||||
predictit,
|
manifold,
|
||||||
rootclaim,
|
metaculus,
|
||||||
smarkets,
|
polymarket,
|
||||||
wildeford,
|
predictit,
|
||||||
xrisk,
|
rootclaim,
|
||||||
];
|
smarkets,
|
||||||
|
wildeford,
|
||||||
|
xrisk,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
let _nameToLabelCache: { [k: string]: string } | undefined;
|
||||||
|
export function platformNameToLabel(name: string): string {
|
||||||
|
if (!_nameToLabelCache) {
|
||||||
|
_nameToLabelCache = Object.fromEntries(
|
||||||
|
getPlatforms().map((platform) => [platform.name, platform.label])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _nameToLabelCache[name] || name;
|
||||||
|
}
|
||||||
|
|
||||||
// get frontend-safe version of platforms data
|
// get frontend-safe version of platforms data
|
||||||
|
|
||||||
export const getPlatformsConfig = (): PlatformConfig[] => {
|
export const getPlatformsConfig = (): PlatformConfig[] => {
|
||||||
const platformsConfig = platforms.map((platform) => ({
|
const platformsConfig = getPlatforms().map((platform) => ({
|
||||||
name: platform.name,
|
name: platform.name,
|
||||||
label: platform.label,
|
label: platform.label,
|
||||||
color: platform.color,
|
color: platform.color,
|
||||||
|
|
|
@ -162,7 +162,6 @@ async function processEventMarkets(event: any, ctx: Context) {
|
||||||
url: "https://smarkets.com/event/" + market.event_id + market.slug,
|
url: "https://smarkets.com/event/" + market.event_id + market.slug,
|
||||||
description: market.description,
|
description: market.description,
|
||||||
options,
|
options,
|
||||||
timestamp: new Date(),
|
|
||||||
qualityindicators: {},
|
qualityindicators: {},
|
||||||
extra: {
|
extra: {
|
||||||
contracts,
|
contracts,
|
||||||
|
|
|
@ -96,7 +96,8 @@ async function processPredictions(
|
||||||
url: prediction["url"],
|
url: prediction["url"],
|
||||||
description: prediction["Notes"] || "",
|
description: prediction["Notes"] || "",
|
||||||
options,
|
options,
|
||||||
timestamp: new Date(Date.parse(prediction["Prediction Date"] + "Z")),
|
//// TODO - use `created` field for this
|
||||||
|
// timestamp: new Date(Date.parse(prediction["Prediction Date"] + "Z")),
|
||||||
qualityindicators: {},
|
qualityindicators: {},
|
||||||
};
|
};
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -3,16 +3,19 @@ import algoliasearch from "algoliasearch";
|
||||||
import { Question } from "@prisma/client";
|
import { Question } from "@prisma/client";
|
||||||
|
|
||||||
import { prisma } from "../database/prisma";
|
import { prisma } from "../database/prisma";
|
||||||
import { platforms } from "../platforms/registry";
|
import { platformNameToLabel } from "../platforms/registry";
|
||||||
|
|
||||||
let cookie = process.env.ALGOLIA_MASTER_API_KEY || "";
|
const cookie = process.env.ALGOLIA_MASTER_API_KEY || "";
|
||||||
const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "";
|
const algoliaAppId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID || "";
|
||||||
const client = algoliasearch(algoliaAppId, cookie);
|
const client = algoliasearch(algoliaAppId, cookie);
|
||||||
const index = client.initIndex("metaforecast");
|
const index = client.initIndex("metaforecast");
|
||||||
|
|
||||||
export type AlgoliaQuestion = Omit<Question, "timestamp"> & {
|
export type AlgoliaQuestion = Omit<Question, "fetched" | "firstSeen"> & {
|
||||||
timestamp: string;
|
fetched: string;
|
||||||
|
firstSeen: string;
|
||||||
optionsstringforsearch?: string;
|
optionsstringforsearch?: string;
|
||||||
|
platformLabel: string;
|
||||||
|
objectID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getoptionsstringforsearch = (record: Question): string => {
|
const getoptionsstringforsearch = (record: Question): string => {
|
||||||
|
@ -26,23 +29,23 @@ const getoptionsstringforsearch = (record: Question): string => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const questionToAlgoliaQuestion = (
|
||||||
|
question: Question
|
||||||
|
): AlgoliaQuestion => {
|
||||||
|
return {
|
||||||
|
...question,
|
||||||
|
fetched: question.fetched.toISOString(),
|
||||||
|
firstSeen: question.firstSeen.toISOString(),
|
||||||
|
platformLabel: platformNameToLabel(question.platform),
|
||||||
|
objectID: question.id,
|
||||||
|
optionsstringforsearch: getoptionsstringforsearch(question),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export async function rebuildAlgoliaDatabase() {
|
export async function rebuildAlgoliaDatabase() {
|
||||||
const questions = await prisma.question.findMany();
|
const questions = await prisma.question.findMany();
|
||||||
|
|
||||||
const platformNameToLabel = Object.fromEntries(
|
const records: AlgoliaQuestion[] = questions.map(questionToAlgoliaQuestion);
|
||||||
platforms.map((platform) => [platform.name, platform.label])
|
|
||||||
);
|
|
||||||
|
|
||||||
const records: AlgoliaQuestion[] = questions.map(
|
|
||||||
(question, index: number) => ({
|
|
||||||
...question,
|
|
||||||
timestamp: `${question.timestamp}`,
|
|
||||||
platformLabel:
|
|
||||||
platformNameToLabel[question.platform] || question.platform,
|
|
||||||
objectID: index,
|
|
||||||
optionsstringforsearch: getoptionsstringforsearch(question),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await index.exists()) {
|
if (await index.exists()) {
|
||||||
console.log("Index exists");
|
console.log("Index exists");
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,5 +1,5 @@
|
||||||
import { prisma } from "../../backend/database/prisma";
|
import { prisma } from "../../backend/database/prisma";
|
||||||
import { platforms } from "../../backend/platforms/registry";
|
import { getPlatforms } from "../../backend/platforms/registry";
|
||||||
import { builder } from "../builder";
|
import { builder } from "../builder";
|
||||||
|
|
||||||
export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
|
@ -20,7 +20,7 @@ export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
return "Guesstimate";
|
return "Guesstimate";
|
||||||
}
|
}
|
||||||
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
|
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
|
||||||
const platform = platforms.find((p) => p.name === platformName);
|
const platform = getPlatforms().find((p) => p.name === platformName);
|
||||||
if (!platform) {
|
if (!platform) {
|
||||||
throw new Error(`Unknown platform ${platformName}`);
|
throw new Error(`Unknown platform ${platformName}`);
|
||||||
}
|
}
|
||||||
|
@ -36,10 +36,10 @@ export const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
platform: platformName,
|
platform: platformName,
|
||||||
},
|
},
|
||||||
_max: {
|
_max: {
|
||||||
timestamp: true,
|
fetched: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return res._max.timestamp;
|
return res._max.fetched;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -49,7 +49,7 @@ builder.queryField("platforms", (t) =>
|
||||||
t.field({
|
t.field({
|
||||||
type: [PlatformObj],
|
type: [PlatformObj],
|
||||||
resolve: async (parent, args) => {
|
resolve: async (parent, args) => {
|
||||||
return platforms.map((platform) => platform.name);
|
return getPlatforms().map((platform) => platform.name);
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -70,8 +70,21 @@ const QuestionShapeInterface = builder
|
||||||
}),
|
}),
|
||||||
timestamp: t.field({
|
timestamp: t.field({
|
||||||
type: "Date",
|
type: "Date",
|
||||||
description: "Timestamp at which metaforecast fetched the question",
|
description:
|
||||||
resolve: (parent) => parent.timestamp,
|
"Last timestamp at which metaforecast fetched the question",
|
||||||
|
deprecationReason: "Renamed to `fetched`",
|
||||||
|
resolve: (parent) => parent.fetched,
|
||||||
|
}),
|
||||||
|
fetched: t.field({
|
||||||
|
type: "Date",
|
||||||
|
description:
|
||||||
|
"Last timestamp at which metaforecast fetched the question",
|
||||||
|
resolve: (parent) => parent.fetched,
|
||||||
|
}),
|
||||||
|
fetchedStr: t.string({
|
||||||
|
description:
|
||||||
|
"Last timestamp at which metaforecast fetched the question, in ISO 8601 format",
|
||||||
|
resolve: (parent) => parent.fetched.toISOString(),
|
||||||
}),
|
}),
|
||||||
qualityIndicators: t.field({
|
qualityIndicators: t.field({
|
||||||
type: QualityIndicatorsObj,
|
type: QualityIndicatorsObj,
|
||||||
|
@ -114,10 +127,20 @@ export const QuestionObj = builder.prismaObject("Question", {
|
||||||
resolve: (parent) => (parent.extra as any)?.visualization, // used for guesstimate only, see searchGuesstimate.ts
|
resolve: (parent) => (parent.extra as any)?.visualization, // used for guesstimate only, see searchGuesstimate.ts
|
||||||
nullable: true,
|
nullable: true,
|
||||||
}),
|
}),
|
||||||
|
firstSeen: t.field({
|
||||||
|
type: "Date",
|
||||||
|
description: "First timestamp at which metaforecast fetched the question",
|
||||||
|
resolve: (parent) => parent.firstSeen,
|
||||||
|
}),
|
||||||
|
firstSeenStr: t.string({
|
||||||
|
description:
|
||||||
|
"First timestamp at which metaforecast fetched the question, in ISO 8601 format",
|
||||||
|
resolve: (parent) => parent.firstSeen.toISOString(),
|
||||||
|
}),
|
||||||
history: t.relation("history", {
|
history: t.relation("history", {
|
||||||
query: () => ({
|
query: () => ({
|
||||||
orderBy: {
|
orderBy: {
|
||||||
timestamp: "asc",
|
fetched: "asc",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -130,7 +153,21 @@ builder.queryField("questions", (t) =>
|
||||||
type: "Question",
|
type: "Question",
|
||||||
cursor: "id",
|
cursor: "id",
|
||||||
maxSize: 1000,
|
maxSize: 1000,
|
||||||
resolve: (query) => prisma.question.findMany({ ...query }),
|
args: {
|
||||||
|
orderBy: t.arg({
|
||||||
|
type: builder.enumType("QuestionsOrderBy", {
|
||||||
|
values: ["FIRST_SEEN_DESC"] as const,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
resolve: (query, parent, args) => {
|
||||||
|
return prisma.question.findMany({
|
||||||
|
...query,
|
||||||
|
...(args.orderBy === "FIRST_SEEN_DESC"
|
||||||
|
? { orderBy: [{ firstSeen: "desc" }, { id: "asc" }] }
|
||||||
|
: {}), // TODO - explicit default order?
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
{}
|
{}
|
||||||
|
|
|
@ -63,7 +63,8 @@ builder.queryField("searchQuestions", (t) =>
|
||||||
|
|
||||||
return results.map((q) => ({
|
return results.map((q) => ({
|
||||||
...q,
|
...q,
|
||||||
timestamp: new Date(q.timestamp),
|
fetched: new Date(q.fetched),
|
||||||
|
firstSeen: new Date(q.firstSeen),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -46,6 +46,10 @@ export type Dashboard = {
|
||||||
export type History = QuestionShape & {
|
export type History = QuestionShape & {
|
||||||
__typename?: 'History';
|
__typename?: 'History';
|
||||||
description: Scalars['String'];
|
description: Scalars['String'];
|
||||||
|
/** Last timestamp at which metaforecast fetched the question */
|
||||||
|
fetched: Scalars['Date'];
|
||||||
|
/** Last timestamp at which metaforecast fetched the question, in ISO 8601 format */
|
||||||
|
fetchedStr: Scalars['String'];
|
||||||
/** History items are identified by their integer ids */
|
/** History items are identified by their integer ids */
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
options: Array<ProbabilityOption>;
|
options: Array<ProbabilityOption>;
|
||||||
|
@ -53,7 +57,10 @@ export type History = QuestionShape & {
|
||||||
qualityIndicators: QualityIndicators;
|
qualityIndicators: QualityIndicators;
|
||||||
/** Unique string which identifies the question */
|
/** Unique string which identifies the question */
|
||||||
questionId: Scalars['ID'];
|
questionId: Scalars['ID'];
|
||||||
/** Timestamp at which metaforecast fetched the question */
|
/**
|
||||||
|
* Last timestamp at which metaforecast fetched the question
|
||||||
|
* @deprecated Renamed to `fetched`
|
||||||
|
*/
|
||||||
timestamp: Scalars['Date'];
|
timestamp: Scalars['Date'];
|
||||||
title: Scalars['String'];
|
title: Scalars['String'];
|
||||||
/** Non-unique, a very small number of platforms have a page for more than one prediction */
|
/** Non-unique, a very small number of platforms have a page for more than one prediction */
|
||||||
|
@ -114,12 +121,12 @@ export type QualityIndicators = {
|
||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
/** Look up a single dashboard by its id */
|
/** Look up a single dashboard by its id */
|
||||||
dashboard: Dashboard;
|
dashboard?: Maybe<Dashboard>;
|
||||||
/** Get a list of questions that are currently on the frontpage */
|
/** Get a list of questions that are currently on the frontpage */
|
||||||
frontpage: Array<Question>;
|
frontpage: Array<Question>;
|
||||||
platforms: Array<Platform>;
|
platforms: Array<Platform>;
|
||||||
/** Look up a single question by its id */
|
/** Look up a single question by its id */
|
||||||
question: Question;
|
question?: Maybe<Question>;
|
||||||
questions: QueryQuestionsConnection;
|
questions: QueryQuestionsConnection;
|
||||||
/** Search for questions; uses Algolia instead of the primary metaforecast database */
|
/** Search for questions; uses Algolia instead of the primary metaforecast database */
|
||||||
searchQuestions: Array<Question>;
|
searchQuestions: Array<Question>;
|
||||||
|
@ -141,6 +148,7 @@ export type QueryQuestionsArgs = {
|
||||||
before?: InputMaybe<Scalars['String']>;
|
before?: InputMaybe<Scalars['String']>;
|
||||||
first?: InputMaybe<Scalars['Int']>;
|
first?: InputMaybe<Scalars['Int']>;
|
||||||
last?: InputMaybe<Scalars['Int']>;
|
last?: InputMaybe<Scalars['Int']>;
|
||||||
|
orderBy?: InputMaybe<QuestionsOrderBy>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,13 +171,24 @@ export type QueryQuestionsConnectionEdge = {
|
||||||
export type Question = QuestionShape & {
|
export type Question = QuestionShape & {
|
||||||
__typename?: 'Question';
|
__typename?: 'Question';
|
||||||
description: Scalars['String'];
|
description: Scalars['String'];
|
||||||
|
/** Last timestamp at which metaforecast fetched the question */
|
||||||
|
fetched: Scalars['Date'];
|
||||||
|
/** Last timestamp at which metaforecast fetched the question, in ISO 8601 format */
|
||||||
|
fetchedStr: Scalars['String'];
|
||||||
|
/** First timestamp at which metaforecast fetched the question */
|
||||||
|
firstSeen: Scalars['Date'];
|
||||||
|
/** First timestamp at which metaforecast fetched the question, in ISO 8601 format */
|
||||||
|
firstSeenStr: Scalars['String'];
|
||||||
history: Array<History>;
|
history: Array<History>;
|
||||||
/** Unique string which identifies the question */
|
/** Unique string which identifies the question */
|
||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
options: Array<ProbabilityOption>;
|
options: Array<ProbabilityOption>;
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
qualityIndicators: QualityIndicators;
|
qualityIndicators: QualityIndicators;
|
||||||
/** Timestamp at which metaforecast fetched the question */
|
/**
|
||||||
|
* Last timestamp at which metaforecast fetched the question
|
||||||
|
* @deprecated Renamed to `fetched`
|
||||||
|
*/
|
||||||
timestamp: Scalars['Date'];
|
timestamp: Scalars['Date'];
|
||||||
title: Scalars['String'];
|
title: Scalars['String'];
|
||||||
/** Non-unique, a very small number of platforms have a page for more than one prediction */
|
/** Non-unique, a very small number of platforms have a page for more than one prediction */
|
||||||
|
@ -179,16 +198,27 @@ export type Question = QuestionShape & {
|
||||||
|
|
||||||
export type QuestionShape = {
|
export type QuestionShape = {
|
||||||
description: Scalars['String'];
|
description: Scalars['String'];
|
||||||
|
/** Last timestamp at which metaforecast fetched the question */
|
||||||
|
fetched: Scalars['Date'];
|
||||||
|
/** Last timestamp at which metaforecast fetched the question, in ISO 8601 format */
|
||||||
|
fetchedStr: Scalars['String'];
|
||||||
options: Array<ProbabilityOption>;
|
options: Array<ProbabilityOption>;
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
qualityIndicators: QualityIndicators;
|
qualityIndicators: QualityIndicators;
|
||||||
/** Timestamp at which metaforecast fetched the question */
|
/**
|
||||||
|
* Last timestamp at which metaforecast fetched the question
|
||||||
|
* @deprecated Renamed to `fetched`
|
||||||
|
*/
|
||||||
timestamp: Scalars['Date'];
|
timestamp: Scalars['Date'];
|
||||||
title: Scalars['String'];
|
title: Scalars['String'];
|
||||||
/** Non-unique, a very small number of platforms have a page for more than one prediction */
|
/** Non-unique, a very small number of platforms have a page for more than one prediction */
|
||||||
url: Scalars['String'];
|
url: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum QuestionsOrderBy {
|
||||||
|
FirstSeenDesc = 'FIRST_SEEN_DESC'
|
||||||
|
}
|
||||||
|
|
||||||
export type SearchInput = {
|
export type SearchInput = {
|
||||||
/** List of platform ids to filter by */
|
/** List of platform ids to filter by */
|
||||||
forecastingPlatforms?: InputMaybe<Array<Scalars['String']>>;
|
forecastingPlatforms?: InputMaybe<Array<Scalars['String']>>;
|
||||||
|
|
|
@ -17,12 +17,14 @@ Router.events.on("routeChangeStart", (as, { shallow }) => {
|
||||||
Router.events.on("routeChangeComplete", () => NProgress.done());
|
Router.events.on("routeChangeComplete", () => NProgress.done());
|
||||||
Router.events.on("routeChangeError", () => NProgress.done());
|
Router.events.on("routeChangeError", () => NProgress.done());
|
||||||
|
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }: AppProps) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<PlausibleProvider domain="metaforecast.org">
|
<PlausibleProvider domain="metaforecast.org">
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</PlausibleProvider>
|
</PlausibleProvider>
|
||||||
);
|
);
|
||||||
|
// Workaround in package.json for: https://github.com/vercel/next.js/issues/36019#issuecomment-1103266481
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withUrqlClient((ssr) => getUrqlClientOptions(ssr), {
|
export default withUrqlClient((ssr) => getUrqlClientOptions(ssr), {
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import {NextApiRequest, NextApiResponse} from "next";
|
||||||
|
|
||||||
// apollo-server-micro is problematic since v3, see https://github.com/apollographql/apollo-server/issues/5547, so we use graphql-yoga instead
|
// apollo-server-micro is problematic since v3, see https://github.com/apollographql/apollo-server/issues/5547, so we use graphql-yoga instead
|
||||||
import { createServer } from "@graphql-yoga/node";
|
import {createYoga} from "graphql-yoga";
|
||||||
|
import {useResponseCache} from '@graphql-yoga/plugin-response-cache'
|
||||||
|
|
||||||
import { schema } from "../../graphql/schema";
|
import {schema} from "../../graphql/schema";
|
||||||
|
|
||||||
const server = createServer<{
|
const server = createYoga < {
|
||||||
req: NextApiRequest;
|
req: NextApiRequest;
|
||||||
res: NextApiResponse;
|
res: NextApiResponse;
|
||||||
}>({ schema });
|
} > ({
|
||||||
|
schema,
|
||||||
|
graphqlEndpoint: '/api/graphql',
|
||||||
|
plugins: [useResponseCache(
|
||||||
|
{ // global cache
|
||||||
|
session: () => null,
|
||||||
|
ttl: 2 * 60 * 60 * 1000,
|
||||||
|
// ^ 2h * 60 mins per hour, 60 seconds per min 1000 miliseconds per second
|
||||||
|
}
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
|
||||||
export default server;
|
export default server;
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
props: {
|
props: {
|
||||||
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
|
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
|
||||||
urqlState: ssrCache.extractData(),
|
urqlState: ssrCache.extractData(),
|
||||||
dashboard,
|
dashboard: dashboard || undefined,
|
||||||
numCols: !numCols ? undefined : numCols < 5 ? numCols : 4,
|
numCols: !numCols ? undefined : numCols < 5 ? numCols : 4,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
props: {
|
props: {
|
||||||
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
|
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
|
||||||
urqlState: ssrCache.extractData(),
|
urqlState: ssrCache.extractData(),
|
||||||
dashboard,
|
dashboard: dashboard || undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getPlatformsConfig, platforms } from "../backend/platforms/registry";
|
import { getPlatforms, getPlatformsConfig } from "../backend/platforms/registry";
|
||||||
import { Layout } from "../web/common/Layout";
|
import { Layout } from "../web/common/Layout";
|
||||||
import { Props, QueryParameters, SearchScreen } from "../web/search/components/SearchScreen";
|
import { Props, QueryParameters, SearchScreen } from "../web/search/components/SearchScreen";
|
||||||
import { FrontpageDocument, SearchDocument } from "../web/search/queries.generated";
|
import { FrontpageDocument, SearchDocument } from "../web/search/queries.generated";
|
||||||
|
@ -19,7 +19,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
query: "",
|
query: "",
|
||||||
starsThreshold: 2,
|
starsThreshold: 2,
|
||||||
forecastsThreshold: 0,
|
forecastsThreshold: 0,
|
||||||
forecastingPlatforms: platforms.map((platform) => platform.name),
|
forecastingPlatforms: getPlatforms().map((platform) => platform.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialQueryParameters: QueryParameters = {
|
const initialQueryParameters: QueryParameters = {
|
||||||
|
|
4
src/pages/questions/embed/[id].tsx
Normal file
4
src/pages/questions/embed/[id].tsx
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export {
|
||||||
|
default,
|
||||||
|
getServerSideProps,
|
||||||
|
} from "../../../web/questions/pages/EmbedQuestionPage";
|
|
@ -3,7 +3,7 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { platforms } from "../backend/platforms/registry";
|
import { getPlatforms } from "../backend/platforms/registry";
|
||||||
import { QuestionFragment } from "../web/fragments.generated";
|
import { QuestionFragment } from "../web/fragments.generated";
|
||||||
import { QuestionCard } from "../web/questions/components/QuestionCard";
|
import { QuestionCard } from "../web/questions/components/QuestionCard";
|
||||||
import { SearchDocument } from "../web/search/queries.generated";
|
import { SearchDocument } from "../web/search/queries.generated";
|
||||||
|
@ -23,7 +23,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
query: "",
|
query: "",
|
||||||
starsThreshold: 2,
|
starsThreshold: 2,
|
||||||
forecastsThreshold: 0,
|
forecastsThreshold: 0,
|
||||||
forecastingPlatforms: platforms.map((platform) => platform.name),
|
forecastingPlatforms: getPlatforms().map((platform) => platform.name),
|
||||||
...urlQuery,
|
...urlQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,6 +65,7 @@ const SecretEmbedPage: NextPage<Props> = ({ results }) => {
|
||||||
question={result}
|
question={result}
|
||||||
showTimeStamp={true}
|
showTimeStamp={true}
|
||||||
expandFooterToFullWidth={true}
|
expandFooterToFullWidth={true}
|
||||||
|
showExpandButton={false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
23
src/web/common/BoxedLink.tsx
Normal file
23
src/web/common/BoxedLink.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { FaExternalLinkAlt } from "react-icons/fa";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string;
|
||||||
|
size?: "normal" | "small";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoxedLink: React.FC<Props> = ({
|
||||||
|
url,
|
||||||
|
size = "normal",
|
||||||
|
children,
|
||||||
|
}) => (
|
||||||
|
<a
|
||||||
|
className={`px-2 py-1 border-2 border-gray-400 rounded-lg text-black no-underline hover:bg-gray-100 inline-flex flex-nowrap space-x-1 items-center text-xs md:text-lg ${
|
||||||
|
size === "small" ? "text-sm" : ""
|
||||||
|
}`}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<span>{children}</span>
|
||||||
|
<FaExternalLinkAlt className="text-gray-400 inline " />
|
||||||
|
</a>
|
||||||
|
);
|
44
src/web/common/Collapsible.tsx
Normal file
44
src/web/common/Collapsible.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FaCaretDown, FaCaretRight } from "react-icons/fa";
|
||||||
|
|
||||||
|
export const Collapsible: React.FC<{
|
||||||
|
title: string;
|
||||||
|
children: () => React.ReactElement | null;
|
||||||
|
}> = ({ title, children }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const expand = (e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const collapse = (e: React.SyntheticEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="decoration-dashed inline-flex items-center"
|
||||||
|
onClick={collapse}
|
||||||
|
>
|
||||||
|
{title} <FaCaretDown />
|
||||||
|
</a>
|
||||||
|
<div>{children()}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="decoration-dashed inline-flex items-center"
|
||||||
|
onClick={expand}
|
||||||
|
>
|
||||||
|
{title} <FaCaretRight />
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -11,7 +11,7 @@ export const Spinner: React.FC = () => (
|
||||||
cy="12"
|
cy="12"
|
||||||
r="10"
|
r="10"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="4"
|
strokeWidth="4"
|
||||||
></circle>
|
></circle>
|
||||||
<path
|
<path
|
||||||
className="opacity-75"
|
className="opacity-75"
|
||||||
|
|
|
@ -2,21 +2,21 @@ import * as Types from '../../graphql/types.generated';
|
||||||
|
|
||||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||||
import { QuestionFragmentDoc } from '../fragments.generated';
|
import { QuestionFragmentDoc } from '../fragments.generated';
|
||||||
export type DashboardFragment = { __typename?: 'Dashboard', id: string, title: string, description: string, creator: string, questions: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> };
|
export type DashboardFragment = { __typename?: 'Dashboard', id: string, title: string, description: string, creator: string, questions: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> };
|
||||||
|
|
||||||
export type DashboardByIdQueryVariables = Types.Exact<{
|
export type DashboardByIdQueryVariables = Types.Exact<{
|
||||||
id: Types.Scalars['ID'];
|
id: Types.Scalars['ID'];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type DashboardByIdQuery = { __typename?: 'Query', result: { __typename?: 'Dashboard', id: string, title: string, description: string, creator: string, questions: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> } };
|
export type DashboardByIdQuery = { __typename?: 'Query', result?: { __typename?: 'Dashboard', id: string, title: string, description: string, creator: string, questions: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> } | null };
|
||||||
|
|
||||||
export type CreateDashboardMutationVariables = Types.Exact<{
|
export type CreateDashboardMutationVariables = Types.Exact<{
|
||||||
input: Types.CreateDashboardInput;
|
input: Types.CreateDashboardInput;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type CreateDashboardMutation = { __typename?: 'Mutation', result: { __typename?: 'CreateDashboardResult', dashboard: { __typename?: 'Dashboard', id: string, title: string, description: string, creator: string, questions: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> } } };
|
export type CreateDashboardMutation = { __typename?: 'Mutation', result: { __typename?: 'CreateDashboardResult', dashboard: { __typename?: 'Dashboard', id: string, title: string, description: string, creator: string, questions: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> } } };
|
||||||
|
|
||||||
export const DashboardFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Dashboard"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Dashboard"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"creator"}},{"kind":"Field","name":{"kind":"Name","value":"questions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<DashboardFragment, unknown>;
|
export const DashboardFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Dashboard"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Dashboard"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"creator"}},{"kind":"Field","name":{"kind":"Name","value":"questions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<DashboardFragment, unknown>;
|
||||||
export const DashboardByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DashboardById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"dashboard"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Dashboard"}}]}}]}},...DashboardFragmentDoc.definitions]} as unknown as DocumentNode<DashboardByIdQuery, DashboardByIdQueryVariables>;
|
export const DashboardByIdDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DashboardById"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"dashboard"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Dashboard"}}]}}]}},...DashboardFragmentDoc.definitions]} as unknown as DocumentNode<DashboardByIdQuery, DashboardByIdQueryVariables>;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import * as Types from '../graphql/types.generated';
|
import * as Types from '../graphql/types.generated';
|
||||||
|
|
||||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||||
export type QuestionFragment = { __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } };
|
export type QuestionFragment = { __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } };
|
||||||
|
|
||||||
export type QuestionWithHistoryFragment = { __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, history: Array<{ __typename?: 'History', timestamp: number, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }> }>, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } };
|
export type QuestionWithHistoryFragment = { __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, history: Array<{ __typename?: 'History', fetched: number, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }> }>, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } };
|
||||||
|
|
||||||
export const QuestionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Question"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Question"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"probability"}}]}},{"kind":"Field","name":{"kind":"Name","value":"platform"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"qualityIndicators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stars"}},{"kind":"Field","name":{"kind":"Name","value":"numForecasts"}},{"kind":"Field","name":{"kind":"Name","value":"numForecasters"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"spread"}},{"kind":"Field","name":{"kind":"Name","value":"sharesVolume"}},{"kind":"Field","name":{"kind":"Name","value":"openInterest"}},{"kind":"Field","name":{"kind":"Name","value":"liquidity"}},{"kind":"Field","name":{"kind":"Name","value":"tradeVolume"}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualization"}}]}}]} as unknown as DocumentNode<QuestionFragment, unknown>;
|
export const QuestionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Question"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Question"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"fetched"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"probability"}}]}},{"kind":"Field","name":{"kind":"Name","value":"platform"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"qualityIndicators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stars"}},{"kind":"Field","name":{"kind":"Name","value":"numForecasts"}},{"kind":"Field","name":{"kind":"Name","value":"numForecasters"}},{"kind":"Field","name":{"kind":"Name","value":"volume"}},{"kind":"Field","name":{"kind":"Name","value":"spread"}},{"kind":"Field","name":{"kind":"Name","value":"sharesVolume"}},{"kind":"Field","name":{"kind":"Name","value":"openInterest"}},{"kind":"Field","name":{"kind":"Name","value":"liquidity"}},{"kind":"Field","name":{"kind":"Name","value":"tradeVolume"}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualization"}}]}}]} as unknown as DocumentNode<QuestionFragment, unknown>;
|
||||||
export const QuestionWithHistoryFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuestionWithHistory"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Question"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"probability"}}]}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<QuestionWithHistoryFragment, unknown>;
|
export const QuestionWithHistoryFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"QuestionWithHistory"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Question"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fetched"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"probability"}}]}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<QuestionWithHistoryFragment, unknown>;
|
|
@ -3,7 +3,7 @@ fragment Question on Question {
|
||||||
url
|
url
|
||||||
title
|
title
|
||||||
description
|
description
|
||||||
timestamp
|
fetched
|
||||||
options {
|
options {
|
||||||
name
|
name
|
||||||
probability
|
probability
|
||||||
|
@ -29,7 +29,7 @@ fragment Question on Question {
|
||||||
fragment QuestionWithHistory on Question {
|
fragment QuestionWithHistory on Question {
|
||||||
...Question
|
...Question
|
||||||
history {
|
history {
|
||||||
timestamp
|
fetched
|
||||||
options {
|
options {
|
||||||
name
|
name
|
||||||
probability
|
probability
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
|
import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
|
||||||
|
import { Resizable } from "re-resizable";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "../../common/Button";
|
import { Button } from "../../common/Button";
|
||||||
import { CopyParagraph } from "../../common/CopyParagraph";
|
import { CopyParagraph } from "../../common/CopyParagraph";
|
||||||
import { QuestionFragment } from "../../fragments.generated";
|
import { QuestionFragment } from "../../fragments.generated";
|
||||||
|
@ -79,6 +79,15 @@ const MetaculusSource: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GrayContainer: React.FC<{ title: string }> = ({ title, children }) => (
|
||||||
|
<div className="bg-gray-100 p-2 space-y-1">
|
||||||
|
<div className="uppercase text-xs font-bold tracking-wide text-gray-600">
|
||||||
|
{title}:
|
||||||
|
</div>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
question: QuestionFragment;
|
question: QuestionFragment;
|
||||||
}
|
}
|
||||||
|
@ -118,35 +127,55 @@ export const CaptureQuestion: React.FC<Props> = ({ question }) => {
|
||||||
await exportAsPictureAndCode();
|
await exportAsPictureAndCode();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (imgSrc) {
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 place-items-center">
|
return (
|
||||||
<div ref={containerRef}>
|
<div className="space-y-4">
|
||||||
<QuestionCard
|
<GrayContainer title="Generated image">
|
||||||
question={question}
|
<a href={imgSrc} target="_blank">
|
||||||
showTimeStamp={true}
|
|
||||||
showExpandButton={false}
|
|
||||||
expandFooterToFullWidth={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button onClick={onCaptureButtonClick}>{mainButtonText}</Button>
|
|
||||||
</div>
|
|
||||||
{imgSrc ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<img src={imgSrc} />
|
<img src={imgSrc} />
|
||||||
|
</a>
|
||||||
|
</GrayContainer>
|
||||||
|
<div>
|
||||||
|
<ImageSource question={question} imgSrc={imgSrc} />
|
||||||
|
</div>
|
||||||
|
{question.platform.id === "metaculus" ? (
|
||||||
|
<>
|
||||||
|
<div className="justify-self-stretch">
|
||||||
|
<MetaculusEmbed question={question} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<MetaculusSource question={question} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2 flex flex-col">
|
||||||
|
<GrayContainer title="Resizable preview">
|
||||||
|
<Resizable
|
||||||
|
minWidth={320}
|
||||||
|
bounds="window"
|
||||||
|
enable={{ right: true, left: true }}
|
||||||
|
>
|
||||||
|
<div ref={containerRef}>
|
||||||
|
<QuestionCard
|
||||||
|
container={(children) => (
|
||||||
|
<div className="px-4 py-3 bg-white">{children}</div>
|
||||||
|
)}
|
||||||
|
question={question}
|
||||||
|
showTimeStamp={true}
|
||||||
|
showExpandButton={false}
|
||||||
|
expandFooterToFullWidth={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</Resizable>
|
||||||
<ImageSource question={question} imgSrc={imgSrc} />
|
</GrayContainer>
|
||||||
</div>
|
<Button onClick={onCaptureButtonClick} size="small">
|
||||||
<div className="justify-self-stretch">
|
{mainButtonText}
|
||||||
<MetaculusEmbed question={question} />
|
</Button>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<MetaculusSource question={question} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,27 +3,33 @@ import {
|
||||||
VictoryAxis,
|
VictoryAxis,
|
||||||
VictoryChart,
|
VictoryChart,
|
||||||
VictoryGroup,
|
VictoryGroup,
|
||||||
|
VictoryStack,
|
||||||
VictoryLabel,
|
VictoryLabel,
|
||||||
VictoryLine,
|
VictoryLine,
|
||||||
VictoryScatter,
|
VictoryScatter,
|
||||||
|
VictoryArea,
|
||||||
VictoryTheme,
|
VictoryTheme,
|
||||||
VictoryTooltip,
|
VictoryTooltip,
|
||||||
VictoryVoronoiContainer,
|
VictoryVoronoiContainer,
|
||||||
} from "victory";
|
} from "victory";
|
||||||
|
|
||||||
import { chartColors, ChartData, ChartSeries, height, width } from "./utils";
|
import { chartColors, ChartData, ChartSeries, goldenRatio } from "./utils";
|
||||||
|
|
||||||
let dateFormat = "MMM do y"; // "yyyy-MM-dd"
|
const height = 200
|
||||||
|
const width = 200 * goldenRatio
|
||||||
|
let dateFormat = "dd/MM/yy"; // "yyyy-MM-dd" // "MMM do yy"
|
||||||
|
|
||||||
// can't be replaced with React component, VictoryChart requires VictoryGroup elements to be immediate children
|
// can't be replaced with React component, VictoryChart requires VictoryGroup elements to be immediate children
|
||||||
const getVictoryGroup = ({
|
const getVictoryGroup = ({
|
||||||
data,
|
data,
|
||||||
i,
|
i,
|
||||||
highlight,
|
highlight,
|
||||||
|
isBinary,
|
||||||
}: {
|
}: {
|
||||||
data: ChartSeries;
|
data: ChartSeries;
|
||||||
i: number;
|
i: number;
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
|
isBinary?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<VictoryGroup color={chartColors[i] || "darkgray"} data={data} key={i}>
|
<VictoryGroup color={chartColors[i] || "darkgray"} data={data} key={i}>
|
||||||
|
@ -31,13 +37,25 @@ const getVictoryGroup = ({
|
||||||
name={`line-${i}`}
|
name={`line-${i}`}
|
||||||
style={{
|
style={{
|
||||||
data: {
|
data: {
|
||||||
strokeOpacity: highlight ? 1 : 0.5,
|
// strokeOpacity: highlight ? 1 : 0.5,
|
||||||
|
strokeOpacity: highlight && !isBinary ? 0.8 : 0.6,
|
||||||
|
strokeWidth: highlight && !isBinary ? 2.5 : 1.5,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{isBinary ? (
|
||||||
|
<VictoryArea
|
||||||
|
standalone={false}
|
||||||
|
style={{
|
||||||
|
data: { fill: chartColors[i], fillOpacity: 0.1, strokeOpacity: 0 },
|
||||||
|
}}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<VictoryScatter
|
<VictoryScatter
|
||||||
name={`scatter-${i}`}
|
name={`scatter-${i}`}
|
||||||
size={({ active }) => (active || highlight ? 3.75 : 3)}
|
size={({ active }) => (active || highlight ? 0 : 0)} //(active || highlight ? 3.75 : 3)}
|
||||||
/>
|
/>
|
||||||
</VictoryGroup>
|
</VictoryGroup>
|
||||||
);
|
);
|
||||||
|
@ -55,15 +73,18 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
const domainMax =
|
const domainMax =
|
||||||
maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1;
|
maxProbability < 0.5 ? Math.round(10 * (maxProbability + 0.05)) / 10 : 1;
|
||||||
const padding = {
|
const padding = {
|
||||||
top: 20,
|
top: 12,
|
||||||
bottom: 65,
|
bottom: 33,
|
||||||
left: 60,
|
left: 30,
|
||||||
right: 5,
|
right: 17,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBinary = seriesList.length == 1;
|
||||||
|
console.log(isBinary);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VictoryChart
|
<VictoryChart
|
||||||
domainPadding={20}
|
domainPadding={{ x: 0 }}
|
||||||
padding={padding}
|
padding={padding}
|
||||||
theme={VictoryTheme.material}
|
theme={VictoryTheme.material}
|
||||||
height={height}
|
height={height}
|
||||||
|
@ -80,12 +101,12 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
<VictoryLabel
|
<VictoryLabel
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
fontSize: 16,
|
fontSize: 10,
|
||||||
fill: "black",
|
fill: "black",
|
||||||
strokeWidth: 0.05,
|
strokeWidth: 0.05,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fontSize: 16,
|
fontSize: 10,
|
||||||
fill: "#777",
|
fill: "#777",
|
||||||
strokeWidth: 0.05,
|
strokeWidth: 0.05,
|
||||||
},
|
},
|
||||||
|
@ -99,7 +120,7 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
)}`
|
)}`
|
||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 17, // needs to be set here and not just in labelComponent for text size calculations
|
fontSize: 10, // needs to be set here and not just in labelComponent for text size calculations
|
||||||
fontFamily:
|
fontFamily:
|
||||||
'"Gill Sans", "Gill Sans MT", "Seravek", "Trebuchet MS", sans-serif',
|
'"Gill Sans", "Gill Sans MT", "Seravek", "Trebuchet MS", sans-serif',
|
||||||
// default font family from Victory, need to be specified explicitly for some reason, otherwise text size gets miscalculated
|
// default font family from Victory, need to be specified explicitly for some reason, otherwise text size gets miscalculated
|
||||||
|
@ -109,10 +130,10 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
fill: "white",
|
fill: "white",
|
||||||
}}
|
}}
|
||||||
cornerRadius={4}
|
cornerRadius={4}
|
||||||
flyoutPadding={{ top: 4, bottom: 4, left: 16, right: 16 }}
|
flyoutPadding={{ top: 4, bottom: 4, left: 10, right: 10 }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
radius={50}
|
radius={20}
|
||||||
voronoiBlacklist={
|
voronoiBlacklist={
|
||||||
[...Array(seriesList.length).keys()].map((i) => `line-${i}`)
|
[...Array(seriesList.length).keys()].map((i) => `line-${i}`)
|
||||||
// see: https://github.com/FormidableLabs/victory/issues/545
|
// see: https://github.com/FormidableLabs/victory/issues/545
|
||||||
|
@ -128,6 +149,11 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
y: [0, domainMax],
|
y: [0, domainMax],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
// Note: axis is not in fact unaligned. Fetchers are at ~12:00
|
||||||
|
// whereas the date is at the beginning of the day
|
||||||
|
// however, it still doesn't look very pretty.
|
||||||
|
}
|
||||||
<VictoryAxis
|
<VictoryAxis
|
||||||
tickCount={Math.min(7, differenceInDays(maxDate, minDate) + 1)}
|
tickCount={Math.min(7, differenceInDays(maxDate, minDate) + 1)}
|
||||||
style={{
|
style={{
|
||||||
|
@ -135,10 +161,10 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
}}
|
}}
|
||||||
tickLabelComponent={
|
tickLabelComponent={
|
||||||
<VictoryLabel
|
<VictoryLabel
|
||||||
dx={-38}
|
dx={-10}
|
||||||
dy={-5}
|
dy={0}
|
||||||
angle={-30}
|
angle={-30}
|
||||||
style={{ fontSize: 15, fill: "#777" }}
|
style={{ fontSize: 9, fill: "#777" }}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
scale={{ x: "time" }}
|
scale={{ x: "time" }}
|
||||||
|
@ -150,29 +176,38 @@ export const InnerChart: React.FC<Props> = ({
|
||||||
grid: { stroke: "#D3D3D3", strokeWidth: 0.5 },
|
grid: { stroke: "#D3D3D3", strokeWidth: 0.5 },
|
||||||
}}
|
}}
|
||||||
tickLabelComponent={
|
tickLabelComponent={
|
||||||
<VictoryLabel dy={0} style={{ fontSize: 18, fill: "#777" }} />
|
<VictoryLabel dy={0} dx={5} style={{ fontSize: 9, fill: "#777" }} />
|
||||||
}
|
}
|
||||||
// tickFormat specifies how ticks should be displayed
|
// tickFormat specifies how ticks should be displayed
|
||||||
tickFormat={(x) => `${x * 100}%`}
|
tickFormat={(x) => `${x * 100}%`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{[...Array(seriesList.length).keys()]
|
{[...Array(seriesList.length).keys()]
|
||||||
.reverse() // affects svg render order, we want to render largest datasets on top of others
|
.reverse() // affects svg render order, we want to render largest datasets on top of others
|
||||||
.filter((i) => i !== highlight)
|
//.filter((i) => i !== highlight)
|
||||||
.map((i) =>
|
.map((i) =>
|
||||||
getVictoryGroup({
|
getVictoryGroup({
|
||||||
data: seriesList[i],
|
data: seriesList[i],
|
||||||
i,
|
i,
|
||||||
highlight: false,
|
highlight: i == highlight, // false
|
||||||
|
isBinary: isBinary,
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
{highlight === undefined
|
|
||||||
? null
|
{
|
||||||
: // render highlighted series on top of everything else
|
// note: this produces an annoying change of color effect
|
||||||
getVictoryGroup({
|
/*
|
||||||
data: seriesList[highlight],
|
highlight === undefined
|
||||||
i: highlight,
|
? null
|
||||||
highlight: true,
|
: // render highlighted series on top of everything else
|
||||||
})}
|
getVictoryGroup({
|
||||||
|
data: seriesList[highlight],
|
||||||
|
i: highlight,
|
||||||
|
highlight: true,
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
</VictoryChart>
|
</VictoryChart>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const HistoryChart: React.FC<Props> = ({ question }) => {
|
||||||
const data = useMemo(() => buildChartData(question), [question]);
|
const data = useMemo(() => buildChartData(question), [question]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center flex-col space-y-4 sm:flex-row sm:space-y-0">
|
<div className="flex items-center space-y-4 sm:flex-row sm:space-y-0 ">
|
||||||
<InnerChart data={data} highlight={highlight} />
|
<InnerChart data={data} highlight={highlight} />
|
||||||
<Legend
|
<Legend
|
||||||
items={data.seriesNames.map((name, i) => ({
|
items={data.seriesNames.map((name, i) => ({
|
||||||
|
|
|
@ -18,7 +18,7 @@ export const chartColors = [
|
||||||
"#F59E0B", // amber-500
|
"#F59E0B", // amber-500
|
||||||
];
|
];
|
||||||
|
|
||||||
const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
export const goldenRatio = (1 + Math.sqrt(5)) / 2;
|
||||||
// used both for chart and for ssr placeholder
|
// used both for chart and for ssr placeholder
|
||||||
export const width = 750;
|
export const width = 750;
|
||||||
export const height = width / goldenRatio;
|
export const height = width / goldenRatio;
|
||||||
|
@ -58,16 +58,16 @@ export const buildChartData = (
|
||||||
let seriesList: ChartSeries[] = [...Array(seriesNames.length)].map((x) => []);
|
let seriesList: ChartSeries[] = [...Array(seriesNames.length)].map((x) => []);
|
||||||
|
|
||||||
const sortedHistory = question.history.sort((a, b) =>
|
const sortedHistory = question.history.sort((a, b) =>
|
||||||
a.timestamp < b.timestamp ? -1 : 1
|
a.fetched < b.fetched ? -1 : 1
|
||||||
);
|
);
|
||||||
|
|
||||||
{
|
{
|
||||||
let previousDate = -Infinity;
|
let previousDate = -Infinity;
|
||||||
for (const item of sortedHistory) {
|
for (const item of sortedHistory) {
|
||||||
if (item.timestamp - previousDate < 12 * 60 * 60) {
|
if (item.fetched - previousDate < 12 * 60 * 60) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const date = new Date(item.timestamp * 1000);
|
const date = new Date(item.fetched * 1000);
|
||||||
|
|
||||||
for (const option of item.options) {
|
for (const option of item.options) {
|
||||||
if (option.name == null || option.probability == null) {
|
if (option.name == null || option.probability == null) {
|
||||||
|
@ -84,7 +84,7 @@ export const buildChartData = (
|
||||||
};
|
};
|
||||||
seriesList[idx].push(result);
|
seriesList[idx].push(result);
|
||||||
}
|
}
|
||||||
previousDate = item.timestamp;
|
previousDate = item.fetched;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,12 +96,12 @@ export const buildChartData = (
|
||||||
}
|
}
|
||||||
|
|
||||||
const minDate = sortedHistory.length
|
const minDate = sortedHistory.length
|
||||||
? startOfDay(new Date(sortedHistory[0].timestamp * 1000))
|
? startOfDay(new Date(sortedHistory[0].fetched * 1000))
|
||||||
: startOfToday();
|
: startOfToday();
|
||||||
const maxDate = sortedHistory.length
|
const maxDate = sortedHistory.length
|
||||||
? addDays(
|
? addDays(
|
||||||
startOfDay(
|
startOfDay(
|
||||||
new Date(sortedHistory[sortedHistory.length - 1].timestamp * 1000)
|
new Date(sortedHistory[sortedHistory.length - 1].fetched * 1000)
|
||||||
),
|
),
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
|
|
6
src/web/questions/components/PlatformLink.tsx
Normal file
6
src/web/questions/components/PlatformLink.tsx
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { BoxedLink } from "../../common/BoxedLink";
|
||||||
|
import { QuestionFragment } from "../../fragments.generated";
|
||||||
|
|
||||||
|
export const PlatformLink: React.FC<{ question: QuestionFragment }> = ({
|
||||||
|
question,
|
||||||
|
}) => <BoxedLink url={question.url}>{question.platform.label}</BoxedLink>;
|
|
@ -74,7 +74,7 @@ const getCurrencySymbolIfNeeded = ({
|
||||||
"openInterest",
|
"openInterest",
|
||||||
"liquidity",
|
"liquidity",
|
||||||
];
|
];
|
||||||
let dollarPlatforms = ["predictit", "kalshi", "polymarket"];
|
let dollarPlatforms = ["predictit", "kalshi", "polymarket", "insight"];
|
||||||
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
|
if (indicatorsWhichNeedCurrencySymbol.includes(indicator)) {
|
||||||
if (dollarPlatforms.includes(platform)) {
|
if (dollarPlatforms.includes(platform)) {
|
||||||
return "$";
|
return "$";
|
||||||
|
@ -160,9 +160,11 @@ export const QuestionFooter: React.FC<Props> = ({
|
||||||
<div
|
<div
|
||||||
className={`grid grid-cols-3 ${
|
className={`grid grid-cols-3 ${
|
||||||
expandFooterToFullWidth ? "justify-between" : ""
|
expandFooterToFullWidth ? "justify-between" : ""
|
||||||
} text-gray-500 mb-2 mt-1`}
|
} text-gray-500`}
|
||||||
>
|
>
|
||||||
<Stars num={question.qualityIndicators.stars} />
|
<div className="self-center">
|
||||||
|
<Stars num={question.qualityIndicators.stars} />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
expandFooterToFullWidth ? "place-self-center" : "self-center"
|
expandFooterToFullWidth ? "place-self-center" : "self-center"
|
||||||
|
@ -170,6 +172,7 @@ export const QuestionFooter: React.FC<Props> = ({
|
||||||
>
|
>
|
||||||
{question.platform.label
|
{question.platform.label
|
||||||
.replace("Good Judgment Open", "GJOpen")
|
.replace("Good Judgment Open", "GJOpen")
|
||||||
|
.replace("Insight Prediction", "Insight")
|
||||||
.replace(/ /g, "\u00a0")}
|
.replace(/ /g, "\u00a0")}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { ReactElement, ReactNode } from "react";
|
||||||
import { FaExpand } from "react-icons/fa";
|
import { FaExpand } from "react-icons/fa";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
import { Card } from "../../../common/Card";
|
import { Card } from "../../../common/Card";
|
||||||
import { CopyText } from "../../../common/CopyText";
|
import { CopyText } from "../../../common/CopyText";
|
||||||
import { QuestionFragment } from "../../../fragments.generated";
|
import { QuestionFragment } from "../../../fragments.generated";
|
||||||
import { cleanText } from "../../../utils";
|
import { cleanText, isQuestionBinary } from "../../../utils";
|
||||||
import { QuestionOptions } from "../QuestionOptions";
|
import { QuestionOptions } from "../QuestionOptions";
|
||||||
import { QuestionFooter } from "./QuestionFooter";
|
import { QuestionFooter } from "./QuestionFooter";
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ const truncateText = (length: number, text: string): string => {
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
const breakpoints = " .!?";
|
const breakpoints = " .!?";
|
||||||
let lastLetter: string | undefined = undefined;
|
let lastLetter
|
||||||
let lastIndex: number | undefined = undefined;
|
let lastIndex
|
||||||
for (let index = length; index > 0; index--) {
|
for (let index = length; index > 0; index--) {
|
||||||
const letter = text[index];
|
const letter = text[index];
|
||||||
if (breakpoints.includes(letter)) {
|
if (breakpoints.includes(letter)) {
|
||||||
|
@ -63,6 +63,7 @@ const LastUpdated: React.FC<{ timestamp: Date }> = ({ timestamp }) => (
|
||||||
// Main component
|
// Main component
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
container?: (children: ReactNode) => ReactElement;
|
||||||
question: QuestionFragment;
|
question: QuestionFragment;
|
||||||
showTimeStamp: boolean;
|
showTimeStamp: boolean;
|
||||||
expandFooterToFullWidth: boolean;
|
expandFooterToFullWidth: boolean;
|
||||||
|
@ -71,6 +72,7 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QuestionCard: React.FC<Props> = ({
|
export const QuestionCard: React.FC<Props> = ({
|
||||||
|
container = (children) => <Card>{children}</Card>,
|
||||||
question,
|
question,
|
||||||
showTimeStamp,
|
showTimeStamp,
|
||||||
expandFooterToFullWidth,
|
expandFooterToFullWidth,
|
||||||
|
@ -78,85 +80,74 @@ export const QuestionCard: React.FC<Props> = ({
|
||||||
showExpandButton = true,
|
showExpandButton = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { options } = question;
|
const { options } = question;
|
||||||
const lastUpdated = new Date(question.timestamp * 1000);
|
const lastUpdated = new Date(question.fetched * 1000);
|
||||||
|
|
||||||
const isBinary =
|
const isBinary = isQuestionBinary(question);
|
||||||
options.length === 2 &&
|
|
||||||
(options[0].name === "Yes" || options[0].name === "No");
|
|
||||||
|
|
||||||
return (
|
return container(
|
||||||
<Card>
|
<div className="h-full flex flex-col space-y-4">
|
||||||
<div className="h-full flex flex-col space-y-4">
|
<div className="flex-grow space-y-4">
|
||||||
<div className="flex-grow space-y-4">
|
{showIdToggle ? (
|
||||||
{showIdToggle ? (
|
<div className="mx-10">
|
||||||
<div className="mx-10">
|
<CopyText text={question.id} displayText={`[${question.id}]`} />
|
||||||
<CopyText text={question.id} displayText={`[${question.id}]`} />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div>
|
|
||||||
{showExpandButton ? (
|
|
||||||
<Link href={`/questions/${question.id}`} passHref>
|
|
||||||
<a className="float-right block ml-2 mt-1.5">
|
|
||||||
<FaExpand
|
|
||||||
size="18"
|
|
||||||
className="text-gray-400 hover:text-gray-700"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
<Card.Title>
|
|
||||||
<a
|
|
||||||
className="text-black no-underline"
|
|
||||||
href={question.url}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{question.title}
|
|
||||||
</a>
|
|
||||||
</Card.Title>
|
|
||||||
</div>
|
</div>
|
||||||
{isBinary ? (
|
) : null}
|
||||||
<div className="flex justify-between">
|
<div>
|
||||||
<QuestionOptions question={question} />
|
{showExpandButton ? (
|
||||||
<div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
|
<Link href={`/questions/${question.id}`} passHref>
|
||||||
<LastUpdated timestamp={lastUpdated} />
|
<a className="float-right block ml-2 mt-1.5">
|
||||||
</div>
|
<FaExpand
|
||||||
</div>
|
size="18"
|
||||||
) : (
|
className="text-gray-400 hover:text-gray-700"
|
||||||
<div className="space-y-2">
|
/>
|
||||||
<QuestionOptions question={question} />
|
</a>
|
||||||
<div className={`hidden ${showTimeStamp ? "sm:block" : ""} ml-6`}>
|
</Link>
|
||||||
<LastUpdated timestamp={lastUpdated} />
|
) : null}
|
||||||
</div>
|
<Card.Title>
|
||||||
</div>
|
<a
|
||||||
)}
|
className="text-black no-underline"
|
||||||
|
href={question.url}
|
||||||
{question.platform.id !== "guesstimate" && options.length < 3 && (
|
target="_blank"
|
||||||
<div className="text-gray-500">
|
>
|
||||||
<DisplayMarkdown description={question.description} />
|
{question.title}
|
||||||
</div>
|
</a>
|
||||||
)}
|
</Card.Title>
|
||||||
|
|
||||||
{question.platform.id === "guesstimate" && question.visualization && (
|
|
||||||
<img
|
|
||||||
className="rounded-sm"
|
|
||||||
src={question.visualization}
|
|
||||||
alt="Guesstimate Screenshot"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={isBinary ? "flex justify-between" : "space-y-4"}>
|
||||||
className={`sm:hidden ${!showTimeStamp ? "hidden" : ""} self-center`}
|
<QuestionOptions question={question} maxNumOptions={5} />
|
||||||
>
|
<div className={`hidden ${showTimeStamp ? "sm:block" : ""}`}>
|
||||||
{/* This one is exclusively for mobile*/}
|
<LastUpdated timestamp={lastUpdated} />
|
||||||
<LastUpdated timestamp={lastUpdated} />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
|
||||||
|
{question.platform.id !== "guesstimate" && options.length < 3 && (
|
||||||
|
<div className="text-gray-500">
|
||||||
|
<DisplayMarkdown description={question.description} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{question.platform.id === "guesstimate" && question.visualization && (
|
||||||
|
<img
|
||||||
|
className="rounded-sm"
|
||||||
|
src={question.visualization}
|
||||||
|
alt="Guesstimate Screenshot"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`sm:hidden ${!showTimeStamp ? "hidden" : ""} self-center`}
|
||||||
|
>
|
||||||
|
{/* This one is exclusively for mobile*/}
|
||||||
|
<LastUpdated timestamp={lastUpdated} />
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="mb-2 mt-1">
|
||||||
<QuestionFooter
|
<QuestionFooter
|
||||||
question={question}
|
question={question}
|
||||||
expandFooterToFullWidth={expandFooterToFullWidth}
|
expandFooterToFullWidth={expandFooterToFullWidth}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { QuestionWithHistoryFragment } from "../../fragments.generated";
|
||||||
|
import { HistoryChart } from "./HistoryChart";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
question: QuestionWithHistoryFragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuestionChartOrVisualization: React.FC<Props> = ({ question }) =>
|
||||||
|
question.platform.id === "guesstimate" && question.visualization ? (
|
||||||
|
<a className="no-underline" href={question.url} target="_blank">
|
||||||
|
<img
|
||||||
|
className="rounded-sm"
|
||||||
|
src={question.visualization}
|
||||||
|
alt="Guesstimate Screenshot"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
) : question.options.length > 0 ? (
|
||||||
|
<HistoryChart question={question} />
|
||||||
|
) : null; /* Don't display chart if there are no options, for now. */
|
20
src/web/questions/components/QuestionInfoRow.tsx
Normal file
20
src/web/questions/components/QuestionInfoRow.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { QuestionFragment } from "../../fragments.generated";
|
||||||
|
import { PlatformLink } from "./PlatformLink";
|
||||||
|
import { QuestionOptions } from "./QuestionOptions";
|
||||||
|
import { Stars } from "./Stars";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
question: QuestionFragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuestionInfoRow: React.FC<Props> = ({ question }) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PlatformLink question={question} />
|
||||||
|
<Stars num={question.qualityIndicators.stars} />
|
||||||
|
<QuestionOptions
|
||||||
|
question={{ ...question }}
|
||||||
|
maxNumOptions={1}
|
||||||
|
forcePrimaryMode={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
|
@ -59,6 +59,18 @@ const primaryForecastColor = (probability: number) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const chooseColor = (probability: number) => {
|
||||||
|
if (probability < 0.1) {
|
||||||
|
return "bg-blue-50 text-blue-500";
|
||||||
|
} else if (probability < 0.3) {
|
||||||
|
return "bg-blue-100 text-blue-600";
|
||||||
|
} else if (probability < 0.7) {
|
||||||
|
return "bg-blue-200 text-blue-700";
|
||||||
|
} else {
|
||||||
|
return "bg-blue-300 text-blue-800";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const primaryEstimateAsText = (probability: number) => {
|
const primaryEstimateAsText = (probability: number) => {
|
||||||
if (probability < 0.03) {
|
if (probability < 0.03) {
|
||||||
return "Exceptionally unlikely";
|
return "Exceptionally unlikely";
|
||||||
|
@ -77,38 +89,48 @@ const primaryEstimateAsText = (probability: number) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const chooseColor = (probability: number) => {
|
type OptionProps = {
|
||||||
if (probability < 0.1) {
|
option: FullQuestionOption;
|
||||||
return "bg-blue-50 text-blue-500";
|
mode: "primary" | "normal"; // affects font size and colors
|
||||||
} else if (probability < 0.3) {
|
textMode: "name" | "probability"; // whether to output option name or probability estimate as text
|
||||||
return "bg-blue-100 text-blue-600";
|
|
||||||
} else if (probability < 0.7) {
|
|
||||||
return "bg-blue-200 text-blue-700";
|
|
||||||
} else {
|
|
||||||
return "bg-blue-300 text-blue-800";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const OptionRow: React.FC<{ option: FullQuestionOption }> = ({ option }) => {
|
const OptionRow: React.FC<OptionProps> = ({ option, mode, textMode }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center space-x-2">
|
||||||
<div
|
<div
|
||||||
className={`${chooseColor(
|
className={`flex-none rounded-md text-center ${
|
||||||
option.probability
|
mode === "primary"
|
||||||
)} w-14 flex-none rounded-md py-0.5 text-sm text-center`}
|
? "text-sm md:text-lg text-normal text-white px-2 py-0.5 font-bold"
|
||||||
|
: "text-sm w-14 py-0.5"
|
||||||
|
} ${
|
||||||
|
mode === "primary"
|
||||||
|
? primaryForecastColor(option.probability)
|
||||||
|
: chooseColor(option.probability)
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{formatProbability(option.probability)}
|
{formatProbability(option.probability)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-gray-700 pl-3 leading-snug text-sm">
|
<div
|
||||||
{option.name}
|
className={`leading-snug ${
|
||||||
|
mode === "primary" ? "text-sm md:text-lg text-normal" : "text-sm"
|
||||||
|
} ${
|
||||||
|
mode === "primary" ? textColor(option.probability) : "text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{textMode === "name"
|
||||||
|
? option.name
|
||||||
|
: primaryEstimateAsText(option.probability)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QuestionOptions: React.FC<{ question: QuestionFragment }> = ({
|
export const QuestionOptions: React.FC<{
|
||||||
question,
|
question: QuestionFragment;
|
||||||
}) => {
|
maxNumOptions: number;
|
||||||
|
forcePrimaryMode?: boolean;
|
||||||
|
}> = ({ question, maxNumOptions, forcePrimaryMode = false }) => {
|
||||||
const isBinary = isQuestionBinary(question);
|
const isBinary = isQuestionBinary(question);
|
||||||
|
|
||||||
if (isBinary) {
|
if (isBinary) {
|
||||||
|
@ -119,35 +141,26 @@ export const QuestionOptions: React.FC<{ question: QuestionFragment }> = ({
|
||||||
if (!isFullQuestionOption(yesOption)) {
|
if (!isFullQuestionOption(yesOption)) {
|
||||||
return null; // missing data
|
return null; // missing data
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-x-2">
|
<OptionRow option={yesOption} mode="primary" textMode="probability" />
|
||||||
<span
|
|
||||||
className={`${primaryForecastColor(
|
|
||||||
yesOption.probability
|
|
||||||
)} text-white w-16 rounded-md px-1.5 py-0.5 font-bold`}
|
|
||||||
>
|
|
||||||
{formatProbability(yesOption.probability)}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
className={`${textColor(
|
|
||||||
yesOption.probability
|
|
||||||
)} text-gray-500 inline-block`}
|
|
||||||
>
|
|
||||||
{primaryEstimateAsText(yesOption.probability)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const optionsSorted = question.options
|
const optionsSorted = question.options
|
||||||
.filter(isFullQuestionOption)
|
.filter(isFullQuestionOption)
|
||||||
.sort((a, b) => b.probability - a.probability);
|
.sort((a, b) => b.probability - a.probability);
|
||||||
|
|
||||||
const optionsMax5 = optionsSorted.slice(0, 5); // display max 5 options.
|
const optionsMaxN = optionsSorted.slice(0, maxNumOptions); // display max 5 options.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{optionsMax5.map((option, i) => (
|
{optionsMaxN.map((option, i) => (
|
||||||
<OptionRow option={option} key={i} />
|
<OptionRow
|
||||||
|
key={i}
|
||||||
|
option={option}
|
||||||
|
mode={forcePrimaryMode ? "primary" : "normal"}
|
||||||
|
textMode="name"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
26
src/web/questions/components/QuestionTitle.tsx
Normal file
26
src/web/questions/components/QuestionTitle.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { QuestionFragment } from "../../fragments.generated";
|
||||||
|
import { getBasePath } from "../../utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
question: QuestionFragment;
|
||||||
|
linkToMetaforecast?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuestionTitle: React.FC<Props> = ({
|
||||||
|
question,
|
||||||
|
linkToMetaforecast,
|
||||||
|
}) => (
|
||||||
|
<h1 className="sm:text-3xl text-lg">
|
||||||
|
<a
|
||||||
|
className="text-black no-underline hover:text-gray-700"
|
||||||
|
href={
|
||||||
|
linkToMetaforecast
|
||||||
|
? getBasePath() + `/questions/${question.id}`
|
||||||
|
: question.url
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{question.title}
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
);
|
|
@ -54,9 +54,5 @@ function getStarsColor(numstars: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Stars: React.FC<{ num: number }> = ({ num }) => {
|
export const Stars: React.FC<{ num: number }> = ({ num }) => {
|
||||||
return (
|
return <div className={getStarsColor(num) + " text-xs md:text-lg"}>{getstars(num)}</div>;
|
||||||
<div className={`self-center col-span-1 ${getStarsColor(num)}`}>
|
|
||||||
{getstars(num)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
64
src/web/questions/pages/EmbedQuestionPage.tsx
Normal file
64
src/web/questions/pages/EmbedQuestionPage.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
|
import NextError from "next/error";
|
||||||
|
|
||||||
|
import { Query } from "../../common/Query";
|
||||||
|
import { ssrUrql } from "../../urql";
|
||||||
|
import { QuestionChartOrVisualization } from "../components/QuestionChartOrVisualization";
|
||||||
|
import { QuestionInfoRow } from "../components/QuestionInfoRow";
|
||||||
|
import { QuestionTitle } from "../components/QuestionTitle";
|
||||||
|
import { QuestionPageDocument } from "../queries.generated";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const [ssrCache, client] = ssrUrql();
|
||||||
|
const id = context.query.id as string;
|
||||||
|
|
||||||
|
const question =
|
||||||
|
(await client.query(QuestionPageDocument, { id }).toPromise()).data
|
||||||
|
?.result || null;
|
||||||
|
|
||||||
|
if (!question) {
|
||||||
|
context.res.statusCode = 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
urqlState: ssrCache.extractData(),
|
||||||
|
id,
|
||||||
|
question
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbedQuestionPage: NextPage<Props> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<div className="block bg-white min-h-screen">
|
||||||
|
<Query document={QuestionPageDocument} variables={{ id }}>
|
||||||
|
{({ data: { result: question } }) =>
|
||||||
|
question ? (
|
||||||
|
<div className="flex flex-col p-2 w-full h-12/12">
|
||||||
|
{/*<QuestionTitle question={question} linkToMetaforecast={true} /> */}
|
||||||
|
|
||||||
|
<div className="mb-1 mt-1">
|
||||||
|
<QuestionInfoRow question={question} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-0">
|
||||||
|
<QuestionChartOrVisualization question={question} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<NextError statusCode={404} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Query>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmbedQuestionPage;
|
|
@ -1,17 +1,20 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import { FaExternalLinkAlt } from "react-icons/fa";
|
import NextError from "next/error";
|
||||||
|
import React from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
import { Card } from "../../common/Card";
|
import { Card } from "../../common/Card";
|
||||||
|
import { Collapsible } from "../../common/Collapsible";
|
||||||
|
import { CopyParagraph } from "../../common/CopyParagraph";
|
||||||
import { Layout } from "../../common/Layout";
|
import { Layout } from "../../common/Layout";
|
||||||
import { LineHeader } from "../../common/LineHeader";
|
|
||||||
import { Query } from "../../common/Query";
|
import { Query } from "../../common/Query";
|
||||||
import { QuestionWithHistoryFragment } from "../../fragments.generated";
|
import { QuestionWithHistoryFragment } from "../../fragments.generated";
|
||||||
import { ssrUrql } from "../../urql";
|
import { ssrUrql } from "../../urql";
|
||||||
|
import { getBasePath } from "../../utils";
|
||||||
import { CaptureQuestion } from "../components/CaptureQuestion";
|
import { CaptureQuestion } from "../components/CaptureQuestion";
|
||||||
import { HistoryChart } from "../components/HistoryChart";
|
|
||||||
import { IndicatorsTable } from "../components/IndicatorsTable";
|
import { IndicatorsTable } from "../components/IndicatorsTable";
|
||||||
import { Stars } from "../components/Stars";
|
import { QuestionChartOrVisualization } from "../components/QuestionChartOrVisualization";
|
||||||
|
import { QuestionInfoRow } from "../components/QuestionInfoRow";
|
||||||
|
import { QuestionTitle } from "../components/QuestionTitle";
|
||||||
import { QuestionPageDocument } from "../queries.generated";
|
import { QuestionPageDocument } from "../queries.generated";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
@ -40,91 +43,99 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const Section: React.FC<{ title: string }> = ({ title, children }) => (
|
const Section: React.FC<{ title: string; id?: string }> = ({
|
||||||
<div className="space-y-2 flex flex-col items-start">
|
title,
|
||||||
<h2 className="text-xl text-gray-900">{title}</h2>
|
children,
|
||||||
|
id,
|
||||||
|
}) => (
|
||||||
|
<div className="space-y-4 flex flex-col items-start" id={id}>
|
||||||
|
<div className="border-b-2 border-gray-200 w-full group">
|
||||||
|
<h2 className="text-xl leading-3 text-gray-900">
|
||||||
|
<span>{title}</span>
|
||||||
|
{id ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<a
|
||||||
|
className="text-gray-300 no-underline hidden group-hover:inline"
|
||||||
|
href={`#${id}`}
|
||||||
|
>
|
||||||
|
#
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const EmbedSection: React.FC<{ question: QuestionWithHistoryFragment }> = ({
|
||||||
|
question,
|
||||||
|
}) => {
|
||||||
|
const url = `https://${getBasePath()}/questions/embed/${question.id}`;
|
||||||
|
return (
|
||||||
|
<Section title="Embed" id="embed">
|
||||||
|
<CopyParagraph
|
||||||
|
text={`<iframe src="${url}" height="600" width="600" frameborder="0" />`}
|
||||||
|
buttonText="Copy HTML"
|
||||||
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Collapsible title="Preview">
|
||||||
|
{() => <iframe src={url} height="600" width="600" frameBorder="0" />}
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const LargeQuestionCard: React.FC<{
|
const LargeQuestionCard: React.FC<{
|
||||||
question: QuestionWithHistoryFragment;
|
question: QuestionWithHistoryFragment;
|
||||||
}> = ({ question }) => (
|
}> = ({ question }) => {
|
||||||
<Card highlightOnHover={false} large={true}>
|
return (
|
||||||
<h1 className="sm:text-3xl text-xl">
|
<Card highlightOnHover={false} large={true}>
|
||||||
<a
|
<QuestionTitle question={question} />
|
||||||
className="text-black no-underline hover:text-gray-700"
|
|
||||||
href={question.url}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{question.title}{" "}
|
|
||||||
<FaExternalLinkAlt className="text-gray-400 inline sm:text-3xl text-xl mb-1" />
|
|
||||||
</a>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="flex gap-2 mb-10">
|
<div className="mb-5 mt-5">
|
||||||
<a
|
<QuestionInfoRow question={question} />
|
||||||
className="text-black no-underline bg-red-300 rounded p-1 px-2 text-xs hover:text-gray-600"
|
</div>
|
||||||
href={question.url}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{question.platform.label}
|
|
||||||
</a>
|
|
||||||
<Stars num={question.qualityIndicators.stars} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
{question.platform.id === "guesstimate" && question.visualization ? (
|
<QuestionChartOrVisualization question={question} />
|
||||||
<a className="no-underline" href={question.url} target="_blank">
|
</div>
|
||||||
<img
|
|
||||||
className="rounded-sm"
|
|
||||||
src={question.visualization}
|
|
||||||
alt="Guesstimate Screenshot"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<HistoryChart question={question} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mx-auto max-w-prose">
|
<div className="mx-auto max-w-prose space-y-8">
|
||||||
<Section title="Question description">
|
<Section title="Question description" id="description">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
linkTarget="_blank"
|
linkTarget="_blank"
|
||||||
className="font-normal text-gray-900"
|
className="font-normal text-gray-900"
|
||||||
>
|
>
|
||||||
{question.description.replaceAll("---", "")}
|
{question.description.replaceAll("---", "")}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</Section>
|
</Section>
|
||||||
<div className="mt-5">
|
<Section title="Indicators" id="indicators">
|
||||||
<Section title="Indicators">
|
|
||||||
<IndicatorsTable question={question} />
|
<IndicatorsTable question={question} />
|
||||||
</Section>
|
</Section>
|
||||||
|
<Section title="Capture" id="capture">
|
||||||
|
<CaptureQuestion question={question} />
|
||||||
|
</Section>
|
||||||
|
<EmbedSection question={question} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
const QuestionScreen: React.FC<{ question: QuestionWithHistoryFragment }> = ({
|
|
||||||
question,
|
|
||||||
}) => (
|
|
||||||
<div className="space-y-8">
|
|
||||||
<LargeQuestionCard question={question} />
|
|
||||||
<div className="space-y-4">
|
|
||||||
<LineHeader>
|
|
||||||
<h1>Capture</h1>
|
|
||||||
</LineHeader>
|
|
||||||
<CaptureQuestion question={question} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const QuestionPage: NextPage<Props> = ({ id }) => {
|
const QuestionPage: NextPage<Props> = ({ id }) => {
|
||||||
return (
|
return (
|
||||||
<Layout page="question">
|
<Layout page="question">
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="max-w-4xl mx-auto">
|
||||||
<Query document={QuestionPageDocument} variables={{ id }}>
|
<Query document={QuestionPageDocument} variables={{ id }}>
|
||||||
{({ data }) => <QuestionScreen question={data.result} />}
|
{({ data }) =>
|
||||||
|
data.result ? (
|
||||||
|
<LargeQuestionCard question={data.result} />
|
||||||
|
) : (
|
||||||
|
<NextError statusCode={404} />
|
||||||
|
)
|
||||||
|
}
|
||||||
</Query>
|
</Query>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -7,7 +7,7 @@ export type QuestionPageQueryVariables = Types.Exact<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type QuestionPageQuery = { __typename?: 'Query', result: { __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, history: Array<{ __typename?: 'History', timestamp: number, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }> }>, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } } };
|
export type QuestionPageQuery = { __typename?: 'Query', result?: { __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, history: Array<{ __typename?: 'History', fetched: number, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }> }>, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } } | null };
|
||||||
|
|
||||||
|
|
||||||
export const QuestionPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QuestionPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"question"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"QuestionWithHistory"}}]}}]}},...QuestionWithHistoryFragmentDoc.definitions]} as unknown as DocumentNode<QuestionPageQuery, QuestionPageQueryVariables>;
|
export const QuestionPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"QuestionPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"question"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"QuestionWithHistory"}}]}}]}},...QuestionWithHistoryFragmentDoc.definitions]} as unknown as DocumentNode<QuestionPageQuery, QuestionPageQueryVariables>;
|
|
@ -5,14 +5,14 @@ import { QuestionFragmentDoc } from '../fragments.generated';
|
||||||
export type FrontpageQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
export type FrontpageQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type FrontpageQuery = { __typename?: 'Query', result: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> };
|
export type FrontpageQuery = { __typename?: 'Query', result: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> };
|
||||||
|
|
||||||
export type SearchQueryVariables = Types.Exact<{
|
export type SearchQueryVariables = Types.Exact<{
|
||||||
input: Types.SearchInput;
|
input: Types.SearchInput;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type SearchQuery = { __typename?: 'Query', result: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, timestamp: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> };
|
export type SearchQuery = { __typename?: 'Query', result: Array<{ __typename?: 'Question', id: string, url: string, title: string, description: string, fetched: number, visualization?: string | null, options: Array<{ __typename?: 'ProbabilityOption', name?: string | null, probability?: number | null }>, platform: { __typename?: 'Platform', id: string, label: string }, qualityIndicators: { __typename?: 'QualityIndicators', stars: number, numForecasts?: number | null, numForecasters?: number | null, volume?: number | null, spread?: number | null, sharesVolume?: number | null, openInterest?: number | null, liquidity?: number | null, tradeVolume?: number | null } }> };
|
||||||
|
|
||||||
|
|
||||||
export const FrontpageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Frontpage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"frontpage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<FrontpageQuery, FrontpageQueryVariables>;
|
export const FrontpageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Frontpage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"frontpage"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<FrontpageQuery, FrontpageQueryVariables>;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { QuestionFragment } from "./fragments.generated";
|
||||||
|
|
||||||
export const getBasePath = () => {
|
export const getBasePath = () => {
|
||||||
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
|
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
|
||||||
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
return `https://metaforecast.org`;//`https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// can be used for local development if you prefer non-default port
|
// can be used for local development if you prefer non-default port
|
||||||
|
@ -14,8 +14,8 @@ export const getBasePath = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cleanText = (text: string): string => {
|
export const cleanText = (text: string): string => {
|
||||||
// Note: should no longer be necessary?
|
// TODO - move to GraphQL:
|
||||||
// Still needed for e.g. /questions/rootclaim-what-caused-the-disappearance-of-malaysia-airlines-flight-370
|
// { description(clean: true, truncate: 250) }
|
||||||
let textString = !!text ? text : "";
|
let textString = !!text ? text : "";
|
||||||
textString = textString
|
textString = textString
|
||||||
.replaceAll("] (", "](")
|
.replaceAll("] (", "](")
|
||||||
|
@ -23,12 +23,13 @@ export const cleanText = (text: string): string => {
|
||||||
.replaceAll("( [", "([")
|
.replaceAll("( [", "([")
|
||||||
.replaceAll(") ,", "),")
|
.replaceAll(") ,", "),")
|
||||||
.replaceAll("==", "") // Denotes a title in markdown
|
.replaceAll("==", "") // Denotes a title in markdown
|
||||||
.replaceAll("Background\n", "")
|
.replaceAll(/^#+\s+/gm, "")
|
||||||
.replaceAll("Context\n", "")
|
.replaceAll(/^Background\n/gm, "")
|
||||||
|
.replaceAll(/^Context\n/gm, "")
|
||||||
.replaceAll("--- \n", "- ")
|
.replaceAll("--- \n", "- ")
|
||||||
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
|
.replaceAll(/\[(.*?)\]\(.*?\)/g, "$1");
|
||||||
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
|
textString = textString.slice(0, 1) == "=" ? textString.slice(1) : textString;
|
||||||
//console.log(textString)
|
|
||||||
return textString;
|
return textString;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,8 @@ export default async function searchWithAlgolia({
|
||||||
title: "No search results match your query",
|
title: "No search results match your query",
|
||||||
url: "https://metaforecast.org",
|
url: "https://metaforecast.org",
|
||||||
platform: "metaforecast",
|
platform: "metaforecast",
|
||||||
description: "Maybe try a broader query?",
|
platformLabel: "metaforecast",
|
||||||
|
description: "Maybe try a broader query, e.g., reduce the number of 'stars' by clicking in 'Advanced options'?",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: "Yes",
|
name: "Yes",
|
||||||
|
@ -109,7 +110,8 @@ export default async function searchWithAlgolia({
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
fetched: new Date().toISOString(),
|
||||||
|
firstSeen: new Date().toISOString(),
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
|
@ -126,8 +128,11 @@ export default async function searchWithAlgolia({
|
||||||
title: `Did you mean: ${queryString}?`,
|
title: `Did you mean: ${queryString}?`,
|
||||||
url: "https://metaforecast.org/recursion?bypassEasterEgg=true",
|
url: "https://metaforecast.org/recursion?bypassEasterEgg=true",
|
||||||
platform: "metaforecast",
|
platform: "metaforecast",
|
||||||
|
platformLabel: "metaforecast",
|
||||||
description:
|
description:
|
||||||
"Fatal error: Too much recursion. Click to proceed anyways",
|
"Fatal error: Too much recursion. Click to proceed anyways",
|
||||||
|
fetched: new Date().toISOString(),
|
||||||
|
firstSeen: new Date().toISOString(),
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: "Yes",
|
name: "Yes",
|
||||||
|
@ -140,7 +145,6 @@ export default async function searchWithAlgolia({
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
|
@ -161,7 +165,8 @@ export default async function searchWithAlgolia({
|
||||||
title: "No search results appear to match your query",
|
title: "No search results appear to match your query",
|
||||||
url: "https://metaforecast.org",
|
url: "https://metaforecast.org",
|
||||||
platform: "metaforecast",
|
platform: "metaforecast",
|
||||||
description: "Maybe try a broader query? That said, we could be wrong.",
|
platformLabel: "metaforecast",
|
||||||
|
description: "Maybe try a broader query? Maybe try a broader query, e.g., reduce the number of 'stars' by clicking in 'Advanced options'? That said, we could be wrong.",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
name: "Yes",
|
name: "Yes",
|
||||||
|
@ -174,7 +179,8 @@ export default async function searchWithAlgolia({
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
fetched: new Date().toISOString(),
|
||||||
|
firstSeen: new Date().toISOString(),
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
|
|
|
@ -5,7 +5,7 @@ export async function uploadToImgur(dataURL: string): Promise<string> {
|
||||||
method: "post",
|
method: "post",
|
||||||
url: "https://api.imgur.com/3/image",
|
url: "https://api.imgur.com/3/image",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer 8e9666fb889318515a62208560d4e8393dac26d8",
|
Authorization: `Bearer ${process.env.IMGUR_BEARER}`,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: "base64",
|
type: "base64",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user