Feature - AI Page Navigation via Chat
Feature - AI Page Navigation via Chat
Hey everyone! Today we are going to understand how the AI chat can navigate you to any page in the app!
User types something like "go to dashboard" or "open Andrei's profile, go to attendance tab" and the app instantly navigates there - no clicking sidebar needed!
This builds on the same SSE action pattern we used for theme change. If you understood that, this is very easy.
What we will cover:
- The Big Picture - One example end to end
- Bot Side - The navigate_to_page Tool
- Frontend Side - handleAction gets a "navigate" action
- The ?tab= Query Param Trick
- Multi-step: When bot needs to search first
- Floating chat vs Full page chat difference
- Complete Code Walkthrough
- Files Changed
The Big Picture - One Example
Let's trace ONE real example: user says "open Andrei's profile, go to attendance tab"
User types: "open Andrei's profile, go to attendance tab"
|
v
┌──────────────────────────────────────────────────────────┐
│ STEP 1: Frontend sends message to Bot via SSE stream │
└─────────────────────────┬────────────────────────────────┘
|
v
┌──────────────────────────────────────────────────────────┐
│ STEP 2: Bot AI reads the message │
│ AI thinks: "User wants to go to a client's profile" │
│ AI thinks: "But I need to find Andrei's ID first!" │
│ │
│ AI calls: get_clients_list(name="Andrei") │
│ API returns: [{id: "5", name: "Andrei Nowak", ...}] │
│ │
│ Now AI has the ID = 5 │
└─────────────────────────┬────────────────────────────────┘
|
v
┌──────────────────────────────────────────────────────────┐
│ STEP 3: Bot calls navigate_to_page tool │
│ │
│ navigate_to_page(path="/clients/5?tab=attendance") │
│ │
│ Tool returns: │
│ { │
│ "success": true, │
│ "actions": [ │
│ {"type": "navigate", "value": "/clients/5?tab=attendance"}│
│ ] │
│ } │
└─────────────────────────┬────────────────────────────────┘
|
v
┌──────────────────────────────────────────────────────────┐
│ STEP 4: Bot sends action via SSE stream │
│ │
│ data: {"action": {"type": "navigate", │
│ "value": "/clients/5?tab=attendance"}} │
│ data: {"token": "Done! Taking you to Andrei's..."} │
│ data: [DONE] │
└─────────────────────────┬────────────────────────────────┘
|
v
┌──────────────────────────────────────────────────────────┐
│ STEP 5: Frontend reads the SSE stream │
│ chat.service.ts sees "action" → calls onAction() │
└─────────────────────────┬────────────────────────────────┘
|
v
┌──────────────────────────────────────────────────────────┐
│ STEP 6: handleAction() runs │
│ │
│ action.type === "navigate" │
│ → navigate("/clients/5?tab=attendance") │
│ → React Router changes the page WITHOUT reload │
│ → Client detail page opens with attendance tab active │
│ │
│ USER SEES: Andrei's profile, attendance tab open! │
└──────────────────────────────────────────────────────────┘
Notice something? Two tool calls happened - first search, then navigate. The AI is smart enough to chain them!
Q: How is this different from theme change?
A: Same pattern, different action type!
Theme change action:
{"type": "change_theme", "value": "dark"}
→ handleAction switches dark mode
Navigate action:
{"type": "navigate", "value": "/clients/5?tab=attendance"}
→ handleAction calls navigate()
SAME pipeline:
Tool → actions list → SSE event → onAction callback → handleAction
The entire SSE pipeline (agent.py collecting actions, chat.service.ts parsing them) was already built for theme change. We just added a new action type!
Bot Side - The navigate_to_page Tool
File: strakly-bot/tools/navigate.py (NEW file)
This tool is dead simple. It does NOT make any API call. It just returns an action that says "navigate here".
from langchain_core.tools import tool
@tool
async def navigate_to_page(path: str) -> dict:
"""Navigate the user to a specific page in the app.
Use this when user says "go to", "open", "take me to".
Args:
path: The route path, e.g. "/dashboard", "/clients/5?tab=attendance"
"""
if not path or not path.startswith("/"):
return {"success": False, "error": "Path must start with /"}
return {"success": True, "actions": [{"type": "navigate", "value": path}]}
That's the entire file! Compare with theme.py:
theme.py returns:
{"type": "change_theme", "value": "dark"}
navigate.py returns:
{"type": "navigate", "value": "/clients/5?tab=attendance"}
Same pattern. Different type and value.
Q: But how does the bot know WHICH path to use?
A: The tool's docstring tells the AI all the routes! The docstring lists every page and every tab. The AI reads this and picks the right path.
Q: What happens after the tool returns?
A: Same flow as theme change. The action goes through agent.py:
agent.py already does this (built for theme change):
====================================================
1. _execute_tool_calls() runs the tool
2. Checks: does the result have "actions"?
→ YES! result["actions"] = [{"type": "navigate", "value": "/clients/5?tab=attendance"}]
3. Collects the action into all_actions list
4. At the end of streaming, sends them as SSE events:
yield f"data: {json.dumps({'action': action})}\n\n"
Which becomes:
data: {"action": {"type": "navigate", "value": "/clients/5?tab=attendance"}}
We didn't change agent.py at all! It already handles any action from any tool.
Frontend Side - handleAction
chat.service.ts already parses action events (built for theme). It calls onAction() which calls handleAction().
We just added one new if block:
File: useAIChatPage.ts (Full page AI chat)
// BEFORE (only theme):
const handleAction = useCallback((action) => {
if (action.type === "change_theme") {
window.dispatchEvent(new CustomEvent("ai-change-theme", { detail: action.value }))
}
if (action.type === "change_accent") {
dispatch(setAccentColor(action.value))
}
}, [dispatch])
// AFTER (theme + navigate):
const handleAction = useCallback((action) => {
if (action.type === "change_theme") {
window.dispatchEvent(new CustomEvent("ai-change-theme", { detail: action.value }))
}
if (action.type === "change_accent") {
dispatch(setAccentColor(action.value))
}
if (action.type === "navigate") { // ← NEW
navigate(action.value) // ← React Router navigates
}
}, [dispatch, navigate])
navigate() is from React Router's useNavigate() hook. It changes the URL and renders the new page — no browser reload!
Q: What about the floating chat (bottom-right bubble)?
A: Same thing, but with one extra step — close the chat panel first!
File: ai-chat.tsx (Floating chat widget)
const handleAction = useCallback((action) => {
if (action.type === "change_theme") {
window.dispatchEvent(new CustomEvent("ai-change-theme", { detail: action.value }))
}
if (action.type === "change_accent") {
dispatch(setAccentColor(action.value))
}
if (action.type === "navigate") {
setIsOpen(false) // ← Close the floating chat panel
setIsExpanded(false) // ← Reset expanded state
navigate(action.value) // ← Then navigate
}
}, [dispatch, navigate])
Why close it? Because the floating chat sits on top of the page. If we navigate but leave the chat open, the user might not even notice the page changed behind it!
Full page AI chat (/ai-chat): → Just navigate. User is leaving the AI chat page anyway. Floating chat (bottom-right bubble): → Close chat panel FIRST → then navigate. → User sees the new page immediately.
The ?tab= Query Param Trick
This is important. Many pages have tabs inside them. Example: Client profile has Information, Attendance, Trainer, Subscription tabs.
Q: How does the bot open a SPECIFIC tab?
A: By adding ?tab=attendance to the URL path!
Just the page: /clients/5 → Opens client 5, default tab With tab: /clients/5?tab=attendance → Opens client 5, attendance tab With tab: /clients/5?tab=subscription → Opens client 5, subscription tab
Q: But how does the page know to read ?tab= from the URL?
A: We changed each page from local state to URL state!
BEFORE (tab stored in React state - URL doesn't matter):
=========================================================
const [activeTab, setActiveTab] = useState("information")
// If someone navigates to /trainers/5?tab=clients
// The page IGNORES ?tab=clients and always shows "information"
AFTER (tab read from URL - ?tab= works):
==========================================
const [searchParams, setSearchParams] = useSearchParams()
const activeTab = searchParams.get("tab") || "information"
const setActiveTab = (tab: string) => {
setSearchParams({ tab }, { replace: true })
}
// Now if someone navigates to /trainers/5?tab=clients
// The page reads ?tab=clients and shows the clients tab!
This is the same pattern that Clients detail page already used. We just copied it to all other pages that have tabs.
| Page | Route | Available Tabs |
|---|---|---|
| Client detail | /clients/:id | information, attendance, trainer, body-metrics, subscription, diet, permissions |
| Trainer detail | /trainers/:id | information, clients |
| Manager detail | /managers/:id | information, permissions |
| Gym detail | /gym/:id | information, owner, subscription, branches |
| User detail | /users/:id | information, gym |
| Subscription detail | /membership-plan/member/:id | membership, payments |
| Salary | /salary | overview, salary |
| Memberships | /memberships | overview, clients |
| Subscription page | /membership-plan | overview, clients, plans, offers |
| Health & Fitness | /health-fitness | information, insights, history |
Multi-step: When the bot needs to SEARCH first
For simple pages like dashboard, the bot can navigate directly:
User: "go to dashboard" Bot: One tool call → navigate_to_page(path="/dashboard") Done!
But for profile pages, the bot needs to find the ID first:
User: "open Andrei Nowak's profile"
Bot thinking: "I need Andrei's ID. Let me search first."
TOOL CALL 1: get_clients_list(name="Andrei")
RESULT: [{id: "5", name: "Andrei Nowak", status: "active"}]
Bot thinking: "Got it! ID is 5. Now I can navigate."
TOOL CALL 2: navigate_to_page(path="/clients/5")
RESULT: {actions: [{type: "navigate", value: "/clients/5"}]}
Bot response: "Done! Taking you to Andrei Nowak's profile."
The AI is smart enough to chain multiple tool calls in one turn. It does the search, gets the result, then navigates.
This is handled by the agent loop in agent.py — it runs up to 6 iterations, so the AI can call multiple tools before responding.
How the system prompt guides the AI
The AI doesn't magically know your routes. We tell it in the system prompt:
## Navigation
When user asks to go to a page → use navigate_to_page.
Simple pages (no ID needed):
- "go to dashboard" → navigate_to_page(path="/dashboard")
- "open clients page" → navigate_to_page(path="/clients")
- "go to settings" → navigate_to_page(path="/settings")
Profile pages (need to find ID first):
- "open Andrei's profile" → first call get_clients_list to find ID,
then navigate_to_page(path="/clients/{id}")
Tab navigation (use ?tab= query param):
- "go to Andrei's attendance" → find ID,
then navigate_to_page(path="/clients/{id}?tab=attendance")
Available client tabs: information, attendance, trainer, body-metrics...
Available trainer tabs: information, clients
...
Important: ALWAYS find the ID first. Never guess IDs.
This is just text in the prompt. The AI reads it, understands the pattern, and applies it when the user asks.
Complete Flow - Code Trace
Let's trace exactly what happens in each file when user says "go to dashboard":
FILE 1: navigate.py
====================
navigate_to_page(path="/dashboard") is called by the AI
→ Returns: {"success": True, "actions": [{"type": "navigate", "value": "/dashboard"}]}
FILE 2: agent.py (already built, no changes)
=============================================
_execute_tool_calls() runs the tool
→ Sees result has "actions" key
→ Adds to all_actions list
Later, in process_chat_stream():
→ yield f'data: {json.dumps({"action": {"type": "navigate", "value": "/dashboard"}})}\n\n'
SSE event sent to frontend!
FILE 3: chat.service.ts (already built, no changes)
====================================================
Reads SSE stream line by line
→ Parses: {"action": {"type": "navigate", "value": "/dashboard"}}
→ Calls: onAction({"type": "navigate", "value": "/dashboard"})
FILE 4: useAIChatPage.ts (or ai-chat.tsx)
==========================================
handleAction receives: {"type": "navigate", "value": "/dashboard"}
→ action.type === "navigate" ✓
→ navigate("/dashboard")
→ React Router renders DashboardPage component
USER SEES: Dashboard page!
Files Changed - Summary
| File | What Changed | Why |
|---|---|---|
| strakly-bot/tools/navigate.py | NEW - navigate_to_page tool | Returns {"type": "navigate", "value": path} action |
| strakly-bot/tools/__init__.py | Registered the new tool | So AI knows it exists |
| strakly-bot/prompts/system_prompt.py | Added Navigation section | Tells AI when/how to use the tool, all routes + tabs |
| useAIChatPage.ts | Added "navigate" in handleAction | Full page chat: navigate(path) |
| ai-chat.tsx | Added "navigate" in handleAction | Floating chat: close chat + navigate(path) |
| Trainers details | useState → useSearchParams | So ?tab= works in URL |
| Managers details | useState → useSearchParams | So ?tab= works in URL |
| Gym details | useState → useSearchParams | So ?tab= works in URL |
| Subscription details | useState → useSearchParams | So ?tab= works in URL |
| Salary page | useState → useSearchParams | So ?tab= works in URL |
| Memberships page | useState → useSearchParams | So ?tab= works in URL |
| Subscription page | useState → useSearchParams | So ?tab= works in URL |
| Health & Fitness page | useState → useSearchParams | So ?tab= works in URL |
Key Takeaway
The entire action pipeline (agent.py → SSE → chat.service.ts → onAction) was already built for theme change. Adding navigation was just:
1. New tool: navigate.py (10 lines of actual code) 2. New handler: if (action.type === "navigate") navigate(action.value) 3. Tab support: useState → useSearchParams (3-line change per page) 4. System prompt: Tell the AI about routes and tabs That's it. The pattern is reusable for ANY future action!
Post a Comment