import { useState, useEffect } from "react" import api from "../lib/api" import Reveal, { RevealItem } from "./Reveal" import { signalColor } from "../lib/signalColor" const G = "linear-gradient(135deg, #1d4ed8 0%, #0891b2 100%)" // Matches ProfileView.jsx's SectionLabel exactly, for the same "eyebrow above // a subheading" pattern used across You/History — Home didn't have one yet. function SectionLabel({ children }) { return (
{children}
) } // Visual identity per trigger type — independent of the (not-yet-built) per- // dimension color palette on the You page; this is scoped to what Home's // dimension-event cards need: a quick visual read on WHAT KIND of thing // happened (a first discovery vs. a shift vs. something to just note), not // WHICH of the 15 dimensions it is. // icon is a fixed glyph per trigger type — it marks WHAT KIND of event this // is and never changes. Direction (below) is a separate, dynamic ↑/↓ suffix // only added when the backend has a real measured direction for this event. const TRIGGER_CONFIG = { first_time_steady: { color: "#f59e0b", label: "New pattern", icon: "●" }, context_shift: { color: "#22d3ee", label: "Context shift", icon: "↕" }, drift: { color: "#34d399", label: "Shifted", icon: "↗" }, recurring: { color: "#a78bfa", label: "Recurring", icon: "↻" }, anomaly: { color: "#fb7185", label: "Worth noting", icon: "✦" }, } function recurringLabel(direction) { return direction === "back_to_usual" ? "Back to usual" : "New pattern" } // The ONLY values that ever mean a real up/down movement are the literal // strings "up"/"down" — every other trigger type's `direction` field means // something else entirely (recurring's is a state name, categorical // anomaly's is "contradicts_established"), so this can't misfire on them. function directionArrow(direction) { if (direction === "up") return "↑" if (direction === "down") return "↓" return "" } function DismissButton({ onDismiss }) { return ( ) } // Dims a hex color's RGB channels — used so the icon GLYPH itself sits a // notch below the badge pill's full-saturation color, rather than competing // with it at the same brightness. function darken(hex, amount) { const num = parseInt(hex.slice(1), 16) const r = Math.round(((num >> 16) & 0xff) * (1 - amount)) const g = Math.round(((num >> 8) & 0xff) * (1 - amount)) const b = Math.round((num & 0xff) * (1 - amount)) return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}` } function IconBadge({ icon, color }) { return (
{icon}
) } // `icon` is optional — only DimensionEventCard passes one. SessionRecapCard // never does, so it keeps its current left-border-only look, untouched. function CardShell({ color, icon, title, badge, faded, onDismiss, children }) { return (
{icon && }
{title} {badge && ( {badge} )}
{children}
) } function SessionRecapCard({ card, faded, onDismiss, onOpenSession }) { const dateLabel = card.date ? new Date(card.date).toLocaleDateString(undefined, { month: "short", day: "numeric" }) : "" const obs = card.observation return ( {card.conversation_summary && (

{card.conversation_summary}

)} {/* Exactly one observation, backend-ranked — the other two + their tips (and the resonance vote) live on the full session detail page */} {obs && (
{obs.signal.replace(/_/g, " ")}

{obs.observation}

{obs.resonance_prompt && (

💭 {obs.resonance_prompt}

)}
)} {card.tip && (

💡 {card.tip}

)}
) } function DimensionEventCard({ card, faded, onDismiss }) { const trig = TRIGGER_CONFIG[card.trigger_type] || { color: "#8b89aa", label: card.trigger_type, icon: "●" } const baseLabel = card.trigger_type === "recurring" ? recurringLabel(card.direction) : trig.label const arrow = card.trigger_type === "recurring" ? "" : directionArrow(card.direction) const badge = arrow ? `${baseLabel} ${arrow}` : baseLabel return ( {card.note && (

{card.note}

)}
) } // A real DimensionEventCard rendered with fabricated content, shown only in // the empty state so a brand-new user sees what their feed becomes instead // of just reading about it. `onDismiss` is a no-op — there's no real card // behind this to persist a dismissal for. function ExampleCard() { const sampleCard = { label: "Curiosity", trigger_type: "first_time_steady", direction: null, note: "You asked more follow-up questions than usual across your last two calls — steady enough now to call it a pattern.", } return (
Example {}} />
) } function HomeCard({ card, i, faded, onDismiss, onOpenSession }) { const dismiss = () => onDismiss(card.card_key) return ( {card.type === "dimension_event" ? : } ) } const VISIBLE_COUNT = 7 export default function HomeView({ active, onOpenSession }) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [expanded, setExpanded] = useState(false) useEffect(() => { if (!active) return setLoading(true) setExpanded(false) api.get("/api/home") .then(res => setData(res.data)) .catch(console.error) .finally(() => setLoading(false)) }, [active]) const handleDismiss = (cardKey) => { // Optimistic — remove immediately, persist in the background, silent on // failure (same pattern as ResultsView's resonance voting: a dismiss is a // low-stakes preference, not something worth blocking or retrying on). setData(d => d ? { ...d, cards: d.cards.filter(c => c.card_key !== cardKey) } : d) const form = new FormData() form.append("card_key", cardKey) api.post("/api/home/dismiss", form).catch(() => {}) } if (loading) return (
Loading your mirror…
) if (!data || data.insufficient_data) { return (

Your mirror is waiting

Record your first Google Meet call to start building your feed.

Open Google Meet →

What a card looks like

) } const cards = data.cards || [] // Backend returns the complete, newest-first, dismissed-filtered list — // pagination is purely a frontend rendering concern: 7 visible + an 8th // shown faded as a teaser, "Show more" reveals the rest uncapped. const hasOverflow = cards.length > VISIBLE_COUNT const visibleCards = expanded ? cards : cards.slice(0, VISIBLE_COUNT + 1) return (
Your Feed

Mirror Feed

{data.session_count} session{data.session_count > 1 ? "s" : ""}
{cards.length === 0 ? (

Nothing new to reflect back yet — keep recording, and your feed will start filling in as patterns become clear.

) : (
{visibleCards.map((card, i) => ( ))} {!expanded && hasOverflow && ( )}
)}
) }