D Diagent docs

Run your workspace

Live human handoff

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.

Visitor flow

  1. The widget header carries an always-visible Talk to a human button. Visitors don't need to wait for the LLM to surface a pill β€” one click reaches a human from any conversation state. The button stays for every agent whose vertical includes the ticket_escalation capability (every preset ships with it enabled by default).
  2. The agent also detects human-handoff intent in the message body β€” HumanIntentDetector matches against ~40 phrases ("talk to a human", "connect me to an agent", "I need to speak with someone", etc.). Typing one of those short-circuits the LLM and goes straight to the handoff flow.
  3. On click / intent-match, three things happen instantly:
    • The conversation is flagged with a human_requested_at timestamp on the server.
    • A HumanRequestedEvent broadcasts to the workspace's Reverb private channel so dashboard tabs update in real time.
    • NotifyOperatorsHumanRequestedJob fans out database + mail notifications to every workspace member with live_chat_available=true.
  4. The widget shows a "Connecting you with someone…" banner. The visitor stays on this banner for up to 2 minutes. While the conversation is in the waiting window, the bot is silent β€” every subsequent visitor message is queued for the operator instead of getting an LLM reply.
  5. If an operator claims within the window, the banner flips to "Sarah joined the chat" (or "An agent joined" if you've turned off personalization in workspace settings).
  6. If no operator claims after the 2-minute timeout, the widget surfaces "No one's around right now β€” drop your email and we'll reach out as soon as someone's free." The lead form opens for out-of-band follow-up. The notification still went out, so operators can claim the conversation from the dashboard later.
  7. 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).

Operator flow

  1. The Conversations sidebar entry shows a red badge with the count of conversations waiting for a human.
  2. Open /app/conversations and switch to the Needs human filter pill. Each row shows when the visitor clicked the human button, what page they're on, and their captured email if any.
  3. Click into a row β†’ the thread shows live messages. Click Claim at the top right to take the conversation.
  4. While you have the conversation claimed:
    • The bot will not auto-respond.
    • Use the reply box at the bottom (Cmd / Ctrl + Enter sends).
    • Visitor messages appear in real time β€” the page polls every 2 seconds while you're claimed.
  5. Click Release when you're done. The bot resumes on the next visitor message; the conversation history stays intact for analytics + future reference.

Workspace settings

A few knobs live in Settings on the workspace level:

  • Live chat personalize (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 <Brand>" instead.

Notifications

Two cascades fire when a visitor asks for a human:

  • HumanRequestedEvent broadcast on the workspace's Reverb private channel. Every open dashboard tab picks it up and updates the Conversations sidebar badge + plays a sonner toast in real time.
  • NotifyOperatorsHumanRequestedJob β€” queued database + mail notification (HumanRequestedNotification) fanned out to every workspace member with users.live_chat_available = true. Members who had the dashboard closed still get an email so they can claim the conversation on next sign-in.

If a captured lead also fires, the lead-captured email + dashboard toast cascade runs in addition. The handoff notification is idempotent: a second click within the 2-minute waiting window does not re-broadcast or re-notify.

Smart routing

The server picks one of three responses based on operator availability and your business hours when a visitor asks for a human:

  • Queued (operator online). An operator is online and within your business hours. The visitor sees "Connecting you with someone…", the conversation gets flagged, the bot stays silent, and operators get notified. The wait is capped at 2 minutes (RequestHumanController::WAIT_TIMEOUT_SECONDS = 120).
  • Queued anyway (no operators online). Within business hours but nobody's marked themselves Receive live chats. Still queues + still notifies β€” the database + mail notification reaches every member with the live-chat opt-in, so they can claim the conversation when they open the dashboard next. The visitor sees the same "Connecting you…" banner for the 2-minute window. After the timeout, the widget flips to "No one's around right now β€” drop your email and we'll reach out."
  • Offline β€” after hours. Outside the business hours you've configured. Visitor sees "We're closed right now. We're back <day at time>. Drop your email and we'll follow up first thing."

Operator opt-in

Each workspace member sets their availability individually in Profile settings:

  • Receive live chats checkbox β€” when on AND your admin tab has been active in the last 2 minutes, you count as an available operator.
  • Heartbeat fires every 60s while the tab is in the foreground, pauses when the tab is hidden.

Business hours

Configure in Settings β†’ Live chat. JSON-edited for now (a visual grid editor lands in the next release):

{
  "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":    []
  }
}
  • Day keys lowercase. 24-hour HH:mm. Empty array = closed all day.
  • Multiple windows per day are supported (e.g. lunch break).
  • Timezone is any IANA identifier β€” the server validates against PHP's tz database.
  • Set "enabled": false (or leave the whole field blank) to stay always-on.

