diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index f3e603c..c2a279e 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -1,3 +1,5 @@ +import { Suspense } from 'react'; + import { redirect } from 'next/navigation'; import { toTitleCase } from '@/lib/utils'; @@ -13,10 +15,8 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import Dashboard from '../_components/dashboard'; import DashboardCards from '../_components/dashboard-cards'; import Recommendations from '../_components/recommendations'; -import { loadAnalyses } from '../_lib/server/load-analyses'; -import { loadRecommendations } from '../_lib/server/load-recommendations'; import { loadCurrentUserAccount } from '../_lib/server/load-user-account'; -import { analysisResponses } from './analysis-results/test/test-responses'; +import Loading from '../loading'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -28,21 +28,11 @@ export const generateMetadata = async () => { }; async function UserHomePage() { - console.log('process.env', process.env.OPENAI_API_KEY); const client = getSupabaseServerClient(); const { account } = await loadCurrentUserAccount(); const api = createUserAnalysesApi(client); const bmiThresholds = await api.fetchBmiThresholds(); - const { analyses, countryCode } = await loadAnalyses(); - - const analysisRecommendations = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - console.log('analysisRecommendations', analysisRecommendations); if (!account) { redirect('/'); @@ -62,13 +52,9 @@ async function UserHomePage() { /> - - - analysisRecommendations.includes(analysis.title), - )} - countryCode={countryCode} - /> + + + ); diff --git a/app/home/(user)/_components/recommendations.tsx b/app/home/(user)/_components/recommendations.tsx index d9eace1..dc45427 100644 --- a/app/home/(user)/_components/recommendations.tsx +++ b/app/home/(user)/_components/recommendations.tsx @@ -1,24 +1,40 @@ -'use client'; +'use server'; -import React from 'react'; +import React, { useState } from 'react'; +import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; + +import { analysisResponses } from '../(dashboard)/analysis-results/test/test-responses'; +import { loadAnalyses } from '../_lib/server/load-analyses'; +import { loadRecommendations } from '../_lib/server/load-recommendations'; import OrderAnalysesCards, { OrderAnalysisCard } from './order-analyses-cards'; -export default function Recommendations({ - recommended, - countryCode, +export default async function Recommendations({ + account, }: { - recommended: OrderAnalysisCard[]; - countryCode: string; + account: AccountWithParams; }) { - if (recommended.length < 1) { + const { analyses, countryCode } = await loadAnalyses(); + const [isLoadingTimeSlots, setIsLoadingTimeSlots] = useState(false); + const analysisResults = analysisResponses; + console.log('selectedDate', isLoadingTimeSlots); + const analysisRecommendations = await loadRecommendations( + analysisResults, + analyses, + account, + ); + const orderAnalyses = analyses.filter((analysis) => + analysisRecommendations.includes(analysis.title), + ); + console.log('analysisRecommendations', analysisRecommendations); + if (orderAnalyses.length < 1) { return null; } return (

Medreport soovitab teile

- +
); } diff --git a/app/home/(user)/_lib/server/load-recommendations.test.ts b/app/home/(user)/_lib/server/load-recommendations.test.ts deleted file mode 100644 index b492eae..0000000 --- a/app/home/(user)/_lib/server/load-recommendations.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { Mock } from 'jest-mock'; - -// ---- Mocks you can tweak per test ---- -const createResponseMock = jest.fn(); -const getLatestResponseTimeMock = jest.fn(); -const getLatestUniqueAnalysResponsesMock = jest.fn(); -const parsePersonalCodeMock = jest.fn(() => ({ gender: { value: 'male' } })); - -// Mock OpenAI SDK -jest.mock('openai', () => { - return { - __esModule: true, - default: class OpenAI { - responses = { create: createResponseMock }; - }, - }; -}); - -// Mock next/cache (global cache map so it persists between calls) -const globalCache = new Map(); -jest.mock('next/cache', () => { - return { - __esModule: true, - unstable_cache: - (fn: (...args: any[]) => Promise, keyParts: any[], _opts?: any) => - async (...args: any[]) => { - const key = JSON.stringify(keyParts); - if (globalCache.has(key)) return globalCache.get(key); - const val = await fn(...args); - globalCache.set(key, val); - return val; - }, - }; -}); - -// Mock your analysis helpers + personal code parser -jest.mock('../src/analysis-utils', () => ({ - __esModule: true, - getLatestUniqueAnalysResponses: getLatestUniqueAnalysResponsesMock, - getLatestResponseTime: getLatestResponseTimeMock, -})); -jest.mock('../src/personal-code', () => ({ - __esModule: true, - PersonalCode: { parsePersonalCode: parsePersonalCodeMock }, -})); - -describe('loadRecommendations', () => { - beforeEach(() => { - createResponseMock.mockReset(); - getLatestResponseTimeMock.mockReset(); - getLatestUniqueAnalysResponsesMock.mockReset(); - globalCache.clear(); - }); - - it('should call OpenAI once when latest date stays the same (cache hit on 2nd call)', async () => { - const date1 = new Date('2025-09-16T12:00:00Z'); - getLatestResponseTimeMock.mockReturnValue(date1); - getLatestUniqueAnalysResponsesMock.mockImplementation((arr: any[]) => arr); - - createResponseMock.mockResolvedValue({ - output_text: JSON.stringify({ recommended: ['A', 'B'] }), - }); - - const { loadRecommendations } = await import('./load-recommendations'); - - const analysisResponses = [ - { name: 'x', value: 1, responseTime: date1 }, - ] as any[]; - const analyses = [{ title: 't', description: 'd' }] as any[]; - const account = { id: 'u1', personal_code: '12345678901' } as any; - - // Act: 1st call (MISS) - const out1 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - // Act: 2nd call with same date (HIT) - const out2 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - // Assert: only one API call, result reused - expect(createResponseMock).toHaveBeenCalledTimes(1); - expect(out1).toEqual(['A', 'B']); - expect(out2).toEqual(['A', 'B']); - }); - - it('should call OpenAI again when latest date changes (new cache key)', async () => { - const date1 = new Date('2025-09-16T12:00:00Z'); - const date2 = new Date('2025-09-17T00:00:00Z'); - - getLatestResponseTimeMock.mockReturnValueOnce(date1); - getLatestResponseTimeMock.mockReturnValueOnce(date2); - - getLatestUniqueAnalysResponsesMock.mockImplementation((arr: any[]) => arr); - - createResponseMock - .mockResolvedValueOnce({ - output_text: JSON.stringify({ recommended: ['A', 'B'] }), - }) - .mockResolvedValueOnce({ - output_text: JSON.stringify({ recommended: ['C'] }), - }); - - const { loadRecommendations } = await import('./load-recommendations'); - - const analysisResponses = [ - { name: 'x', value: 1, responseTime: date1 }, - ] as any[]; - const analyses = [{ title: 't', description: 'd' }] as any[]; - const account = { id: 'u1', personal_code: '12345678901' } as any; - - const out1 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - const out2 = await loadRecommendations( - analysisResponses, - analyses, - account, - ); - - expect(createResponseMock).toHaveBeenCalledTimes(2); - expect(out1).toEqual(['A', 'B']); - expect(out2).toEqual(['C']); - }); -}); diff --git a/app/home/(user)/_lib/server/load-recommendations.ts b/app/home/(user)/_lib/server/load-recommendations.ts index ed8e6f1..a843c47 100644 --- a/app/home/(user)/_lib/server/load-recommendations.ts +++ b/app/home/(user)/_lib/server/load-recommendations.ts @@ -1,9 +1,6 @@ import { cache } from 'react'; -import { unstable_cache } from 'next/cache'; - import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts'; -import crypto from 'crypto'; import OpenAI from 'openai'; import PersonalCode from '~/lib/utils'; @@ -19,20 +16,6 @@ type FormattedAnalysisResponse = { responseTime: string; }; -function canonical(v: unknown): string { - return JSON.stringify(v, (_k, val) => { - if (val && typeof val === 'object' && !Array.isArray(val)) { - // sort object keys for stable JSON - return Object.keys(val) - .sort() - .reduce((o: any, k) => ((o[k] = (val as any)[k]), o), {}); - } - return val; - }); -} -const sha256 = (s: string) => - crypto.createHash('sha256').update(s).digest('hex'); - const getLatestResponseTime = (items: FormattedAnalysisResponse[]) => { if (!items?.length) return null; @@ -56,23 +39,29 @@ const getLatestUniqueAnalysResponses = ( console.log('analysisElements', analysisElements.length); - const map = new Map(); - for (const it of analysisElements) { - const prev = map.get(it.analysisName); - if (it.results.responseTime) { - if ( - !prev || - new Date(it.results.responseTime) > new Date(prev.responseTime) - ) { - map.set(it.analysisName, { - name: it.analysisName, - value: it.results.responseValue, - responseTime: it.results.responseTime, - }); - } + const byName = analysisElements.reduce< + Record + >((acc, it) => { + const responseTime = it?.results?.responseTime; + const responseValue = it?.results?.responseValue; + if (!responseTime || !responseValue) return acc; + + const key = it.analysisName; + const cur = acc[key]; + const t = Date.parse(responseTime); + const prevT = cur ? Date.parse(cur.responseTime) : -Infinity; + + if (!cur || t > prevT) { + acc[key] = { + name: key, + value: responseValue.toString(), + responseTime, + }; } - } - return [...map.values()]; + return acc; + }, {}); + + return Object.values(byName); }; async function recommendationsLoader( @@ -80,6 +69,9 @@ async function recommendationsLoader( analyses: OrderAnalysisCard[], account: AccountWithParams | null, ): Promise { + if (!process.env.OPENAI_API_KEY) { + return []; + } if (!account?.personal_code) { return []; } @@ -90,11 +82,11 @@ async function recommendationsLoader( console.log('analyises', analyses); const latestUniqueAnalysResponses = getLatestUniqueAnalysResponses(analysisResponses); - const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); - const latestISO = latestResponseTime - ? new Date(latestResponseTime).toISOString() - : 'none'; - console.log('latestResponseTime', latestResponseTime); + // const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses); + // const latestISO = latestResponseTime + // ? new Date(latestResponseTime).toISOString() + // : 'none'; + // console.log('latestResponseTime', latestResponseTime); const formattedAnalysisResponses = latestUniqueAnalysResponses.map( ({ name, value }) => ({ name, value }), ); @@ -108,8 +100,9 @@ async function recommendationsLoader( 'latestUniqueAnalysResponses', JSON.stringify(latestUniqueAnalysResponses), ); + const response = await client.responses.create({ - model: 'gpt-5', + model: 'gpt-5-mini', store: false, prompt: { id: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', @@ -121,33 +114,7 @@ async function recommendationsLoader( }, }); - const responseJson = JSON.parse(response.output_text); - console.log('responseJson: ', responseJson); - - const keyPayload = { - model: 'gpt-5', // swap to a model your project can access - promptId: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd', - latestISO, - }; - - const key = 'recs:' + sha256(canonical(keyPayload)); - - const run = unstable_cache(async () => { - const response = await client.responses.create({ - model: keyPayload.model, - store: false, - prompt: { - id: keyPayload.promptId, - variables: { - analyses: JSON.stringify(formattedAnalyses), - results: JSON.stringify(latestUniqueAnalysResponses), - gender: gender.value, - }, - }, - }); - const json = JSON.parse(response.output_text); - return json.recommended; - }, ['recommendations', key]); - - return await run(); + const json = JSON.parse(response.output_text); + console.log('response.output_text', response.output_text); + return json.recommended; }