From 3d213d1fbf2a3c4a5797f1f9977425a05015898b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 18 Jun 2026 19:07:38 +0200 Subject: [PATCH] feat(insights): turn the no-provider dead-ends into guided setup Coach, the daily briefing, and the per-metric assessment tiles all default ON but need a connected AI provider that onboarding never mentions, so a fresh user hits a bare error on the headline feature. - The daily-briefing and per-metric no-provider states now explain what the AI does and why a provider is needed (your own key or a local model, data stays with you) rather than a one-line 'unavailable'. - The per-metric tile gains a direct Settings link. - The Coach 'no provider configured' turn maps to its own copy with a Settings link instead of the transient 'try again in a moment' that would 422 forever. Adds insights.noProviderAction, insights.coach.errorNoProvider and errorNoProviderAction across all six locales. --- messages/de.json | 7 +++++-- messages/en.json | 7 +++++-- messages/es.json | 7 +++++-- messages/fr.json | 7 +++++-- messages/it.json | 7 +++++-- messages/pl.json | 7 +++++-- .../__tests__/insight-status-card.test.tsx | 6 +++++- .../__tests__/message-thread.test.tsx | 9 +++++++- .../insights/coach-panel/message-thread.tsx | 21 ++++++++++++++++++- .../insights/insight-status-card.tsx | 12 +++++++++-- 10 files changed, 73 insertions(+), 17 deletions(-) diff --git a/messages/de.json b/messages/de.json index 36c227c57..dc5188ff6 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1892,7 +1892,8 @@ "bmiEmptyAction": "Account-Einstellungen öffnen", "noAnalysisYet": "Noch keine Einschätzung vorhanden.", "assessmentPreparing": "Einschätzung wird vorbereitet…", - "noProviderConfigured": "Einschätzung nicht verfügbar.", + "noProviderConfigured": "Verbinde einen KI-Anbieter, um hier eine Einschätzung zu deinen Werten zu sehen.", + "noProviderAction": "Zu den KI-Einstellungen", "startAnalysis": "Einschätzung starten", "refreshAnalysis": "Einschätzung aktualisieren", "keyTakeaway": "Das Wichtigste", @@ -2007,7 +2008,7 @@ "emptyAction": "Briefing erstellen", "loadingLabel": "Briefing wird zusammengestellt", "noProviderTitle": "Kein KI-Anbieter verbunden", - "noProviderDescription": "Verbinde einen KI-Anbieter in den Einstellungen, um tägliche Briefings zu erhalten.", + "noProviderDescription": "Das Tagesbriefing fasst deine Werte in Klartext zusammen. Dafür braucht HealthLog einen KI-Anbieter — nutze deinen eigenen Schlüssel (OpenAI, Anthropic) oder ein lokales Modell. Deine Daten bleiben dabei bei dir.", "noProviderAction": "Zu den KI-Einstellungen" }, "trendsRow": { @@ -2072,6 +2073,8 @@ "refusalInjection": "In deiner Nachricht stehen Anweisungen, die meine Vorgaben überschreiben sollen. Ich bin der HealthLog-Coach und fasse ausschließlich deine eigenen Gesundheitsdaten zusammen — formuliere deine Frage bitte in diesem Rahmen neu.", "errorBudget": "Du hast das heutige Coach-Limit erreicht. Das Budget setzt sich um Mitternacht (UTC) zurück.", "errorProvider": "Der Coach konnte gerade keinen Auswertungs-Anbieter erreichen. Bitte versuche es in einem Moment erneut.", + "errorNoProvider": "Der Coach liest deine erfassten Gesundheitsdaten und beantwortet Fragen dazu (keine medizinische Beratung). Dafür braucht HealthLog einen KI-Anbieter — verbinde deinen eigenen Schlüssel oder ein lokales Modell.", + "errorNoProviderAction": "KI-Anbieter verbinden", "errorNetwork": "Keine Internetverbindung — versuche es erneut, sobald du online bist.", "prefsSaveError": "Coach-Einstellungen konnten nicht gespeichert werden. Bitte versuche es erneut.", "errorCredentialExpired": "Deine Verbindung zum KI-Anbieter ist abgelaufen. Stelle sie in den Einstellungen wieder her, damit der Coach weiterläuft.", diff --git a/messages/en.json b/messages/en.json index 18cf5182a..2cc1234f7 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1892,7 +1892,8 @@ "bmiEmptyTitle": "Set your height to see BMI", "bmiEmptyDescription": "BMI needs your height. Add it under Settings → Account.", "bmiEmptyAction": "Open account settings", - "noProviderConfigured": "Assessment unavailable.", + "noProviderConfigured": "Connect an AI provider to see an assessment of your readings here.", + "noProviderAction": "Open AI settings", "startAnalysis": "Start assessment", "refreshAnalysis": "Refresh assessment", "keyTakeaway": "Key Takeaway", @@ -2007,7 +2008,7 @@ "emptyAction": "Generate briefing", "loadingLabel": "Synthesising today's briefing", "noProviderTitle": "No AI provider connected", - "noProviderDescription": "Connect an AI provider in settings to receive daily briefings.", + "noProviderDescription": "The daily briefing turns your readings into plain-language summary. HealthLog needs an AI provider for that — use your own key (OpenAI, Anthropic) or a local model. Your data stays with you.", "noProviderAction": "Open AI settings" }, "trendsRow": { @@ -2072,6 +2073,8 @@ "refusalInjection": "I noticed wording that tries to override my instructions. I am the HealthLog Coach and only summarise your own health data — please rephrase your question in those terms.", "errorBudget": "You've reached today's Coach usage limit. The budget refreshes at midnight UTC.", "errorProvider": "The Coach could not reach an Insights provider just now. Please try again in a moment.", + "errorNoProvider": "The Coach reads your logged health data and answers questions about it (not medical advice). HealthLog needs an AI provider for that — connect your own key or a local model.", + "errorNoProviderAction": "Connect an AI provider", "errorNetwork": "No internet connection — try again once you're back online.", "prefsSaveError": "Couldn't save your Coach preferences. Please try again.", "errorCredentialExpired": "Your AI provider connection has expired. Reconnect it in Settings to keep the Coach running.", diff --git a/messages/es.json b/messages/es.json index 1b2e2c7a0..649638cc1 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1892,7 +1892,8 @@ "bmiEmptyTitle": "Añade tu altura para calcular el IMC", "bmiEmptyDescription": "El IMC necesita tu altura. Guárdala en Ajustes → Cuenta.", "bmiEmptyAction": "Abrir ajustes de cuenta", - "noProviderConfigured": "Valoración no disponible.", + "noProviderConfigured": "Conecta un proveedor de IA para ver aquí una valoración de tus lecturas.", + "noProviderAction": "Abrir ajustes de IA", "startAnalysis": "Iniciar valoración", "refreshAnalysis": "Actualizar valoración", "keyTakeaway": "Lo más importante", @@ -2007,7 +2008,7 @@ "emptyAction": "Generar resumen", "loadingLabel": "Preparando el resumen", "noProviderTitle": "Ningún proveedor de IA conectado", - "noProviderDescription": "Conecta un proveedor de IA en los ajustes para recibir resúmenes diarios.", + "noProviderDescription": "El briefing diario resume tus lecturas en lenguaje claro. HealthLog necesita un proveedor de IA para ello: usa tu propia clave (OpenAI, Anthropic) o un modelo local. Tus datos siguen siendo tuyos.", "noProviderAction": "Abrir ajustes de IA" }, "trendsRow": { @@ -2072,6 +2073,8 @@ "refusalInjection": "Tu mensaje contiene instrucciones que pretenden sobrescribir mis indicaciones. Soy el coach de HealthLog y solo resumo tus propios datos de salud — reformula tu pregunta dentro de ese marco.", "errorBudget": "Has alcanzado el límite diario del coach. El cupo se reinicia a medianoche (UTC).", "errorProvider": "El coach no ha podido contactar con un proveedor de análisis ahora mismo. Inténtalo en un momento.", + "errorNoProvider": "El coach lee tus datos de salud registrados y responde preguntas sobre ellos (no es consejo médico). HealthLog necesita un proveedor de IA para ello: conecta tu propia clave o un modelo local.", + "errorNoProviderAction": "Conectar un proveedor de IA", "errorNetwork": "Sin conexión a Internet — inténtalo de nuevo cuando vuelvas a estar en línea.", "prefsSaveError": "No se pudieron guardar tus preferencias del Coach. Inténtalo de nuevo.", "errorCredentialExpired": "Tu conexión con el proveedor de IA ha caducado. Vuelve a conectarla en Ajustes para que el Coach siga funcionando.", diff --git a/messages/fr.json b/messages/fr.json index e278cc028..9fe484ea4 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1892,7 +1892,8 @@ "bmiEmptyTitle": "Ajoute ta taille pour l’IMC", "bmiEmptyDescription": "L’IMC nécessite ta taille. Renseigne-la dans Paramètres → Compte.", "bmiEmptyAction": "Ouvrir les paramètres du compte", - "noProviderConfigured": "Évaluation indisponible.", + "noProviderConfigured": "Connecte un fournisseur d'IA pour voir ici une évaluation de tes mesures.", + "noProviderAction": "Ouvrir les réglages IA", "startAnalysis": "Lancer l’évaluation", "refreshAnalysis": "Actualiser l’évaluation", "keyTakeaway": "L’essentiel", @@ -2007,7 +2008,7 @@ "emptyAction": "Générer le briefing", "loadingLabel": "Préparation du briefing", "noProviderTitle": "Aucun fournisseur d'IA connecté", - "noProviderDescription": "Connecte un fournisseur d'IA dans les réglages pour recevoir des briefings quotidiens.", + "noProviderDescription": "Le briefing quotidien résume tes mesures en langage clair. HealthLog a besoin d'un fournisseur d'IA pour cela : utilise ta propre clé (OpenAI, Anthropic) ou un modèle local. Tes données restent chez toi.", "noProviderAction": "Ouvrir les réglages IA" }, "trendsRow": { @@ -2072,6 +2073,8 @@ "refusalInjection": "Ton message contient des instructions qui cherchent à remplacer mes consignes. Je suis le coach HealthLog et je ne résume que tes propres données de santé — reformule ta question dans ce cadre.", "errorBudget": "Tu as atteint la limite quotidienne du coach. Le quota se réinitialise à minuit (UTC).", "errorProvider": "Le coach n’a pas pu joindre un fournisseur d’analyse maintenant. Réessaie dans un instant.", + "errorNoProvider": "Le coach lit tes données de santé enregistrées et répond aux questions à leur sujet (pas un avis médical). HealthLog a besoin d'un fournisseur d'IA pour cela : connecte ta propre clé ou un modèle local.", + "errorNoProviderAction": "Connecter un fournisseur d'IA", "errorNetwork": "Pas de connexion Internet — réessaie une fois reconnecté.", "prefsSaveError": "Impossible d’enregistrer vos préférences du Coach. Réessayez.", "errorCredentialExpired": "Ta connexion au fournisseur d'IA a expiré. Reconnecte-la dans les Réglages pour que le Coach continue de fonctionner.", diff --git a/messages/it.json b/messages/it.json index e5fb79eb1..a50e007a6 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1892,7 +1892,8 @@ "bmiEmptyTitle": "Aggiungi l’altezza per il BMI", "bmiEmptyDescription": "Per il BMI serve la tua altezza. Salvala in Impostazioni → Account.", "bmiEmptyAction": "Apri impostazioni account", - "noProviderConfigured": "Valutazione non disponibile.", + "noProviderConfigured": "Collega un provider IA per vedere qui una valutazione delle tue misurazioni.", + "noProviderAction": "Apri impostazioni IA", "startAnalysis": "Avvia valutazione", "refreshAnalysis": "Aggiorna valutazione", "keyTakeaway": "Ciò che conta", @@ -2007,7 +2008,7 @@ "emptyAction": "Genera briefing", "loadingLabel": "Preparazione del briefing", "noProviderTitle": "Nessun provider IA collegato", - "noProviderDescription": "Collega un provider IA nelle impostazioni per ricevere briefing giornalieri.", + "noProviderDescription": "Il briefing giornaliero riassume le tue misurazioni in linguaggio chiaro. HealthLog ha bisogno di un provider IA per questo: usa la tua chiave (OpenAI, Anthropic) o un modello locale. I tuoi dati restano tuoi.", "noProviderAction": "Apri impostazioni IA" }, "trendsRow": { @@ -2072,6 +2073,8 @@ "refusalInjection": "Nel tuo messaggio ci sono istruzioni che cercano di sovrascrivere le mie indicazioni. Sono il coach di HealthLog e riassumo solo i tuoi dati di salute — riformula la domanda in questo perimetro.", "errorBudget": "Hai raggiunto il limite giornaliero del coach. Il budget si reimposta a mezzanotte (UTC).", "errorProvider": "Il coach non è riuscito a contattare un provider di analisi ora. Riprova fra poco.", + "errorNoProvider": "Il coach legge i tuoi dati di salute registrati e risponde a domande su di essi (non è consulenza medica). HealthLog ha bisogno di un provider IA per questo: collega la tua chiave o un modello locale.", + "errorNoProviderAction": "Collega un provider IA", "errorNetwork": "Nessuna connessione Internet — riprova quando sei di nuovo online.", "prefsSaveError": "Impossibile salvare le preferenze del Coach. Riprova.", "errorCredentialExpired": "La connessione al provider di IA è scaduta. Riconnettila nelle Impostazioni per mantenere attivo il Coach.", diff --git a/messages/pl.json b/messages/pl.json index f09755820..b3353ef57 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1892,7 +1892,8 @@ "bmiEmptyTitle": "Dodaj wzrost, aby obliczyć BMI", "bmiEmptyDescription": "BMI wymaga twojego wzrostu. Zapisz go w Ustawieniach → Konto.", "bmiEmptyAction": "Otwórz ustawienia konta", - "noProviderConfigured": "Ocena niedostępna.", + "noProviderConfigured": "Połącz dostawcę AI, aby zobaczyć tutaj ocenę swoich pomiarów.", + "noProviderAction": "Otwórz ustawienia AI", "startAnalysis": "Uruchom ocenę", "refreshAnalysis": "Odśwież ocenę", "keyTakeaway": "Najważniejsze", @@ -2007,7 +2008,7 @@ "emptyAction": "Wygeneruj briefing", "loadingLabel": "Przygotowanie briefingu", "noProviderTitle": "Brak połączonego dostawcy AI", - "noProviderDescription": "Połącz dostawcę AI w ustawieniach, aby otrzymywać codzienne briefingi.", + "noProviderDescription": "Codzienny briefing podsumowuje Twoje pomiary prostym językiem. HealthLog potrzebuje do tego dostawcy AI — użyj własnego klucza (OpenAI, Anthropic) lub modelu lokalnego. Twoje dane pozostają u Ciebie.", "noProviderAction": "Otwórz ustawienia AI" }, "trendsRow": { @@ -2072,6 +2073,8 @@ "refusalInjection": "Twoja wiadomość zawiera instrukcje, które próbują nadpisać moje wytyczne. Jestem coachem HealthLog i podsumowuję wyłącznie twoje własne dane zdrowotne — przeformułuj pytanie w tych ramach.", "errorBudget": "Osiągnięto dzienny limit coacha. Limit resetuje się o północy (UTC).", "errorProvider": "Coach nie mógł teraz skontaktować się z dostawcą analizy. Spróbuj za chwilę.", + "errorNoProvider": "Coach czyta Twoje zapisane dane zdrowotne i odpowiada na pytania o nie (to nie porada medyczna). HealthLog potrzebuje do tego dostawcy AI — połącz własny klucz lub model lokalny.", + "errorNoProviderAction": "Połącz dostawcę AI", "errorNetwork": "Brak połączenia z Internetem — spróbuj ponownie, gdy wrócisz online.", "prefsSaveError": "Nie udało się zapisać ustawień Coacha. Spróbuj ponownie.", "errorCredentialExpired": "Połączenie z dostawcą AI wygasło. Połącz je ponownie w Ustawieniach, aby Coach działał dalej.", diff --git a/src/components/insights/__tests__/insight-status-card.test.tsx b/src/components/insights/__tests__/insight-status-card.test.tsx index 6ecabe3fc..d55bf0954 100644 --- a/src/components/insights/__tests__/insight-status-card.test.tsx +++ b/src/components/insights/__tests__/insight-status-card.test.tsx @@ -80,7 +80,11 @@ describe("", () => { text="ignored when hasProvider is false" />, ); - expect(html).toContain("Assessment unavailable."); + // v1.18.6 — the no-provider tile is a guided-setup explainer + a + // Settings link, not a bare "unavailable" line. + expect(html).toContain("Connect an AI provider"); + expect(html).toContain("Open AI settings"); + expect(html).toContain("/settings/ai"); expect(html).not.toContain("ignored when hasProvider is false"); }); diff --git a/src/components/insights/coach-panel/__tests__/message-thread.test.tsx b/src/components/insights/coach-panel/__tests__/message-thread.test.tsx index fc6cea16c..e09507a2f 100644 --- a/src/components/insights/coach-panel/__tests__/message-thread.test.tsx +++ b/src/components/insights/coach-panel/__tests__/message-thread.test.tsx @@ -662,13 +662,20 @@ describe("errorCodeToI18nKey", () => { for (const code of [ "coach.provider.unavailable", "coach.provider.empty", - "coach.provider.none", "coach.stream", ]) { expect(errorCodeToI18nKey(code)).toBe("insights.coach.errorProvider"); } }); + // v1.18.6 — a "no provider configured anywhere" turn is a setup gap, + // not a transient failure, so it carries its own guided-setup copy. + it("maps the no-provider case to its dedicated guided-setup copy", () => { + expect(errorCodeToI18nKey("coach.provider.none")).toBe( + "insights.coach.errorNoProvider", + ); + }); + // v1.4.43 QoL (M6) — `coach.network` no longer collapses to the // generic provider copy; the user needs the actionable offline hint. it("maps coach.network to the dedicated offline key", () => { diff --git a/src/components/insights/coach-panel/message-thread.tsx b/src/components/insights/coach-panel/message-thread.tsx index 59f0755e2..75a632f59 100644 --- a/src/components/insights/coach-panel/message-thread.tsx +++ b/src/components/insights/coach-panel/message-thread.tsx @@ -1,6 +1,7 @@ "use client"; import { Fragment, useEffect, useId, useMemo, useRef, useState } from "react"; +import Link from "next/link"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; import { @@ -168,9 +169,14 @@ export function errorCodeToI18nKey(code: string): string { // (auth-class failure). The next action is "reconnect", not "try // again later", so it carries its own copy pointing at Settings. return "insights.coach.errorCredentialExpired"; + case "coach.provider.none": + // v1.18.6 — no AI provider is configured anywhere, so "try again + // in a moment" is the wrong instruction (it will 422 forever). Send + // the user to guided setup with a one-line explainer + a Settings + // link rendered alongside the bubble copy. + return "insights.coach.errorNoProvider"; case "coach.provider.unavailable": case "coach.provider.empty": - case "coach.provider.none": case "coach.stream": return "insights.coach.errorProvider"; default: @@ -613,6 +619,19 @@ function ChatBubble({ {safeError && content && (

{safeError}

)} + {/* v1.18.6 — a "no provider configured anywhere" turn is a + setup gap, not a transient failure: surface a direct link to + Settings → AI so the Coach guides the user into BYOK / local + setup instead of inviting an endless retry. */} + {errorCode === "coach.provider.none" && ( + + {t("insights.coach.errorNoProviderAction")} + + )} {/* v1.12.0 — collapse the whole provenance block ("what was included") behind one disclosure, collapsed by default. The source chips used to render fully expanded above the diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx index b0a5b2dfc..a4682ed07 100644 --- a/src/components/insights/insight-status-card.tsx +++ b/src/components/insights/insight-status-card.tsx @@ -1,6 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; +import Link from "next/link"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; @@ -144,14 +145,21 @@ export function InsightStatusCard({ if (!hasProvider) { return ( - + - +

{t("insights.noProviderConfigured")}

+ + {t("insights.noProviderAction")} +
);