Add document-level styles so the downloaded .docx has: - Noto Serif font throughout (matching the web UI) - Sized headings: H1 20pt, H2 16pt, H3 14pt, H4 12pt - 11pt body text with 1.15× line spacing - Consistent color (#1f1b16) and spacing - Styled hyperlinks (blue, underlined) - List paragraph font inheritance Co-Authored-By: alex <alex@semipublic.co>
PMC Funder Discovery Tool
A React + Vite frontend (Tailwind, Noto Serif) that collects public media station details and streams an Anthropic Managed Agent response back to the browser through a Netlify Edge Function proxy.
Setup
npm install
Add your Anthropic credentials (in .env for local dev, in the Netlify UI
for production):
ANTHROPIC_API_KEY=sk-ant-...
# Optional - if you've provisioned a Managed Agent in the Anthropic console:
ANTHROPIC_AGENT_ID=agent_...
# Optional - override the model:
ANTHROPIC_MODEL=claude-sonnet-4-5
Run locally
npm run dev # netlify dev (proxies Vite + functions on :8888)
Open http://localhost:8888.
How streaming works
- The browser POSTs the form to
/api/agent-proxy. - A Netlify Edge Function (Deno runtime) creates an Anthropic Managed
Agent session, opens the upstream SSE event stream, and sends the
user message. It tails the stream and re-opens it (with an
events.listbackfill, deduped by event id) whenever it drops mid session, until the session reports a terminalsession.status_*event or a 20-minute wall-clock budget is hit. - Downstream to the browser the function emits newline-delimited JSON
(
application/x-ndjson) —text,status,heartbeat,segment_end,done,error— one object per line. - The React app reads
response.body.getReader(), splits on\n, and parses each line.textlines append to the brief;statuslines drive a separate “what the agent is doing now” banner. - A
Thinkingflag staystrueuntil the first chunk arrives, then flips to a streaming state with a pulsing cursor.
Why segmented streaming
Netlify Edge Functions empirically cap a single streaming response at ~60 s wall-clock (Netlify's own canonical SSE example cuts at the same mark, even though the docs imply "indefinite" streaming). The Anthropic Managed Agent session itself is fine for several minutes; the limit is per-HTTP-response. So the proxy segments the stream:
- Just before ~54 s the function writes one final NDJSON line —
{"type":"segment_end","sessionId":"sess_…","lastEventId":"evt_…","startedAt":…}— and closes the response. - The React side sees
segment_end, immediately POSTs to/api/agent-proxyagain with{sessionId, lastEventId, startedAt}in the body. - The function recognises this as a resume payload: it does not
create a new session or re-send the user message. It just reopens
the live SSE stream, backfills via
GET /v1/sessions/{id}/events?after_id=lastEventId(deduped byevent.id), and keeps tailing.
The 20-minute overall budget is enforced via Date.now() - startedAt
so it spans across all segments. The previous v2 Node Function (on AWS
Lambda) was killed at ~27 s, well before any reconnect could fire —
moving to Edge Functions extended that to ~60 s per response, and the
segment loop extends it the rest of the way.
Files
- src/App.jsx — form, NDJSON streaming reader, segment-end resume loop, UI states.
- netlify/edge-functions/agent-proxy.ts — Managed Agent proxy with reconnect, backfill, segmentation, and NDJSON wire protocol.