feat: live agent answers (SSE streaming) + «Чат» tab on phones
CI / ruff · pytest (pull_request) Successful in 2m43s

- POST /dialog/{token}/events/stream: Server-Sent Events with the agent's steps («Смотрю вашу карту ЕМИАС…»,
  «Ищу в клинических рекомендациях…»), the answer as the model writes it (OpenRouter stream=true), then the
  final replies (same as /events, incl. the forbidden-output filter and sources). X-Accel-Buffering: no.
- Web: free-text messages use the stream — a live bubble with steps, typing dots and a cursor; falls back to
  the plain request if the stream breaks.
- Phones: the bottom tab bar gets «Чат» (it had no way back to the chat).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 12:52:19 +03:00
co-authored by Claude Opus 5.5
parent d662a69c7e
commit d1f92407f9
11 changed files with 381 additions and 6 deletions
+27 -1
View File
@@ -18,7 +18,7 @@ export function Messages() {
useEffect(() => {
dialog.markRead();
endRef.current?.scrollIntoView({ block: "end" });
}, [dialog.entries.length]);
}, [dialog.entries.length, dialog.live?.text, dialog.live?.status.length]);
const send = async (e?: FormEvent) => {
e?.preventDefault();
@@ -56,6 +56,32 @@ export function Messages() {
</li>
),
)}
{dialog.live && (
<li className="bubble bubble--bot bubble--live" aria-busy="true">
<span className="bubble__avatar">
<Icon name="smile" size={20} />
</span>
<div className="live">
{dialog.live.status.map((st, i) => (
<p key={i} className={`live__step${i === dialog.live!.status.length - 1 && !dialog.live!.text ? " is-active" : ""}`}>
{st}
</p>
))}
{dialog.live.text ? (
<p className="live__text">
{dialog.live.text}
<span className="live__cursor" aria-hidden="true" />
</p>
) : (
<p className="live__dots" aria-label={t.typing}>
<span />
<span />
<span />
</p>
)}
</div>
</li>
)}
</ol>
{quick.length > 0 && (
<div className="quickreplies">