Daily Briefing

This commit is contained in:
Konstantin Wohlwend 2026-07-08 11:20:50 -07:00
parent fef19c6b0f
commit ab8e45c76a
9 changed files with 950 additions and 8 deletions

View File

@ -46,6 +46,8 @@ import { LayoutGroup, motion, useReducedMotion, type Transition } from "motion/r
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import { type ElementType, type ReactNode, Suspense, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
import { AnalyticsEventLimitBanner } from "../analytics/shared";
import { DailyBriefOverviewCard } from "../_daily-brief/components";
import { TODAYS_DAILY_BRIEF } from "../_daily-brief/mock-data";
import { PageLayout } from "../page-layout";
import { useAdminApp, useProjectId } from "../use-admin-app";
import { UserPageMetricCard } from "../users/[userId]/user-page-metric-card";
@ -1899,6 +1901,8 @@ function MetricsContent({
/>
</div>
<DailyBriefOverviewCard brief={TODAYS_DAILY_BRIEF} projectId={projectId} />
<div
ref={gridContainerRef}
className={cn(

View File

@ -0,0 +1,148 @@
"use client";
import { DesignBadge, DesignButton, DesignCard } from "@/components/design-components";
import { Link } from "@/components/link";
import { cn } from "@/lib/utils";
import { ArrowRightIcon, ChartBarIcon, CheckCircleIcon, LightningIcon, SparkleIcon } from "@phosphor-icons/react";
import type { DailyBrief, DailyBriefBullet, DailyBriefSuggestion } from "./mock-data";
export function SuggestionActionCard({
suggestion,
completed,
onApply,
compact = false,
}: {
suggestion: DailyBriefSuggestion,
completed: boolean,
onApply: (suggestionId: string) => void,
compact?: boolean,
}) {
return (
<div
data-completed={completed ? "true" : "false"}
className={cn(
"rounded-2xl border border-blue-500/15 bg-blue-500/[0.06] p-4 ring-1 ring-blue-500/10 transition-all duration-150 hover:transition-none",
"data-[completed=true]:border-emerald-500/20 data-[completed=true]:bg-emerald-500/[0.08] data-[completed=true]:ring-emerald-500/15",
completed && "scale-[0.99]",
compact && "p-3",
)}
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="min-w-0 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<DesignBadge
label={completed ? "Completed" : "AI suggestion"}
color={completed ? "green" : "blue"}
icon={completed ? CheckCircleIcon : SparkleIcon}
size="sm"
/>
<span className="text-xs font-semibold text-foreground">
{suggestion.title}
</span>
</div>
<p className={cn("text-sm text-muted-foreground", compact && "text-xs")}>
{suggestion.summary}
</p>
<p className="text-xs font-medium text-foreground">
{suggestion.impact}
</p>
</div>
<DesignButton
size="sm"
variant={completed ? "secondary" : "default"}
disabled={completed}
onClick={() => onApply(suggestion.id)}
className={cn(
"shrink-0 transition-all duration-150 hover:transition-none",
completed && "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
)}
>
<span className="inline-flex items-center gap-1.5">
{completed ? <CheckCircleIcon className="h-3.5 w-3.5" weight="fill" /> : null}
{completed ? "Completed" : suggestion.actionLabel}
</span>
</DesignButton>
</div>
</div>
);
}
export function DailyBriefOverviewCard({
brief,
projectId,
}: {
brief: DailyBrief,
projectId: string,
}) {
const previewBullets: DailyBriefBullet[] = brief.sections.flatMap((section) => section.blocks.flatMap((block) => block.type === "bullets" ? block.bullets : [])).slice(0, 3);
const suggestionCount = brief.sections.flatMap((section) => section.blocks.filter((block) => block.type === "suggestion")).length;
const coreMetrics = brief.sections.flatMap((section) => section.blocks.flatMap((block) => block.type === "metrics" ? block.metrics : [])).slice(0, 2);
return (
<DesignCard
title="Today's Daily Brief"
subtitle="Daily notes for Demo Project"
icon={SparkleIcon}
gradient="purple"
actions={
<DesignBadge
label={`${suggestionCount} suggestions`}
color="purple"
icon={LightningIcon}
size="sm"
/>
}
contentClassName="space-y-4"
>
<div className="grid gap-4 lg:grid-cols-[1fr_16rem]">
<div className="min-w-0 space-y-3">
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium text-foreground">{brief.dateLabel}</span>
<span aria-hidden>/</span>
<span>{brief.readTime}</span>
</div>
<h2 className="text-xl font-semibold tracking-tight text-foreground">
{brief.title}
</h2>
</div>
<ul className="space-y-2 text-sm leading-6 text-muted-foreground">
{previewBullets.map((bullet) => (
<li key={bullet.label} className="flex gap-2">
<span aria-hidden className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-purple-500/70" />
<span>
<span className="font-medium text-foreground">{bullet.label}:</span>{" "}
{bullet.text}
</span>
</li>
))}
</ul>
<DesignButton asChild variant="outline" size="sm" className="w-fit">
<Link href={`/projects/${encodeURIComponent(projectId)}/daily-brief`}>
<span className="inline-flex items-center gap-1.5">
Read full brief
<ArrowRightIcon className="h-3.5 w-3.5" />
</span>
</Link>
</DesignButton>
</div>
<div className="rounded-2xl bg-foreground/[0.04] p-4 ring-1 ring-foreground/[0.08]">
<div className="mb-3 flex items-center gap-2">
<ChartBarIcon className="h-4 w-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
Briefing signals
</span>
</div>
<div className="space-y-3 text-sm">
{coreMetrics.map((metric) => (
<div key={metric.label}>
<div className="text-lg font-semibold tabular-nums text-foreground">{metric.value}</div>
<div className="text-xs text-muted-foreground">{metric.label} / {metric.delta}</div>
</div>
))}
</div>
</div>
</div>
</DesignCard>
);
}

View File

@ -0,0 +1,368 @@
export type DailyBriefSuggestion = {
id: string,
title: string,
summary: string,
impact: string,
actionLabel: string,
};
export type DailyBriefBullet = {
label: string,
text: string,
};
export type DailyBriefMetric = {
label: string,
value: string,
delta: string,
tone: "good" | "watch" | "neutral",
};
export type DailyBriefImportantUser = {
name: string,
email: string,
company: string,
signal: string,
};
export type DailyBriefVisual = {
title: string,
caption: string,
kind: "chart" | "screenshot" | "map",
};
export type DailyBriefBlock =
| { type: "bullets", bullets: DailyBriefBullet[] }
| { type: "metrics", metrics: DailyBriefMetric[] }
| { type: "users", users: DailyBriefImportantUser[] }
| { type: "visual", visual: DailyBriefVisual }
| { type: "suggestion", suggestion: DailyBriefSuggestion };
export type DailyBriefSection = {
heading: string,
intro?: string,
blocks: DailyBriefBlock[],
};
export type DailyBrief = {
id: string,
dateLabel: string,
dayLabel: string,
title: string,
summary: string,
readTime: string,
tags: string[],
sections: DailyBriefSection[],
};
export const DAILY_BRIEFS: DailyBrief[] = [
{
id: "2026-07-08",
dateLabel: "July 8, 2026",
dayLabel: "Today",
title: "Search traffic is working better than social",
summary: "Short notes on growth, new users, revenue, and suggested next steps.",
readTime: "5 min read",
tags: ["Growth", "Users", "Revenue"],
sections: [
{
heading: "Executive summary",
intro: "Quick read. The main changes are below.",
blocks: [
{
type: "bullets",
bullets: [
{
label: "Search is stronger",
text: "Fewer visits than direct traffic. More sign-ups and first payments.",
},
{
label: "Team invites matter",
text: "Users who invited teammates finished setup faster.",
},
{
label: "No urgent issue",
text: "Email and payment health look normal today.",
},
],
},
],
},
{
heading: "Core metric growth",
blocks: [
{
type: "metrics",
metrics: [
{ label: "Qualified sign-ups", value: "128", delta: "+18% vs last week", tone: "good" },
{ label: "Activated projects", value: "42", delta: "+11% vs last week", tone: "good" },
{ label: "First purchase revenue", value: "$675", delta: "+6% vs prior period", tone: "good" },
{ label: "Avg. setup time", value: "14m", delta: "-9% faster", tone: "good" },
],
},
{
type: "visual",
visual: {
kind: "chart",
title: "Setup by source",
caption: "Mock chart: search users reach setup steps sooner than paid social users.",
},
},
{
type: "bullets",
bullets: [
{
label: "Best search terms",
text: "\"auth pricing calculator\" and \"B2B user management SDK\" convert 38% above average.",
},
{
label: "Social is weaker",
text: "Paid social brings visits, but fewer users finish setup.",
},
],
},
{
type: "suggestion",
suggestion: {
id: "increase-search-budget-auth-pricing",
title: "Move budget to search",
summary: "Increase budget for \"auth pricing calculator\" and \"B2B user management SDK\".",
impact: "+12-18% expected good-fit sign-ups",
actionLabel: "Apply suggestion",
},
},
],
},
{
heading: "Important recent sign-ups",
blocks: [
{
type: "users",
users: [
{
name: "Maya Patel",
email: "maya@northstar.dev",
company: "Northstar Labs",
signal: "Created a project. Invited 3 teammates. Viewed payments.",
},
{
name: "Ethan Brooks",
email: "ethan@arcforge.ai",
company: "ArcForge AI",
signal: "Came from Google. Read RBAC docs twice. Signed up.",
},
{
name: "Sofia Nguyen",
email: "sofia@cloudlane.co",
company: "Cloudlane",
signal: "Set up email templates. Sent a test email in 11 minutes.",
},
],
},
{
type: "bullets",
bullets: [
{
label: "Follow up",
text: "Prioritize accounts that invite teammates and view payments.",
},
{
label: "Main drop-off",
text: "Users still get stuck before adding a production domain.",
},
],
},
{
type: "suggestion",
suggestion: {
id: "launch-domain-setup-nudge",
title: "Add domain setup nudge",
summary: "Prompt new admins if no domain is added after 20 minutes.",
impact: "+7% expected project activation",
actionLabel: "Apply suggestion",
},
},
],
},
{
heading: "Extra context",
blocks: [
{
type: "visual",
visual: {
kind: "screenshot",
title: "Landing page test",
caption: "Mock image: add a short setup proof section near pricing.",
},
},
{
type: "bullets",
bullets: [
{
label: "Revenue",
text: "First purchases mostly come from accounts with multiple active users.",
},
{
label: "Today",
text: "Focus on search. Fix the domain setup drop-off.",
},
],
},
],
},
],
},
{
id: "2026-07-07",
dateLabel: "July 7, 2026",
dayLabel: "Yesterday",
title: "Team invites helped users finish setup",
summary: "Projects with early teammate invites were more active.",
readTime: "4 min read",
tags: ["Activation", "Teams"],
sections: [
{
heading: "Brief",
blocks: [
{
type: "bullets",
bullets: [
{
label: "Invites helped",
text: "Teams looked at roles, permissions, and email templates sooner.",
},
{
label: "Make it early",
text: "Ask for teammate invites earlier in setup.",
},
],
},
],
},
{
heading: "Recommended follow-up",
blocks: [
{
type: "suggestion",
suggestion: {
id: "promote-team-invite-onboarding",
title: "Move team invite up",
summary: "Show team invites earlier for new projects.",
impact: "+9% expected week-one retention",
actionLabel: "Apply suggestion",
},
},
],
},
],
},
{
id: "2026-07-06",
dateLabel: "July 6, 2026",
dayLabel: "Monday",
title: "Email delivery is fine. Template setup needs help.",
summary: "Delivery is stable. Some users edit templates but never send a test.",
readTime: "3 min read",
tags: ["Emails", "Retention"],
sections: [
{
heading: "Brief",
blocks: [
{
type: "metrics",
metrics: [
{ label: "Delivered emails", value: "1.8k", delta: "+4%", tone: "good" },
{ label: "Bounce rate", value: "1.2%", delta: "flat", tone: "neutral" },
{ label: "Template stalls", value: "14", delta: "+22%", tone: "watch" },
],
},
{
type: "bullets",
bullets: [
{
label: "Delivery is fine",
text: "No bounce spike today.",
},
{
label: "Template setup stalls",
text: "Some users edit templates but never send a test.",
},
],
},
],
},
{
heading: "Recommended follow-up",
blocks: [
{
type: "bullets",
bullets: [
{
label: "UX nudge",
text: "Show test-send after two template edits.",
},
],
},
],
},
],
},
{
id: "2026-07-03",
dateLabel: "July 3, 2026",
dayLabel: "Friday",
title: "Pricing visitors want proof faster",
summary: "Pricing visitors often check docs before signing up.",
readTime: "4 min read",
tags: ["Pricing", "Website"],
sections: [
{
heading: "Brief",
blocks: [
{
type: "visual",
visual: {
kind: "map",
title: "Pricing to docs",
caption: "Mock journey: users check setup effort before signing up.",
},
},
{
type: "bullets",
bullets: [
{
label: "High intent",
text: "Pricing visitors open setup docs more often.",
},
{
label: "Missing proof",
text: "Add setup proof near pricing.",
},
],
},
],
},
{
heading: "Recommended follow-up",
blocks: [
{
type: "suggestion",
suggestion: {
id: "add-pricing-implementation-proof",
title: "Add setup proof",
summary: "Add a short setup block to the pricing page.",
impact: "+5% expected pricing-to-sign-up conversion",
actionLabel: "Apply suggestion",
},
},
],
},
],
},
];
export const TODAYS_DAILY_BRIEF = DAILY_BRIEFS[0];
export function getAllDailyBriefSuggestions(): DailyBriefSuggestion[] {
return DAILY_BRIEFS.flatMap((brief) => brief.sections.flatMap((section) => section.blocks.flatMap((block) => block.type === "suggestion" ? [block.suggestion] : [])));
}

View File

@ -0,0 +1,386 @@
"use client";
import { DesignBadge, DesignButton, DesignCard, DesignDialog, DesignDialogClose } from "@/components/design-components";
import { Checkbox } from "@/components/ui";
import { cn } from "@/lib/utils";
import { CheckCircleIcon, GearIcon, ImageIcon, LightningIcon, SparkleIcon, UserCircleIcon } from "@phosphor-icons/react";
import { useMemo, useState } from "react";
import { SuggestionActionCard } from "../_daily-brief/components";
import { DAILY_BRIEFS, getAllDailyBriefSuggestions, type DailyBrief, type DailyBriefBlock, type DailyBriefImportantUser, type DailyBriefMetric, type DailyBriefVisual } from "../_daily-brief/mock-data";
import { PageLayout } from "../page-layout";
const WEEKDAYS = [
{ id: "mon", label: "Mo" },
{ id: "tue", label: "Tu" },
{ id: "wed", label: "We" },
{ id: "thu", label: "Th" },
{ id: "fri", label: "Fr" },
{ id: "sat", label: "Sa" },
{ id: "sun", label: "Su" },
] as const;
type WeekdayId = typeof WEEKDAYS[number]["id"];
function MetricsBlock({ metrics }: { metrics: DailyBriefMetric[] }) {
return (
<div className="grid gap-x-6 gap-y-3 border-l border-foreground/[0.08] pl-4 sm:grid-cols-2 xl:grid-cols-4">
{metrics.map((metric) => (
<div key={metric.label} className="min-w-0">
<div className="text-xs font-medium text-muted-foreground">{metric.label}</div>
<div className="mt-1 text-2xl font-semibold tabular-nums text-foreground">{metric.value}</div>
<div className={cn(
"mt-1 text-xs font-medium",
metric.tone === "good" && "text-emerald-700 dark:text-emerald-300",
metric.tone === "watch" && "text-amber-700 dark:text-amber-300",
metric.tone === "neutral" && "text-muted-foreground",
)}>
{metric.delta}
</div>
</div>
))}
</div>
);
}
function UsersBlock({ users }: { users: DailyBriefImportantUser[] }) {
return (
<div className="grid gap-x-6 gap-y-4 border-l border-foreground/[0.08] pl-4 xl:grid-cols-3">
{users.map((user) => (
<div key={user.email} className="min-w-0">
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-purple-500/10 text-purple-700 dark:text-purple-300">
<UserCircleIcon className="h-5 w-5" weight="fill" />
</div>
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-foreground">{user.name}</div>
<div className="truncate text-xs text-muted-foreground">{user.company} / {user.email}</div>
</div>
</div>
<p className="mt-2 text-xs leading-5 text-muted-foreground">
{user.signal}
</p>
</div>
))}
</div>
);
}
function VisualBlock({ visual }: { visual: DailyBriefVisual }) {
return (
<div className="border-l border-foreground/[0.08] pl-4">
<div className="max-w-3xl overflow-hidden rounded-xl border border-dashed border-foreground/[0.12] bg-foreground/[0.025]">
<div className="relative h-40 bg-gradient-to-br from-purple-500/12 via-blue-500/8 to-cyan-500/12">
<div className="absolute inset-x-6 bottom-6 grid grid-cols-5 items-end gap-2 opacity-80">
{[42, 58, 36, 74, 91].map((height) => (
<div key={height} className="rounded-t bg-foreground/20" style={{ height: height * 0.7 }} />
))}
</div>
<div className="absolute left-4 top-4 inline-flex items-center gap-2 rounded-full bg-background/75 px-2.5 py-1 text-xs font-medium text-foreground shadow-sm ring-1 ring-foreground/[0.08] backdrop-blur-xl">
<ImageIcon className="h-3.5 w-3.5" />
{visual.kind === "chart" ? "Mock chart" : visual.kind === "map" ? "Mock journey map" : "Mock image"}
</div>
</div>
<div className="px-4 py-3">
<div className="text-sm font-semibold text-foreground">{visual.title}</div>
<p className="mt-1 text-xs leading-5 text-muted-foreground">{visual.caption}</p>
</div>
</div>
</div>
);
}
function DailyBriefBlockRenderer({
block,
completedSuggestionIds,
onApplySuggestion,
}: {
block: DailyBriefBlock,
completedSuggestionIds: Set<string>,
onApplySuggestion: (suggestionId: string) => void,
}) {
switch (block.type) {
case "bullets": {
return (
<ul className="max-w-4xl space-y-2.5 border-l border-foreground/[0.08] pl-4 text-sm leading-6 text-muted-foreground">
{block.bullets.map((bullet) => (
<li key={bullet.label} className="relative">
<span aria-hidden className="absolute -left-[1.18rem] top-2.5 h-px w-3 bg-foreground/[0.16]" />
<div className="text-sm font-semibold text-foreground">{bullet.label}</div>
<div className="text-sm text-muted-foreground">{bullet.text}</div>
</li>
))}
</ul>
);
}
case "metrics": {
return <MetricsBlock metrics={block.metrics} />;
}
case "users": {
return <UsersBlock users={block.users} />;
}
case "visual": {
return <VisualBlock visual={block.visual} />;
}
case "suggestion": {
return (
<SuggestionActionCard
suggestion={block.suggestion}
completed={completedSuggestionIds.has(block.suggestion.id)}
onApply={onApplySuggestion}
/>
);
}
}
}
function BriefTreeSection({
section,
sectionIndex,
completedSuggestionIds,
onApplySuggestion,
}: {
section: DailyBrief["sections"][number],
sectionIndex: number,
completedSuggestionIds: Set<string>,
onApplySuggestion: (suggestionId: string) => void,
}) {
return (
<section className="relative grid grid-cols-[2.25rem_1fr] gap-3">
<div aria-hidden className="relative flex justify-center">
<div className="absolute bottom-[-1.5rem] top-7 w-px bg-foreground/[0.12]" />
<div className="relative z-10 flex h-7 w-7 items-center justify-center rounded-lg bg-purple-500/10 text-xs font-bold text-purple-700 ring-1 ring-purple-500/20 dark:text-purple-300">
+
</div>
</div>
<div className="min-w-0 pb-5">
<div className="mb-3">
<div className="flex flex-wrap items-baseline gap-2">
<span className="text-[10px] font-semibold uppercase tracking-[0.18em] text-purple-700 dark:text-purple-300">
Section {sectionIndex + 1}
</span>
<h3 className="text-lg font-semibold tracking-tight text-foreground">
{section.heading}
</h3>
</div>
{section.intro != null && (
<p className="mt-1 max-w-3xl text-sm leading-6 text-muted-foreground">
{section.intro}
</p>
)}
</div>
<div className="space-y-4 pl-3">
{section.blocks.map((block, index) => (
<DailyBriefBlockRenderer
key={`${section.heading}-${index}`}
block={block}
completedSuggestionIds={completedSuggestionIds}
onApplySuggestion={onApplySuggestion}
/>
))}
</div>
</div>
</section>
);
}
function BriefTree({
brief,
completedSuggestionIds,
onApplySuggestion,
}: {
brief: DailyBrief,
completedSuggestionIds: Set<string>,
onApplySuggestion: (suggestionId: string) => void,
}) {
return (
<article className="space-y-1">
<header className="grid grid-cols-[2.25rem_1fr] gap-3">
<div aria-hidden className="relative flex justify-center">
<div className="absolute top-8 h-5 w-px bg-foreground/[0.12]" />
<div className="relative z-10 flex h-8 w-8 items-center justify-center rounded-xl bg-foreground text-background shadow-sm">
<SparkleIcon className="h-4 w-4" weight="fill" />
</div>
</div>
<div className="min-w-0 pb-3">
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>{brief.dayLabel}</span>
<span aria-hidden>/</span>
<span>{brief.readTime}</span>
</div>
<h2 className="mt-1 max-w-4xl text-2xl font-semibold tracking-tight text-foreground">
{brief.title}
</h2>
</div>
</header>
{brief.sections.map((section, sectionIndex) => (
<BriefTreeSection
key={section.heading}
section={section}
sectionIndex={sectionIndex}
completedSuggestionIds={completedSuggestionIds}
onApplySuggestion={onApplySuggestion}
/>
))}
</article>
);
}
export default function PageClient() {
const allSuggestions = useMemo(() => getAllDailyBriefSuggestions(), []);
const [completedSuggestionIds, setCompletedSuggestionIds] = useState<Set<string>>(() => new Set());
const [enabledBriefDays, setEnabledBriefDays] = useState<Set<WeekdayId>>(() => new Set(["mon", "tue", "wed", "thu", "fri"]));
const completedCount = allSuggestions.filter((suggestion) => completedSuggestionIds.has(suggestion.id)).length;
const allSuggestionsCompleted = allSuggestions.length > 0 && completedCount === allSuggestions.length;
const enabledDayLabel = WEEKDAYS.filter((day) => enabledBriefDays.has(day.id)).map((day) => day.label).join(", ");
const applySuggestion = (suggestionId: string) => {
setCompletedSuggestionIds((previous) => {
const next = new Set(previous);
next.add(suggestionId);
return next;
});
};
const applyAllSuggestions = () => {
setCompletedSuggestionIds(new Set(allSuggestions.map((suggestion) => suggestion.id)));
};
const toggleBriefDay = (dayId: WeekdayId, enabled: boolean) => {
setEnabledBriefDays((previous) => {
const next = new Set(previous);
if (enabled) {
next.add(dayId);
} else {
next.delete(dayId);
}
return next;
});
};
return (
<PageLayout
title="Daily Brief"
description="Short daily notes for Demo Project. Mock data only for the team presentation."
actions={
<div className="flex flex-wrap items-center gap-2">
<DesignBadge
label={`${completedCount}/${allSuggestions.length} completed`}
color={allSuggestionsCompleted ? "green" : "blue"}
icon={allSuggestionsCompleted ? CheckCircleIcon : LightningIcon}
size="sm"
/>
<DesignButton
size="sm"
onClick={applyAllSuggestions}
disabled={allSuggestionsCompleted}
variant={allSuggestionsCompleted ? "secondary" : "default"}
className={cn(
"transition-all duration-150 hover:transition-none",
allSuggestionsCompleted && "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
)}
>
<span className="inline-flex items-center gap-1.5">
{allSuggestionsCompleted ? <CheckCircleIcon className="h-3.5 w-3.5" weight="fill" /> : null}
{allSuggestionsCompleted ? "All suggestions completed" : "Apply all suggestions"}
</span>
</DesignButton>
<DesignDialog
trigger={
<DesignButton variant="outline" size="icon" aria-label="Configure Daily Brief schedule">
<GearIcon className="h-4 w-4" />
</DesignButton>
}
icon={GearIcon}
title="Daily Brief schedule"
description="Choose which days should create a new brief. This is mock UI for now."
size="md"
footer={
<DesignDialogClose asChild>
<DesignButton size="sm">Done</DesignButton>
</DesignDialogClose>
}
>
<div className="space-y-4">
<div className="rounded-2xl bg-foreground/[0.04] p-3 text-sm text-muted-foreground ring-1 ring-foreground/[0.08]">
Current schedule: <span className="font-medium text-foreground">{enabledDayLabel || "No days selected"}</span>
</div>
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{WEEKDAYS.map((day) => (
<label
key={day.id}
className="flex cursor-pointer items-center gap-2 rounded-xl bg-foreground/[0.035] px-3 py-2 text-sm font-medium text-foreground ring-1 ring-foreground/[0.08] transition-colors duration-150 hover:bg-foreground/[0.06] hover:transition-none"
>
<Checkbox
checked={enabledBriefDays.has(day.id)}
onCheckedChange={(checked) => toggleBriefDay(day.id, checked === true)}
/>
{day.label}
</label>
))}
</div>
</div>
</DesignDialog>
</div>
}
fillWidth
wrapHeaderInCard
>
<div className="grid gap-4 lg:grid-cols-[16rem_1fr]">
<DesignCard title="Archive" icon={SparkleIcon} gradient="purple" contentClassName="space-y-2">
{DAILY_BRIEFS.map((brief) => {
const suggestionCount = brief.sections.flatMap((section) => section.blocks.filter((block) => block.type === "suggestion")).length;
const suggestionLabel = `${suggestionCount} suggested ${suggestionCount === 1 ? "action" : "actions"}`;
return (
<a
key={brief.id}
href={`#${brief.id}`}
className="block rounded-xl p-3 transition-colors duration-150 hover:bg-foreground/[0.04] hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground/[0.16]"
>
<div className="mb-1 flex items-center justify-between gap-2">
<span className="truncate text-xs font-semibold text-foreground">{brief.dayLabel}</span>
<span className="shrink-0 text-[10px] text-muted-foreground">{brief.readTime}</span>
</div>
<p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
{brief.title}
</p>
{suggestionCount > 0 && (
<div className="mt-2 text-[10px] font-medium text-blue-700 dark:text-blue-300">
{suggestionLabel}
</div>
)}
</a>
);
})}
</DesignCard>
<div className="space-y-4">
{DAILY_BRIEFS.map((brief) => (
<DesignCard
key={brief.id}
id={brief.id}
title={brief.dateLabel}
subtitle={brief.summary}
icon={SparkleIcon}
gradient="purple"
actions={
<div className="flex flex-wrap justify-end gap-1.5">
{brief.tags.map((tag) => (
<DesignBadge key={tag} label={tag} color="purple" size="sm" />
))}
</div>
}
contentClassName="space-y-6"
>
<BriefTree
brief={brief}
completedSuggestionIds={completedSuggestionIds}
onApplySuggestion={applySuggestion}
/>
</DesignCard>
))}
</div>
</div>
</PageLayout>
);
}

View File

@ -0,0 +1,11 @@
import PageClient from "./page-client";
export const metadata = {
title: "Daily Brief",
};
export default function Page() {
return (
<PageClient />
);
}

View File

@ -35,6 +35,7 @@ import {
ListIcon,
PlusIcon,
SidebarIcon,
SparkleIcon,
UsersIcon,
type Icon as PhosphorIcon,
} from "@phosphor-icons/react";
@ -101,6 +102,14 @@ const usersItem: Item = {
type: "item",
};
const dailyBriefItem: Item = {
name: "Daily Brief",
href: "/daily-brief",
regex: /^\/projects\/[^\/]+\/daily-brief(\/.*)?$/,
icon: SparkleIcon,
type: "item",
};
const dashboardsItem: Item = {
name: "Dashboards",
href: "/dashboards",
@ -552,6 +561,12 @@ function SidebarContent({
href={`/projects/${projectId}${overviewItem.href}`}
isCollapsed={isCollapsed}
/>
<NavItem
item={dailyBriefItem}
onClick={onNavigate}
href={`/projects/${projectId}${dailyBriefItem.href}`}
isCollapsed={isCollapsed}
/>
<NavItem
item={usersItem}
onClick={onNavigate}

View File

@ -282,6 +282,14 @@ const PROJECT_SHORTCUTS: ProjectShortcutDefinition[] = [
href: "/users",
keywords: ["users", "user", "people", "members", "accounts"],
},
{
id: "navigation/daily-brief",
icon: SparkleIcon,
label: "Daily Brief",
description: "Navigation",
href: "/daily-brief",
keywords: ["daily brief", "briefing", "ai brief", "gtm assistant", "recommendations", "suggestions"],
},
{
id: "navigation/dashboards",
icon: ChartBarIcon,

View File

@ -2974,7 +2974,7 @@
"/emails/send-email": {
"post": {
"summary": "Send email",
"description": "Send an email to a list of users. The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
"description": "Send an email to a list of users (user_ids), all users (all_users), or arbitrary email addresses (emails). The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
"parameters": [],
"tags": [
"Emails"
@ -2995,11 +2995,12 @@
"properties": {
"user_id": {
"type": "string"
},
"email": {
"type": "string"
}
},
"required": [
"user_id"
]
"required": []
}
}
},

View File

@ -2934,7 +2934,7 @@
"/emails/send-email": {
"post": {
"summary": "Send email",
"description": "Send an email to a list of users. The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
"description": "Send an email to a list of users (user_ids), all users (all_users), or arbitrary email addresses (emails). The content field should contain either {html} for HTML emails, {template_id, variables} for template-based emails, or {draft_id} for a draft email.",
"parameters": [],
"tags": [
"Emails"
@ -2955,11 +2955,12 @@
"properties": {
"user_id": {
"type": "string"
},
"email": {
"type": "string"
}
},
"required": [
"user_id"
]
"required": []
}
}
},