From 9a01c15a76d6a3b111c01cda71caf3344474fe96 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Thu, 23 Oct 2025 16:48:27 +0300 Subject: [PATCH] change recommendations to update through doctor --- app/home/(user)/(dashboard)/page.tsx | 5 +- app/home/(user)/_components/ai/ai-blocks.tsx | 22 +-- .../(user)/_components/ai/life-style-card.tsx | 11 +- .../(user)/_components/ai/recommendations.tsx | 2 +- app/home/(user)/_lib/server/ai-actions.ts | 130 ++++++++++++++++-- .../(user)/_lib/server/load-life-style.ts | 15 +- .../_lib/server/load-recommendations.ts | 125 ++--------------- ...3163400_fix_analysis_response_function.sql | 42 ++++++ 8 files changed, 205 insertions(+), 147 deletions(-) create mode 100644 supabase/migrations/20251023163400_fix_analysis_response_function.sql diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index 49db975..b754b22 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -29,7 +29,10 @@ async function UserHomePage() { const { account } = await loadCurrentUserAccount(); const api = createUserAnalysesApi(client); + const userAnalysesApi = createUserAnalysesApi(client); + const bmiThresholds = await api.fetchBmiThresholds(); + const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); if (!account) { redirect('/'); @@ -54,7 +57,7 @@ async function UserHomePage() {
- +
diff --git a/app/home/(user)/_components/ai/ai-blocks.tsx b/app/home/(user)/_components/ai/ai-blocks.tsx index 7c44026..8c8a954 100644 --- a/app/home/(user)/_components/ai/ai-blocks.tsx +++ b/app/home/(user)/_components/ai/ai-blocks.tsx @@ -5,27 +5,33 @@ import React, { Suspense } from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; import { isValidOpenAiEnv } from '../../_lib/server/is-valid-open-ai-env'; -import { loadAnalyses } from '../../_lib/server/load-analyses'; import LifeStyleCard from './life-style-card'; import OrderAnalysesPackageCard from './order-analyses-package-card'; import Recommendations from './recommendations'; import RecommendationsSkeleton from './recommendations-skeleton'; +import { AnalysisResponses } from './types'; -const AIBlocks = async ({ account }: { account: AccountWithParams }) => { +const AIBlocks = async ({ + account, + analysisResponses, +}: { + account: AccountWithParams; + analysisResponses?: AnalysisResponses; +}) => { const isOpenAiAvailable = await isValidOpenAiEnv(); if (!isOpenAiAvailable) { return ; } - - const { analyses } = await loadAnalyses(); - - if (analyses.length === 0) { + if (analysisResponses?.length === 0) { return ( <> }> - + ); @@ -33,7 +39,7 @@ const AIBlocks = async ({ account }: { account: AccountWithParams }) => { return ( }> - + ); diff --git a/app/home/(user)/_components/ai/life-style-card.tsx b/app/home/(user)/_components/ai/life-style-card.tsx index 14da00d..5ca0aed 100644 --- a/app/home/(user)/_components/ai/life-style-card.tsx +++ b/app/home/(user)/_components/ai/life-style-card.tsx @@ -13,9 +13,16 @@ import { Button } from '@kit/ui/shadcn/button'; import { Card, CardHeader } from '@kit/ui/shadcn/card'; import { loadLifeStyle } from '../../_lib/server/load-life-style'; +import { AnalysisResponses } from './types'; -const LifeStyleCard = async ({ account }: { account: AccountWithParams }) => { - const data = await loadLifeStyle(account); +const LifeStyleCard = async ({ + account, + analysisResponses, +}: { + account: AccountWithParams; + analysisResponses?: AnalysisResponses; +}) => { + const data = await loadLifeStyle(account, analysisResponses); return ( diff --git a/app/home/(user)/_components/ai/recommendations.tsx b/app/home/(user)/_components/ai/recommendations.tsx index b8d2722..7208d36 100644 --- a/app/home/(user)/_components/ai/recommendations.tsx +++ b/app/home/(user)/_components/ai/recommendations.tsx @@ -15,7 +15,7 @@ export default async function Recommendations({ }) { const { analyses, countryCode } = await loadAnalyses(); - const analysisRecommendations = await loadRecommendations(analyses, account); + const analysisRecommendations = await loadRecommendations(account); const orderAnalyses = analyses.filter((analysis) => analysisRecommendations.includes(analysis.title), ); diff --git a/app/home/(user)/_lib/server/ai-actions.ts b/app/home/(user)/_lib/server/ai-actions.ts index 5031e50..d014619 100644 --- a/app/home/(user)/_lib/server/ai-actions.ts +++ b/app/home/(user)/_lib/server/ai-actions.ts @@ -1,7 +1,6 @@ 'use server'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import OpenAI from 'openai'; @@ -12,8 +11,9 @@ import { ILifeStyleResponse, PROMPT_NAME, } from '../../_components/ai/types'; +import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; -async function getLatestResponseTime(items: AnalysisResponses) { +async function getLatestResponseTime(items?: AnalysisResponses) { if (!items?.length) return null; let latest = null; @@ -29,8 +29,10 @@ async function getLatestResponseTime(items: AnalysisResponses) { export async function updateLifeStyle({ account, + analysisResponses, }: { account: AccountWithParams; + analysisResponses?: AnalysisResponses; }): Promise { const LIFE_STYLE_PROMPT_ID = process.env.PROMPT_ID_LIFE_STYLE; if (!LIFE_STYLE_PROMPT_ID || !account?.personal_code) { @@ -42,13 +44,26 @@ export async function updateLifeStyle({ const openAIClient = new OpenAI(); const supabaseClient = getSupabaseServerClient(); - const userAnalysesApi = createUserAnalysesApi(supabaseClient); - - const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); const weight = account.accountParams?.weight || 'unknown'; const height = account.accountParams?.height || 'unknown'; const isSmoker = !!account.accountParams?.isSmoker; + const cholesterol = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'Kolesterool') + ?.response_value.toString() || 'unknown'; + const ldl = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'LDL kolesterool') + ?.response_value.toString() || 'unknown'; + const hdl = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'HDL kolesterool') + ?.response_value.toString() || 'unknown'; + const vitamind = + analysisResponses + ?.find((ar) => ar.analysis_name_lab === 'Vitamiin D (25-OH)') + ?.response_value.toString() || 'unknown'; const latestResponseTime = await getLatestResponseTime(analysisResponses); const latestISO = latestResponseTime @@ -65,10 +80,10 @@ export async function updateLifeStyle({ age: age.toString(), weight: weight.toString(), height: height.toString(), - cholesterol: '', - ldl: '', - hdl: '', - vitamind: '', + cholesterol, + ldl, + hdl, + vitamind, is_smoker: isSmoker.toString(), }, }, @@ -85,10 +100,10 @@ export async function updateLifeStyle({ gender: gender.value, age: age.toString(), weight: weight.toString(), - cholesterol: '', - ldl: '', - hdl: '', - vitamind: '', + cholesterol, + ldl, + hdl, + vitamind, is_smoker: isSmoker.toString(), }), latest_data_change: latestISO, @@ -106,3 +121,92 @@ export async function updateLifeStyle({ }; } } + +export async function updateRecommendations({ + analyses, + analysisResponses, + account, +}: { + analyses: OrderAnalysisCard[]; + analysisResponses?: AnalysisResponses; + account: AccountWithParams; +}) { + const RECOMMENDATIONS_PROMPT_IT = + process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS; + + if (!RECOMMENDATIONS_PROMPT_IT || !account?.personal_code) { + console.error('No prompt ID for analysis recommendations'); + return []; + } + + const openAIClient = new OpenAI(); + const supabaseClient = getSupabaseServerClient(); + + const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); + const weight = account.accountParams?.weight || 'unknown'; + + const formattedAnalysisResponses = analysisResponses?.map( + ({ + analysis_name_lab, + response_value, + norm_upper, + norm_lower, + norm_status, + }) => ({ + name: analysis_name_lab, + value: response_value, + normUpper: norm_upper, + normLower: norm_lower, + normStatus: norm_status, + }), + ); + const formattedAnalyses = analyses.map(({ description, title }) => ({ + description, + title, + })); + const latestResponseTime = await getLatestResponseTime(analysisResponses); + const latestISO = latestResponseTime + ? new Date(latestResponseTime).toISOString() + : new Date('2025').toISOString(); + + try { + const response = await openAIClient.responses.create({ + store: false, + prompt: { + id: RECOMMENDATIONS_PROMPT_IT, + variables: { + analyses: JSON.stringify(formattedAnalyses), + results: JSON.stringify(formattedAnalysisResponses), + gender: gender.value, + age: age.toString(), + weight: weight.toString(), + }, + }, + }); + + await supabaseClient + .schema('medreport') + .from('ai_responses') + .insert({ + account_id: account.id, + prompt_name: PROMPT_NAME.ANALYSIS_RECOMMENDATIONS, + prompt_id: RECOMMENDATIONS_PROMPT_IT, + input: JSON.stringify({ + analyses: formattedAnalyses, + results: formattedAnalysisResponses, + gender, + age, + weight, + }), + latest_data_change: latestISO, + response: response.output_text, + }); + + const json = JSON.parse(response.output_text); + + return json.recommended; + } catch (error) { + console.error('Error getting recommendations: ', error); + return []; + } +} diff --git a/app/home/(user)/_lib/server/load-life-style.ts b/app/home/(user)/_lib/server/load-life-style.ts index 889bddd..95dd06f 100644 --- a/app/home/(user)/_lib/server/load-life-style.ts +++ b/app/home/(user)/_lib/server/load-life-style.ts @@ -3,7 +3,11 @@ import { cache } from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; -import { ILifeStyleResponse, PROMPT_NAME } from '../../_components/ai/types'; +import { + AnalysisResponses, + ILifeStyleResponse, + PROMPT_NAME, +} from '../../_components/ai/types'; import { updateLifeStyle } from './ai-actions'; const failedResponse = { @@ -13,17 +17,12 @@ const failedResponse = { async function lifeStyleLoader( account: AccountWithParams, + analysisResponses?: AnalysisResponses, ): Promise { if (!account?.personal_code) { return failedResponse; } - const lifeStylePromptId = process.env.PROMPT_ID_LIFE_STYLE; - - if (!lifeStylePromptId) { - return failedResponse; - } - const supabaseClient = getSupabaseServerClient(); const { data, error } = await supabaseClient .schema('medreport') @@ -43,7 +42,7 @@ async function lifeStyleLoader( if (data?.response) { return JSON.parse(data.response as string); } else { - return await updateLifeStyle({ account }); + return await updateLifeStyle({ account, analysisResponses }); } } export const loadLifeStyle = cache(lifeStyleLoader); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index 4c89ae3..a1ae213 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -1,141 +1,38 @@ import { cache } from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; -import { Database } from '@/packages/supabase/src/database.types'; -import OpenAI from 'openai'; - -import PersonalCode from '~/lib/utils'; import { PROMPT_NAME } from '../../_components/ai/types'; -import { OrderAnalysisCard } from '../../_components/order-analyses-cards'; export const loadRecommendations = cache(recommendationsLoader); -type AnalysisResponses = - Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']; - -const getLatestResponseTime = (items: AnalysisResponses) => { - if (!items?.length) return null; - - let latest = null; - for (const it of items) { - const d = new Date(it.response_time); - const t = d.getTime(); - if (!Number.isNaN(t) && (latest === null || t > latest.getTime())) { - latest = d; - } - } - return latest; -}; - async function recommendationsLoader( - analyses: OrderAnalysisCard[], account: AccountWithParams | null, ): Promise { - if (!process.env.OPENAI_API_KEY) { - return []; - } if (!account?.personal_code) { return []; } const supabaseClient = getSupabaseServerClient(); - const userAnalysesApi = createUserAnalysesApi(supabaseClient); - const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); - const analysesRecommendationsPromptId = - process.env.PROMPT_ID_ANALYSIS_RECOMMENDATIONS; - const latestResponseTime = getLatestResponseTime(analysisResponses); - const latestISO = latestResponseTime - ? new Date(latestResponseTime).toISOString() - : new Date('2025').toISOString(); - if (!analysesRecommendationsPromptId) { - console.error('No prompt ID for analysis recommendations'); - return []; - } - - const previouslyRecommended = await supabaseClient + const { data, error } = await supabaseClient .schema('medreport') .from('ai_responses') .select('*') .eq('account_id', account.id) - .eq('prompt_id', analysesRecommendationsPromptId) - .eq('latest_data_change', latestISO); + .eq('prompt_name', PROMPT_NAME.ANALYSIS_RECOMMENDATIONS) + .order('latest_data_change', { ascending: false, nullsFirst: false }) + .limit(1) + .maybeSingle(); - if (previouslyRecommended.data?.[0]?.response) { - return JSON.parse(previouslyRecommended.data[0].response as string) - .recommended; - } - - const openAIClient = new OpenAI(); - const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); - const weight = account.accountParams?.weight || 'unknown'; - - const formattedAnalysisResponses = analysisResponses.map( - ({ - analysis_name_lab, - response_value, - norm_upper, - norm_lower, - norm_status, - }) => ({ - name: analysis_name_lab, - value: response_value, - normUpper: norm_upper, - normLower: norm_lower, - normStatus: norm_status, - }), - ); - const formattedAnalyses = analyses.map(({ description, title }) => ({ - description, - title, - })); - - let response; - - try { - response = await openAIClient.responses.create({ - store: false, - prompt: { - id: analysesRecommendationsPromptId, - variables: { - analyses: JSON.stringify(formattedAnalyses), - results: JSON.stringify(formattedAnalysisResponses), - gender: gender.value, - age: age.toString(), - weight: weight.toString(), - }, - }, - }); - } catch (error) { - console.error('Error calling OpenAI: ', error); + if (error) { + console.error('Error fetching AI response from DB: ', error); return []; } - const json = JSON.parse(response.output_text); - - try { - await supabaseClient - .schema('medreport') - .from('ai_responses') - .insert({ - account_id: account.id, - prompt_name: PROMPT_NAME.ANALYSIS_RECOMMENDATIONS, - prompt_id: analysesRecommendationsPromptId, - input: JSON.stringify({ - analyses: formattedAnalyses, - results: formattedAnalysisResponses, - gender, - age, - weight, - }), - latest_data_change: latestISO, - response: response.output_text, - }); - } catch (error) { - console.error('Error saving AI response: ', error); + if (data?.response) { + return JSON.parse(data.response as string).recommended; + } else { + return []; } - - return json.recommended; } diff --git a/supabase/migrations/20251023163400_fix_analysis_response_function.sql b/supabase/migrations/20251023163400_fix_analysis_response_function.sql new file mode 100644 index 0000000..d614c3b --- /dev/null +++ b/supabase/migrations/20251023163400_fix_analysis_response_function.sql @@ -0,0 +1,42 @@ +drop function if exists medreport.get_latest_analysis_response_elements_for_current_user(uuid); + +create or replace function medreport.get_latest_analysis_response_elements_for_current_user(p_user_id uuid) +returns table ( + analysis_name medreport.analysis_response_elements.analysis_name%type, + response_time medreport.analysis_response_elements.response_time%type, + norm_upper medreport.analysis_response_elements.norm_upper%type, + norm_lower medreport.analysis_response_elements.norm_lower%type, + norm_status medreport.analysis_response_elements.norm_status%type, + response_value medreport.analysis_response_elements.response_value%type, + analysis_name_lab medreport.analysis_elements.analysis_name_lab%type +) +language sql +as $$ + WITH ranked AS ( + SELECT + are.analysis_name, + are.response_time, + are.norm_upper, + are.norm_lower, + are.norm_status, + are.response_value, + ae.analysis_name_lab, + ROW_NUMBER() OVER ( + PARTITION BY are.analysis_name + ORDER BY are.response_time DESC, are.id DESC + ) AS rn + FROM medreport.analysis_responses ar + JOIN medreport.analysis_response_elements are + ON are.analysis_response_id = ar.id + JOIN medreport.analysis_elements ae + ON are.analysis_element_original_id = ae.analysis_id_original + WHERE ar.user_id = auth.uid() + AND ar.order_status IN ('COMPLETED', 'ON_HOLD') + ) + SELECT analysis_name, response_time, norm_upper, norm_lower, norm_status, response_value, analysis_name_lab + FROM ranked + WHERE rn = 1 + ORDER BY analysis_name; +$$; + +grant execute on function medreport.get_latest_analysis_response_elements_for_current_user(uuid) to authenticated, service_role;