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!