supermemory/apps/web/components/brain-home/widgets.tsx
vorflux[bot] 2731de5c06
fix(web): add Gmail connector logo (#1428)
Co-authored-by: Vorflux AI <249966464+vorflux[bot]@users.noreply.github.com>
2026-08-07 17:35:26 -07:00

639 lines
18 KiB
TypeScript

"use client"
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { Gmail, GoogleDrive, Notion } from "@ui/assets/icons"
import {
AlertCircle,
ArrowRight,
Calendar,
CheckCircle2,
FileText,
Github,
Loader2,
MessageSquare,
Plus,
Quote,
Sparkles,
Target,
} from "lucide-react"
import {
ACTIVITY,
AGENT_SERIES,
CONNECTED_SOURCES,
RECENT_ANSWERS,
RECOMMENDED_SOURCES,
SESSIONS_BY_DAY,
STATS,
TEAM_RECENT,
TEAM_THIS_WEEK,
WEEKLY_DIGEST,
type ActivityEvent,
type ConnectedSource,
type RecommendedSource,
type Stat,
} from "./data"
export const modalCardStyle = {
boxShadow:
"0 2.842px 14.211px 0 rgba(0, 0, 0, 0.25), 0.711px 0.711px 0.711px 0 rgba(255, 255, 255, 0.10) inset",
}
export const inputBevelStyle = {
boxShadow:
"0px 1px 2px 0px rgba(0,43,87,0.1), inset 0px 0px 0px 1px rgba(43,49,67,0.08), inset 0px 1px 1px 0px rgba(0,0,0,0.08), inset 0px 2px 4px 0px rgba(0,0,0,0.02)",
}
export function StatsStrip({ stats = STATS }: { stats?: Stat[] }) {
return (
<section
className="rounded-[16px] bg-[#1B1F24] grid grid-cols-2 md:grid-cols-4 divide-y md:divide-y-0 md:divide-x divide-white/[0.04]"
style={modalCardStyle}
>
{stats.map((s) => (
<div key={s.label} className="px-5 py-4">
<p className="text-[10px] uppercase tracking-[0.12em] text-[#737373] font-semibold">
{s.label}
</p>
<p
className={cn(
"text-[22px] font-semibold text-[#fafafa] mt-1.5 tabular-nums leading-none",
dmSans125ClassName(),
)}
>
{s.value}
</p>
{s.delta && (
<p
className={cn(
"text-[11px] font-medium mt-1.5",
s.deltaTint === "up"
? "text-[#4BA0FA]"
: s.deltaTint === "down"
? "text-[#FF8A47]"
: "text-[#737373]",
)}
>
{s.delta}
</p>
)}
</div>
))}
</section>
)
}
export function SectionHeader({ title, cta }: { title: string; cta?: string }) {
return (
<div className="flex items-end justify-between mb-3 px-1">
<p className="text-[11px] uppercase tracking-[0.12em] text-[#737373] font-semibold">
{title}
</p>
{cta && (
<button
type="button"
className="text-[12px] text-[#737373] hover:text-[#fafafa] transition-colors font-medium"
>
{cta}
</button>
)}
</div>
)
}
export function RecentAnswersCard() {
return (
<div
className="rounded-[14px] bg-[#1B1F24] p-4 flex flex-col"
style={modalCardStyle}
>
<div className="flex items-center justify-between mb-3">
<p className="text-[10px] uppercase tracking-[0.12em] text-[#737373] font-semibold">
Recent answers
</p>
<button
type="button"
className="text-[10px] text-[#737373] hover:text-[#fafafa] transition-colors font-medium"
>
See all
</button>
</div>
<ul className="space-y-2.5">
{RECENT_ANSWERS.map((a) => (
<li
key={a.question}
className="border-l border-[#2261CA33] pl-2.5 hover:border-[#4BA0FA] transition-colors"
>
<p className="text-[12px] text-[#fafafa] font-semibold leading-snug truncate">
{a.question}
</p>
<p className="text-[11px] text-[#A1A1AA] leading-[1.4] font-medium mt-0.5 line-clamp-1">
{a.answer}
</p>
<p className="text-[10px] text-[#737373] font-medium mt-1">
{a.who} · {a.when}
</p>
</li>
))}
</ul>
</div>
)
}
export function WeeklyDigestCard() {
return (
<div
className="rounded-[14px] bg-[#1B1F24] p-4 md:p-5 flex flex-col relative overflow-hidden"
style={modalCardStyle}
>
<div
aria-hidden
className="absolute -top-px left-0 right-0 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.3), transparent)",
}}
/>
<div className="flex items-end justify-between mb-3">
<p className="text-[10px] uppercase tracking-[0.12em] text-[#737373] font-semibold">
This week
</p>
<p className="text-[10px] uppercase tracking-[0.12em] text-[#525D6E] font-semibold">
{WEEKLY_DIGEST.range}
</p>
</div>
<p
className={cn(
"text-[14px] font-semibold text-[#fafafa] leading-snug",
dmSans125ClassName(),
)}
>
{WEEKLY_DIGEST.headline.split(/(\d+)/).map((piece, i) =>
/^\d+$/.test(piece) ? (
<span key={i} className="text-[#4BA0FA]">
{piece}
</span>
) : (
<span key={i}>{piece}</span>
),
)}
</p>
<ul className="mt-3 space-y-1.5">
{WEEKLY_DIGEST.bullets.map((b) => (
<li key={b.label} className="flex items-start gap-2">
<span
aria-hidden
className={cn("mt-1.5 size-1 rounded-full bg-current", b.tint)}
/>
<span className={cn("text-[12px] font-medium", b.tint)}>
{b.label}
</span>
</li>
))}
</ul>
<button
type="button"
className="mt-auto pt-3 self-start text-[11px] text-[#A1A1AA] hover:text-[#fafafa] transition-colors font-medium inline-flex items-center gap-1.5"
>
Open weekly digest <ArrowRight className="size-3" />
</button>
</div>
)
}
export function BrainPulseBlock() {
return (
<section className="grid lg:grid-cols-[2fr_3fr] gap-3 items-stretch">
<RecentAnswersCard />
<WeeklyDigestCard />
</section>
)
}
export function BrainActivityChart() {
const totalsByAgent = AGENT_SERIES.map((s) => ({
...s,
total: SESSIONS_BY_DAY.reduce((sum, d) => sum + (d.values[s.key] ?? 0), 0),
}))
const grandTotal = totalsByAgent.reduce((sum, s) => sum + s.total, 0)
const dayTotals = SESSIONS_BY_DAY.map((d) =>
AGENT_SERIES.reduce((sum, s) => sum + (d.values[s.key] ?? 0), 0),
)
const max = Math.max(...dayTotals)
return (
<section
className="rounded-[20px] bg-[#1B1F24] p-5 md:p-6 relative overflow-hidden"
style={modalCardStyle}
>
<div
aria-hidden
className="absolute -top-px left-0 right-0 h-px"
style={{
background:
"linear-gradient(to right, transparent, rgba(75,160,250,0.3), transparent)",
}}
/>
<div className="flex flex-wrap items-end justify-between gap-3 mb-5">
<div>
<p
className={cn(
"text-[16px] font-semibold text-[#fafafa]",
dmSans125ClassName(),
)}
>
Sessions by agent
</p>
<p className="text-[12px] text-[#737373] font-medium mt-0.5">
{grandTotal} agent sessions, last 7 days which tool the team
reached for.
</p>
</div>
<div className="flex flex-wrap items-center gap-3 text-[11px] font-medium">
{totalsByAgent.map((s) => (
<span
key={s.key}
className="inline-flex items-center gap-1.5 text-[#A1A1AA]"
>
<span
aria-hidden
className="size-2 rounded-full"
style={{ background: s.color }}
/>
{s.label}{" "}
<span className="text-[#fafafa] tabular-nums">{s.total}</span>
</span>
))}
</div>
</div>
<div className="flex items-end gap-2">
{SESSIONS_BY_DAY.map((d, dayIdx) => {
const BAR_AREA_PX = 140
const totalForDay = dayTotals[dayIdx] ?? 0
const barHeightPx = Math.max(
6,
Math.round((totalForDay / max) * BAR_AREA_PX),
)
return (
<div
key={d.label}
className="flex-1 flex flex-col items-center gap-2"
>
<div
className="w-full flex items-end"
style={{ height: `${BAR_AREA_PX}px` }}
>
<div
className="w-full rounded-[4px] overflow-hidden flex flex-col"
style={{ height: `${barHeightPx}px` }}
title={`${d.label}: ${totalForDay} sessions`}
>
{AGENT_SERIES.map((series, sIdx) => {
const val = d.values[series.key] ?? 0
if (val === 0) return null
const segHeight =
totalForDay > 0 ? (val / totalForDay) * 100 : 0
return (
<div
key={series.key}
style={{
height: `${segHeight}%`,
background: series.color,
}}
className={cn(
sIdx === 0 && "rounded-t-[4px]",
sIdx === AGENT_SERIES.length - 1 && "rounded-b-[4px]",
)}
title={`${series.label} · ${val}`}
/>
)
})}
</div>
</div>
<span className="text-[10px] text-[#737373] font-medium tabular-nums">
{d.label}
</span>
</div>
)
})}
</div>
</section>
)
}
function activitySourceIcon(s: ActivityEvent["source"]) {
if (s === "drive") return <GoogleDrive className="size-3.5" />
if (s === "notion") return <Notion className="size-3.5" />
if (s === "slack")
return <MessageSquare className="size-3.5 text-[#4BA0FA]" />
if (s === "answer") return <Sparkles className="size-3.5 text-[#4BA0FA]" />
if (s === "decision")
return <CheckCircle2 className="size-3.5 text-[#4BA0FA]" />
return <Sparkles className="size-3.5 text-[#8B8B8B]" />
}
export function ActivityFeedBlock() {
return (
<section>
<SectionHeader title="Activity feed" cta="Show more →" />
<div
className="rounded-[14px] bg-[#14161A] border border-[rgba(82,89,102,0.2)] divide-y divide-white/[0.04]"
style={inputBevelStyle}
>
{ACTIVITY.map((a, i) => (
<div key={i} className="flex items-center gap-3 px-4 py-3">
<div className="size-7 rounded-[8px] bg-[#0F1217] flex items-center justify-center shrink-0">
{activitySourceIcon(a.source)}
</div>
<p className="flex-1 text-[13px] text-[#fafafa] font-medium">
{a.text}
</p>
<span className="text-[11px] text-[#737373] font-medium tabular-nums shrink-0">
{a.when}
</span>
</div>
))}
</div>
</section>
)
}
export function TeamActivityRail() {
return (
<section
className="rounded-[20px] bg-[#1B1F24] p-5 md:p-6"
style={modalCardStyle}
>
<div className="flex items-center justify-between mb-1">
<p
className={cn(
"text-[16px] font-semibold leading-tight",
dmSans125ClassName(),
)}
>
Team activity
</p>
<span className="text-[10px] uppercase tracking-[0.12em] text-[#737373] font-semibold">
This week
</span>
</div>
<p className="text-[11px] text-[#737373] font-medium">
What your team's been adding to the brain.
</p>
<div className="mt-5 space-y-2.5">
{TEAM_THIS_WEEK.map((member) => (
<div
key={member.email}
className="flex items-center gap-3 rounded-[10px] px-2 py-1.5 hover:bg-white/[0.03] transition-colors"
>
<div
className="size-8 rounded-full flex items-center justify-center text-[11px] font-semibold text-[#0F1217] shrink-0"
style={{ background: member.color }}
>
{member.name[0]}
</div>
<div className="min-w-0 flex-1">
<p className="text-[13px] text-[#fafafa] font-medium truncate">
{member.name}
</p>
<p className="text-[11px] text-[#737373] font-medium truncate">
{member.summary}
</p>
</div>
<span className="text-[12px] text-[#A1A1AA] tabular-nums font-semibold shrink-0">
{member.contributions}
</span>
</div>
))}
</div>
<div className="mt-5 pt-4 border-t border-white/[0.04]">
<p className="text-[10px] uppercase tracking-[0.12em] text-[#737373] font-semibold mb-2.5">
Recent
</p>
<ul className="space-y-2">
{TEAM_RECENT.map((event, i) => (
<li
key={i}
className="text-[12px] text-[#A1A1AA] leading-[1.5] font-medium"
>
<span className="text-[#fafafa]">{event.who}</span> {event.what}
<span className="text-[#525D6E]"> · {event.when}</span>
</li>
))}
</ul>
</div>
<button
type="button"
className="mt-5 w-full inline-flex items-center justify-center gap-1.5 h-9 rounded-full bg-[#0D121A] border border-[rgba(115,115,115,0.15)] text-[12px] font-medium text-[#fafafa] hover:bg-[#14161A] transition-colors"
>
<Plus className="size-3.5" />
Invite teammates
</button>
</section>
)
}
export function BrainHomeBackground() {
return (
<div className="pointer-events-none fixed inset-0 z-0">
<div
className="absolute inset-0"
style={{
background:
"radial-gradient(ellipse 80% 60% at 50% 0%, rgba(75,160,250,0.08) 0%, rgba(34,97,202,0.04) 35%, transparent 70%)",
}}
/>
<div className="absolute inset-0 bg-[#05080D]/55" aria-hidden />
<div
id="brain-home-grid"
className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(105,167,240,0.18)_1px,transparent_1px)] bg-size-[32px_32px] mask-[radial-gradient(ellipse_at_center,black_55%,transparent_100%)]"
/>
</div>
)
}
export function Quotation({ children }: { children: React.ReactNode }) {
return (
<span className="inline-flex items-center gap-1 text-[#737373]">
<Quote className="size-3" />
{children}
</span>
)
}
function connectedSourceIcon(key: ConnectedSource["iconKey"]) {
if (key === "drive") return <GoogleDrive className="size-4" />
if (key === "notion") return <Notion className="size-4" />
if (key === "gmail") return <Gmail className="size-4" />
if (key === "github") return <Github className="size-4 text-[#fafafa]" />
return <FileText className="size-4 text-[#8B8B8B]" />
}
function sourceStatusMeta(status: ConnectedSource["status"]) {
if (status === "healthy")
return {
dot: "bg-[#4BA0FA]",
tint: "text-[#4BA0FA]",
icon: <CheckCircle2 className="size-3" />,
}
if (status === "syncing")
return {
dot: "bg-[#A1A1AA]",
tint: "text-[#A1A1AA]",
icon: <Loader2 className="size-3 animate-spin" />,
}
if (status === "warning")
return {
dot: "bg-[#FF8A47]",
tint: "text-[#FF8A47]",
icon: <AlertCircle className="size-3" />,
}
return {
dot: "bg-[#FF5C5C]",
tint: "text-[#FF5C5C]",
icon: <AlertCircle className="size-3" />,
}
}
export function SourcesHealthRail({ empty }: { empty?: boolean }) {
if (empty) {
return (
<section className="rounded-[16px] border border-white/[0.05] bg-white/[0.02] p-4">
<p
className={cn(
"text-[14px] font-semibold leading-tight",
dmSans125ClassName(),
)}
>
Sources health
</p>
<p className="text-[11px] text-[#737373] font-medium mt-0.5 leading-[1.5]">
Nothing connected yet Drive, Notion, Gmail and more are ready when
you are.
</p>
<button
type="button"
className="mt-3 w-full inline-flex items-center justify-center gap-1.5 h-8 rounded-full bg-[#0D121A] border border-[rgba(115,115,115,0.15)] text-[12px] font-medium text-[#fafafa] hover:bg-[#14161A] transition-colors"
>
<Plus className="size-3.5" />
Connect a source
</button>
</section>
)
}
const issueCount = CONNECTED_SOURCES.filter(
(s) => s.status === "warning" || s.status === "error",
).length
return (
<section className="rounded-[16px] border border-white/[0.05] bg-white/[0.02] p-4">
<div className="flex items-center justify-between mb-1">
<p
className={cn(
"text-[14px] font-semibold leading-tight",
dmSans125ClassName(),
)}
>
Sources health
</p>
{issueCount > 0 ? (
<span className="text-[10px] uppercase tracking-[0.12em] text-[#FF8A47] font-semibold">
{issueCount} need attention
</span>
) : (
<span className="text-[10px] uppercase tracking-[0.12em] text-[#4BA0FA] font-semibold">
All healthy
</span>
)}
</div>
<p className="text-[11px] text-[#737373] font-medium">
Is the brain getting fed?
</p>
<ul className="mt-3 space-y-0.5">
{CONNECTED_SOURCES.map((s) => {
const meta = sourceStatusMeta(s.status)
return (
<li
key={s.id}
className="flex items-center gap-2.5 rounded-[8px] px-1.5 py-2 hover:bg-white/[0.03] transition-colors"
>
<div className="size-6 flex items-center justify-center shrink-0">
{connectedSourceIcon(s.iconKey)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2">
<p className="text-[12px] text-[#fafafa] font-medium truncate">
{s.name}
</p>
<span className="text-[10px] text-[#737373] font-medium tabular-nums shrink-0">
{s.when}
</span>
</div>
<p
className={cn(
"text-[11px] font-medium mt-0.5 inline-flex items-center gap-1",
meta.tint,
)}
>
{meta.icon}
<span className="truncate">{s.statusMessage ?? s.delta}</span>
</p>
</div>
</li>
)
})}
</ul>
</section>
)
}
function recommendedIcon(key: RecommendedSource["iconKey"]) {
if (key === "slack")
return <MessageSquare className="size-4 text-[#4BA0FA]" />
if (key === "github") return <Github className="size-4 text-[#fafafa]" />
if (key === "linear") return <Target className="size-4 text-[#fafafa]" />
if (key === "granola") return <Sparkles className="size-4 text-[#FF8A47]" />
if (key === "calendar") return <Calendar className="size-4 text-[#A1A1AA]" />
return <FileText className="size-4 text-[#8B8B8B]" />
}
export function RecommendedRail() {
return (
<section className="rounded-[16px] border border-white/[0.05] bg-white/[0.02] p-4">
<p
className={cn(
"text-[14px] font-semibold leading-tight",
dmSans125ClassName(),
)}
>
Add more to the brain
</p>
<p className="text-[11px] text-[#737373] font-medium mt-0.5">
Teams like yours connect these next.
</p>
<ul className="mt-3 space-y-0.5">
{RECOMMENDED_SOURCES.map((r) => (
<li
key={r.id}
className="group flex items-start gap-2.5 rounded-[8px] px-1.5 py-2 hover:bg-white/[0.03] transition-colors cursor-pointer"
>
<div className="size-6 flex items-center justify-center shrink-0">
{recommendedIcon(r.iconKey)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="text-[12px] text-[#fafafa] font-medium truncate">
{r.name}
</p>
<ArrowRight className="size-3 text-[#525D6E] group-hover:text-[#fafafa] transition-colors shrink-0" />
</div>
<p className="text-[11px] text-[#737373] font-medium leading-[1.4] mt-0.5 line-clamp-2">
{r.reason}
</p>
</div>
</li>
))}
</ul>
</section>
)
}