Skip to content

feat: stream chat responses with automatic poll fallback#176

Open
HardeepAsrani wants to merge 3 commits into
feat/variousfrom
feat/streaming-responses
Open

feat: stream chat responses with automatic poll fallback#176
HardeepAsrani wants to merge 3 commits into
feat/variousfrom
feat/streaming-responses

Conversation

@HardeepAsrani

Copy link
Copy Markdown
Member

Summary

Adds progressive (streaming) chat replies so the assistant's answer appears as it's written, with an automatic fallback to the existing background + polling flow on hosting that buffers or can't stream — the bot keeps working everywhere.

Implements Codeinwp/hyve#183.

⚠️ Stacked on #173 — this branch targets feat/various because it builds on that branch's frontend widget + get_frontend_data. Merge/retarget after #173 lands.

How it works

  • Streaming endpoint — a custom admin-ajax action (inc/Stream.php) sets text/event-stream + X-Accel-Buffering: no, calls OpenAI with stream: true, and flushes each delta. The API key stays server-side (browser ↔ WP ↔ OpenAI).
  • Single source of truth — one prompt, one JSON schema, and one response interpreter (OpenAI::interpret_chat_payload / build_chat_items) drive both the streaming and poll paths, so answers never diverge by transport.
  • Capability detection + fallback — the widget tries streaming first; if no real SSE event arrives within 8s (buffering proxy / no streaming), it aborts and falls back to the proven poll flow. The result is cached per browser and re-probed after 24h.
  • Correct incremental rendering — the streamed response field is decoded via json_decode with incomplete trailing escapes/surrogates/UTF-8 held back, so escapes and emoji render correctly mid-stream and match the final reply.
  • Contracts preserved — moderation stays up front; hyve_chat_request / hyve_chat_response fire once per reply; thread logging, analytics and unanswered-question tracking are unaffected.

Files

  • New inc/Stream.php — SSE endpoint, sentinel-free structured-output streaming, partial-field extraction.
  • inc/OpenAI.phpstream_response() (raw cURL), shared get_chat_response_params / build_chat_items / interpret_chat_payload.
  • inc/API.phpsend_chat split into prepare_chat + create_background_run with a mode param; get_chat uses the shared interpreter.
  • inc/Main.php — registers Stream, localizes ajaxUrl + streamNonce.
  • src/frontend/App.js — streaming attempt + poll fallback, progressive rendering, 24h capability cache.
  • tests/php/unit/tests/test-stream.php — partial-extraction tests (escapes, emoji surrogate pairs, split multibyte, incomplete sequences).

Test plan

  1. Streamable host: send a message → reply appears progressively; non-ASCII/emoji render correctly.
  2. No answer: ask something outside the KB → default_message shown, nothing shown before that's decided.
  3. Buffering host (simulate proxy_buffering on): reply isn't progressive → falls back to poll within ~8s, bot still answers; subsequent messages skip straight to poll for 24h.
  4. Analytics: confirm Threads + message counts + unanswered questions still populate the same as the poll flow.

Notes

  • Streaming uses raw cURL (scoped phpcs:ignore) because wp_remote_* can't read a response body incrementally.
  • The cURL proxy holds one PHP worker per active stream for the reply's duration — inherent to a PHP SSE proxy; worth keeping in mind for high-concurrency sites.
  • Reviewed and hardened: watchdog keys on first event (not first byte), connection_aborted residual edge documented, validation before SSE headers, embedding-transient TTL raised to cover the stream window.

Checks: ESLint + frontend build clean, phpcs clean, phpstan at baseline; PHPUnit/e2e run on CI.

- Redesign the frontend chat widget (header with avatar/name/status,
  modern bubbles, pill input, refined launcher, entrance animation).
- Add a global "Chat Sound" toggle and a per-visitor mute control in the
  chat menu; remove the open/close click sound.
- Add a "Message Timestamp" show/hide toggle.
- Show the welcome message and suggested questions on first load of the
  inline chat block.
- Inline the chat-button SVGs server-side (no runtime fetch / icon flash)
  and use a bundled SVG as the default icon instead of an emoji.
- Keep the typing dots, suggestion chips, and menu legible on similar
  colors, and theme the menu from the chat colors.
- Resolve outstanding PHPStan issues (gmdate guard, require_once ignore).
Reveal the assistant's reply progressively over Server-Sent Events, with a
graceful fallback to the existing background + polling flow on hosts that
buffer or cannot stream, so the bot keeps working everywhere.

- Add a custom admin-ajax streaming endpoint (Stream) that proxies OpenAI's
  stream to the browser; the API key stays server-side.
- Share a single prompt, JSON schema and response interpreter across the
  streaming and poll paths so replies never diverge by transport.
- The widget tries streaming first and falls back to polling when no event
  arrives in time; the outcome is cached per browser and re-probed after 24h.
- Decode the streamed `response` field incrementally via json_decode so
  escapes and surrogate-pair emoji render correctly while streaming.
- Preserve existing contracts: moderation stays up front, hyve_chat_request
  and hyve_chat_response fire once per reply, analytics unaffected.

Closes Codeinwp/hyve#183
@HardeepAsrani HardeepAsrani marked this pull request as ready for review July 2, 2026 16:44
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Plugin build for f6d86fb is ready 🛎️!

@HardeepAsrani

Copy link
Copy Markdown
Member Author

@copilot resolve the merge conflicts in this pull request

…g-responses

Co-authored-by: HardeepAsrani <2649903+HardeepAsrani@users.noreply.github.com>

Copilot AI commented Jul 3, 2026

Copy link
Copy Markdown

@copilot resolve the merge conflicts in this pull request

Resolved in f6d86fb.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants