From 9ad3fed9becdf3204f705f4fb0e53fe20f49bab7 Mon Sep 17 00:00:00 2001 From: "Mike P. Sinn" Date: Sun, 8 Sep 2024 20:57:24 -0500 Subject: [PATCH] Add comprehensive treatment and condition search functionality Implemented a new TreatmentList component with sorting and effectiveness score calculations. Added a search box for treatments and conditions to the DFDA page. Updated condition pages to dynamically load treatments and sort conditions by number of treatments. --- .../TreatmentConditionSearchBox.tsx | 97 +++++++++ app/dfda/components/TreatmentList.tsx | 149 +++++++++++++ app/dfda/conditions/[conditionName]/page.tsx | 18 +- app/dfda/conditions/page.tsx | 21 +- app/dfda/dfdaActions.ts | 206 ++++++++++++------ app/dfda/page.tsx | 2 + app/dfda/search/page.tsx | 96 ++++++++ tests/github-issue-agent.test.ts | 4 +- 8 files changed, 511 insertions(+), 82 deletions(-) create mode 100644 app/dfda/components/TreatmentConditionSearchBox.tsx create mode 100644 app/dfda/components/TreatmentList.tsx create mode 100644 app/dfda/search/page.tsx diff --git a/app/dfda/components/TreatmentConditionSearchBox.tsx b/app/dfda/components/TreatmentConditionSearchBox.tsx new file mode 100644 index 0000000..8fced7b --- /dev/null +++ b/app/dfda/components/TreatmentConditionSearchBox.tsx @@ -0,0 +1,97 @@ +'use client' + +import { useState, useEffect, useRef } from 'react' +import { Input } from "@/components/ui/input" +import {Loader2, Search} from "lucide-react" +import Link from 'next/link' +import {searchTreatmentsAndConditions} from "@/app/dfda/dfdaActions"; + +type SearchResult = { + id: number + name: string + type: 'treatment' | 'condition' +} + +export default function TreatmentConditionSearchBox() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const [showDropdown, setShowDropdown] = useState(false) + const dropdownRef = useRef(null) + + useEffect(() => { + const handleSearch = async () => { + if (query.length > 2) { + setIsLoading(true) + const searchResults = await searchTreatmentsAndConditions(query) + setResults(searchResults.map(result => ({ + ...result, + type: result.type as 'treatment' | 'condition' + }))) + setIsLoading(false) + setShowDropdown(true) + } else { + setResults([]) + setShowDropdown(false) + } + } + + const debounce = setTimeout(() => { + handleSearch() + }, 300) + + return () => clearTimeout(debounce) + }, [query]) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowDropdown(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + return ( +
+
+ setQuery(e.target.value)} + /> + {isLoading ? ( + + ) : ( + + )} +
+ {showDropdown && ( +
+ {results.length > 0 ? ( +
    + {results.map((result) => ( +
  • + + {result.name} ({result.type}) + +
  • + ))} +
+ ) : ( +
No results found
+ )} +
+ )} +
+ ) +} \ No newline at end of file diff --git a/app/dfda/components/TreatmentList.tsx b/app/dfda/components/TreatmentList.tsx new file mode 100644 index 0000000..d9b5f23 --- /dev/null +++ b/app/dfda/components/TreatmentList.tsx @@ -0,0 +1,149 @@ +'use client' + +import { useState } from 'react' +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select" +import { Card, CardContent } from "@/components/ui/card" +import Image from 'next/image' +import Link from 'next/link' +import { DfdaCondition, DfdaConditionTreatment } from "@prisma/client"; +import { Badge } from "@/components/ui/badge"; + +type TreatmentListProps = { + condition: DfdaCondition & { + conditionTreatments: (DfdaConditionTreatment & { + treatment: { name: string } + })[] + } +}; + +function calculateEffectivenessScore(treatment: { + majorImprovement: number; + moderateImprovement: number; + noEffect: number; + worse: number; + muchWorse: number; +}): number { + const weights = { + majorImprovement: 2, + moderateImprovement: 1, + noEffect: 0, + worse: -1, + muchWorse: -2 + }; + + const totalResponses = + treatment.majorImprovement + + treatment.moderateImprovement + + treatment.noEffect + + treatment.worse + + treatment.muchWorse; + + if (totalResponses === 0) return 0; + + const weightedSum = + weights.majorImprovement * treatment.majorImprovement + + weights.moderateImprovement * treatment.moderateImprovement + + weights.noEffect * treatment.noEffect + + weights.worse * treatment.worse + + weights.muchWorse * treatment.muchWorse; + + // Normalize to a 0-100 scale + return ((weightedSum / totalResponses) + 2) * 25; +} + +function toTitleCase(str: string): string { + return str.replace( + /\w\S*/g, + function(txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + } + ); +} + +export default function TreatmentList({ condition }: TreatmentListProps) { + + const [treatments, setTreatments] = useState(condition.conditionTreatments) + const [sortBy, setSortBy] = useState<'effectiveness' | 'popularity'>('effectiveness') + + const handleSort = (type: 'effectiveness' | 'popularity') => { + setSortBy(type) + const sorted = [...treatments].sort((a, b) => + type === 'popularity' + ? b.popularity - a.popularity + : calculateEffectivenessScore(b) - calculateEffectivenessScore(a) + ) + setTreatments(sorted) + } + + const getConfidence = (popularity: number) => { + if (popularity > 50) return { level: 'HIGH', color: 'bg-green-500' }; + if (popularity > 25) return { level: 'MEDIUM', color: 'bg-yellow-500' }; + return { level: 'LOW', color: 'bg-red-500' }; + } + + return ( + <> +
+

