Fix chat SSE stream being buffered by compression middleware - #6
Conversation
The global compression() middleware gzip-buffered the /api/chat text/event-stream response, so tokens were held until the response ended instead of streaming incrementally. Mark the SSE response no-transform (which compression respects), disable proxy buffering, and flush after each event write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk
The full-duplex voice broker was reachable only from loopback or with a debug token, so it could not run on a deployed persistent Node host. Accept a WebSocket upgrade when the request Origin matches the app host (with x-forwarded-host support behind a proxy), which lets the deployed app's own page connect while blocking cross-site WebSocket hijacking. Voice keys stay BYOK per connection; server fallback keys remain gated behind the existing ALLOW_SERVER_* env flags. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk
The intro cards mounted with transform: translate(-50%, 80px), which GSAP parses as a pixel x offset and caches under every subsequent xPercent tween, permanently shifting the whole fan half a card-width left — on phones the featured upload card hung off the viewport edge. Let GSAP own centering (x: 0 + xPercent), keep only translateY in the mount style, raise the mobile card scale from 0.56 to 0.7 so cards read app-sized, and pad the headline so it clears the settings gear. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk
The tour previously panned the rendered SVG with a CSS transform: it reset the transform and measured the active node one frame later while the 900-1280ms transform transition was still in flight, so the pan was computed against mid-animation geometry, and the translate was clamped to ~20% of the viewport, so distant nodes never centered. Focus now animates the SVG viewBox in diagram coordinates (rAF tween, eased, clamped to the diagram bounds), so the highlighted node lands exactly in frame and the diagram cannot escape its card. Nodes are click-to-focus, and the tour bar gains prev/next controls and step dots. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk
Image search results were only visible after expanding the collapsed 'N sources reviewed' accordion, so pulled images effectively never surfaced. Render up to four image results as a visible strip above the accordion with title/domain overlays, sized responsively for phone and desktop widths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR addresses streaming reliability and voice-broker reachability on deployed hosts by preventing middleware/proxy buffering of the /api/chat SSE stream and loosening /api/voice-broker authorization to allow same-origin WebSocket handshakes. It also includes additional UI/UX updates in Study and Chat surfaces plus an added bug review report markdown file.
Changes:
- Prevent SSE compression/proxy buffering for
/api/chatand flush per event write. - Allow same-origin WebSocket upgrades for
/api/voice-broker(in addition to existing debug/local authorization). - Update client UI behavior (Mermaid focus tour, source image strip, Study intro layout) and add
BUG_REPORT.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/views/StudyView.tsx |
Adjusts GSAP card fan positioning/initial transform and responsive layout/scaling for the Study intro splash. |
src/components/ChatPanel.tsx |
Improves Mermaid diagram focus tour behavior/controls and adds an image strip to the final sources panel. |
server.ts |
Updates /api/chat SSE headers/flush behavior and adds a same-origin guard path for /api/voice-broker WebSocket upgrades. |
BUG_REPORT.md |
Adds a narrative bug review report describing fixed and open findings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }} | ||
| > | ||
| <div className="origin-top scale-[0.56] sm:scale-[0.64] md:scale-[0.73] lg:scale-[0.82] xl:scale-[0.89]"> | ||
| <div className="origin-top scale-[0.7] sm:scale-[0.64] md:scale-[0.73] lg:scale-[0.82] xl:scale-[0.89]"> |
| node.setAttribute("data-mermaid-tour-node", "true"); | ||
| node.setAttribute("data-mermaid-tour-index", String(index)); | ||
| node.setAttribute("tabindex", "0"); | ||
| node.addEventListener("click", () => setActiveNodeIndex(index)); |
| src={source.imageUrl || source.thumbnailUrl} | ||
| alt={source.title || "Web image result"} | ||
| loading="lazy" | ||
| className="h-32 w-full object-cover transition-transform duration-300 group-hover:scale-[1.03] sm:h-36" | ||
| /> |
| # Bug Review Report | ||
|
|
||
| Review of the Tutor-System codebase (server, client memory engines, views, | ||
| and the voice/bilateral architecture). Typecheck passes; 277/281 tests pass | ||
| (the 3 failures are environmental — `pymupdf` is not installed in the review | ||
| container — not code defects). |
| type MermaidViewBox = [number, number, number, number]; | ||
|
|
||
| const parseMermaidViewBox = (svg: SVGSVGElement): MermaidViewBox | null => { | ||
| const parts = (svg.getAttribute("viewBox") || "") | ||
| .trim() |
| const current = parseMermaidViewBox(svg) || original; | ||
| const svgRect = svg.getBoundingClientRect(); | ||
| const nodeRect = activeNode.getBoundingClientRect(); | ||
| if (svgRect.width < 1 || svgRect.height < 1) return; |
| let originHost = ""; | ||
| try { | ||
| originHost = new URL(origin).host.toLowerCase(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| if (!originHost) return false; | ||
| const forwardedHost = hostNameFromHeader( | ||
| request.headers["x-forwarded-host"] as string | string[] | undefined, | ||
| ); | ||
| const hostCandidates = [ | ||
| firstHeader(request.headers.host).trim().toLowerCase(), | ||
| forwardedHost, | ||
| hostNameFromHeader(request.headers.host), | ||
| ].filter(Boolean); | ||
| const originHostname = originHost.split(":")[0]; | ||
| return hostCandidates.some( | ||
| (candidate) => candidate === originHost || candidate === originHostname, | ||
| ); |
The global compression() middleware gzip-buffered the /api/chat
text/event-stream response, so tokens were held until the response
ended instead of streaming incrementally. Mark the SSE response
no-transform (which compression respects), disable proxy buffering,
and flush after each event write.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_015k1NuAvQnJTirVQHXsZeRk