From 582a0fcdf8bdaf2bad9d615a8fdad9caf4ebe1e0 Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Sat, 14 Sep 2024 14:36:44 -0500 Subject: [PATCH] Add new functions for searching clinical trials and FDA treatments Introduced functions to search clinical trials and treatments using various APIs. Modified functions to include URL error handling, and improved user ID handling in researcher agent functions. --- components/ArticleGrid.tsx | 40 +++++++------- components/ArticleRenderer.tsx | 84 +++++++++++++++++++++++------ components/SidebarMobile.tsx | 1 - components/icons.tsx | 3 ++ config/links.ts | 20 +++++++ lib/agents/researcher/researcher.ts | 12 +++-- lib/clinicaltables.ts | 84 ++++++++++++++++++++++++++++- tests/dfda.test.ts | 21 ++++++-- 8 files changed, 220 insertions(+), 45 deletions(-) diff --git a/components/ArticleGrid.tsx b/components/ArticleGrid.tsx index 756778bf..733c65d3 100644 --- a/components/ArticleGrid.tsx +++ b/components/ArticleGrid.tsx @@ -25,39 +25,41 @@ type ArticleGridProps = { export default function ArticleGrid({ articles }: ArticleGridProps) { return ( -
+
{articles.map((article) => ( - + {article.featuredImage && ( - - {article.title} + +
+ {article.title} +
)} - - - + + {article.category.name} + + + {article.title} - - {article.category.name} - -

{article.description}

+

{article.description}

- +
{article.tags.map((tag) => ( - {tag.name} + {tag.name} ))}
diff --git a/components/ArticleRenderer.tsx b/components/ArticleRenderer.tsx index 91aa3e5e..ba119209 100644 --- a/components/ArticleRenderer.tsx +++ b/components/ArticleRenderer.tsx @@ -16,8 +16,11 @@ import { import { Separator } from "@/components/ui/separator" import { toast } from "@/components/ui/use-toast" import { CustomReactMarkdown } from "@/components/CustomReactMarkdown" +import { deleteArticle } from "@/app/researcher/researcherActions" +import { useRouter } from "next/navigation" import {generateImage} from "@/app/researcher/researcherActions"; +import { UrlDisplay } from "./article/UrlDisplay" function GenerateImageButton({ onClick, @@ -40,7 +43,14 @@ function GenerateImageButton({ ) } -export default function ArticleRenderer(article: ArticleWithRelations) { +export default function ArticleRenderer({ + article, + currentUserId +}: { + article: ArticleWithRelations, + currentUserId?: string +}) { + const router = useRouter(); const [expandedResult, setExpandedResult] = useState(null) const [isCopied, setIsCopied] = useState(false) const [isGeneratingImage, setIsGeneratingImage] = useState(false) @@ -56,6 +66,7 @@ export default function ArticleRenderer(article: ArticleWithRelations) { category, searchResults, featuredImage, + userId, } = article const readingTime = Math.ceil(content.split(' ').length / 200) @@ -112,6 +123,35 @@ ${sources?.map((source) => `- [${source.title}](${source.url})`).join("\n")} }) } + // Add this function to handle article deletion + async function handleDeleteArticle() { + if (!currentUserId) { + toast({ + title: "Please login to delete articles", + description: "You must be logged in to delete articles.", + variant: "destructive", + }); + return; + } + if (confirm("Are you sure you want to delete this article?")) { + try { + await deleteArticle(title, currentUserId); + toast({ + title: "Article deleted", + description: "The article has been successfully deleted.", + }); + router.push("/"); // Redirect to home page after deletion + } catch (error) { + console.error("Failed to delete article:", error); + toast({ + title: "Failed to delete article", + description: "An error occurred while deleting the article.", + variant: "destructive", + }); + } + } + } + return (
@@ -124,6 +164,13 @@ ${sources?.map((source) => `- [${source.title}](${source.url})`).join("\n")} {content} + {currentUserId === userId && ( + + + + )}
@@ -209,21 +256,25 @@ ${sources?.map((source) => `- [${source.title}](${source.url})`).join("\n")} Sources - +
@@ -241,11 +292,12 @@ ${sources?.map((source) => `- [${source.title}](${source.url})`).join("\n")} href={result.url} target="_blank" rel="noopener noreferrer" - className="text-blue-500 hover:underline" + className="text-blue-500" > {result.title} + {result.publishedDate && (

Published on:{" "} @@ -263,7 +315,7 @@ ${sources?.map((source) => `- [${source.title}](${source.url})`).join("\n")} expandedResult === result.id ? null : result.id ) } - className="ml-2 text-blue-500 hover:underline" + className="ml-2" > {expandedResult === result.id ? "Show less" : "Show more"} diff --git a/components/SidebarMobile.tsx b/components/SidebarMobile.tsx index 5de20bff..d839e00f 100644 --- a/components/SidebarMobile.tsx +++ b/components/SidebarMobile.tsx @@ -4,7 +4,6 @@ import { SidebarSimple } from "@phosphor-icons/react" import { useTheme } from "next-themes" import Sidebar from "./Sidebar" -import { Button } from "./ui/button" import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet" interface SidebarMobileProps { diff --git a/components/icons.tsx b/components/icons.tsx index 40385924..97e9e536 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -44,6 +44,7 @@ import { import { FaHand, FaMessage, + FaPencil, FaRankingStar, FaSquarePollVertical, } from "react-icons/fa6" @@ -122,6 +123,8 @@ const icons = { edit: FaEdit, eye: FaEye, eyeOff: FaEyeSlash, + book: FaBook, + pencil: FaPencil, } export const Icons: IconsType = icons diff --git a/config/links.ts b/config/links.ts index c33337b3..113ad0f2 100644 --- a/config/links.ts +++ b/config/links.ts @@ -76,6 +76,20 @@ export const githubLink: NavItem = { external: true, } +export const researcherLink: NavItem = { + title: "Researcher", + href: "/researcher", + icon: "pencil", + tooltip: "Your very own AI researcher", +} + +export const dfdaLink: NavItem = { + title: "DFDA", + href: "/dfda", + icon: "health", + tooltip: "The Decentralized Food and Drug Administration", +} + export const profileSettingsLink: NavItem = { title: "Profile Settings", href: "/dashboard/settings", @@ -134,6 +148,8 @@ export const avatarNav: Navigation = { //wishingWellsLink, //globalProblemsVoteLink, //createWish + researcherLink, + dfdaLink, ], } @@ -152,6 +168,8 @@ export const generalSidebarNav: Navigation = { //globalSolutionsResultsLink, docsLink, githubLink, + researcherLink, + dfdaLink, ], } @@ -163,5 +181,7 @@ export const generalFooterNav: Navigation = { globalProblemsResultsLink, docsLink, githubLink, + researcherLink, + dfdaLink, ], } diff --git a/lib/agents/researcher/researcher.ts b/lib/agents/researcher/researcher.ts index 5bb54edf..8bee7a32 100644 --- a/lib/agents/researcher/researcher.ts +++ b/lib/agents/researcher/researcher.ts @@ -59,7 +59,7 @@ function getModel(modelName: string): LanguageModelV1 { export async function writeArticle( topic: string, - userId: string = 'test-user', + userId: string, options: { numberOfSearchQueryVariations?: number, numberOfWebResultsToInclude?: number, @@ -120,7 +120,9 @@ export async function writeArticle( if (citationStyle === 'footnote') { citationInstructions = 'Provide citations in the text using markdown footnote notation like [^1].'; } else if (citationStyle === 'hyperlinked-text') { - citationInstructions = 'Hyperlink the relevant text in the report to the source URLs used using markdown hyperlink notation like [text](https://link-where-you-got-the-information).'; + citationInstructions = `'YOU MUST HYPERLINK THE RELEVANT text in the report to the source + URLs used using markdown hyperlink notation + like [text](https://link-where-you-got-the-information).';` } const prompt = ` @@ -331,7 +333,7 @@ export async function findOrCreateArticleByPromptedTopic(promptedTopic: string, return article; } -export async function deleteArticleByPromptedTopic(promptedTopic: string, userId: string = 'test-user'): Promise { +export async function deleteArticleByPromptedTopic(promptedTopic: string, userId: string): Promise { // Find the article(s) to delete const articlesToDelete = await prisma.article.findMany({ where: { @@ -355,12 +357,12 @@ export async function deleteArticleByPromptedTopic(promptedTopic: string, userId }); } -export async function findArticleByTopic(promptedTopic: string, userId: string = 'test-user'): +export async function findArticleByTopic(promptedTopic: string, userId?: string): Promise { return prisma.article.findFirst({ where: { promptedTopic: promptedTopic, - userId: userId + ...(userId ? { userId } : {}) }, include: { user: true, diff --git a/lib/clinicaltables.ts b/lib/clinicaltables.ts index dde66efd..86279e97 100644 --- a/lib/clinicaltables.ts +++ b/lib/clinicaltables.ts @@ -1,6 +1,88 @@ +import { GlobalVariable } from "@/types/models/GlobalVariable"; + export async function searchConditions(condition: string): Promise { const response = await fetch(`https://clinicaltables.nlm.nih.gov/api/conditions/v3/search?terms=${condition}&ef=name`); const data = await response.json(); // Extract the suggestions from the fourth element of the array return data[3].map((item: string[]) => item[0]); -} \ No newline at end of file +} + +export async function searchClinicalTrialsGovInterventions(intervention: string): Promise { + // Find matching interventions with the clinicaltrials.gov api + // Example: https://clinicaltrials.gov/api/query/study_fields?expr=INTERVENTION.INTERVENTION_NAME:*aspirin*&fields=NCTId,InterventionName&fmt=json + const response = await fetch(`https://clinicaltrials.gov/api/query/study_fields?expr=INTERVENTION.INTERVENTION_NAME:*${intervention}*&fields=NCTId,InterventionName&fmt=json`); + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}, body: ${errorBody}`); + } + const data = await response.json(); + return data.StudyFieldsResponse.StudyFields.map((item: any) => item.InterventionName); +} + +export async function searchFdaTreatments(treatment: string): Promise { + try { + const encodedTreatment = encodeURIComponent(treatment); + const response = await fetch(`https://api.fda.gov/drug/label.json?search=indications_and_usage:"${encodedTreatment}"&limit=10`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + if (!data.results || !Array.isArray(data.results)) { + return []; + } + + return data.results + .filter((item: any) => item.openfda && item.openfda.brand_name) + .map((item: any) => item.openfda.brand_name[0]); + } catch (error) { + console.error("Error in searchFdaTreatments:", error); + return []; + } +} + +export async function searchDfdaTreatments(treatment: string): Promise { + try { + const baseUrl = 'https://safe.fdai.earth/api/v3/variables'; + const params = new URLSearchParams({ + appName: 'Wishonia', + clientId: 'oauth_test_client', + limit: '10', + includePublic: 'true', + variableCategoryName: 'Treatments', + searchPhrase: treatment, + }); + + const url = `${baseUrl}?${params.toString()}`; + console.log(`Fetching from URL: ${url}`); + + const response = await fetch(url); + + if (!response.ok) { + const errorBody = await response.text(); + throw new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}, body: ${errorBody}`); + } + + const data = await response.json(); + + if (!Array.isArray(data)) { + console.error('Unexpected response structure:', JSON.stringify(data, null, 2)); + throw new Error("Unexpected response format: 'data' field is missing or not an array"); + } + + const treatments = data.map((item: GlobalVariable) => item.name); + + console.log(`Found ${treatments.length} treatments`); + return treatments; + } catch (error) { + console.error("Error in searchTreatments:", error); + if (error instanceof Error) { + console.error("Error message:", error.message); + console.error("Error stack:", error.stack); + } + return []; + } +} + diff --git a/tests/dfda.test.ts b/tests/dfda.test.ts index eb8df44c..9a0fb5fc 100644 --- a/tests/dfda.test.ts +++ b/tests/dfda.test.ts @@ -4,11 +4,26 @@ import { PrismaClient } from '@prisma/client' import fs from 'fs' import path from 'path' +import {getMetaAnalysis} from "@/app/dfda/dfdaActions"; +import {searchClinicalTrialsGovInterventions, searchFdaTreatments} from "@/lib/clinicaltables"; -describe("Database-seeder tests", () => { +describe("dFDA tests", () => { jest.setTimeout(6000000) + it("Searches for searchClinicalTrialsGovInterventions", async () => { + const treatmentMatches = await searchClinicalTrialsGovInterventions('Exercise'); + expect(treatmentMatches).toBeDefined() + }); + it("searchFdaTreatments for treatments", async () => { + const treatmentMatches = await searchFdaTreatments('Exercise'); + expect(treatmentMatches).toBeDefined() + }); + + it("Gets a meta analysis article", async () => { + const article = await getMetaAnalysis('Exercise', 'Depression') + expect(article).toBeDefined() + }); - it("Deletes dates and variable id from JSON file", async () => { + it.skip("Deletes dates and variable id from JSON file", async () => { // delete the updated_at, created_at, deleted_at, and variable_id fields from all // the objects inn the array in the json files // in the prisma folder that start with ct_ and save them to a new file @@ -62,7 +77,7 @@ describe("Database-seeder tests", () => { }) } - it("Imports DfdaCause from JSON file", async () => { + it.skip("Imports DfdaCause from JSON file", async () => { await importJsonToPrisma('dfdaCause') await importJsonToPrisma('dfdaCondition') await importJsonToPrisma('dfdaSymptom')