send analyses to ai
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { toTitleCase } from '@/lib/utils';
|
import { toTitleCase } from '@/lib/utils';
|
||||||
@@ -13,10 +15,8 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
import Dashboard from '../_components/dashboard';
|
import Dashboard from '../_components/dashboard';
|
||||||
import DashboardCards from '../_components/dashboard-cards';
|
import DashboardCards from '../_components/dashboard-cards';
|
||||||
import Recommendations from '../_components/recommendations';
|
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 { loadCurrentUserAccount } from '../_lib/server/load-user-account';
|
||||||
import { analysisResponses } from './analysis-results/test/test-responses';
|
import Loading from '../loading';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
@@ -28,21 +28,11 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function UserHomePage() {
|
async function UserHomePage() {
|
||||||
console.log('process.env', process.env.OPENAI_API_KEY);
|
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const { account } = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
const api = createUserAnalysesApi(client);
|
const api = createUserAnalysesApi(client);
|
||||||
const bmiThresholds = await api.fetchBmiThresholds();
|
const bmiThresholds = await api.fetchBmiThresholds();
|
||||||
const { analyses, countryCode } = await loadAnalyses();
|
|
||||||
|
|
||||||
const analysisRecommendations = await loadRecommendations(
|
|
||||||
analysisResponses,
|
|
||||||
analyses,
|
|
||||||
account,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('analysisRecommendations', analysisRecommendations);
|
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
redirect('/');
|
redirect('/');
|
||||||
@@ -62,13 +52,9 @@ async function UserHomePage() {
|
|||||||
/>
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Dashboard account={account} bmiThresholds={bmiThresholds} />
|
<Dashboard account={account} bmiThresholds={bmiThresholds} />
|
||||||
|
<Suspense fallback={null}>
|
||||||
<Recommendations
|
<Recommendations account={account} />
|
||||||
recommended={analyses.filter((analysis) =>
|
</Suspense>
|
||||||
analysisRecommendations.includes(analysis.title),
|
|
||||||
)}
|
|
||||||
countryCode={countryCode}
|
|
||||||
/>
|
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
import OrderAnalysesCards, { OrderAnalysisCard } from './order-analyses-cards';
|
||||||
|
|
||||||
export default function Recommendations({
|
export default async function Recommendations({
|
||||||
recommended,
|
account,
|
||||||
countryCode,
|
|
||||||
}: {
|
}: {
|
||||||
recommended: OrderAnalysisCard[];
|
account: AccountWithParams;
|
||||||
countryCode: string;
|
|
||||||
}) {
|
}) {
|
||||||
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 null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h4>Medreport soovitab teile</h4>
|
<h4>Medreport soovitab teile</h4>
|
||||||
<OrderAnalysesCards analyses={recommended} countryCode={countryCode} />
|
<OrderAnalysesCards analyses={orderAnalyses} countryCode={countryCode} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<string, unknown>();
|
|
||||||
jest.mock('next/cache', () => {
|
|
||||||
return {
|
|
||||||
__esModule: true,
|
|
||||||
unstable_cache:
|
|
||||||
(fn: (...args: any[]) => Promise<any>, 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']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { cache } from 'react';
|
import { cache } from 'react';
|
||||||
|
|
||||||
import { unstable_cache } from 'next/cache';
|
|
||||||
|
|
||||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
||||||
import crypto from 'crypto';
|
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
|
|
||||||
import PersonalCode from '~/lib/utils';
|
import PersonalCode from '~/lib/utils';
|
||||||
@@ -19,20 +16,6 @@ type FormattedAnalysisResponse = {
|
|||||||
responseTime: string;
|
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[]) => {
|
const getLatestResponseTime = (items: FormattedAnalysisResponse[]) => {
|
||||||
if (!items?.length) return null;
|
if (!items?.length) return null;
|
||||||
|
|
||||||
@@ -56,23 +39,29 @@ const getLatestUniqueAnalysResponses = (
|
|||||||
|
|
||||||
console.log('analysisElements', analysisElements.length);
|
console.log('analysisElements', analysisElements.length);
|
||||||
|
|
||||||
const map = new Map();
|
const byName = analysisElements.reduce<
|
||||||
for (const it of analysisElements) {
|
Record<string, { name: string; value: string; responseTime: string }>
|
||||||
const prev = map.get(it.analysisName);
|
>((acc, it) => {
|
||||||
if (it.results.responseTime) {
|
const responseTime = it?.results?.responseTime;
|
||||||
if (
|
const responseValue = it?.results?.responseValue;
|
||||||
!prev ||
|
if (!responseTime || !responseValue) return acc;
|
||||||
new Date(it.results.responseTime) > new Date(prev.responseTime)
|
|
||||||
) {
|
const key = it.analysisName;
|
||||||
map.set(it.analysisName, {
|
const cur = acc[key];
|
||||||
name: it.analysisName,
|
const t = Date.parse(responseTime);
|
||||||
value: it.results.responseValue,
|
const prevT = cur ? Date.parse(cur.responseTime) : -Infinity;
|
||||||
responseTime: it.results.responseTime,
|
|
||||||
});
|
if (!cur || t > prevT) {
|
||||||
|
acc[key] = {
|
||||||
|
name: key,
|
||||||
|
value: responseValue.toString(),
|
||||||
|
responseTime,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return acc;
|
||||||
}
|
}, {});
|
||||||
return [...map.values()];
|
|
||||||
|
return Object.values(byName);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function recommendationsLoader(
|
async function recommendationsLoader(
|
||||||
@@ -80,6 +69,9 @@ async function recommendationsLoader(
|
|||||||
analyses: OrderAnalysisCard[],
|
analyses: OrderAnalysisCard[],
|
||||||
account: AccountWithParams | null,
|
account: AccountWithParams | null,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
if (!process.env.OPENAI_API_KEY) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
if (!account?.personal_code) {
|
if (!account?.personal_code) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -90,11 +82,11 @@ async function recommendationsLoader(
|
|||||||
console.log('analyises', analyses);
|
console.log('analyises', analyses);
|
||||||
const latestUniqueAnalysResponses =
|
const latestUniqueAnalysResponses =
|
||||||
getLatestUniqueAnalysResponses(analysisResponses);
|
getLatestUniqueAnalysResponses(analysisResponses);
|
||||||
const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses);
|
// const latestResponseTime = getLatestResponseTime(latestUniqueAnalysResponses);
|
||||||
const latestISO = latestResponseTime
|
// const latestISO = latestResponseTime
|
||||||
? new Date(latestResponseTime).toISOString()
|
// ? new Date(latestResponseTime).toISOString()
|
||||||
: 'none';
|
// : 'none';
|
||||||
console.log('latestResponseTime', latestResponseTime);
|
// console.log('latestResponseTime', latestResponseTime);
|
||||||
const formattedAnalysisResponses = latestUniqueAnalysResponses.map(
|
const formattedAnalysisResponses = latestUniqueAnalysResponses.map(
|
||||||
({ name, value }) => ({ name, value }),
|
({ name, value }) => ({ name, value }),
|
||||||
);
|
);
|
||||||
@@ -108,8 +100,9 @@ async function recommendationsLoader(
|
|||||||
'latestUniqueAnalysResponses',
|
'latestUniqueAnalysResponses',
|
||||||
JSON.stringify(latestUniqueAnalysResponses),
|
JSON.stringify(latestUniqueAnalysResponses),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await client.responses.create({
|
const response = await client.responses.create({
|
||||||
model: 'gpt-5',
|
model: 'gpt-5-mini',
|
||||||
store: false,
|
store: false,
|
||||||
prompt: {
|
prompt: {
|
||||||
id: 'pmpt_68ca9c8bfa8c8193b27eadc6496c36440df449ece4f5a8dd',
|
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);
|
const json = JSON.parse(response.output_text);
|
||||||
|
console.log('response.output_text', response.output_text);
|
||||||
return json.recommended;
|
return json.recommended;
|
||||||
}, ['recommendations', key]);
|
|
||||||
|
|
||||||
return await run();
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user