Harsh200415's picture
Phase B1: quarantine pyannote/voiceprint off the live path, remove Upload UI
0507574
Raw
History Blame Contribute Delete
14.3 kB
import { useState, useEffect } from "react"
import { supabase } from "./lib/supabase"
import api from "./lib/api"
import AuthView from "./components/AuthView"
import OnboardingView from "./components/OnboardingView"
import ResultsView from "./components/ResultsView"
import HistoryView from "./components/HistoryView"
import ProfileView from "./components/ProfileView"
import HowItWorksView from "./components/HowItWorksView"
import MeetStatusBanner from "./components/MeetStatusBanner"
import FeedbackModal from "./components/FeedbackModal"
const isExtension = typeof chrome !== "undefined" && !!chrome.runtime?.id
const isFullPage = new URLSearchParams(window.location.search).get("fullpage") === "1"
function openFullPage() {
chrome.tabs.create({ url: chrome.runtime.getURL("index.html") + "?fullpage=1" })
}
export default function App() {
const [session, setSession] = useState(null)
const [authLoading, setAuthLoading] = useState(true)
const [onboardingDone, setOnboardingDone] = useState(
() => !!localStorage.getItem("bm_onboarding_v1")
)
const [view, setView] = useState("profile")
const [results, setResults] = useState(null)
const [showAccountMenu, setShowAccountMenu] = useState(false)
const [confirmDelete, setConfirmDelete] = useState(false)
const [deleting, setDeleting] = useState(false)
const [showFeedback, setShowFeedback] = useState(false)
// Each key tracks how many times we've navigated TO that view — re-mounting
// the wrapper div causes the .view-enter animation to fire every tab switch.
const [viewKeys, setViewKeys] = useState({ profile: 0, history: 0 })
useEffect(() => {
if (view === "profile" || view === "history") {
setViewKeys(k => ({ ...k, [view]: k[view] + 1 }))
}
}, [view])
const handleDeleteAccount = async () => {
setDeleting(true)
try {
await api.delete("/api/account")
await supabase.auth.signOut()
setDeleting(false)
setConfirmDelete(false)
} catch (e) {
console.error("Delete account failed:", e)
setDeleting(false)
setConfirmDelete(false)
}
}
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
setSession(session)
setAuthLoading(false)
// Sync token on initial load so background worker can authenticate Meet uploads
if (session && typeof chrome !== 'undefined' && chrome.runtime?.id) {
chrome.runtime.sendMessage({
action: 'sync_token',
token: session.access_token,
userId: session.user.id,
}).catch(() => {})
}
})
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setSession(session)
if (!session) {
setConfirmDelete(false)
setShowAccountMenu(false)
setDeleting(false)
}
if (session) {
// Keep background service worker's JWT in sync for Meet recording
if (typeof chrome !== 'undefined' && chrome.runtime?.id) {
chrome.runtime.sendMessage({
action: 'sync_token',
token: session.access_token,
userId: session.user.id,
}).catch(() => {})
}
}
})
return () => subscription.unsubscribe()
}, [])
// Lets background.js force a fresh token on demand (e.g. after a 401 mid-recording) —
// getSession() proactively refreshes if the current session is expired, regardless of
// whether Supabase's own background auto-refresh timer has fired yet.
useEffect(() => {
if (typeof chrome === 'undefined' || !chrome.runtime?.id) return
function onMessage(msg, _sender, sendResponse) {
if (msg.action !== 'request_token_refresh') return
supabase.auth.getSession().then(({ data: { session } }) => {
if (session) {
chrome.runtime.sendMessage({
action: 'sync_token',
token: session.access_token,
userId: session.user.id,
}).catch(() => {})
}
sendResponse({ token: session?.access_token || null, userId: session?.user?.id || null })
})
return true
}
chrome.runtime.onMessage.addListener(onMessage)
return () => chrome.runtime.onMessage.removeListener(onMessage)
}, [])
if (authLoading) return (
<div style={{ textAlign: "center", padding: 80, color: "#2e3464" }}>
Loading…
</div>
)
if (!session) return <AuthView onAuth={setSession} />
if (!onboardingDone) return (
<OnboardingView onDone={() => setOnboardingDone(true)} />
)
return (
<>
{/* Ambient background blobs */}
<div style={{ position: "fixed", inset: 0, overflow: "hidden", pointerEvents: "none", zIndex: 0 }}>
<div style={{
position: "absolute", top: "-15%", right: "-8%",
width: 650, height: 650, borderRadius: "50%",
background: "radial-gradient(circle, rgba(29,78,216,0.09) 0%, transparent 68%)",
filter: "blur(1px)",
animation: "blobDrift1 22s ease-in-out infinite",
}} />
<div style={{
position: "absolute", bottom: "-12%", left: "-6%",
width: 550, height: 550, borderRadius: "50%",
background: "radial-gradient(circle, rgba(8,145,178,0.07) 0%, transparent 68%)",
filter: "blur(1px)",
animation: "blobDrift2 28s ease-in-out infinite",
}} />
</div>
<div style={{ maxWidth: isFullPage ? 900 : 600, margin: "0 auto", padding: isFullPage ? "32px 40px" : 24, position: "relative", zIndex: 1 }}>
{/* Header */}
<div style={{ marginBottom: 28, display: "flex",
justifyContent: "space-between", alignItems: "flex-start" }}>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<svg width="26" height="26" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="hdr-g" x1="0" y1="0" x2="32" y2="0" gradientUnits="userSpaceOnUse">
<stop stopColor="#1d4ed8"/><stop offset="1" stopColor="#0891b2"/>
</linearGradient>
</defs>
<rect x="2" y="12" width="4" height="4" rx="2" fill="url(#hdr-g)" opacity=".4"/>
<rect x="8" y="8" width="4" height="8" rx="2" fill="url(#hdr-g)" opacity=".65"/>
<rect x="14" y="4" width="4" height="12" rx="2" fill="url(#hdr-g)"/>
<rect x="20" y="8" width="4" height="8" rx="2" fill="url(#hdr-g)" opacity=".65"/>
<rect x="26" y="12" width="4" height="4" rx="2" fill="url(#hdr-g)" opacity=".4"/>
<line x1="0" y1="17.5" x2="32" y2="17.5" stroke="#1e2438" strokeWidth="1"/>
<rect x="2" y="18" width="4" height="4" rx="2" fill="url(#hdr-g)" opacity=".18"/>
<rect x="8" y="18" width="4" height="8" rx="2" fill="url(#hdr-g)" opacity=".3"/>
<rect x="14" y="18" width="4" height="12" rx="2" fill="url(#hdr-g)" opacity=".38"/>
<rect x="20" y="18" width="4" height="8" rx="2" fill="url(#hdr-g)" opacity=".3"/>
<rect x="26" y="18" width="4" height="4" rx="2" fill="url(#hdr-g)" opacity=".18"/>
</svg>
<h1 style={{ fontSize: 22, fontWeight: 700, margin: 0, letterSpacing: "-0.3px", color: "#f0eeff" }}>
mirror<span style={{ color: "#1d4ed8" }}>.</span>
</h1>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{/* Open as full page — only in side panel */}
{isExtension && !isFullPage && (
<button
onClick={openFullPage}
title="Open in full page"
style={{
background: "none", border: "1px solid #1e2438",
borderRadius: 7, padding: "6px 10px",
cursor: "pointer", color: "#4a4865",
display: "flex", alignItems: "center",
transition: "border-color 0.15s, color 0.15s",
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = "#2e3464"; e.currentTarget.style.color = "#8b89aa" }}
onMouseLeave={e => { e.currentTarget.style.borderColor = "#1e2438"; e.currentTarget.style.color = "#4a4865" }}
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M8.5 1.5H12.5V5.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M12.5 1.5L7.5 6.5" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
<path d="M6 2.5H2.5C1.9 2.5 1.5 2.9 1.5 3.5V11.5C1.5 12.1 1.9 12.5 2.5 12.5H10.5C11.1 12.5 11.5 12.1 11.5 11.5V8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
</svg>
</button>
)}
<div style={{ position: "relative" }}>
{confirmDelete ? (
<div style={{ display: "flex", alignItems: "center", gap: 8,
background: "rgba(248,113,113,0.08)", border: "1px solid rgba(248,113,113,0.25)",
borderRadius: 8, padding: "8px 12px", fontSize: 13 }}>
<span style={{ color: "#f87171" }}>Delete all data?</span>
<button onClick={() => { setConfirmDelete(false); setShowAccountMenu(false) }}
style={{ background: "none", border: "none", cursor: "pointer",
color: "#8b89aa", fontSize: 13, padding: "0 4px" }}>
Cancel
</button>
<button onClick={handleDeleteAccount} disabled={deleting}
style={{ background: "#f87171", color: "white", border: "none",
borderRadius: 6, padding: "4px 12px", fontSize: 13, fontWeight: 600,
cursor: deleting ? "not-allowed" : "pointer", opacity: deleting ? 0.6 : 1 }}>
{deleting ? "Deleting…" : "Yes, delete"}
</button>
</div>
) : (
<>
<button onClick={() => setShowAccountMenu(v => !v)}
style={{ background: "#151922", border: "1px solid #1e2438", borderRadius: 7,
padding: "6px 14px", cursor: "pointer", fontSize: 13, color: "#8b89aa" }}>
Account ▾
</button>
{showAccountMenu && (
<div style={{ position: "absolute", right: 0, top: "110%",
background: "#151922", border: "1px solid #1e2438", borderRadius: 10,
boxShadow: "0 8px 40px rgba(0,0,0,0.7)", zIndex: 20,
minWidth: 200, overflow: "hidden" }}>
{[
{ label: "How it works",
onClick: () => { setView("howItWorks"); setShowAccountMenu(false) },
color: "#f0eeff" },
{ label: "Send feedback",
onClick: () => { setShowFeedback(true); setShowAccountMenu(false) },
color: "#f0eeff" },
{ label: "Sign out",
onClick: () => { supabase.auth.signOut(); setShowAccountMenu(false) },
color: "#f0eeff" },
{ label: "Delete account",
onClick: () => { setConfirmDelete(true); setShowAccountMenu(false) },
color: "#f87171" },
].map(({ label, onClick, color }) => (
<button key={label} onClick={onClick}
style={{ display: "block", width: "100%", padding: "10px 14px",
textAlign: "left", background: "none", border: "none",
borderBottom: "1px solid #1e2438",
cursor: "pointer", fontSize: 13, color }}>
{label}
</button>
))}
<a
href="https://mirrorai.live/privacy"
target="_blank"
rel="noreferrer"
style={{ display: "block", padding: "10px 14px",
fontSize: 13, color: "#4a4d6a", textDecoration: "none" }}
onMouseEnter={e => e.currentTarget.style.color = "#8b89aa"}
onMouseLeave={e => e.currentTarget.style.color = "#4a4d6a"}>
Privacy Policy
</a>
</div>
)}
</>
)}
</div>
</div>{/* end flex header-right */}
</div>
{!isFullPage && <MeetStatusBanner onViewHistory={() => setView("history")} />}
{/* Nav */}
<nav style={{ display: "flex", gap: 0, marginBottom: 32,
borderBottom: "1px solid #1e2438" }}>
{[
{ key: "profile", label: "Profile" },
{ key: "history", label: "History" },
].map(({ key, label }) => (
<button key={key} onClick={() => setView(key)}
className={`nav-tab${view === key ? " active" : ""}`}
style={{ background: "none", border: "none", cursor: "pointer",
fontWeight: view === key ? 600 : 400, fontSize: 14,
color: view === key ? "#5b9cf6" : "#8b89aa",
padding: "0 16px 12px" }}>
{label}
</button>
))}
</nav>
{showFeedback && (
<FeedbackModal onClose={() => setShowFeedback(false)} />
)}
{view === "howItWorks" && (
<HowItWorksView onBack={() => setView("profile")} />
)}
{/* Persistent views — kept mounted, but wrapper remounts each tab visit
so .view-enter animation fires every time you switch to that tab */}
<div style={{ display: view === "profile" ? "block" : "none" }}>
<div key={`profile-${viewKeys.profile}`} className="view-enter">
<ProfileView active={view === "profile"} />
</div>
</div>
{view === "results" && results && (
<div className="view-enter">
<ResultsView results={results} onBack={() => setView("history")} />
</div>
)}
<div style={{ display: view === "history" ? "block" : "none" }}>
<div key={`history-${viewKeys.history}`} className="view-enter">
<HistoryView active={view === "history"}
onSelect={(r) => { setResults(r); setView("results") }} />
</div>
</div>
</div>
</>
)
}