handleSort('popularity')} + > + MOST TRIED +

+

handleSort('effectiveness')} + > + HIGHEST AVERAGE RATING +

+
+ +
+ {treatments.map((treatment, index) => { + const effectivenessScore = calculateEffectivenessScore(treatment); + const confidence = getConfidence(treatment.popularity); + return ( + + +
+ Confidence + + {confidence.level} + +
+
+
#{index + 1}
+ +

{toTitleCase(treatment.treatment.name)}

+ +
+
+ Effectiveness +
+
+
+ {Math.round(effectivenessScore)}% +
+
+
+
+
+ ) + })} +
+ + ) +} \ No newline at end of file diff --git a/app/dfda/conditions/[conditionName]/page.tsx b/app/dfda/conditions/[conditionName]/page.tsx index 92dcc6a..f28e345 100644 --- a/app/dfda/conditions/[conditionName]/page.tsx +++ b/app/dfda/conditions/[conditionName]/page.tsx @@ -1,13 +1,21 @@ -"use client" -import React from 'react' +import {getConditionByName} from "@/app/dfda/dfdaActions"; +import TreatmentList from "@/app/dfda/components/TreatmentList"; +export default async function ConditionPage({ params }: { params: { conditionName: string } }) { + // Decode the conditionName from the URL + const decodedConditionName = decodeURIComponent(params.conditionName); + + const condition = await getConditionByName(decodedConditionName) -export default function ConditionPage() { + if (!condition) { + return
Condition not found
+ } return ( -
- +
+

{condition.name}

+
) } \ No newline at end of file diff --git a/app/dfda/conditions/page.tsx b/app/dfda/conditions/page.tsx index 87828db..f7b1695 100644 --- a/app/dfda/conditions/page.tsx +++ b/app/dfda/conditions/page.tsx @@ -1,17 +1,26 @@ -"use client" import React from 'react' +import Link from 'next/link' import { fetchConditions } from '../dfdaActions' -export default async function ConditionListPage() { +export default async function ConditionListPage() { const conditions = await fetchConditions() + const sortedConditions = conditions.sort((a, b) => b.numberOfTreatments - a.numberOfTreatments) + return (

Conditions

-
    - {conditions.map((condition) => ( -
  • {condition.name}
  • +
    + {sortedConditions.map((condition) => ( + +
    +

    {condition.name}

    + + {condition.numberOfTreatments} Treatments + +
    + ))} -
+
) } \ No newline at end of file diff --git a/app/dfda/dfdaActions.ts b/app/dfda/dfdaActions.ts index 8fd05b8..1f8b45e 100644 --- a/app/dfda/dfdaActions.ts +++ b/app/dfda/dfdaActions.ts @@ -1,84 +1,152 @@ "use server" -import { prisma } from "@/lib/db" + import { Effectiveness } from "@prisma/client" -export async function fetchConditions() { - return prisma.dfdaCondition.findMany(); -} +import { prisma } from "@/lib/db" -export async function fetchTreatments(userId: string, conditionName: string) { - return [] +export async function fetchConditions() { + return prisma.dfdaCondition.findMany() } -export async function addTreatment(userId: string, conditionName: string, treatmentName: string, effectiveness: Effectiveness) { - - const treatment = await prisma.dfdaTreatment.findUnique({ - where: { - name: treatmentName - } - }) - if (!treatment) { - throw new Error("Treatment not found") - } - const condition = await prisma.dfdaCondition.findUnique({ - where: { - name: conditionName - } - }) - - if (!condition) { - throw new Error("Condition not found") - } - - const userTreatment = await prisma.dfdaUserTreatmentReport.create({ - data: { - userId, - conditionId: condition.id, - treatmentId: treatment.id, - effectiveness, - tried: true // Add this line - } - }) - - return userTreatment +export async function addTreatment( + userId: string, + conditionName: string, + treatmentName: string, + effectiveness: Effectiveness +) { + const treatment = await prisma.dfdaTreatment.findUnique({ + where: { + name: treatmentName, + }, + }) + + if (!treatment) { + throw new Error("Treatment not found") + } + + const condition = await prisma.dfdaCondition.findUnique({ + where: { + name: conditionName, + }, + }) + + if (!condition) { + throw new Error("Condition not found") + } + + const userTreatment = await prisma.dfdaUserTreatmentReport.create({ + data: { + userId, + conditionId: condition.id, + treatmentId: treatment.id, + effectiveness, + tried: true, // Add this line + }, + }) + + return userTreatment } -export async function updateTreatmentReport(userId: string, conditionName: string, treatmentName: string, effectiveness: Effectiveness) { - - const treatment = await prisma.dfdaTreatment.findUnique({ - where: { - name: treatmentName - } - }) - - if (!treatment) { - throw new Error("Treatment not found") - } - - const condition = await prisma.dfdaCondition.findUnique({ - where: { - name: conditionName - } - }) +export async function updateTreatmentReport( + userId: string, + conditionName: string, + treatmentName: string, + effectiveness: Effectiveness +) { + const treatment = await prisma.dfdaTreatment.findUnique({ + where: { + name: treatmentName, + }, + }) + + if (!treatment) { + throw new Error("Treatment not found") + } + + const condition = await prisma.dfdaCondition.findUnique({ + where: { + name: conditionName, + }, + }) + + if (!condition) { + throw new Error("Condition not found") + } + + const userTreatment = await prisma.dfdaUserTreatmentReport.update({ + where: { + userId_treatmentId_conditionId: { + userId, + conditionId: condition.id, + treatmentId: treatment.id, + }, + }, + data: { + effectiveness, + }, + }) + + return userTreatment +} - if (!condition) { - throw new Error("Condition not found") - } +export async function searchTreatmentsAndConditions(query: string) { + const treatments = await prisma.dfdaTreatment.findMany({ + where: { + name: { + contains: query, + mode: "insensitive", + }, + }, + select: { + id: true, + name: true, + featuredImage: true, + }, + take: 5, + }) + + const conditions = await prisma.dfdaCondition.findMany({ + where: { + name: { + contains: query, + mode: "insensitive", + }, + }, + select: { + id: true, + name: true, + }, + take: 5, + }) + + return [ + ...treatments.map((t) => ({ ...t, type: "treatment" })), + ...conditions.map((c) => ({ ...c, type: "condition" })), + ] +} - const userTreatment = await prisma.dfdaUserTreatmentReport.update({ +export async function getConditionByName(name: string) { + return prisma.dfdaCondition.findFirst({ where: { - userId_treatmentId_conditionId: { - userId, - conditionId: condition.id, - treatmentId: treatment.id + name: { + equals: name, + mode: 'insensitive' } }, - data: { - effectiveness - } - }) - - return userTreatment -} + include: { + conditionTreatments: { + where: { + popularity: { + gt: 10 + } + }, + include: { + treatment: true, + }, + orderBy: [{popularity: "desc"}, {averageEffect: "desc"}], + }, + }, + }); +} \ No newline at end of file diff --git a/app/dfda/page.tsx b/app/dfda/page.tsx index f96b970..c414912 100644 --- a/app/dfda/page.tsx +++ b/app/dfda/page.tsx @@ -4,10 +4,12 @@ import FindTreatments from './components/FindTreatments' import HealthTrackingLinkBoxes from './components/HealthTrackingLinkBoxes' import CostBenefitAnalysis from './components/CostBenefitAnalysis' import TopTreatments from './components/TopTreatments' +import TreatmentConditionSearchBox from "@/app/dfda/components/TreatmentConditionSearchBox"; export default function DFDAPage() { return (
+ diff --git a/app/dfda/search/page.tsx b/app/dfda/search/page.tsx new file mode 100644 index 0000000..4eb69e1 --- /dev/null +++ b/app/dfda/search/page.tsx @@ -0,0 +1,96 @@ +'use client' + +import { useState, useEffect } from 'react' +import { X } from "lucide-react" +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { searchTreatmentsAndConditions } from '../dfdaActions' + +type SearchResult = { + id: number + name: string + type: 'treatment' | 'condition' +} + +export default function TreatmentConditionSearchBox() { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + + useEffect(() => { + const handleSearch = async () => { + if (query.length > 2) { + setIsLoading(true) + const searchResults = await searchTreatmentsAndConditions(query) + setResults(searchResults as SearchResult[]) + setIsLoading(false) + } else { + setResults([]) + } + } + + const debounce = setTimeout(() => { + handleSearch() + }, 300) + + return () => clearTimeout(debounce) + }, [query]) + + const handleClear = () => { + setQuery('') + setResults([]) + } + + const handleClose = () => { + router.back() + } + + return ( +
+
+ +
+
+
+ setQuery(e.target.value)} + /> + {query && ( + + )} +
+ {isLoading ? ( +
Loading...
+ ) : ( +
    + {results.map((result) => ( +
  • + +

    {result.name}

    +

    {result.type}

    + +
  • + ))} +
+ )} +
+
+ ) +} \ No newline at end of file diff --git a/tests/github-issue-agent.test.ts b/tests/github-issue-agent.test.ts index 4859d86..833b259 100644 --- a/tests/github-issue-agent.test.ts +++ b/tests/github-issue-agent.test.ts @@ -57,9 +57,9 @@ describe("IssueManager tests", () => { // Mock the Octokit issues.create method const mockCreate = jest.fn().mockResolvedValue({ data: { html_url: 'https://github.com/test-owner/test-repo/issues/1' } }); - (Octokit as jest.Mock).mockImplementation(() => ({ + (Octokit as jest.MockedClass).mockImplementation(() => ({ issues: { create: mockCreate } - })); + }) as unknown as InstanceType); await saveIssuesToGitHub(mockIssues, repoOwner, repoName, githubToken);