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.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 19:07:38 +02:00
1 parent 63bca26177
commit 3d213d1fbf
10 files changed
+73 -17

No files matched your search

+5 -2
View File
@@ -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.",
+5 -2
View File
@@ -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.",
+5 -2
View File
@@ -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.",
+5 -2
View File
@@ -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.",
+5 -2
View File
@@ -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.",
+5 -2
View File
@@ -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.",
@@ -80,7 +80,11 @@ describe("<InsightStatusCard>", () => {
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");
});
@@ -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", () => {
@@ -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 && (
<p className="text-warning/90 text-xs">{safeError}</p>
)}
{/* 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" && (
<Link
href="/settings/ai"
data-slot="coach-no-provider-cta"
className="text-primary text-xs font-medium underline-offset-4 hover:underline"
>
{t("insights.coach.errorNoProviderAction")}
</Link>
)}
{/* 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
@@ -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 (
<Card className="gap-1.5 py-4 opacity-60 md:py-5">
<Card className="gap-1.5 py-4 opacity-80 md:py-5">
<CardHeader className="pb-1">
<TileHeader icon={nodeIcon(icon)} title={title} />
</CardHeader>
<CardContent>
<CardContent className="flex flex-col gap-1.5">
<p className="text-muted-foreground text-sm">
{t("insights.noProviderConfigured")}
</p>
<Link
href="/settings/ai"
data-slot="insight-status-no-provider-cta"
className="text-primary text-sm font-medium underline-offset-4 hover:underline"
>
{t("insights.noProviderAction")}
</Link>
</CardContent>
</Card>
);