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
+17 -1
View File
@@ -103,7 +103,7 @@ button:disabled {
inset: auto 0 0 0;
z-index: 20;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(5, 1fr);
height: calc(var(--nav-h) + var(--safe-bottom));
padding-bottom: var(--safe-bottom);
background: var(--c-surface);
@@ -2937,3 +2937,19 @@ button.row-card:hover:not(:disabled) {
animation: none;
}
}
/* Live agent reply: steps + the answer as it is written (SSE) */
.bubble--live .live { display: grid; gap: 4px; }
.live__step { margin: 0; font-size: 13px; color: var(--muted, #6b7280); }
.live__step.is-active::after { content: ""; display: inline-block; width: 1em; animation: live-ellipsis 1.2s steps(4) infinite; overflow: hidden; vertical-align: bottom; }
.live__text { margin: 2px 0 0; white-space: pre-wrap; }
.live__cursor { display: inline-block; width: 7px; height: 1em; margin-left: 2px; background: currentColor; vertical-align: text-bottom; animation: live-blink 1s steps(2) infinite; }
.live__dots { display: inline-flex; gap: 4px; margin: 4px 0 0; }
.live__dots span { width: 6px; height: 6px; border-radius: 50%; background: var(--muted, #9ca3af); animation: live-dot 1.2s infinite ease-in-out; }
.live__dots span:nth-child(2) { animation-delay: .15s; }
.live__dots span:nth-child(3) { animation-delay: .3s; }
@keyframes live-blink { 50% { opacity: 0; } }
@keyframes live-dot { 0%, 80%, 100% { opacity: .3; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
@keyframes live-ellipsis { from { width: 0; } to { width: 1em; } }
.tabbar__item span { font-size: 11px; }