From 243f6455437e6a7245d53de6a17611c7e05e7030 Mon Sep 17 00:00:00 2001 From: AhmadHammad21 Date: Tue, 12 May 2026 02:02:54 +0300 Subject: [PATCH] feat: follow-up question chips (closes #17) + dashboard auto-refresh (closes #20) Follow-up questions (#17): - Add follow_up_questions: list[str] to submit_investigation tool schema; generated by the agent in the same final call at zero extra LLM cost - Extract questions from tool_calls_log in chat.py and include in the done SSE event - Render up to 3 clickable chips above the input box after investigation completes; clicking a chip sends it immediately and clears the chips Dashboard auto-refresh (#20): - Extract fetch into a named function and call it on a 30s setInterval with cleanup on unmount; silent background refresh (no spinner on subsequent loads) Docs: - docs/ui.md: follow-up chips, dashboard auto-refresh, sidebar rename/delete menu, corrected font stack description - CLAUDE.md: sidebar now documents rename + portal dropdown; remove stray attribution from font descriptions; note follow-up questions in chat page features --- CLAUDE.md | 6 +++--- docs/ui.md | 14 +++++++++++--- frontend/src/pages/ChatPage.tsx | 27 ++++++++++++++++++++++----- frontend/src/pages/DashboardPage.tsx | 15 ++++++++++----- src/api/routers/chat.py | 10 +++++++++- src/tools/final_answer.py | 8 +++++++- 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1f8dddd..00a2ea7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,10 +103,10 @@ Everything below is built and working in the codebase: ### Frontend - React 18 + TypeScript + Vite + Tailwind CSS + `@tailwindcss/typography` -- Font: Inter Variable (Google Fonts) with Linear's full fallback stack +- Font: Inter Variable (Google Fonts) with a full system fallback stack - Routes: `/`, `/chat/:sessionId`, `/dashboard`, `/monitoring`, `/monitoring/:alertId`, `/history`, `/settings`, `/users`, `/login` - Chat page: SSE streaming, tool call inspector, cost/latency card, stop button, suggestion chips on empty state, `?prompt=` deeplink support -- Sidebar: paginated session list (15 at a time), three-dot menu with delete, real `` links for native right-click +- Sidebar: paginated session list (15 at a time), three-dot menu with rename + delete (portal-based, no overflow clipping), real `` links for native right-click - Settings: Environment, Agent Config, Integrations (UI stubs), AWS Configuration (editable, admin only), Preferences (dark mode) ### Auth & MCP @@ -168,7 +168,7 @@ Incomplete items from the README roadmap (do not mark complete here — update R | Linting | `ruff>=0.4.0` (line-length 100, Python 3.11 target, `asyncio_mode = "auto"`) | | Package manager | **uv** — always `uv run` and `uv add`, never bare `pip` | | Frontend | React 18, TypeScript, Vite, Tailwind CSS 3, `@tailwindcss/typography` | -| Font | Inter Variable (Google Fonts) — Linear-style fallback stack in `tailwind.config.js` | +| Font | Inter Variable (Google Fonts) — full system fallback stack in `tailwind.config.js` | --- diff --git a/docs/ui.md b/docs/ui.md index 09896f5..8310e85 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -20,6 +20,11 @@ Tool calls appear as collapsible cards as they complete, showing the tool name, arguments, and result. A contextual status label ("Digging through CloudTrail…", "Checking Lambda metrics…") appears while each tool runs. +**Follow-up suggestions** — after an investigation completes, up to 3 drill-down +question chips appear above the input box. Clicking one sends it immediately as the +next message. The chips are generated by the agent in the same final tool call at +no extra cost and disappear once you start a new message. + **Tool call inspector** — each tool call card is collapsed by default; click to expand and see the full arguments and result JSON. @@ -45,8 +50,9 @@ Lists all past sessions sorted by last activity. Each entry shows: **Actions:** - Click any session to load it in the chat view with full history restored - **New chat** button starts a fresh session with a new UUID -- **Delete** (trash icon) soft-deletes the session — it disappears from the list - immediately but data is preserved in the database +- **⋯ menu** on each session row: + - **Rename** — shows an inline input pre-filled with the current title; Enter or blur saves, Escape cancels + - **Delete** — soft-deletes the session; it disappears from the list immediately but data is preserved in the database --- @@ -64,6 +70,8 @@ Click any row to open the session in the chat page. ## Dashboard page +Stats are automatically refreshed every 30 seconds — no manual page reload needed. + See [`dashboard.md`](dashboard.md) for full details on each panel. --- @@ -132,7 +140,7 @@ The UI respects the system dark/light mode preference automatically. No toggle i ## Theme and design -- Font: system monospace stack +- Font: Inter Variable (Google Fonts) with a full system fallback stack - Framework: React 18 + Tailwind CSS - Icons: Lucide React - Build tool: Vite with HMR in development diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx index e252785..cdb4e8b 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/ChatPage.tsx @@ -38,11 +38,12 @@ interface Props { export default function ChatPage({ onSessionsChange, onNew }: Props) { const { sessionId } = useParams<{ sessionId: string }>(); const [searchParams, setSearchParams] = useSearchParams(); - const [messages, setMessages] = useState([]); - const [busy, setBusy] = useState(false); - const abortRef = useRef(null); - const bottomRef = useRef(null); - const autoPromptFired = useRef(false); + const [messages, setMessages] = useState([]); + const [busy, setBusy] = useState(false); + const [followUpQuestions, setFollowUpQuestions] = useState([]); + const abortRef = useRef(null); + const bottomRef = useRef(null); + const autoPromptFired = useRef(false); useEffect(() => { if (sessionId) localStorage.setItem('devops-session-id', sessionId); @@ -74,6 +75,7 @@ export default function ChatPage({ onSessionsChange, onNew }: Props) { const send = async (text: string) => { if (!sessionId || busy || !text.trim()) return; + setFollowUpQuestions([]); const agentId = crypto.randomUUID(); setMessages(prev => [ @@ -135,6 +137,8 @@ export default function ChatPage({ onSessionsChange, onNew }: Props) { })); } else if (payload.type === 'done') { patch(agentId, { streaming: false, usage: payload.usage as AgentMsg['usage'] }); + const fq = payload.follow_up_questions; + if (Array.isArray(fq) && fq.length > 0) setFollowUpQuestions(fq as string[]); } else if (payload.type === 'error') { const msg = payload.message as string; patch(agentId, { streaming: false, error: msg }); @@ -208,6 +212,19 @@ export default function ChatPage({ onSessionsChange, onNew }: Props) {
+ {followUpQuestions.length > 0 && !busy && ( +
+ {followUpQuestions.map((q, i) => ( + + ))} +
+ )} ); diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 5797887..5e23bc5 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -70,11 +70,16 @@ export default function DashboardPage() { const [error, setError] = useState(null); useEffect(() => { - fetch('/stats') - .then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }) - .then(setStats) - .catch(e => setError(e.message)) - .finally(() => setLoading(false)); + const load = () => + fetch('/stats') + .then(r => { if (!r.ok) throw new Error(r.statusText); return r.json(); }) + .then(setStats) + .catch(e => setError(e.message)) + .finally(() => setLoading(false)); + + load(); + const interval = setInterval(load, 30_000); + return () => clearInterval(interval); }, []); if (loading) { diff --git a/src/api/routers/chat.py b/src/api/routers/chat.py index 92ee226..769246b 100644 --- a/src/api/routers/chat.py +++ b/src/api/routers/chat.py @@ -251,7 +251,15 @@ def _flush_text_buf(): await save_turn(session_id, user_message, response_text, tool_calls_log, usage) await notify_slack(session_id, tool_calls_log) - yield f"data: {json.dumps({'type': 'done', 'usage': usage})}\n\n" + follow_up_questions: list[str] = [] + for tc in tool_calls_log: + if tc["tool"] == "submit_investigation": + fq = tc["args"].get("follow_up_questions") + if isinstance(fq, list): + follow_up_questions = [q for q in fq if isinstance(q, str)] + break + + yield f"data: {json.dumps({'type': 'done', 'usage': usage, 'follow_up_questions': follow_up_questions})}\n\n" @router.post("/chat") diff --git a/src/tools/final_answer.py b/src/tools/final_answer.py index 0000f3a..bdf8d37 100644 --- a/src/tools/final_answer.py +++ b/src/tools/final_answer.py @@ -22,10 +22,16 @@ def submit_investigation( confidence: VALID_CONFIDENCE, services_affected: list[str], recommended_follow_up: str, + follow_up_questions: list[str], ) -> str: """Submit the final structured investigation result. Call this exactly once when you have gathered sufficient evidence and reached a conclusion. Do not output a JSON block in - free text — call this tool instead.""" + free text — call this tool instead. + + follow_up_questions: 3 short drill-down questions the user might want to ask next, + e.g. ["What caused the spike at 14:32?", "Are retries configured on the Lambda?", + "Has this happened before this week?"]. + """ return "Investigation result recorded."