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 (
)}
{/* Exactly one observation, backend-ranked — the other two + their tips
(and the resonance vote) live on the full session detail page */}
{obs && (
)}
)
}
// 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.