mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(ux): close medium UX-audit findings across flows, IA, and onboarding
Coach deep-links now carry launch context in the URL instead of dropping it at the route boundary: /coach reads ?scope= (validated against the closed CoachScopeSource enum) and ?ask= to seed the drawer's own prefill/scope props, the Today check-in's "Adjust" action seeds ?ask= with the plan's own words instead of opening a blank chat, and the workout detail page scopes its coach button to workouts. IA: give the document vault a "View N lab values" link on LAB_RESULT-shaped documents (the auto-stage/OCR-commit linkage existed server-side with no UI surface); disambiguate the two "Recovery" pages (RECOVERY_SCORE anatomy vs the composite wearable page) with a distinct label plus a two-way cross-link; move the opt-in nutrients sync card from Source priority to Integrations, where every other sync surface lives; rename the user-menu theme switcher off "Appearance" (collides with the Settings hub) and the Environment settings section to "Location & weather"; cross-link mood, mental-wellbeing check-ins, and mood insights, which were three unconnected islands. Onboarding: the getting-started checklist's data-source/notifications rows now gate their fetch on the same checklistRelevant condition the other rows use, instead of only-while-onboarding — they used to be stuck undone forever for anyone who finished the wizard with under 5 measurements; the tour wrap-up no longer promises a replay affordance that was removed; the wizard's Withings card shows a connected badge once the OAuth round-trip lands instead of leaving the button looking untouched in the tab it was opened from; the done screen and Modules hub get a quiet cross-link; the sleep empty-state CTA reads "Connect a source" instead of being pinned to one vendor. All copy changes land across every locale; de.json reviewed for register.
This commit is contained in:
1 parent
e9e186dca5
commit
8dd04e0170
23 files changed
+431
-79
No files matched your search
+18
-8
@@ -228,7 +228,7 @@
|
||||
"logout": "Abmelden",
|
||||
"login": "Anmelden",
|
||||
"notifications": "Benachrichtigungen",
|
||||
"theme": "Darstellung",
|
||||
"theme": "Design",
|
||||
"themeSystem": "System",
|
||||
"themeDark": "Dunkel",
|
||||
"themeLight": "Hell",
|
||||
@@ -605,6 +605,7 @@
|
||||
"mood": {
|
||||
"title": "Stimmung",
|
||||
"subtitle": "Stimmungsverlauf und Einträge",
|
||||
"mentalWellbeingLink": "Check-in zum seelischen Wohlbefinden machen",
|
||||
"addEntry": "Hinzufügen",
|
||||
"editEntry": "Eintrag bearbeiten",
|
||||
"moodLevel": "Stimmung",
|
||||
@@ -2894,7 +2895,7 @@
|
||||
"sleep": {
|
||||
"title": "Noch keine Schlafwerte",
|
||||
"description": "Sobald Schlafdaten vorliegen (manuell oder via Withings / Apple Health), erscheint hier der nächtliche Stadien-Verlauf.",
|
||||
"cta": "Withings verbinden"
|
||||
"cta": "Quelle verbinden"
|
||||
}
|
||||
},
|
||||
"navWorkouts": "Workouts",
|
||||
@@ -3262,6 +3263,8 @@
|
||||
},
|
||||
"navWalkingSpeed": "Gehgeschwindigkeit",
|
||||
"mood": {
|
||||
"logLink": "Stimmung eintragen",
|
||||
"mentalWellbeingLink": "Check-in zum seelischen Wohlbefinden machen",
|
||||
"heatmapTitle": "Stimmungskalender",
|
||||
"heatmapNoEntry": "Kein Eintrag",
|
||||
"legendGreat": "Super",
|
||||
@@ -3550,7 +3553,8 @@
|
||||
"sectionSubtitle": "Zusammengesetzte Werte für Erholung, Stress und Belastung aus deinen verbundenen Geräten",
|
||||
"loadError": "Deine Wellness-Werte konnten gerade nicht geladen werden.",
|
||||
"loadingLabel": "Deine Gesundheitswerte werden geladen",
|
||||
"recovery": "Erholung",
|
||||
"recovery": "Erholungs-Score",
|
||||
"recoveryCrossLink": "Erholungs- und Belastungsdaten deines Wearables ansehen",
|
||||
"stress": "Stress",
|
||||
"strain": "Belastung"
|
||||
},
|
||||
@@ -3721,6 +3725,7 @@
|
||||
"recovery": {
|
||||
"title": "Erholung",
|
||||
"description": "Wie gut sich dein Körper erholt hat – die Erholungs-, Belastungs- und Load-Signale, die dein Wearable über Nacht aufzeichnet.",
|
||||
"scoreCrossLink": "Den zusammengesetzten Erholungs-Score ansehen",
|
||||
"emptyTitle": "Noch keine Erholungsdaten",
|
||||
"empty": "Noch keine Erholungsdaten. Verbinde ein WHOOP-, Polar- oder Oura-Konto, dann füllen sich diese Werte, sobald dein Gerät synchronisiert.",
|
||||
"block": {
|
||||
@@ -4009,7 +4014,7 @@
|
||||
},
|
||||
"wrapUp": {
|
||||
"title": "Das war die Tour",
|
||||
"body": "Du kannst jedes Modul jederzeit einzeln erneut ansehen („Diese Tour zeigen“ auf jeder Seite) oder die komplette Tour unter Einstellungen → Erweitert neu starten."
|
||||
"body": "Diese Führung läuft einmalig – sie öffnet sich nicht von selbst erneut. Alles Gezeigte bleibt erhalten, und du kannst Funktionen jederzeit unter Einstellungen → Module ein- oder ausschalten."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4084,7 +4089,8 @@
|
||||
"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",
|
||||
"modulesCta": "Module auswählen"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"title": "Willkommen zurück",
|
||||
@@ -4549,7 +4555,7 @@
|
||||
"description": "Strukturierte Datensätze zu Allergien und Familienanamnese."
|
||||
},
|
||||
"environment": {
|
||||
"title": "Umwelt",
|
||||
"title": "Standort & Wetter",
|
||||
"subtitle": "Wetter, Tageslicht und Standort-Kontext.",
|
||||
"disabledHint": "Aktiviere das Umwelt-Modul, um einen Heimatort festzulegen und tägliches Wetter und Tageslicht abzurufen.",
|
||||
"searchPlaceholder": "Stadt suchen",
|
||||
@@ -7959,6 +7965,7 @@
|
||||
"mentalHealth": {
|
||||
"pageTitle": "Seelisches Wohlbefinden",
|
||||
"pageDescription": "Vier validierte, freiwillige Selbsttests — PHQ-9 (Stimmung), GAD-7 (Sorgen), WHO-5 (Wohlbefinden) und SCI (Schlaf) — ergänzend zu deinem Stimmungstagebuch. Aussagekräftig ist der Verlauf über die Zeit, nicht ein einzelner Wert.",
|
||||
"moodLink": "Deinen Stimmungsverlauf ansehen",
|
||||
"attributionLabel": "Quelle",
|
||||
"attributionShow": "Quelle und Lizenz für {instrument}",
|
||||
"choosePrompt": "Check-in auswählen",
|
||||
@@ -8404,7 +8411,8 @@
|
||||
"noConditions": "Mit keiner Erkrankung verknüpft",
|
||||
"previewUnavailable": "Keine Inline-Vorschau für diesen Dateityp",
|
||||
"uploadedMeta": "Hinzugefügt am {date} · {size}",
|
||||
"share": "Teilen"
|
||||
"share": "Teilen",
|
||||
"labValuesLink": "{count} Laborwerte ansehen"
|
||||
},
|
||||
"dropOverlay": {
|
||||
"title": "Zum Hochladen ablegen",
|
||||
@@ -8666,7 +8674,9 @@
|
||||
"coachCheckin": {
|
||||
"title": "Zeit, einen Plan anzuschauen",
|
||||
"body": "Es ist etwa eine Woche her, seit du diesen Plan gesetzt hast — behalte ihn, passe ihn an oder lass ihn los. Ganz ohne Druck.",
|
||||
"bodyPlan": "Vor etwa einer Woche hast du dir vorgenommen: „{plan}“. Behalte es, passe es an oder lass es los — ganz ohne Druck."
|
||||
"bodyPlan": "Vor etwa einer Woche hast du dir vorgenommen: „{plan}“. Behalte es, passe es an oder lass es los — ganz ohne Druck.",
|
||||
"adjustPrompt": "Ich möchte meinen Check-in-Plan anpassen.",
|
||||
"adjustPromptPlan": "Ich möchte diesen Plan anpassen: „{plan}“"
|
||||
},
|
||||
"tensionWindow": {
|
||||
"title": "Vielleicht eine angespannte Phase",
|
||||
|
||||
+18
-8
@@ -228,7 +228,7 @@
|
||||
"logout": "Sign out",
|
||||
"login": "Sign in",
|
||||
"notifications": "Notifications",
|
||||
"theme": "Appearance",
|
||||
"theme": "Theme",
|
||||
"themeSystem": "System",
|
||||
"themeDark": "Dark",
|
||||
"themeLight": "Light",
|
||||
@@ -605,6 +605,7 @@
|
||||
"mood": {
|
||||
"title": "Mood",
|
||||
"subtitle": "Mood tracker and entries",
|
||||
"mentalWellbeingLink": "Take a mental-wellbeing check-in",
|
||||
"addEntry": "Add",
|
||||
"editEntry": "Edit entry",
|
||||
"moodLevel": "Mood",
|
||||
@@ -2894,7 +2895,7 @@
|
||||
"sleep": {
|
||||
"title": "No sleep entries yet",
|
||||
"description": "Once sleep data lands (manual or via Withings / Apple Health), the per-night stage breakdown will appear here.",
|
||||
"cta": "Connect Withings"
|
||||
"cta": "Connect a source"
|
||||
}
|
||||
},
|
||||
"navWorkouts": "Workouts",
|
||||
@@ -3262,6 +3263,8 @@
|
||||
},
|
||||
"navWalkingSpeed": "Walking Speed",
|
||||
"mood": {
|
||||
"logLink": "Log a mood entry",
|
||||
"mentalWellbeingLink": "Take a mental-wellbeing check-in",
|
||||
"heatmapTitle": "Mood calendar",
|
||||
"heatmapNoEntry": "No entry",
|
||||
"legendGreat": "Great",
|
||||
@@ -3550,7 +3553,8 @@
|
||||
"sectionSubtitle": "Composite recovery, stress and strain read from your connected devices",
|
||||
"loadError": "Could not load your wellness scores right now.",
|
||||
"loadingLabel": "Loading your health scores",
|
||||
"recovery": "Recovery",
|
||||
"recovery": "Recovery score",
|
||||
"recoveryCrossLink": "See your wearable's recovery & strain data",
|
||||
"stress": "Stress",
|
||||
"strain": "Strain"
|
||||
},
|
||||
@@ -3721,6 +3725,7 @@
|
||||
"recovery": {
|
||||
"title": "Recovery",
|
||||
"description": "How well your body has bounced back — the recovery, strain and load signals your wearable records overnight.",
|
||||
"scoreCrossLink": "See the composite recovery score",
|
||||
"emptyTitle": "No recovery data yet",
|
||||
"empty": "No recovery data yet. Connect a WHOOP, Polar or Oura account and these scores fill in once your device syncs.",
|
||||
"block": {
|
||||
@@ -4009,7 +4014,7 @@
|
||||
},
|
||||
"wrapUp": {
|
||||
"title": "That’s the tour",
|
||||
"body": "You can revisit any module on its own at any time (“Show this tour” on every page) or restart the whole tour under Settings → Advanced."
|
||||
"body": "This walkthrough runs once — it won't reopen on its own. Everything it covered stays put, and you can turn features on or off any time under Settings → Modules."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4084,7 +4089,8 @@
|
||||
"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",
|
||||
"modulesCta": "Choose your modules"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"title": "Welcome back",
|
||||
@@ -4549,7 +4555,7 @@
|
||||
"description": "Structured allergies and family history records."
|
||||
},
|
||||
"environment": {
|
||||
"title": "Environment",
|
||||
"title": "Location & weather",
|
||||
"subtitle": "Weather, daylight, and location context.",
|
||||
"disabledHint": "Turn on the Environment module to set a home location and pull in daily weather and daylight.",
|
||||
"searchPlaceholder": "Search for a city",
|
||||
@@ -7959,6 +7965,7 @@
|
||||
"mentalHealth": {
|
||||
"pageTitle": "Mental wellbeing",
|
||||
"pageDescription": "Validated, opt-in self-checks — PHQ-9 (mood), GAD-7 (worry), WHO-5 (well-being) and SCI (sleep) — that sit beside your mood tracking. The value is the trend over time, not any single number.",
|
||||
"moodLink": "View your mood history",
|
||||
"attributionLabel": "Attribution",
|
||||
"attributionShow": "Source and licence for {instrument}",
|
||||
"choosePrompt": "Choose a check-in",
|
||||
@@ -8404,7 +8411,8 @@
|
||||
"noConditions": "Not linked to a condition",
|
||||
"previewUnavailable": "No inline preview for this file type",
|
||||
"uploadedMeta": "Added {date} · {size}",
|
||||
"share": "Share"
|
||||
"share": "Share",
|
||||
"labValuesLink": "View {count} lab values"
|
||||
},
|
||||
"dropOverlay": {
|
||||
"title": "Drop files to upload",
|
||||
@@ -8666,7 +8674,9 @@
|
||||
"coachCheckin": {
|
||||
"title": "Time to check in on a plan",
|
||||
"body": "It's been about a week since you set this plan — keep it, adjust it, or let it go. No pressure either way.",
|
||||
"bodyPlan": "About a week ago you set: “{plan}”. Keep it, adjust it, or let it go — no pressure either way."
|
||||
"bodyPlan": "About a week ago you set: “{plan}”. Keep it, adjust it, or let it go — no pressure either way.",
|
||||
"adjustPrompt": "I'd like to adjust my check-in plan.",
|
||||
"adjustPromptPlan": "I'd like to adjust this plan: “{plan}”"
|
||||
},
|
||||
"tensionWindow": {
|
||||
"title": "Possible tension today",
|
||||
|
||||
+18
-8
@@ -228,7 +228,7 @@
|
||||
"logout": "Cerrar sesión",
|
||||
"login": "Iniciar sesión",
|
||||
"notifications": "Notificaciones",
|
||||
"theme": "Apariencia",
|
||||
"theme": "Tema",
|
||||
"themeSystem": "Sistema",
|
||||
"themeDark": "Oscuro",
|
||||
"themeLight": "Claro",
|
||||
@@ -605,6 +605,7 @@
|
||||
"mood": {
|
||||
"title": "Estado de ánimo",
|
||||
"subtitle": "Tendencia y registros de estado de ánimo",
|
||||
"mentalWellbeingLink": "Hacer un chequeo de bienestar mental",
|
||||
"addEntry": "Añadir",
|
||||
"editEntry": "Editar entrada",
|
||||
"moodLevel": "Estado de ánimo",
|
||||
@@ -2894,7 +2895,7 @@
|
||||
"sleep": {
|
||||
"title": "Aún no hay registros de sueño",
|
||||
"description": "Una vez disponibles los datos de sueño (manual o vía Withings / Apple Health), aquí aparecerá el desglose por fases.",
|
||||
"cta": "Conectar Withings"
|
||||
"cta": "Conectar una fuente"
|
||||
}
|
||||
},
|
||||
"navWorkouts": "Entrenamientos",
|
||||
@@ -3262,6 +3263,8 @@
|
||||
},
|
||||
"navWalkingSpeed": "Velocidad al andar",
|
||||
"mood": {
|
||||
"logLink": "Registrar un estado de ánimo",
|
||||
"mentalWellbeingLink": "Hacer un chequeo de bienestar mental",
|
||||
"heatmapTitle": "Calendario de ánimo",
|
||||
"heatmapNoEntry": "Sin registro",
|
||||
"legendGreat": "Genial",
|
||||
@@ -3550,7 +3553,8 @@
|
||||
"sectionSubtitle": "Valores combinados de recuperación, estrés y esfuerzo leídos de tus dispositivos conectados",
|
||||
"loadError": "No se pudieron cargar tus puntuaciones de bienestar en este momento.",
|
||||
"loadingLabel": "Cargando tus indicadores de salud",
|
||||
"recovery": "Recuperación",
|
||||
"recovery": "Puntuación de recuperación",
|
||||
"recoveryCrossLink": "Ver los datos de recuperación y esfuerzo de tu wearable",
|
||||
"stress": "Estrés",
|
||||
"strain": "Esfuerzo"
|
||||
},
|
||||
@@ -3721,6 +3725,7 @@
|
||||
"recovery": {
|
||||
"title": "Recuperación",
|
||||
"description": "Cómo se ha recuperado tu cuerpo: las señales de recuperación, esfuerzo y carga que tu wearable registra durante la noche.",
|
||||
"scoreCrossLink": "Ver la puntuación de recuperación combinada",
|
||||
"emptyTitle": "Aún no hay datos de recuperación",
|
||||
"empty": "Aún no hay datos de recuperación. Conecta una cuenta de WHOOP, Polar u Oura y estas puntuaciones se completarán cuando tu dispositivo sincronice.",
|
||||
"block": {
|
||||
@@ -4009,7 +4014,7 @@
|
||||
},
|
||||
"wrapUp": {
|
||||
"title": "Esto es la visita",
|
||||
"body": "Puedes volver a ver cualquier módulo por separado cuando quieras («Mostrar esta visita» en cada página) o reiniciar la visita completa en Ajustes → Avanzado."
|
||||
"body": "Esta visita guiada se muestra una sola vez, no volverá a abrirse por sí sola. Todo lo que has visto sigue disponible, y puedes activar o desactivar funciones cuando quieras en Ajustes → Módulos."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4084,7 +4089,8 @@
|
||||
"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",
|
||||
"modulesCta": "Elige tus módulos"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"title": "Bienvenido de nuevo",
|
||||
@@ -4549,7 +4555,7 @@
|
||||
"description": "Registros estructurados de alergias y antecedentes familiares."
|
||||
},
|
||||
"environment": {
|
||||
"title": "Entorno",
|
||||
"title": "Ubicación y clima",
|
||||
"subtitle": "Contexto de tiempo, luz diurna y ubicación.",
|
||||
"disabledHint": "Activa el módulo Entorno para definir una ubicación principal e incorporar el tiempo y las horas de luz diarias.",
|
||||
"searchPlaceholder": "Buscar una ciudad",
|
||||
@@ -7959,6 +7965,7 @@
|
||||
"mentalHealth": {
|
||||
"pageTitle": "Bienestar mental",
|
||||
"pageDescription": "Cuatro autoevaluaciones validadas y opcionales — PHQ-9 (estado de ánimo), GAD-7 (preocupación), WHO-5 (bienestar) y SCI (sueño) — junto a tu registro de ánimo. Lo valioso es la tendencia en el tiempo, no un único número.",
|
||||
"moodLink": "Ver tu historial de estado de ánimo",
|
||||
"attributionLabel": "Fuente",
|
||||
"attributionShow": "Fuente y licencia de {instrument}",
|
||||
"choosePrompt": "Elige una revisión",
|
||||
@@ -8404,7 +8411,8 @@
|
||||
"noConditions": "Sin vincular a una afección",
|
||||
"previewUnavailable": "Sin vista previa integrada para este tipo de archivo",
|
||||
"uploadedMeta": "Añadido el {date} · {size}",
|
||||
"share": "Compartir"
|
||||
"share": "Compartir",
|
||||
"labValuesLink": "Ver {count} valores de laboratorio"
|
||||
},
|
||||
"dropOverlay": {
|
||||
"title": "Suelta los archivos para subirlos",
|
||||
@@ -8666,7 +8674,9 @@
|
||||
"coachCheckin": {
|
||||
"title": "Es buen momento para revisar un plan",
|
||||
"body": "Ha pasado alrededor de una semana desde que definiste este plan: consérvalo, ajústalo o déjalo ir. Sin presión.",
|
||||
"bodyPlan": "Hace aproximadamente una semana te propusiste: «{plan}». Consérvalo, ajústalo o déjalo ir, sin presión."
|
||||
"bodyPlan": "Hace aproximadamente una semana te propusiste: «{plan}». Consérvalo, ajústalo o déjalo ir, sin presión.",
|
||||
"adjustPrompt": "Me gustaría ajustar mi plan de seguimiento.",
|
||||
"adjustPromptPlan": "Me gustaría ajustar este plan: «{plan}»"
|
||||
},
|
||||
"tensionWindow": {
|
||||
"title": "Quizá algo de tensión hoy",
|
||||
|
||||
+18
-8
@@ -228,7 +228,7 @@
|
||||
"logout": "Se déconnecter",
|
||||
"login": "Se connecter",
|
||||
"notifications": "Notifications",
|
||||
"theme": "Apparence",
|
||||
"theme": "Thème",
|
||||
"themeSystem": "Système",
|
||||
"themeDark": "Sombre",
|
||||
"themeLight": "Clair",
|
||||
@@ -605,6 +605,7 @@
|
||||
"mood": {
|
||||
"title": "Humeur",
|
||||
"subtitle": "Tendance et entrées d’humeur",
|
||||
"mentalWellbeingLink": "Faire un bilan de bien-être mental",
|
||||
"addEntry": "Ajouter",
|
||||
"editEntry": "Modifier l’entrée",
|
||||
"moodLevel": "Humeur",
|
||||
@@ -2894,7 +2895,7 @@
|
||||
"sleep": {
|
||||
"title": "Aucune donnée de sommeil",
|
||||
"description": "Dès que des données de sommeil sont disponibles (manuel ou via Withings / Apple Health), la répartition par stade apparaîtra ici.",
|
||||
"cta": "Connecter Withings"
|
||||
"cta": "Connecter une source"
|
||||
}
|
||||
},
|
||||
"navWorkouts": "Entraînements",
|
||||
@@ -3262,6 +3263,8 @@
|
||||
},
|
||||
"navWalkingSpeed": "Vitesse de marche",
|
||||
"mood": {
|
||||
"logLink": "Enregistrer une humeur",
|
||||
"mentalWellbeingLink": "Faire un bilan de bien-être mental",
|
||||
"heatmapTitle": "Calendrier d'humeur",
|
||||
"heatmapNoEntry": "Aucune entrée",
|
||||
"legendGreat": "Excellent",
|
||||
@@ -3550,7 +3553,8 @@
|
||||
"sectionSubtitle": "Valeurs combinées de récupération, de stress et d'effort issues de tes appareils connectés",
|
||||
"loadError": "Impossible de charger vos scores de bien-être pour le moment.",
|
||||
"loadingLabel": "Chargement de tes indicateurs de santé",
|
||||
"recovery": "Récupération",
|
||||
"recovery": "Score de récupération",
|
||||
"recoveryCrossLink": "Voir les données de récupération et d'effort de votre wearable",
|
||||
"stress": "Stress",
|
||||
"strain": "Effort"
|
||||
},
|
||||
@@ -3721,6 +3725,7 @@
|
||||
"recovery": {
|
||||
"title": "Récupération",
|
||||
"description": "À quel point votre corps a récupéré : les signaux de récupération, de charge et d'effort que votre wearable enregistre la nuit.",
|
||||
"scoreCrossLink": "Voir le score de récupération composite",
|
||||
"emptyTitle": "Pas encore de données de récupération",
|
||||
"empty": "Pas encore de données de récupération. Connectez un compte WHOOP, Polar ou Oura et ces scores se rempliront dès que votre appareil se synchronise.",
|
||||
"block": {
|
||||
@@ -4009,7 +4014,7 @@
|
||||
},
|
||||
"wrapUp": {
|
||||
"title": "C’est la fin de la visite",
|
||||
"body": "Vous pouvez revoir chaque module séparément à tout moment (« Afficher cette visite » sur chaque page) ou relancer la visite complète dans Réglages → Avancé."
|
||||
"body": "Cette visite guidée ne s'affiche qu'une fois — elle ne se rouvrira pas d'elle-même. Tout ce qu'elle a montré reste en place, et vous pouvez activer ou désactiver des fonctionnalités à tout moment dans Réglages → Modules."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4084,7 +4089,8 @@
|
||||
"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",
|
||||
"modulesCta": "Choisir vos modules"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"title": "Bon retour",
|
||||
@@ -4549,7 +4555,7 @@
|
||||
"description": "Dossiers structurés des allergies et des antécédents familiaux."
|
||||
},
|
||||
"environment": {
|
||||
"title": "Environnement",
|
||||
"title": "Localisation et météo",
|
||||
"subtitle": "Contexte météo, lumière du jour et lieu.",
|
||||
"disabledHint": "Activez le module Environnement pour définir un lieu de référence et intégrer la météo et la durée du jour quotidiennes.",
|
||||
"searchPlaceholder": "Rechercher une ville",
|
||||
@@ -7959,6 +7965,7 @@
|
||||
"mentalHealth": {
|
||||
"pageTitle": "Bien-être mental",
|
||||
"pageDescription": "Quatre auto-évaluations validées et facultatives — PHQ-9 (humeur), GAD-7 (inquiétude), WHO-5 (bien-être) et SCI (sommeil) — en complément de ton suivi de l'humeur. Ce qui compte, c'est la tendance dans le temps, pas un chiffre isolé.",
|
||||
"moodLink": "Voir ton historique d'humeur",
|
||||
"attributionLabel": "Source",
|
||||
"attributionShow": "Source et licence pour {instrument}",
|
||||
"choosePrompt": "Choisis un bilan",
|
||||
@@ -8404,7 +8411,8 @@
|
||||
"noConditions": "Lié à aucune affection",
|
||||
"previewUnavailable": "Pas d'aperçu intégré pour ce type de fichier",
|
||||
"uploadedMeta": "Ajouté le {date} · {size}",
|
||||
"share": "Partager"
|
||||
"share": "Partager",
|
||||
"labValuesLink": "Voir {count} valeurs de laboratoire"
|
||||
},
|
||||
"dropOverlay": {
|
||||
"title": "Déposez les fichiers pour les téléverser",
|
||||
@@ -8666,7 +8674,9 @@
|
||||
"coachCheckin": {
|
||||
"title": "Il est temps de revoir un plan",
|
||||
"body": "Cela fait environ une semaine que vous avez défini ce plan : gardez-le, ajustez-le ou laissez-le. Sans pression.",
|
||||
"bodyPlan": "Il y a environ une semaine, vous vous étiez fixé : « {plan} ». Gardez-le, ajustez-le ou laissez-le, sans pression."
|
||||
"bodyPlan": "Il y a environ une semaine, vous vous étiez fixé : « {plan} ». Gardez-le, ajustez-le ou laissez-le, sans pression.",
|
||||
"adjustPrompt": "J'aimerais ajuster mon plan de suivi.",
|
||||
"adjustPromptPlan": "J'aimerais ajuster ce plan : « {plan} »"
|
||||
},
|
||||
"tensionWindow": {
|
||||
"title": "Peut-être un peu de tension",
|
||||
|
||||
+18
-8
@@ -228,7 +228,7 @@
|
||||
"logout": "Esci",
|
||||
"login": "Accedi",
|
||||
"notifications": "Notifiche",
|
||||
"theme": "Aspetto",
|
||||
"theme": "Tema",
|
||||
"themeSystem": "Sistema",
|
||||
"themeDark": "Scuro",
|
||||
"themeLight": "Chiaro",
|
||||
@@ -605,6 +605,7 @@
|
||||
"mood": {
|
||||
"title": "Umore",
|
||||
"subtitle": "Andamento e voci dell’umore",
|
||||
"mentalWellbeingLink": "Fai un check-in sul benessere mentale",
|
||||
"addEntry": "Aggiungi",
|
||||
"editEntry": "Modifica voce",
|
||||
"moodLevel": "Umore",
|
||||
@@ -2894,7 +2895,7 @@
|
||||
"sleep": {
|
||||
"title": "Nessuna voce di sonno",
|
||||
"description": "Quando saranno disponibili dati sul sonno (manuale o tramite Withings / Apple Health), qui apparirà la suddivisione per fase.",
|
||||
"cta": "Collega Withings"
|
||||
"cta": "Collega una fonte"
|
||||
}
|
||||
},
|
||||
"navWorkouts": "Allenamenti",
|
||||
@@ -3262,6 +3263,8 @@
|
||||
},
|
||||
"navWalkingSpeed": "Velocità di cammino",
|
||||
"mood": {
|
||||
"logLink": "Registra un umore",
|
||||
"mentalWellbeingLink": "Fai un check-in sul benessere mentale",
|
||||
"heatmapTitle": "Calendario dell'umore",
|
||||
"heatmapNoEntry": "Nessun dato",
|
||||
"legendGreat": "Ottimo",
|
||||
@@ -3550,7 +3553,8 @@
|
||||
"sectionSubtitle": "Valori combinati di recupero, stress e sforzo letti dai tuoi dispositivi connessi",
|
||||
"loadError": "Al momento non è stato possibile caricare i tuoi punteggi di benessere.",
|
||||
"loadingLabel": "Caricamento dei tuoi indicatori di salute",
|
||||
"recovery": "Recupero",
|
||||
"recovery": "Punteggio di recupero",
|
||||
"recoveryCrossLink": "Vedi i dati di recupero e sforzo del tuo wearable",
|
||||
"stress": "Stress",
|
||||
"strain": "Sforzo"
|
||||
},
|
||||
@@ -3721,6 +3725,7 @@
|
||||
"recovery": {
|
||||
"title": "Recupero",
|
||||
"description": "Quanto bene il tuo corpo si è ripreso: i segnali di recupero, sforzo e carico che il tuo wearable registra durante la notte.",
|
||||
"scoreCrossLink": "Vedi il punteggio di recupero composito",
|
||||
"emptyTitle": "Ancora nessun dato di recupero",
|
||||
"empty": "Ancora nessun dato di recupero. Collega un account WHOOP, Polar o Oura e questi punteggi si popoleranno appena il dispositivo si sincronizza.",
|
||||
"block": {
|
||||
@@ -4009,7 +4014,7 @@
|
||||
},
|
||||
"wrapUp": {
|
||||
"title": "Questa è la visita guidata",
|
||||
"body": "Puoi rivedere ogni modulo singolarmente in qualsiasi momento («Mostra questa visita» su ogni pagina) o riavviare l’intera visita da Impostazioni → Avanzate."
|
||||
"body": "Questa visita guidata viene mostrata una sola volta: non si riaprirà da sola. Tutto ciò che hai visto resta al suo posto e puoi attivare o disattivare le funzioni quando vuoi da Impostazioni → Moduli."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4084,7 +4089,8 @@
|
||||
"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",
|
||||
"modulesCta": "Scegli i tuoi moduli"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"title": "Bentornato",
|
||||
@@ -4549,7 +4555,7 @@
|
||||
"description": "Record strutturati di allergie e anamnesi familiare."
|
||||
},
|
||||
"environment": {
|
||||
"title": "Ambiente",
|
||||
"title": "Posizione e meteo",
|
||||
"subtitle": "Contesto meteo, luce del giorno e posizione.",
|
||||
"disabledHint": "Attiva il modulo Ambiente per impostare una posizione principale e integrare il meteo e le ore di luce giornaliere.",
|
||||
"searchPlaceholder": "Cerca una città",
|
||||
@@ -7959,6 +7965,7 @@
|
||||
"mentalHealth": {
|
||||
"pageTitle": "Benessere mentale",
|
||||
"pageDescription": "Quattro autovalutazioni validate e facoltative — PHQ-9 (umore), GAD-7 (preoccupazione), WHO-5 (benessere) e SCI (sonno) — accanto al tuo diario dell'umore. Ciò che conta è l'andamento nel tempo, non un singolo numero.",
|
||||
"moodLink": "Vedi il tuo andamento dell'umore",
|
||||
"attributionLabel": "Fonte",
|
||||
"attributionShow": "Fonte e licenza di {instrument}",
|
||||
"choosePrompt": "Scegli un check-in",
|
||||
@@ -8404,7 +8411,8 @@
|
||||
"noConditions": "Non collegato a una patologia",
|
||||
"previewUnavailable": "Nessuna anteprima integrata per questo tipo di file",
|
||||
"uploadedMeta": "Aggiunto il {date} · {size}",
|
||||
"share": "Condividi"
|
||||
"share": "Condividi",
|
||||
"labValuesLink": "Vedi {count} valori di laboratorio"
|
||||
},
|
||||
"dropOverlay": {
|
||||
"title": "Rilascia i file per caricarli",
|
||||
@@ -8666,7 +8674,9 @@
|
||||
"coachCheckin": {
|
||||
"title": "È il momento di rivedere un piano",
|
||||
"body": "È passata circa una settimana da quando hai definito questo piano: mantienilo, modificalo o lascialo andare. Senza pressioni.",
|
||||
"bodyPlan": "Circa una settimana fa ti eri proposto: «{plan}». Mantienilo, modificalo o lascialo andare, senza pressioni."
|
||||
"bodyPlan": "Circa una settimana fa ti eri proposto: «{plan}». Mantienilo, modificalo o lascialo andare, senza pressioni.",
|
||||
"adjustPrompt": "Vorrei modificare il mio piano di controllo.",
|
||||
"adjustPromptPlan": "Vorrei modificare questo piano: «{plan}»"
|
||||
},
|
||||
"tensionWindow": {
|
||||
"title": "Forse un po' di tensione oggi",
|
||||
|
||||
+18
-8
@@ -228,7 +228,7 @@
|
||||
"logout": "Wyloguj",
|
||||
"login": "Zaloguj",
|
||||
"notifications": "Powiadomienia",
|
||||
"theme": "Wygląd",
|
||||
"theme": "Motyw",
|
||||
"themeSystem": "Systemowy",
|
||||
"themeDark": "Ciemny",
|
||||
"themeLight": "Jasny",
|
||||
@@ -605,6 +605,7 @@
|
||||
"mood": {
|
||||
"title": "Nastrój",
|
||||
"subtitle": "Trend i wpisy nastroju",
|
||||
"mentalWellbeingLink": "Wykonaj sprawdzian dobrostanu psychicznego",
|
||||
"addEntry": "Dodaj",
|
||||
"editEntry": "Edytuj wpis",
|
||||
"moodLevel": "Nastrój",
|
||||
@@ -2894,7 +2895,7 @@
|
||||
"sleep": {
|
||||
"title": "Brak danych o śnie",
|
||||
"description": "Gdy pojawią się dane o śnie (ręcznie lub przez Withings / Apple Health), wyświetli się tu rozkład faz na noc.",
|
||||
"cta": "Połącz Withings"
|
||||
"cta": "Połącz źródło"
|
||||
}
|
||||
},
|
||||
"navWorkouts": "Treningi",
|
||||
@@ -3262,6 +3263,8 @@
|
||||
},
|
||||
"navWalkingSpeed": "Prędkość chodu",
|
||||
"mood": {
|
||||
"logLink": "Zapisz nastrój",
|
||||
"mentalWellbeingLink": "Wykonaj sprawdzian dobrostanu psychicznego",
|
||||
"heatmapTitle": "Kalendarz nastroju",
|
||||
"heatmapNoEntry": "Brak wpisu",
|
||||
"legendGreat": "Świetnie",
|
||||
@@ -3550,7 +3553,8 @@
|
||||
"sectionSubtitle": "Złożone wartości regeneracji, stresu i obciążenia odczytane z Twoich połączonych urządzeń",
|
||||
"loadError": "Nie udało się teraz wczytać Twoich wyników dobrostanu.",
|
||||
"loadingLabel": "Wczytywanie Twoich wskaźników zdrowia",
|
||||
"recovery": "Regeneracja",
|
||||
"recovery": "Wynik regeneracji",
|
||||
"recoveryCrossLink": "Zobacz dane regeneracji i obciążenia z Twojego urządzenia",
|
||||
"stress": "Stres",
|
||||
"strain": "Obciążenie"
|
||||
},
|
||||
@@ -3721,6 +3725,7 @@
|
||||
"recovery": {
|
||||
"title": "Regeneracja",
|
||||
"description": "Jak dobrze Twój organizm się zregenerował – sygnały regeneracji, obciążenia i load, które Twoje urządzenie rejestruje w nocy.",
|
||||
"scoreCrossLink": "Zobacz łączny wynik regeneracji",
|
||||
"emptyTitle": "Brak danych o regeneracji",
|
||||
"empty": "Brak danych o regeneracji. Połącz konto WHOOP, Polar lub Oura, a te wyniki uzupełnią się po synchronizacji urządzenia.",
|
||||
"block": {
|
||||
@@ -4009,7 +4014,7 @@
|
||||
},
|
||||
"wrapUp": {
|
||||
"title": "To koniec przewodnika",
|
||||
"body": "Każdy moduł możesz obejrzeć osobno w dowolnej chwili („Pokaż ten przewodnik” na każdej stronie) lub uruchomić cały przewodnik ponownie w Ustawienia → Zaawansowane."
|
||||
"body": "Ten przewodnik pokazuje się tylko raz — nie otworzy się ponownie sam. Wszystko, co pokazał, zostaje na miejscu, a funkcje możesz włączać i wyłączać w dowolnym momencie w Ustawienia → Moduły."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4084,7 +4089,8 @@
|
||||
"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ę",
|
||||
"modulesCta": "Wybierz swoje moduły"
|
||||
},
|
||||
"welcomeBack": {
|
||||
"title": "Witaj ponownie",
|
||||
@@ -4549,7 +4555,7 @@
|
||||
"description": "Ustrukturyzowane rekordy alergii i wywiadu rodzinnego."
|
||||
},
|
||||
"environment": {
|
||||
"title": "Środowisko",
|
||||
"title": "Lokalizacja i pogoda",
|
||||
"subtitle": "Kontekst pogody, światła dziennego i lokalizacji.",
|
||||
"disabledHint": "Włącz moduł Środowisko, aby ustawić lokalizację domową i pobierać dzienną pogodę oraz długość dnia.",
|
||||
"searchPlaceholder": "Szukaj miasta",
|
||||
@@ -7959,6 +7965,7 @@
|
||||
"mentalHealth": {
|
||||
"pageTitle": "Dobrostan psychiczny",
|
||||
"pageDescription": "Cztery zwalidowane, dobrowolne testy — PHQ-9 (nastrój), GAD-7 (lęk), WHO-5 (samopoczucie) i SCI (sen) — obok Twojego dziennika nastroju. Liczy się trend w czasie, a nie pojedynczy wynik.",
|
||||
"moodLink": "Zobacz historię swojego nastroju",
|
||||
"attributionLabel": "Źródło",
|
||||
"attributionShow": "Źródło i licencja: {instrument}",
|
||||
"choosePrompt": "Wybierz sprawdzenie",
|
||||
@@ -8404,7 +8411,8 @@
|
||||
"noConditions": "Niepowiązany ze schorzeniem",
|
||||
"previewUnavailable": "Brak podglądu dla tego typu pliku",
|
||||
"uploadedMeta": "Dodano {date} · {size}",
|
||||
"share": "Udostępnij"
|
||||
"share": "Udostępnij",
|
||||
"labValuesLink": "Zobacz {count} wyników laboratoryjnych"
|
||||
},
|
||||
"dropOverlay": {
|
||||
"title": "Upuść pliki, aby je przesłać",
|
||||
@@ -8666,7 +8674,9 @@
|
||||
"coachCheckin": {
|
||||
"title": "Czas przyjrzeć się planowi",
|
||||
"body": "Minął mniej więcej tydzień, odkąd ustaliłeś ten plan — zachowaj go, dostosuj lub odpuść. Bez presji.",
|
||||
"bodyPlan": "Około tygodnia temu postanowiłeś: „{plan}”. Zachowaj to, dostosuj lub odpuść — bez presji."
|
||||
"bodyPlan": "Około tygodnia temu postanowiłeś: „{plan}”. Zachowaj to, dostosuj lub odpuść — bez presji.",
|
||||
"adjustPrompt": "Chciałbym dostosować mój plan kontrolny.",
|
||||
"adjustPromptPlan": "Chciałbym dostosować ten plan: „{plan}”"
|
||||
},
|
||||
"tensionWindow": {
|
||||
"title": "Może chwila napięcia dzisiaj",
|
||||
|
||||
+42
-2
@@ -6,11 +6,13 @@ import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation";
|
||||
import type { CoachNudgeStatus } from "@/components/insights/layout-coach-fab";
|
||||
import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useDisableCoach } from "@/hooks/use-disable-coach";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { coachScopeSourceSchema } from "@/lib/ai/coach/types";
|
||||
|
||||
/**
|
||||
* v1.12.0 (Coach v2 #6) — full-page Coach conversation.
|
||||
@@ -57,6 +59,23 @@ import { apiGet } from "@/lib/api/api-fetch";
|
||||
* most-recent thread so the user lands on what the Coach said.
|
||||
* The search-param read sits in a Suspense child so the client-bailout
|
||||
* never de-opts the route.
|
||||
*
|
||||
* 2026-07-17 UX-flows audit — the route understood only `?c=` / `?doc=`, so
|
||||
* every other cross-surface hand-off (a metric card's "ask about this", the
|
||||
* Today check-in's "Adjust", a workout detail's coach button) dropped its
|
||||
* context at the URL boundary and landed on a blank composer (F1-2 / F4-1 /
|
||||
* F6-1). Two additive params close that gap, both seeding the SAME props
|
||||
* `<CoachConversation>` already exposes for the drawer's suggested-prompt
|
||||
* chips — no new plumbing inside the conversation surface itself:
|
||||
* - `?scope=<CoachScopeSource>` — narrows the snapshot the FIRST turn of a
|
||||
* fresh conversation reads (validated against the closed enum; an
|
||||
* unrecognised value is silently dropped rather than reaching the chat
|
||||
* route with a free-form string).
|
||||
* - `?ask=<text>` — seeds the composer prefill (mirrors `prefill` on the
|
||||
* in-app `askCoach()` launch call). The user still reviews/sends it —
|
||||
* this is a prefill, not an auto-send.
|
||||
* Both are ignored once `?c=` or `?doc=` pin an existing/scoped thread — scope
|
||||
* only ever applies to a fresh conversation's first turn.
|
||||
*/
|
||||
function CoachPageBody() {
|
||||
const searchParams = useSearchParams();
|
||||
@@ -71,13 +90,30 @@ function CoachPageBody() {
|
||||
const rawDoc = searchParams.get("doc");
|
||||
const seedDocumentId = deepLinkedId === null && rawDoc ? rawDoc : null;
|
||||
|
||||
// A fresh conversation only — an existing thread (`?c=`) or a doc-scoped
|
||||
// chat (`?doc=`) keeps its own established scope.
|
||||
const freshChat = deepLinkedId === null && seedDocumentId === null;
|
||||
const rawScope = freshChat ? searchParams.get("scope") : null;
|
||||
const scopeResult = rawScope
|
||||
? coachScopeSourceSchema.safeParse(rawScope)
|
||||
: null;
|
||||
const launchScope: CoachLaunchScope | null = scopeResult?.success
|
||||
? { metric: scopeResult.data }
|
||||
: null;
|
||||
const seedPrefill = freshChat ? searchParams.get("ask") : null;
|
||||
|
||||
// C1 exception — an unread coach-initiated message opens the most-recent
|
||||
// conversation (which holds that proactive turn). Only consulted when the
|
||||
// entry did not pin a specific thread or ask for a fresh chat.
|
||||
// A `?doc=` open is an explicit fresh doc-scoped chat — never override it by
|
||||
// resuming the most-recent thread on an unread nudge.
|
||||
// resuming the most-recent thread on an unread nudge. A `?scope=`/`?ask=`
|
||||
// hand-off is likewise an explicit fresh-chat request.
|
||||
const exceptionEligible =
|
||||
deepLinkedId === null && rawC !== "new" && seedDocumentId === null;
|
||||
deepLinkedId === null &&
|
||||
rawC !== "new" &&
|
||||
seedDocumentId === null &&
|
||||
launchScope === null &&
|
||||
!seedPrefill;
|
||||
const { data: nudge } = useQuery({
|
||||
queryKey: queryKeys.coachNudgeStatus(),
|
||||
queryFn: async (): Promise<CoachNudgeStatus> =>
|
||||
@@ -95,6 +131,10 @@ function CoachPageBody() {
|
||||
// v1.28.51 — seed the document scope for a `?doc=<id>` open so the first
|
||||
// turn is created + sent through the hardened fenced document endpoint.
|
||||
initialDocumentId={seedDocumentId}
|
||||
// 2026-07-17 UX-flows audit F1-2/F4-1/F6-1 — seed the scope/prefill a
|
||||
// cross-surface hand-off carried in the URL.
|
||||
launchScope={launchScope}
|
||||
prefill={seedPrefill}
|
||||
// Default is the new-chat hero; only resume most-recent for the
|
||||
// unread coach-initiated exception.
|
||||
autoOpenMostRecent={hasUnreadCoachMessage}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { Smile } from "lucide-react";
|
||||
import { ArrowRight, Smile } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -41,7 +41,7 @@ interface ComprehensiveMoodData {
|
||||
}
|
||||
|
||||
export default function InsightsStimmungPage() {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { t } = useTranslations();
|
||||
const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
|
||||
|
||||
@@ -101,6 +101,32 @@ export default function InsightsStimmungPage() {
|
||||
The heatmap and the assessment are lifted out of `MoodInsightsSections`
|
||||
into their own regions so they land in this exact order; all three
|
||||
regions share one `moodInsights` query (TanStack dedups the fetch). */}
|
||||
|
||||
{/* 2026-07-17 UX/IA audit M9 — mood, the mental-wellbeing screeners, and
|
||||
this insights page form one mental-health domain; this cross-links
|
||||
into the other two so the trend a user reads here can lead
|
||||
somewhere. */}
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5">
|
||||
<Link
|
||||
href="/mood"
|
||||
data-slot="insights-mood-log-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{t("insights.mood.logLink")}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
{user?.modules?.mentalHealth === true ? (
|
||||
<Link
|
||||
href="/mental-wellbeing"
|
||||
data-slot="insights-mood-mental-wellbeing-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{t("insights.mood.mentalWellbeingLink")}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<MoodInsightsSections region="heatmap" />
|
||||
|
||||
{/* No `<MetricRangeControls>` here: mood is event-driven, not a
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight, Loader2 } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useModulePageGuard } from "@/hooks/use-module-page-guard";
|
||||
@@ -46,6 +47,20 @@ export default function InsightsRecoveryPage() {
|
||||
explainerMetric="recoveryPage"
|
||||
coachLaunch
|
||||
>
|
||||
{/* 2026-07-17 UX/IA audit M4 — reverse direction of the cross-link added
|
||||
to the score-anatomy page: this composite wearable page shares the
|
||||
"Recovery" nav label with the RECOVERY_SCORE anatomy view at
|
||||
`/insights/scores/recovery`, so a user who arrived here looking for
|
||||
the score ring / contributor breakdown gets a pointer instead of a
|
||||
dead end. */}
|
||||
<Link
|
||||
href="/insights/scores/recovery"
|
||||
data-slot="recovery-anatomy-cross-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mt-2 inline-flex items-center gap-1.5 text-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{t("insights.recovery.scoreCrossLink")}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
<RecoverySection />
|
||||
</SubPageShell>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { BackLink } from "@/components/ui/back-link";
|
||||
@@ -94,6 +96,22 @@ export default function CompositeScorePage() {
|
||||
}
|
||||
coachLaunch
|
||||
>
|
||||
{/* 2026-07-17 UX/IA audit M4 — this anatomy view and the `/insights/recovery`
|
||||
composite-wearable page both used to render as plain "Recovery", a
|
||||
same-name/different-content trap: the score ring here promised one
|
||||
thing, the nav pill led somewhere else with no pointer back. The
|
||||
label above is now "Recovery score" (matches `measurements.typeRecoveryScore`);
|
||||
this line closes the loop with an explicit cross-link. */}
|
||||
{metric === "RECOVERY_SCORE" ? (
|
||||
<Link
|
||||
href="/insights/recovery"
|
||||
data-slot="recovery-score-cross-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mt-2 inline-flex items-center gap-1.5 text-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{t("insights.derived.scores.recoveryCrossLink")}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
) : null}
|
||||
<CompositeScoreAnatomy metric={metric} />
|
||||
</SubPageShell>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,11 @@ export default function InsightsWorkoutDetailPage({
|
||||
<WorkoutDetailStats workout={data} />
|
||||
<WorkoutDetailRoute workout={data} />
|
||||
<WorkoutDetailHRChart workout={data} />
|
||||
<CoachLaunchButton />
|
||||
{/* 2026-07-17 UX-flows audit F6-1 — the button used to open an
|
||||
unscoped chat; the user had to restate which session they
|
||||
meant. `workouts` narrows the snapshot the first turn reads
|
||||
(pairs with the coach-scope param added for F4-1). */}
|
||||
<CoachLaunchButton scope={{ metric: "workouts" }} />
|
||||
</>
|
||||
)}
|
||||
</SubPageShell>
|
||||
|
||||
+17
-1
@@ -13,7 +13,7 @@ import { PageHeader } from "@/components/ui/page-header";
|
||||
import { PageAuthGate } from "@/components/ui/page-auth-gate";
|
||||
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Plus, Wrench } from "lucide-react";
|
||||
import { ArrowRight, Plus, Wrench } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -120,6 +120,22 @@ export default function MoodPage() {
|
||||
</ResponsiveSheet>
|
||||
|
||||
<MoodList onAddFirst={() => setDialogOpen(true)} />
|
||||
|
||||
{/* 2026-07-17 UX/IA audit M9 — mood tracking, the mental-wellbeing
|
||||
screeners, and the mood insights page form one mental-health
|
||||
domain but used to be three unconnected islands. A quiet pointer
|
||||
here (module-gated — no nav change) rather than a merge: capture
|
||||
vs. screener stay two defensible surfaces, just cross-linked. */}
|
||||
{user?.modules?.mentalHealth === true ? (
|
||||
<Link
|
||||
href="/mental-wellbeing"
|
||||
data-slot="mood-mental-wellbeing-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{t("mood.mentalWellbeingLink")}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,8 +18,10 @@
|
||||
*/
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
Download,
|
||||
FlaskConical,
|
||||
Pencil,
|
||||
Plus,
|
||||
Share2,
|
||||
@@ -32,6 +34,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { useIllnessEpisodes } from "@/components/illness/use-illness";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DateField } from "@/components/ui/date-field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -230,6 +233,21 @@ export function DocumentDetailSheet({
|
||||
});
|
||||
const doc = detail.data;
|
||||
|
||||
// 2026-07-17 UX/IA audit M5 / F5-5 — the document → lab-values direction
|
||||
// had no UI link at all: an auto-staged (PENDING) or OCR-confirmed
|
||||
// (APPROVED, see `linkOcrLabsToVaultDocument`) OBSERVATION fact already
|
||||
// ties this document to specific lab rows, but nothing on the sheet said
|
||||
// so. There is no per-value route yet (`/labs` has no `?analyte=`
|
||||
// deep-link — see the labs page's own "no per-value relation" comment),
|
||||
// so this links to the labs list rather than a single biomarker; still a
|
||||
// real jump where there was none before.
|
||||
const labFactCount =
|
||||
doc?.facts.filter(
|
||||
(fact) => fact.factType === "OBSERVATION" && fact.status !== "REJECTED",
|
||||
).length ?? 0;
|
||||
const { user } = useAuth();
|
||||
const labsModuleEnabled = user?.modules?.labs !== false;
|
||||
|
||||
const episodes = useIllnessEpisodes(true);
|
||||
|
||||
// AI layer — capability drives the vision/text transport; the mutations run
|
||||
@@ -792,6 +810,18 @@ export function DocumentDetailSheet({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{labsModuleEnabled && labFactCount > 0 ? (
|
||||
<Link
|
||||
href="/labs"
|
||||
data-slot="document-detail-lab-values-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 self-start rounded-md text-sm transition-colors focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
<FlaskConical className="size-4" aria-hidden="true" />
|
||||
{t("documents.detail.labValuesLink", { count: labFactCount })}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
) : null}
|
||||
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("documents.detail.uploadedMeta", {
|
||||
date: format.date(doc.createdAt),
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Sparkles } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { useDisableCoach } from "@/hooks/use-disable-coach";
|
||||
@@ -31,6 +32,13 @@ export interface CoachLaunchButtonProps {
|
||||
label?: string;
|
||||
/** Optional prefill seed for the next Coach turn. */
|
||||
prefill?: string;
|
||||
/**
|
||||
* 2026-07-17 UX-flows audit F6-1 — optional scope so the conversation this
|
||||
* button opens narrows to the relevant source(s) instead of starting from
|
||||
* a blank, unscoped chat. Passed straight through to `askCoach`'s second
|
||||
* argument (mirrors every other ambient-scope call site).
|
||||
*/
|
||||
scope?: CoachLaunchScope;
|
||||
/** Optional className passthrough for inline overrides. */
|
||||
className?: string;
|
||||
/**
|
||||
@@ -49,6 +57,7 @@ export interface CoachLaunchButtonProps {
|
||||
export function CoachLaunchButton({
|
||||
label,
|
||||
prefill,
|
||||
scope,
|
||||
className,
|
||||
variant = "inline",
|
||||
}: CoachLaunchButtonProps) {
|
||||
@@ -83,7 +92,7 @@ export function CoachLaunchButton({
|
||||
data-slot="coach-launch-icon"
|
||||
aria-label={accessibleLabel}
|
||||
title={accessibleLabel}
|
||||
onClick={() => launch.askCoach(prefill ?? null)}
|
||||
onClick={() => launch.askCoach(prefill ?? null, scope)}
|
||||
className={cn("text-muted-foreground hover:text-foreground", className)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
@@ -101,7 +110,7 @@ export function CoachLaunchButton({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-slot="coach-launch-inline"
|
||||
onClick={() => launch.askCoach(prefill ?? null)}
|
||||
onClick={() => launch.askCoach(prefill ?? null, scope)}
|
||||
className={cn("inline-flex h-10 gap-2 self-end", className)}
|
||||
>
|
||||
<Sparkles className="size-4" aria-hidden="true" />
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
* the page someone arrives at to take stock.
|
||||
*/
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { apiGet, apiPost } from "@/lib/api/api-fetch";
|
||||
@@ -143,6 +145,19 @@ export function MentalWellbeing() {
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* 2026-07-17 UX/IA audit M9 — mood tracking, this screener surface,
|
||||
and the mood insights page form one mental-health domain but used
|
||||
to be three unconnected islands. A quiet pointer to mood history,
|
||||
mirroring the one added on `/mood` pointing back here. */}
|
||||
<Link
|
||||
href="/mood"
|
||||
data-slot="mental-wellbeing-mood-link"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 -mt-2 inline-flex items-center gap-1.5 self-start text-sm underline-offset-4 transition-colors hover:underline focus-visible:ring-[3px] focus-visible:outline-none"
|
||||
>
|
||||
{t("mentalHealth.moodLink")}
|
||||
<ArrowRight className="size-3.5" aria-hidden="true" />
|
||||
</Link>
|
||||
|
||||
{/* Per-instrument detail, opened from a card body: last result,
|
||||
trend chart, dated history, Start — the Verlauf lives here,
|
||||
not on the landing. */}
|
||||
|
||||
@@ -15,7 +15,11 @@ const integrationStatusState = vi.hoisted(() => ({
|
||||
data: undefined as
|
||||
| {
|
||||
threshold: number;
|
||||
integrations: Array<{ integration: string; configured?: boolean }>;
|
||||
integrations: Array<{
|
||||
integration: string;
|
||||
configured?: boolean;
|
||||
connected?: boolean;
|
||||
}>;
|
||||
}
|
||||
| undefined,
|
||||
}));
|
||||
@@ -88,4 +92,23 @@ describe("<SourceCardGrid> Withings card", () => {
|
||||
expect(html).not.toContain('data-testid="source-card-withings-setup"');
|
||||
expect(html).toContain("Connect Withings");
|
||||
});
|
||||
|
||||
// 2026-07-17 UX-onboarding audit M6 — the connect CTA opens OAuth in a
|
||||
// new tab; the callback flips the connection in THAT tab while this
|
||||
// wizard tab used to render the same unchanged button forever. Once
|
||||
// `useIntegrationStatuses` (refetches on window focus) reports
|
||||
// `connected: true`, the card must swap to a badge instead of a dead
|
||||
// "Connect Withings" button that would just reopen a redundant flow.
|
||||
it("shows a connected badge instead of the connect button once Withings is linked", () => {
|
||||
integrationStatusState.data = {
|
||||
threshold: 3,
|
||||
integrations: [
|
||||
{ integration: "withings", configured: true, connected: true },
|
||||
],
|
||||
};
|
||||
const html = render();
|
||||
expect(html).toContain('data-testid="source-card-withings-connected"');
|
||||
expect(html).not.toContain('href="/api/withings/connect"');
|
||||
expect(html).not.toContain("Connect Withings");
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,14 @@
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CheckCircle2, FileUp, PlusCircle, Plug, Sparkles } from "lucide-react";
|
||||
import {
|
||||
CheckCircle2,
|
||||
FileUp,
|
||||
PlusCircle,
|
||||
Plug,
|
||||
Settings2,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MedicalDisclaimer } from "@/components/common/medical-disclaimer";
|
||||
@@ -220,13 +227,29 @@ export function DoneScreen() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/settings/export"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<FileUp className="size-3.5" />
|
||||
{t("onboarding.done.importCta")}
|
||||
</Link>
|
||||
{/* 2026-07-17 UX-onboarding audit M5 — the module system (default-on
|
||||
set + opt-ins like nutrients / mental health / documents) is
|
||||
otherwise undiscoverable during first-run: the wizard never
|
||||
mentions it, and the only pointer used to be one tour stop.
|
||||
A quiet link here, beside the existing import link, gives every
|
||||
new account one honest pointer to Settings → Modules before they
|
||||
leave the wizard. */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5">
|
||||
<Link
|
||||
href="/settings/export"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<FileUp className="size-3.5" />
|
||||
{t("onboarding.done.importCta")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/settings/modules"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-sm underline-offset-4 hover:underline"
|
||||
>
|
||||
<Settings2 className="size-3.5" />
|
||||
{t("onboarding.done.modulesCta")}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -249,13 +249,19 @@ export function GettingStartedChecklist() {
|
||||
enabled: checklistRelevant,
|
||||
});
|
||||
|
||||
// v1.5 perf audit: skip these two fetches once the user is past
|
||||
// onboarding. The checklist hides itself anyway via shouldShowChecklist
|
||||
// when onboardingCompletedAt != null AND measurementCount >= 5;
|
||||
// v1.5 perf audit: skip these two fetches once the card can't render at
|
||||
// all (`checklistRelevant`, same gate the meds/AI queries above use).
|
||||
// withings/status and notifications/preferences are unique to this
|
||||
// component and were burning ~950 ms of network on every dashboard load
|
||||
// for established users. See docs/audit/v15-performance.md.
|
||||
const onboardingPending = !!user && user.onboardingCompletedAt == null;
|
||||
//
|
||||
// 2026-07-17 UX audit M1 — this used to read a separate `onboardingPending`
|
||||
// (`onboardingCompletedAt == null`) instead of `checklistRelevant`, so a
|
||||
// user who finished the wizard but still had under 5 measurements kept
|
||||
// seeing the card (`shouldShowChecklist` allows that) while these two
|
||||
// queries stayed disabled forever: "Connect a data source" and
|
||||
// "Configure notifications" could never flip done even after the user
|
||||
// actually connected Withings or a push channel post-wizard.
|
||||
|
||||
// v1.17.0 — any connected data source satisfies the step, so we read
|
||||
// the consolidated integrations envelope (Withings / WHOOP / Fitbit /
|
||||
@@ -265,7 +271,7 @@ export function GettingStartedChecklist() {
|
||||
queryFn: async () => {
|
||||
return apiGet("/api/integrations/status");
|
||||
},
|
||||
enabled: onboardingPending,
|
||||
enabled: checklistRelevant,
|
||||
});
|
||||
|
||||
const { data: notificationsData } = useQuery<NotificationsPreferences>({
|
||||
@@ -273,7 +279,7 @@ export function GettingStartedChecklist() {
|
||||
queryFn: async () => {
|
||||
return apiGet("/api/notifications/preferences");
|
||||
},
|
||||
enabled: onboardingPending,
|
||||
enabled: checklistRelevant,
|
||||
});
|
||||
|
||||
const medicationCount = medsData?.length ?? 0;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
CheckCircle2,
|
||||
ClipboardCheck,
|
||||
FileUp,
|
||||
Plug,
|
||||
@@ -119,6 +120,16 @@ export function SourceCardGrid() {
|
||||
const { data: integrationStatus } = useIntegrationStatuses(isAuthenticated);
|
||||
const withingsConfigured =
|
||||
pickStatus(integrationStatus, "withings")?.configured ?? false;
|
||||
// 2026-07-17 UX-onboarding audit M6 — the connect CTA opens the OAuth
|
||||
// handshake in a new tab; the callback lands THAT tab on
|
||||
// Settings → Integrations while this wizard tab never learned the
|
||||
// connection succeeded. `useIntegrationStatuses` already refetches on
|
||||
// window focus, so returning to this tab after the OAuth round trip
|
||||
// picks up the fresh `connected` flag for free — the only missing piece
|
||||
// was rendering it as a badge instead of leaving the button looking
|
||||
// untouched.
|
||||
const withingsConnected =
|
||||
pickStatus(integrationStatus, "withings")?.connected ?? false;
|
||||
|
||||
async function advance() {
|
||||
if (advancing) return;
|
||||
@@ -164,6 +175,8 @@ export function SourceCardGrid() {
|
||||
}
|
||||
withingsCtaLabel={t("onboarding.source.withings.cta")}
|
||||
withingsConfigured={withingsConfigured}
|
||||
withingsConnected={withingsConnected}
|
||||
withingsConnectedLabel={t("settings.integrationPill.connected")}
|
||||
withingsSetupCtaLabel={t("onboarding.source.withings.setupCta")}
|
||||
withingsSetupHint={t("onboarding.source.withings.setupHint")}
|
||||
/>
|
||||
@@ -260,6 +273,13 @@ interface SourceCardItemProps {
|
||||
* credentials saved — i.e. whether `/api/withings/connect` can
|
||||
* actually succeed right now. */
|
||||
withingsConfigured: boolean;
|
||||
/** Whether Withings is already connected (OAuth completed in the tab the
|
||||
* connect CTA opened). Replaces the connect button with a badge so
|
||||
* returning to this wizard tab shows the outcome instead of an
|
||||
* unchanged card. */
|
||||
withingsConnected: boolean;
|
||||
/** Localised "Connected" label for the badge above. */
|
||||
withingsConnectedLabel: string;
|
||||
/** CTA label shown instead of "Connect Withings" when unconfigured. */
|
||||
withingsSetupCtaLabel: string;
|
||||
/** One-sentence explanation of the BYO credential prerequisite. */
|
||||
@@ -275,6 +295,8 @@ function SourceCardItem({
|
||||
recommendedLabel,
|
||||
withingsCtaLabel,
|
||||
withingsConfigured,
|
||||
withingsConnected,
|
||||
withingsConnectedLabel,
|
||||
withingsSetupCtaLabel,
|
||||
withingsSetupHint,
|
||||
}: SourceCardItemProps) {
|
||||
@@ -325,7 +347,15 @@ function SourceCardItem({
|
||||
>
|
||||
{headerRow}
|
||||
{body}
|
||||
{withingsConfigured ? (
|
||||
{withingsConnected ? (
|
||||
<div
|
||||
className="text-primary flex items-center gap-1.5 pt-1 text-sm font-medium"
|
||||
data-testid="source-card-withings-connected"
|
||||
>
|
||||
<CheckCircle2 className="size-3.5" aria-hidden="true" />
|
||||
{withingsConnectedLabel}
|
||||
</div>
|
||||
) : withingsConfigured ? (
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<a
|
||||
|
||||
@@ -19,6 +19,13 @@
|
||||
*
|
||||
* `parseOAuthOutcome` / `oauthReasonKey` are re-exported from
|
||||
* `connections-panel.tsx` so existing unit-test imports keep resolving.
|
||||
*
|
||||
* 2026-07-17 UX/IA audit M7 — `<NutrientIntakeCard>` (v1.28) moved here from
|
||||
* Settings → Sources. It is a read-only sync inventory (what the Apple
|
||||
* Health nutrients opt-in has stored), not a which-connection-wins ranking —
|
||||
* "Source priority" describes the latter only. Matches the standing
|
||||
* providers→Integrations rule; the card still renders nothing while the
|
||||
* `nutrients` module is off.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
@@ -28,6 +35,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
|
||||
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
|
||||
import { NutrientIntakeCard } from "@/components/settings/nutrient-intake-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export {
|
||||
@@ -55,6 +63,15 @@ export function IntegrationsSection() {
|
||||
)}
|
||||
/>
|
||||
<ConnectionsPanel />
|
||||
|
||||
{/* 2026-07-17 UX/IA audit M7 — the opt-in nutrients sync inventory
|
||||
(moved from Settings → Sources, where "source priority" only
|
||||
ever meant which-connection-wins ranking, not a read-only sync
|
||||
list). The card carries its own header + renders nothing while
|
||||
the `nutrients` module is off, so it slots in as a sibling card
|
||||
under the same Connections group rather than a new labelled
|
||||
section that could paint an orphaned heading over nothing. */}
|
||||
<NutrientIntakeCard />
|
||||
</section>
|
||||
|
||||
{/* Delivery channels ("where it's delivered"). The existing channels
|
||||
|
||||
@@ -45,7 +45,6 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { SettingsCard } from "@/components/settings/settings-card";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { NutrientIntakeCard } from "@/components/settings/nutrient-intake-card";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import {
|
||||
@@ -585,9 +584,11 @@ export function SourcesSection() {
|
||||
</p>
|
||||
</SettingsCard>
|
||||
|
||||
{/* v1.28 — read-only synced-nutrient list for the opt-in `nutrients`
|
||||
module (renders nothing while the module is off). */}
|
||||
<NutrientIntakeCard />
|
||||
{/* v1.28 shipped `<NutrientIntakeCard>` here; the 2026-07-17 UX/IA
|
||||
audit (M7) moved it to Settings → Integrations — it is a read-only
|
||||
Apple-Health sync inventory, not a which-connection-wins ranking,
|
||||
so it never belonged on the "source priority" ladder. See
|
||||
`<IntegrationsSection>` for the card's new home. */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -335,7 +335,14 @@ describe("buildDailyDigest — coach check-in (S3)", () => {
|
||||
expect(item?.body).toContain("every morning → weigh in");
|
||||
// Keep / let-go carry the plan id; adjust is a plain navigation href.
|
||||
expect(item?.actions[0].intent).toBe(`${COACH_CHECKIN_KEEP_INTENT}:p1`);
|
||||
expect(item?.actions[1].href).toBe("/coach");
|
||||
// 2026-07-17 UX-flows audit F1-2 — adjust used to be a bare `/coach` link
|
||||
// dropping all plan context; it now carries `?ask=` seeding the coach
|
||||
// composer with the plan's own words, so the target is the coach route
|
||||
// with the plan text echoed in the query, not a blank chat.
|
||||
expect(item?.actions[1].href).toContain("/coach?ask=");
|
||||
expect(item?.actions[1].href).toContain(
|
||||
encodeURIComponent("every morning → weigh in"),
|
||||
);
|
||||
expect(item?.actions[2].intent).toBe(`${COACH_CHECKIN_LETGO_INTENT}:p1`);
|
||||
});
|
||||
|
||||
|
||||
+13
-1
@@ -451,6 +451,18 @@ function buildCoachCheckinItem(
|
||||
? t("daily.item.coachCheckin.bodyPlan", { plan: plan.planText })
|
||||
: t("daily.item.coachCheckin.body");
|
||||
|
||||
// 2026-07-17 UX-flows audit F1-2 — "Adjust" used to be a bare `/coach`
|
||||
// link carrying no plan context: keep/let-go both thread the plan id
|
||||
// into their intent, but adjust dropped the user into an empty new chat
|
||||
// with no memory of which plan prompted the tap. The coach route's
|
||||
// full-page surface now reads `?ask=` and seeds it as the composer
|
||||
// prefill for a fresh conversation (`CoachConversation`'s `prefill` prop,
|
||||
// same mechanism the drawer's suggested-prompt chips use) — the user
|
||||
// lands with the plan already named instead of a blank composer.
|
||||
const adjustPrompt = plan.planText
|
||||
? t("daily.item.coachCheckin.adjustPromptPlan", { plan: plan.planText })
|
||||
: t("daily.item.coachCheckin.adjustPrompt");
|
||||
|
||||
return {
|
||||
kind: "coach_checkin",
|
||||
title: t("daily.item.coachCheckin.title"),
|
||||
@@ -464,7 +476,7 @@ function buildCoachCheckinItem(
|
||||
{
|
||||
labelKey: "daily.action.checkinAdjust",
|
||||
intent: COACH_CHECKIN_ADJUST_INTENT,
|
||||
href: "/coach",
|
||||
href: `/coach?ask=${encodeURIComponent(adjustPrompt)}`,
|
||||
},
|
||||
{
|
||||
labelKey: "daily.action.checkinLetGo",
|
||||
|
||||
Reference in new issue
Block a user