diff --git a/site/gatsby-site/gatsby-config.js b/site/gatsby-site/gatsby-config.js
index b61b2df203..d6e994118f 100755
--- a/site/gatsby-site/gatsby-config.js
+++ b/site/gatsby-site/gatsby-config.js
@@ -86,6 +86,7 @@ const plugins = [
'classifications',
'reports',
'entities',
+ 'publications',
],
connectionString: config.mongodb.connectionString,
extraParams: {
diff --git a/site/gatsby-site/gatsby-node.js b/site/gatsby-site/gatsby-node.js
index ccf35b4662..2adcf2e2cd 100644
--- a/site/gatsby-site/gatsby-node.js
+++ b/site/gatsby-site/gatsby-node.js
@@ -290,6 +290,17 @@ exports.createSchemaCustomization = ({ actions }) => {
public: Boolean
complete_from: completeFrom
}
+
+
+ type mongodbAiidprodPublicationsHarm_labels {
+ label: String
+ labeler: String
+ }
+ type mongodbAiidprodPublications implements Node {
+ domain: String
+ title: String
+ harm_labels: [mongodbAiidprodPublicationsHarm_labels]
+ }
`;
createTypes(typeDefs);
diff --git a/site/gatsby-site/migrations/2023.03.01T23.01.01.add-political-bias-labels.js b/site/gatsby-site/migrations/2023.03.01T23.01.01.add-political-bias-labels.js
new file mode 100644
index 0000000000..1aac5262a0
--- /dev/null
+++ b/site/gatsby-site/migrations/2023.03.01T23.01.01.add-political-bias-labels.js
@@ -0,0 +1,120 @@
+const config = require('../config');
+
+const axios = require('axios');
+
+const jsdom = require('jsdom');
+
+exports.up = async ({ context: { client } }) => {
+ const { JSDOM } = jsdom;
+
+ const publications = [];
+
+ const addBiasLabel = (publicationTitle, publicationDomain, labeler, label) => {
+ const existingPublication = publications.find((p) => p.domain == publicationDomain);
+
+ if (existingPublication) {
+ existingPublication.bias_labels.push({ label, labeler });
+ } else {
+ publications.push({
+ title: publicationTitle,
+ domain: publicationDomain,
+ bias_labels: [{ label, labeler }],
+ });
+ }
+ };
+
+ for (const alignment of [
+ 'left',
+ 'leftcenter',
+ 'center',
+ 'right-center',
+ 'right',
+ 'fake-news',
+ 'conspiracy',
+ ]) {
+ const mbfcResponse = await axios.get('https://mediabiasfactcheck.com/' + alignment);
+
+ if (mbfcResponse.status == 200) {
+ const mbfcPage = new JSDOM(mbfcResponse.data);
+
+ for (const tr of [...mbfcPage.window.document.querySelectorAll('#mbfc-table tr')]) {
+ const deepestChild = getDeepestChild(tr);
+
+ let tokens = deepestChild.textContent.split(' ');
+
+ let lastToken = tokens.pop();
+
+ let domain;
+
+ if (lastToken[0] == '(') {
+ domain = new URL(
+ 'http://' +
+ lastToken
+ .slice(1, lastToken.length - 1) // Remove parentheses
+ .replace(/^(www|m)\./, '')
+ ).hostname;
+ } else {
+ tokens.push(lastToken);
+ }
+ const title = tokens.join(' ');
+
+ if (domain) {
+ addBiasLabel(
+ title,
+ domain,
+ 'mediabiasfactcheck.com',
+ {
+ left: 'left',
+ right: 'right',
+
+ // These occur in most sources, and it's best to normalize them
+ // to a specific spelling / wording across sources
+ // so we can check if different sources agree.
+ leftcenter: 'center-left',
+ 'right-center': 'center-right',
+
+ // They use "center" in the url but "least biased" in the site text.
+ // There's a difference between being unbiased
+ // and being biased towards the center.
+ // They seem to be trying to capture the former:
+ //
+ // > These sources have minimal bias and use very few loaded words
+ // > (wording that attempts to influence an audience
+ // > by using appeal to emotion or stereotypes).
+ // > The reporting is factual and usually sourced.
+ // > These are the most credible media sources.
+ //
+ // Same with "fake-news" and "conspiracy".
+ center: 'least biased',
+ 'fake-news': 'questionable',
+ conspiracy: 'conspiracy/pseudoscience',
+ }[alignment]
+ );
+ }
+ }
+ }
+ }
+ await client.connect();
+
+ const publicationsCollection = await client
+ .db(config.realm.production_db.db_name)
+ .createCollection('publications');
+
+ for (const publication of publications) {
+ publicationsCollection.insertOne(publication);
+ }
+};
+
+exports.down = async ({ context: { client } }) => {
+ await client.connect();
+
+ await client.db(config.realm.production_db.db_name).dropCollection('publications');
+};
+
+var getDeepestChild = (htmlNode) => {
+ if (htmlNode.children.length == 0) {
+ return htmlNode;
+ } else {
+ return getDeepestChild(htmlNode.children[0]);
+ }
+};
diff --git a/site/gatsby-site/page-creators/createCitationPages.js b/site/gatsby-site/page-creators/createCitationPages.js
index b29e07dddb..e06b1976b1 100644
--- a/site/gatsby-site/page-creators/createCitationPages.js
+++ b/site/gatsby-site/page-creators/createCitationPages.js
@@ -5,7 +5,7 @@ const { switchLocalizedPath } = require('../i18n');
const createCitationPages = async (graphql, createPage, { languages }) => {
const result = await graphql(
`
- query IncidentIDs {
+ query ContextData {
allMongodbAiidprodIncidents {
nodes {
incident_id
@@ -32,13 +32,26 @@ const createCitationPages = async (graphql, createPage, { languages }) => {
language
image_url
cloudinary_id
+ source_domain
+ }
+ }
+
+ allMongodbAiidprodPublications {
+ nodes {
+ title
+ domain
+ bias_labels {
+ label
+ labeler
+ }
}
}
}
`
);
- const { allMongodbAiidprodIncidents, allMongodbAiidprodReports } = result.data;
+ const { allMongodbAiidprodIncidents, allMongodbAiidprodReports, allMongodbAiidprodPublications } =
+ result.data;
// Incident reports list
const incidentReportsMap = {};
@@ -80,6 +93,10 @@ const createCitationPages = async (graphql, createPage, { languages }) => {
reports: incidentReportsMap[incident_id],
}));
+ const publications = allMongodbAiidprodPublications.nodes.filter((publication) =>
+ incidentReportsMap[incident_id].some((report) => report.source_domain == publication.domain)
+ );
+
pageContexts.push({
incident,
incident_id,
@@ -89,6 +106,7 @@ const createCitationPages = async (graphql, createPage, { languages }) => {
nlp_similar_incidents,
editor_similar_incidents,
editor_dissimilar_incidents,
+ publications,
});
}
diff --git a/site/gatsby-site/src/components/BiasLabels.js b/site/gatsby-site/src/components/BiasLabels.js
new file mode 100644
index 0000000000..8c6b611abf
--- /dev/null
+++ b/site/gatsby-site/src/components/BiasLabels.js
@@ -0,0 +1,85 @@
+import React, { useState } from 'react';
+import { Modal } from 'flowbite-react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import {
+ faArrowCircleLeft,
+ faArrowCircleRight,
+ faChevronCircleLeft,
+ faChevronCircleRight,
+ faCheckCircle,
+ faExclamationCircle,
+} from '@fortawesome/free-solid-svg-icons';
+
+export function BiasIcon({ bias_labels, publicationName, className, style }) {
+ bias_labels ||= [];
+
+ // The modal causes server-side-rendering problems,
+ // so we need to disable rendering it
+ // until an interaction has occurred on the client-side.
+ const [modalRendered, setModalRendered] = useState(false);
+
+ const [modalVisible, setModalVisible] = useState(false);
+
+ const hasLabelName = (labelName) => bias_labels.some((biasLabel) => biasLabel.label == labelName);
+
+ const possibleLabels = [
+ 'least biased',
+ 'questionable',
+ 'left',
+ 'right',
+ 'center-left',
+ 'center-right',
+ ];
+
+ return (
+ <>
+
+ {modalRendered && (
+ The bias of this source was assessed as follows:
+ {bias_labels.map((biasLabel) => (
+
+