diff --git a/messages/de.json b/messages/de.json index 36c227c57..21ee4c4f2 100644 --- a/messages/de.json +++ b/messages/de.json @@ -414,7 +414,7 @@ }, "measurements": { "title": "Messungen", - "subtitle": "Gewicht, Blutdruck, Puls und mehr", + "subtitle": "Werte erfassen und die vollständige Historie einsehen. Trends und Auswertungen findest du unter Auswertungen.", "addMeasurement": "Hinzufügen", "editMeasurement": "Messwert bearbeiten", "type": "Typ", @@ -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", @@ -1965,6 +1966,7 @@ "progressAria": "Gesundheitsscore-Fortschritt", "deltaVsLastWeek": "{delta} im Vergleich zur Vorwoche", "deltaUnavailable": "Noch keine Historie — schau nächste Woche wieder vorbei.", + "restModePaused": "Während einer Krankheitsphase pausiert — dein Score wird heute nicht bewertet.", "componentBp": "Blutdruck", "componentWeight": "Gewicht", "componentMood": "Stimmung", @@ -2007,7 +2009,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 +2074,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.", @@ -2709,6 +2713,7 @@ "title": "Glukose-Überblick", "subtitle": "Ein Blick auf deine letzten {days} Tage, aus {count} Messungen.", "spotCaveat": "Das sind Schätzungen aus einzelnen Stichproben-Messungen, kein Profil eines kontinuierlichen Sensors – lies sie als Richtung, nicht als klinisches Urteil.", + "labsLink": "Laborergebnisse? HbA1c und Nüchternglukose aus einer Blutabnahme trägst du unter Labor ein.", "learningTitle": "Lerne deine Glukose noch kennen", "learningBody": "Bisher sind das {count} über {days}. Mit etwas mehr Verlauf wird das Bild klarer – mess weiter oder lass es ein verbundenes Gerät ergänzen.", "learningBodyEmpty": "Sobald ein paar Messungen vorliegen – von dir erfasst oder von einem Gerät synchronisiert – nehmen Zeit-im-Zielbereich, Durchschnitt und Schwankung hier Gestalt an.", @@ -3623,7 +3628,10 @@ "learning": "Manche Einblicke – Glukose, Schlafrhythmus, Blutdrucktrends, Zyklus – schärfen sich in den ersten ein bis zwei Wochen, sobald sich deine Daten ansammeln. Leere Kacheln heißen jetzt nur, dass wir deine Basislinie noch lernen.", "connectCta": "Gerät verbinden", "logCta": "Ersten Wert eintragen", - "importCta": "Bestehende Daten importieren" + "importCta": "Bestehende Daten importieren", + "aiTitle": "KI-Einblicke aktivieren (optional)", + "aiBody": "Coach, Tagesbriefing und Einschätzungen fassen deine Werte in Klartext zusammen. Dafür verbindest du deinen eigenen KI-Schlüssel (OpenAI, Anthropic) oder ein lokales Modell — deine Daten bleiben bei dir.", + "aiCta": "KI-Anbieter verbinden" }, "welcomeBack": { "title": "Willkommen zurück", diff --git a/messages/en.json b/messages/en.json index 18cf5182a..e91951fe9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -414,7 +414,7 @@ }, "measurements": { "title": "Measurements", - "subtitle": "Weight, blood pressure, pulse and more", + "subtitle": "Log readings and browse the full history. Trends and analysis live under Insights.", "addMeasurement": "Add", "editMeasurement": "Edit measurement", "type": "Type", @@ -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", @@ -1965,6 +1966,7 @@ "progressAria": "Health Score progress", "deltaVsLastWeek": "{delta} vs last week", "deltaUnavailable": "No history yet — check back next week.", + "restModePaused": "Paused during illness — your score isn't being judged today.", "componentBp": "BP", "componentWeight": "Weight", "componentMood": "Mood", @@ -2007,7 +2009,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 +2074,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.", @@ -2709,6 +2713,7 @@ "title": "Glucose overview", "subtitle": "A read on your last {days} days, from {count} readings.", "spotCaveat": "These are spot-reading estimates from individual measurements, not a continuous-monitor profile — read them as a direction of travel rather than a clinical verdict.", + "labsLink": "Lab results? Log a blood-draw HbA1c or fasting glucose under Labs.", "learningTitle": "Still learning your glucose", "learningBody": "So far that's {count} over {days}. A little more history and a fuller picture comes into focus — keep logging, or let a connected device fill it in.", "learningBodyEmpty": "Once a handful of readings come in — logged by you or synced from a device — your time-in-range, average, and variability take shape here.", @@ -3623,7 +3628,10 @@ "learning": "Some insights — glucose, sleep rhythm, blood-pressure trends, cycle — sharpen over the first week or two as your data builds up. Empty tiles now just mean we're still learning your baseline.", "connectCta": "Connect a device", "logCta": "Log your first measurement", - "importCta": "Import existing history" + "importCta": "Import existing history", + "aiTitle": "Turn on AI Insights (optional)", + "aiBody": "Coach, the daily briefing and assessments turn your readings into plain language. For that you connect your own AI key (OpenAI, Anthropic) or a local model — your data stays with you.", + "aiCta": "Connect an AI provider" }, "welcomeBack": { "title": "Welcome back", diff --git a/messages/es.json b/messages/es.json index 1b2e2c7a0..6f2928108 100644 --- a/messages/es.json +++ b/messages/es.json @@ -414,7 +414,7 @@ }, "measurements": { "title": "Mediciones", - "subtitle": "Peso, tensión arterial, pulso y más", + "subtitle": "Registra lecturas y consulta el historial completo. Las tendencias y los análisis están en Análisis.", "addMeasurement": "Añadir", "editMeasurement": "Editar medición", "type": "Tipo", @@ -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", @@ -1965,6 +1966,7 @@ "progressAria": "Progreso de la puntuación de salud", "deltaVsLastWeek": "{delta} respecto a la semana anterior", "deltaUnavailable": "Aún no hay historial — vuelve la semana que viene.", + "restModePaused": "En pausa durante la enfermedad — hoy tu puntuación no se evalúa.", "componentBp": "PA", "componentWeight": "Peso", "componentMood": "Estado de ánimo", @@ -2007,7 +2009,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 +2074,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.", @@ -2709,6 +2713,7 @@ "title": "Resumen de glucosa", "subtitle": "Una lectura de tus últimos {days} días, a partir de {count} mediciones.", "spotCaveat": "Son estimaciones a partir de mediciones puntuales, no un perfil de un sensor continuo: léelas como una tendencia, no como un veredicto clínico.", + "labsLink": "¿Resultados de laboratorio? Registra una HbA1c o glucosa en ayunas de una analítica en Laboratorio.", "learningTitle": "Aún conociendo tu glucosa", "learningBody": "De momento son {count} en {days}. Con algo más de historial el panorama se aclara: sigue registrando o deja que lo complete un dispositivo conectado.", "learningBodyEmpty": "En cuanto lleguen unas cuantas mediciones —registradas por ti o sincronizadas desde un dispositivo— tu tiempo en rango, promedio y variabilidad tomarán forma aquí.", @@ -3623,7 +3628,10 @@ "learning": "Algunas conclusiones —glucosa, ritmo de sueño, tendencias de presión arterial, ciclo— se afinan durante la primera semana o dos a medida que se acumulan tus datos. Las casillas vacías ahora solo significan que aún estamos aprendiendo tu línea base.", "connectCta": "Conectar un dispositivo", "logCta": "Registra tu primera medición", - "importCta": "Importar historial existente" + "importCta": "Importar historial existente", + "aiTitle": "Activar Insights con IA (opcional)", + "aiBody": "El coach, el briefing diario y las valoraciones convierten tus lecturas en lenguaje claro. Para ello conectas tu propia clave de IA (OpenAI, Anthropic) o un modelo local: tus datos siguen siendo tuyos.", + "aiCta": "Conectar un proveedor de IA" }, "welcomeBack": { "title": "Bienvenido de nuevo", diff --git a/messages/fr.json b/messages/fr.json index e278cc028..d2a4076fe 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -414,7 +414,7 @@ }, "measurements": { "title": "Mesures", - "subtitle": "Poids, tension artérielle, pouls et plus", + "subtitle": "Saisis tes mesures et consulte l'historique complet. Les tendances et analyses sont dans Analyses.", "addMeasurement": "Ajouter", "editMeasurement": "Modifier la mesure", "type": "Type", @@ -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", @@ -1965,6 +1966,7 @@ "progressAria": "Progression du score de santé", "deltaVsLastWeek": "{delta} par rapport à la semaine précédente", "deltaUnavailable": "Pas encore d’historique — reviens la semaine prochaine.", + "restModePaused": "En pause pendant la maladie — ton score n'est pas évalué aujourd'hui.", "componentBp": "TA", "componentWeight": "Poids", "componentMood": "Humeur", @@ -2007,7 +2009,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 +2074,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.", @@ -2709,6 +2713,7 @@ "title": "Aperçu du glucose", "subtitle": "Un point sur vos {days} derniers jours, à partir de {count} mesures.", "spotCaveat": "Ce sont des estimations issues de mesures ponctuelles, pas le profil d’un capteur continu : lisez-les comme une tendance, non comme un verdict clinique.", + "labsLink": "Résultats de laboratoire ? Saisis une HbA1c ou une glycémie à jeun issue d'une prise de sang dans Laboratoire.", "learningTitle": "Votre glucose se découvre encore", "learningBody": "Pour l’instant, cela fait {count} sur {days}. Avec un peu plus d’historique, le tableau se précise : continuez à enregistrer ou laissez un appareil connecté compléter.", "learningBodyEmpty": "Dès que quelques mesures arrivent — saisies par vous ou synchronisées depuis un appareil — votre temps dans la cible, votre moyenne et votre variabilité prennent forme ici.", @@ -3623,7 +3628,10 @@ "learning": "Certaines analyses — glycémie, rythme de sommeil, tendances de tension, cycle — s'affinent au cours de la première semaine ou deux à mesure que vos données s'accumulent. Des tuiles vides signifient simplement que nous apprenons encore votre référence.", "connectCta": "Connecter un appareil", "logCta": "Enregistrez votre première mesure", - "importCta": "Importer un historique existant" + "importCta": "Importer un historique existant", + "aiTitle": "Activer les Insights IA (facultatif)", + "aiBody": "Le coach, le briefing quotidien et les évaluations traduisent tes mesures en langage clair. Pour cela, connecte ta propre clé IA (OpenAI, Anthropic) ou un modèle local — tes données restent chez toi.", + "aiCta": "Connecter un fournisseur d'IA" }, "welcomeBack": { "title": "Bon retour", diff --git a/messages/it.json b/messages/it.json index e5fb79eb1..8873b3ec7 100644 --- a/messages/it.json +++ b/messages/it.json @@ -414,7 +414,7 @@ }, "measurements": { "title": "Misurazioni", - "subtitle": "Peso, pressione arteriosa, pulsazioni e altro", + "subtitle": "Registra le misurazioni e consulta lo storico completo. Tendenze e analisi sono in Analisi.", "addMeasurement": "Aggiungi", "editMeasurement": "Modifica misurazione", "type": "Tipo", @@ -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", @@ -1965,6 +1966,7 @@ "progressAria": "Avanzamento del punteggio di salute", "deltaVsLastWeek": "{delta} rispetto alla settimana scorsa", "deltaUnavailable": "Ancora nessuno storico — ripassa la prossima settimana.", + "restModePaused": "In pausa durante la malattia — oggi il tuo punteggio non viene valutato.", "componentBp": "PA", "componentWeight": "Peso", "componentMood": "Umore", @@ -2007,7 +2009,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 +2074,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.", @@ -2709,6 +2713,7 @@ "title": "Panoramica della glicemia", "subtitle": "Una lettura dei tuoi ultimi {days} giorni, da {count} misurazioni.", "spotCaveat": "Sono stime da singole misurazioni puntuali, non il profilo di un sensore continuo: leggile come una tendenza, non come un verdetto clinico.", + "labsLink": "Risultati di laboratorio? Registra una HbA1c o la glicemia a digiuno da un prelievo in Laboratorio.", "learningTitle": "Stiamo ancora imparando la tua glicemia", "learningBody": "Per ora sono {count} su {days}. Con un po’ più di storico il quadro si chiarisce: continua a registrare o lascia che lo completi un dispositivo connesso.", "learningBodyEmpty": "Appena arrivano alcune misurazioni — registrate da te o sincronizzate da un dispositivo — il tempo nel range, la media e la variabilità prendono forma qui.", @@ -3623,7 +3628,10 @@ "learning": "Alcune analisi — glicemia, ritmo del sonno, tendenze della pressione, ciclo — si affinano nella prima settimana o due man mano che i tuoi dati si accumulano. Le caselle vuote ora indicano solo che stiamo ancora imparando il tuo riferimento.", "connectCta": "Collega un dispositivo", "logCta": "Registra la tua prima misurazione", - "importCta": "Importa la cronologia esistente" + "importCta": "Importa la cronologia esistente", + "aiTitle": "Attiva gli Insight IA (facoltativo)", + "aiBody": "Il coach, il briefing giornaliero e le valutazioni trasformano le tue misurazioni in linguaggio chiaro. Per farlo colleghi la tua chiave IA (OpenAI, Anthropic) o un modello locale: i tuoi dati restano tuoi.", + "aiCta": "Collega un provider IA" }, "welcomeBack": { "title": "Bentornato", diff --git a/messages/pl.json b/messages/pl.json index f09755820..0f4eb78f3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -414,7 +414,7 @@ }, "measurements": { "title": "Pomiary", - "subtitle": "Waga, ciśnienie tętnicze, tętno i więcej", + "subtitle": "Zapisuj pomiary i przeglądaj pełną historię. Trendy i analizy znajdziesz w Analizach.", "addMeasurement": "Dodaj", "editMeasurement": "Edytuj pomiar", "type": "Typ", @@ -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", @@ -1965,6 +1966,7 @@ "progressAria": "Postęp wyniku zdrowia", "deltaVsLastWeek": "{delta} w porównaniu z ubiegłym tygodniem", "deltaUnavailable": "Brak historii — zajrzyj w przyszłym tygodniu.", + "restModePaused": "Wstrzymane podczas choroby — Twój wynik nie jest dziś oceniany.", "componentBp": "RR", "componentWeight": "Waga", "componentMood": "Nastrój", @@ -2007,7 +2009,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 +2074,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.", @@ -2709,6 +2713,7 @@ "title": "Przegląd glukozy", "subtitle": "Spojrzenie na ostatnie {days} dni, na podstawie {count} pomiarów.", "spotCaveat": "To szacunki z pojedynczych pomiarów punktowych, a nie profil z czujnika ciągłego — traktuj je jako kierunek, nie jako diagnozę.", + "labsLink": "Wyniki laboratoryjne? HbA1c lub glukozę na czczo z pobrania krwi zapisz w sekcji Laboratorium.", "learningTitle": "Wciąż poznajemy Twoją glukozę", "learningBody": "Na razie to {count} w ciągu {days}. Przy nieco dłuższej historii obraz się wyostrza — rejestruj dalej lub pozwól uzupełnić to podłączonemu urządzeniu.", "learningBodyEmpty": "Gdy pojawi się kilka pomiarów — wpisanych przez Ciebie lub zsynchronizowanych z urządzenia — czas w zakresie, średnia i zmienność nabiorą tu kształtu.", @@ -3623,7 +3628,10 @@ "learning": "Niektóre wnioski — glukoza, rytm snu, trendy ciśnienia, cykl — wyostrzają się w ciągu pierwszego tygodnia lub dwóch, w miarę gromadzenia danych. Puste kafelki oznaczają teraz tylko, że wciąż poznajemy Twój punkt odniesienia.", "connectCta": "Połącz urządzenie", "logCta": "Zapisz pierwszy pomiar", - "importCta": "Importuj istniejącą historię" + "importCta": "Importuj istniejącą historię", + "aiTitle": "Włącz Insighty AI (opcjonalnie)", + "aiBody": "Coach, codzienny briefing i oceny zamieniają Twoje pomiary w prosty język. W tym celu łączysz własny klucz AI (OpenAI, Anthropic) lub model lokalny — Twoje dane pozostają u Ciebie.", + "aiCta": "Połącz dostawcę AI" }, "welcomeBack": { "title": "Witaj ponownie", diff --git a/src/app/page.tsx b/src/app/page.tsx index c7a111143..8de115c44 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -106,18 +106,7 @@ import { isDashboardSnapshotEnabled } from "@/lib/dashboard/snapshot-flag"; import type { DataSummary } from "@/lib/analytics/trends"; import { mergeSlimAndThickAnalytics } from "@/lib/analytics/merge-slim-thick"; import { isWindowSufficient } from "@/lib/analytics/window-confidence"; -import { getBpTargets } from "@/lib/analytics/bp-targets"; -import { - buildTrafficLightBands, - buildTrafficRange, - buildWeightBandsFromHeight, - buildWeightRangeFromHeight, - getBodyFatTargetRange, -} from "@/lib/analytics/value-bands"; -import { - getAgeFromDateOfBirth, - getPersonalizedPulseTarget, -} from "@/lib/analytics/pulse-targets"; +import { buildDashboardBands } from "@/lib/dashboard/bands"; import { apiGet } from "@/lib/api/api-fetch"; // v1.18.6 — the pure first-paint / skeleton-reservation gates moved to @@ -537,34 +526,22 @@ export default function DashboardPage() { const serverBands = snapshotEnabled ? snapshotQuery.data?.targetBands : undefined; - const bpTargets = snapshotEnabled - ? (serverBands?.bpTargets ?? null) - : user?.dateOfBirth != null - ? getBpTargets(new Date(user.dateOfBirth)) - : null; - const bodyFatRange = snapshotEnabled - ? (serverBands?.bodyFatRange ?? getBodyFatTargetRange(user?.gender)) - : getBodyFatTargetRange(user?.gender); - const weightRange = snapshotEnabled - ? (serverBands?.weightRange ?? null) - : user?.heightCm - ? buildWeightRangeFromHeight(user.heightCm) - : null; - const weightBands = snapshotEnabled - ? (serverBands?.weightBands ?? undefined) - : user?.heightCm - ? buildWeightBandsFromHeight(user.heightCm, { - lowerBound: 30, - upperBound: 250, - }) - : undefined; - // The pulse target range / bands carry no i18n — read them server-side - // under snapshot mode; recompute from the profile otherwise. - const pulseAge = getAgeFromDateOfBirth(user?.dateOfBirth ?? null); - const pulseTarget = getPersonalizedPulseTarget( - pulseAge, - (user?.gender as "MALE" | "FEMALE" | null | undefined) ?? null, - ); + // Resolve the bands through the SAME `buildDashboardBands` helper the + // server snapshot uses, so the snapshot path and the client fallback + // are byte-identical by construction (no second inline copy of the + // band math to drift). Always compute the client copy from the profile + // — it is pure + cheap — so a still-loading snapshot frame keeps real + // bands rather than blank charts; `serverBands ?? clientBands` then + // prefers the authoritative server numbers the moment they arrive. + const clientBands = buildDashboardBands({ + dateOfBirth: user?.dateOfBirth ? new Date(user.dateOfBirth) : null, + gender: (user?.gender as "MALE" | "FEMALE" | null | undefined) ?? null, + heightCm: user?.heightCm ?? null, + }); + const bands = serverBands ?? clientBands; + const bpTargets = bands.bpTargets; + const weightRange = bands.weightRange; + const weightBands = bands.weightBands ?? undefined; const bpTargetZones = bpTargets ? [ { @@ -587,50 +564,11 @@ export default function DashboardPage() { }, ] : undefined; - const bpSysRange = snapshotEnabled - ? (serverBands?.bpSysRange ?? null) - : bpTargets - ? buildTrafficRange(bpTargets.sysLow, bpTargets.sysHigh) - : null; - const bpDiaRange = snapshotEnabled - ? (serverBands?.bpDiaRange ?? null) - : bpTargets - ? buildTrafficRange(bpTargets.diaLow, bpTargets.diaHigh) - : null; - const pulseDisplayRange = serverBands?.pulseDisplayRange ?? { - greenMin: pulseTarget.greenMin, - greenMax: pulseTarget.greenMax, - orangeMin: pulseTarget.orangeMin, - orangeMax: pulseTarget.orangeMax, - }; - const pulseBands = serverBands?.pulseBands ?? [ - { min: 30, max: pulseTarget.orangeMin, color: "#ff5555", opacity: 0.16 }, - { - min: pulseTarget.orangeMin, - max: pulseTarget.greenMin, - color: "#ffb86c", - opacity: 0.18, - }, - { - min: pulseTarget.greenMin, - max: pulseTarget.greenMax, - color: "#50fa7b", - opacity: 0.2, - }, - { - min: pulseTarget.greenMax, - max: pulseTarget.orangeMax, - color: "#ffb86c", - opacity: 0.18, - }, - { min: pulseTarget.orangeMax, max: 220, color: "#ff5555", opacity: 0.16 }, - ].filter((band) => band.max > band.min); - const bodyFatBands = - serverBands?.bodyFatBands ?? - buildTrafficLightBands(bodyFatRange.min, bodyFatRange.max, { - lowerBound: 2, - upperBound: 55, - }); + const bpSysRange = bands.bpSysRange; + const bpDiaRange = bands.bpDiaRange; + const pulseDisplayRange = bands.pulseDisplayRange; + const pulseBands = bands.pulseBands; + const bodyFatBands = bands.bodyFatBands; // v1.18.6 — ONE batched fetch for every visible non-sleep chart series // (audit finding #2). Pre-fix, each `` fired its own 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/glucose/glucose-clinical-panel.tsx b/src/components/insights/glucose/glucose-clinical-panel.tsx index 144447eff..3e233c593 100644 --- a/src/components/insights/glucose/glucose-clinical-panel.tsx +++ b/src/components/insights/glucose/glucose-clinical-panel.tsx @@ -1,6 +1,7 @@ "use client"; -import { Activity, Droplet, TrendingUp } from "lucide-react"; +import Link from "next/link"; +import { Activity, Droplet, FlaskConical, TrendingUp } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -113,6 +114,9 @@ export function GlucoseClinicalPanel() { } caveat={t("insights.bloodGlucose.clinical.spotCaveat")} /> +
+ +
); @@ -225,11 +229,34 @@ export function GlucoseClinicalPanel() { byContext={byContext} /> ) : null} + + ); } +/** + * v1.18.6 — signpost the measurement-vs-lab mental model. This panel + * estimates A1c / GMI from spot or CGM readings over time; a lab-ordered + * HbA1c or fasting-glucose result is a dated panel value that lives under + * Labs. The cross-link keeps the two surfaces referencing each other so a + * user knows which door to use rather than treating them as rivals. + */ +function LabsCrossLink() { + const { t } = useTranslations(); + return ( + +