diff --git a/blotztask-api/Modules/Reviews/Domain/ReviewLetter.cs b/blotztask-api/Modules/Reviews/Domain/ReviewLetter.cs
index dbdac3cd0..d1df5164f 100644
--- a/blotztask-api/Modules/Reviews/Domain/ReviewLetter.cs
+++ b/blotztask-api/Modules/Reviews/Domain/ReviewLetter.cs
@@ -3,7 +3,8 @@
namespace BlotzTask.Modules.Reviews.Domain;
-// Theme and OneThingToTryNext are optional: the model omits them for a quiet period.
+// Theme and OneThingToTryNext are optional: the model omits Theme for a quiet period, and
+// OneThingToTryNext when the data doesn't support a specific suggestion.
public record ReviewLetter(string Body, string? Theme, string? OneThingToTryNext);
public static class ReviewLetterParser
diff --git a/blotztask-api/Modules/Reviews/Prompts/ReviewPrompts.cs b/blotztask-api/Modules/Reviews/Prompts/ReviewPrompts.cs
index 91d367ae7..274eadd53 100644
--- a/blotztask-api/Modules/Reviews/Prompts/ReviewPrompts.cs
+++ b/blotztask-api/Modules/Reviews/Prompts/ReviewPrompts.cs
@@ -55,7 +55,7 @@ public static string GetReviewPrompt(
First, decide which tasks carry real signal about how the user actually spent or intended their time. Disregard entries that are not genuine activity — placeholder, sample or example content, tests, or text that reads as random rather than a real task. Base everything below only on the tasks that remain.
- If little or no genuine activity remains after that, do NOT invent themes, patterns, or meaning. Put two or three warm, honest sentences acknowledging it was a quiet {periodNoun} in "body", leave "theme" and "oneThingToTryNext" null, and stop there.
+ If little or no genuine activity remains after that, do NOT invent themes, patterns, or meaning. Put two or three warm, honest sentences acknowledging it was a quiet {periodNoun} in "body" and leave "theme" null. Still write "oneThingToTryNext": for a quiet {periodNoun} it is not advice drawn from the data but one small, kind invitation for next {periodNoun} — something easy to say yes to, like noting one moment a day, picking one small thing to finish each week, or setting aside fifteen minutes for something they enjoy. These are only examples — vary the invitation and do not reuse them word for word. Then stop there.
Otherwise, write ONE short {reviewKind} review letter in three parts:
diff --git a/blotztask-mobile/src/feature/review/components/letter-card-content.tsx b/blotztask-mobile/src/feature/review/components/letter-card-content.tsx
index c546c8e69..f76b222f6 100644
--- a/blotztask-mobile/src/feature/review/components/letter-card-content.tsx
+++ b/blotztask-mobile/src/feature/review/components/letter-card-content.tsx
@@ -1,11 +1,15 @@
import { Text, View } from "react-native";
import { useTranslation } from "react-i18next";
import { CustomSpinner } from "@/shared/components/custom-spinner";
+import { FormDivider } from "@/shared/components/form-divider";
import { ReviewPeriodType, ReviewReportDTO } from "../models/review-dto";
import { LetterBody } from "./letter-body";
import { LetterGeneratingState } from "./letter-generating-state";
+import { LetterNextStep } from "./letter-next-step";
import { LetterSignature } from "./letter-signature";
+import { LetterStats } from "./letter-stats";
+import { LetterTheme } from "./letter-theme";
import { MonthlyLetterInProgressState } from "./monthly-letter-in-progress-state";
import { LetterReadyState } from "./letter-ready-state";
import { LetterStamp } from "./letter-stamp";
@@ -49,9 +53,33 @@ export function LetterCardContent({
} else if (isCurrentMonth) {
content = ;
} else if (report) {
+ // Theme is what marks a letter as written in parts — a quiet month and a pre-split letter
+ // both leave it null, and both render body-only. A quiet month still gets a suggestion.
+ const theme = periodType === ReviewPeriodType.Monthly ? report.theme : null;
+ const nextStep = periodType === ReviewPeriodType.Monthly ? report.oneThingToTryNext : null;
+
content = (
<>
+ {theme != null && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+
+ {nextStep != null && }
+
+ {(theme != null || nextStep != null) && (
+
+
+
+ )}
+
{t(`${ns}.aiDisclosure`)}
diff --git a/blotztask-mobile/src/feature/review/components/letter-next-step.tsx b/blotztask-mobile/src/feature/review/components/letter-next-step.tsx
new file mode 100644
index 000000000..7bc3d96ab
--- /dev/null
+++ b/blotztask-mobile/src/feature/review/components/letter-next-step.tsx
@@ -0,0 +1,22 @@
+import { Text, View } from "react-native";
+import { useTranslation } from "react-i18next";
+
+type Props = {
+ suggestion: string;
+};
+
+export function LetterNextStep({ suggestion }: Props) {
+ const { t } = useTranslation("settings");
+
+ return (
+
+
+ {t("monthlyReview.nextMonthTitle")}
+
+
+
+ {suggestion}
+
+
+ );
+}
diff --git a/blotztask-mobile/src/feature/review/components/letter-stats.tsx b/blotztask-mobile/src/feature/review/components/letter-stats.tsx
new file mode 100644
index 000000000..c5846e9a0
--- /dev/null
+++ b/blotztask-mobile/src/feature/review/components/letter-stats.tsx
@@ -0,0 +1,54 @@
+import { Text, View } from "react-native";
+import MaterialCommunityIcons from "@react-native-vector-icons/material-design-icons/static";
+import { useTranslation } from "react-i18next";
+
+type Props = {
+ tasksCompleted: number | null;
+};
+
+type Stat = {
+ key: string;
+ value: number;
+ label: string;
+ icon: "check-circle";
+ color: string;
+};
+
+// The design shows three stats, but only tasksCompleted has a data source today — so the row
+// renders whichever stats it was given rather than a fixed three.
+export function LetterStats({ tasksCompleted }: Props) {
+ const { t } = useTranslation("settings");
+
+ const stats: Stat[] = [];
+
+ // Hide a zero count — the letter is gentle about unfinished tasks, and a big green "0" reads harsh.
+ if (tasksCompleted !== null && tasksCompleted > 0) {
+ stats.push({
+ key: "tasksCompleted",
+ value: tasksCompleted,
+ label: t("monthlyReview.tasksCompleted"),
+ icon: "check-circle",
+ color: "#84CC16",
+ });
+ }
+
+ if (stats.length === 0) return null;
+
+ return (
+
+ {stats.map((stat) => (
+
+
+
+
+
+ {stat.value}
+
+
+
+ {stat.label}
+
+ ))}
+
+ );
+}
diff --git a/blotztask-mobile/src/feature/review/components/letter-theme.tsx b/blotztask-mobile/src/feature/review/components/letter-theme.tsx
new file mode 100644
index 000000000..201c6ff7c
--- /dev/null
+++ b/blotztask-mobile/src/feature/review/components/letter-theme.tsx
@@ -0,0 +1,20 @@
+import { Text, View } from "react-native";
+import { useTranslation } from "react-i18next";
+
+type Props = {
+ theme: string;
+};
+
+export function LetterTheme({ theme }: Props) {
+ const { t } = useTranslation("settings");
+
+ return (
+
+
+ {t("monthlyReview.themeLabel")}
+
+
+ {theme}
+
+ );
+}
diff --git a/blotztask-mobile/src/feature/review/models/review-dto.ts b/blotztask-mobile/src/feature/review/models/review-dto.ts
index 7e4893b48..4417df306 100644
--- a/blotztask-mobile/src/feature/review/models/review-dto.ts
+++ b/blotztask-mobile/src/feature/review/models/review-dto.ts
@@ -9,7 +9,14 @@ export type ReviewReportDTO = {
periodType: ReviewPeriodType;
periodStartLocal: string;
periodEndLocalExclusive: string;
+ // letter holds the body; theme and oneThingToTryNext are null on letters written before the
+ // backend split them out. theme is also null on a period too quiet to name one, and
+ // oneThingToTryNext when the data doesn't support a specific suggestion.
letter: string | null;
+ theme: string | null;
+ oneThingToTryNext: string | null;
+ // Counted live from the tasks, so it has a value even for a period with no letter.
+ tasksCompleted: number;
isLowActivity: boolean;
generatedAtUtc: string | null;
};
diff --git a/blotztask-mobile/src/i18n/locales/en/settings.json b/blotztask-mobile/src/i18n/locales/en/settings.json
index 28c394ca3..b07b2eb18 100644
--- a/blotztask-mobile/src/i18n/locales/en/settings.json
+++ b/blotztask-mobile/src/i18n/locales/en/settings.json
@@ -83,6 +83,9 @@
"comingSoonTitle": "Your first letter is being written...",
"comingSoonBody": "Your first Monthly Letter will be ready after you've recorded for 1 month.",
"aiDisclosure": "Generated by AI from the tasks you created this month",
+ "themeLabel": "Theme of the month",
+ "tasksCompleted": "tasks completed",
+ "nextMonthTitle": "🎯 One thing to try next month",
"readyTitle": "Your {{periodLabel}} Letter is ready.",
"readLetter": "Read letter",
"recordToday": "Record Today",
diff --git a/blotztask-mobile/src/i18n/locales/zh/settings.json b/blotztask-mobile/src/i18n/locales/zh/settings.json
index fbce44203..5dc9c86d1 100644
--- a/blotztask-mobile/src/i18n/locales/zh/settings.json
+++ b/blotztask-mobile/src/i18n/locales/zh/settings.json
@@ -83,6 +83,9 @@
"comingSoonTitle": "你的第一封信正在路上 ✨",
"comingSoonBody": "月度回顾会在你使用 Blotz 满一个整月后送达。继续记录你的任务,很快就会有一封信等着你!🌱",
"aiDisclosure": "由AI根据您本月创建的任务生成",
+ "themeLabel": "本月主题",
+ "tasksCompleted": "完成任务",
+ "nextMonthTitle": "🎯 下个月可以试试",
"recordToday": "记录今天",
"readyTitle": "你的{{periodLabel}}月度信件已准备好。",
"readLetter": "阅读信件",