From 9017a1b5d41c9539f7b77b0cb552edefc2c0e3ae Mon Sep 17 00:00:00 2001 From: sohamd22 <85427822+sohamd22@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:06:40 +0000 Subject: [PATCH] add weekly digests, log, and opt out options (#1107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### TL;DR Adds a Weekly Digests view to the web app, allowing users to browse and read their personalized weekly memory recaps directly in the UI, along with a notification preference toggle to opt out of digest emails. ### What changed? - Added a new `digests` view mode that renders a `DigestsView` component, accessible from the dashboard via a new weekly digest preview card that appears when a digest exists. - `DigestsView` displays a master-detail layout: a scrollable list of past weekly digests on the left, and a full digest content pane on the right. The detail pane renders the intro (with a floating brain illustration), numbered highlights, and feature recommendations styled to mirror the email layout. - Added thumbs-up/thumbs-down feedback controls and an optional free-text input on each digest, wired to analytics events. - Added `useDigests` and `useDigest` hooks that fetch digest list and detail data from the API, with 5- and 10-minute stale times respectively. - Registered four new API schema endpoints: `GET /digests`, `GET /digests/:id`, `GET /digests/preferences`, and `POST /digests/preferences`. - Added a `DigestPreferences` section to the account settings page with a toggle that lets users opt in or out of the weekly digest email. - Extended the `viewModeChanged` analytics event and the `viewLiterals` search param list to include `"digests"`, and added `digestViewed`, `digestFeedback`, and `digestFeedbackDetail` analytics events. - Added documentation for the `SUPERMEMORY_EMBEDDING_RAM_LIMIT` and `SUPERMEMORY_INGEST_CONCURRENCY` environment variables, explaining the memory-bounded ingestion queue and its live terminal status output. ### How to test? 1. Navigate to the dashboard and confirm the weekly digest preview card appears when a digest exists, and clicking it transitions to the `digests` view. 2. In the digests view, verify the list renders past digests with the most recent highlighted by a gradient border, and selecting a row loads the correct detail content. 3. Confirm the empty state renders correctly when no digests exist. 4. Use the thumbs-up/thumbs-down buttons and the detailed feedback textarea on a digest, verifying the toast confirmation appears on submission. 5. Open account settings, locate the "Notifications" section, and toggle the weekly digest switch on and off, verifying the preference persists without errors. 6. Confirm the `digests` view mode is reflected in the URL search params when active. ### Why make this change? Users currently receive weekly digest emails but have no way to revisit past digests within the app. This change surfaces digest history directly in the UI, adds in-product feedback collection on digest quality, and gives users control over whether they receive the emails — improving discoverability, engagement, and preference management. --- apps/web/app/(app)/page.tsx | 17 +- apps/web/components/dashboard-view.tsx | 43 ++ apps/web/components/digests-view.tsx | 479 ++++++++++++++++++++++ apps/web/components/settings/account.tsx | 70 ++++ apps/web/hooks/use-digests.ts | 72 ++++ apps/web/lib/analytics.ts | 17 +- apps/web/lib/search-params.ts | 1 + apps/web/public/images/digest/feat-fs.svg | 135 ++++++ packages/lib/api.ts | 70 ++++ 9 files changed, 900 insertions(+), 4 deletions(-) create mode 100644 apps/web/components/digests-view.tsx create mode 100644 apps/web/hooks/use-digests.ts create mode 100644 apps/web/public/images/digest/feat-fs.svg diff --git a/apps/web/app/(app)/page.tsx b/apps/web/app/(app)/page.tsx index 98febe44..020cad49 100644 --- a/apps/web/app/(app)/page.tsx +++ b/apps/web/app/(app)/page.tsx @@ -31,6 +31,7 @@ import { DocumentModal } from "@/components/document-modal" import { DocumentsCommandPalette } from "@/components/documents-command-palette" import { FullscreenNoteModal } from "@/components/fullscreen-note-modal" import type { HighlightItem } from "@/components/highlights-card" +import { DigestsView } from "@/components/digests-view" import { HotkeysProvider } from "react-hotkeys-hook" import { useHotkeys } from "react-hotkeys-hook" import { useIsMobile } from "@hooks/use-mobile" @@ -577,7 +578,10 @@ export default function NewPage() { const isChatView = viewMode === "chat" const showNovaBackdrop = - viewMode === "graph" || viewMode === "list" || viewMode === "dashboard" + viewMode === "graph" || + viewMode === "list" || + viewMode === "dashboard" || + viewMode === "digests" const isDashboardShell = viewMode === "dashboard" || (viewMode === "graph" && isMobile) const isGraphMode = viewMode === "graph" @@ -591,7 +595,8 @@ export default function NewPage() {
void setViewMode("integrations")} /> + ) : viewMode === "digests" ? ( +
+ +
) : viewMode === "graph" ? (
@@ -763,6 +773,7 @@ export default function NewPage() { onHighlightsChat={handleHighlightsChat} onHighlightsShowRelated={handleHighlightsShowRelated} onResetHighlights={handleResetHighlights} + onOpenDigests={() => void setViewMode("digests")} memoryOfDay={memoryOfDay} /> )} diff --git a/apps/web/components/dashboard-view.tsx b/apps/web/components/dashboard-view.tsx index fe2e3f0c..6edbc1ca 100644 --- a/apps/web/components/dashboard-view.tsx +++ b/apps/web/components/dashboard-view.tsx @@ -41,6 +41,7 @@ import { } from "@/hooks/use-personalization" import { normalizePluginClientId } from "@/lib/plugin-catalog" import { detectPluginSpace } from "@/lib/plugin-space" +import { useDigests } from "@/hooks/use-digests" type DocumentsResponse = z.infer type DocumentWithMemories = DocumentsResponse["documents"][0] @@ -1153,6 +1154,7 @@ export function DashboardView({ onHighlightsChat, onHighlightsShowRelated, onResetHighlights, + onOpenDigests, memoryOfDay, }: { spaceLabel: string @@ -1173,6 +1175,7 @@ export function DashboardView({ onHighlightsChat: (highlightContent: string, userReply: string) => void onHighlightsShowRelated: (query: string) => void onResetHighlights: () => void + onOpenDigests: () => void memoryOfDay: MemoryOfDay | null }) { const { user, org } = useAuth() @@ -1279,6 +1282,9 @@ export function DashboardView({ setProfession, } = usePersonalization() + const { data: digestList } = useDigests() + const latestDigest = digestList?.[0] + const recents = recentsData?.documents ?? [] const recentToolUsageItems = toolUsageItems .filter((item) => item.type === "Plugin" && item.lastDocument) @@ -1472,6 +1478,43 @@ export function DashboardView({

+ {/* Weekly digest preview */} + {latestDigest && ( + +
+ +
+
+ + Weekly digest + +

+ {latestDigest.title || "Your week in Supermemory"} ·{" "} + {latestDigest.memoryCount} memories +

+
+ + View + + +
+ )} + {/* Recently saved + Suggested for you */} = { + connections: "feat-router.png", + chat: "feat-memory.png", + extension: "feat-retrieval.png", + plugins: "feat-profiles.png", + mcp: "feat-router.png", + search: "feat-retrieval.png", +} +const BRAIN_IMG = "https://supermemory.ai/images/brain-head.png" + +function formatIsoWeek(isoWeek: string): string { + const match = isoWeek.match(/^(\d{4})-W(\d{2})$/) + if (!match) return isoWeek + const year = Number.parseInt(match[1] as string, 10) + const week = Number.parseInt(match[2] as string, 10) + const jan4 = new Date(year, 0, 4) + const dow = jan4.getDay() || 7 + const start = new Date(jan4) + start.setDate(jan4.getDate() - dow + 1 + (week - 1) * 7) + const end = new Date(start) + end.setDate(start.getDate() + 6) + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ] + const sm = months[start.getMonth()] ?? "" + const em = months[end.getMonth()] ?? "" + return `${sm} ${start.getDate()}–${sm === em ? "" : `${em} `}${end.getDate()}, ${year}` +} + +function isoWeekLabel(isoWeek: string): string { + const m = isoWeek.match(/-W(\d{2})$/) + return m ? `W${Number.parseInt(m[1] as string, 10)}` : isoWeek +} + +// ─── Left list row ──────────────────────────────────────────────────────────── +function DigestRow({ + digest, + featured, + selected, + onSelect, +}: { + digest: DigestSummary + featured: boolean + selected: boolean + onSelect: () => void +}) { + return ( + + ) +} + +// ─── Right pane: a single digest, faithful to the email layout ───────────────── +function DigestContent({ + digestId, + isoWeek, +}: { + digestId: string + isoWeek: string +}) { + const { data: digest, isLoading } = useDigest(digestId) + const [rating, setRating] = useState<"up" | "down" | null>(null) + const [showInput, setShowInput] = useState(false) + const [message, setMessage] = useState("") + + // reset feedback state when switching digests + useEffect(() => { + setRating(null) + setShowInput(false) + setMessage("") + analytics.digestViewed({ digest_id: digestId, iso_week: isoWeek }) + }, [digestId, isoWeek]) + + if (isLoading) { + return ( +
+ +
+ ) + } + if (!digest) { + return ( +
+ Digest not found. +
+ ) + } + + const { digestData } = digest + + const rate = (r: "up" | "down") => { + setRating(r) + analytics.digestFeedback({ + digest_id: digestId, + iso_week: isoWeek, + rating: r, + }) + } + const submitDetail = () => { + if (!message.trim()) return + analytics.digestFeedbackDetail({ + digest_id: digestId, + iso_week: isoWeek, + rating, + message: message.trim(), + }) + setMessage("") + setShowInput(false) + toast.success("Thanks for the feedback!") + } + + return ( +
+ {/* Header */} +

+ Weekly digest · {formatIsoWeek(digest.isoWeek)} +

+

+ {digestData.title || "Your week in Supermemory"} +

+ + {/* Greeting + intro, brain floated right */} +
+ +

+ {digestData.intro} +

+
+
+ +
+ + {/* Highlights — numbered, no boxes */} + {digestData.highlights.length > 0 && ( + <> +

+ This week's highlights +

+
+ {digestData.highlights.map((h, i) => ( +
+ + {String(i + 1).padStart(2, "0")} + +
+

+ {h.title} +

+

+ {h.content} +

+
+
+ ))} +
+ + )} + + {/* Worth trying — the one place we use boxes (subtle) */} + {digestData.featureRecommendations.length > 0 && ( +
+

+ Worth trying +

+
+ {digestData.featureRecommendations.map((r) => ( + + +
+

+ {r.headline} +

+

+ {r.body}{" "} + + {r.ctaLabel} → + +

+
+
+ ))} +
+
+ )} + + {/* Feedback */} +
+
+ + Was this digest useful? + +
+ + + +
+
+ + {showInput && ( +
+