diff --git a/e2e/onboarding-flicker.spec.ts b/e2e/onboarding-flicker.spec.ts index 04d6fd482..d69f78d61 100644 --- a/e2e/onboarding-flicker.spec.ts +++ b/e2e/onboarding-flicker.spec.ts @@ -242,6 +242,20 @@ test.describe("dashboard onboarding card flicker guard", () => { body: JSON.stringify({ data: { channels: [] }, error: null }), }), ); + // v1.28 — the checklist's "Turn on insights" row self-satisfies from + // `/api/user/ai-provider` (`aiAvailable` presence-only). Stub it so the + // row renders deterministically as not-done and no real request fires + // while the card mounts. + await page.route("**/api/user/ai-provider", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { aiAvailable: false, managedBy: null }, + error: null, + }), + }), + ); // v1.4.16 Wave-C — the dashboard added `/api/dashboard/widgets` // (A5 tile-strip persistence) since this spec was written. Without // a stub the unauthenticated layout query 500s in CI and React diff --git a/messages/de.json b/messages/de.json index 097bc51db..f1a9548d9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3988,10 +3988,7 @@ "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", - "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" + "importCta": "Bestehende Daten importieren" }, "welcomeBack": { "title": "Willkommen zurück", @@ -4033,6 +4030,26 @@ "allergiesLabel": "Allergien oder Unverträglichkeiten", "allergiesPlaceholder": "z. B. Penicillin, Laktose", "privacyNote": "Verschlüsselt gespeichert und nur für besseren Kontext genutzt. Medikamente fragen wir hier nicht ab – die kannst du jederzeit auf der Medikamenten-Seite ergänzen." + }, + "ai": { + "panelTitle": "So sehen Insights aus", + "panelIntro": "Coach, das tägliche Briefing und die Einschätzungen je Messwert übersetzen deine eigenen Zahlen in Klartext. So sieht das aus – bevor du irgendetwas verbindest.", + "showSample": "Beispiel-Briefing ansehen", + "sampleTitle": "Tägliches Briefing", + "sampleTag": "Beispiel", + "sampleAriaLabel": "Beispielhaftes tägliches Briefing", + "sampleBody": "Gewicht in zwei Wochen um 1,2 kg gesunken. Ruhepuls stabil bei 58 bpm. Blutdruck an 6 von 7 Tagen im Zielbereich – der einzige hohe Wert folgte auf ein spätes, salziges Abendessen.", + "sampleCaption": "Nur ein Beispiel – nicht deine eigenen Daten. So liest sich ein Briefing, sobald Insights aktiv sind.", + "ladderTitle": "Drei Wege, es zu aktivieren", + "ladderLocalTitle": "Lokales Modell – nichts verlässt dein Netzwerk", + "ladderLocalBody": "Richte HealthLog auf Ollama oder LM Studio auf deinem eigenen Rechner aus. Kostenlos und vollständig privat – der ruhige Standard.", + "ladderByokTitle": "Eigenen Schlüssel verwenden", + "ladderByokBody": "Nutze deinen eigenen OpenAI- oder Anthropic-Schlüssel. Er läuft über dein Konto und deine Abrechnung, und die API-Stufe hält deine Daten standardmäßig aus dem Training heraus.", + "ladderOauthTitle": "Mit einem Abo anmelden", + "ladderOauthBody": "Verbinde ein ChatGPT-Konto – keine Kosten pro Token, aber die Bedingungen der Verbraucherstufe können Inhalte fürs Training nutzen. Deshalb eine bewusste Wahl, nie der Standard für deine Gesundheitsdaten.", + "keylessLine": "HealthLog ist auch ohne KI vollständig nutzbar. Deine Kacheln, Diagramme, Trends und Exporte funktionieren alle – KI ergänzt nur den Klartext obendrauf.", + "setupCta": "Einrichten (etwa eine Minute)", + "sharedKeyNote": "Diese Instanz stellt bereits einen gemeinsamen KI-Anbieter bereit, deshalb funktionieren Insights für dich sofort – keine Einrichtung nötig." } }, "gettingStarted": { @@ -4056,7 +4073,10 @@ "dataSourceCta": "Verbinden", "notificationsTitle": "Benachrichtigungen einrichten", "notificationsDescription": "Wähle, wie HealthLog dich erinnert — Telegram, ntfy oder Web Push.", - "notificationsCta": "Benachrichtigungen öffnen" + "notificationsCta": "Benachrichtigungen öffnen", + "insightsTitle": "Insights aktivieren (optional)", + "insightsDescription": "Füge ein lokales Modell oder deinen eigenen KI-Schlüssel hinzu, um das tägliche Briefing und Coach freizuschalten.", + "insightsCta": "Insights aktivieren" } }, "trendHints": { diff --git a/messages/en.json b/messages/en.json index bd988529d..c592d27a3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3988,10 +3988,7 @@ "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", - "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" + "importCta": "Import existing history" }, "welcomeBack": { "title": "Welcome back", @@ -4033,6 +4030,26 @@ "allergiesLabel": "Allergies or intolerances", "allergiesPlaceholder": "e.g. penicillin, lactose", "privacyNote": "Stored encrypted and used only to give you better context. We don't ask for your medications here — add those anytime from the Medications page." + }, + "ai": { + "panelTitle": "See what insights look like", + "panelIntro": "Coach, the daily briefing and the per-metric reads turn your own numbers into plain language. Here's the shape of it — before you connect anything.", + "showSample": "See a sample briefing", + "sampleTitle": "Daily Briefing", + "sampleTag": "Example", + "sampleAriaLabel": "Example daily briefing", + "sampleBody": "Weight down 1.2 kg over two weeks. Resting pulse steady at 58 bpm. Blood pressure in range on 6 of 7 days — the one high reading followed a late, salty dinner.", + "sampleCaption": "Example only — not your own data. This is what a briefing reads like once insights are on.", + "ladderTitle": "Three ways to turn it on", + "ladderLocalTitle": "Local model — nothing leaves your network", + "ladderLocalBody": "Point HealthLog at Ollama or LM Studio on your own machine. Free and fully private — the calm default.", + "ladderByokTitle": "Bring your own key", + "ladderByokBody": "Use your own OpenAI or Anthropic key. It runs under your account and billing, and the API tier keeps your data out of training by default.", + "ladderOauthTitle": "Sign in with a subscription", + "ladderOauthBody": "Connect a ChatGPT account — no per-token cost, but the consumer tier's terms may use content for training, so it's a deliberate choice, never the default for your health data.", + "keylessLine": "HealthLog is fully useful without AI. Your tiles, charts, trends and exports all work — AI only adds the plain-language read on top.", + "setupCta": "Set it up (about a minute)", + "sharedKeyNote": "This instance already provides a shared AI provider, so insights work for you right now — no setup needed." } }, "gettingStarted": { @@ -4056,7 +4073,10 @@ "dataSourceCta": "Connect", "notificationsTitle": "Configure notifications", "notificationsDescription": "Choose how HealthLog should remind you — Telegram, ntfy or Web Push.", - "notificationsCta": "Open notifications" + "notificationsCta": "Open notifications", + "insightsTitle": "Turn on insights (optional)", + "insightsDescription": "Add a local model or your own AI key to unlock the daily briefing and Coach.", + "insightsCta": "Turn on insights" } }, "trendHints": { diff --git a/messages/es.json b/messages/es.json index b38a7e064..6f7029960 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3988,10 +3988,7 @@ "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", - "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" + "importCta": "Importar historial existente" }, "welcomeBack": { "title": "Bienvenido de nuevo", @@ -4033,6 +4030,26 @@ "allergiesLabel": "Alergias o intolerancias", "allergiesPlaceholder": "p. ej. penicilina, lactosa", "privacyNote": "Se almacena cifrado y solo se usa para darte mejor contexto. Aquí no te pedimos tu medicación: puedes añadirla cuando quieras desde la página de Medicación." + }, + "ai": { + "panelTitle": "Mira cómo son los insights", + "panelIntro": "Coach, el resumen diario y las lecturas por métrica convierten tus propios números en lenguaje claro. Así se ve, antes de conectar nada.", + "showSample": "Ver un resumen de ejemplo", + "sampleTitle": "Resumen diario", + "sampleTag": "Ejemplo", + "sampleAriaLabel": "Resumen diario de ejemplo", + "sampleBody": "Peso 1,2 kg menos en dos semanas. Pulso en reposo estable en 58 lpm. Tensión dentro del rango 6 de 7 días — la única lectura alta llegó tras una cena tardía y salada.", + "sampleCaption": "Solo un ejemplo — no son tus datos. Así se lee un resumen cuando los insights están activos.", + "ladderTitle": "Tres formas de activarlo", + "ladderLocalTitle": "Modelo local — nada sale de tu red", + "ladderLocalBody": "Apunta HealthLog a Ollama o LM Studio en tu propia máquina. Gratis y totalmente privado — la opción tranquila por defecto.", + "ladderByokTitle": "Usa tu propia clave", + "ladderByokBody": "Usa tu propia clave de OpenAI o Anthropic. Funciona con tu cuenta y facturación, y el nivel de API mantiene tus datos fuera del entrenamiento por defecto.", + "ladderOauthTitle": "Inicia sesión con una suscripción", + "ladderOauthBody": "Conecta una cuenta de ChatGPT — sin coste por token, pero los términos del nivel de consumo pueden usar el contenido para entrenamiento, así que es una elección deliberada, nunca la opción por defecto para tus datos de salud.", + "keylessLine": "HealthLog es plenamente útil sin IA. Tus paneles, gráficos, tendencias y exportaciones funcionan — la IA solo añade la lectura en lenguaje claro por encima.", + "setupCta": "Configúralo (un minuto aprox.)", + "sharedKeyNote": "Esta instancia ya ofrece un proveedor de IA compartido, así que los insights funcionan para ti ahora mismo — sin configuración." } }, "gettingStarted": { @@ -4056,7 +4073,10 @@ "dataSourceCta": "Conectar", "notificationsTitle": "Configurar notificaciones", "notificationsDescription": "Elige cómo te recuerda HealthLog — Telegram, ntfy o Web Push.", - "notificationsCta": "Abrir notificaciones" + "notificationsCta": "Abrir notificaciones", + "insightsTitle": "Activar insights (opcional)", + "insightsDescription": "Añade un modelo local o tu propia clave de IA para desbloquear el resumen diario y Coach.", + "insightsCta": "Activar insights" } }, "trendHints": { diff --git a/messages/fr.json b/messages/fr.json index 806eb8370..d954c75d1 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3988,10 +3988,7 @@ "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", - "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" + "importCta": "Importer un historique existant" }, "welcomeBack": { "title": "Bon retour", @@ -4033,6 +4030,26 @@ "allergiesLabel": "Allergies ou intolérances", "allergiesPlaceholder": "p. ex. pénicilline, lactose", "privacyNote": "Stocké chiffré et utilisé uniquement pour un meilleur contexte. Nous ne demandons pas vos médicaments ici — ajoutez-les quand vous voulez depuis la page Médicaments." + }, + "ai": { + "panelTitle": "Voyez à quoi ressemblent les insights", + "panelIntro": "Coach, le briefing quotidien et les lectures par mesure traduisent vos propres chiffres en langage clair. Voici l'idée, avant de connecter quoi que ce soit.", + "showSample": "Voir un exemple de briefing", + "sampleTitle": "Briefing quotidien", + "sampleTag": "Exemple", + "sampleAriaLabel": "Exemple de briefing quotidien", + "sampleBody": "Poids en baisse de 1,2 kg en deux semaines. Pouls au repos stable à 58 bpm. Tension dans la plage 6 jours sur 7 — la seule valeur élevée a suivi un dîner tardif et salé.", + "sampleCaption": "Exemple uniquement — pas vos données. Voilà à quoi ressemble un briefing une fois les insights activés.", + "ladderTitle": "Trois façons de l'activer", + "ladderLocalTitle": "Modèle local — rien ne quitte votre réseau", + "ladderLocalBody": "Pointez HealthLog vers Ollama ou LM Studio sur votre propre machine. Gratuit et totalement privé — le choix calme par défaut.", + "ladderByokTitle": "Apportez votre clé", + "ladderByokBody": "Utilisez votre propre clé OpenAI ou Anthropic. Elle fonctionne avec votre compte et votre facturation, et le niveau API exclut vos données de l'entraînement par défaut.", + "ladderOauthTitle": "Connectez-vous avec un abonnement", + "ladderOauthBody": "Reliez un compte ChatGPT — sans coût par jeton, mais les conditions du niveau grand public peuvent utiliser le contenu pour l'entraînement : c'est donc un choix délibéré, jamais l'option par défaut pour vos données de santé.", + "keylessLine": "HealthLog est pleinement utile sans IA. Vos tuiles, graphiques, tendances et exports fonctionnent tous — l'IA n'ajoute que la lecture en langage clair par-dessus.", + "setupCta": "Configurez-le (environ une minute)", + "sharedKeyNote": "Cette instance fournit déjà un fournisseur d'IA partagé : les insights fonctionnent pour vous dès maintenant — aucune configuration nécessaire." } }, "gettingStarted": { @@ -4056,7 +4073,10 @@ "dataSourceCta": "Connecter", "notificationsTitle": "Configurer les notifications", "notificationsDescription": "Choisis comment HealthLog te prévient — Telegram, ntfy ou Web Push.", - "notificationsCta": "Ouvrir les notifications" + "notificationsCta": "Ouvrir les notifications", + "insightsTitle": "Activer les insights (facultatif)", + "insightsDescription": "Ajoutez un modèle local ou votre propre clé IA pour débloquer le briefing quotidien et Coach.", + "insightsCta": "Activer les insights" } }, "trendHints": { diff --git a/messages/it.json b/messages/it.json index 5af82562b..2d09de6cd 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3988,10 +3988,7 @@ "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", - "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" + "importCta": "Importa la cronologia esistente" }, "welcomeBack": { "title": "Bentornato", @@ -4033,6 +4030,26 @@ "allergiesLabel": "Allergie o intolleranze", "allergiesPlaceholder": "es. penicillina, lattosio", "privacyNote": "Memorizzato cifrato e usato solo per darti un contesto migliore. Qui non chiediamo i tuoi farmaci: aggiungili quando vuoi dalla pagina Farmaci." + }, + "ai": { + "panelTitle": "Guarda come sono gli insight", + "panelIntro": "Coach, il briefing quotidiano e le letture per metrica traducono i tuoi numeri in linguaggio chiaro. Ecco com'è, prima di collegare qualsiasi cosa.", + "showSample": "Vedi un briefing di esempio", + "sampleTitle": "Briefing quotidiano", + "sampleTag": "Esempio", + "sampleAriaLabel": "Briefing quotidiano di esempio", + "sampleBody": "Peso in calo di 1,2 kg in due settimane. Battito a riposo stabile a 58 bpm. Pressione nel range in 6 giorni su 7 — l'unico valore alto è arrivato dopo una cena tardiva e salata.", + "sampleCaption": "Solo un esempio — non i tuoi dati. Ecco come si legge un briefing quando gli insight sono attivi.", + "ladderTitle": "Tre modi per attivarlo", + "ladderLocalTitle": "Modello locale — nulla lascia la tua rete", + "ladderLocalBody": "Punta HealthLog su Ollama o LM Studio sulla tua macchina. Gratis e del tutto privato — la scelta tranquilla di default.", + "ladderByokTitle": "Usa la tua chiave", + "ladderByokBody": "Usa la tua chiave OpenAI o Anthropic. Gira sul tuo account e sulla tua fatturazione, e il piano API tiene i tuoi dati fuori dall'addestramento in modo predefinito.", + "ladderOauthTitle": "Accedi con un abbonamento", + "ladderOauthBody": "Collega un account ChatGPT — nessun costo per token, ma i termini del piano consumer possono usare i contenuti per l'addestramento, quindi è una scelta deliberata, mai l'impostazione predefinita per i tuoi dati sanitari.", + "keylessLine": "HealthLog è pienamente utile anche senza IA. Le tue schede, i grafici, i trend e le esportazioni funzionano tutti — l'IA aggiunge solo la lettura in linguaggio chiaro.", + "setupCta": "Configuralo (circa un minuto)", + "sharedKeyNote": "Questa istanza fornisce già un provider IA condiviso, quindi gli insight funzionano per te fin da subito — nessuna configurazione necessaria." } }, "gettingStarted": { @@ -4056,7 +4073,10 @@ "dataSourceCta": "Collega", "notificationsTitle": "Configura le notifiche", "notificationsDescription": "Scegli come HealthLog ti ricorda — Telegram, ntfy o Web Push.", - "notificationsCta": "Apri notifiche" + "notificationsCta": "Apri notifiche", + "insightsTitle": "Attiva gli insight (facoltativo)", + "insightsDescription": "Aggiungi un modello locale o la tua chiave IA per sbloccare il briefing quotidiano e Coach.", + "insightsCta": "Attiva gli insight" } }, "trendHints": { diff --git a/messages/pl.json b/messages/pl.json index a4fca6cf0..2d5c5995b 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3988,10 +3988,7 @@ "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ę", - "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" + "importCta": "Importuj istniejącą historię" }, "welcomeBack": { "title": "Witaj ponownie", @@ -4033,6 +4030,26 @@ "allergiesLabel": "Alergie lub nietolerancje", "allergiesPlaceholder": "np. penicylina, laktoza", "privacyNote": "Przechowywane w postaci zaszyfrowanej i używane tylko po to, by dać Ci lepszy kontekst. Nie pytamy tu o leki — dodasz je w dowolnej chwili na stronie Leki." + }, + "ai": { + "panelTitle": "Zobacz, jak wyglądają insighty", + "panelIntro": "Coach, codzienny briefing i oceny dla poszczególnych metryk zamieniają Twoje własne liczby w prosty język. Oto jak to wygląda — zanim cokolwiek podłączysz.", + "showSample": "Zobacz przykładowy briefing", + "sampleTitle": "Codzienny briefing", + "sampleTag": "Przykład", + "sampleAriaLabel": "Przykładowy codzienny briefing", + "sampleBody": "Waga niższa o 1,2 kg w ciągu dwóch tygodni. Tętno spoczynkowe stabilne na 58 bpm. Ciśnienie w normie przez 6 z 7 dni — jedyny wysoki odczyt nastąpił po późnej, słonej kolacji.", + "sampleCaption": "Tylko przykład — to nie są Twoje dane. Tak czyta się briefing, gdy insighty są włączone.", + "ladderTitle": "Trzy sposoby, by to włączyć", + "ladderLocalTitle": "Model lokalny — nic nie opuszcza Twojej sieci", + "ladderLocalBody": "Skieruj HealthLog na Ollama lub LM Studio na własnej maszynie. Za darmo i w pełni prywatnie — spokojny wybór domyślny.", + "ladderByokTitle": "Użyj własnego klucza", + "ladderByokBody": "Użyj własnego klucza OpenAI lub Anthropic. Działa na Twoim koncie i rozliczeniu, a poziom API domyślnie wyklucza Twoje dane z treningu.", + "ladderOauthTitle": "Zaloguj się przez subskrypcję", + "ladderOauthBody": "Podłącz konto ChatGPT — bez kosztu za token, ale warunki poziomu konsumenckiego mogą wykorzystywać treści do treningu, więc to świadomy wybór, nigdy domyślny dla danych o zdrowiu.", + "keylessLine": "HealthLog jest w pełni użyteczny bez AI. Twoje kafelki, wykresy, trendy i eksporty działają — AI dokłada tylko odczyt prostym językiem.", + "setupCta": "Skonfiguruj (około minuty)", + "sharedKeyNote": "Ta instancja udostępnia już wspólnego dostawcę AI, więc insighty działają dla Ciebie od razu — bez konfiguracji." } }, "gettingStarted": { @@ -4056,7 +4073,10 @@ "dataSourceCta": "Połącz", "notificationsTitle": "Skonfiguruj powiadomienia", "notificationsDescription": "Wybierz, jak HealthLog ma ci przypominać — Telegram, ntfy lub Web Push.", - "notificationsCta": "Otwórz powiadomienia" + "notificationsCta": "Otwórz powiadomienia", + "insightsTitle": "Włącz insighty (opcjonalnie)", + "insightsDescription": "Dodaj model lokalny lub własny klucz AI, aby odblokować codzienny briefing i Coach.", + "insightsCta": "Włącz insighty" } }, "trendHints": { diff --git a/src/components/onboarding/__tests__/done-screen.test.tsx b/src/components/onboarding/__tests__/done-screen.test.tsx new file mode 100644 index 000000000..72038c37b --- /dev/null +++ b/src/components/onboarding/__tests__/done-screen.test.tsx @@ -0,0 +1,83 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; + +/** + * The elevated done-screen AI panel must lead with value and gate nothing: + * it shows the local-first ladder and the honest "useful without AI" line, + * yet the three original exits (connect / log / dashboard) stay intact, so + * a keyless user is never stranded. The shared-key note only appears when + * the deployment's provider serves the user (`managedBy === "server"`). + */ + +const aiProviderState = vi.hoisted(() => ({ + data: undefined as { managedBy?: string | null } | undefined, +})); + +vi.mock("@tanstack/react-query", () => ({ + useQuery: () => ({ data: aiProviderState.data }), +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: { id: "u1" } }), +})); + +vi.mock("@/components/onboarding/tour-launcher", () => ({ + setTourReferrer: () => {}, +})); + +vi.mock("next/link", () => ({ + default: ({ + href, + children, + ...rest + }: { + href: string; + children: React.ReactNode; + }) => ( + + {children} + + ), +})); + +import { DoneScreen } from "../done-screen"; + +function render() { + return renderToStaticMarkup( + + + , + ); +} + +describe(" AI panel", () => { + it("leads with the value-first panel and the local-first ladder", () => { + aiProviderState.data = undefined; + const html = render(); + expect(html).toContain('data-slot="onboarding-ai-panel"'); + // Local model is surfaced first (the calm private default). + expect(html).toContain("nothing leaves your network"); + // The honest release valve is present and prominent. + expect(html).toContain('data-slot="onboarding-ai-keyless"'); + expect(html).toContain("fully useful without AI"); + // Setup is a single optional deep-link, not a gate. + expect(html).toContain('href="/settings/ai"'); + }); + + it("keeps every exit so the panel is fully skippable", () => { + aiProviderState.data = undefined; + const html = render(); + expect(html).toContain('href="/settings/integrations"'); + expect(html).toContain('href="/measurements"'); + expect(html).toContain('href="/"'); + }); + + it("shows the shared-key note only when the operator key serves the user", () => { + aiProviderState.data = { managedBy: "user" }; + expect(render()).not.toContain('data-slot="onboarding-ai-shared-key"'); + aiProviderState.data = { managedBy: "server" }; + expect(render()).toContain('data-slot="onboarding-ai-shared-key"'); + }); +}); diff --git a/src/components/onboarding/__tests__/sample-briefing-card.test.tsx b/src/components/onboarding/__tests__/sample-briefing-card.test.tsx new file mode 100644 index 000000000..6d3b38cc1 --- /dev/null +++ b/src/components/onboarding/__tests__/sample-briefing-card.test.tsx @@ -0,0 +1,41 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; + +import { I18nProvider } from "@/lib/i18n/context"; +import { SampleBriefingCard } from "../sample-briefing-card"; + +/** + * The onboarding "aha" artifact must be a STATIC, clearly-labelled example + * with zero egress: no provider is configured when it renders, so it can + * never make a model call. These tests pin both properties — the "Example" + * tag is present (so it's never mistaken for real data) and nothing on the + * component touches the network. + */ + +function render(node: React.ReactElement) { + return renderToStaticMarkup( + {node}, + ); +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe("", () => { + it("renders the sample briefing with an explicit Example tag", () => { + const html = render(); + expect(html).toContain("Example"); + // The worked-example numbers are fixed copy, never derived from a user. + expect(html).toContain("1.2 kg"); + expect(html).toContain('data-slot="sample-briefing-tag"'); + // The caption states plainly that this is not the user's data. + expect(html).toContain("not your own data"); + }); + + it("makes no network call — no provider exists yet, zero egress", () => { + const fetchSpy = vi.spyOn(globalThis, "fetch"); + render(); + expect(fetchSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/src/components/onboarding/done-screen.tsx b/src/components/onboarding/done-screen.tsx index e08f67ada..083a3d57e 100644 --- a/src/components/onboarding/done-screen.tsx +++ b/src/components/onboarding/done-screen.tsx @@ -1,14 +1,24 @@ "use client"; import Link from "next/link"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { CheckCircle2, FileUp, PlusCircle, Plug, Sparkles } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { MedicalDisclaimer } from "@/components/common/medical-disclaimer"; +import { SampleBriefingCard } from "@/components/onboarding/sample-briefing-card"; import { useAuth } from "@/hooks/use-auth"; import { setTourReferrer } from "@/components/onboarding/tour-launcher"; +import { queryKeys } from "@/lib/query-keys"; +import { apiGet } from "@/lib/api/api-fetch"; import { useTranslations } from "@/lib/i18n/context"; +interface AiProviderStatus { + /** Origin of the provider that would serve this user, if any. */ + managedBy?: "user" | "local" | "server" | null; +} + /** * v1.4.25 W14b-Content — onboarding step 4 (done). * @@ -32,11 +42,25 @@ import { useTranslations } from "@/lib/i18n/context"; export function DoneScreen() { const { t } = useTranslations(); const { user } = useAuth(); + const [sampleOpen, setSampleOpen] = useState(false); useEffect(() => { if (user?.id) setTourReferrer(user.id); }, [user?.id]); + // The shared-key note is the ONLY honest divergence in the panel: on a + // deployment that ships an operator key, insights already work for the + // user, so we say so plainly instead of pushing a BYOK/local setup they + // don't need. Presence-only read; no key material is exposed. + const { data: aiProvider } = useQuery({ + queryKey: queryKeys.userAiProvider(), + queryFn: async () => { + return apiGet("/api/user/ai-provider"); + }, + enabled: !!user, + }); + const sharedKeyServes = aiProvider?.managedBy === "server"; + return (
- {/* v1.18.6 — surface the AI Insights / Coach / briefing setup at - the highest-intent moment. The flagship feature needs a BYOK or - local provider that the rest of onboarding never mentions, so a - fresh user otherwise discovers it as a cold error. This card is - skippable by construction — it is an optional deep-link, not a - gate. */} -
-
+