commit
02acc5ee6b
32
codegen.yml
Normal file
32
codegen.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
schema: src/graphql/build-schema.js
|
||||||
|
documents:
|
||||||
|
- "src/**/*.graphql"
|
||||||
|
|
||||||
|
# This should be updated to match your client files
|
||||||
|
# documents: 'client/**/!(*.d).{ts,tsx}'
|
||||||
|
generates:
|
||||||
|
# This will take your schema and print an SDL schema.
|
||||||
|
schema.graphql:
|
||||||
|
plugins:
|
||||||
|
- schema-ast
|
||||||
|
|
||||||
|
src/graphql/types.generated.ts:
|
||||||
|
plugins:
|
||||||
|
- typescript
|
||||||
|
|
||||||
|
src/graphql/introspection.json:
|
||||||
|
plugins:
|
||||||
|
- introspection:
|
||||||
|
minify: true
|
||||||
|
|
||||||
|
src/:
|
||||||
|
preset: near-operation-file
|
||||||
|
presetConfig:
|
||||||
|
extension: .generated.tsx
|
||||||
|
baseTypesPath: graphql/types.generated.ts
|
||||||
|
plugins:
|
||||||
|
- typescript-operations:
|
||||||
|
strictScalars: true
|
||||||
|
scalars:
|
||||||
|
Date: number
|
||||||
|
- typed-document-node
|
|
@ -13,7 +13,7 @@
|
||||||
# React
|
# React
|
||||||
|
|
||||||
- create one file per one component (tiny helper components in the same file are fine)
|
- create one file per one component (tiny helper components in the same file are fine)
|
||||||
- name file identically to the component it describes (e.g. `const DisplayForecasts: React.FC<Props> = ...` in `DisplayForecasts.ts`)
|
- name file identically to the component it describes (e.g. `const DisplayQuestions: React.FC<Props> = ...` in `DisplayQuestions.ts`)
|
||||||
- use named export instead of default export for all React components
|
- use named export instead of default export for all React components
|
||||||
- it's better for refactoring
|
- it's better for refactoring
|
||||||
- and it plays well with `React.FC` typing
|
- and it plays well with `React.FC` typing
|
||||||
|
|
45
docs/graphql.md
Normal file
45
docs/graphql.md
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
Metaforecast website is implemented on top of GraphQL API.
|
||||||
|
|
||||||
|
Tech stack:
|
||||||
|
|
||||||
|
- [Pothos](https://pothos-graphql.dev/) on the backend for implementing our graphql server
|
||||||
|
- [urql](https://formidable.com/open-source/urql/) on the frontend
|
||||||
|
- [GraphQL Code Generator](https://www.graphql-code-generator.com/) for schema generation, queries generation and schema introspection
|
||||||
|
|
||||||
|
Rationale for this stack can be found on [#32](https://github.com/quantified-uncertainty/metaforecast/issues/32) and [#21](https://github.com/quantified-uncertainty/metaforecast/issues/32) in comments.
|
||||||
|
|
||||||
|
# Code layout
|
||||||
|
|
||||||
|
List of all files used for graphql:
|
||||||
|
|
||||||
|
- [schema.graphql](../schema.graphql), GraphQL schema generated by graphql-code-generator.
|
||||||
|
- [codegen.yml](../codegen.yml), graphql-code-generator [config](https://www.graphql-code-generator.com/docs/config-reference/codegen-config)
|
||||||
|
- [graphql.config.yaml](../graphql.config.yaml), [GraphQL Config](https://www.graphql-config.com/) for better VS Code integration ([VS Code GraphQL extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql) type-checks graphql files thanks to it)
|
||||||
|
- [src/pages/api/graphql.ts](../src/pages/api/graphql.ts) implements the GraphQL HTTP endpoint and GraphQL playground
|
||||||
|
- [src/web/urql.ts](../src/web/urql.ts) contains some helper functions.
|
||||||
|
|
||||||
|
[src/graphql/](../src/graphql) dir contains GraphQL server code.
|
||||||
|
|
||||||
|
`*.graphql` files in `src/web/**` contain GraphQL fragments, queries and mutations which are used on the frontend.
|
||||||
|
|
||||||
|
`graphql-code-generator` converts those into `*.generated.ts` files which can be imported from the React components.
|
||||||
|
|
||||||
|
# Recipes
|
||||||
|
|
||||||
|
**I want to check out what Metaforecast's GraphQL API is capable of**
|
||||||
|
|
||||||
|
Go to [/api/graphql](https://metaforecast.org/api/graphql) and do some queries by hand. Note the "Docs" link in the top right corner.
|
||||||
|
|
||||||
|
**I want to add a new query/mutation to our schema**
|
||||||
|
|
||||||
|
Read the [Pothos](https://pothos-graphql.dev/) docs to learn how to implement new objects and fields. Add the new code somewhere in `src/graphql/schema/`.
|
||||||
|
|
||||||
|
**I want to display a new piece of data on the metaforecast website**
|
||||||
|
|
||||||
|
- add a query in a nearest `queries.graphql` file (if there isn't one, create it)
|
||||||
|
- run `gql-gen` (or keep it running with `gql-gen -w`) to get a `graphql.generated.ts` file
|
||||||
|
- use [useQuery](https://formidable.com/open-source/urql/docs/basics/react-preact/#queries) with your new `MyQueryDocument` which you can import from the `graphql.generated.ts` file
|
||||||
|
|
||||||
|
If you need SSR, you'll also need to do **the same** query with **the same variables** from your page's `getServerSideProps` function. Check out any existing page which calls `ssrUrql`.
|
||||||
|
|
||||||
|
(You might not need to use `useQuery` in your components if you're doing SSR and the page content is not dynamic, just pass the data from `getServerSideProps` in `props` and avoid GraphQL on the client side).
|
1
graphql.config.yaml
Normal file
1
graphql.config.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
schema: http://localhost:3000/api/graphql
|
7610
package-lock.json
generated
7610
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
|
@ -21,12 +21,16 @@
|
||||||
"reload": "heroku run:detached ts-node -T src/backend/flow/doEverythingForScheduler.js",
|
"reload": "heroku run:detached ts-node -T src/backend/flow/doEverythingForScheduler.js",
|
||||||
"setCookies": "./src/backend/manual/setCookies.sh",
|
"setCookies": "./src/backend/manual/setCookies.sh",
|
||||||
"next-dev": "next dev",
|
"next-dev": "next dev",
|
||||||
"next-build": "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"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@graphql-yoga/node": "^2.1.0",
|
||||||
|
"@pothos/core": "^3.5.1",
|
||||||
|
"@pothos/plugin-prisma": "^3.4.0",
|
||||||
|
"@pothos/plugin-relay": "^3.10.0",
|
||||||
"@prisma/client": "^3.11.1",
|
"@prisma/client": "^3.11.1",
|
||||||
"@tailwindcss/forms": "^0.4.0",
|
"@tailwindcss/forms": "^0.4.0",
|
||||||
"@tailwindcss/typography": "^0.5.1",
|
"@tailwindcss/typography": "^0.5.1",
|
||||||
|
@ -55,6 +59,7 @@
|
||||||
"multiselect-react-dropdown": "^2.0.17",
|
"multiselect-react-dropdown": "^2.0.17",
|
||||||
"next": "12",
|
"next": "12",
|
||||||
"next-plausible": "^3.1.6",
|
"next-plausible": "^3.1.6",
|
||||||
|
"next-urql": "^3.3.2",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"open": "^7.3.1",
|
"open": "^7.3.1",
|
||||||
"papaparse": "^5.3.0",
|
"papaparse": "^5.3.0",
|
||||||
|
@ -72,6 +77,7 @@
|
||||||
"react-dropdown": "^1.9.2",
|
"react-dropdown": "^1.9.2",
|
||||||
"react-hook-form": "^7.27.0",
|
"react-hook-form": "^7.27.0",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
|
"react-is": "^18.0.0",
|
||||||
"react-markdown": "^8.0.0",
|
"react-markdown": "^8.0.0",
|
||||||
"react-safe": "^1.3.0",
|
"react-safe": "^1.3.0",
|
||||||
"react-select": "^5.2.2",
|
"react-select": "^5.2.2",
|
||||||
|
@ -81,9 +87,18 @@
|
||||||
"tailwindcss": "^3.0.22",
|
"tailwindcss": "^3.0.22",
|
||||||
"textversionjs": "^1.1.3",
|
"textversionjs": "^1.1.3",
|
||||||
"ts-node": "^10.7.0",
|
"ts-node": "^10.7.0",
|
||||||
"tunnel": "^0.0.6"
|
"tunnel": "^0.0.6",
|
||||||
|
"urql": "^2.2.0",
|
||||||
|
"urql-custom-scalars-exchange": "^0.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "^2.6.2",
|
||||||
|
"@graphql-codegen/introspection": "^2.1.1",
|
||||||
|
"@graphql-codegen/near-operation-file-preset": "^2.2.9",
|
||||||
|
"@graphql-codegen/schema-ast": "^2.4.1",
|
||||||
|
"@graphql-codegen/typed-document-node": "^2.2.8",
|
||||||
|
"@graphql-codegen/typescript": "^2.4.8",
|
||||||
|
"@graphql-codegen/typescript-operations": "^2.3.5",
|
||||||
"@netlify/plugin-nextjs": "^4.2.4",
|
"@netlify/plugin-nextjs": "^4.2.4",
|
||||||
"@svgr/cli": "^6.2.1",
|
"@svgr/cli": "^6.2.1",
|
||||||
"@types/pg": "^8.6.5",
|
"@types/pg": "^8.6.5",
|
||||||
|
|
|
@ -2,12 +2,16 @@ generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generator pothos {
|
||||||
|
provider = "prisma-pothos-types"
|
||||||
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DIGITALOCEAN_POSTGRES")
|
url = env("DIGITALOCEAN_POSTGRES")
|
||||||
}
|
}
|
||||||
|
|
||||||
model dashboards {
|
model Dashboard {
|
||||||
id String @id
|
id String @id
|
||||||
title String
|
title String
|
||||||
description String
|
description String
|
||||||
|
@ -15,15 +19,19 @@ model dashboards {
|
||||||
timestamp DateTime @db.Timestamp(6)
|
timestamp DateTime @db.Timestamp(6)
|
||||||
creator String
|
creator String
|
||||||
extra Json
|
extra Json
|
||||||
|
|
||||||
|
@@map("dashboards")
|
||||||
}
|
}
|
||||||
|
|
||||||
model frontpage {
|
model Frontpage {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
frontpage_full Json
|
frontpage_full Json
|
||||||
frontpage_sliced Json
|
frontpage_sliced Json
|
||||||
|
|
||||||
|
@@map("frontpage")
|
||||||
}
|
}
|
||||||
|
|
||||||
model history {
|
model History {
|
||||||
id String
|
id String
|
||||||
title String
|
title String
|
||||||
url String
|
url String
|
||||||
|
@ -37,9 +45,10 @@ model history {
|
||||||
pk Int @id @default(autoincrement())
|
pk Int @id @default(autoincrement())
|
||||||
|
|
||||||
@@index([id])
|
@@index([id])
|
||||||
|
@@map("history")
|
||||||
}
|
}
|
||||||
|
|
||||||
model questions {
|
model Question {
|
||||||
id String @id
|
id String @id
|
||||||
title String
|
title String
|
||||||
url String
|
url String
|
||||||
|
@ -50,4 +59,6 @@ model questions {
|
||||||
stars Int
|
stars Int
|
||||||
qualityindicators Json
|
qualityindicators Json
|
||||||
extra Json
|
extra Json
|
||||||
|
|
||||||
|
@@map("questions")
|
||||||
}
|
}
|
||||||
|
|
86
schema.graphql
Normal file
86
schema.graphql
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
input CreateDashboardInput {
|
||||||
|
creator: String
|
||||||
|
description: String
|
||||||
|
ids: [ID!]!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateDashboardResult {
|
||||||
|
dashboard: Dashboard!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dashboard {
|
||||||
|
creator: String!
|
||||||
|
description: String!
|
||||||
|
id: ID!
|
||||||
|
questions: [Question!]!
|
||||||
|
title: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
"""Date serialized as the Unix timestamp."""
|
||||||
|
scalar Date
|
||||||
|
|
||||||
|
type Mutation {
|
||||||
|
createDashboard(input: CreateDashboardInput!): CreateDashboardResult!
|
||||||
|
}
|
||||||
|
|
||||||
|
type PageInfo {
|
||||||
|
endCursor: String
|
||||||
|
hasNextPage: Boolean!
|
||||||
|
hasPreviousPage: Boolean!
|
||||||
|
startCursor: String
|
||||||
|
}
|
||||||
|
|
||||||
|
"""Platform supported by metaforecast"""
|
||||||
|
type Platform {
|
||||||
|
id: ID!
|
||||||
|
label: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProbabilityOption {
|
||||||
|
name: String
|
||||||
|
probability: Float
|
||||||
|
}
|
||||||
|
|
||||||
|
"""Various indicators of the question's quality"""
|
||||||
|
type QualityIndicators {
|
||||||
|
numForecasts: Int
|
||||||
|
stars: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
dashboard(id: ID!): Dashboard!
|
||||||
|
frontpage: [Question!]!
|
||||||
|
questions(after: String, before: String, first: Int, last: Int): QueryQuestionsConnection!
|
||||||
|
searchQuestions(input: SearchInput!): [Question!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryQuestionsConnection {
|
||||||
|
edges: [QueryQuestionsConnectionEdge]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryQuestionsConnectionEdge {
|
||||||
|
cursor: String!
|
||||||
|
node: Question!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Question {
|
||||||
|
description: String!
|
||||||
|
id: ID!
|
||||||
|
options: [ProbabilityOption!]!
|
||||||
|
platform: Platform!
|
||||||
|
qualityIndicators: QualityIndicators!
|
||||||
|
timestamp: Date!
|
||||||
|
title: String!
|
||||||
|
url: String!
|
||||||
|
visualization: String
|
||||||
|
}
|
||||||
|
|
||||||
|
input SearchInput {
|
||||||
|
forecastingPlatforms: [String!]
|
||||||
|
forecastsThreshold: Int
|
||||||
|
limit: Int
|
||||||
|
query: String!
|
||||||
|
starsThreshold: Int
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
export interface DashboardItem {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
contents: any;
|
|
||||||
timestamp: string;
|
|
||||||
creator: string;
|
|
||||||
extra: any;
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Pool, PoolClient } from "pg";
|
import { Pool, PoolClient } from "pg";
|
||||||
|
|
||||||
import { Forecast } from "../platforms";
|
import { Question } from "../platforms";
|
||||||
import { hash } from "../utils/hash";
|
import { hash } from "../utils/hash";
|
||||||
import { measureTime } from "../utils/measureTime";
|
import { measureTime } from "../utils/measureTime";
|
||||||
import { roughSizeOfObject } from "../utils/roughSize";
|
import { roughSizeOfObject } from "../utils/roughSize";
|
||||||
|
|
||||||
const forecastTableNames = ["questions", "history"];
|
const questionTableNames = ["questions", "history"];
|
||||||
|
|
||||||
const allTableNames = [...forecastTableNames, "dashboards", "frontpage"];
|
const allTableNames = [...questionTableNames, "dashboards", "frontpage"];
|
||||||
|
|
||||||
/* Postgres database connection code */
|
/* Postgres database connection code */
|
||||||
const databaseURL = process.env.DIGITALOCEAN_POSTGRES;
|
const databaseURL = process.env.DIGITALOCEAN_POSTGRES;
|
||||||
|
@ -51,11 +51,11 @@ export async function pgBulkInsert({
|
||||||
tableName,
|
tableName,
|
||||||
client,
|
client,
|
||||||
}: {
|
}: {
|
||||||
data: Forecast[];
|
data: Question[];
|
||||||
tableName: string;
|
tableName: string;
|
||||||
client: PoolClient;
|
client: PoolClient;
|
||||||
}) {
|
}) {
|
||||||
if (!forecastTableNames.includes(tableName)) {
|
if (!questionTableNames.includes(tableName)) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Table ${tableName} not in whitelist; stopping to avoid tricky sql injections`
|
`Table ${tableName} not in whitelist; stopping to avoid tricky sql injections`
|
||||||
);
|
);
|
||||||
|
@ -171,11 +171,11 @@ export async function pgUpsert({
|
||||||
tableName,
|
tableName,
|
||||||
replacePlatform,
|
replacePlatform,
|
||||||
}: {
|
}: {
|
||||||
contents: Forecast[];
|
contents: Question[];
|
||||||
tableName: string;
|
tableName: string;
|
||||||
replacePlatform?: string;
|
replacePlatform?: string;
|
||||||
}) {
|
}) {
|
||||||
if (!forecastTableNames.includes(tableName)) {
|
if (!questionTableNames.includes(tableName)) {
|
||||||
throw Error(
|
throw Error(
|
||||||
`Table ${tableName} not in whitelist; stopping to avoid tricky sql injections`
|
`Table ${tableName} not in whitelist; stopping to avoid tricky sql injections`
|
||||||
);
|
);
|
||||||
|
|
15
src/backend/database/prisma.ts
Normal file
15
src/backend/database/prisma.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// allow global `var` declarations
|
||||||
|
// eslint-disable-next-line no-var
|
||||||
|
var prisma: PrismaClient | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prisma =
|
||||||
|
global.prisma ||
|
||||||
|
new PrismaClient({
|
||||||
|
log: ["query"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
|
|
@ -1,7 +1,7 @@
|
||||||
import { pgRead, pool } from "./database/pg-wrapper";
|
import { pgRead, pool } from "./database/pg-wrapper";
|
||||||
import { Forecast } from "./platforms";
|
import { Question } from "./platforms";
|
||||||
|
|
||||||
export async function getFrontpage(): Promise<Forecast[]> {
|
export async function getFrontpage(): Promise<Question[]> {
|
||||||
const res = await pool.query(
|
const res = await pool.query(
|
||||||
"SELECT frontpage_sliced FROM frontpage ORDER BY id DESC LIMIT 1"
|
"SELECT frontpage_sliced FROM frontpage ORDER BY id DESC LIMIT 1"
|
||||||
);
|
);
|
||||||
|
@ -9,7 +9,7 @@ export async function getFrontpage(): Promise<Forecast[]> {
|
||||||
return res.rows[0].frontpage_sliced;
|
return res.rows[0].frontpage_sliced;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFrontpageFull(): Promise<Forecast[]> {
|
export async function getFrontpageFull(): Promise<Question[]> {
|
||||||
const res = await pool.query(
|
const res = await pool.query(
|
||||||
"SELECT frontpage_full FROM frontpage ORDER BY id DESC LIMIT 1"
|
"SELECT frontpage_full FROM frontpage ORDER BY id DESC LIMIT 1"
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@ const migrate = async () => {
|
||||||
FantasySCOTUS: "fantasyscotus",
|
FantasySCOTUS: "fantasyscotus",
|
||||||
Foretold: "foretold",
|
Foretold: "foretold",
|
||||||
"GiveWell/OpenPhilanthropy": "givewellopenphil",
|
"GiveWell/OpenPhilanthropy": "givewellopenphil",
|
||||||
"Good Judgment": "goodjudgement",
|
"Good Judgment": "goodjudgment",
|
||||||
"Good Judgment Open": "goodjudgmentopen",
|
"Good Judgment Open": "goodjudgmentopen",
|
||||||
Infer: "infer",
|
Infer: "infer",
|
||||||
Kalshi: "kalshi",
|
Kalshi: "kalshi",
|
||||||
|
|
|
@ -3,7 +3,7 @@ import axios from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
const platformName = "betfair";
|
const platformName = "betfair";
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ async function whipIntoShape(data) {
|
||||||
async function processPredictions(data) {
|
async function processPredictions(data) {
|
||||||
let predictions = await whipIntoShape(data);
|
let predictions = await whipIntoShape(data);
|
||||||
// console.log(JSON.stringify(predictions, null, 4))
|
// console.log(JSON.stringify(predictions, null, 4))
|
||||||
let results: Forecast[] = predictions.map((prediction) => {
|
let results: Question[] = predictions.map((prediction) => {
|
||||||
/* if(Math.floor(Math.random() * 10) % 20 ==0){
|
/* if(Math.floor(Math.random() * 10) % 20 ==0){
|
||||||
console.log(JSON.stringify(prediction, null, 4))
|
console.log(JSON.stringify(prediction, null, 4))
|
||||||
} */
|
} */
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
const platformName = "fantasyscotus";
|
const platformName = "fantasyscotus";
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ async function processData(data) {
|
||||||
let historicalPercentageCorrect = data.stats.pcnt_correct;
|
let historicalPercentageCorrect = data.stats.pcnt_correct;
|
||||||
let historicalProbabilityCorrect =
|
let historicalProbabilityCorrect =
|
||||||
Number(historicalPercentageCorrect.replace("%", "")) / 100;
|
Number(historicalPercentageCorrect.replace("%", "")) / 100;
|
||||||
let results: Forecast[] = [];
|
let results: Question[] = [];
|
||||||
for (let event of events) {
|
for (let event of events) {
|
||||||
if (event.accuracy == "") {
|
if (event.accuracy == "") {
|
||||||
let id = `${platformName}-${event.id}`;
|
let id = `${platformName}-${event.id}`;
|
||||||
|
@ -75,7 +75,7 @@ async function processData(data) {
|
||||||
let predictionData = await getPredictionsData(event.docket_url);
|
let predictionData = await getPredictionsData(event.docket_url);
|
||||||
let pAffirm = predictionData.proportionAffirm;
|
let pAffirm = predictionData.proportionAffirm;
|
||||||
//let trackRecord = event.prediction.includes("Affirm") ? historicalProbabilityCorrect : 1-historicalProbabilityCorrect
|
//let trackRecord = event.prediction.includes("Affirm") ? historicalProbabilityCorrect : 1-historicalProbabilityCorrect
|
||||||
let eventObject: Forecast = {
|
let eventObject: Question = {
|
||||||
id: id,
|
id: id,
|
||||||
title: `In ${event.short_name}, the SCOTUS will affirm the lower court's decision`,
|
title: `In ${event.short_name}, the SCOTUS will affirm the lower court's decision`,
|
||||||
url: `https://fantasyscotus.net/user-predictions${event.docket_url}`,
|
url: `https://fantasyscotus.net/user-predictions${event.docket_url}`,
|
||||||
|
|
|
@ -16,7 +16,21 @@ import { smarkets } from "./smarkets";
|
||||||
import { wildeford } from "./wildeford";
|
import { wildeford } from "./wildeford";
|
||||||
import { xrisk } from "./xrisk";
|
import { xrisk } from "./xrisk";
|
||||||
|
|
||||||
export interface Forecast {
|
export interface QualityIndicators {
|
||||||
|
stars: number;
|
||||||
|
numforecasts?: number | string;
|
||||||
|
numforecasters?: number;
|
||||||
|
liquidity?: number | string;
|
||||||
|
volume?: number;
|
||||||
|
volume7Days?: number;
|
||||||
|
volume24Hours?: number;
|
||||||
|
address?: number;
|
||||||
|
tradevolume?: string;
|
||||||
|
pool?: any;
|
||||||
|
createdTime?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Question {
|
||||||
id: string;
|
id: string;
|
||||||
// "fantasyscotus-580"
|
// "fantasyscotus-580"
|
||||||
|
|
||||||
|
@ -53,7 +67,7 @@ export interface Forecast {
|
||||||
stars?: number;
|
stars?: number;
|
||||||
// 2
|
// 2
|
||||||
|
|
||||||
qualityindicators: any;
|
qualityindicators: QualityIndicators;
|
||||||
/*
|
/*
|
||||||
{
|
{
|
||||||
"numforecasts": 120,
|
"numforecasts": 120,
|
||||||
|
@ -63,8 +77,8 @@ export interface Forecast {
|
||||||
extra?: any;
|
extra?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetcher should return null if platform failed to fetch forecasts for some reason
|
// fetcher should return null if platform failed to fetch questions for some reason
|
||||||
export type PlatformFetcher = () => Promise<Forecast[] | null>;
|
export type PlatformFetcher = () => Promise<Question[] | null>;
|
||||||
|
|
||||||
export interface Platform {
|
export interface Platform {
|
||||||
name: string; // short name for ids and `platform` db column, e.g. "xrisk"
|
name: string; // short name for ids and `platform` db column, e.g. "xrisk"
|
||||||
|
@ -76,7 +90,7 @@ export interface Platform {
|
||||||
// draft for the future callback-based streaming/chunking API:
|
// draft for the future callback-based streaming/chunking API:
|
||||||
// interface FetchOptions {
|
// interface FetchOptions {
|
||||||
// since?: string; // some kind of cursor, Date object or opaque string?
|
// since?: string; // some kind of cursor, Date object or opaque string?
|
||||||
// save: (forecasts: Forecast[]) => Promise<void>;
|
// save: (questions: Question[]) => Promise<void>;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// export type PlatformFetcher = (options: FetchOptions) => Promise<void>;
|
// export type PlatformFetcher = (options: FetchOptions) => Promise<void>;
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
/* Imports */
|
/* Imports */
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Tabletojson } from "tabletojson";
|
|
||||||
|
|
||||||
import { applyIfSecretExists } from "../utils/getSecrets";
|
import { applyIfSecretExists } from "../utils/getSecrets";
|
||||||
import { measureTime } from "../utils/measureTime";
|
import { measureTime } from "../utils/measureTime";
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import toMarkdown from "../utils/toMarkdown";
|
import toMarkdown from "../utils/toMarkdown";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "infer";
|
const platformName = "infer";
|
||||||
|
@ -78,12 +77,12 @@ async function fetchStats(questionUrl, cookie) {
|
||||||
let comments_count = firstEmbeddedJson.question.comments_count;
|
let comments_count = firstEmbeddedJson.question.comments_count;
|
||||||
let numforecasters = firstEmbeddedJson.question.predictors_count;
|
let numforecasters = firstEmbeddedJson.question.predictors_count;
|
||||||
let numforecasts = firstEmbeddedJson.question.prediction_sets_count;
|
let numforecasts = firstEmbeddedJson.question.prediction_sets_count;
|
||||||
let forecastType = firstEmbeddedJson.question.type;
|
let questionType = firstEmbeddedJson.question.type;
|
||||||
if (
|
if (
|
||||||
forecastType.includes("Binary") ||
|
questionType.includes("Binary") ||
|
||||||
forecastType.includes("NonExclusiveOpinionPoolQuestion") ||
|
questionType.includes("NonExclusiveOpinionPoolQuestion") ||
|
||||||
forecastType.includes("Forecast::Question") ||
|
questionType.includes("Forecast::Question") ||
|
||||||
!forecastType.includes("Forecast::MultiTimePeriodQuestion")
|
!questionType.includes("Forecast::MultiTimePeriodQuestion")
|
||||||
) {
|
) {
|
||||||
options = firstEmbeddedJson.question.answers.map((answer) => ({
|
options = firstEmbeddedJson.question.answers.map((answer) => ({
|
||||||
name: answer.name,
|
name: answer.name,
|
||||||
|
@ -148,7 +147,7 @@ function sleep(ms) {
|
||||||
async function infer_inner(cookie: string) {
|
async function infer_inner(cookie: string) {
|
||||||
let i = 1;
|
let i = 1;
|
||||||
let response = await fetchPage(i, cookie);
|
let response = await fetchPage(i, cookie);
|
||||||
let results: Forecast[] = [];
|
let results: Question[] = [];
|
||||||
|
|
||||||
await measureTime(async () => {
|
await measureTime(async () => {
|
||||||
// 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.")
|
||||||
|
@ -179,7 +178,7 @@ async function infer_inner(cookie: string) {
|
||||||
let questionNumRegex = new RegExp("questions/([0-9]+)");
|
let questionNumRegex = new RegExp("questions/([0-9]+)");
|
||||||
let questionNum = url.match(questionNumRegex)[1]; //.split("questions/")[1].split("-")[0];
|
let questionNum = url.match(questionNumRegex)[1]; //.split("questions/")[1].split("-")[0];
|
||||||
let id = `${platformName}-${questionNum}`;
|
let id = `${platformName}-${questionNum}`;
|
||||||
let question: Forecast = {
|
let question: Question = {
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: title,
|
||||||
description: moreinfo.description,
|
description: moreinfo.description,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "manifold";
|
const platformName = "manifold";
|
||||||
|
@ -23,7 +23,7 @@ async function fetchData() {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStatistics(results: Forecast[]) {
|
function showStatistics(results: Question[]) {
|
||||||
console.log(`Num unresolved markets: ${results.length}`);
|
console.log(`Num unresolved markets: ${results.length}`);
|
||||||
let sum = (arr) => arr.reduce((tally, a) => tally + a, 0);
|
let sum = (arr) => arr.reduce((tally, a) => tally + a, 0);
|
||||||
let num2StarsOrMore = results.filter(
|
let num2StarsOrMore = results.filter(
|
||||||
|
@ -44,7 +44,7 @@ function showStatistics(results: Forecast[]) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processPredictions(predictions) {
|
async function processPredictions(predictions) {
|
||||||
let results: Forecast[] = await predictions.map((prediction) => {
|
let results: Question[] = await predictions.map((prediction) => {
|
||||||
let id = `${platformName}-${prediction.id}`; // oops, doesn't match platform name
|
let id = `${platformName}-${prediction.id}`; // oops, doesn't match platform name
|
||||||
let probability = prediction.probability;
|
let probability = prediction.probability;
|
||||||
let options = [
|
let options = [
|
||||||
|
@ -59,7 +59,7 @@ async function processPredictions(predictions) {
|
||||||
type: "PROBABILITY",
|
type: "PROBABILITY",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const result: Forecast = {
|
const result: Question = {
|
||||||
id: id,
|
id: id,
|
||||||
title: prediction.question,
|
title: prediction.question,
|
||||||
url: prediction.url,
|
url: prediction.url,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "polymarket";
|
const platformName = "polymarket";
|
||||||
|
@ -68,7 +68,7 @@ export const polymarket: Platform = {
|
||||||
label: "PolyMarket",
|
label: "PolyMarket",
|
||||||
color: "#00314e",
|
color: "#00314e",
|
||||||
async fetcher() {
|
async fetcher() {
|
||||||
let results: Forecast[] = [];
|
let results: Question[] = [];
|
||||||
let webpageEndpointData = await fetchAllContractInfo();
|
let webpageEndpointData = await fetchAllContractInfo();
|
||||||
for (let marketInfo of webpageEndpointData) {
|
for (let marketInfo of webpageEndpointData) {
|
||||||
let address = marketInfo.marketMakerAddress;
|
let address = marketInfo.marketMakerAddress;
|
||||||
|
@ -102,7 +102,7 @@ export const polymarket: Platform = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: Forecast = {
|
let result: Question = {
|
||||||
id: id,
|
id: id,
|
||||||
title: marketInfo.question,
|
title: marketInfo.question,
|
||||||
url: "https://polymarket.com/market/" + marketInfo.slug,
|
url: "https://polymarket.com/market/" + marketInfo.slug,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import toMarkdown from "../utils/toMarkdown";
|
import toMarkdown from "../utils/toMarkdown";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
const platformName = "rootclaim";
|
const platformName = "rootclaim";
|
||||||
const jsonEndpoint =
|
const jsonEndpoint =
|
||||||
|
@ -50,7 +50,7 @@ export const rootclaim: Platform = {
|
||||||
color: "#0d1624",
|
color: "#0d1624",
|
||||||
async fetcher() {
|
async fetcher() {
|
||||||
const claims = await fetchAllRootclaims();
|
const claims = await fetchAllRootclaims();
|
||||||
const results: Forecast[] = [];
|
const results: Question[] = [];
|
||||||
|
|
||||||
for (const claim of claims) {
|
for (const claim of claims) {
|
||||||
const id = `${platformName}-${claim.slug.toLowerCase()}`;
|
const id = `${platformName}-${claim.slug.toLowerCase()}`;
|
||||||
|
@ -71,7 +71,7 @@ export const rootclaim: Platform = {
|
||||||
|
|
||||||
const description = await fetchDescription(url, claim.isclaim);
|
const description = await fetchDescription(url, claim.isclaim);
|
||||||
|
|
||||||
let obj: Forecast = {
|
let obj: Question = {
|
||||||
id,
|
id,
|
||||||
title: toMarkdown(claim.question).replace("\n", ""),
|
title: toMarkdown(claim.question).replace("\n", ""),
|
||||||
url,
|
url,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { calculateStars } from "../utils/stars";
|
import { calculateStars } from "../utils/stars";
|
||||||
import { Forecast, Platform } from "./";
|
import { Platform, Question } from "./";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
const platformName = "smarkets";
|
const platformName = "smarkets";
|
||||||
|
@ -159,7 +159,7 @@ export const smarkets: Platform = {
|
||||||
name = name+ (contractName=="Yes"?'':` (${contracts["contracts"][0].name})`)
|
name = name+ (contractName=="Yes"?'':` (${contracts["contracts"][0].name})`)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let result: Forecast = {
|
let result: Question = {
|
||||||
id: id,
|
id: id,
|
||||||
title: name,
|
title: name,
|
||||||
url: "https://smarkets.com/event/" + market.event_id + market.slug,
|
url: "https://smarkets.com/event/" + market.event_id + market.slug,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import algoliasearch from "algoliasearch";
|
import algoliasearch from "algoliasearch";
|
||||||
|
|
||||||
import { pgRead } from "../database/pg-wrapper";
|
import { Question } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "../database/prisma";
|
||||||
import { platforms } from "../platforms";
|
import { platforms } from "../platforms";
|
||||||
|
|
||||||
let cookie = process.env.ALGOLIA_MASTER_API_KEY;
|
let cookie = process.env.ALGOLIA_MASTER_API_KEY;
|
||||||
|
@ -8,10 +10,14 @@ 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");
|
||||||
|
|
||||||
let getoptionsstringforsearch = (record: any) => {
|
export type AlgoliaQuestion = Omit<Question, "timestamp"> & {
|
||||||
|
timestamp: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getoptionsstringforsearch = (record: Question): string => {
|
||||||
let result = "";
|
let result = "";
|
||||||
if (!!record.options && record.options.length > 0) {
|
if (!!record.options && (record.options as any[]).length > 0) {
|
||||||
result = record.options
|
result = (record.options as any[])
|
||||||
.map((option: any) => option.name || null)
|
.map((option: any) => option.name || null)
|
||||||
.filter((x: any) => x != null)
|
.filter((x: any) => x != null)
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
@ -19,23 +25,23 @@ let getoptionsstringforsearch = (record: any) => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function rebuildAlgoliaDatabaseTheEasyWay() {
|
export async function rebuildAlgoliaDatabase() {
|
||||||
let records: any[] = await pgRead({
|
const questions = await prisma.question.findMany();
|
||||||
tableName: "questions",
|
|
||||||
});
|
|
||||||
|
|
||||||
const platformNameToLabel = Object.fromEntries(
|
const platformNameToLabel = Object.fromEntries(
|
||||||
platforms.map((platform) => [platform.name, platform.label])
|
platforms.map((platform) => [platform.name, platform.label])
|
||||||
);
|
);
|
||||||
|
|
||||||
records = records.map((record, index: number) => ({
|
const records: AlgoliaQuestion[] = questions.map(
|
||||||
...record,
|
(question, index: number) => ({
|
||||||
platformLabel: platformNameToLabel[record.platform] || record.platform,
|
...question,
|
||||||
has_numforecasts: record.numforecasts ? true : false,
|
timestamp: `${question.timestamp}`,
|
||||||
|
platformLabel:
|
||||||
|
platformNameToLabel[question.platform] || question.platform,
|
||||||
objectID: index,
|
objectID: index,
|
||||||
optionsstringforsearch: getoptionsstringforsearch(record),
|
optionsstringforsearch: getoptionsstringforsearch(question),
|
||||||
}));
|
})
|
||||||
// this is necessary to filter by missing attributes https://www.algolia.com/doc/guides/managing-results/refine-results/filtering/how-to/filter-by-null-or-missing-attributes/
|
);
|
||||||
|
|
||||||
if (index.exists()) {
|
if (index.exists()) {
|
||||||
console.log("Index exists");
|
console.log("Index exists");
|
||||||
|
@ -45,5 +51,3 @@ export async function rebuildAlgoliaDatabaseTheEasyWay() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rebuildAlgoliaDatabase = rebuildAlgoliaDatabaseTheEasyWay;
|
|
||||||
|
|
|
@ -8,14 +8,14 @@ import { pgRead } from "../../database/pg-wrapper";
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
let getQualityIndicators = (forecast) =>
|
const getQualityIndicators = (question) =>
|
||||||
Object.entries(forecast.qualityindicators)
|
Object.entries(question.qualityindicators)
|
||||||
.map((entry) => `${entry[0]}: ${entry[1]}`)
|
.map((entry) => `${entry[0]}: ${entry[1]}`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
|
|
||||||
let main = async () => {
|
const main = async () => {
|
||||||
let highQualityPlatforms = [
|
let highQualityPlatforms = [
|
||||||
"CSET-foretell",
|
"CSET-foretell",
|
||||||
"Foretold",
|
"Foretold",
|
||||||
|
@ -24,21 +24,21 @@ let main = async () => {
|
||||||
"PredictIt",
|
"PredictIt",
|
||||||
"Rootclaim",
|
"Rootclaim",
|
||||||
];
|
];
|
||||||
let json = await pgRead({ tableName: "questions" });
|
const json = await pgRead({ tableName: "questions" });
|
||||||
console.log(json.length);
|
console.log(json.length);
|
||||||
//let uniquePlatforms = [...new Set(json.map(forecast => forecast.platform))]
|
//let uniquePlatforms = [...new Set(json.map(forecast => forecast.platform))]
|
||||||
//console.log(uniquePlatforms)
|
//console.log(uniquePlatforms)
|
||||||
|
|
||||||
let forecastsFromGoodPlatforms = json.filter((forecast) =>
|
const questionsFromGoodPlatforms = json.filter((question) =>
|
||||||
highQualityPlatforms.includes(forecast.platform)
|
highQualityPlatforms.includes(question.platform)
|
||||||
);
|
);
|
||||||
let tsv =
|
const tsv =
|
||||||
"index\ttitle\turl\tqualityindicators\n" +
|
"index\ttitle\turl\tqualityindicators\n" +
|
||||||
forecastsFromGoodPlatforms
|
questionsFromGoodPlatforms
|
||||||
.map((forecast, index) => {
|
.map((question, index) => {
|
||||||
let row = `${index}\t${forecast.title}\t${
|
let row = `${index}\t${question.title}\t${
|
||||||
forecast.url
|
question.url
|
||||||
}\t${getQualityIndicators(forecast)}`;
|
}\t${getQualityIndicators(question)}`;
|
||||||
console.log(row);
|
console.log(row);
|
||||||
return row;
|
return row;
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/* Imports */
|
/* Imports */
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
|
||||||
|
import { shuffleArray } from "../../../utils";
|
||||||
import { pgRead } from "../../database/pg-wrapper";
|
import { pgRead } from "../../database/pg-wrapper";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
|
@ -8,42 +9,33 @@ import { pgRead } from "../../database/pg-wrapper";
|
||||||
/* Utilities */
|
/* Utilities */
|
||||||
|
|
||||||
/* Support functions */
|
/* Support functions */
|
||||||
let getQualityIndicators = (forecast) =>
|
let getQualityIndicators = (question) =>
|
||||||
Object.entries(forecast.qualityindicators)
|
Object.entries(question.qualityindicators)
|
||||||
.map((entry) => `${entry[0]}: ${entry[1]}`)
|
.map((entry) => `${entry[0]}: ${entry[1]}`)
|
||||||
.join("; ");
|
.join("; ");
|
||||||
|
|
||||||
let shuffleArray = (array) => {
|
|
||||||
// See: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
|
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
|
||||||
}
|
|
||||||
return array;
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
|
|
||||||
let main = async () => {
|
let main = async () => {
|
||||||
let highQualityPlatforms = ["Metaculus"]; // ['CSET-foretell', 'Foretold', 'Good Judgment Open', 'Metaculus', 'PredictIt', 'Rootclaim']
|
let highQualityPlatforms = ["Metaculus"]; // ['CSET-foretell', 'Foretold', 'Good Judgment Open', 'Metaculus', 'PredictIt', 'Rootclaim']
|
||||||
let json = await pgRead({ tableName: "questions" });
|
let json = await pgRead({ tableName: "questions" });
|
||||||
console.log(json.length);
|
console.log(json.length);
|
||||||
//let uniquePlatforms = [...new Set(json.map(forecast => forecast.platform))]
|
//let uniquePlatforms = [...new Set(json.map(question => question.platform))]
|
||||||
//console.log(uniquePlatforms)
|
//console.log(uniquePlatforms)
|
||||||
|
|
||||||
let forecastsFromGoodPlatforms = json.filter((forecast) =>
|
let questionsFromGoodPlatforms = json.filter((question) =>
|
||||||
highQualityPlatforms.includes(forecast.platform)
|
highQualityPlatforms.includes(question.platform)
|
||||||
);
|
);
|
||||||
let forecastsFromGoodPlatformsShuffled = shuffleArray(
|
let questionsFromGoodPlatformsShuffled = shuffleArray(
|
||||||
forecastsFromGoodPlatforms
|
questionsFromGoodPlatforms
|
||||||
);
|
);
|
||||||
let tsv =
|
let tsv =
|
||||||
"index\ttitle\turl\tqualityindicators\n" +
|
"index\ttitle\turl\tqualityindicators\n" +
|
||||||
forecastsFromGoodPlatforms
|
questionsFromGoodPlatforms
|
||||||
.map((forecast, index) => {
|
.map((question, index) => {
|
||||||
let row = `${index}\t${forecast.title}\t${
|
let row = `${index}\t${question.title}\t${
|
||||||
forecast.url
|
question.url
|
||||||
}\t${getQualityIndicators(forecast)}`;
|
}\t${getQualityIndicators(question)}`;
|
||||||
console.log(row);
|
console.log(row);
|
||||||
return row;
|
return row;
|
||||||
})
|
})
|
||||||
|
|
5
src/graphql/build-schema.js
Normal file
5
src/graphql/build-schema.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// see https://pothos-graphql.dev/docs/guide/generating-client-types#export-schema-in-a-js-file,
|
||||||
|
// but we use ts-node instead of @boost/module
|
||||||
|
require("ts-node").register({});
|
||||||
|
|
||||||
|
module.exports = require("./schema/index.ts");
|
40
src/graphql/builder.ts
Normal file
40
src/graphql/builder.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import SchemaBuilder from "@pothos/core";
|
||||||
|
import PrismaPlugin from "@pothos/plugin-prisma";
|
||||||
|
import RelayPlugin from "@pothos/plugin-relay";
|
||||||
|
|
||||||
|
import { prisma } from "../backend/database/prisma";
|
||||||
|
|
||||||
|
import type PrismaTypes from "@pothos/plugin-prisma/generated";
|
||||||
|
export const builder = new SchemaBuilder<{
|
||||||
|
PrismaTypes: PrismaTypes;
|
||||||
|
Scalars: {
|
||||||
|
Date: {
|
||||||
|
Input: Date;
|
||||||
|
Output: Date;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>({
|
||||||
|
plugins: [PrismaPlugin, RelayPlugin],
|
||||||
|
prisma: {
|
||||||
|
client: prisma,
|
||||||
|
},
|
||||||
|
relayOptions: {
|
||||||
|
clientMutationId: "omit",
|
||||||
|
cursorType: "String",
|
||||||
|
// these are required for some reason, though it's not documented and probably a bug
|
||||||
|
brandLoadedObjects: undefined,
|
||||||
|
encodeGlobalID: undefined,
|
||||||
|
decodeGlobalID: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.scalarType("Date", {
|
||||||
|
description: "Date serialized as the Unix timestamp.",
|
||||||
|
serialize: (d) => d.getTime() / 1000,
|
||||||
|
parseValue: (d) => {
|
||||||
|
return new Date(d as string); // not sure if this is correct, need to check
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.queryType({});
|
||||||
|
builder.mutationType({});
|
1
src/graphql/introspection.json
Normal file
1
src/graphql/introspection.json
Normal file
File diff suppressed because one or more lines are too long
110
src/graphql/schema/dashboards.ts
Normal file
110
src/graphql/schema/dashboards.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
import { Dashboard } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "../../backend/database/prisma";
|
||||||
|
import { hash } from "../../backend/utils/hash";
|
||||||
|
import { builder } from "../builder";
|
||||||
|
import { QuestionObj } from "./questions";
|
||||||
|
|
||||||
|
const DashboardObj = builder.objectRef<Dashboard>("Dashboard").implement({
|
||||||
|
fields: (t) => ({
|
||||||
|
id: t.exposeID("id"),
|
||||||
|
title: t.exposeString("title", {
|
||||||
|
description: "The title of the dashboard",
|
||||||
|
}),
|
||||||
|
description: t.exposeString("description", {
|
||||||
|
description: "The longer description of the dashboard",
|
||||||
|
}),
|
||||||
|
creator: t.exposeString("creator", {
|
||||||
|
description: 'The creator of the dashboard, e.g. "Peter Parker"',
|
||||||
|
}),
|
||||||
|
questions: t.field({
|
||||||
|
type: [QuestionObj],
|
||||||
|
description: "The list of questions on the dashboard",
|
||||||
|
resolve: async (parent) => {
|
||||||
|
return await prisma.question.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: parent.contents as string[],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.queryField("dashboard", (t) =>
|
||||||
|
t.field({
|
||||||
|
type: DashboardObj,
|
||||||
|
description: "Look up a single dashboard by its id",
|
||||||
|
args: {
|
||||||
|
id: t.arg({ type: "ID", required: true }),
|
||||||
|
},
|
||||||
|
resolve: async (parent, args) => {
|
||||||
|
return await prisma.dashboard.findUnique({
|
||||||
|
where: {
|
||||||
|
id: String(args.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const CreateDashboardResult = builder
|
||||||
|
.objectRef<{ dashboard: Dashboard }>("CreateDashboardResult")
|
||||||
|
.implement({
|
||||||
|
fields: (t) => ({
|
||||||
|
dashboard: t.field({
|
||||||
|
type: DashboardObj,
|
||||||
|
resolve: (parent) => parent.dashboard,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const CreateDashboardInput = builder.inputType("CreateDashboardInput", {
|
||||||
|
fields: (t) => ({
|
||||||
|
title: t.string({
|
||||||
|
required: true,
|
||||||
|
description: "The title of the dashboard",
|
||||||
|
}),
|
||||||
|
description: t.string({
|
||||||
|
description: "The longer description of the dashboard",
|
||||||
|
}),
|
||||||
|
creator: t.string({
|
||||||
|
description: 'The creator of the dashboard, e.g. "Peter Parker"',
|
||||||
|
}),
|
||||||
|
ids: t.idList({ required: true, description: "List of question ids" }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.mutationField("createDashboard", (t) =>
|
||||||
|
t.field({
|
||||||
|
type: CreateDashboardResult,
|
||||||
|
description:
|
||||||
|
"Create a new dashboard; if the dashboard with given ids already exists then it will be returned instead.",
|
||||||
|
args: {
|
||||||
|
input: t.arg({ type: CreateDashboardInput, required: true }),
|
||||||
|
},
|
||||||
|
resolve: async (parent, args) => {
|
||||||
|
const id = hash(JSON.stringify(args.input.ids));
|
||||||
|
const dashboard = await prisma.dashboard.upsert({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
update: {},
|
||||||
|
create: {
|
||||||
|
id,
|
||||||
|
title: args.input.title,
|
||||||
|
description: args.input.description || "",
|
||||||
|
creator: args.input.creator || "",
|
||||||
|
contents: args.input.ids,
|
||||||
|
extra: [],
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
dashboard,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
30
src/graphql/schema/frontpage.ts
Normal file
30
src/graphql/schema/frontpage.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { Question } from "@prisma/client";
|
||||||
|
|
||||||
|
import { prisma } from "../../backend/database/prisma";
|
||||||
|
import { getFrontpage } from "../../backend/frontpage";
|
||||||
|
import { builder } from "../builder";
|
||||||
|
import { QuestionObj } from "./questions";
|
||||||
|
|
||||||
|
builder.queryField("frontpage", (t) =>
|
||||||
|
t.field({
|
||||||
|
type: [QuestionObj],
|
||||||
|
description: "Get a list of questions that are currently on the frontpage",
|
||||||
|
resolve: async () => {
|
||||||
|
const legacyQuestions = await getFrontpage();
|
||||||
|
const ids = legacyQuestions.map((q) => q.id);
|
||||||
|
const questions = await prisma.question.findMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: ids,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const id2q: { [k: string]: Question } = {};
|
||||||
|
for (const q of questions) {
|
||||||
|
id2q[q.id] = q;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids.map((id) => id2q[id] || null).filter((q) => q !== null);
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
8
src/graphql/schema/index.ts
Normal file
8
src/graphql/schema/index.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import "./dashboards";
|
||||||
|
import "./frontpage";
|
||||||
|
import "./questions";
|
||||||
|
import "./search";
|
||||||
|
|
||||||
|
import { builder } from "../builder";
|
||||||
|
|
||||||
|
export const schema = builder.toSchema({});
|
116
src/graphql/schema/questions.ts
Normal file
116
src/graphql/schema/questions.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { prisma } from "../../backend/database/prisma";
|
||||||
|
import { platforms, QualityIndicators } from "../../backend/platforms";
|
||||||
|
import { builder } from "../builder";
|
||||||
|
|
||||||
|
const PlatformObj = builder.objectRef<string>("Platform").implement({
|
||||||
|
description: "Forecasting platform supported by Metaforecast",
|
||||||
|
fields: (t) => ({
|
||||||
|
id: t.id({
|
||||||
|
description: 'Short unique platform name, e.g. "xrisk"',
|
||||||
|
resolve: (x) => x,
|
||||||
|
}),
|
||||||
|
label: t.string({
|
||||||
|
description:
|
||||||
|
'Platform name for displaying on frontend etc., e.g. "X-risk estimates"',
|
||||||
|
resolve: (platformName) => {
|
||||||
|
if (platformName === "metaforecast") {
|
||||||
|
return "Metaforecast";
|
||||||
|
}
|
||||||
|
if (platformName === "guesstimate") {
|
||||||
|
return "Guesstimate";
|
||||||
|
}
|
||||||
|
// kinda slow and repetitive, TODO - store a map {name => platform} somewhere and `getPlatform` util function?
|
||||||
|
const platform = platforms.find((p) => p.name === platformName);
|
||||||
|
if (!platform) {
|
||||||
|
throw new Error(`Unknown platform ${platformName}`);
|
||||||
|
}
|
||||||
|
return platform.label;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QualityIndicatorsObj = builder
|
||||||
|
.objectRef<QualityIndicators>("QualityIndicators")
|
||||||
|
.implement({
|
||||||
|
description: "Various indicators of the question's quality",
|
||||||
|
fields: (t) => ({
|
||||||
|
stars: t.exposeInt("stars", {
|
||||||
|
description: "0 to 5",
|
||||||
|
}),
|
||||||
|
numForecasts: t.int({
|
||||||
|
nullable: true,
|
||||||
|
resolve: (parent) =>
|
||||||
|
parent.numforecasts === undefined
|
||||||
|
? undefined
|
||||||
|
: Number(parent.numforecasts),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ProbabilityOptionObj = builder
|
||||||
|
.objectRef<{ name: string; probability: number }>("ProbabilityOption")
|
||||||
|
.implement({
|
||||||
|
fields: (t) => ({
|
||||||
|
name: t.exposeString("name", { nullable: true }),
|
||||||
|
probability: t.exposeFloat("probability", {
|
||||||
|
description: "0 to 1",
|
||||||
|
nullable: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuestionObj = builder.prismaObject("Question", {
|
||||||
|
findUnique: (question) => ({ id: question.id }),
|
||||||
|
fields: (t) => ({
|
||||||
|
id: t.exposeID("id", {
|
||||||
|
description: "Unique string which identifies the question",
|
||||||
|
}),
|
||||||
|
title: t.exposeString("title"),
|
||||||
|
description: t.exposeString("description"),
|
||||||
|
url: t.exposeString("url", {
|
||||||
|
description:
|
||||||
|
"Non-unique, a very small number of platforms have a page for more than one prediction",
|
||||||
|
}),
|
||||||
|
timestamp: t.field({
|
||||||
|
type: "Date",
|
||||||
|
description: "Timestamp at which metaforecast fetched the question",
|
||||||
|
resolve: (parent) => parent.timestamp,
|
||||||
|
}),
|
||||||
|
platform: t.field({
|
||||||
|
type: PlatformObj,
|
||||||
|
resolve: (parent) => parent.platform,
|
||||||
|
}),
|
||||||
|
qualityIndicators: t.field({
|
||||||
|
type: QualityIndicatorsObj,
|
||||||
|
resolve: (parent) => parent.qualityindicators as any as QualityIndicators,
|
||||||
|
}),
|
||||||
|
options: t.field({
|
||||||
|
type: [ProbabilityOptionObj],
|
||||||
|
resolve: ({ options }) => {
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return options as any[];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
visualization: t.string({
|
||||||
|
resolve: (parent) => (parent.extra as any)?.visualization, // used for guesstimate only, see searchGuesstimate.ts
|
||||||
|
nullable: true,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.queryField("questions", (t) =>
|
||||||
|
t.prismaConnection(
|
||||||
|
{
|
||||||
|
type: "Question",
|
||||||
|
cursor: "id",
|
||||||
|
maxSize: 1000,
|
||||||
|
resolve: (query, parent, args, context, info) =>
|
||||||
|
prisma.question.findMany({ ...query }),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
{}
|
||||||
|
)
|
||||||
|
);
|
70
src/graphql/schema/search.ts
Normal file
70
src/graphql/schema/search.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { AlgoliaQuestion } from "../../backend/utils/algolia";
|
||||||
|
import searchGuesstimate from "../../web/worker/searchGuesstimate";
|
||||||
|
import searchWithAlgolia from "../../web/worker/searchWithAlgolia";
|
||||||
|
import { builder } from "../builder";
|
||||||
|
import { QuestionObj } from "./questions";
|
||||||
|
|
||||||
|
const SearchInput = builder.inputType("SearchInput", {
|
||||||
|
fields: (t) => ({
|
||||||
|
query: t.string({ required: true }),
|
||||||
|
starsThreshold: t.int({
|
||||||
|
description: "Minimum number of stars on a question",
|
||||||
|
}),
|
||||||
|
forecastsThreshold: t.int({
|
||||||
|
description: "Minimum number of forecasts on a question",
|
||||||
|
}),
|
||||||
|
forecastingPlatforms: t.stringList({
|
||||||
|
description: "List of platform ids to filter by",
|
||||||
|
}),
|
||||||
|
limit: t.int(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.queryField("searchQuestions", (t) =>
|
||||||
|
t.field({
|
||||||
|
type: [QuestionObj],
|
||||||
|
description:
|
||||||
|
"Search for questions; uses Algolia instead of the primary metaforecast database",
|
||||||
|
args: {
|
||||||
|
input: t.arg({ type: SearchInput, required: true }),
|
||||||
|
},
|
||||||
|
resolve: async (parent, { input }) => {
|
||||||
|
// defs
|
||||||
|
const query = input.query === undefined ? "" : input.query;
|
||||||
|
if (query === "") return [];
|
||||||
|
const forecastsThreshold = input.forecastsThreshold;
|
||||||
|
const starsThreshold = input.starsThreshold;
|
||||||
|
const platformsIncludeGuesstimate =
|
||||||
|
input.forecastingPlatforms?.includes("guesstimate") &&
|
||||||
|
starsThreshold <= 1;
|
||||||
|
|
||||||
|
// preparation
|
||||||
|
const unawaitedAlgoliaResponse = searchWithAlgolia({
|
||||||
|
queryString: query,
|
||||||
|
hitsPerPage: input.limit + 50,
|
||||||
|
starsThreshold,
|
||||||
|
filterByPlatforms: input.forecastingPlatforms,
|
||||||
|
forecastsThreshold,
|
||||||
|
});
|
||||||
|
|
||||||
|
let results: AlgoliaQuestion[] = [];
|
||||||
|
|
||||||
|
// consider the guesstimate and the non-guesstimate cases separately.
|
||||||
|
if (platformsIncludeGuesstimate) {
|
||||||
|
const [responsesNotGuesstimate, responsesGuesstimate] =
|
||||||
|
await Promise.all([
|
||||||
|
unawaitedAlgoliaResponse,
|
||||||
|
searchGuesstimate(query),
|
||||||
|
]); // faster than two separate requests
|
||||||
|
results = [...responsesNotGuesstimate, ...responsesGuesstimate];
|
||||||
|
} else {
|
||||||
|
results = await unawaitedAlgoliaResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map((q) => ({
|
||||||
|
...q,
|
||||||
|
timestamp: new Date(q.timestamp),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
133
src/graphql/types.generated.ts
Normal file
133
src/graphql/types.generated.ts
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
export type Maybe<T> = T | null;
|
||||||
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||||
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||||
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
|
export type Scalars = {
|
||||||
|
ID: string;
|
||||||
|
String: string;
|
||||||
|
Boolean: boolean;
|
||||||
|
Int: number;
|
||||||
|
Float: number;
|
||||||
|
/** Date serialized as the Unix timestamp. */
|
||||||
|
Date: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDashboardInput = {
|
||||||
|
creator?: InputMaybe<Scalars['String']>;
|
||||||
|
description?: InputMaybe<Scalars['String']>;
|
||||||
|
ids: Array<Scalars['ID']>;
|
||||||
|
title: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDashboardResult = {
|
||||||
|
__typename?: 'CreateDashboardResult';
|
||||||
|
dashboard: Dashboard;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Dashboard = {
|
||||||
|
__typename?: 'Dashboard';
|
||||||
|
creator: Scalars['String'];
|
||||||
|
description: Scalars['String'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
questions: Array<Question>;
|
||||||
|
title: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
createDashboard: CreateDashboardResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateDashboardArgs = {
|
||||||
|
input: CreateDashboardInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageInfo = {
|
||||||
|
__typename?: 'PageInfo';
|
||||||
|
endCursor?: Maybe<Scalars['String']>;
|
||||||
|
hasNextPage: Scalars['Boolean'];
|
||||||
|
hasPreviousPage: Scalars['Boolean'];
|
||||||
|
startCursor?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Platform supported by metaforecast */
|
||||||
|
export type Platform = {
|
||||||
|
__typename?: 'Platform';
|
||||||
|
id: Scalars['ID'];
|
||||||
|
label: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProbabilityOption = {
|
||||||
|
__typename?: 'ProbabilityOption';
|
||||||
|
name?: Maybe<Scalars['String']>;
|
||||||
|
probability?: Maybe<Scalars['Float']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Various indicators of the question's quality */
|
||||||
|
export type QualityIndicators = {
|
||||||
|
__typename?: 'QualityIndicators';
|
||||||
|
numForecasts?: Maybe<Scalars['Int']>;
|
||||||
|
stars: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Query = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
dashboard: Dashboard;
|
||||||
|
frontpage: Array<Question>;
|
||||||
|
questions: QueryQuestionsConnection;
|
||||||
|
searchQuestions: Array<Question>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryDashboardArgs = {
|
||||||
|
id: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryQuestionsArgs = {
|
||||||
|
after?: InputMaybe<Scalars['String']>;
|
||||||
|
before?: InputMaybe<Scalars['String']>;
|
||||||
|
first?: InputMaybe<Scalars['Int']>;
|
||||||
|
last?: InputMaybe<Scalars['Int']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QuerySearchQuestionsArgs = {
|
||||||
|
input: SearchInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryQuestionsConnection = {
|
||||||
|
__typename?: 'QueryQuestionsConnection';
|
||||||
|
edges: Array<Maybe<QueryQuestionsConnectionEdge>>;
|
||||||
|
pageInfo: PageInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QueryQuestionsConnectionEdge = {
|
||||||
|
__typename?: 'QueryQuestionsConnectionEdge';
|
||||||
|
cursor: Scalars['String'];
|
||||||
|
node: Question;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Question = {
|
||||||
|
__typename?: 'Question';
|
||||||
|
description: Scalars['String'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
options: Array<ProbabilityOption>;
|
||||||
|
platform: Platform;
|
||||||
|
qualityIndicators: QualityIndicators;
|
||||||
|
timestamp: Scalars['Date'];
|
||||||
|
title: Scalars['String'];
|
||||||
|
url: Scalars['String'];
|
||||||
|
visualization?: Maybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SearchInput = {
|
||||||
|
forecastingPlatforms?: InputMaybe<Array<Scalars['String']>>;
|
||||||
|
forecastsThreshold?: InputMaybe<Scalars['Int']>;
|
||||||
|
limit?: InputMaybe<Scalars['Int']>;
|
||||||
|
query: Scalars['String'];
|
||||||
|
starsThreshold?: InputMaybe<Scalars['Int']>;
|
||||||
|
};
|
|
@ -1,13 +1,15 @@
|
||||||
import "nprogress/nprogress.css";
|
import "nprogress/nprogress.css";
|
||||||
import "../styles/main.css";
|
import "../styles/main.css";
|
||||||
|
|
||||||
|
import PlausibleProvider from "next-plausible";
|
||||||
|
import { withUrqlClient } from "next-urql";
|
||||||
|
import { AppProps } from "next/app";
|
||||||
import Router from "next/router";
|
import Router from "next/router";
|
||||||
import NProgress from "nprogress";
|
import NProgress from "nprogress";
|
||||||
|
|
||||||
import PlausibleProvider from "next-plausible";
|
import { getUrqlClientOptions } from "../web/urql";
|
||||||
|
|
||||||
Router.events.on("routeChangeStart", (as, { shallow }) => {
|
Router.events.on("routeChangeStart", (as, { shallow }) => {
|
||||||
console.log(shallow);
|
|
||||||
if (!shallow) {
|
if (!shallow) {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
}
|
}
|
||||||
|
@ -15,7 +17,7 @@ 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 }) {
|
function MyApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<PlausibleProvider domain="metaforecast.org">
|
<PlausibleProvider domain="metaforecast.org">
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
@ -23,4 +25,6 @@ function MyApp({ Component, pageProps }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MyApp;
|
export default withUrqlClient((ssr) => getUrqlClientOptions(ssr), {
|
||||||
|
ssr: false,
|
||||||
|
})(MyApp);
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next/types";
|
|
||||||
|
|
||||||
import { getFrontpageFull } from "../../backend/frontpage";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
let frontpageFull = await getFrontpageFull();
|
|
||||||
console.log(frontpageFull.map((element) => element.title).slice(0, 5));
|
|
||||||
console.log("...");
|
|
||||||
res.status(200).json(frontpageFull);
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next/types";
|
|
||||||
|
|
||||||
import { pgInsertIntoDashboard } from "../../backend/database/pg-wrapper";
|
|
||||||
import { hash } from "../../backend/utils/hash";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
res.status(400).send("Expected POST request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = req.body;
|
|
||||||
console.log(body);
|
|
||||||
try {
|
|
||||||
let id = hash(JSON.stringify(body.ids));
|
|
||||||
let pgResponse = await pgInsertIntoDashboard({
|
|
||||||
datum: {
|
|
||||||
id: id,
|
|
||||||
title: body.title || "",
|
|
||||||
description: body.description || "",
|
|
||||||
contents: body.ids,
|
|
||||||
creator: body.creator || "",
|
|
||||||
extra: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
res.status(200).send({
|
|
||||||
dashboardId: id,
|
|
||||||
pgResponse: pgResponse,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({
|
|
||||||
id: null,
|
|
||||||
pgResponse: JSON.stringify(error),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next/types";
|
|
||||||
|
|
||||||
import { pgGetByIds } from "../../backend/database/pg-wrapper";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
res.status(400).send("Expected POST request");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(req.body);
|
|
||||||
let id = req.body.id;
|
|
||||||
console.log(id);
|
|
||||||
let dashboardItemArray = await pgGetByIds({
|
|
||||||
ids: [id],
|
|
||||||
table: "dashboards",
|
|
||||||
});
|
|
||||||
if (!!dashboardItemArray && dashboardItemArray.length > 0) {
|
|
||||||
let dashboardItem = dashboardItemArray[0];
|
|
||||||
console.log(dashboardItem);
|
|
||||||
let dashboardContents = await pgGetByIds({
|
|
||||||
ids: dashboardItem.contents,
|
|
||||||
table: "questions",
|
|
||||||
});
|
|
||||||
res.status(200).send({
|
|
||||||
dashboardContents,
|
|
||||||
dashboardItem,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
res.status(404).send({ error: `Dashboard not found with id ${id}` });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next/types";
|
|
||||||
|
|
||||||
import { getFrontpage } from "../../backend/frontpage";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
let frontpageElements = await getFrontpage();
|
|
||||||
res.status(200).json(frontpageElements);
|
|
||||||
}
|
|
13
src/pages/api/graphql.ts
Normal file
13
src/pages/api/graphql.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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
|
||||||
|
import { createServer } from "@graphql-yoga/node";
|
||||||
|
|
||||||
|
import { schema } from "../../graphql/schema";
|
||||||
|
|
||||||
|
const server = createServer<{
|
||||||
|
req: NextApiRequest;
|
||||||
|
res: NextApiResponse;
|
||||||
|
}>({ schema });
|
||||||
|
|
||||||
|
export default server;
|
|
@ -1,15 +0,0 @@
|
||||||
# Metaforecast API
|
|
||||||
|
|
||||||
Modelled roughly after the [Manifold Markets API.](https://manifoldmarkets.notion.site/Manifold-Markets-API-5e7d0aef4dcf452bb04b319e178fabc5). Much as theirs, the metaforecast API is also in alpha. It has at various points been
|
|
||||||
|
|
||||||
## List out all markets
|
|
||||||
|
|
||||||
## Get markets for one particular platform
|
|
||||||
|
|
||||||
## Get history
|
|
||||||
|
|
||||||
Not yet implemented
|
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
https://nextjs.org/docs/messages/api-routes-response-size-limit
|
|
|
@ -1,13 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from "next/types";
|
|
||||||
|
|
||||||
import { pgRead } from "../../backend/database/pg-wrapper";
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
let allQuestions = await pgRead({ tableName: "questions" });
|
|
||||||
console.log(allQuestions.map((element) => element.title).slice(0, 5));
|
|
||||||
console.log("...");
|
|
||||||
res.status(200).json(allQuestions);
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { displayForecastsWrapperForCapture } from "../web/display/displayForecastsWrappers";
|
import { displayQuestionsWrapperForCapture } from "../web/display/displayQuestionsWrappers";
|
||||||
import { Layout } from "../web/display/Layout";
|
import { Layout } from "../web/display/Layout";
|
||||||
import { Props } from "../web/search/anySearchPage";
|
import { Props } from "../web/search/anySearchPage";
|
||||||
import CommonDisplay from "../web/search/CommonDisplay";
|
import CommonDisplay from "../web/search/CommonDisplay";
|
||||||
|
@ -10,15 +10,15 @@ export { getServerSideProps } from "../web/search/anySearchPage";
|
||||||
|
|
||||||
const CapturePage: NextPage<Props> = (props) => {
|
const CapturePage: NextPage<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Layout page={"capture"}>
|
<Layout page="capture">
|
||||||
<CommonDisplay
|
<CommonDisplay
|
||||||
{...props}
|
{...props}
|
||||||
hasSearchbar={true}
|
hasSearchbar={true}
|
||||||
hasCapture={true}
|
hasCapture={true}
|
||||||
hasAdvancedOptions={false}
|
hasAdvancedOptions={false}
|
||||||
placeholder={"Get best title match..."}
|
placeholder="Get best title match..."
|
||||||
displaySeeMoreHint={false}
|
displaySeeMoreHint={false}
|
||||||
displayForecastsWrapper={displayForecastsWrapperForCapture}
|
displayQuestionsWrapper={displayQuestionsWrapperForCapture}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,49 +1,44 @@
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
import { GetServerSideProps, NextPage } from "next";
|
||||||
import Error from "next/error";
|
import Error from "next/error";
|
||||||
|
|
||||||
import { DashboardItem } from "../../../backend/dashboards";
|
import {
|
||||||
import { DisplayForecasts } from "../../../web/display/DisplayForecasts";
|
DashboardByIdDocument, DashboardFragment
|
||||||
import { FrontendForecast } from "../../../web/platforms";
|
} from "../../../web/dashboards/queries.generated";
|
||||||
import { getDashboardForecastsByDashboardId } from "../../../web/worker/getDashboardForecasts";
|
import { DisplayQuestions } from "../../../web/display/DisplayQuestions";
|
||||||
import { reqToBasePath } from "../../../web/utils";
|
import { ssrUrql } from "../../../web/urql";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboardForecasts: FrontendForecast[];
|
dashboard?: DashboardFragment;
|
||||||
dashboardItem: DashboardItem;
|
|
||||||
numCols?: number;
|
numCols?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async (
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
|
const [ssrCache, client] = ssrUrql();
|
||||||
const dashboardId = context.query.id as string;
|
const dashboardId = context.query.id as string;
|
||||||
const numCols = Number(context.query.numCols);
|
const numCols = Number(context.query.numCols);
|
||||||
|
|
||||||
const { dashboardItem, dashboardForecasts } =
|
const dashboard = (
|
||||||
await getDashboardForecastsByDashboardId({
|
await client.query(DashboardByIdDocument, { id: dashboardId }).toPromise()
|
||||||
dashboardId,
|
).data?.result;
|
||||||
basePath: reqToBasePath(context.req), // required on server side to find the API endpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dashboardItem) {
|
if (!dashboard) {
|
||||||
context.res.statusCode = 404;
|
context.res.statusCode = 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
dashboardForecasts,
|
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
|
||||||
dashboardItem,
|
urqlState: ssrCache.extractData(),
|
||||||
|
dashboard,
|
||||||
numCols: !numCols ? null : numCols < 5 ? numCols : 4,
|
numCols: !numCols ? null : numCols < 5 ? numCols : 4,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const EmbedDashboardPage: NextPage<Props> = ({
|
const EmbedDashboardPage: NextPage<Props> = ({ dashboard, numCols }) => {
|
||||||
dashboardForecasts,
|
if (!dashboard) {
|
||||||
dashboardItem,
|
|
||||||
numCols,
|
|
||||||
}) => {
|
|
||||||
if (!dashboardItem) {
|
|
||||||
return <Error statusCode={404} />;
|
return <Error statusCode={404} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,9 +52,9 @@ const EmbedDashboardPage: NextPage<Props> = ({
|
||||||
numCols || 3
|
numCols || 3
|
||||||
} gap-4 mb-6`}
|
} gap-4 mb-6`}
|
||||||
>
|
>
|
||||||
<DisplayForecasts
|
<DisplayQuestions
|
||||||
results={dashboardForecasts}
|
results={dashboard.questions}
|
||||||
numDisplay={dashboardForecasts.length}
|
numDisplay={dashboard.questions.length}
|
||||||
showIdToggle={false}
|
showIdToggle={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,24 +1,32 @@
|
||||||
import axios from "axios";
|
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useMutation } from "urql";
|
||||||
|
|
||||||
|
import { CreateDashboardDocument } from "../../web/dashboards/queries.generated";
|
||||||
import { DashboardCreator } from "../../web/display/DashboardCreator";
|
import { DashboardCreator } from "../../web/display/DashboardCreator";
|
||||||
import { Layout } from "../../web/display/Layout";
|
import { Layout } from "../../web/display/Layout";
|
||||||
import { LineHeader } from "../../web/display/LineHeader";
|
import { LineHeader } from "../../web/display/LineHeader";
|
||||||
|
|
||||||
const DashboardsPage: NextPage = () => {
|
const DashboardsPage: NextPage = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [createDashboardResult, createDashboard] = useMutation(
|
||||||
|
CreateDashboardDocument
|
||||||
|
);
|
||||||
|
|
||||||
const handleSubmit = async (data) => {
|
const handleSubmit = async (data: any) => {
|
||||||
// Send to server to create
|
const result = await createDashboard({
|
||||||
// Get back the id
|
input: {
|
||||||
let response = await axios({
|
title: data.title,
|
||||||
url: "/api/create-dashboard-from-ids",
|
description: data.description,
|
||||||
method: "POST",
|
creator: data.creator,
|
||||||
headers: { "Content-Type": "application/json" },
|
ids: data.ids,
|
||||||
data: JSON.stringify(data),
|
},
|
||||||
}).then((res) => res.data);
|
});
|
||||||
await router.push(`/dashboards/view/${response.dashboardId}`);
|
const dashboardId = result?.data?.result?.dashboard?.id;
|
||||||
|
if (!dashboardId) {
|
||||||
|
throw new Error("Couldn't create a dashboard"); // TODO - toaster
|
||||||
|
}
|
||||||
|
await router.push(`/dashboards/view/${dashboardId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -2,57 +2,56 @@ import { GetServerSideProps, NextPage } from "next";
|
||||||
import Error from "next/error";
|
import Error from "next/error";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { DashboardItem } from "../../../backend/dashboards";
|
import {
|
||||||
import { DisplayForecasts } from "../../../web/display/DisplayForecasts";
|
DashboardByIdDocument, DashboardFragment
|
||||||
|
} from "../../../web/dashboards/queries.generated";
|
||||||
|
import { DisplayQuestions } from "../../../web/display/DisplayQuestions";
|
||||||
import { InfoBox } from "../../../web/display/InfoBox";
|
import { InfoBox } from "../../../web/display/InfoBox";
|
||||||
import { Layout } from "../../../web/display/Layout";
|
import { Layout } from "../../../web/display/Layout";
|
||||||
import { LineHeader } from "../../../web/display/LineHeader";
|
import { LineHeader } from "../../../web/display/LineHeader";
|
||||||
import { FrontendForecast } from "../../../web/platforms";
|
import { ssrUrql } from "../../../web/urql";
|
||||||
import { reqToBasePath } from "../../../web/utils";
|
|
||||||
import { getDashboardForecastsByDashboardId } from "../../../web/worker/getDashboardForecasts";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
dashboardForecasts: FrontendForecast[];
|
dashboard?: DashboardFragment;
|
||||||
dashboardItem: DashboardItem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async (
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
|
const [ssrCache, client] = ssrUrql();
|
||||||
const dashboardId = context.query.id as string;
|
const dashboardId = context.query.id as string;
|
||||||
|
|
||||||
const { dashboardForecasts, dashboardItem } =
|
const dashboard = (
|
||||||
await getDashboardForecastsByDashboardId({
|
await client.query(DashboardByIdDocument, { id: dashboardId }).toPromise()
|
||||||
dashboardId,
|
).data?.result;
|
||||||
basePath: reqToBasePath(context.req), // required on server side to find the API endpoint
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!dashboardItem) {
|
if (!dashboard) {
|
||||||
context.res.statusCode = 404;
|
context.res.statusCode = 404;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
dashboardForecasts,
|
// reduntant: page component doesn't do graphql requests, but it's still nice/more consistent to have data in cache
|
||||||
dashboardItem,
|
urqlState: ssrCache.extractData(),
|
||||||
|
dashboard,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const DashboardMetadata: React.FC<{ dashboardItem: DashboardItem }> = ({
|
const DashboardMetadata: React.FC<{ dashboard: DashboardFragment }> = ({
|
||||||
dashboardItem,
|
dashboard,
|
||||||
}) => (
|
}) => (
|
||||||
<div>
|
<div>
|
||||||
{dashboardItem?.title ? (
|
{dashboard.title ? (
|
||||||
<h1 className="text-4xl text-center text-gray-600 mt-2 mb-2">
|
<h1 className="text-4xl text-center text-gray-600 mt-2 mb-2">
|
||||||
{dashboardItem.title}
|
{dashboard.title}
|
||||||
</h1>
|
</h1>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{dashboardItem && dashboardItem.creator ? (
|
{dashboard.creator ? (
|
||||||
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
|
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
|
||||||
Created by:{" "}
|
Created by:{" "}
|
||||||
{dashboardItem.creator === "Clay Graubard" ? (
|
{dashboard.creator === "Clay Graubard" ? (
|
||||||
<>
|
<>
|
||||||
@
|
@
|
||||||
<a
|
<a
|
||||||
|
@ -63,40 +62,38 @@ const DashboardMetadata: React.FC<{ dashboardItem: DashboardItem }> = ({
|
||||||
</a>
|
</a>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
dashboardItem.creator
|
dashboard.creator
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{dashboardItem?.description ? (
|
{dashboard.description ? (
|
||||||
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
|
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
|
||||||
{dashboardItem.description}
|
{dashboard.description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
const ViewDashboardPage: NextPage<Props> = ({
|
const ViewDashboardPage: NextPage<Props> = ({ dashboard }) => {
|
||||||
dashboardForecasts,
|
|
||||||
dashboardItem,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Layout page="view-dashboard">
|
<Layout page="view-dashboard">
|
||||||
<div className="flex flex-col my-8 space-y-8">
|
<div className="flex flex-col my-8 space-y-8">
|
||||||
{dashboardItem ? (
|
{dashboard ? (
|
||||||
<DashboardMetadata dashboardItem={dashboardItem} />
|
<>
|
||||||
) : (
|
<DashboardMetadata dashboard={dashboard} />
|
||||||
<Error statusCode={404} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<DisplayForecasts
|
<DisplayQuestions
|
||||||
results={dashboardForecasts}
|
results={dashboard.questions}
|
||||||
numDisplay={dashboardForecasts.length}
|
numDisplay={dashboard.questions.length}
|
||||||
showIdToggle={false}
|
showIdToggle={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Error statusCode={404} />
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="max-w-xl self-center">
|
<div className="max-w-xl self-center">
|
||||||
<InfoBox>
|
<InfoBox>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { displayForecastsWrapperForSearch } from "../web/display/displayForecastsWrappers";
|
import { displayQuestionsWrapperForSearch } from "../web/display/displayQuestionsWrappers";
|
||||||
import { Layout } from "../web/display/Layout";
|
import { Layout } from "../web/display/Layout";
|
||||||
import { Props } from "../web/search/anySearchPage";
|
import { Props } from "../web/search/anySearchPage";
|
||||||
import CommonDisplay from "../web/search/CommonDisplay";
|
import CommonDisplay from "../web/search/CommonDisplay";
|
||||||
|
@ -10,7 +10,7 @@ export { getServerSideProps } from "../web/search/anySearchPage";
|
||||||
|
|
||||||
const IndexPage: NextPage<Props> = (props) => {
|
const IndexPage: NextPage<Props> = (props) => {
|
||||||
return (
|
return (
|
||||||
<Layout page={"search"}>
|
<Layout page="search">
|
||||||
<CommonDisplay
|
<CommonDisplay
|
||||||
{...props}
|
{...props}
|
||||||
hasSearchbar={true}
|
hasSearchbar={true}
|
||||||
|
@ -18,7 +18,7 @@ const IndexPage: NextPage<Props> = (props) => {
|
||||||
hasAdvancedOptions={true}
|
hasAdvancedOptions={true}
|
||||||
placeholder={"Find forecasts about..."}
|
placeholder={"Find forecasts about..."}
|
||||||
displaySeeMoreHint={true}
|
displaySeeMoreHint={true}
|
||||||
displayForecastsWrapper={displayForecastsWrapperForSearch}
|
displayQuestionsWrapper={displayQuestionsWrapperForSearch}
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,18 +4,19 @@ import { GetServerSideProps, NextPage } from "next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { platforms } from "../backend/platforms";
|
import { platforms } from "../backend/platforms";
|
||||||
import { DisplayForecast } from "../web/display/DisplayForecast";
|
import { DisplayQuestion } from "../web/display/DisplayQuestion";
|
||||||
import { FrontendForecast } from "../web/platforms";
|
import { QuestionFragment, SearchDocument } from "../web/search/queries.generated";
|
||||||
import searchAccordingToQueryData from "../web/worker/searchAccordingToQueryData";
|
import { ssrUrql } from "../web/urql";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
results: FrontendForecast[];
|
results: QuestionFragment[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async (
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
let urlQuery = context.query; // this is an object, not a string which I have to parse!!
|
const [ssrCache, client] = ssrUrql();
|
||||||
|
let urlQuery = context.query;
|
||||||
|
|
||||||
let initialQueryParameters = {
|
let initialQueryParameters = {
|
||||||
query: "",
|
query: "",
|
||||||
|
@ -25,14 +26,24 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
...urlQuery,
|
...urlQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
let results: FrontendForecast[] = [];
|
let results: QuestionFragment[] = [];
|
||||||
if (initialQueryParameters.query != "") {
|
if (initialQueryParameters.query !== "") {
|
||||||
results = await searchAccordingToQueryData(initialQueryParameters, 1);
|
results = (
|
||||||
|
await client
|
||||||
|
.query(SearchDocument, {
|
||||||
|
input: {
|
||||||
|
...initialQueryParameters,
|
||||||
|
limit: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toPromise()
|
||||||
|
).data.result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
results: results,
|
urqlState: ssrCache.extractData(),
|
||||||
|
results,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -46,8 +57,8 @@ const SecretEmbedPage: NextPage<Props> = ({ results }) => {
|
||||||
<div>
|
<div>
|
||||||
<div id="secretEmbed">
|
<div id="secretEmbed">
|
||||||
{result ? (
|
{result ? (
|
||||||
<DisplayForecast
|
<DisplayQuestion
|
||||||
forecast={result}
|
question={result}
|
||||||
showTimeStamp={true}
|
showTimeStamp={true}
|
||||||
expandFooterToFullWidth={true}
|
expandFooterToFullWidth={true}
|
||||||
/>
|
/>
|
||||||
|
|
8
src/utils.ts
Normal file
8
src/utils.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const shuffleArray = <T>(array: T[]): T[] => {
|
||||||
|
// See: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[array[i], array[j]] = [array[j], array[i]];
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
};
|
23
src/web/dashboards/queries.generated.tsx
Normal file
23
src/web/dashboards/queries.generated.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as Types from '../../graphql/types.generated';
|
||||||
|
|
||||||
|
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||||
|
import { QuestionFragmentDoc } from '../search/queries.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 } }> };
|
||||||
|
|
||||||
|
export type DashboardByIdQueryVariables = Types.Exact<{
|
||||||
|
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 } }> } };
|
||||||
|
|
||||||
|
export type CreateDashboardMutationVariables = Types.Exact<{
|
||||||
|
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 } }> } } };
|
||||||
|
|
||||||
|
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 CreateDashboardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDashboard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateDashboardInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"createDashboard"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Dashboard"}}]}}]}}]}},...DashboardFragmentDoc.definitions]} as unknown as DocumentNode<CreateDashboardMutation, CreateDashboardMutationVariables>;
|
23
src/web/dashboards/queries.graphql
Normal file
23
src/web/dashboards/queries.graphql
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
fragment Dashboard on Dashboard {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
description
|
||||||
|
creator
|
||||||
|
questions {
|
||||||
|
...Question
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query DashboardById($id: ID!) {
|
||||||
|
result: dashboard(id: $id) {
|
||||||
|
...Dashboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreateDashboard($input: CreateDashboardInput!) {
|
||||||
|
result: createDashboard(input: $input) {
|
||||||
|
dashboard {
|
||||||
|
...Dashboard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,8 +60,10 @@ Your old input was: ${value}`;
|
||||||
|
|
||||||
<InfoBox>
|
<InfoBox>
|
||||||
You can find the necessary ids by toggling the advanced options in the
|
You can find the necessary ids by toggling the advanced options in the
|
||||||
search, or by visiting{" "}
|
search, or by using{" "}
|
||||||
<a href="/api/all-forecasts">/api/all-forecasts</a>
|
<a href="/api/graphql?query=%7B%0A++questions%28first%3A+100%29+%7B%0A++++pageInfo+%7B%0A++++++hasNextPage%0A++++++endCursor%0A++++%7D%0A++++edges+%7B%0A++++++node+%7B%0A++++++++id%0A++++++++title%0A++++++++url%0A++++++%7D%0A++++%7D%0A++%7D%0A%7D">
|
||||||
|
GraphQL API
|
||||||
|
</a>
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { FrontendForecast } from "../platforms";
|
|
||||||
import { DisplayForecast } from "./DisplayForecast";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
results: FrontendForecast[];
|
|
||||||
numDisplay: number;
|
|
||||||
showIdToggle: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DisplayForecasts: React.FC<Props> = ({
|
|
||||||
results,
|
|
||||||
numDisplay,
|
|
||||||
showIdToggle,
|
|
||||||
}) => {
|
|
||||||
if (!results) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{results.slice(0, numDisplay).map((result) => (
|
|
||||||
/*let displayWithMetaculusCapture =
|
|
||||||
fuseSearchResult.item.platform == "Metaculus"
|
|
||||||
? metaculusEmbed(fuseSearchResult.item)
|
|
||||||
: displayForecast({ ...fuseSearchResult.item });
|
|
||||||
*/
|
|
||||||
<DisplayForecast
|
|
||||||
key={result.id}
|
|
||||||
forecast={result}
|
|
||||||
showTimeStamp={false}
|
|
||||||
expandFooterToFullWidth={false}
|
|
||||||
showIdToggle={showIdToggle}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -2,16 +2,16 @@ import domtoimage from "dom-to-image"; // https://github.com/tsayen/dom-to-image
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||||
|
|
||||||
import { FrontendForecast } from "../platforms";
|
import { QuestionFragment } from "../search/queries.generated";
|
||||||
import { uploadToImgur } from "../worker/uploadToImgur";
|
import { uploadToImgur } from "../worker/uploadToImgur";
|
||||||
import { DisplayForecast } from "./DisplayForecast";
|
import { DisplayQuestion } from "./DisplayQuestion";
|
||||||
|
|
||||||
function displayOneForecastInner(result: FrontendForecast, containerRef) {
|
function displayOneQuestionInner(result: QuestionFragment, containerRef) {
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
{result ? (
|
{result ? (
|
||||||
<DisplayForecast
|
<DisplayQuestion
|
||||||
forecast={result}
|
question={result}
|
||||||
showTimeStamp={true}
|
showTimeStamp={true}
|
||||||
expandFooterToFullWidth={true}
|
expandFooterToFullWidth={true}
|
||||||
/>
|
/>
|
||||||
|
@ -168,10 +168,10 @@ let generateMetaculusSource = (result, hasDisplayBeenCaptured) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
result: FrontendForecast;
|
result: QuestionFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DisplayOneForecastForCapture: React.FC<Props> = ({ result }) => {
|
export const DisplayOneQuestionForCapture: React.FC<Props> = ({ result }) => {
|
||||||
const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false);
|
const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -226,7 +226,7 @@ export const DisplayOneForecastForCapture: React.FC<Props> = ({ result }) => {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-center">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 w-full justify-center">
|
||||||
<div className="flex col-span-1 items-center justify-center">
|
<div className="flex col-span-1 items-center justify-center">
|
||||||
{displayOneForecastInner(result, containerRef)}
|
{displayOneQuestionInner(result, containerRef)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex col-span-1 items-center justify-center">
|
<div className="flex col-span-1 items-center justify-center">
|
||||||
{generateCaptureButton(result, onCaptureButtonClick)}
|
{generateCaptureButton(result, onCaptureButtonClick)}
|
|
@ -105,7 +105,7 @@ const getCurrencySymbolIfNeeded = ({
|
||||||
|
|
||||||
const showFirstQualityIndicator = ({
|
const showFirstQualityIndicator = ({
|
||||||
numforecasts,
|
numforecasts,
|
||||||
timestamp,
|
lastUpdated,
|
||||||
showTimeStamp,
|
showTimeStamp,
|
||||||
qualityindicators,
|
qualityindicators,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -124,7 +124,7 @@ const showFirstQualityIndicator = ({
|
||||||
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
||||||
</svg>
|
</svg>
|
||||||
{`Last updated: ${
|
{`Last updated: ${
|
||||||
timestamp && !!timestamp.slice ? timestamp.slice(0, 10) : "unknown"
|
lastUpdated ? lastUpdated.toISOString().slice(0, 10) : "unknown"
|
||||||
}`}
|
}`}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -135,13 +135,13 @@ const showFirstQualityIndicator = ({
|
||||||
|
|
||||||
const displayQualityIndicators: React.FC<{
|
const displayQualityIndicators: React.FC<{
|
||||||
numforecasts: number;
|
numforecasts: number;
|
||||||
timestamp: number;
|
lastUpdated: Date;
|
||||||
showTimeStamp: boolean;
|
showTimeStamp: boolean;
|
||||||
qualityindicators: any;
|
qualityindicators: any;
|
||||||
platform: string; // id string - e.g. "goodjudgment", not "Good Judgment"
|
platform: string; // id string - e.g. "goodjudgment", not "Good Judgment"
|
||||||
}> = ({
|
}> = ({
|
||||||
numforecasts,
|
numforecasts,
|
||||||
timestamp,
|
lastUpdated,
|
||||||
showTimeStamp,
|
showTimeStamp,
|
||||||
qualityindicators,
|
qualityindicators,
|
||||||
platform,
|
platform,
|
||||||
|
@ -151,7 +151,7 @@ const displayQualityIndicators: React.FC<{
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
{showFirstQualityIndicator({
|
{showFirstQualityIndicator({
|
||||||
numforecasts,
|
numforecasts,
|
||||||
timestamp,
|
lastUpdated,
|
||||||
showTimeStamp,
|
showTimeStamp,
|
||||||
qualityindicators,
|
qualityindicators,
|
||||||
})}
|
})}
|
||||||
|
@ -238,18 +238,18 @@ interface Props {
|
||||||
platformLabel: string;
|
platformLabel: string;
|
||||||
numforecasts: any;
|
numforecasts: any;
|
||||||
qualityindicators: any;
|
qualityindicators: any;
|
||||||
timestamp: any;
|
lastUpdated: Date;
|
||||||
showTimeStamp: boolean;
|
showTimeStamp: boolean;
|
||||||
expandFooterToFullWidth: boolean;
|
expandFooterToFullWidth: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ForecastFooter: React.FC<Props> = ({
|
export const QuestionFooter: React.FC<Props> = ({
|
||||||
stars,
|
stars,
|
||||||
platform,
|
platform,
|
||||||
platformLabel,
|
platformLabel,
|
||||||
numforecasts,
|
numforecasts,
|
||||||
qualityindicators,
|
qualityindicators,
|
||||||
timestamp,
|
lastUpdated,
|
||||||
showTimeStamp,
|
showTimeStamp,
|
||||||
expandFooterToFullWidth,
|
expandFooterToFullWidth,
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -288,7 +288,7 @@ export const ForecastFooter: React.FC<Props> = ({
|
||||||
>
|
>
|
||||||
{displayQualityIndicators({
|
{displayQualityIndicators({
|
||||||
numforecasts,
|
numforecasts,
|
||||||
timestamp,
|
lastUpdated,
|
||||||
showTimeStamp,
|
showTimeStamp,
|
||||||
qualityindicators,
|
qualityindicators,
|
||||||
platform,
|
platform,
|
|
@ -1,9 +1,9 @@
|
||||||
import { FaRegClipboard } from "react-icons/fa";
|
import { FaRegClipboard } from "react-icons/fa";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
|
|
||||||
import { FrontendForecast } from "../../platforms";
|
import { QuestionFragment } from "../../search/queries.generated";
|
||||||
import { Card } from "../Card";
|
import { Card } from "../Card";
|
||||||
import { ForecastFooter } from "./ForecastFooter";
|
import { QuestionFooter } from "./QuestionFooter";
|
||||||
|
|
||||||
const truncateText = (length: number, text: string): string => {
|
const truncateText = (length: number, text: string): string => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
|
@ -254,13 +254,14 @@ const CopyText: React.FC<{ text: string; displayText: string }> = ({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const LastUpdated: React.FC<{ timestamp: string }> = ({ timestamp }) => (
|
const LastUpdated: React.FC<{ timestamp: Date }> = ({ timestamp }) => (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<svg className="mt-1" height="10" width="16">
|
<svg className="mt-1" height="10" width="16">
|
||||||
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
<circle cx="4" cy="4" r="4" fill="rgb(29, 78, 216)" />
|
||||||
</svg>
|
</svg>
|
||||||
<span className="text-gray-600">
|
<span className="text-gray-600">
|
||||||
Last updated: {timestamp ? timestamp.slice(0, 10) : "unknown"}
|
Last updated:{" "}
|
||||||
|
{timestamp ? timestamp.toISOString().slice(0, 10) : "unknown"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -268,22 +269,21 @@ const LastUpdated: React.FC<{ timestamp: string }> = ({ timestamp }) => (
|
||||||
// Main component
|
// Main component
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
forecast: FrontendForecast;
|
question: QuestionFragment;
|
||||||
showTimeStamp: boolean;
|
showTimeStamp: boolean;
|
||||||
expandFooterToFullWidth: boolean;
|
expandFooterToFullWidth: boolean;
|
||||||
showIdToggle?: boolean;
|
showIdToggle?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DisplayForecast: React.FC<Props> = ({
|
export const DisplayQuestion: React.FC<Props> = ({
|
||||||
forecast: {
|
question: {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
url,
|
url,
|
||||||
platform,
|
platform,
|
||||||
platformLabel,
|
|
||||||
description,
|
description,
|
||||||
options,
|
options,
|
||||||
qualityindicators,
|
qualityIndicators,
|
||||||
timestamp,
|
timestamp,
|
||||||
visualization,
|
visualization,
|
||||||
},
|
},
|
||||||
|
@ -291,8 +291,9 @@ export const DisplayForecast: React.FC<Props> = ({
|
||||||
expandFooterToFullWidth,
|
expandFooterToFullWidth,
|
||||||
showIdToggle,
|
showIdToggle,
|
||||||
}) => {
|
}) => {
|
||||||
|
const lastUpdated = new Date(timestamp * 1000);
|
||||||
const displayTimestampAtBottom =
|
const displayTimestampAtBottom =
|
||||||
checkIfDisplayTimeStampAtBottom(qualityindicators);
|
checkIfDisplayTimeStampAtBottom(qualityIndicators);
|
||||||
|
|
||||||
const yesNoOptions =
|
const yesNoOptions =
|
||||||
options.length === 2 &&
|
options.length === 2 &&
|
||||||
|
@ -332,7 +333,7 @@ export const DisplayForecast: React.FC<Props> = ({
|
||||||
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
|
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<LastUpdated timestamp={timestamp} />
|
<LastUpdated timestamp={lastUpdated} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -344,18 +345,18 @@ export const DisplayForecast: React.FC<Props> = ({
|
||||||
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
|
showTimeStamp && !displayTimestampAtBottom ? "sm:block" : ""
|
||||||
} ml-6`}
|
} ml-6`}
|
||||||
>
|
>
|
||||||
<LastUpdated timestamp={timestamp} />
|
<LastUpdated timestamp={lastUpdated} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{platform !== "guesstimate" && options.length < 3 && (
|
{platform.id !== "guesstimate" && options.length < 3 && (
|
||||||
<div className="text-gray-500">
|
<div className="text-gray-500">
|
||||||
<DisplayMarkdown description={description} />
|
<DisplayMarkdown description={description} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{platform === "guesstimate" && (
|
{platform.id === "guesstimate" && (
|
||||||
<img
|
<img
|
||||||
className="rounded-sm"
|
className="rounded-sm"
|
||||||
src={visualization}
|
src={visualization}
|
||||||
|
@ -369,16 +370,16 @@ export const DisplayForecast: React.FC<Props> = ({
|
||||||
} self-center`}
|
} self-center`}
|
||||||
>
|
>
|
||||||
{/* This one is exclusively for mobile*/}
|
{/* This one is exclusively for mobile*/}
|
||||||
<LastUpdated timestamp={timestamp} />
|
<LastUpdated timestamp={lastUpdated} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ForecastFooter
|
<QuestionFooter
|
||||||
stars={qualityindicators.stars}
|
stars={qualityIndicators.stars}
|
||||||
platform={platform}
|
platform={platform.id}
|
||||||
platformLabel={platformLabel || platform} // author || platformLabel,
|
platformLabel={platform.label}
|
||||||
numforecasts={qualityindicators.numforecasts}
|
numforecasts={qualityIndicators.numForecasts}
|
||||||
qualityindicators={qualityindicators}
|
qualityindicators={qualityIndicators}
|
||||||
timestamp={timestamp}
|
lastUpdated={lastUpdated}
|
||||||
showTimeStamp={showTimeStamp && displayTimestampAtBottom}
|
showTimeStamp={showTimeStamp && displayTimestampAtBottom}
|
||||||
expandFooterToFullWidth={expandFooterToFullWidth}
|
expandFooterToFullWidth={expandFooterToFullWidth}
|
||||||
/>
|
/>
|
33
src/web/display/DisplayQuestions.tsx
Normal file
33
src/web/display/DisplayQuestions.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { QuestionFragment } from "../search/queries.generated";
|
||||||
|
import { DisplayQuestion } from "./DisplayQuestion";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
results: QuestionFragment[];
|
||||||
|
numDisplay: number;
|
||||||
|
showIdToggle: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisplayQuestions: React.FC<Props> = ({
|
||||||
|
results,
|
||||||
|
numDisplay,
|
||||||
|
showIdToggle,
|
||||||
|
}) => {
|
||||||
|
if (!results) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{results.slice(0, numDisplay).map((result) => (
|
||||||
|
<DisplayQuestion
|
||||||
|
key={result.id}
|
||||||
|
question={result}
|
||||||
|
showTimeStamp={false}
|
||||||
|
expandFooterToFullWidth={false}
|
||||||
|
showIdToggle={showIdToggle}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,14 +1,14 @@
|
||||||
import { DisplayForecasts } from "./DisplayForecasts";
|
import { DisplayOneQuestionForCapture } from "./DisplayOneQuestionForCapture";
|
||||||
import { DisplayOneForecastForCapture } from "./DisplayOneForecastForCapture";
|
import { DisplayQuestions } from "./DisplayQuestions";
|
||||||
|
|
||||||
export function displayForecastsWrapperForSearch({
|
export function displayQuestionsWrapperForSearch({
|
||||||
results,
|
results,
|
||||||
numDisplay,
|
numDisplay,
|
||||||
showIdToggle,
|
showIdToggle,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
<DisplayForecasts
|
<DisplayQuestions
|
||||||
results={results || []}
|
results={results || []}
|
||||||
numDisplay={numDisplay}
|
numDisplay={numDisplay}
|
||||||
showIdToggle={showIdToggle}
|
showIdToggle={showIdToggle}
|
||||||
|
@ -17,13 +17,13 @@ export function displayForecastsWrapperForSearch({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function displayForecastsWrapperForCapture({
|
export function displayQuestionsWrapperForCapture({
|
||||||
results,
|
results,
|
||||||
whichResultToDisplayAndCapture,
|
whichResultToDisplayAndCapture,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 w-full justify-center">
|
<div className="grid grid-cols-1 w-full justify-center">
|
||||||
<DisplayOneForecastForCapture
|
<DisplayOneQuestionForCapture
|
||||||
result={results[whichResultToDisplayAndCapture]}
|
result={results[whichResultToDisplayAndCapture]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
|
@ -13,3 +13,15 @@ export const useNoInitialEffect = (
|
||||||
return effect();
|
return effect();
|
||||||
}, deps);
|
}, deps);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useIsFirstRender = (): boolean => {
|
||||||
|
const isFirst = React.useRef(true);
|
||||||
|
|
||||||
|
if (isFirst.current) {
|
||||||
|
isFirst.current = false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFirst.current;
|
||||||
|
};
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { Forecast, PlatformConfig } from "../backend/platforms";
|
|
||||||
|
|
||||||
export type FrontendForecast = Forecast & {
|
|
||||||
platformLabel: string;
|
|
||||||
visualization?: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ok on client side
|
|
||||||
export const addLabelsToForecasts = (
|
|
||||||
forecasts: Forecast[],
|
|
||||||
platformsConfig: PlatformConfig[]
|
|
||||||
): FrontendForecast[] => {
|
|
||||||
const platformNameToLabel = Object.fromEntries(
|
|
||||||
platformsConfig.map((platform) => [platform.name, platform.label])
|
|
||||||
);
|
|
||||||
|
|
||||||
return forecasts.map((result) => ({
|
|
||||||
...result,
|
|
||||||
platformLabel: platformNameToLabel[result.platform] || result.platform,
|
|
||||||
}));
|
|
||||||
};
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React, { Fragment, useState } from "react";
|
import React, { Fragment, useMemo, useState } from "react";
|
||||||
|
import { useQuery } from "urql";
|
||||||
|
|
||||||
import { ButtonsForStars } from "../display/ButtonsForStars";
|
import { ButtonsForStars } from "../display/ButtonsForStars";
|
||||||
import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
|
import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
|
||||||
import { QueryForm } from "../display/QueryForm";
|
import { QueryForm } from "../display/QueryForm";
|
||||||
import { SliderElement } from "../display/SliderElement";
|
import { SliderElement } from "../display/SliderElement";
|
||||||
import { useNoInitialEffect } from "../hooks";
|
import { useIsFirstRender, useNoInitialEffect } from "../hooks";
|
||||||
import { FrontendForecast } from "../platforms";
|
|
||||||
import searchAccordingToQueryData from "../worker/searchAccordingToQueryData";
|
|
||||||
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
|
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
|
||||||
|
import { QuestionFragment, SearchDocument } from "./queries.generated";
|
||||||
|
|
||||||
interface Props extends AnySearchPageProps {
|
interface Props extends AnySearchPageProps {
|
||||||
hasSearchbar: boolean;
|
hasSearchbar: boolean;
|
||||||
|
@ -16,8 +16,8 @@ interface Props extends AnySearchPageProps {
|
||||||
hasAdvancedOptions: boolean;
|
hasAdvancedOptions: boolean;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
displaySeeMoreHint: boolean;
|
displaySeeMoreHint: boolean;
|
||||||
displayForecastsWrapper: (opts: {
|
displayQuestionsWrapper: (opts: {
|
||||||
results: FrontendForecast[];
|
results: QuestionFragment[];
|
||||||
numDisplay: number;
|
numDisplay: number;
|
||||||
whichResultToDisplayAndCapture: number;
|
whichResultToDisplayAndCapture: number;
|
||||||
showIdToggle: boolean;
|
showIdToggle: boolean;
|
||||||
|
@ -27,7 +27,6 @@ interface Props extends AnySearchPageProps {
|
||||||
/* Body */
|
/* Body */
|
||||||
const CommonDisplay: React.FC<Props> = ({
|
const CommonDisplay: React.FC<Props> = ({
|
||||||
defaultResults,
|
defaultResults,
|
||||||
initialResults,
|
|
||||||
initialQueryParameters,
|
initialQueryParameters,
|
||||||
defaultQueryParameters,
|
defaultQueryParameters,
|
||||||
initialNumDisplay,
|
initialNumDisplay,
|
||||||
|
@ -38,10 +37,11 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
hasAdvancedOptions,
|
hasAdvancedOptions,
|
||||||
placeholder,
|
placeholder,
|
||||||
displaySeeMoreHint,
|
displaySeeMoreHint,
|
||||||
displayForecastsWrapper,
|
displayQuestionsWrapper,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
|
||||||
/* States */
|
/* States */
|
||||||
|
const router = useRouter();
|
||||||
|
const isFirstRender = useIsFirstRender();
|
||||||
|
|
||||||
const [queryParameters, setQueryParameters] = useState<QueryParameters>(
|
const [queryParameters, setQueryParameters] = useState<QueryParameters>(
|
||||||
initialQueryParameters
|
initialQueryParameters
|
||||||
|
@ -52,60 +52,67 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
// used to distinguish numDisplay updates which force search and don't force search, see effects below
|
// used to distinguish numDisplay updates which force search and don't force search, see effects below
|
||||||
const [forceSearch, setForceSearch] = useState(0);
|
const [forceSearch, setForceSearch] = useState(0);
|
||||||
|
|
||||||
const [results, setResults] = useState(initialResults);
|
|
||||||
const [advancedOptions, showAdvancedOptions] = useState(false);
|
const [advancedOptions, showAdvancedOptions] = useState(false);
|
||||||
const [whichResultToDisplayAndCapture, setWhichResultToDisplayAndCapture] =
|
const [whichResultToDisplayAndCapture, setWhichResultToDisplayAndCapture] =
|
||||||
useState(0);
|
useState(0);
|
||||||
const [showIdToggle, setShowIdToggle] = useState(false);
|
const [showIdToggle, setShowIdToggle] = useState(false);
|
||||||
|
|
||||||
/* Functions which I want to have access to the Home namespace */
|
const [typing, setTyping] = useState(false);
|
||||||
// I don't want to create an "defaultResults" object for each search.
|
|
||||||
async function executeSearchOrAnswerWithDefaultResults() {
|
|
||||||
const queryData = {
|
|
||||||
...queryParameters,
|
|
||||||
numDisplay,
|
|
||||||
};
|
|
||||||
|
|
||||||
const filterManually = (
|
// must match the query from anySearchPage.ts getServerSideProps
|
||||||
queryData: QueryParameters,
|
const [queryResults, reexecuteQuery] = useQuery({
|
||||||
results: FrontendForecast[]
|
query: SearchDocument,
|
||||||
) => {
|
variables: {
|
||||||
if (
|
input: {
|
||||||
queryData.forecastingPlatforms &&
|
...queryParameters,
|
||||||
queryData.forecastingPlatforms.length > 0
|
limit: numDisplay,
|
||||||
) {
|
},
|
||||||
results = results.filter((result) =>
|
},
|
||||||
queryData.forecastingPlatforms.includes(result.platform)
|
pause: !isFirstRender,
|
||||||
);
|
// note that if we don't force cache-only on SSR then queryResults.fetching is true which leads to an empty page
|
||||||
}
|
requestPolicy: isFirstRender ? "cache-only" : "network-only",
|
||||||
if (queryData.starsThreshold === 4) {
|
});
|
||||||
results = results.filter(
|
|
||||||
(result) => result.qualityindicators.stars >= 4
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (queryData.forecastsThreshold) {
|
|
||||||
// results = results.filter(result => (result.qualityindicators && result.qualityindicators.numforecasts > forecastsThreshold))
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryIsEmpty =
|
const queryIsEmpty =
|
||||||
!queryData || queryData.query == "" || queryData.query == undefined;
|
queryParameters.query === undefined || queryParameters.query === "";
|
||||||
|
|
||||||
const results = queryIsEmpty
|
const results: QuestionFragment[] = useMemo(() => {
|
||||||
? filterManually(queryData, defaultResults)
|
if (typing || queryResults.fetching) return []; // TODO - return results but show spinner or darken out all cards?
|
||||||
: await searchAccordingToQueryData(queryData, numDisplay);
|
|
||||||
|
|
||||||
setResults(results);
|
if (queryIsEmpty) {
|
||||||
|
const filterManually = (results: QuestionFragment[]) => {
|
||||||
|
let filteredResults = [...results];
|
||||||
|
if (
|
||||||
|
queryParameters.forecastingPlatforms &&
|
||||||
|
queryParameters.forecastingPlatforms.length > 0
|
||||||
|
) {
|
||||||
|
filteredResults = filteredResults.filter((result) =>
|
||||||
|
queryParameters.forecastingPlatforms.includes(result.platform.id)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
if (queryParameters.starsThreshold === 4) {
|
||||||
|
filteredResults = filteredResults.filter(
|
||||||
|
(result) => result.qualityIndicators.stars >= 4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (queryParameters.forecastsThreshold) {
|
||||||
|
// TODO / FIXME / remove?
|
||||||
|
}
|
||||||
|
return filteredResults;
|
||||||
|
};
|
||||||
|
return filterManually(defaultResults);
|
||||||
|
} else {
|
||||||
|
return queryResults.data?.result || [];
|
||||||
|
}
|
||||||
|
}, [queryResults.data, queryParameters]);
|
||||||
|
|
||||||
// I don't want the function which display forecasts (displayForecasts) to change with a change in queryParameters. But I want it to have access to the queryParameters, and in particular access to queryParameters.numDisplay. Hence why this function lives inside Home.
|
// I don't want the component which display questions (DisplayQuestions) to change with a change in queryParameters. But I want it to have access to the queryParameters, and in particular access to queryParameters.numDisplay. Hence why this function lives inside Home.
|
||||||
const getInfoToDisplayForecastsFunction = () => {
|
const getInfoToDisplayQuestionsFunction = () => {
|
||||||
const numDisplayRounded =
|
const numDisplayRounded =
|
||||||
numDisplay % 3 != 0
|
numDisplay % 3 != 0
|
||||||
? numDisplay + (3 - (Math.round(numDisplay) % 3))
|
? numDisplay + (3 - (Math.round(numDisplay) % 3))
|
||||||
: numDisplay;
|
: numDisplay;
|
||||||
return displayForecastsWrapper({
|
return displayQuestionsWrapper({
|
||||||
results,
|
results,
|
||||||
numDisplay: numDisplayRounded,
|
numDisplay: numDisplayRounded,
|
||||||
whichResultToDisplayAndCapture,
|
whichResultToDisplayAndCapture,
|
||||||
|
@ -145,10 +152,13 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
useNoInitialEffect(updateRoute, [numDisplay]);
|
useNoInitialEffect(updateRoute, [numDisplay]);
|
||||||
|
|
||||||
useNoInitialEffect(() => {
|
useNoInitialEffect(() => {
|
||||||
setResults([]);
|
setTyping(true);
|
||||||
const newTimeoutId = setTimeout(() => {
|
const newTimeoutId = setTimeout(async () => {
|
||||||
updateRoute();
|
updateRoute();
|
||||||
executeSearchOrAnswerWithDefaultResults();
|
if (!queryIsEmpty) {
|
||||||
|
reexecuteQuery();
|
||||||
|
}
|
||||||
|
setTyping(false);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// avoid sending results if user has not stopped typing.
|
// avoid sending results if user has not stopped typing.
|
||||||
|
@ -307,10 +317,10 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div>{getInfoToDisplayForecastsFunction()}</div>
|
<div>{getInfoToDisplayQuestionsFunction()}</div>
|
||||||
|
|
||||||
{displaySeeMoreHint &&
|
{displaySeeMoreHint &&
|
||||||
(!results || (results.length != 0 && numDisplay < results.length)) ? (
|
(!results || (results.length !== 0 && numDisplay < results.length)) ? (
|
||||||
<div>
|
<div>
|
||||||
<p className="mt-4 mb-4">
|
<p className="mt-4 mb-4">
|
||||||
{"Can't find what you were looking for?"}
|
{"Can't find what you were looking for?"}
|
||||||
|
@ -336,7 +346,7 @@ const CommonDisplay: React.FC<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<br></br>
|
<br />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { GetServerSideProps } from "next";
|
import { GetServerSideProps } from "next";
|
||||||
|
|
||||||
import { getFrontpage } from "../../backend/frontpage";
|
|
||||||
import { getPlatformsConfig, PlatformConfig, platforms } from "../../backend/platforms";
|
import { getPlatformsConfig, PlatformConfig, platforms } from "../../backend/platforms";
|
||||||
import { addLabelsToForecasts, FrontendForecast } from "../platforms";
|
import { ssrUrql } from "../urql";
|
||||||
import searchAccordingToQueryData from "../worker/searchAccordingToQueryData";
|
import { FrontpageDocument, QuestionFragment, SearchDocument } from "./queries.generated";
|
||||||
|
|
||||||
/* Common code for / and /capture */
|
/* Common code for / and /capture */
|
||||||
|
|
||||||
|
@ -15,8 +14,7 @@ export interface QueryParameters {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
defaultResults: FrontendForecast[];
|
defaultResults: QuestionFragment[];
|
||||||
initialResults: FrontendForecast[];
|
|
||||||
initialQueryParameters: QueryParameters;
|
initialQueryParameters: QueryParameters;
|
||||||
defaultQueryParameters: QueryParameters;
|
defaultQueryParameters: QueryParameters;
|
||||||
initialNumDisplay: number;
|
initialNumDisplay: number;
|
||||||
|
@ -27,6 +25,7 @@ export interface Props {
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async (
|
export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
|
const [ssrCache, client] = ssrUrql();
|
||||||
const urlQuery = context.query;
|
const urlQuery = context.query;
|
||||||
|
|
||||||
const platformsConfig = getPlatformsConfig({ withGuesstimate: true });
|
const platformsConfig = getPlatformsConfig({ withGuesstimate: true });
|
||||||
|
@ -61,28 +60,32 @@ export const getServerSideProps: GetServerSideProps<Props> = async (
|
||||||
const defaultNumDisplay = 21;
|
const defaultNumDisplay = 21;
|
||||||
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;
|
const initialNumDisplay = Number(urlQuery.numDisplay) || defaultNumDisplay;
|
||||||
|
|
||||||
const defaultResults = addLabelsToForecasts(
|
const defaultResults = (await client.query(FrontpageDocument).toPromise())
|
||||||
await getFrontpage(),
|
.data.result;
|
||||||
platformsConfig
|
|
||||||
);
|
|
||||||
|
|
||||||
const initialResults =
|
if (
|
||||||
!!initialQueryParameters &&
|
!!initialQueryParameters &&
|
||||||
initialQueryParameters.query != "" &&
|
initialQueryParameters.query != "" &&
|
||||||
initialQueryParameters.query != undefined
|
initialQueryParameters.query != undefined
|
||||||
? await searchAccordingToQueryData(
|
) {
|
||||||
initialQueryParameters,
|
// must match the query from CommonDisplay
|
||||||
initialNumDisplay
|
await client
|
||||||
)
|
.query(SearchDocument, {
|
||||||
: defaultResults;
|
input: {
|
||||||
|
...initialQueryParameters,
|
||||||
|
limit: initialNumDisplay,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
urqlState: ssrCache.extractData(),
|
||||||
initialQueryParameters,
|
initialQueryParameters,
|
||||||
defaultQueryParameters,
|
defaultQueryParameters,
|
||||||
initialNumDisplay,
|
initialNumDisplay,
|
||||||
defaultNumDisplay,
|
defaultNumDisplay,
|
||||||
initialResults,
|
|
||||||
defaultResults,
|
defaultResults,
|
||||||
platformsConfig,
|
platformsConfig,
|
||||||
},
|
},
|
||||||
|
|
20
src/web/search/queries.generated.tsx
Normal file
20
src/web/search/queries.generated.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import * as Types from '../../graphql/types.generated';
|
||||||
|
|
||||||
|
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 } };
|
||||||
|
|
||||||
|
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 } }> };
|
||||||
|
|
||||||
|
export type SearchQueryVariables = Types.Exact<{
|
||||||
|
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 } }> };
|
||||||
|
|
||||||
|
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":"visualization"}}]}}]} as unknown as DocumentNode<QuestionFragment, unknown>;
|
||||||
|
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 SearchDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Search"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SearchInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"result"},"name":{"kind":"Name","value":"searchQuestions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Question"}}]}}]}},...QuestionFragmentDoc.definitions]} as unknown as DocumentNode<SearchQuery, SearchQueryVariables>;
|
32
src/web/search/queries.graphql
Normal file
32
src/web/search/queries.graphql
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
fragment Question on Question {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
title
|
||||||
|
description
|
||||||
|
timestamp
|
||||||
|
options {
|
||||||
|
name
|
||||||
|
probability
|
||||||
|
}
|
||||||
|
platform {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
}
|
||||||
|
qualityIndicators {
|
||||||
|
stars
|
||||||
|
numForecasts
|
||||||
|
}
|
||||||
|
visualization
|
||||||
|
}
|
||||||
|
|
||||||
|
query Frontpage {
|
||||||
|
result: frontpage {
|
||||||
|
...Question
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query Search($input: SearchInput!) {
|
||||||
|
result: searchQuestions(input: $input) {
|
||||||
|
...Question
|
||||||
|
}
|
||||||
|
}
|
40
src/web/urql.ts
Normal file
40
src/web/urql.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { initUrqlClient, SSRExchange } from "next-urql";
|
||||||
|
import { cacheExchange, dedupExchange, fetchExchange, ssrExchange } from "urql";
|
||||||
|
import customScalarsExchange from "urql-custom-scalars-exchange";
|
||||||
|
|
||||||
|
import schema from "../graphql/introspection.json";
|
||||||
|
import { getBasePath } from "./utils";
|
||||||
|
|
||||||
|
export const graphqlEndpoint = `${getBasePath()}/api/graphql`;
|
||||||
|
|
||||||
|
const scalarsExchange = customScalarsExchange({
|
||||||
|
// Types don't match for some reason.
|
||||||
|
// Related:
|
||||||
|
// - https://github.com/apollographql/apollo-tooling/issues/1491
|
||||||
|
// - https://spectrum.chat/urql/help/schema-property-kind-is-missing-in-type~29c8f416-068c-485a-adf1-935686b99d05
|
||||||
|
schema: schema as any,
|
||||||
|
scalars: {
|
||||||
|
/* not compatible with next.js serialization limitations, unfortunately */
|
||||||
|
// Date(value: number) {
|
||||||
|
// return new Date(value * 1000);
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getUrqlClientOptions = (ssr: SSRExchange) => ({
|
||||||
|
url: graphqlEndpoint,
|
||||||
|
exchanges: [
|
||||||
|
dedupExchange,
|
||||||
|
scalarsExchange,
|
||||||
|
cacheExchange,
|
||||||
|
ssr,
|
||||||
|
fetchExchange,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// for getServerSideProps/getStaticProps only
|
||||||
|
export const ssrUrql = () => {
|
||||||
|
const ssrCache = ssrExchange({ isClient: false });
|
||||||
|
const client = initUrqlClient(getUrqlClientOptions(ssrCache), false);
|
||||||
|
return [ssrCache, client] as const;
|
||||||
|
};
|
|
@ -1,11 +1,12 @@
|
||||||
import { IncomingMessage } from "http";
|
export const getBasePath = () => {
|
||||||
|
|
||||||
export const reqToBasePath = (req: IncomingMessage) => {
|
|
||||||
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
|
if (process.env.NEXT_PUBLIC_VERCEL_URL) {
|
||||||
console.log(process.env.NEXT_PUBLIC_VERCEL_URL);
|
|
||||||
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
return `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we could just hardcode http://localhost:3000 here, but then `next dev -p <CUSTOM_PORT>` would break
|
// can be used for local development if you prefer non-default port
|
||||||
return "http://" + req.headers.host;
|
if (process.env.NEXT_PUBLIC_SITE_URL) {
|
||||||
|
return process.env.NEXT_PUBLIC_SITE_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "http://localhost:3000";
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import { DashboardItem } from "../../backend/dashboards";
|
|
||||||
import { Forecast, getPlatformsConfig } from "../../backend/platforms";
|
|
||||||
import { addLabelsToForecasts, FrontendForecast } from "../platforms";
|
|
||||||
|
|
||||||
export async function getDashboardForecastsByDashboardId({
|
|
||||||
dashboardId,
|
|
||||||
basePath,
|
|
||||||
}: {
|
|
||||||
dashboardId: string;
|
|
||||||
basePath?: string;
|
|
||||||
}): Promise<{
|
|
||||||
dashboardForecasts: FrontendForecast[];
|
|
||||||
dashboardItem: DashboardItem;
|
|
||||||
}> {
|
|
||||||
console.log("getDashboardForecastsByDashboardId: ");
|
|
||||||
if (typeof window === undefined && !basePath) {
|
|
||||||
throw new Error("`basePath` option is required on server side");
|
|
||||||
}
|
|
||||||
|
|
||||||
let dashboardForecasts: Forecast[] = [];
|
|
||||||
let dashboardItem: DashboardItem | null = null;
|
|
||||||
try {
|
|
||||||
let { data } = await axios({
|
|
||||||
url: `${basePath || ""}/api/dashboard-by-id`,
|
|
||||||
method: "post",
|
|
||||||
data: {
|
|
||||||
id: dashboardId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
dashboardForecasts = data.dashboardContents;
|
|
||||||
dashboardItem = data.dashboardItem as DashboardItem;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
const labeledDashboardForecasts = addLabelsToForecasts(
|
|
||||||
dashboardForecasts,
|
|
||||||
getPlatformsConfig({ withGuesstimate: false })
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
dashboardForecasts: labeledDashboardForecasts,
|
|
||||||
dashboardItem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createDashboard(payload) {
|
|
||||||
let data = { dashboardId: null };
|
|
||||||
try {
|
|
||||||
let { title, description, ids, creator, extra } = payload;
|
|
||||||
console.log(payload);
|
|
||||||
let response = await axios({
|
|
||||||
url: "/api/create-dashboard-from-ids",
|
|
||||||
method: "post",
|
|
||||||
data: {
|
|
||||||
title: title || "",
|
|
||||||
description: description || "",
|
|
||||||
ids: ids,
|
|
||||||
creator: creator || "",
|
|
||||||
extra: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
data = response.data;
|
|
||||||
console.log(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { FrontendForecast } from "../platforms";
|
|
||||||
import { QueryParameters } from "../search/anySearchPage";
|
|
||||||
import searchGuesstimate from "./searchGuesstimate";
|
|
||||||
import searchWithAlgolia from "./searchWithAlgolia";
|
|
||||||
|
|
||||||
export default async function searchAccordingToQueryData(
|
|
||||||
queryData: QueryParameters,
|
|
||||||
limit: number
|
|
||||||
): Promise<FrontendForecast[]> {
|
|
||||||
let results: FrontendForecast[] = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// defs
|
|
||||||
let query = queryData.query == undefined ? "" : queryData.query;
|
|
||||||
if (query == "") return [];
|
|
||||||
let forecastsThreshold = queryData.forecastsThreshold;
|
|
||||||
let starsThreshold = queryData.starsThreshold;
|
|
||||||
let platformsIncludeGuesstimate =
|
|
||||||
queryData.forecastingPlatforms.includes("guesstimate") &&
|
|
||||||
starsThreshold <= 1;
|
|
||||||
|
|
||||||
// preparation
|
|
||||||
let unawaitedAlgoliaResponse = searchWithAlgolia({
|
|
||||||
queryString: query,
|
|
||||||
hitsPerPage: limit + 50,
|
|
||||||
starsThreshold,
|
|
||||||
filterByPlatforms: queryData.forecastingPlatforms,
|
|
||||||
forecastsThreshold,
|
|
||||||
});
|
|
||||||
|
|
||||||
// consider the guesstimate and the non-guesstimate cases separately.
|
|
||||||
if (platformsIncludeGuesstimate) {
|
|
||||||
let responses = await Promise.all([
|
|
||||||
unawaitedAlgoliaResponse,
|
|
||||||
searchGuesstimate(query),
|
|
||||||
]); // faster than two separate requests
|
|
||||||
let responsesNotGuesstimate = responses[0];
|
|
||||||
let responsesGuesstimate = responses[1];
|
|
||||||
results = [...responsesNotGuesstimate, ...responsesGuesstimate];
|
|
||||||
//results.sort((x,y)=> x.ranking < y.ranking ? -1: 1)
|
|
||||||
} else {
|
|
||||||
results = await unawaitedAlgoliaResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
} finally {
|
|
||||||
console.log(results);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +1,18 @@
|
||||||
/* Imports */
|
/* Imports */
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import { FrontendForecast } from "../platforms";
|
import { AlgoliaQuestion } from "../../backend/utils/algolia";
|
||||||
|
|
||||||
/* Definitions */
|
/* Definitions */
|
||||||
let urlEndPoint =
|
const urlEndPoint =
|
||||||
"https://m629r9ugsg-dsn.algolia.net/1/indexes/Space_production/query?x-algolia-agent=Algolia%20for%20vanilla%20JavaScript%203.32.1&x-algolia-application-id=M629R9UGSG&x-algolia-api-key=4e893740a2bd467a96c8bfcf95b2809c";
|
"https://m629r9ugsg-dsn.algolia.net/1/indexes/Space_production/query?x-algolia-agent=Algolia%20for%20vanilla%20JavaScript%203.32.1&x-algolia-application-id=M629R9UGSG&x-algolia-api-key=4e893740a2bd467a96c8bfcf95b2809c";
|
||||||
|
|
||||||
/* Body */
|
/* Body */
|
||||||
|
|
||||||
export default async function searchGuesstimate(
|
export default async function searchGuesstimate(
|
||||||
query
|
query
|
||||||
): Promise<FrontendForecast[]> {
|
): Promise<AlgoliaQuestion[]> {
|
||||||
let response = await axios({
|
const response = await axios({
|
||||||
url: urlEndPoint,
|
url: urlEndPoint,
|
||||||
// credentials: "omit",
|
// credentials: "omit",
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -21,8 +21,6 @@ export default async function searchGuesstimate(
|
||||||
"Accept-Language": "en-US,en;q=0.5",
|
"Accept-Language": "en-US,en;q=0.5",
|
||||||
"content-type": "application/x-www-form-urlencoded",
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
},
|
},
|
||||||
// referrer:
|
|
||||||
// "https://m629r9ugsg-dsn.algolia.net/1/indexes/Space_production/query?x-algolia-agent=Algolia%20for%20vanilla%20JavaScript%203.32.1&x-algolia-application-id=M629R9UGSG&x-algolia-api-key=4e893740a2bd467a96c8bfcf95b2809c",
|
|
||||||
data: `{\"params\":\"query=${query.replace(
|
data: `{\"params\":\"query=${query.replace(
|
||||||
/ /g,
|
/ /g,
|
||||||
"%20"
|
"%20"
|
||||||
|
@ -31,33 +29,36 @@ export default async function searchGuesstimate(
|
||||||
});
|
});
|
||||||
|
|
||||||
const models: any[] = response.data.hits;
|
const models: any[] = response.data.hits;
|
||||||
const mappedModels: FrontendForecast[] = models.map((model, index) => {
|
const mappedModels: AlgoliaQuestion[] = models.map((model, index) => {
|
||||||
let description = model.description
|
const description = model.description
|
||||||
? model.description.replace(/\n/g, " ").replace(/ /g, " ")
|
? model.description.replace(/\n/g, " ").replace(/ /g, " ")
|
||||||
: "";
|
: "";
|
||||||
let stars = description.length > 250 ? 2 : 1;
|
const stars = description.length > 250 ? 2 : 1;
|
||||||
return {
|
const q: AlgoliaQuestion = {
|
||||||
id: `guesstimate-${model.id}`,
|
id: `guesstimate-${model.id}`,
|
||||||
title: model.name,
|
title: model.name,
|
||||||
url: `https://www.getguesstimate.com/models/${model.id}`,
|
url: `https://www.getguesstimate.com/models/${model.id}`,
|
||||||
timestamp: model.created_at, // TODO - check that format matches
|
timestamp: model.created_at, // TODO - check that format matches
|
||||||
platform: "guesstimate",
|
platform: "guesstimate",
|
||||||
platformLabel: "Guesstimate",
|
description,
|
||||||
description: description,
|
|
||||||
options: [],
|
options: [],
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
stars: stars,
|
stars: stars,
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
},
|
},
|
||||||
|
stars,
|
||||||
|
extra: {
|
||||||
visualization: model.big_screenshot,
|
visualization: model.big_screenshot,
|
||||||
ranking: 10 * (index + 1) - 0.5, //(model._rankingInfo - 1*index)// hack
|
},
|
||||||
|
// ranking: 10 * (index + 1) - 0.5, //(model._rankingInfo - 1*index)// hack
|
||||||
};
|
};
|
||||||
|
return q;
|
||||||
});
|
});
|
||||||
|
|
||||||
// filter for duplicates. Surprisingly common.
|
// filter for duplicates. Surprisingly common.
|
||||||
let uniqueTitles = [];
|
let uniqueTitles = [];
|
||||||
let uniqueModels: FrontendForecast[] = [];
|
let uniqueModels: AlgoliaQuestion[] = [];
|
||||||
for (let model of mappedModels) {
|
for (let model of mappedModels) {
|
||||||
if (!uniqueTitles.includes(model.title) && !model.title.includes("copy")) {
|
if (!uniqueTitles.includes(model.title) && !model.title.includes("copy")) {
|
||||||
uniqueModels.push(model);
|
uniqueModels.push(model);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import algoliasearch from "algoliasearch";
|
import algoliasearch from "algoliasearch";
|
||||||
|
|
||||||
import { FrontendForecast } from "../platforms";
|
import { AlgoliaQuestion } from "../../backend/utils/algolia";
|
||||||
|
|
||||||
const client = algoliasearch(
|
const client = algoliasearch(
|
||||||
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
|
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
|
||||||
|
@ -13,22 +13,21 @@ let buildFilter = ({
|
||||||
filterByPlatforms,
|
filterByPlatforms,
|
||||||
forecastsThreshold,
|
forecastsThreshold,
|
||||||
}) => {
|
}) => {
|
||||||
let starsFilter = starsThreshold
|
const starsFilter = starsThreshold
|
||||||
? `qualityindicators.stars >= ${starsThreshold}`
|
? `qualityindicators.stars >= ${starsThreshold}`
|
||||||
: null;
|
: null;
|
||||||
let platformsFilter = filterByPlatforms
|
const platformsFilter = filterByPlatforms
|
||||||
? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ")
|
? filterByPlatforms.map((platform) => `platform:"${platform}"`).join(" OR ")
|
||||||
: null;
|
: null;
|
||||||
console.log(platformsFilter);
|
const numForecastsFilter =
|
||||||
// let numForecastsFilter = forecastsThreshold ? `has_numforecasts:true AND qualityindicators.numforecasts >= ${forecastsThreshold}` : null
|
|
||||||
let numForecastsFilter =
|
|
||||||
forecastsThreshold > 0
|
forecastsThreshold > 0
|
||||||
? `qualityindicators.numforecasts >= ${forecastsThreshold}`
|
? `qualityindicators.numforecasts >= ${forecastsThreshold}`
|
||||||
: null;
|
: null;
|
||||||
let finalFilter = [starsFilter, platformsFilter, numForecastsFilter]
|
const finalFilter = [starsFilter, platformsFilter, numForecastsFilter]
|
||||||
.filter((f) => f != null)
|
.filter((f) => f != null)
|
||||||
.map((f) => `( ${f} )`)
|
.map((f) => `( ${f} )`)
|
||||||
.join(" AND ");
|
.join(" AND ");
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"searchWithAlgolia.js/searchWithAlgolia/buildFilter",
|
"searchWithAlgolia.js/searchWithAlgolia/buildFilter",
|
||||||
finalFilter
|
finalFilter
|
||||||
|
@ -51,18 +50,6 @@ let buildFacetFilter = ({ filterByPlatforms }) => {
|
||||||
return platformsFilter;
|
return platformsFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
let normalizeArray = (array) => {
|
|
||||||
if (array.length == 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
let mean = array.reduce((a, b) => a + b) / array.length;
|
|
||||||
let sd = Math.sqrt(
|
|
||||||
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b)
|
|
||||||
);
|
|
||||||
let normalizedArray = array.map((result) => (result - sd) / mean);
|
|
||||||
return normalizedArray;
|
|
||||||
};
|
|
||||||
|
|
||||||
let noExactMatch = (queryString, result) => {
|
let noExactMatch = (queryString, result) => {
|
||||||
queryString = queryString.toLowerCase();
|
queryString = queryString.toLowerCase();
|
||||||
let title = result.title.toLowerCase();
|
let title = result.title.toLowerCase();
|
||||||
|
@ -75,6 +62,14 @@ let noExactMatch = (queryString, result) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface SearchOpts {
|
||||||
|
queryString: string;
|
||||||
|
hitsPerPage?: number;
|
||||||
|
starsThreshold: number;
|
||||||
|
filterByPlatforms: string[];
|
||||||
|
forecastsThreshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
// only query string
|
// only query string
|
||||||
export default async function searchWithAlgolia({
|
export default async function searchWithAlgolia({
|
||||||
queryString,
|
queryString,
|
||||||
|
@ -82,8 +77,8 @@ export default async function searchWithAlgolia({
|
||||||
starsThreshold,
|
starsThreshold,
|
||||||
filterByPlatforms,
|
filterByPlatforms,
|
||||||
forecastsThreshold,
|
forecastsThreshold,
|
||||||
}): Promise<FrontendForecast[]> {
|
}: SearchOpts): Promise<AlgoliaQuestion[]> {
|
||||||
let response = await index.search<FrontendForecast>(queryString, {
|
const response = await index.search<AlgoliaQuestion>(queryString, {
|
||||||
hitsPerPage,
|
hitsPerPage,
|
||||||
filters: buildFilter({
|
filters: buildFilter({
|
||||||
starsThreshold,
|
starsThreshold,
|
||||||
|
@ -93,7 +88,7 @@ export default async function searchWithAlgolia({
|
||||||
//facetFilters: buildFacetFilter({filterByPlatforms}),
|
//facetFilters: buildFacetFilter({filterByPlatforms}),
|
||||||
getRankingInfo: true,
|
getRankingInfo: true,
|
||||||
});
|
});
|
||||||
let results: FrontendForecast[] = response.hits;
|
let results = response.hits;
|
||||||
|
|
||||||
let recursionError = ["metaforecast", "metaforecasts", "metaforecasting"];
|
let recursionError = ["metaforecast", "metaforecasts", "metaforecasting"];
|
||||||
if (
|
if (
|
||||||
|
@ -103,10 +98,10 @@ export default async function searchWithAlgolia({
|
||||||
results = [
|
results = [
|
||||||
{
|
{
|
||||||
id: "not-found",
|
id: "not-found",
|
||||||
|
objectID: "not-found",
|
||||||
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",
|
||||||
platformLabel: "Metaforecast",
|
|
||||||
description: "Maybe try a broader query?",
|
description: "Maybe try a broader query?",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
|
@ -121,24 +116,23 @@ export default async function searchWithAlgolia({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
||||||
|
stars: 5, // legacy
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
stars: 5,
|
stars: 5,
|
||||||
},
|
},
|
||||||
// noExactSearchResults: true,
|
extra: {},
|
||||||
// optionsstringforsearch: "Yes, No",
|
|
||||||
// has_numforecasts: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
} else if (recursionError.includes(queryString.toLowerCase())) {
|
} else if (recursionError.includes(queryString.toLowerCase())) {
|
||||||
results = [
|
results = [
|
||||||
{
|
{
|
||||||
id: "recursion-error",
|
id: "recursion-error",
|
||||||
|
objectID: "recursion-error",
|
||||||
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",
|
||||||
options: [
|
options: [
|
||||||
|
@ -154,14 +148,13 @@ export default async function searchWithAlgolia({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
||||||
|
stars: 5, // legacy
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
stars: 5,
|
stars: 5,
|
||||||
},
|
},
|
||||||
// noExactSearchResults: true,
|
extra: {},
|
||||||
// optionsstringforsearch: "Yes, No",
|
|
||||||
// has_numforecasts: true,
|
|
||||||
},
|
},
|
||||||
...results,
|
...results,
|
||||||
];
|
];
|
||||||
|
@ -172,10 +165,10 @@ export default async function searchWithAlgolia({
|
||||||
) {
|
) {
|
||||||
results.unshift({
|
results.unshift({
|
||||||
id: "not-found-2",
|
id: "not-found-2",
|
||||||
|
objectID: "not-found-2",
|
||||||
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",
|
||||||
platformLabel: "Metaforecast",
|
|
||||||
description: "Maybe try a broader query? That said, we could be wrong.",
|
description: "Maybe try a broader query? That said, we could be wrong.",
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
|
@ -190,17 +183,14 @@ export default async function searchWithAlgolia({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
timestamp: `${new Date().toISOString().slice(0, 10)}`,
|
||||||
|
stars: 1, // legacy
|
||||||
qualityindicators: {
|
qualityindicators: {
|
||||||
numforecasts: 1,
|
numforecasts: 1,
|
||||||
numforecasters: 1,
|
numforecasters: 1,
|
||||||
stars: 1,
|
stars: 1,
|
||||||
},
|
},
|
||||||
// noExactSearchResults: true,
|
extra: {},
|
||||||
// optionsstringforsearch: "Yes, No",
|
|
||||||
// has_numforecasts: true,
|
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// results[0].noExactSearchResults = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user