Slack / Teams notifications

In Settings β†’ Live chat, 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.

  • Slack β€” uses the standard incoming-webhook URL from the Slack app config. Slack auto-unfurls the conversation link.
  • Microsoft Teams β€” incoming-webhook URL from the Teams channel connector. We post as a MessageCard with an "Open conversation" button.

Auto-fallback for unclaimed conversations

Visitors should never sit on a "Connecting you…" bubble forever. The widget runs a client-side 2-minute timer the moment the handoff is requested (see RequestHumanController::WAIT_TIMEOUT_SECONDS). When the timer fires without a claim, the banner flips to the "No one's around" copy and the lead form opens so the visitor can leave their email. The conversation row stays flagged on the server so the operator can still claim later β€” the notification cascade fires once on the initial request, so operators see the queued conversation in the Conversations sidebar regardless of whether the visitor stayed on the page.

Operator polish (Phase 3)

Canned replies

Save the replies your team types over and over (password reset instructions, refund policy, shipping ETAs) at Settings β†’ Canned replies. 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.

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

Internal notes

Toggle the reply box from Reply to Internal note (the textarea turns amber). Internal notes are visible to other operators in the conversation thread but never 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.

Typing indicators

Both directions, no setup required:

  • The visitor sees "<Operator> is typing…" above their chat while you're typing in the operator console.
  • The operator sees a three-dot bubble in the message log while the visitor is typing in the widget.

Implemented as 5-second self-expiring server-side timestamps; both sides poll the existing endpoints, so no extra infrastructure is needed.

Conversation transfer

Click the Transfer 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.

Business-hours grid editor

The Phase 2 JSON textarea is replaced by a visual 7-day grid at Settings β†’ Live chat. 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.

Tags + ratings + UI rebuild (Phase 4)

Conversation tags

Categorize conversations so your team can filter and report on them. Manage the list at Settings β†’ Tags: each tag has a label (max 60 chars) and a hex color that drives the chip background.

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

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.

Satisfaction ratings

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.

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.

Conversation thread UI rebuild

The thread page is now a true help-desk surface:

  • Two-pane layout 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.
  • Right-pane sidebar 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).
  • Compact unified action bar at the top: live pill, status badges, claim / release / force-release buttons in one cluster.
  • Composer stays at the bottom with Reply / Note tabs, canned reply picker, transfer dropdown, and typing debouncer (all carried over from Phase 3).

Routes shipped (cumulative)

  • POST /app/conversations/{id}/note β€” internal note
  • POST /app/conversations/{id}/typing β€” operator typing hint
  • POST /app/conversations/{id}/transfer β€” reassign claim
  • POST / DELETE /app/conversations/{id}/tags/{tagId} β€” attach / detach tag
  • POST /api/v1/widget/typing β€” visitor typing hint (JWT)
  • POST /api/v1/widget/satisfaction β€” visitor rating (JWT)
  • GET / POST / PATCH / DELETE /app/settings/canned-replies/… β€” CRUD + reorder
  • GET / POST / PATCH / DELETE /app/settings/tags/… β€” workspace tag CRUD