<p>
    Visitors who hit a wall with the AI agent can ask for a human and
    actually get one — in real time, using the same Conversations menu
    your team already uses to read transcripts.
</p>

<h2>Visitor flow</h2>

<ol class="mt-2 list-decimal space-y-1.5 ps-6 text-[15px] leading-7 text-slate-700">
    <li>
        The agent (on the help-center vertical, or via a curated answer
        you've configured) shows a <strong>Connect me with a human</strong>
        pill in its reply.
    </li>
    <li>
        The visitor clicks the pill. Two things happen instantly:
        <ul class="mt-1 list-disc space-y-0.5 ps-6 text-sm">
            <li>
                The conversation is flagged with a <code>human_requested_at</code>
                timestamp on the server.
            </li>
            <li>
                The lead-capture form opens so the visitor can leave their
                email — useful if they navigate away before an operator
                joins, so your team can follow up out-of-band.
            </li>
        </ul>
    </li>
    <li>
        The widget shows a <strong>"Connecting you with someone…"</strong>
        banner. Until an operator claims the conversation, the bot is
        silent — every subsequent visitor message is queued for the
        operator instead of getting an LLM reply.
    </li>
    <li>
        When an operator claims, the banner flips to
        <strong>"Sarah joined the chat"</strong> (or
        <strong>"An agent joined"</strong> if you've turned off
        personalization in workspace settings — see below).
    </li>
    <li>
        The operator's replies appear inline as chat bubbles with an
        "Operator" label. The visitor's messages flow back to the
        operator's console live (3-second polling).
    </li>
</ol>

<h2>Operator flow</h2>

<ol class="mt-2 list-decimal space-y-1.5 ps-6 text-[15px] leading-7 text-slate-700">
    <li>
        The <strong>Conversations</strong> sidebar entry shows a red
        badge with the count of conversations waiting for a human.
    </li>
    <li>
        Open <a href="/app/conversations" class="font-semibold underline-offset-2 hover:underline">/app/conversations</a>
        and switch to the <strong>Needs human</strong> filter pill.
        Each row shows when the visitor clicked the human button, what
        page they're on, and their captured email if any.
    </li>
    <li>
        Click into a row → the thread shows live messages. Click
        <strong>Claim</strong> at the top right to take the conversation.
    </li>
    <li>
        While you have the conversation claimed:
        <ul class="mt-1 list-disc space-y-0.5 ps-6 text-sm">
            <li>The bot will <strong>not</strong> auto-respond.</li>
            <li>Use the reply box at the bottom (Cmd / Ctrl + Enter sends).</li>
            <li>Visitor messages appear in real time — the page polls every 2 seconds while you're claimed.</li>
        </ul>
    </li>
    <li>
        Click <strong>Release</strong> when you're done. The bot resumes
        on the next visitor message; the conversation history stays
        intact for analytics + future reference.
    </li>
</ol>

<h2>Workspace settings</h2>

<p>
    A few knobs live in <a href="/settings/system" class="font-semibold underline-offset-2 hover:underline">Settings</a> on
    the workspace level:
</p>

<ul class="mt-2 list-disc space-y-2 ps-6 text-[15px] leading-7 text-slate-700">
    <li>
        <strong>Live chat personalize</strong> (default ON). When on,
        the visitor sees the operator's real name in the joined
        banner. Turn it off for regulated industries (legal,
        healthcare, finance) where individual operator identities
        shouldn't be exposed — the visitor sees "An agent from
        &lt;Brand&gt;" instead.
    </li>
</ul>

<h2>Notifications</h2>

<p>
    Every captured lead fires the existing notification cascade
    (lead-captured email, in-dashboard sonner toast, browser push for
    members who opted in). The same triggers apply when a visitor
    requests a human — the lead form firing is the canonical signal.
</p>

<h2>Smart routing (Phase 2)</h2>

<p>
    When a visitor clicks <strong>Connect me with a human</strong>, the
    server picks one of three responses based on operator availability
    and your business hours:
</p>

<ul class="mt-2 list-disc space-y-2 ps-6 text-[15px] leading-7 text-slate-700">
    <li>
        <strong>Queued.</strong> An operator is online and within your
        business hours. The visitor sees
        <em>"Connecting you with someone…"</em>, the conversation gets
        flagged, and the bot stays silent until claimed.
    </li>
    <li>
        <strong>Offline — no operators around.</strong> Within business
        hours but nobody's marked themselves <em>Receive live chats</em>
        in their profile (or no admin tabs are open). Visitor sees
        <em>"No one's around right now — drop your email and we'll
        reach out."</em> The conversation is <strong>not</strong>
        flagged; the bot resumes normally on the visitor's next message.
    </li>
    <li>
        <strong>Offline — after hours.</strong> Outside the business
        hours you've configured. Visitor sees <em>"We're closed right
        now. We're back &lt;day at time&gt;. Drop your email and we'll
        follow up first thing."</em>
    </li>
</ul>

<h2>Operator opt-in</h2>

<p>
    Each workspace member sets their availability individually in
    <a href="/settings/profile" class="font-semibold underline-offset-2 hover:underline">Profile settings</a>:
</p>

<ul class="mt-2 list-disc space-y-1 ps-6 text-[15px] leading-7 text-slate-700">
    <li>
        <strong>Receive live chats</strong> checkbox — when on AND your
        admin tab has been active in the last 2 minutes, you count as
        an available operator.
    </li>
    <li>
        Heartbeat fires every 60s while the tab is in the foreground,
        pauses when the tab is hidden.
    </li>
</ul>

<h2>Business hours</h2>

<p>
    Configure in
    <a href="/app/settings/live-chat" class="font-semibold underline-offset-2 hover:underline">Settings → Live chat</a>.
    JSON-edited for now (a visual grid editor lands in the next
    release):
</p>

<pre class="mt-3 overflow-x-auto rounded-md border bg-slate-50 p-3 text-[13px] leading-6 text-slate-700"><code>{
  "enabled": true,
  "timezone": "America/New_York",
  "schedule": {
    "monday":    [{"start": "09:00", "end": "17:00"}],
    "tuesday":   [{"start": "09:00", "end": "17:00"}],
    "wednesday": [
      {"start": "09:00", "end": "12:00"},
      {"start": "13:00", "end": "17:00"}
    ],
    "thursday":  [{"start": "09:00", "end": "17:00"}],
    "friday":    [{"start": "09:00", "end": "17:00"}],
    "saturday":  [],
    "sunday":    []
  }
}</code></pre>

<ul class="mt-3 list-disc space-y-1 ps-6 text-[15px] leading-7 text-slate-700">
    <li>Day keys lowercase. 24-hour <code>HH:mm</code>. Empty array = closed all day.</li>
    <li>Multiple windows per day are supported (e.g. lunch break).</li>
    <li>Timezone is any IANA identifier — the server validates against PHP's tz database.</li>
    <li>Set <code>"enabled": false</code> (or leave the whole field blank) to stay always-on.</li>
</ul>

<h2>Slack / Teams notifications</h2>

<p>
    In <a href="/app/settings/live-chat" class="font-semibold underline-offset-2 hover:underline">Settings → Live chat</a>,
    paste an incoming-webhook URL for either platform. When a visitor
    asks for a human, a queued listener fires a compact ping with the
    conversation URL so an operator can jump straight in from
    Slack / Teams without opening the dashboard.
</p>

<ul class="mt-2 list-disc space-y-1 ps-6 text-[15px] leading-7 text-slate-700">
    <li>
        Slack — uses the standard <code>incoming-webhook</code> URL from
        the Slack app config. Slack auto-unfurls the conversation link.
    </li>
    <li>
        Microsoft Teams — incoming-webhook URL from the Teams channel
        connector. We post as a MessageCard with an "Open conversation"
        button.
    </li>
</ul>

<h2>Auto-fallback for unclaimed conversations</h2>

<p>
    Visitors should never sit on a "Connecting you…" bubble forever.
    A scheduled job runs every minute and clears the
    <code>human_requested_at</code> flag on any conversation that's
    been waiting more than 5 minutes without a claim. The bot drops a
    closure note ("Looks like everyone's busy at the moment — leave
    your email and we'll follow up") and resumes auto-replying.
</p>

<h2>Operator polish (Phase 3)</h2>

<h3>Canned replies</h3>

<p>
    Save the replies your team types over and over (password reset
    instructions, refund policy, shipping ETAs) at
    <a href="/app/settings/canned-replies" class="font-semibold underline-offset-2 hover:underline">Settings → Canned replies</a>.
    Each entry has a short label (what operators search by) and the
    full reply text. Reorder with the up/down handles — most-used
    replies should sit at the top.
</p>

<p>
    In any live conversation, click the <strong>Canned reply</strong>
    button above the textarea. Fuzzy-search the label or content,
    pick one, and the textarea fills in. The operator can edit before
    hitting Send.
</p>

<h3>Internal notes</h3>

<p>
    Toggle the reply box from <strong>Reply</strong> to
    <strong>Internal note</strong> (the textarea turns amber). Internal
    notes are visible to other operators in the conversation thread
    but <em>never</em> sent to the visitor. Useful for handoff
    context: "Visitor seems frustrated — I tried X already, please
    pick up." Auto-generated transfer audit messages also use this
    role.
</p>

<h3>Typing indicators</h3>

<p>Both directions, no setup required:</p>
<ul class="mt-2 list-disc space-y-1 ps-6 text-[15px] leading-7 text-slate-700">
    <li>The visitor sees <em>"&lt;Operator&gt; is typing…"</em> above
        their chat while you're typing in the operator console.</li>
    <li>The operator sees a three-dot bubble in the message log while
        the visitor is typing in the widget.</li>
</ul>
<p class="mt-2">
    Implemented as 5-second self-expiring server-side timestamps; both
    sides poll the existing endpoints, so no extra infrastructure is
    needed.
</p>

<h3>Conversation transfer</h3>

<p>
    Click the <strong>Transfer</strong> button in the reply bar. Online
    teammates surface at the top with a green "Online" badge; offline
    teammates are still listed (you may want to hand off to someone
    who'll claim later). Picking a target reassigns the claim,
    broadcasts to the visitor's widget so the "joined the chat" banner
    refreshes to the new operator's name, and drops a system note in
    the thread for context.
</p>

<h3>Business-hours grid editor</h3>

<p>
    The Phase 2 JSON textarea is replaced by a visual 7-day grid at
    <a href="/app/settings/live-chat" class="font-semibold underline-offset-2 hover:underline">Settings → Live chat</a>.
    Click the "+ Window" button on any day to add another open block
    (lunch break, split shifts), or check "Closed" to take that day
    off. Timezone picker has the 15 most common IANA zones plus a
    "Custom…" option for any zone PHP recognizes.
</p>

<h2>Tags + ratings + UI rebuild (Phase 4)</h2>

<h3>Conversation tags</h3>

<p>
    Categorize conversations so your team can filter and report on them.
    Manage the list at
    <a href="/app/settings/tags" class="font-semibold underline-offset-2 hover:underline">Settings → Tags</a>:
    each tag has a label (max 60 chars) and a hex color that drives the
    chip background.
</p>

<p>
    In any conversation thread, the end-side panel has a Tags
    section. Click a chip's <strong>×</strong> to detach;
    <strong>+ Tag</strong> opens a fuzzy-search dropdown of unselected
    workspace tags.
</p>

<p>
    Conversations list: a "Tags" select dropdown joins the existing
    Needs human / Live now filter pills. Each row also surfaces
    applied tags as compact chips alongside the existing badges.
</p>

<h3>Satisfaction ratings</h3>

<p>
    After an operator releases the conversation, the visitor sees a
    "Was this helpful?" prompt with thumbs up / down buttons + an
    optional comment field. The first rating is locked server-side;
    later submissions update the comment only — buyers complained
    about Intercom-style "rating overwritten" surprises.
</p>

<p>
    Operator side: the end-pane context panel surfaces the rating
    + comment under the visitor card. The Conversations list also
    shows a 👍 / 👎 chip on each row when a rating exists.
</p>

<h3>Conversation thread UI rebuild</h3>

<p>
    The thread page is now a true help-desk surface:
</p>

<ul class="mt-2 list-disc space-y-1 ps-6 text-[15px] leading-7 text-slate-700">
    <li><strong>Two-pane layout</strong> on desktop (≥ md) — chat on
        the left, context sidebar on the right. The sidebar collapses
        to a Sheet drawer with a "Details" button on mobile.</li>
    <li><strong>Right-pane sidebar</strong> sections: visitor (anon
        ID, language, returning flag, page URL with link icon), lead
        (email mailto / phone tel: links), tags, satisfaction signal,
        timing tile (claim age / waiting time / started-at).</li>
    <li><strong>Compact unified action bar</strong> at the top: live
        pill, status badges, claim / release / force-release buttons
        in one cluster.</li>
    <li><strong>Composer</strong> stays at the bottom with Reply /
        Note tabs, canned reply picker, transfer dropdown, and typing
        debouncer (all carried over from Phase 3).</li>
</ul>

<h2>Routes shipped (cumulative)</h2>

<ul class="mt-2 list-disc space-y-1 ps-6 text-[15px] leading-7 text-slate-700">
    <li><code>POST /app/conversations/&#123;id&#125;/note</code> — internal note</li>
    <li><code>POST /app/conversations/&#123;id&#125;/typing</code> — operator typing hint</li>
    <li><code>POST /app/conversations/&#123;id&#125;/transfer</code> — reassign claim</li>
    <li><code>POST / DELETE /app/conversations/&#123;id&#125;/tags/&#123;tagId&#125;</code> — attach / detach tag</li>
    <li><code>POST /api/v1/widget/typing</code> — visitor typing hint (JWT)</li>
    <li><code>POST /api/v1/widget/satisfaction</code> — visitor rating (JWT)</li>
    <li><code>GET / POST / PATCH / DELETE /app/settings/canned-replies/…</code> — CRUD + reorder</li>
    <li><code>GET / POST / PATCH / DELETE /app/settings/tags/…</code> — workspace tag CRUD</li>
</ul>
