From 6dcc91a20654faa60fdb67997fc705d2cbe1bf38 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 21 Oct 2025 09:36:29 +0300 Subject: [PATCH 1/6] WIP: add lifestyle block --- .../(dashboard)/order-analysis/page.tsx | 4 +- app/home/(user)/(dashboard)/page.tsx | 23 ++-- app/home/(user)/_components/ai/ai-blocks.tsx | 42 ++++++ .../(user)/_components/ai/life-style-card.tsx | 19 +++ .../ai/order-analyses-package-card.tsx | 51 +++++++ .../{ => ai}/recommendations-skeleton.tsx | 0 .../_components/{ => ai}/recommendations.tsx | 10 +- .../_components/order-analyses-cards.tsx | 128 ++++++++---------- .../(user)/_lib/server/load-life-style.ts | 41 ++++++ public/locales/et/dashboard.json | 4 + 10 files changed, 233 insertions(+), 89 deletions(-) create mode 100644 app/home/(user)/_components/ai/ai-blocks.tsx create mode 100644 app/home/(user)/_components/ai/life-style-card.tsx create mode 100644 app/home/(user)/_components/ai/order-analyses-package-card.tsx rename app/home/(user)/_components/{ => ai}/recommendations-skeleton.tsx (100%) rename app/home/(user)/_components/{ => ai}/recommendations.tsx (65%) create mode 100644 app/home/(user)/_lib/server/load-life-style.ts diff --git a/app/home/(user)/(dashboard)/order-analysis/page.tsx b/app/home/(user)/(dashboard)/order-analysis/page.tsx index edd0ad7..5e2bd2f 100644 --- a/app/home/(user)/(dashboard)/order-analysis/page.tsx +++ b/app/home/(user)/(dashboard)/order-analysis/page.tsx @@ -37,8 +37,8 @@ async function OrderAnalysisPage() { return ( <> } - description={} + title={} + description={} /> diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index cdb7bcf..49db975 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -1,5 +1,3 @@ -import { Suspense } from 'react'; - import { redirect } from 'next/navigation'; import { toTitleCase } from '@/lib/utils'; @@ -12,11 +10,9 @@ import { createUserAnalysesApi } from '@kit/user-analyses/api'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import AIBlocks from '../_components/ai/ai-blocks'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; -import Recommendations from '../_components/recommendations'; -import RecommendationsSkeleton from '../_components/recommendations-skeleton'; -import { isValidOpenAiEnv } from '../_lib/server/is-valid-open-ai-env'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; export const generateMetadata = async () => { @@ -53,16 +49,13 @@ async function UserHomePage() { /> - {(await isValidOpenAiEnv()) && ( - <> -

- -

- }> - - - - )} + +

+ +

+
+ +
); diff --git a/app/home/(user)/_components/ai/ai-blocks.tsx b/app/home/(user)/_components/ai/ai-blocks.tsx new file mode 100644 index 0000000..fe1ff9d --- /dev/null +++ b/app/home/(user)/_components/ai/ai-blocks.tsx @@ -0,0 +1,42 @@ +'use server'; + +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'; + +const AIBlocks = async ({ account }: { account: AccountWithParams }) => { + const isOpenAiAvailable = await isValidOpenAiEnv(); + + if (!isOpenAiAvailable) { + return ; + } + + const { analyses, countryCode } = await loadAnalyses(); + + if (analyses.length === 0) { + return ( + <> + + }> + + + + ); + } + + return ( + }> + + + + ); +}; + +export default AIBlocks; diff --git a/app/home/(user)/_components/ai/life-style-card.tsx b/app/home/(user)/_components/ai/life-style-card.tsx new file mode 100644 index 0000000..e2fffd0 --- /dev/null +++ b/app/home/(user)/_components/ai/life-style-card.tsx @@ -0,0 +1,19 @@ +'use server'; + +import React from 'react'; + +import { Card } from '@kit/ui/shadcn/card'; + +import { loadLifeStyle } from '../../_lib/server/load-life-style'; + +const LifeStyleCard = async () => { + const data = await loadLifeStyle(); + + return ( + + Test + + ); +}; + +export default LifeStyleCard; diff --git a/app/home/(user)/_components/ai/order-analyses-package-card.tsx b/app/home/(user)/_components/ai/order-analyses-package-card.tsx new file mode 100644 index 0000000..a27339e --- /dev/null +++ b/app/home/(user)/_components/ai/order-analyses-package-card.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import Link from 'next/link'; + +import { pathsConfig } from '@/packages/shared/src/config'; +import { ChevronRight, HeartPulse } from 'lucide-react'; + +import { Trans } from '@kit/ui/makerkit/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import { + Card, + CardDescription, + CardFooter, + CardHeader, +} from '@kit/ui/shadcn/card'; + +const OrderAnalysesPackageCard = () => { + return ( + + +
+ +
+
+ + + +
+
+ +
+ +
+ + + +
+
+ ); +}; + +export default OrderAnalysesPackageCard; diff --git a/app/home/(user)/_components/recommendations-skeleton.tsx b/app/home/(user)/_components/ai/recommendations-skeleton.tsx similarity index 100% rename from app/home/(user)/_components/recommendations-skeleton.tsx rename to app/home/(user)/_components/ai/recommendations-skeleton.tsx diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/ai/recommendations.tsx similarity index 65% rename from app/home/(user)/_components/recommendations.tsx rename to app/home/(user)/_components/ai/recommendations.tsx index 71403ef..b8d2722 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/ai/recommendations.tsx @@ -4,9 +4,9 @@ import React from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import { loadAnalyses } from '../_lib/server/load-analyses'; -import { loadRecommendations } from '../_lib/server/load-recommendations'; -import OrderAnalysesCards from './order-analyses-cards'; +import { loadAnalyses } from '../../_lib/server/load-analyses'; +import { loadRecommendations } from '../../_lib/server/load-recommendations'; +import OrderAnalysesCards from '../order-analyses-cards'; export default async function Recommendations({ account, @@ -25,6 +25,8 @@ export default async function Recommendations({ } return ( - + <> + + ); } diff --git a/app/home/(user)/_components/order-analyses-cards.tsx b/app/home/(user)/_components/order-analyses-cards.tsx index 88574fa..b6f73e3 100644 --- a/app/home/(user)/_components/order-analyses-cards.tsx +++ b/app/home/(user)/_components/order-analyses-cards.tsx @@ -57,72 +57,64 @@ export default function OrderAnalysesCards({ } }; - return ( -
- {analyses.map(({ title, variant, description, subtitle, price }) => { - const formattedPrice = - typeof price === 'number' - ? formatCurrency({ - currencyCode: 'eur', - locale: language, - value: price, - }) - : null; - return ( - - -
- -
-
- -
-
- -
-
- {title} - {description && ( - <> - - {formattedPrice} - {description} -
- } - /> - - )} - - {subtitle && {subtitle}} -
-
- {formattedPrice} -
- - - ); - })} - - ); + return analyses.map(({ title, variant, description, subtitle, price }) => { + const formattedPrice = + typeof price === 'number' + ? formatCurrency({ + currencyCode: 'eur', + locale: language, + value: price, + }) + : null; + return ( + + +
+ +
+
+ +
+
+ +
+
+ {title} + {description && ( + <> + + {formattedPrice} + {description} +
+ } + /> + + )} + + {subtitle && {subtitle}} + +
+ {formattedPrice} +
+
+
+ ); + }); } diff --git a/app/home/(user)/_lib/server/load-life-style.ts b/app/home/(user)/_lib/server/load-life-style.ts new file mode 100644 index 0000000..631ffd4 --- /dev/null +++ b/app/home/(user)/_lib/server/load-life-style.ts @@ -0,0 +1,41 @@ +import { cache } from 'react'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import OpenAI from 'openai'; + +import PersonalCode from '~/lib/utils'; + +const failedResponse = { + lifeStyle: null, + summary: null, +}; + +async function lifeStyleLoader(account: AccountWithParams) { + if (!account?.personal_code) { + return failedResponse; + } + + const openAIClient = new OpenAI(); + const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); + try { + const 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(), + }, + }, + }); + + return response; + } catch (error) { + console.error('Error calling OpenAI: ', error); + return failedResponse; + } +} +export const loadLifeStyle = cache(lifeStyleLoader); diff --git a/public/locales/et/dashboard.json b/public/locales/et/dashboard.json index 8fce772..ffc7aa7 100644 --- a/public/locales/et/dashboard.json +++ b/public/locales/et/dashboard.json @@ -21,6 +21,10 @@ "benefits": { "title": "Sinu Medreport konto seis", "validUntil": "Kehtiv kuni {{date}}" + }, + "orderPackage": { + "title": "Telli analüüside pakett", + "description": "Võrdle erinevate pakettide vahel ja vali endale sobiv" } }, "recommendations": { From 76c2382e115c482a3cc5cb80ec8082b63b504b5f Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 21 Oct 2025 16:04:01 +0300 Subject: [PATCH 2/6] lifestyle development --- .../(user)/(dashboard)/life-style/page.tsx | 84 ++++++++++++++ app/home/(user)/_components/ai/ai-blocks.tsx | 8 +- .../(user)/_components/ai/life-style-card.tsx | 26 ++++- .../ai/recommendations-skeleton.tsx | 91 +++++++-------- app/home/(user)/_components/ai/types.ts | 18 +++ .../_components/orders/order-items-table.tsx | 90 +++++++-------- app/home/(user)/_lib/server/ai-actions.ts | 108 ++++++++++++++++++ .../(user)/_lib/server/load-life-style.ts | 54 +++++---- .../_lib/server/load-recommendations.ts | 3 +- lib/services/audit/pageView.service.ts | 1 + packages/shared/src/config/paths.config.ts | 2 + .../personal-account-navigation.config.tsx | 7 ++ public/locales/et/common.json | 8 +- public/locales/et/dashboard.json | 3 + 14 files changed, 374 insertions(+), 129 deletions(-) create mode 100644 app/home/(user)/(dashboard)/life-style/page.tsx create mode 100644 app/home/(user)/_components/ai/types.ts create mode 100644 app/home/(user)/_lib/server/ai-actions.ts diff --git a/app/home/(user)/(dashboard)/life-style/page.tsx b/app/home/(user)/(dashboard)/life-style/page.tsx new file mode 100644 index 0000000..e359c73 --- /dev/null +++ b/app/home/(user)/(dashboard)/life-style/page.tsx @@ -0,0 +1,84 @@ +import React from 'react'; + +import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; +import { Circle } from 'lucide-react'; + +import { cn } from '@kit/ui/lib/utils'; +import { PageBody } from '@kit/ui/makerkit/page'; +import { Trans } from '@kit/ui/makerkit/trans'; +import { Skeleton } from '@kit/ui/shadcn/skeleton'; + +import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; +import { withI18n } from '~/lib/i18n/with-i18n'; +import { + PageViewAction, + createPageViewLog, +} from '~/lib/services/audit/pageView.service'; + +import { HomeLayoutPageHeader } from '../../_components/home-page-header'; +import { loadLifeStyle } from '../../_lib/server/load-life-style'; +import { loadCurrentUserAccount } from '../../_lib/server/load-user-account'; + +export async function generateMetadata() { + const { t } = await createI18nServerInstance(); + + return { + title: t('common:lifeStyle.title'), + }; +} + +async function LifeStylePage() { + const supabaseClient = getSupabaseServerClient(); + const userAnalysesApi = createUserAnalysesApi(supabaseClient); + + const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); + console.log('analysisResponses', analysisResponses); + const { account } = await loadCurrentUserAccount(); + if (!account) { + return null; + } + const data = await loadLifeStyle(account); + + await createPageViewLog({ + accountId: account.id, + action: PageViewAction.VIEW_LIFE_STYLE, + }); + + if (!data.lifestyle) { + return ; + } + + return ( + <> + } + description="" + /> + + +
+ {data.lifestyle.map(({ title, description, score }, index) => ( + +
+

{title}

+ +
+

{description}

+
+ ))} +
+
+ + ); +} + +export default withI18n(LifeStylePage); diff --git a/app/home/(user)/_components/ai/ai-blocks.tsx b/app/home/(user)/_components/ai/ai-blocks.tsx index fe1ff9d..7c44026 100644 --- a/app/home/(user)/_components/ai/ai-blocks.tsx +++ b/app/home/(user)/_components/ai/ai-blocks.tsx @@ -18,14 +18,14 @@ const AIBlocks = async ({ account }: { account: AccountWithParams }) => { return ; } - const { analyses, countryCode } = await loadAnalyses(); + const { analyses } = await loadAnalyses(); if (analyses.length === 0) { return ( <> - }> - + }> + ); @@ -33,7 +33,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 e2fffd0..14da00d 100644 --- a/app/home/(user)/_components/ai/life-style-card.tsx +++ b/app/home/(user)/_components/ai/life-style-card.tsx @@ -2,16 +2,34 @@ import React from 'react'; -import { Card } from '@kit/ui/shadcn/card'; +import Link from 'next/link'; + +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; +import { pathsConfig } from '@/packages/shared/src/config'; +import { ChevronRight } from 'lucide-react'; + +import { Trans } from '@kit/ui/makerkit/trans'; +import { Button } from '@kit/ui/shadcn/button'; +import { Card, CardHeader } from '@kit/ui/shadcn/card'; import { loadLifeStyle } from '../../_lib/server/load-life-style'; -const LifeStyleCard = async () => { - const data = await loadLifeStyle(); +const LifeStyleCard = async ({ account }: { account: AccountWithParams }) => { + const data = await loadLifeStyle(account); return ( - Test + +
+ +
+ + + +
+ {data.summary}
); }; diff --git a/app/home/(user)/_components/ai/recommendations-skeleton.tsx b/app/home/(user)/_components/ai/recommendations-skeleton.tsx index fad1225..2964784 100644 --- a/app/home/(user)/_components/ai/recommendations-skeleton.tsx +++ b/app/home/(user)/_components/ai/recommendations-skeleton.tsx @@ -11,7 +11,7 @@ import { } from '@kit/ui/shadcn/card'; import { Skeleton } from '@kit/ui/skeleton'; -const RecommendationsSkeleton = () => { +const RecommendationsSkeleton = ({ amount = 2 }: { amount?: number }) => { const emptyData = [ { title: '1', @@ -20,55 +20,48 @@ const RecommendationsSkeleton = () => { variant: { id: '' }, price: 1, }, - { - title: '2', - description: '', - subtitle: '', - variant: { id: '' }, - price: 1, - }, ]; - return ( -
- {emptyData.map(({ title, description, subtitle }) => ( - - - -
-
-
- - -
-
- {title} - {description && ( - <> - {' '} - - {description} -
- } - /> - - )} - - {subtitle && {subtitle}} -
-
- -
-
- ))} -
- ); + return Array.from({ length: amount }, (_, index) => { + const { title, description, subtitle } = emptyData[0]!; + + return ( + + + +
+
+
+ + +
+
+ {title} + {description && ( + <> + {' '} + + {description} +
+ } + /> + + )} + + {subtitle && {subtitle}} +
+
+ +
+
+ ); + }); }; export default RecommendationsSkeleton; diff --git a/app/home/(user)/_components/ai/types.ts b/app/home/(user)/_components/ai/types.ts new file mode 100644 index 0000000..2026cce --- /dev/null +++ b/app/home/(user)/_components/ai/types.ts @@ -0,0 +1,18 @@ +import { Database } from '@/packages/supabase/src/database.types'; + +export interface ILifeStyleResponse { + lifestyle: { + title: string; + description: string; + score: 0 | 1 | 2; + }[]; + summary: string | null; +} + +export enum PROMPT_NAME { + LIFE_STYLE = 'Life Style', + ANALYSIS_RECOMMENDATIONS = 'Analysis Recommendations', +} + +export type AnalysisResponses = + Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns']; diff --git a/app/home/(user)/_components/orders/order-items-table.tsx b/app/home/(user)/_components/orders/order-items-table.tsx index ec9bbef..3eb3840 100644 --- a/app/home/(user)/_components/orders/order-items-table.tsx +++ b/app/home/(user)/_components/orders/order-items-table.tsx @@ -63,55 +63,53 @@ export default function OrderItemsTable({ return ( <> - - {items - .sort((a, b) => - (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1, - ) - .map((orderItem) => ( -
+ {items + .sort((a, b) => + (a.created_at ?? '') > (b.created_at ?? '') ? -1 : 1, + ) + .map((orderItem) => ( + + + + {order.location && ( - - {order.location && ( - - )} - - - - - + {isTtoservice && order.bookingCode && ( + - {isTtoservice && order.bookingCode && ( - - )} - - -
- ))} -
+ )} + + + + ))}
diff --git a/app/home/(user)/_lib/server/ai-actions.ts b/app/home/(user)/_lib/server/ai-actions.ts new file mode 100644 index 0000000..5031e50 --- /dev/null +++ b/app/home/(user)/_lib/server/ai-actions.ts @@ -0,0 +1,108 @@ +'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'; + +import PersonalCode from '~/lib/utils'; + +import { + AnalysisResponses, + ILifeStyleResponse, + PROMPT_NAME, +} from '../../_components/ai/types'; + +async function 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; +} + +export async function updateLifeStyle({ + account, +}: { + account: AccountWithParams; +}): Promise { + const LIFE_STYLE_PROMPT_ID = process.env.PROMPT_ID_LIFE_STYLE; + if (!LIFE_STYLE_PROMPT_ID || !account?.personal_code) { + return { + lifestyle: [], + summary: null, + }; + } + + 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 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: LIFE_STYLE_PROMPT_ID, + variables: { + gender: gender.value, + age: age.toString(), + weight: weight.toString(), + height: height.toString(), + cholesterol: '', + ldl: '', + hdl: '', + vitamind: '', + is_smoker: isSmoker.toString(), + }, + }, + }); + + await supabaseClient + .schema('medreport') + .from('ai_responses') + .insert({ + account_id: account.id, + prompt_name: PROMPT_NAME.LIFE_STYLE, + prompt_id: LIFE_STYLE_PROMPT_ID, + input: JSON.stringify({ + gender: gender.value, + age: age.toString(), + weight: weight.toString(), + cholesterol: '', + ldl: '', + hdl: '', + vitamind: '', + is_smoker: isSmoker.toString(), + }), + latest_data_change: latestISO, + response: response.output_text, + }); + + const json = JSON.parse(response.output_text); + + return json; + } catch (error) { + console.error('Error calling OpenAI: ', error); + return { + lifestyle: [], + summary: null, + }; + } +} diff --git a/app/home/(user)/_lib/server/load-life-style.ts b/app/home/(user)/_lib/server/load-life-style.ts index 631ffd4..889bddd 100644 --- a/app/home/(user)/_lib/server/load-life-style.ts +++ b/app/home/(user)/_lib/server/load-life-style.ts @@ -1,41 +1,49 @@ import { cache } from 'react'; import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import OpenAI from 'openai'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; -import PersonalCode from '~/lib/utils'; +import { ILifeStyleResponse, PROMPT_NAME } from '../../_components/ai/types'; +import { updateLifeStyle } from './ai-actions'; const failedResponse = { - lifeStyle: null, + lifestyle: [], summary: null, }; -async function lifeStyleLoader(account: AccountWithParams) { +async function lifeStyleLoader( + account: AccountWithParams, +): Promise { if (!account?.personal_code) { return failedResponse; } - const openAIClient = new OpenAI(); - const { gender, age } = PersonalCode.parsePersonalCode(account.personal_code); - try { - const 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(), - }, - }, - }); + const lifeStylePromptId = process.env.PROMPT_ID_LIFE_STYLE; - return response; - } catch (error) { - console.error('Error calling OpenAI: ', error); + if (!lifeStylePromptId) { return failedResponse; } + + const supabaseClient = getSupabaseServerClient(); + const { data, error } = await supabaseClient + .schema('medreport') + .from('ai_responses') + .select('response') + .eq('account_id', account.id) + .eq('prompt_name', PROMPT_NAME.LIFE_STYLE) + .order('latest_data_change', { ascending: false, nullsFirst: false }) + .limit(1) + .maybeSingle(); + + if (error) { + console.error('Error fetching AI response from DB: ', error); + return failedResponse; + } + + if (data?.response) { + return JSON.parse(data.response as string); + } else { + return await updateLifeStyle({ account }); + } } 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 8763c87..4c89ae3 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -8,6 +8,7 @@ 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); @@ -120,7 +121,7 @@ async function recommendationsLoader( .from('ai_responses') .insert({ account_id: account.id, - prompt_name: 'Analysis Recommendations', + prompt_name: PROMPT_NAME.ANALYSIS_RECOMMENDATIONS, prompt_id: analysesRecommendationsPromptId, input: JSON.stringify({ analyses: formattedAnalyses, diff --git a/lib/services/audit/pageView.service.ts b/lib/services/audit/pageView.service.ts index c107cf1..2f45566 100644 --- a/lib/services/audit/pageView.service.ts +++ b/lib/services/audit/pageView.service.ts @@ -7,6 +7,7 @@ export enum PageViewAction { VIEW_ORDER_ANALYSIS = 'VIEW_ORDER_ANALYSIS', VIEW_TEAM_ACCOUNT_DASHBOARD = 'VIEW_TEAM_ACCOUNT_DASHBOARD', VIEW_TTO_SERVICE_BOOKING = 'VIEW_TTO_SERVICE_BOOKING', + VIEW_LIFE_STYLE = 'VIEW_LIFE_STYLE', } export const createPageViewLog = async ({ diff --git a/packages/shared/src/config/paths.config.ts b/packages/shared/src/config/paths.config.ts index b3f4965..383fdf3 100644 --- a/packages/shared/src/config/paths.config.ts +++ b/packages/shared/src/config/paths.config.ts @@ -40,6 +40,7 @@ const PathsSchema = z.object({ completedJobs: z.string().min(1), openJobs: z.string().min(1), analysisDetails: z.string().min(1), + lifeStyle: z.string().min(1), }), }); @@ -83,6 +84,7 @@ const pathsConfig = PathsSchema.parse({ completedJobs: '/doctor/completed-jobs', openJobs: '/doctor/open-jobs', analysisDetails: 'doctor/analysis', + lifeStyle: '/home/life-style', }, } satisfies z.infer); diff --git a/packages/shared/src/config/personal-account-navigation.config.tsx b/packages/shared/src/config/personal-account-navigation.config.tsx index d2763d7..c4bde8f 100644 --- a/packages/shared/src/config/personal-account-navigation.config.tsx +++ b/packages/shared/src/config/personal-account-navigation.config.tsx @@ -1,4 +1,5 @@ import { + Bike, FileLineChart, HeartPulse, LineChart, @@ -53,6 +54,12 @@ const routes = [ Icon: , end: true, }, + { + label: 'common:routes.lifeStyle', + path: pathsConfig.app.lifeStyle, + Icon: , + end: true, + }, ], }, ] satisfies z.infer['routes']; diff --git a/public/locales/et/common.json b/public/locales/et/common.json index 8615a16..1c9f38a 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -84,7 +84,8 @@ "preferences": "Eelistused", "security": "Turvalisus", "admin": "Admin", - "accounts": "Kontod" + "accounts": "Kontod", + "lifeStyle": "Elustiil" }, "roles": { "owner": { @@ -150,5 +151,8 @@ "no": "Ei", "preferNotToAnswer": "Eelistan mitte vastata", "book": "Broneeri", - "change": "Muuda" + "change": "Muuda", + "lifeStyle": { + "title": "Elustiili soovitused" + } } diff --git a/public/locales/et/dashboard.json b/public/locales/et/dashboard.json index ffc7aa7..acf7f42 100644 --- a/public/locales/et/dashboard.json +++ b/public/locales/et/dashboard.json @@ -25,6 +25,9 @@ "orderPackage": { "title": "Telli analüüside pakett", "description": "Võrdle erinevate pakettide vahel ja vali endale sobiv" + }, + "lifeStyle": { + "title": "Elustiili soovitused" } }, "recommendations": { From 9a01c15a76d6a3b111c01cda71caf3344474fe96 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Thu, 23 Oct 2025 16:48:27 +0300 Subject: [PATCH 3/6] 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; From 38c487b54f0bf8ec12b31d904b771f1d72643829 Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Fri, 24 Oct 2025 09:35:35 +0300 Subject: [PATCH 4/6] code fix --- app/home/(user)/(dashboard)/life-style/page.tsx | 12 +----------- app/home/(user)/_components/ai/ai-blocks.tsx | 1 + app/home/(user)/_components/ai/recommendations.tsx | 4 +--- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/home/(user)/(dashboard)/life-style/page.tsx b/app/home/(user)/(dashboard)/life-style/page.tsx index e359c73..97862ab 100644 --- a/app/home/(user)/(dashboard)/life-style/page.tsx +++ b/app/home/(user)/(dashboard)/life-style/page.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import { createUserAnalysesApi } from '@/packages/features/user-analyses/src/server/api'; -import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; import { Circle } from 'lucide-react'; import { cn } from '@kit/ui/lib/utils'; @@ -29,11 +27,6 @@ export async function generateMetadata() { } async function LifeStylePage() { - const supabaseClient = getSupabaseServerClient(); - const userAnalysesApi = createUserAnalysesApi(supabaseClient); - - const analysisResponses = await userAnalysesApi.getAllUserAnalysisResponses(); - console.log('analysisResponses', analysisResponses); const { account } = await loadCurrentUserAccount(); if (!account) { return null; @@ -60,10 +53,7 @@ async function LifeStylePage() {
{data.lifestyle.map(({ title, description, score }, index) => ( -
+

{title}

; } + if (analysisResponses?.length === 0) { return ( <> diff --git a/app/home/(user)/_components/ai/recommendations.tsx b/app/home/(user)/_components/ai/recommendations.tsx index 7208d36..10b6ef8 100644 --- a/app/home/(user)/_components/ai/recommendations.tsx +++ b/app/home/(user)/_components/ai/recommendations.tsx @@ -25,8 +25,6 @@ export default async function Recommendations({ } return ( - <> - - + ); } From 8bc6089a7f07820f509b393a1ca3aee1e1d58a0d Mon Sep 17 00:00:00 2001 From: Danel Kungla Date: Tue, 28 Oct 2025 16:09:06 +0200 Subject: [PATCH 5/6] add doctor feedback --- app/doctor/_components/analysis-fallback.tsx | 27 +++ app/doctor/_components/analysis-feedback.tsx | 41 +++-- app/doctor/_components/analysis-view.tsx | 45 ++++- .../doctor-recommended-analyses.tsx | 53 ++++++ .../new-analysis-recommendations-loader.tsx | 90 ++++++++++ .../_components/prepare-ai-parameters.tsx | 55 ++++++ .../_lib/server/load-doctor-feedback.ts | 98 +++++++++++ app/doctor/analysis/[id]/page.tsx | 24 ++- .../(user)/(dashboard)/life-style/page.tsx | 25 +-- app/home/(user)/_components/ai/ai-blocks.tsx | 5 +- .../(user)/_components/ai/life-style-card.tsx | 22 ++- .../(user)/_components/ai/recommendations.tsx | 13 +- app/home/(user)/_components/ai/types.ts | 1 + .../_components/home-mobile-navigation.tsx | 4 +- app/home/(user)/_lib/server/ai-actions.ts | 162 +++++++++++++++--- .../(user)/_lib/server/load-life-style.ts | 63 +++++-- .../_lib/server/load-recommendations.ts | 54 +++++- lib/utils.ts | 15 ++ .../server/actions/doctor-server-actions.ts | 23 +++ .../doctor-analysis-detail-view.schema.ts | 3 +- .../server/schema/doctor-analysis.schema.ts | 4 + .../services/doctor-analysis.service.ts | 3 +- .../features/user-analyses/src/server/api.ts | 6 +- .../personal-account-navigation.config.tsx | 4 +- packages/supabase/src/database.types.ts | 3 + public/locales/et/common.json | 1 + public/locales/et/doctor.json | 5 +- .../20251024164100_improve_ai_responses.sql | 66 +++++++ 28 files changed, 820 insertions(+), 95 deletions(-) create mode 100644 app/doctor/_components/analysis-fallback.tsx create mode 100644 app/doctor/_components/doctor-recommended-analyses.tsx create mode 100644 app/doctor/_components/new-analysis-recommendations-loader.tsx create mode 100644 app/doctor/_components/prepare-ai-parameters.tsx create mode 100644 app/doctor/_lib/server/load-doctor-feedback.ts create mode 100644 supabase/migrations/20251024164100_improve_ai_responses.sql diff --git a/app/doctor/_components/analysis-fallback.tsx b/app/doctor/_components/analysis-fallback.tsx new file mode 100644 index 0000000..9431ad7 --- /dev/null +++ b/app/doctor/_components/analysis-fallback.tsx @@ -0,0 +1,27 @@ +'use server'; + +import React from 'react'; + +import { Spinner } from '@kit/ui/makerkit/spinner'; +import { Trans } from '@kit/ui/makerkit/trans'; +import { Progress } from '@kit/ui/shadcn/progress'; + +import { withI18n } from '~/lib/i18n/with-i18n'; + +const AnalysisFallback = ({ + progress, + progressTextKey, +}: { + progress: number; + progressTextKey: string; +}) => { + return ( +
+ + + +
+ ); +}; + +export default withI18n(AnalysisFallback); diff --git a/app/doctor/_components/analysis-feedback.tsx b/app/doctor/_components/analysis-feedback.tsx index c1d4c54..df0a234 100644 --- a/app/doctor/_components/analysis-feedback.tsx +++ b/app/doctor/_components/analysis-feedback.tsx @@ -16,6 +16,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useQueryClient } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; +import { Spinner } from '@kit/ui/makerkit/spinner'; import { Trans } from '@kit/ui/makerkit/trans'; import { Button } from '@kit/ui/shadcn/button'; import { @@ -32,12 +33,21 @@ const AnalysisFeedback = ({ feedback, patient, order, + aiDoctorFeedback, + timestamp, + recommendations, + isRecommendationsEdited, }: { feedback?: DoctorFeedback; patient: Patient; order: Order; + aiDoctorFeedback?: string; + timestamp?: string; + recommendations: string[]; + isRecommendationsEdited: boolean; }) => { const [isDraftSubmitting, setIsDraftSubmitting] = useState(false); + const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false); const [isConfirmOpen, setIsConfirmOpen] = useState(false); const { data: user } = useUser(); const queryClient = useQueryClient(); @@ -46,7 +56,7 @@ const AnalysisFeedback = ({ resolver: zodResolver(doctorAnalysisFeedbackFormSchema), reValidateMode: 'onChange', defaultValues: { - feedbackValue: feedback?.value ?? '', + feedbackValue: feedback?.value ?? aiDoctorFeedback ?? '', userId: patient.userId, }, }); @@ -71,23 +81,30 @@ const AnalysisFeedback = ({ data: DoctorAnalysisFeedbackForm, status: 'DRAFT' | 'COMPLETED', ) => { + setIsConfirmOpen(false); + setIsSubmittingFeedback(true); + const result = await giveFeedbackAction({ ...data, analysisOrderId: order.analysisOrderId, status, + patientId: patient.userId, + timestamp, + recommendations, + isRecommendationsEdited, }); if (!result.success) { return toast.error(); } + setIsSubmittingFeedback(false); + queryClient.invalidateQueries({ predicate: (query) => query.queryKey.includes('doctor-jobs'), }); - toast.success(); - - return setIsConfirmOpen(false); + return toast.success(); }; const confirmComplete = form.handleSubmit(async (data) => { @@ -96,10 +113,6 @@ const AnalysisFeedback = ({ return ( <> -

- -

-

{feedback?.value ?? '-'}

{!isReadOnly && (
@@ -109,7 +122,11 @@ const AnalysisFeedback = ({ render={({ field }) => ( -