diff --git a/messages/de.json b/messages/de.json index be3cf59b6..c690c5de5 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2324,6 +2324,33 @@ "groupYesterday": "Gestern", "groupThisWeek": "Diese Woche", "groupEarlier": "Früher" + }, + "seed": { + "bloodPressure": "Geh mit mir meinen Blutdruckverlauf der letzten 30 Tage durch — gibt es etwas, das ich im Auge behalten sollte?", + "weight": "Wie hat sich mein Gewicht zuletzt entwickelt, und ist die Richtung etwas, worauf ich reagieren sollte?", + "bmi": "Was sagt mir mein BMI-Verlauf, und wie sollte ich ihn lesen?", + "pulse": "Geh mit mir meine Pulswerte durch — ist irgendetwas ungewöhnlich?", + "restingHr": "Wie hat sich mein Ruhepuls entwickelt, und was sagt er über meine Fitness?", + "hrv": "Was sagt mir meine Herzfrequenzvariabilität zuletzt über Erholung und Stress?", + "sleep": "Geh mit mir meinen letzten Schlaf durch — Dauer, Regelmäßigkeit und was sich zu ändern lohnt.", + "mood": "Welche Muster siehst du in meiner Stimmung über die letzten Wochen?", + "medications": "Wie sieht meine Medikamentenadhärenz aus, und passt sie dazu, wie ich mich gefühlt habe?", + "steps": "Wie aktiv war ich zuletzt gemessen an meinen Schritten, und wie ist das im Vergleich zu einem gesunden Richtwert?", + "activeEnergy": "Was sagt mein Aktivenergie-Verlauf über mein Aktivitätsniveau zuletzt?", + "cardioFitness": "Was bedeutet mein Cardio-Fitness-Verlauf (VO₂max), und wie bewege ich ihn in die richtige Richtung?", + "bloodGlucose": "Geh mit mir meine letzten Glukosewerte durch — liegen sie in einem gesunden Bereich?", + "oxygenSaturation": "Was sagt mir mein Sauerstoffsättigungs-Verlauf (SpO₂)?", + "respiratoryRate": "Was sagt mein Atemfrequenz-Verlauf über meine Gesundheit?", + "workouts": "Geh mit mir meine letzten Workouts durch — Belastung, Häufigkeit und wie ich mich dazwischen erhole.", + "recoveryPage": "Warum steht meine Erholung gerade dort, wo sie steht, und was treibt das?", + "briefing": "Geh mit mir das heutige Briefing durch — worauf sollte ich mich konzentrieren, und gibt es etwas, das einen Schritt wert ist?", + "healthScore": "Warum liegt mein Gesundheits-Score bei {score} von 100, und was würde ihn am stärksten bewegen?", + "periodWeek": "Geh mit mir durch, wie meine letzte Woche über meine Gesundheitswerte hinweg lief — was sticht heraus?", + "periodMonth": "Geh mit mir durch, wie mein letzter Monat über meine Gesundheitswerte hinweg lief — was sticht heraus?", + "correlationBpCompliance": "Wie hängt meine Medikamentenadhärenz mit meinem Blutdruck zusammen? Geh mit mir durch, was dieser Zusammenhang für mich bedeutet.", + "correlationMoodPulse": "Gibt es einen echten Zusammenhang zwischen meiner Stimmung und meinem Puls? Hilf mir, diesen Zusammenhang zu lesen.", + "correlationWeightWeekday": "Folgt mein Gewicht einem Wochenmuster? Was sagt mir dieser Wochentags-Zusammenhang?", + "recommendation": "Erzähl mir mehr zu dieser Empfehlung: \"{text}\" — warum trifft sie auf mich zu, und was sollte ich tun?" } }, "relativeJustNow": "gerade eben", @@ -3617,7 +3644,10 @@ "returnLabel": "Dein {metric} ist seit {count} Tagen wieder in deinem üblichen Bereich — was es auch war, es ist vorbei.", "daysInside": "{count} Tage drinnen", "daysOutside": "{count} Tage draußen" - } + }, + "updatedTodayAt": "Heute aktualisiert, {time}", + "updatedYesterday": "Gestern aktualisiert", + "updatedOn": "Aktualisiert am {date}" }, "thresholds": { "defaultLabel": "Default", diff --git a/messages/en.json b/messages/en.json index 0186d745a..9d9382cc6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2324,6 +2324,33 @@ "groupYesterday": "Yesterday", "groupThisWeek": "This week", "groupEarlier": "Earlier" + }, + "seed": { + "bloodPressure": "Walk me through my blood pressure trend over the last 30 days — anything I should keep an eye on?", + "weight": "How has my weight been trending lately, and is the direction something to act on?", + "bmi": "What does my BMI trend tell me, and how should I read it?", + "pulse": "Walk me through my pulse readings — is anything out of the ordinary?", + "restingHr": "How has my resting heart rate been trending, and what does it say about my fitness?", + "hrv": "What is my heart-rate variability telling me about recovery and stress lately?", + "sleep": "Walk me through my recent sleep — duration, consistency, and anything worth changing.", + "mood": "What patterns do you see in my mood over the last few weeks?", + "medications": "How is my medication adherence looking, and is it lined up with how I've been feeling?", + "steps": "How active have I been lately based on my steps, and how does it compare to a healthy baseline?", + "activeEnergy": "What does my active-energy trend say about my activity level lately?", + "cardioFitness": "What does my cardio fitness (VO₂ max) trend mean, and how do I move it in the right direction?", + "bloodGlucose": "Walk me through my recent glucose readings — are they in a healthy range?", + "oxygenSaturation": "What does my blood-oxygen (SpO₂) trend tell me?", + "respiratoryRate": "What does my respiratory-rate trend say about my health?", + "workouts": "Walk me through my recent workouts — load, frequency, and how I'm recovering between them.", + "recoveryPage": "Why is my recovery where it is right now, and what's driving it?", + "briefing": "Walk me through today's briefing — what should I focus on, and is anything worth acting on?", + "healthScore": "Why is my health score {score} out of 100, and what would move it the most?", + "periodWeek": "Walk me through how my last week went across my health metrics — what stands out?", + "periodMonth": "Walk me through how my last month went across my health metrics — what stands out?", + "correlationBpCompliance": "How does my medication adherence relate to my blood pressure? Walk me through what this correlation means for me.", + "correlationMoodPulse": "Is there a real link between my mood and my pulse? Help me read this correlation.", + "correlationWeightWeekday": "Does my weight follow a weekly pattern? What does this day-of-week correlation tell me?", + "recommendation": "Tell me more about this recommendation: \"{text}\" — why does it apply to me, and what should I do about it?" } }, "relativeJustNow": "just now", @@ -3617,7 +3644,10 @@ "returnLabel": "Your {metric} has been back inside your usual range for {count} days now — whatever that was, it's passed.", "daysInside": "{count} days inside", "daysOutside": "{count} days outside" - } + }, + "updatedTodayAt": "Updated today, {time}", + "updatedYesterday": "Updated yesterday", + "updatedOn": "Updated on {date}" }, "thresholds": { "defaultLabel": "Default", diff --git a/src/app/api/insights/chat/route.ts b/src/app/api/insights/chat/route.ts index 8ba4f7e7f..f43d84ec0 100644 --- a/src/app/api/insights/chat/route.ts +++ b/src/app/api/insights/chat/route.ts @@ -72,6 +72,11 @@ import { coachOutboundFallback, } from "@/lib/ai/coach/outbound-guard"; import { getCoachSystemPrompt } from "@/lib/ai/coach/system-prompt"; +import { + openerArchetypeHint, + shouldUseNameForTurn, + firstNameFromDisplayName, +} from "@/lib/ai/prompts/opener-archetype"; import { getSelfContextTextForUser } from "@/lib/ai/coach/about-me"; import { buildCoachSnapshot } from "@/lib/ai/coach/snapshot"; import { @@ -366,7 +371,7 @@ async function handleChatRequest(request: NextRequest): Promise { // we don't accidentally widen narrow per-call scopes. const prefsRow = await prisma.user.findUnique({ where: { id: userId }, - select: { coachPrefsJson: true }, + select: { coachPrefsJson: true, displayName: true }, }); const coachPrefs = parseCoachPrefs(prefsRow?.coachPrefsJson); const effectiveScope = @@ -386,7 +391,19 @@ async function handleChatRequest(request: NextRequest): Promise { // teaches the model to emit `---REMEMBER---` (durable "remind me" capture), // one teaches the closed `---SUGGEST-ACTION---` confirm-card allowlist. Both // are provider-neutral sentinels stripped from the prose before it streams. - const systemPrompt = `${getCoachSystemPrompt(locale, coachPrefs, aboutMe)}\n\n${buildRememberAddendum(locale)}\n\n${buildSuggestActionAddendum(locale)}`; + // v1.22 (W6) — per-turn personalization: a sparse, hash-gated first name and + // an opener-archetype hint so multi-turn sessions vary. The turn index is the + // count of prior turns, so the name surfaces on ~1-in-3 turns, varied and + // never on a fixed cadence; both omit cleanly when no display name is set. + const turnIndex = priorTurns.length; + const firstName = firstNameFromDisplayName(prefsRow?.displayName ?? null); + const coachPersonalization = { + firstName, + mayUseName: + firstName != null && shouldUseNameForTurn(`${userId}:${turnIndex}`), + openerHint: openerArchetypeHint(`${userId}:${turnIndex}`, locale), + }; + const systemPrompt = `${getCoachSystemPrompt(locale, coachPrefs, aboutMe, coachPersonalization)}\n\n${buildRememberAddendum(locale)}\n\n${buildSuggestActionAddendum(locale)}`; const allTurns: CoachTurn[] = [ ...priorTurns, { role: "user", content: message }, diff --git a/src/app/api/insights/generate/route.ts b/src/app/api/insights/generate/route.ts index 849e7ad85..72c5af606 100644 --- a/src/app/api/insights/generate/route.ts +++ b/src/app/api/insights/generate/route.ts @@ -34,7 +34,10 @@ import { buildUserPrompt, type ComparisonSnapshot, } from "@/lib/ai/prompts/insight-system-prompt"; -import { buildSystemPromptWithReferences } from "@/lib/ai/prompts/insight-generator"; +import { + buildSystemPromptWithReferences, + buildBriefingPersonalisationBlock, +} from "@/lib/ai/prompts/insight-generator"; import { buildAboutMeInsightBlock, getSelfContextTextForUser, @@ -398,6 +401,8 @@ export const POST = apiHandler((request: NextRequest) => dateOfBirth: true, gender: true, heightCm: true, + // v1.22 (W6) — first name for the sparse, hash-gated briefing opener. + displayName: true, }, }); @@ -666,6 +671,14 @@ export const POST = apiHandler((request: NextRequest) => userPrompt += buildBriefingIllnessCyclePrompt(illnessCycleCtx, locale); } + // v1.22 (W6) — opener-archetype rotation + sparse first-name personalization + // (deterministic per user+day; omitted for unnamed accounts). + userPrompt += buildBriefingPersonalisationBlock( + userId, + dbUser?.displayName ?? null, + locale, + ); + // v1.12.7 (B5) — inject the curated SOURCES block for the metric sections // this briefing actually carries, so a normative claim can cite a real // `referenceId` the schema + UI footnote already support. Returns the plain @@ -826,6 +839,7 @@ export const POST = apiHandler((request: NextRequest) => let ungrounded = findUngroundedBriefingNumbers( readBriefingBlock(insights), signals, + features, ); if (ungrounded.length > 0) { annotate({ @@ -855,6 +869,7 @@ export const POST = apiHandler((request: NextRequest) => ungrounded = findUngroundedBriefingNumbers( readBriefingBlock(retryInsights), signals, + features, ); if (ungrounded.length === 0) { insights = retryInsights; diff --git a/src/components/insights/__tests__/daily-briefing.test.tsx b/src/components/insights/__tests__/daily-briefing.test.tsx index be94ffffb..10496b20b 100644 --- a/src/components/insights/__tests__/daily-briefing.test.tsx +++ b/src/components/insights/__tests__/daily-briefing.test.tsx @@ -119,13 +119,15 @@ describe("", () => { expect(html).toContain("bg-info"); // tone=info bar }); - it("renders the 'Generated