diff --git a/site/gatsby-site/cypress/e2e/integration/apps/newsdigest.cy.js b/site/gatsby-site/cypress/e2e/integration/apps/newsdigest.cy.js deleted file mode 100644 index 78c4ecf18a..0000000000 --- a/site/gatsby-site/cypress/e2e/integration/apps/newsdigest.cy.js +++ /dev/null @@ -1,110 +0,0 @@ -import { format } from 'date-fns'; -import { conditionalIt } from '../../../support/utils'; -import newsArticles from '../../../fixtures/candidates/newsArticles.json'; - -describe('News Digest', () => { - const url = '/apps/newsdigest'; - - it('Successfully loads', () => { - cy.visit(url); - }); - - it('Should load candidate cards', () => { - newsArticles.data.candidates[0].date_published = format(new Date(), 'yyyy-MM-dd'); - newsArticles.data.candidates[1].date_published = format(new Date(), 'yyyy-MM-dd'); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'NewsArticles', - 'NewsArticles', - newsArticles - ); - - cy.visit(url); - cy.get('[data-cy="candidate-card"]', { timeout: 15000 }).should('exist'); - }); - - it('Should open submit form on pressing submit', () => { - newsArticles.data.candidates[0].date_published = format(new Date(), 'yyyy-MM-dd'); - newsArticles.data.candidates[1].date_published = format(new Date(), 'yyyy-MM-dd'); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'NewsArticles', - 'NewsArticles', - newsArticles - ); - - cy.visit(url, { - onBeforeLoad(window) { - cy.stub(window, 'open', (url) => { - expect(url.slice(0, 12)).to.equal('/apps/submit'); - }); - }, - }); - cy.get('[data-cy="candidate-dropdown"] button').first().click(); - cy.get('[data-cy="submit-icon"]', { timeout: 15000 }).first().parent().click(); - cy.window().its('open').should('be.called'); - }); - - conditionalIt( - !Cypress.env('isEmptyEnvironment') && Cypress.env('e2eUsername') && Cypress.env('e2ePassword'), - 'Should dismiss and restore items', - () => { - cy.login(Cypress.env('e2eUsername'), Cypress.env('e2ePassword')); - - newsArticles.data.candidates[0].date_published = format(new Date(), 'yyyy-MM-dd'); - newsArticles.data.candidates[1].date_published = format(new Date(), 'yyyy-MM-dd'); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'NewsArticles', - 'NewsArticles', - newsArticles - ); - - cy.conditionalIntercept( - '**/graphql', - (req) => req.body.operationName == 'UpdateCandidate', - 'UpdateCandidate', - { - data: { - updateOneCandidate: { - url: 'https://dummy.com', - }, - }, - } - ); - - cy.visit(url); - - cy.get('[data-cy="results"] [data-cy="candidate-card"] [data-cy="candidate-dropdown"]', { - timeout: 15000, - }) - .first() - .parent() - .parent() - .parent() - .invoke('attr', 'data-id') - .then((dataId) => { - cy.get(`[data-id="${dataId}"] [data-cy="candidate-dropdown"]`).click(); - - cy.get(`[data-id="${dataId}"] [data-cy="dismiss-icon"]`).parent().click(); - - cy.get(`[data-cy="dismissed"] [data-id="${dataId}"]`).should('exist'); - - cy.get(`[data-cy="results"] [data-id="${dataId}"]`).should('not.exist'); - - cy.get(`[data-cy="dismissed-summary"]`).click(); - - cy.get(`[data-id="${dataId}"] [data-cy="candidate-dropdown"]`).click(); - - cy.get(`[data-id="${dataId}"] [data-cy="restore-icon"]`).parent().click(); - - cy.get(`[data-cy="results"] [data-id="${dataId}"]`, { timeout: 8000 }).should('exist'); - - cy.get(`[data-cy="dismissed"] [data-id="${dataId}"]`).should('not.exist'); - }); - } - ); -}); diff --git a/site/gatsby-site/cypress/fixtures/candidates/newsArticles.json b/site/gatsby-site/cypress/fixtures/candidates/newsArticles.json deleted file mode 100644 index c1eae80312..0000000000 --- a/site/gatsby-site/cypress/fixtures/candidates/newsArticles.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "data": { - "candidates": [ - { - "__typename": "Candidate", - "date_published": "2022-09-22", - "dismissed": null, - "matching_keywords": ["AI"], - "similarity": 0.9999999999999999, - "text": "![An excerpt from Zarya of the Dawn, which received a US copyright\nregistration.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/zarya_hero-800x448.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/zarya_hero.jpg) /\n\nAn excerpt from the AI-assisted comic book\n\n_Zarya of the Dawn_\n\n, which received a US copyright registration.\n\nIn what might be a first, a New York-based artist named Kris Kashtanova has\nreceived US copyright registration on their graphic novel that features AI-\ngenerated artwork created by [latent\ndiffusion](https://www.louisbouchard.ai/latent-diffusion-models/) AI,\naccording to [their Instagram feed](https://www.instagram.com/p/CivS3iiPigt/)\nand confirmed through a public records search by Ars Technica.\n\nThe registration, effective September 15, applies to a comic book called\n[_Zarya of the Dawn_](https://aicomicbooks.com/). Kashtanova created the\nartwork for _Zarya_ using Midjourney, a commercial image synthesis service. In\ntheir post announcing the news from Tuesday, Kashtanova wrote:\n\n\u003e I got Copyright from the Copyright Office of the USA on my Ai-generated\n\u003e graphic novel. I was open how it was made and put Midjourney on the cover\n\u003e page. It wasn’t altered in any other way. Just the way you saw it here.\n\u003e\n\u003e I tried to make a case that we do own copyright when we make something using\n\u003e AI. I registered it as visual arts work. My certificate is in the mail and I\n\u003e got the number and a confirmation today that it was approved.\n\u003e\n\u003e My friend lawyer gave me this idea and I decided to make a precedent.\n\nGoing by their announcement, Kashtanova approached the registration by saying\nthe artwork was AI-assisted and not created entirely by the AI. Kashtanova\nwrote the comic book story, created the layout, and made artistic choices to\npiece the images together.\n\nIt's likely that artists have registered works created by machine or\nalgorithms before because the history of generative art [extends back to the\n1960s](https://en.wikipedia.org/wiki/Generative_art#History). But this is the\nfirst time we know of that an artist has registered a copyright for art\ncreated by the recent round of image synthesis models powered by latent\ndiffusion, which has been a [contentious\nsubject](https://arstechnica.com/information-technology/2022/09/artists-begin-\nselling-ai-generated-artwork-on-stock-photography-websites/) among artists.\n\nSpeculation about whether AI artwork can be copyrighted has been the subject\nof many articles over the past few months, and just yesterday, [we wrote\nabout](https://arstechnica.com/information-technology/2022/09/fearing-\ncopyright-issues-getty-images-bans-ai-generated-artwork/) Getty Images banning\nAI-generated artwork on its site over unresolved issues about copyright and\nethics issues.\n\nDespite popular misconception (explained in the Getty piece), the US Copyright\nOffice has not ruled against copyright on AI artworks. Instead, it [ruled\nout](https://www.theverge.com/2022/2/21/22944335/us-copyright-office-reject-\nai-generated-art-recent-entrance-to-paradise) copyright registered to an AI as\nthe author instead of a human.\n\n_Zarya of the Dawn_ , which features a main character with an uncanny\nresemblance to the actress [Zendaya](https://en.wikipedia.org/wiki/Zendaya),\nis available for free through the AI Comic Books website. AI artists often use\ncelebrity names in their prompts to achieve consistency between images, since\nthere are many celebrity photographs in the data set used to train Midjourney.\n\n", - "title": "Artist receives first known US copyright registration for latent diffusion AI art | Ars Technica", - "url": "https://arstechnica.com/?p=1883867" - }, - { - "__typename": "Candidate", - "date_published": "2022-09-22", - "dismissed": null, - "matching_keywords": ["AI", "neural net"], - "matching_harm_keywords": ["harm"], - "similarity": 0.9999999999999999, - "text": "![A selection of Stable Diffusion images with a strike-out through\nthem.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/ai_image_ban_1-800x448.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/ai_image_ban_1.jpg) /\n\nA selection of Stable Diffusion images with a strikeout through them.\n\nArs Technica\n\nGetty Images has banned the sale of AI generative artwork created using image\nsynthesis models such as Stable Diffusion, DALL-E 2, and Midjourney through\nits service, [The Verge\nreports](https://www.theverge.com/2022/9/21/23364696/getty-images-ai-ban-\ngenerated-artwork-illustration-copyright).\n\nTo clarify the new policy, The Verge spoke with Getty Images CEO Craig Peters.\n\"There are real concerns with respect to the copyright of outputs from these\nmodels and unaddressed rights issues with respect to the imagery, the image\nmetadata and those individuals contained within the imagery,\" Peters told the\npublication.\n\nGetty Images is a large repository of stock and archival photographs and\nillustrations, often used by publications (such as Ars Technica) to illustrate\narticles after paying a license fee.\n\nGetty's move [follows](https://arstechnica.com/information-\ntechnology/2022/09/flooded-with-ai-generated-images-some-art-communities-ban-\nthem-completely/) image synthesis bans by smaller art community sites earlier\nthis month, which found their sites flooded with AI-generated work that\nthreatened to overwhelm artwork created without the use of those tools. Getty\nImages competitor Shutterstock [allows](https://arstechnica.com/information-\ntechnology/2022/09/artists-begin-selling-ai-generated-artwork-on-stock-\nphotography-websites/) AI-generated artwork on its site (and although Vice\n[recently reported](https://www.vice.com/en/article/v7vzpj/shutterstock-is-\nremoving-ai-generated-images) the site was removing AI artwork, we still see\nthe same amount as before—and Shutterstock's content submission terms have not\nchanged).\n\n[![A notice from Getty Images and iStock about a ban on \"AI generated\ncontent.\"](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/getty_images_notice-1-640x479.jpg)](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/getty_images_notice-1.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/getty_images_notice-1.jpg) /\n\nA notice from Getty Images and iStock about a ban on \"AI generated content.\"\n\nGetty Images\n\nThe ability to copyright AI-generated artwork has not been tested in court,\nand the ethics of using artists' work without consent (including artwork\n[found](https://waxy.org/2022/08/exploring-12-million-of-the-images-used-to-\ntrain-stable-diffusions-image-generator/) on Getty Images) to train neural\nnetworks that can create almost human-level artwork is still [an open\nquestion](https://arstechnica.com/information-technology/2022/09/have-ai-\nimage-generators-assimilated-your-art-new-tool-lets-you-check/) being debated\nonline. To protect the company's brand and its customers, Getty decided to\navoid the issue altogether with its ban. That said, Ars Technica searched the\nGetty Images library and found AI-generated artwork.\n\n## Can AI artwork be copyrighted?\n\nWhile the creators of popular AI image synthesis models insist their products\ncreate work protected by copyright, the issue of copyright over AI-generated\nimages has not yet been fully resolved. It's worth pointing out that an\n[often-cited article](https://www.smithsonianmag.com/smart-news/us-copyright-\noffice-rules-ai-art-cant-be-copyrighted-180979808/) in the Smithsonian titled\n\"US Copyright Office Rules AI Art Can't Be Copyrighted\" has an erroneous title\nand is often misunderstood. In that case, a researcher attempted to register\nan AI algorithm as the non-human owner of a copyright, which the Copyright\nOffice denied. The copyright owner must be human (or a group of humans, in the\ncase of a corporation).\n\nCurrently, AI image synthesis firms operate under the assumption that the\ncopyright for AI artwork can be registered to a human or corporation, just as\nit is with the output of any other artistic tool. There is some strong\nprecedent to this, and in the Copyright Office's [2022\ndecision](https://www.copyright.gov/rulings-filings/review-\nboard/docs/a-recent-entrance-to-paradise.pdf) rejecting the registry of\ncopyright to an AI (as mentioned above), it referenced a landmark 1884 legal\ncase that affirmed the copyright status of photographs.\n\nEarly in the camera's history, the defendant in the case ( _[Burrow-Giles\nLithographic Co. v. Sarony](https://en.wikipedia.org/wiki/Burrow-\nGiles_Lithographic_Co._v._Sarony))_ claimed that photographs could not be\ncopyrighted because a photo is \"a reproduction on paper of the exact features\nof some natural object or of some person.\" In effect, they argued that a photo\nis the work of a machine and not a creative expression. Instead, the court\nruled that photos can be copyrighted because they are \"representatives of\noriginal intellectual conceptions of [an] author.\"\n\nPeople familiar with the AI generative art process as it now stands, at least\nregarding text-to-image generators, will recognize that their image synthesis\noutputs are \"representatives of original intellectual conceptions of [an]\nauthor\" as well. Despite misconceptions to the contrary, creative input and\nguidance of a human are still necessary to create image synthesis work, no\nmatter how small the contribution. Even the selection of the tool and the\ndecision to execute it is a creative act.\n\nUnder US copyright law, pressing the shutter button of a camera randomly\npointed at a wall [still assigns\ncopyright](https://www.copyright.gov/help/faq/faq-general.html) to the human\nwho took the picture, and yet the human creative input in an image synthesis\nartwork can be much more extensive. So it would make sense that the person who\ninitiated the AI-generated work holds the copyright to the image unless\notherwise restrained by license or terms of use.\n\nAll that said, the question of copyright over AI artwork has yet to be legally\nresolved one way or the other in the United States. Stay tuned for further\ndevelopments.\n\n", - "title": "Fearing copyright issues, Getty Images bans AI-generated artwork | Ars Technica", - "url": "https://arstechnica.com/?p=1883513" - }, - { - "__typename": "Candidate", - "date_published": "2022-09-19", - "dismissed": null, - "matching_keywords": ["AI"], - "similarity": 0.0, - "text": "![Negative photo image of a western blot test\nresult.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/western_blot_3-800x448.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/western_blot_3.jpg) /\n\nColorized photo image of a Western blot test result.\n\nScientific publishers such as the American Association for Cancer Research\n(AACR) and Taylor \u0026 Francis have begun attempting to detect fraud in academic\npaper submissions with an AI image-checking program called Proofig,\n[reports](https://www.theregister.com/2022/09/12/academic_publishers_are_using_ai)\nThe Register. Proofig, a product of an Israeli firm of the same name, aims to\nhelp use \"artificial intelligence, computer vision and image processing to\nreview image integrity in scientific publications,\"\n[according](https://www.proofig.com/) to the company's website.\n\nDuring a trial that ran from January 2021 to May 2022, AACR used Proofig to\nscreen 1,367 papers accepted for publication, according to The Register. Of\nthose, 208 papers required author contact to clear up issues such as mistaken\nduplications, and four papers were withdrawn.\n\nIn particular, many journals need help detecting image duplication fraud in\n[Western blots](https://en.wikipedia.org/wiki/Western_blot), which are a\nspecific style of protein-detection imagery consisting of line segments of\nvarious widths. Subtle differences in a blot's appearance can translate to\ndramatically different conclusions about test results, and [many cases of\nacademic fraud](https://www.science.org/content/blog-post/down-western-blot)\nhave seen unscrupulous researchers duplicate, crop, stretch, and rotate\nWestern blots to make it appear like they have more (or different) data than\nthey really do. Detecting duplicate images can be tedious work for human eyes,\nwhich is why some firms like Proofig and [ImageTwin](https://imagetwin.ai/),\nan Austrian firm, are attempting to automate the process.\n\nBut both Proofig's and ImageTwin's solutions currently have significant\nlimitations, according to The Register. First, human expertise is still\nnecessary to interpret detection results and reduce false positives. Second,\nProofig is currently expensive due to its computationally intensive process,\ncosting $99 to analyze 120 images for an individual (the journals have\nnegotiated cheaper rates). Currently, both high cost and the requirement for\nmanual oversight is keeping the journals from analyzing every paper at the\nsubmission stage. Instead, they have been reserving its use for later in the\npublication process.\n\nAcademic fraud, while uncommon, can still have a devastating effect on a\npublication's reputation. Between the [massive\nvolume](https://www.nature.com/articles/nj7612-457a) of academic papers being\npublished today and recent revelations about image fraud in [widely cited\nAlzheimer's research](https://www.science.org/content/article/potential-\nfabrication-research-images-threatens-key-theory-alzheimers-disease), the\nfield does seem ripe for computer vision tools that can assist humans with\nfraud detection. Their overall effectiveness—and how widely they become\nadopted—is still a developing story.\n\n", - "title": "AI software helps bust image fraud in academic papers | Ars Technica", - "url": "https://arstechnica.com/?p=1882596" - }, - { - "__typename": "Candidate", - "date_published": "2022-09-22", - "dismissed": null, - "matching_keywords": ["AI", "neural net"], - "similarity": 0.9964958016539909, - "text": "![A pink waveform on a blue background, poetically suggesting\naudio.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/waveform_hero_1-800x448.jpg)\n\nBenj Edwards / Ars Technica\n\nOn Wednesday, OpenAI released a new open source AI model called\n[Whisper](https://openai.com/blog/whisper/) that recognizes and translates\naudio at a level that approaches human recognition ability. It can transcribe\ninterviews, podcasts, conversations, and more.\n\nOpenAI [trained Whisper](https://cdn.openai.com/papers/whisper.pdf) on 680,000\nhours of audio data and matching transcripts in 98 languages collected from\nthe web. According to OpenAI, this open-collection approach has led to\n\"improved robustness to accents, background noise, and technical language.\" It\ncan also detect the spoken language and translate it to English.\n\nOpenAI describes Whisper as an [encoder-decoder\ntransformer](https://kikaben.com/transformers-encoder-decoder/), a type of\nneural network that can use context gleaned from input data to learn\nassociations that can then be translated into the model's output. OpenAI\npresents this overview of Whisper's operation:\n\n\u003e Input audio is split into 30-second chunks, converted into a log-Mel\n\u003e spectrogram, and then passed into an encoder. A decoder is trained to\n\u003e predict the corresponding text caption, intermixed with special tokens that\n\u003e direct the single model to perform tasks such as language identification,\n\u003e phrase-level timestamps, multilingual speech transcription, and to-English\n\u003e speech translation.\n\nBy open-sourcing Whisper, OpenAI hopes to introduce a new foundation model\nthat others can build on in the future to improve speech processing and\naccessibility tools. OpenAI has a significant track record on this front. In\nJanuary 2021, OpenAI released [CLIP](https://openai.com/blog/clip/), an open\nsource computer vision model that arguably ignited the recent era of rapidly\nprogressing image synthesis technology such as DALL-E 2 and [Stable\nDiffusion](https://arstechnica.com/information-technology/2022/09/with-stable-\ndiffusion-you-may-never-believe-what-you-see-online-again/).\n\nAt Ars Technica, we tested Whisper from code [available on\nGitHub](https://github.com/openai/whisper), and we fed it multiple samples,\nincluding a podcast episode and a particularly difficult-to-understand section\nof audio taken from a telephone interview. Although it took some time while\nrunning through a standard Intel desktop CPU (the technology doesn't work in\nreal time yet), Whisper did a good job of transcribing the audio into text\nthrough the demonstration Python program—far better than some AI-powered audio\ntranscription services we have tried in the past.\n\n[![Example console output from the OpenAI's Whisper demonstration program as\nit transcribes a podcast.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/whisper_podcast_output-640x142.jpg)](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/whisper_podcast_output.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/whisper_podcast_output.jpg) /\n\nExample console output from the OpenAI's Whisper demonstration program as it\ntranscribes a podcast.\n\nBenj Edwards / Ars Technica\n\nWith the proper setup, Whisper could easily be used to transcribe interviews,\npodcasts, and potentially translate podcasts produced in non-English languages\nto English on your machine—for free. That's a potent combination that might\neventually disrupt the transcription industry.\n\nAs with almost every major new AI model these days, Whisper brings positive\nadvantages and the potential for misuse. On Whisper's [model\ncard](https://github.com/openai/whisper/blob/main/model-card.md) (under the\n\"Broader Implications\" section), OpenAI warns that Whisper could be used to\nautomate surveillance or identify individual speakers in a conversation, but\nthe company hopes it will be used \"primarily for beneficial purposes.\"\n\n", - "title": "AI model from OpenAI automatically recognizes speech and translates it to English | Ars Technica", - "url": "https://arstechnica.com/?p=1883524" - }, - { - "__typename": "Candidate", - "date_published": "2022-09-21", - "dismissed": null, - "matching_keywords": ["AI", "facial recognition"], - "similarity": 0.0, - "text": "![Censored medical images found in the LAION-5B data set used to train AI. The\nblack bars and distortion have been added.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/medical_images_hero2-800x448.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/medical_images_hero2.jpg) /\n\nCensored medical images found in the LAION-5B data set used to train AI. The\nblack bars and distortion have been added.\n\nArs Technica\n\nLate last week, a California-based [AI\nartist](https://twitter.com/LapineDeLaTerre) who goes by the name Lapine\n[discovered](https://twitter.com/LapineDeLaTerre/status/1570889343845404672?s=20\u0026t=KThzGIaLvD7nV0GNxmu0UA)\nprivate medical record photos taken by her doctor in 2013 referenced in the\n[LAION-5B](https://laion.ai/blog/laion-5b/) image set, which is a scrape of\npublicly available images on the web. AI researchers download a subset of that\ndata to train AI image synthesis models such as Stable Diffusion and [Google\nImagen](https://imagen.research.google/).\n\nLapine discovered her medical photos on a site called [Have I Been\nTrained](https://arstechnica.com/information-technology/2022/09/have-ai-image-\ngenerators-assimilated-your-art-new-tool-lets-you-check/), which lets artists\nsee if their work is in the LAION-5B data set. Instead of doing a text search\non the site, Lapine uploaded a recent photo of herself using the site's\nreverse image search feature. She was surprised to discover a set of two\nbefore-and-after medical photos of her face, which had only been authorized\nfor private use by her doctor, as reflected in an authorization form Lapine\n[tweeted](https://twitter.com/LapineDeLaTerre/status/1570889343845404672) and\nalso provided to Ars.\n\nLapine has a genetic condition called [Dyskeratosis\nCongenita](https://rarediseases.org/rare-diseases/dyskeratosis-congenita/).\n\"It affects everything from my skin to my bones and teeth,\" Lapine told Ars\nTechnica in an interview. \"In 2013, I underwent a small set of procedures to\nrestore facial contours after having been through so many rounds of mouth and\njaw surgeries. These pictures are from my last set of procedures with this\nsurgeon.\"\n\nThe surgeon who possessed the medical photos died of cancer in 2018, according\nto Lapine, and she suspects that they somehow left his practice's custody\nafter that. \"It’s the digital equivalent of receiving stolen property,\" says\nLapine. \"Someone stole the image from my deceased doctor’s files and it ended\nup somewhere online, and then it was scraped into this dataset.\"\n\nLapine prefers to conceal her identity for medical privacy reasons. With\nrecords and photos provided by Lapine, Ars confirmed that there are medical\nimages of her referenced in the LAION data set. During our search for Lapine's\nphotos, we also discovered thousands of similar patient medical record photos\nin the data set, each of which may have a similar questionable ethical or\nlegal status, many of which have likely been integrated into popular image\nsynthesis models that companies like Midjourney and Stability AI offer as a\ncommercial service.\n\nThis does not mean that anyone can suddenly create an AI version of Lapine's\nface (as the technology stands at the moment)—and her name is not linked to\nthe photos—but it bothers her that private medical images have been baked into\na product without any form of consent or recourse to remove them. \"It’s bad\nenough to have a photo leaked, but now it’s part of a product,\" says Lapine.\n\"And this goes for anyone’s photos, medical record or not. And the future\nabuse potential is really high.\"\n\n## Who watches the watchers?\n\nLAION [describes itself](https://laion.ai/about/) as a nonprofit organization\nwith members worldwide, \"aiming to make large-scale machine learning models,\ndatasets and related code available to the general public.\" Its data can be\nused in various projects, from facial recognition to computer vision to image\nsynthesis.\n\nFor example, after an AI training process, some of the images in the LAION\ndata set become the basis of Stable Diffusion's [amazing\nability](https://arstechnica.com/information-technology/2022/09/with-stable-\ndiffusion-you-may-never-believe-what-you-see-online-again/) to generate images\nfrom text descriptions. Since LAION is a [set of URLs](https://laion.ai/faq/)\npointing to images on the web, LAION does not host the images themselves.\nInstead, LAION [says](https://laion.ai/faq/) that researchers must download\nthe images from various locations when they want to use them in a project.\n\n[![The LAION data set is replete with potentially sensitive images collected\nfrom the Internet, such as these, which are now being integrated into\ncommercial machine learning products. Black bars have been added by Ars for\nprivacy purposes.](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/medical_images-640x405.jpg)](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/medical_images.jpg)\n\n[Enlarge](https://cdn.arstechnica.net/wp-\ncontent/uploads/2022/09/medical_images.jpg) /\n\nThe LAION data set is replete with potentially sensitive images collected from\nthe Internet, such as these, which are now being integrated into commercial\nmachine learning products. Black bars have been added by Ars for privacy\npurposes.\n\nArs Technica\n\nUnder these conditions, responsibility for a particular image's inclusion in\nthe LAION set then becomes a fancy game of pass the buck. A friend of Lapine's\nposed an open question on the #safety-and-privacy channel of LAION's Discord\nserver last Friday asking how to remove her images from the set. LAION\nengineer Romain Beaumont replied, \"The best way to remove an image from the\nInternet is to ask for the hosting website to stop hosting it,\" wrote\nBeaumont. \"We are not hosting any of these images.\"\n\nIn the US, scraping publicly available data from the Internet [appears to be\nlegal](https://medium.com/@tjwaterman99/web-scraping-is-now-\nlegal-6bf0e5730a78), as the results from a 2019 court case affirm. Is it\nmostly the deceased doctor's fault, then? Or the site that hosts Lapine's\nillicit images on the web?\n\nArs contacted LAION for comment on these questions but did not receive a\nresponse by press time. LAION's website does provide [a\nform](https://laion.ai/gdpr/) where European citizens can request information\nremoved from their database to comply with the EU's GDPR laws, but only if a\nphoto of a person is associated with a name in the image's metadata. Thanks to\nservices such as [PimEyes](https://pimeyes.com/en), however, it has become\ntrivial to associate someone's face with names through other means.\n\nUltimately, Lapine understands how the chain of custody over her private\nimages failed but still would like to see her images removed from the LAION\ndata set. \"I would like to have a way for anyone to ask to have their image\nremoved from the data set without sacrificing personal information. Just\nbecause they scraped it from the web doesn’t mean it was supposed to be public\ninformation, or even on the web at all.\"\n\n", - "title": "Artist finds private medical record photos in popular AI training data set | Ars Technica", - "url": "https://arstechnica.com/?p=1882591" - } - ] - } -} diff --git a/site/gatsby-site/playwright/e2e/apps/newsdigest.spec.ts b/site/gatsby-site/playwright/e2e/apps/newsdigest.spec.ts new file mode 100644 index 0000000000..8d5f070b85 --- /dev/null +++ b/site/gatsby-site/playwright/e2e/apps/newsdigest.spec.ts @@ -0,0 +1,107 @@ +import { expect } from '@playwright/test'; +import { test, query } from '../../utils'; +import { gql } from '@apollo/client'; +import { init } from '../../memory-mongo'; +import config from '../../config'; + +test.describe('News Digest', () => { + const url = '/apps/newsdigest'; + + test('Successfully loads', async ({ page }) => { + await page.goto(url); + }); + + test('Should load candidate cards', async ({ page }) => { + await init(); + + await page.goto(url); + await page.locator('[data-cy="candidate-card"]').first().waitFor(); + }); + + test('Should open submit form on pressing submit', async ({ page }) => { + await init(); + + await page.goto(url); + await page.exposeFunction('open', (url: string) => { + expect(url.slice(0, 12)).toBe('/apps/submit'); + }); + + await page.locator('[data-cy="candidate-dropdown"] button').first().click(); + await page.locator('[data-cy="submit-icon"]').first().click(); + }); + + test('Should dismiss and restore items', async ({ page, login, skipOnEmptyEnvironment }) => { + + const userId = await login(config.E2E_ADMIN_USERNAME, config.E2E_ADMIN_PASSWORD); + await init({ customData: { users: [{ userId, first_name: 'Test', last_name: 'User', roles: ['admin'] }] } }, { drop: true }); + + await page.goto(url); + + const dataId = await page.locator('[data-cy="results"] [data-cy="candidate-card"] [data-cy="candidate-dropdown"]').first().evaluate(el => el.closest('[data-id]').getAttribute('data-id')); + + await page.locator(`[data-id="${dataId}"] [data-cy="candidate-dropdown"]`).click(); + await page.locator(`[data-id="${dataId}"] [data-cy="dismiss-icon"]`).click(); + + await expect(page.locator(`[data-cy="results"] [data-id="${dataId}"]`)).toHaveCount(0); + + await expect(page.locator('[data-cy="toast"]')).toContainText(`Dismissed article:`); + + const response = await query( + { + query: gql` + query NewsArticles($filter: CandidateFilterType!) { + candidates(filter: $filter) { + title + url + similarity + matching_keywords + matching_harm_keywords + matching_entities + date_published + dismissed + } + } + `, + variables: { + filter: { + dismissed: { EQ: true }, + }, + }, + }, + ); + await expect(response.data.candidates).toHaveLength(1); + await page.locator(`[data-cy="dismissed-summary"]`).click(); + await page.locator(`[data-id="${dataId}"] [data-cy="candidate-dropdown"]`).click(); + await page.locator(`[data-id="${dataId}"] [data-cy="restore-icon"]`).click(); + + await expect(page.locator(`[data-cy="dismissed"] [data-id="${dataId}"]`)).toHaveCount(0); + await expect(page.locator(`[data-cy="results"] [data-id="${dataId}"]`)).toHaveCount(1); + await expect(page.locator('[data-cy="toast"]').filter({ hasText: 'Restored article:' })).toContainText(`Restored article:`); + + const response2 = await query( + { + query: gql` + query NewsArticles($filter: CandidateFilterType!) { + candidates(filter: $filter) { + title + url + similarity + matching_keywords + matching_harm_keywords + matching_entities + date_published + dismissed + } + } + `, + variables: { + filter: { + dismissed: { EQ: true }, + }, + }, + }, + ); + + await expect(response2.data.candidates).toHaveLength(0); + }); +}); diff --git a/site/gatsby-site/playwright/memory-mongo.ts b/site/gatsby-site/playwright/memory-mongo.ts index dd7b5fa0e7..1042421597 100644 --- a/site/gatsby-site/playwright/memory-mongo.ts +++ b/site/gatsby-site/playwright/memory-mongo.ts @@ -9,6 +9,7 @@ import entities from './seeds/aiidprod/entities'; import reports_es from './seeds/translations/reports_es'; import classifications from './seeds/aiidprod/classifications'; import taxa from './seeds/aiidprod/taxa'; +import candidates from './seeds/aiidprod/candidates'; import duplicates from './seeds/aiidprod/duplicates'; import users from './seeds/customData/users'; @@ -25,6 +26,7 @@ export const init = async (extra?: Record { + const newDate = new Date( + new Date().getTime() - 86400000 * i // i days ago + ) + + const day = newDate.getDate(); + const month = newDate.getMonth(); + const year = newDate.getFullYear(); + + return new Date(year, month, day).toISOString().slice(0, 10); + }) + +const candidates: DBCandidate[] = [ + { + match: true, + title: "Candidate 1", + url: "https://www.candidate1.com", + date_published: dates[0], + dismissed: false, + similarity: 0.99, + matching_keywords: ["keyword1", "keyword2"], + matching_harm_keywords: ["harmkeyword1"], + matching_entities: ["entity1"], + text: "Candidate 1 Text", + plain_text: "Candidate 1 Plain Text" + }, + { + match: true, + title: "Candidate 2", + url: "https://www.candidate2.com", + date_published: dates[1], + dismissed: false, + similarity: 0.85, + matching_keywords: ["keyword3"], + matching_harm_keywords: ["harmkeyword2"], + matching_entities: ["entity2"], + text: "Candidate 2 Text", + plain_text: "Candidate 2 Plain Text" + }, + { + match: false, + title: "Candidate 3", + url: "https://www.candidate3.com", + date_published: dates[2], + dismissed: false, + similarity: 0.99, + matching_keywords: ["keyword4", "keyword5"], + matching_harm_keywords: ["harmkeyword3"], + matching_entities: [], + text: "Candidate 3 Text", + plain_text: "Candidate 3 Plain Text" + }, + { + match: true, + title: "Candidate 4", + url: "https://www.candidate4.com", + date_published: dates[3], + dismissed: false, + similarity: 0.99, + matching_keywords: ["keyword6", "keyword7"], + matching_harm_keywords: ["harmkeyword4"], + matching_entities: ["entity2"], + text: "Candidate 4 Text", + plain_text: "Candidate 4 Plain Text" + }, +]; + +export default candidates; \ No newline at end of file diff --git a/site/gatsby-site/server/fields/candidates.ts b/site/gatsby-site/server/fields/candidates.ts new file mode 100644 index 0000000000..11f0add44a --- /dev/null +++ b/site/gatsby-site/server/fields/candidates.ts @@ -0,0 +1,91 @@ +import { GraphQLBoolean, GraphQLFieldConfigMap, GraphQLFloat, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString } from "graphql"; +import { allow } from "graphql-shield"; +import { isAdmin } from "../rules"; +import { ObjectIdScalar } from "../scalars"; +import { generateMutationFields, generateQueryFields } from "../utils"; +import { GraphQLDateTime } from "graphql-scalars"; + +const CandidateType = new GraphQLObjectType({ + name: 'Candidate', + fields: { + _id: { + type: ObjectIdScalar, + }, + title: { + type: new GraphQLNonNull(GraphQLString), + }, + date_published: { + type: new GraphQLNonNull(GraphQLString), + }, + matching_keywords: { + type: new GraphQLList(GraphQLString), + }, + matching_harm_keywords: { + type: new GraphQLList(GraphQLString), + }, + matching_entities: { + type: new GraphQLList(GraphQLString), + }, + + embedding: { + type: new GraphQLObjectType({ + name: 'CandidateEmbedding', + fields: { + vector: { + type: new GraphQLList(GraphQLInt) + }, + source: { + type: GraphQLString + } + } + }) + }, + similarity: { + type: GraphQLFloat, + }, + match: { + type: GraphQLBoolean, + }, + created_at: { + type: GraphQLDateTime + }, + url: { + type: new GraphQLNonNull(GraphQLString), + }, + dismissed: { + type: GraphQLBoolean, + }, + text: { + type: GraphQLString, + }, + plain_text: { + type: GraphQLString, + }, + }, +}); + +export const queryFields: GraphQLFieldConfigMap = { + + ...generateQueryFields({ collectionName: 'candidates', Type: CandidateType }) +} + +export const mutationFields: GraphQLFieldConfigMap = { + + ...generateMutationFields({ collectionName: 'candidates', Type: CandidateType }), +} + +export const permissions = { + Query: { + candidate: allow, + candidates: allow, + }, + Mutation: { + deleteOneCandidate: isAdmin, + deleteManyCandidates: isAdmin, + insertOneCandidate: allow, + insertManyCandidates: isAdmin, + updateOneCandidate: isAdmin, + updateManyCandidates: isAdmin, + upsertOneCandidate: isAdmin, + } +} \ No newline at end of file diff --git a/site/gatsby-site/server/generated/graphql.ts b/site/gatsby-site/server/generated/graphql.ts index c4d92ecd46..f29eb371ec 100644 --- a/site/gatsby-site/server/generated/graphql.ts +++ b/site/gatsby-site/server/generated/graphql.ts @@ -98,25 +98,18 @@ export type BooleanNotFilter = { export type Candidate = { __typename?: 'Candidate'; _id?: Maybe; - authors?: Maybe>>; - classification_similarity?: Maybe>>; - date_downloaded?: Maybe; - date_published?: Maybe; + created_at?: Maybe; + date_published: Scalars['String']['output']; dismissed?: Maybe; embedding?: Maybe; - epoch_date_downloaded?: Maybe; - epoch_date_published?: Maybe; - image_url?: Maybe; - language?: Maybe; - match: Scalars['Boolean']['output']; + match?: Maybe; matching_entities?: Maybe>>; matching_harm_keywords?: Maybe>>; matching_keywords?: Maybe>>; plain_text?: Maybe; similarity?: Maybe; - source_domain?: Maybe; text?: Maybe; - title?: Maybe; + title: Scalars['String']['output']; url: Scalars['String']['output']; }; @@ -165,6 +158,7 @@ export type CandidateClassification_SimilarityUpdateInput = { export type CandidateEmbedding = { __typename?: 'CandidateEmbedding'; from_text_hash?: Maybe; + source?: Maybe; vector?: Maybe>>; }; @@ -173,6 +167,17 @@ export type CandidateEmbeddingInsertInput = { vector?: InputMaybe>>; }; +export type CandidateEmbeddingInsertType = { + source?: InputMaybe; + vector?: InputMaybe>>; +}; + +export type CandidateEmbeddingObjectFilterType = { + opr?: InputMaybe; + source?: InputMaybe; + vector?: InputMaybe; +}; + export type CandidateEmbeddingQueryInput = { AND?: InputMaybe>; OR?: InputMaybe>; @@ -191,6 +196,17 @@ export type CandidateEmbeddingQueryInput = { vector_nin?: InputMaybe>>; }; +export type CandidateEmbeddingSetObjectType = { + /** If set to true, the object would be overwriten entirely, including fields that are not specified. Non-null validation rules will apply. Once set to true, any child object will overwriten invariably of the value set to this field. */ + _OVERWRITE?: InputMaybe; + source?: InputMaybe; + vector?: InputMaybe>>; +}; + +export type CandidateEmbeddingSortType = { + source?: InputMaybe; +}; + export type CandidateEmbeddingUpdateInput = { from_text_hash?: InputMaybe; from_text_hash_unset?: InputMaybe; @@ -198,178 +214,58 @@ export type CandidateEmbeddingUpdateInput = { vector_unset?: InputMaybe; }; -export type CandidateInsertInput = { +export type CandidateFilterType = { + AND?: InputMaybe>>; + NOR?: InputMaybe>>; + OR?: InputMaybe>>; + _id?: InputMaybe; + created_at?: InputMaybe; + date_published?: InputMaybe; + dismissed?: InputMaybe; + embedding?: InputMaybe; + match?: InputMaybe; + matching_entities?: InputMaybe; + matching_harm_keywords?: InputMaybe; + matching_keywords?: InputMaybe; + plain_text?: InputMaybe; + similarity?: InputMaybe; + text?: InputMaybe; + title?: InputMaybe; + url?: InputMaybe; +}; + +export type CandidateInsertType = { _id?: InputMaybe; - authors?: InputMaybe>>; - classification_similarity?: InputMaybe>>; - date_downloaded?: InputMaybe; - date_published?: InputMaybe; + created_at?: InputMaybe; + date_published: Scalars['String']['input']; dismissed?: InputMaybe; - embedding?: InputMaybe; - epoch_date_downloaded?: InputMaybe; - epoch_date_published?: InputMaybe; - image_url?: InputMaybe; - language?: InputMaybe; - match: Scalars['Boolean']['input']; + embedding?: InputMaybe; + match?: InputMaybe; matching_entities?: InputMaybe>>; matching_harm_keywords?: InputMaybe>>; matching_keywords?: InputMaybe>>; plain_text?: InputMaybe; similarity?: InputMaybe; - source_domain?: InputMaybe; text?: InputMaybe; - title?: InputMaybe; + title: Scalars['String']['input']; url: Scalars['String']['input']; }; -export type CandidateQueryInput = { - AND?: InputMaybe>; - OR?: InputMaybe>; +export type CandidateSetType = { _id?: InputMaybe; - _id_exists?: InputMaybe; - _id_gt?: InputMaybe; - _id_gte?: InputMaybe; - _id_in?: InputMaybe>>; - _id_lt?: InputMaybe; - _id_lte?: InputMaybe; - _id_ne?: InputMaybe; - _id_nin?: InputMaybe>>; - authors?: InputMaybe>>; - authors_exists?: InputMaybe; - authors_in?: InputMaybe>>; - authors_nin?: InputMaybe>>; - classification_similarity?: InputMaybe>>; - classification_similarity_exists?: InputMaybe; - classification_similarity_in?: InputMaybe>>; - classification_similarity_nin?: InputMaybe>>; - date_downloaded?: InputMaybe; - date_downloaded_exists?: InputMaybe; - date_downloaded_gt?: InputMaybe; - date_downloaded_gte?: InputMaybe; - date_downloaded_in?: InputMaybe>>; - date_downloaded_lt?: InputMaybe; - date_downloaded_lte?: InputMaybe; - date_downloaded_ne?: InputMaybe; - date_downloaded_nin?: InputMaybe>>; + created_at?: InputMaybe; date_published?: InputMaybe; - date_published_exists?: InputMaybe; - date_published_gt?: InputMaybe; - date_published_gte?: InputMaybe; - date_published_in?: InputMaybe>>; - date_published_lt?: InputMaybe; - date_published_lte?: InputMaybe; - date_published_ne?: InputMaybe; - date_published_nin?: InputMaybe>>; dismissed?: InputMaybe; - dismissed_exists?: InputMaybe; - dismissed_ne?: InputMaybe; - embedding?: InputMaybe; - embedding_exists?: InputMaybe; - epoch_date_downloaded?: InputMaybe; - epoch_date_downloaded_exists?: InputMaybe; - epoch_date_downloaded_gt?: InputMaybe; - epoch_date_downloaded_gte?: InputMaybe; - epoch_date_downloaded_in?: InputMaybe>>; - epoch_date_downloaded_lt?: InputMaybe; - epoch_date_downloaded_lte?: InputMaybe; - epoch_date_downloaded_ne?: InputMaybe; - epoch_date_downloaded_nin?: InputMaybe>>; - epoch_date_published?: InputMaybe; - epoch_date_published_exists?: InputMaybe; - epoch_date_published_gt?: InputMaybe; - epoch_date_published_gte?: InputMaybe; - epoch_date_published_in?: InputMaybe>>; - epoch_date_published_lt?: InputMaybe; - epoch_date_published_lte?: InputMaybe; - epoch_date_published_ne?: InputMaybe; - epoch_date_published_nin?: InputMaybe>>; - image_url?: InputMaybe; - image_url_exists?: InputMaybe; - image_url_gt?: InputMaybe; - image_url_gte?: InputMaybe; - image_url_in?: InputMaybe>>; - image_url_lt?: InputMaybe; - image_url_lte?: InputMaybe; - image_url_ne?: InputMaybe; - image_url_nin?: InputMaybe>>; - language?: InputMaybe; - language_exists?: InputMaybe; - language_gt?: InputMaybe; - language_gte?: InputMaybe; - language_in?: InputMaybe>>; - language_lt?: InputMaybe; - language_lte?: InputMaybe; - language_ne?: InputMaybe; - language_nin?: InputMaybe>>; + embedding?: InputMaybe; match?: InputMaybe; - match_exists?: InputMaybe; - match_ne?: InputMaybe; matching_entities?: InputMaybe>>; - matching_entities_exists?: InputMaybe; - matching_entities_in?: InputMaybe>>; - matching_entities_nin?: InputMaybe>>; matching_harm_keywords?: InputMaybe>>; - matching_harm_keywords_exists?: InputMaybe; - matching_harm_keywords_in?: InputMaybe>>; - matching_harm_keywords_nin?: InputMaybe>>; matching_keywords?: InputMaybe>>; - matching_keywords_exists?: InputMaybe; - matching_keywords_in?: InputMaybe>>; - matching_keywords_nin?: InputMaybe>>; plain_text?: InputMaybe; - plain_text_exists?: InputMaybe; - plain_text_gt?: InputMaybe; - plain_text_gte?: InputMaybe; - plain_text_in?: InputMaybe>>; - plain_text_lt?: InputMaybe; - plain_text_lte?: InputMaybe; - plain_text_ne?: InputMaybe; - plain_text_nin?: InputMaybe>>; similarity?: InputMaybe; - similarity_exists?: InputMaybe; - similarity_gt?: InputMaybe; - similarity_gte?: InputMaybe; - similarity_in?: InputMaybe>>; - similarity_lt?: InputMaybe; - similarity_lte?: InputMaybe; - similarity_ne?: InputMaybe; - similarity_nin?: InputMaybe>>; - source_domain?: InputMaybe; - source_domain_exists?: InputMaybe; - source_domain_gt?: InputMaybe; - source_domain_gte?: InputMaybe; - source_domain_in?: InputMaybe>>; - source_domain_lt?: InputMaybe; - source_domain_lte?: InputMaybe; - source_domain_ne?: InputMaybe; - source_domain_nin?: InputMaybe>>; text?: InputMaybe; - text_exists?: InputMaybe; - text_gt?: InputMaybe; - text_gte?: InputMaybe; - text_in?: InputMaybe>>; - text_lt?: InputMaybe; - text_lte?: InputMaybe; - text_ne?: InputMaybe; - text_nin?: InputMaybe>>; title?: InputMaybe; - title_exists?: InputMaybe; - title_gt?: InputMaybe; - title_gte?: InputMaybe; - title_in?: InputMaybe>>; - title_lt?: InputMaybe; - title_lte?: InputMaybe; - title_ne?: InputMaybe; - title_nin?: InputMaybe>>; url?: InputMaybe; - url_exists?: InputMaybe; - url_gt?: InputMaybe; - url_gte?: InputMaybe; - url_in?: InputMaybe>>; - url_lt?: InputMaybe; - url_lte?: InputMaybe; - url_ne?: InputMaybe; - url_nin?: InputMaybe>>; }; export enum CandidateSortByInput { @@ -401,52 +297,22 @@ export enum CandidateSortByInput { IdDesc = '_ID_DESC' } -export type CandidateUpdateInput = { - _id?: InputMaybe; - _id_unset?: InputMaybe; - authors?: InputMaybe>>; - authors_unset?: InputMaybe; - classification_similarity?: InputMaybe>>; - classification_similarity_unset?: InputMaybe; - date_downloaded?: InputMaybe; - date_downloaded_unset?: InputMaybe; - date_published?: InputMaybe; - date_published_unset?: InputMaybe; - dismissed?: InputMaybe; - dismissed_unset?: InputMaybe; - embedding?: InputMaybe; - embedding_unset?: InputMaybe; - epoch_date_downloaded?: InputMaybe; - epoch_date_downloaded_inc?: InputMaybe; - epoch_date_downloaded_unset?: InputMaybe; - epoch_date_published?: InputMaybe; - epoch_date_published_inc?: InputMaybe; - epoch_date_published_unset?: InputMaybe; - image_url?: InputMaybe; - image_url_unset?: InputMaybe; - language?: InputMaybe; - language_unset?: InputMaybe; - match?: InputMaybe; - match_unset?: InputMaybe; - matching_entities?: InputMaybe>>; - matching_entities_unset?: InputMaybe; - matching_harm_keywords?: InputMaybe>>; - matching_harm_keywords_unset?: InputMaybe; - matching_keywords?: InputMaybe>>; - matching_keywords_unset?: InputMaybe; - plain_text?: InputMaybe; - plain_text_unset?: InputMaybe; - similarity?: InputMaybe; - similarity_inc?: InputMaybe; - similarity_unset?: InputMaybe; - source_domain?: InputMaybe; - source_domain_unset?: InputMaybe; - text?: InputMaybe; - text_unset?: InputMaybe; - title?: InputMaybe; - title_unset?: InputMaybe; - url?: InputMaybe; - url_unset?: InputMaybe; +export type CandidateSortType = { + _id?: InputMaybe; + created_at?: InputMaybe; + date_published?: InputMaybe; + dismissed?: InputMaybe; + embedding?: InputMaybe; + match?: InputMaybe; + plain_text?: InputMaybe; + similarity?: InputMaybe; + text?: InputMaybe; + title?: InputMaybe; + url?: InputMaybe; +}; + +export type CandidateUpdateType = { + set?: InputMaybe; }; export type Checklist = { @@ -2694,7 +2560,6 @@ export type Mutation = { logReportHistory?: Maybe; processNotifications?: Maybe; promoteSubmissionToReport: PromoteSubmissionToReportPayload; - replaceOneCandidate?: Maybe; replaceOneChecklist?: Maybe; replaceOneHistory_incident?: Maybe; replaceOneHistory_report?: Maybe; @@ -2747,7 +2612,9 @@ export type MutationCreateVariantArgs = { export type MutationDeleteManyCandidatesArgs = { - query?: InputMaybe; + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; }; @@ -2793,7 +2660,9 @@ export type MutationDeleteManySubscriptionsArgs = { export type MutationDeleteOneCandidateArgs = { - query: CandidateQueryInput; + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; }; @@ -2863,7 +2732,7 @@ export type MutationGetUserArgs = { export type MutationInsertManyCandidatesArgs = { - data: Array; + data: Array>; }; @@ -2898,7 +2767,7 @@ export type MutationInsertManyQuickaddsArgs = { export type MutationInsertOneCandidateArgs = { - data: CandidateInsertInput; + data: CandidateInsertType; }; @@ -2967,12 +2836,6 @@ export type MutationPromoteSubmissionToReportArgs = { }; -export type MutationReplaceOneCandidateArgs = { - data: CandidateInsertInput; - query?: InputMaybe; -}; - - export type MutationReplaceOneChecklistArgs = { data: ChecklistInsertInput; query?: InputMaybe; @@ -3008,8 +2871,8 @@ export type MutationReplaceOneUserArgs = { export type MutationUpdateManyCandidatesArgs = { - query?: InputMaybe; - set: CandidateUpdateInput; + filter: CandidateFilterType; + update: CandidateUpdateType; }; @@ -3056,8 +2919,8 @@ export type MutationUpdateManyQuickaddsArgs = { export type MutationUpdateOneCandidateArgs = { - query?: InputMaybe; - set: CandidateUpdateInput; + filter: CandidateFilterType; + update: CandidateUpdateType; }; @@ -3139,8 +3002,8 @@ export type MutationUpdateOneUserArgs = { export type MutationUpsertOneCandidateArgs = { - data: CandidateInsertInput; - query?: InputMaybe; + filter: CandidateFilterType; + update: CandidateInsertType; }; @@ -3388,7 +3251,7 @@ export type Query = { /** Custom scalar for MongoDB ObjectID */ ObjectId?: Maybe; candidate?: Maybe; - candidates: Array>; + candidates?: Maybe>>; checklist?: Maybe; checklists: Array>; classification?: Maybe; @@ -3422,14 +3285,16 @@ export type Query = { export type QueryCandidateArgs = { - query?: InputMaybe; + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; }; export type QueryCandidatesArgs = { - limit?: InputMaybe; - query?: InputMaybe; - sortBy?: InputMaybe; + filter?: InputMaybe; + pagination?: InputMaybe; + sort?: InputMaybe; }; diff --git a/site/gatsby-site/server/local.ts b/site/gatsby-site/server/local.ts index 6333941ecf..fd75b53a29 100644 --- a/site/gatsby-site/server/local.ts +++ b/site/gatsby-site/server/local.ts @@ -50,6 +50,12 @@ import { permissions as taxaPermissions } from './fields/taxa'; +import { + queryFields as candidatesQueryFields, + mutationFields as candidatesMutationFields, + permissions as candidatesPermissions +} from './fields/candidates'; + import { queryFields as subscriptionsQueryFields, mutationFields as subscriptionsMutationFields, @@ -80,6 +86,7 @@ export const getSchema = () => { ...submissionsQueryFields, ...classificationsQueryFields, ...taxaQueryFields, + ...candidatesQueryFields, ...subscriptionsQueryFields, ...duplicatesQueryFields, } @@ -95,6 +102,7 @@ export const getSchema = () => { ...usersMutationFields, ...submissionsMutationFields, ...classificationsMutationFields, + ...candidatesMutationFields, ...subscriptionsMutationFields, ...duplicatesMutationFields, } @@ -132,6 +140,7 @@ export const getSchema = () => { ...submissionsPermissions.Query, ...classificationsPermissions.Query, ...taxaPermissions.Query, + ...candidatesPermissions.Query, ...subscriptionsPermissions.Query, ...duplicatesPermissions.Query, }, @@ -144,6 +153,7 @@ export const getSchema = () => { ...usersPermissions.Mutation, ...submissionsPermissions.Mutation, ...classificationsPermissions.Mutation, + ...candidatesPermissions.Mutation, ...subscriptionsPermissions.Mutation, ...duplicatesPermissions.Mutation, }, diff --git a/site/gatsby-site/server/remote.ts b/site/gatsby-site/server/remote.ts index d0391ad1ff..3c840a7e3f 100644 --- a/site/gatsby-site/server/remote.ts +++ b/site/gatsby-site/server/remote.ts @@ -85,6 +85,10 @@ const ignoreTypes = [ 'TaxaUpdateInput', 'TaxaInsertInput', + 'Candidate', + 'CandidateQueryInput', + 'CandidateUpdateInput', + 'CandidateInsertInput', 'Subscription', 'SubscriptionQueryInput', 'SubscriptionUpdateInput', @@ -120,6 +124,9 @@ const ignoredQueries = [ 'taxa', 'taxas', + + 'candidate', + 'candidates', 'subscription', 'subscriptions', @@ -209,6 +216,16 @@ const ignoredMutations = [ 'updateOneTaxa', 'updateManyTaxas', 'upsertOneTaxa', + 'upsertManyTaxas', + + 'deleteOneCandidates', + 'deleteManyCandidates', + 'insertOneCandidate', + 'insertManyCandidates', + 'updateOneCandidate', + 'updateManyCandidates', + 'upsertOneCandidate', + 'upsertManyCandidates', 'deleteOneSubscription', 'deleteManySubscriptions', diff --git a/site/gatsby-site/src/pages/apps/newsdigest.js b/site/gatsby-site/src/pages/apps/newsdigest.js index 9fa0c6f40d..68cd1922b2 100644 --- a/site/gatsby-site/src/pages/apps/newsdigest.js +++ b/site/gatsby-site/src/pages/apps/newsdigest.js @@ -18,6 +18,7 @@ import { useUserContext } from 'contexts/userContext'; import CardSkeleton from 'elements/Skeletons/Card'; import { useLocalization } from 'plugins/gatsby-theme-i18n'; import useLocalizePath from 'components/i18n/useLocalizePath'; +import useToast, { SEVERITY } from '../../hooks/useToast'; export default function NewsSearchPage() { const { t } = useTranslation(['submit']); @@ -28,8 +29,8 @@ export default function NewsSearchPage() { const { data: newsArticlesData, loading } = useQuery( gql` - query NewsArticles($query: CandidateQueryInput!) { - candidates(query: $query, limit: 9999) { + query NewsArticles($filter: CandidateFilterType!) { + candidates(filter: $filter) { title url similarity @@ -38,22 +39,25 @@ export default function NewsSearchPage() { matching_entities date_published dismissed + text } } `, { variables: { - query: { - match: true, - date_published_in: Array(14) - .fill() - .map((e, i) => - new Date( - new Date().getTime() - 86400000 * i // i days ago - ) - .toISOString() - .slice(0, 10) - ), + filter: { + match: { EQ: true }, + date_published: { + IN: Array(14) + .fill() + .map((e, i) => + new Date( + new Date().getTime() - 86400000 * i // i days ago + ) + .toISOString() + .slice(0, 10) + ), + }, }, }, } @@ -99,8 +103,8 @@ export default function NewsSearchPage() { ); const [updateCandidate] = useMutation(gql` - mutation UpdateCandidate($query: CandidateQueryInput!, $set: CandidateUpdateInput!) { - updateOneCandidate(query: $query, set: $set) { + mutation UpdateCandidate($filter: CandidateFilterType!, $update: CandidateUpdateType!) { + updateOneCandidate(filter: $filter, update: $update) { url } } @@ -273,6 +277,8 @@ function CandidateCard({ dismissed = false, existingSubmissions, }) { + const addToast = useToast(); + const { isRole } = useUserContext(); const localizePath = useLocalizePath(); @@ -345,19 +351,24 @@ function CandidateCard({ {isRole('incident_editor') && (dismissed ? ( { + onClick={async () => { setDismissedArticles((dismissedArticles) => { const updatedValue = { ...dismissedArticles }; updatedValue[newsArticle.url] = false; return updatedValue; }); - updateCandidate({ + await updateCandidate({ variables: { - query: { url: newsArticle.url }, - set: { dismissed: false }, + filter: { url: { EQ: newsArticle.url } }, + update: { set: { dismissed: false } }, }, }); + + addToast({ + message: `Restored article: ${newsArticle.url}`, + severity: SEVERITY.success, + }); }} > ) : ( { + onClick={async () => { setDismissedArticles((dismissedArticles) => { const updatedValue = { ...dismissedArticles }; updatedValue[newsArticle.url] = true; return updatedValue; }); - updateCandidate({ + await updateCandidate({ variables: { - query: { url: newsArticle.url }, - set: { dismissed: true }, + filter: { url: { EQ: newsArticle.url } }, + update: { set: { dismissed: true } }, }, }); + addToast({ + message: `Dismissed article: ${newsArticle.url}`, + severity: SEVERITY.success, + }); }} >