89 lines
5.4 KiB
Markdown
89 lines
5.4 KiB
Markdown
#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
|