import { useEffect, useRef, useState } from 'react'
import { useClerk, useOrganization, useOrganizationList, useUser } from '@clerk/react'
import { useQuery } from '@tanstack/react-query'
import { NavLink, Outlet, useLocation } from 'react-router-dom'
import { LANGS, locale, setLang, t, useLang, type Key } from '../i18n'
import { workspaceCredits } from '../api/engine'
import { Brand, Icon, type IconName } from './Icon'
import { Toaster } from './Toast'
// Learning Studio (learning-lab/frontend); both apps share one Clerk instance, so the user stays signed in.
const STUDIO_URL: string = import.meta.env.VITE_STUDIO_URL
// The Production dashboard shows what Express is making; the four settings doors run it:
// feed the Brain, give an instruction, watch the schedule, collect results.
export const NAV_GROUPS: { label: Key; items: { to: string; label: Key; icon: IconName }[] }[] = [
{ label: 'nav.dashboard', items: [{ to: '/production', label: 'nav.production', icon: 'chart' }] },
{
label: 'nav.settings',
items: [
{ to: '/upload', label: 'nav.brain', icon: 'brain' },
{ to: '/brain', label: 'nav.instruction', icon: 'spark' },
{ to: '/schedule', label: 'nav.schedule', icon: 'calendar' },
{ to: '/results', label: 'nav.results', icon: 'layers' },
],
},
]
export const NAV = NAV_GROUPS.flatMap(g => g.items)
export function LanguageSwitch() {
const lang = useLang()
return (
{LANGS.map(l => (
setLang(l.code)}>{l.label}
))}
)
}
// The four steps of Express, from the mockup's "how it works" guide.
const HELP_STEPS: { icon: IconName; title: Key; body: Key }[] = [
{ icon: 'brain', title: 'help.brainTitle', body: 'help.brainBody' },
{ icon: 'spark', title: 'help.instructionTitle', body: 'help.instructionBody' },
{ icon: 'calendar', title: 'help.scheduleTitle', body: 'help.scheduleBody' },
{ icon: 'layers', title: 'help.resultsTitle', body: 'help.resultsBody' },
]
function HelpDialog({ onClose }: { onClose: () => void }) {
const ref = useRef(null)
// Same pattern as the Results preview: open once, let the dialog's own close event unmount it.
useEffect(() => { if (ref.current && !ref.current.open) ref.current.showModal() }, [])
return (
e.target === ref.current && ref.current.close()}>
{t('help.title')} {t('help.subtitle')}
ref.current?.close()}>
{HELP_STEPS.map((s, i) => (
))}
{t('help.note')}
ref.current?.close()}>{t('help.done')}
)
}
// Polled because agents and schedules also spend credits outside this tab; main.tsx refreshes it after every mutation.
function CreditBalance() {
const credits = useQuery({ queryKey: ['credits'], queryFn: workspaceCredits, refetchInterval: 30_000 })
if (credits.data == null) return null
return (
{t('common.credits', { n: credits.data.toLocaleString(locale()) })}
)
}
// The Learning space card switches the active organization. AuthGate keys the pages by orgId and
// clears the query cache, so Brain, schedules, results and credits all reload for the new one.
function OrgSwitcher() {
const [open, setOpen] = useState(false)
const ref = useRef(null)
const { organization } = useOrganization()
const { setActive, userMemberships } = useOrganizationList({ userMemberships: { infinite: true } })
useEffect(() => {
if (!open) return
const onClick = (e: MouseEvent) => { if (!ref.current?.contains(e.target as Node)) setOpen(false) }
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) }
document.addEventListener('mousedown', onClick)
document.addEventListener('keydown', onKey)
return () => { document.removeEventListener('mousedown', onClick); document.removeEventListener('keydown', onKey) }
}, [open])
const choose = (id: string) => {
setOpen(false)
if (id !== organization?.id) void setActive?.({ organization: id })
}
return (
setOpen(o => !o)}>
{(organization?.name || 'L').charAt(0).toUpperCase()}
{t('layout.learningSpace')}{organization?.name ?? '-'}
{open && (
{userMemberships.data?.map(({ organization: org }) => (
choose(org.id)}>
{org.name.charAt(0).toUpperCase()}
{org.name}
{org.id === organization?.id && }
))}
)}
)
}
export function AppLayout() {
useLang()
const [help, setHelp] = useState(false)
const { user } = useUser()
const { signOut } = useClerk()
const { pathname } = useLocation()
const current = NAV.find(n => pathname.startsWith(n.to))
// Nav groups stay closed unless they hold the current page; a click on the label overrides that until the next route.
const [toggled, setToggled] = useState>>({})
useEffect(() => setToggled({}), [pathname])
const name = user?.fullName || user?.primaryEmailAddress?.emailAddress || '-'
return (
{/* Keyed by route so each page plays the fade-in from theme.css. */}
Learning Lab Express by Bawana {t('layout.footer')}
{help &&
setHelp(false)} />}
)
}