Files
zeekay e981114824 chat: a signed-out reader asks the buying agent, and it routes them to pay
/chat signed out is the buying agent (components/sales/Guest, Agent): the
question the home composer carried in ?q= is asked of cloud's
POST /v1/ai/sales, the reply is drawn as text, and each next step the
server built is a control: a call (the lead is filed first, then the
calendar opens on it), the sign-up, a plan's card checkout, Add funds. A
quiet line says it is a limited preview and how many messages are left; at
the ceiling the sign-up takes the composer's place. A conversation's own
address still signs in, and a signed-in reader keeps the app.

/buying-specialist is the same agent; its keyword script, its typed prices
and its claims to see an account are gone. The model pages' no-key card is
the real lane, POST /v1/chat/public, not a guest key that answers 401.

Measured in the site's names: chat_started, cta_clicked {cta: kind, plan,
agent: sales}, sales_contacted. The floating privacy button is hidden as
room.css meant: the rule names its tag to outrank gui's atomic class.
2026-10-27 01:29:38 -07:00

61 lines
2.5 KiB
TypeScript

'use client'
// /chat, and by the edge every conversation under it (lib/edge.ts `APP`): the
// app in Chat. A conversation is its path, `/chat/<id>` (@hanzo/app lib/host
// `talk`, `place`).
//
// signed in the app (app/_web.tsx), drawn here as on `/`. A conversation
// at the address it had before, `/chat?thread=<id>`, is moved to
// its path first, once, so Back does not land on it (`moved`).
// signed out at /chat itself, the buying agent (components/sales/Guest): a
// limited free preview that needs no account, asked the question
// `?q=` carried in from the home page's composer. A conversation's
// own address is a signed-in reader's, so there it is sign in
// first (destination.ts `enter`); the address is written down on
// the way out, so the sign-in returns here and the first case
// takes over.
import { useEffect, useState } from 'react'
import dynamic from 'next/dynamic'
import { useIam } from '@hanzo/iam/react'
import { enter } from '@hanzo/app/lib/destination'
import { CHAT, talk } from '@hanzo/app/lib/host'
import { Guest } from '@/components/sales/Guest'
/** The app, fetched only for a signed-in reader. */
const App = dynamic(() => import('../../_web'), { ssr: false })
/** The path a retired Chat address names; null when it is current. */
export function moved(pathname: string, q: URLSearchParams): string | null {
if (pathname !== CHAT) return null
const thread = q.get('thread')
return thread ? talk(thread) : null
}
/** The question a signed-out reader at /chat itself is asked, or null where the
* address is a conversation's and only a signed-in reader may open it. */
export function guest(pathname: string, q: URLSearchParams): string | null {
if (pathname.replace(/\/$/, '') !== CHAT || q.has('thread')) return null
return q.get('q') ?? ''
}
export function Door() {
const { isLoading, isAuthenticated } = useIam()
const [door, setDoor] = useState<{ app: true } | { ask: string } | null>(null)
useEffect(() => {
if (isLoading) return
const q = new URLSearchParams(window.location.search)
if (!isAuthenticated) {
const ask = guest(window.location.pathname, q)
return ask === null ? enter() : setDoor({ ask })
}
const to = moved(window.location.pathname, q)
if (to) window.history.replaceState(null, '', to)
setDoor({ app: true })
}, [isLoading, isAuthenticated])
if (!door) return null
return 'app' in door ? <App /> : <Guest seed={door.ask} source="chat" />
}