#region AgentChat.Ux [C:3] [TYPE ADR] [SEMANTICS ux,agent-chat,panel,native-stack] @defgroup Ux UX contract — Gradio submit() + LangChain v1 astream_events(). Structured JSON metadata (ChatMessage.metadata). No REST, no WebSocket, no follower. ## FSM ``` idle ──[submit("/chat")]──→ streaming ──[metadata.type: stream_token]──→ (tokens appear) streaming ──[metadata.type: tool_start]──→ (tool card + spinner) streaming ──[metadata.type: tool_end]──→ (tool done, checkmark) streaming ──[metadata.type: tool_error]──→ (tool error, cross) streaming ──[metadata.type: confirm_required]──→ awaiting_confirmation awaiting_confirmation ──[2nd submit(__resume__,"confirm")]──→ streaming (confirm_resolved) awaiting_confirmation ──[2nd submit(__resume__,"deny")]──→ idle (confirm_resolved: denied) awaiting_confirmation ──[timeout 5min]──→ idle (expired) streaming ──[final chunk]──→ idle streaming ──[submission.cancel()]──→ idle (partial) streaming ──[metadata.type: error]──→ error idle ──[load history]──→ loading_history ──[success]──→ idle loading_history ──[failure]──→ error idle ──[connect fail]──→ disconnected ──[retry 5×5s]──→ idle disconnected ──[retry exhausted]──→ disconnected_permanent ``` ## State Mappings | @UX_STATE | Visual | ARIA | User Can | |-----------|--------|------|----------| | idle (with msgs) | Input enabled, Send active, Stop hidden. Connection dot green. | — | Send, switch/delete convos, expand to /agent | | idle (new convo) | Welcome card + examples chips (Gradio `examples`). | `aria-label="Новый диалог"` | Click chip, type | | streaming | Tokens appear in real time. Tool cards inline. Send→Stop (destructive). Input locked. | `aria-live="polite" aria-busy="true"` | Stop, expand tool cards | | awaiting_confirmation | Streaming pauses. Warning card: `bg-warning-light border-warning-DEFAULT`. Prompt text + Confirm/Deny buttons. Timer 5:00→0:00. | `role="alertdialog"` | Confirm, Deny | | awaiting_confirmation (expired) | Card stays. Buttons disabled. Timer=0. "Повторить запрос?" | — | Retry | | error | Card: `bg-destructive-light border-destructive-ring`. Error + Retry. Partial text preserved. | `role="alert"` | Retry, new message | | disconnected | Dot red. Banner: "Агент недоступен. Переподключение через Xс..." | `role="alert"` | Read history, manual reconnect | | disconnected_permanent | Dot red. Manual reconnect button only. | `role="alert"` | Manual reconnect | | loading_history | Skeleton cards (3-5, `animate-pulse bg-surface-muted`). Input locked. | `aria-busy="true"` | Wait | ## Feedback | Trigger | Feedback | |---------|----------| | `metadata.type: stream_token` | Append token to assistant message | | `metadata.type: tool_start` | Tool card: name (mono) + spinner | | `metadata.type: tool_end` | Spinner→checkmark, result in tooltip | | `metadata.type: tool_error` | Spinner→cross, error in tooltip | | `metadata.type: confirm_required` | Confirmation card + timer, Confirm/Deny | | `metadata.type: confirm_resolved(result=confirmed)` | Card collapses, streaming resumes | | `metadata.type: confirm_resolved(result=denied)` | Card collapses, agent responds | | Stream complete | Stop→Send, input enabled | | Connection lost | Dot red. Banner. Retry countdown | | Connection restored | Dot green. Banner dismissed | ## Recovery | From | Action | To | |------|--------|-----| | error | Retry (re-sends last msg) | streaming | | disconnected | Auto-retry 5×5s or manual | idle | | awaiting_confirmation (expired) | "Повторить запрос" | streaming | | streaming (cancelled) | Type new msg | idle→streaming | | empty (no convos) | Click chip or type | streaming | ## Reactivity - `AgentChat.Model` atoms → component props → DOM - `@gradio/client` `submit()` → `for await (event)` → `model._onStreamData()` → `$state` update - No `$effect` for data loading — all via stream events - `assistantChatStore.conversationId` → model reads (read-only) ## UX Tests | @UX_TEST | Given | When | Then | |----------|-------|------|------| | Happy path | idle (new) | Type "привет", Send | streaming, tokens appear, idle | | Tool call | streaming | Agent calls tool | Card with spinner→checkmark | | Dangerous confirm | streaming | Agent wants deploy | "⏸️" card, confirm_id, timer | | Confirm accepted | awaiting_confirmation | 2nd submit(__resume__) | "▶️", streaming resumes | | Confirm denied | awaiting_confirmation | 2nd submit(deny) | "⏹️", idle | | Stream cancel | streaming | submission.cancel() | idle, partial text | | LLM error | streaming | LLM fails | "Ошибка: ..." + retry | | Gradio disconnect | idle | Gradio dies | Dot red, 5×5s retry | | Multi-tab gate | idle | Tab 2 opens same convo | Tab 2: send rejected | | Empty convo | idle (new) | Load | Examples chips visible | | History load error | idle | API fails | Error + retry | #endregion AgentChat.Ux