From 917f187acc72df8a84d0a3780dbdd23acbfd4260 Mon Sep 17 00:00:00 2001 From: HimanM <67066047+HimanM@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:53:23 +0530 Subject: [PATCH 1/2] Add order tracking flow and feature checklists --- backend/agent/tools.py | 89 +++++++++++++++ backend/routes/chat.py | 24 +++++ backend/tests/test_chat_route.py | 16 +++ backend/tests/test_tool_arg_sanitizer.py | 27 ++++- docs/feature-checklists/02-order-tracking.md | 32 ++++++ docs/feature-checklists/03-pay-link-card.md | 25 +++++ docs/feature-checklists/04-voice-mode.md | 24 +++++ .../05-quick-actions-and-welcome.md | 23 ++++ docs/feature-checklists/06-multilingual-ui.md | 24 +++++ .../07-reliability-and-debug-surface.md | 25 +++++ docs/feature-checklists/README.md | 17 +++ frontend/src/app/page.tsx | 1 + frontend/src/components/ChatMessage.tsx | 17 ++- frontend/src/components/TrackingCard.tsx | 102 ++++++++++++++++++ frontend/src/hooks/useChat.ts | 7 +- frontend/src/lib/api.ts | 23 ++++ 16 files changed, 469 insertions(+), 7 deletions(-) create mode 100644 backend/tests/test_chat_route.py create mode 100644 docs/feature-checklists/02-order-tracking.md create mode 100644 docs/feature-checklists/03-pay-link-card.md create mode 100644 docs/feature-checklists/04-voice-mode.md create mode 100644 docs/feature-checklists/05-quick-actions-and-welcome.md create mode 100644 docs/feature-checklists/06-multilingual-ui.md create mode 100644 docs/feature-checklists/07-reliability-and-debug-surface.md create mode 100644 docs/feature-checklists/README.md create mode 100644 frontend/src/components/TrackingCard.tsx diff --git a/backend/agent/tools.py b/backend/agent/tools.py index 8f97d7b..f16110b 100644 --- a/backend/agent/tools.py +++ b/backend/agent/tools.py @@ -284,6 +284,74 @@ def parse_search_products_markdown(text: str, limit: int = 6) -> list[dict]: return products +def parse_tracking_result(text: str) -> dict: + raw = text or "" + result = { + "order_number": "", + "status": "", + "recipient": "", + "estimated_delivery": "", + "location": "", + "items": [], + "events": [], + "raw": raw, + } + + if not raw: + return result + + try: + data = json.loads(raw) if isinstance(raw, str) else raw + except json.JSONDecodeError: + return result + + if not isinstance(data, dict): + return result + + result["order_number"] = str( + data.get("order_number") + or data.get("orderNumber") + or data.get("id") + or "" + ).strip() + result["status"] = str(data.get("status") or data.get("order_status") or "").strip() + result["recipient"] = str(data.get("recipient_name") or data.get("recipient") or "").strip() + result["estimated_delivery"] = str( + data.get("estimated_delivery") or data.get("delivery_date") or data.get("estimatedDelivery") or "" + ).strip() + result["location"] = str(data.get("current_location") or data.get("location") or "").strip() + + items = data.get("items") or data.get("products") or data.get("cart") or [] + if isinstance(items, list): + for item in items[:8]: + if isinstance(item, dict): + name = str(item.get("name") or item.get("title") or item.get("product_name") or "").strip() + quantity_raw = item.get("quantity") + quantity = None + if quantity_raw not in (None, ""): + try: + quantity = _coerce_int(quantity_raw) + except (TypeError, ValueError): + quantity = None + if name: + result["items"].append({"name": name, "quantity": quantity}) + elif isinstance(item, str) and item.strip(): + result["items"].append({"name": item.strip(), "quantity": None}) + + events = data.get("timeline") or data.get("history") or data.get("events") or [] + if isinstance(events, list): + for event in events[:8]: + if not isinstance(event, dict): + continue + label = str(event.get("status") or event.get("event") or event.get("description") or "").strip() + time = str(event.get("timestamp") or event.get("date") or event.get("time") or "").strip() + location = str(event.get("location") or "").strip() + if label: + result["events"].append({"label": label, "time": time, "location": location}) + + return result + + def build_tool_result_event(tool_name: str, result: Any) -> dict[str, Any]: event: dict[str, Any] = {"result": result} if tool_name == "search_products" and isinstance(result, str): @@ -296,6 +364,11 @@ def build_tool_result_event(tool_name: str, result: Any) -> dict[str, Any]: if product.get("name") or product.get("product_id") or product.get("product_url"): event["product"] = product event["raw"] = result + elif tool_name == "track_order" and isinstance(result, str): + tracking = parse_tracking_result(result) + if tracking.get("order_number") or tracking.get("status") or tracking.get("events"): + event["tracking"] = tracking + event["raw"] = result return event @@ -364,6 +437,10 @@ async def _check_delivery(city: str, delivery_date: str, product_id: str, **_) - }) +async def _track_order(order_number: str, **_) -> Any: + return await mcp_client.call_tool("kapruka_track_order", {"order_number": order_number}) + + async def _add_to_cart(session_id: str, product_id: str, quantity: int = 1, **_) -> str: raw = await mcp_client.call_tool("kapruka_get_product", {"product_id": product_id}) @@ -568,6 +645,17 @@ async def _checkout(session_id: str, **_) -> str: "required": ["city", "delivery_date", "product_id"], }, }, + { + "name": "track_order", + "description": "Track a Kapruka order by order number and return the latest status and timeline.", + "parameters": { + "type": "object", + "properties": { + "order_number": {"type": "string", "description": "Kapruka order number"}, + }, + "required": ["order_number"], + }, + }, { "name": "add_to_cart", "description": "Add a product to the user's shopping cart. Use this when user wants to add an item.", @@ -666,6 +754,7 @@ async def _checkout(session_id: str, **_) -> str: "list_categories": _list_categories, "list_delivery_cities": _list_delivery_cities, "check_delivery": _check_delivery, + "track_order": _track_order, "add_to_cart": _add_to_cart, "remove_from_cart": _remove_from_cart, "update_cart_quantity": _update_cart_quantity, diff --git a/backend/routes/chat.py b/backend/routes/chat.py index b0ced20..76bf27a 100644 --- a/backend/routes/chat.py +++ b/backend/routes/chat.py @@ -2,6 +2,7 @@ import json import logging +import re import uuid from fastapi import APIRouter @@ -21,6 +22,23 @@ class ChatRequest(BaseModel): history: list[dict] | None = None +def _needs_order_number_prompt(message_text: str) -> bool: + normalized = message_text.lower() + asks_to_track = any( + phrase in normalized + for phrase in ( + "track order", + "track my order", + "track package", + "order status", + "where is my order", + "delivery status", + ) + ) + has_order_number = bool(re.search(r"\b\d{5,}\b", message_text)) + return asks_to_track and not has_order_number + + def _provider_fallback_order(message_text: str) -> list[str]: preferred = resolve_provider_config(message_text).provider ordered: list[str] = [] @@ -58,6 +76,12 @@ async def chat_endpoint(req: ChatRequest): provider_config = resolve_provider_config(req.message) async def event_stream(): + if _needs_order_number_prompt(req.message): + yield f"data: {json.dumps({'type': 'session_id', 'session_id': session_id, 'provider': 'local', 'model': 'order-tracking-guard'})}\n\n" + yield f"data: {json.dumps({'type': 'text', 'text': 'Please share your Kapruka order number and I will track the latest status for you.'})}\n\n" + yield 'data: [DONE]\n\n' + return + candidates = _provider_fallback_order(req.message) if not candidates: yield ( diff --git a/backend/tests/test_chat_route.py b/backend/tests/test_chat_route.py new file mode 100644 index 0000000..e573ea9 --- /dev/null +++ b/backend/tests/test_chat_route.py @@ -0,0 +1,16 @@ +import unittest + +from routes.chat import _needs_order_number_prompt + + +class ChatRouteTest(unittest.TestCase): + def test_prompts_for_order_number_when_tracking_without_one(self): + self.assertTrue(_needs_order_number_prompt("I want to track my order")) + self.assertTrue(_needs_order_number_prompt("Can you check my order status?")) + + def test_skips_prompt_when_order_number_is_present(self): + self.assertFalse(_needs_order_number_prompt("Track order 12345678")) + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/tests/test_tool_arg_sanitizer.py b/backend/tests/test_tool_arg_sanitizer.py index 223d9ce..03bd56b 100644 --- a/backend/tests/test_tool_arg_sanitizer.py +++ b/backend/tests/test_tool_arg_sanitizer.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import AsyncMock, patch -from agent.tools import _search_products, build_tool_result_event, coerce_tool_args, format_tool_result_for_model, parse_failed_tool_generation, parse_product_markdown, parse_search_products_markdown +from agent.tools import _search_products, build_tool_result_event, coerce_tool_args, format_tool_result_for_model, parse_failed_tool_generation, parse_product_markdown, parse_search_products_markdown, parse_tracking_result class ToolArgSanitizerTest(unittest.TestCase): @@ -115,6 +115,31 @@ def test_parses_bracketed_failed_generation(self): "sort": "relevance", })) + def test_parses_tracking_result_and_emits_tracking_event(self): + tracking_json = """{ + "order_number": "12345678", + "status": "Out for delivery", + "recipient_name": "Alex", + "estimated_delivery": "2026-06-30", + "current_location": "Colombo", + "items": [{"name": "Chocolate Box", "quantity": 1}], + "timeline": [ + {"status": "Out for delivery", "timestamp": "2026-06-29 10:30", "location": "Colombo"}, + {"status": "Processed", "timestamp": "2026-06-28 18:00"} + ] + }""" + + parsed = parse_tracking_result(tracking_json) + self.assertEqual(parsed["order_number"], "12345678") + self.assertEqual(parsed["status"], "Out for delivery") + self.assertEqual(parsed["recipient"], "Alex") + self.assertEqual(parsed["items"][0]["name"], "Chocolate Box") + self.assertEqual(parsed["events"][0]["label"], "Out for delivery") + + event = build_tool_result_event("track_order", tracking_json) + self.assertIn("tracking", event) + self.assertEqual(event["tracking"]["location"], "Colombo") + if __name__ == "__main__": unittest.main() diff --git a/docs/feature-checklists/02-order-tracking.md b/docs/feature-checklists/02-order-tracking.md new file mode 100644 index 0000000..c1402f0 --- /dev/null +++ b/docs/feature-checklists/02-order-tracking.md @@ -0,0 +1,32 @@ +# Feature 02: Order Tracking + +## Goal + +Add order-tracking capability inspired by the candidate repos, including a friendly prompt flow, MCP-backed tracking tool support, and a dedicated tracking status card in chat. + +## Checklist + +- [x] Review candidate tracking implementations and card layouts +- [x] Pick the smallest tracking flow that fits the current architecture +- [x] Add backend `track_order` tool wrapper around `kapruka_track_order` +- [x] Add structured tracking payload support to tool-result events +- [x] Add tracking card component in the frontend +- [x] Add clear entry points from quick actions or common prompts +- [x] Handle the no-order-number case with a helpful assistant response +- [x] Verify tracking render logic with a representative response shape +- [x] Run backend tests relevant to provider/tool behavior +- [x] Run `npm run lint` +- [x] Run `npm run build` +- [x] Verify the flow in a live browser session +- [ ] Commit, push branch, and open draft PR + +## Notes + +- Candidate reference: `tmp_compare/kavi-kapruka/components/checkout/TrackOrderCard.tsx` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/components/TrackingCard.js` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/agents/tracking-agent.js` +- Browser verification used `http://localhost:3000` through a Playwright fallback because the in-app Browser tool was not callable in this thread. +- Live browser checks covered: + - Missing-order-number flow: "I want to track my order" returns a direct request for the Kapruka order number. + - Tool path flow: "Track order 12345678" executes the tracking path and returns a graceful not-found response. +- Structured tracking payload normalization is covered by backend unit tests with a representative JSON response shape. diff --git a/docs/feature-checklists/03-pay-link-card.md b/docs/feature-checklists/03-pay-link-card.md new file mode 100644 index 0000000..552be11 --- /dev/null +++ b/docs/feature-checklists/03-pay-link-card.md @@ -0,0 +1,25 @@ +# Feature 03: Pay Link Card + +## Goal + +Upgrade the checkout completion experience so successful order placement produces a visible payment-link or order-summary card instead of only plain chat text. + +## Checklist + +- [ ] Review candidate pay-link and order-summary implementations +- [ ] Confirm current `checkout` tool result shape from the MCP +- [ ] Add structured order payload support to backend tool-result events +- [ ] Add a dedicated pay-link or order-summary card component +- [ ] Surface price lock / payment guidance clearly in the UI +- [ ] Preserve the existing cart and checkout drawer flow +- [ ] Verify behavior for success and incomplete-checkout cases +- [ ] Run backend tests relevant to order payload parsing +- [ ] Run `npm run lint` +- [ ] Run `npm run build` +- [ ] Verify the flow in a live browser session +- [ ] Commit, push branch, and open draft PR + +## Notes + +- Candidate reference: `tmp_compare/kavi-kapruka/components/checkout/PayLinkCard.tsx` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/components/OrderSummary.js` diff --git a/docs/feature-checklists/04-voice-mode.md b/docs/feature-checklists/04-voice-mode.md new file mode 100644 index 0000000..9a323eb --- /dev/null +++ b/docs/feature-checklists/04-voice-mode.md @@ -0,0 +1,24 @@ +# Feature 04: Voice Mode + +## Goal + +Add browser-native voice input and optional spoken assistant replies using built-in Web Speech APIs, following the candidate approach without new paid services. + +## Checklist + +- [ ] Review candidate voice input/output implementation +- [ ] Decide the smallest supported browser behavior and fallback text +- [ ] Add voice input button and listening state +- [ ] Add optional voice output toggle for assistant replies +- [ ] Keep the chat input usable when voice APIs are unavailable +- [ ] Ensure the UI remains clean on desktop and mobile +- [ ] Add at least one lightweight check for prompt/input behavior +- [ ] Run `npm run lint` +- [ ] Run `npm run build` +- [ ] Verify the flow in a live browser session +- [ ] Commit, push branch, and open draft PR + +## Notes + +- Candidate reference: `tmp_compare/kavi-kapruka/lib/speech.ts` +- Candidate reference: `tmp_compare/kavi-kapruka/components/chat/VoiceButton.tsx` diff --git a/docs/feature-checklists/05-quick-actions-and-welcome.md b/docs/feature-checklists/05-quick-actions-and-welcome.md new file mode 100644 index 0000000..c0680af --- /dev/null +++ b/docs/feature-checklists/05-quick-actions-and-welcome.md @@ -0,0 +1,23 @@ +# Feature 05: Quick Actions And Welcome + +## Goal + +Improve discovery and usability with better quick actions, a stronger welcome state, and candidate-inspired shortcuts into gifts, categories, checkout, and tracking. + +## Checklist + +- [ ] Review candidate welcome screens and quick-action patterns +- [ ] Decide which shortcuts belong on the empty state versus active chat state +- [ ] Add better quick-action chips for common shopping intents +- [ ] Improve the welcome state so it advertises core capabilities clearly +- [ ] Keep the new layout consistent with the current visual direction +- [ ] Verify prompts generated by quick actions still lead to cards or useful assistant text +- [ ] Run `npm run lint` +- [ ] Run `npm run build` +- [ ] Verify the flow in a live browser session +- [ ] Commit, push branch, and open draft PR + +## Notes + +- Candidate reference: `tmp_compare/kavi-kapruka/components/chat/QuickActions.tsx` +- Candidate reference: `tmp_compare/kavi-kapruka/components/chat/WelcomeScreen.tsx` diff --git a/docs/feature-checklists/06-multilingual-ui.md b/docs/feature-checklists/06-multilingual-ui.md new file mode 100644 index 0000000..4d079b2 --- /dev/null +++ b/docs/feature-checklists/06-multilingual-ui.md @@ -0,0 +1,24 @@ +# Feature 06: Multilingual UI + +## Goal + +Make the product feel more Sri Lanka-aware by adding localized UI copy and lightweight language-aware helper responses on top of the current backend language handling. + +## Checklist + +- [ ] Review candidate multilingual dictionaries and language-detection behavior +- [ ] Decide the minimum viable language surface for this app +- [ ] Add localized labels and helper copy for the main UI flows +- [ ] Keep language changes lightweight and avoid duplicating business logic +- [ ] Verify English remains the default fallback +- [ ] Add at least one focused test for language selection or copy mapping +- [ ] Run `npm run lint` +- [ ] Run `npm run build` +- [ ] Verify the flow in a live browser session +- [ ] Commit, push branch, and open draft PR + +## Notes + +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/index.js` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/en.js` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/si.js` diff --git a/docs/feature-checklists/07-reliability-and-debug-surface.md b/docs/feature-checklists/07-reliability-and-debug-surface.md new file mode 100644 index 0000000..ae105c2 --- /dev/null +++ b/docs/feature-checklists/07-reliability-and-debug-surface.md @@ -0,0 +1,25 @@ +# Feature 07: Reliability And Debug Surface + +## Goal + +Finish the candidate-inspired hardening work: predictable small-model defaults, provider fallback, and a minimal MCP debug surface for easier iteration and demos. + +## Checklist + +- [ ] Review candidate MCP route and rate-limiting/debug patterns +- [ ] Confirm which reliability work is already shipped versus still missing +- [ ] Add or refine a minimal MCP debug route if still valuable +- [ ] Add lightweight request hardening only where it helps the demo app +- [ ] Keep the solution small and avoid unnecessary platform complexity +- [ ] Verify provider fallback behavior against live or simulated failures +- [ ] Run backend tests relevant to provider selection and fallback +- [ ] Run `npm run lint` +- [ ] Run `npm run build` +- [ ] Verify the flow in a live browser session +- [ ] Commit, push branch, and open draft PR + +## Notes + +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/app/api/mcp/route.js` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/rate-limiter.js` +- Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/mcp/client.js` diff --git a/docs/feature-checklists/README.md b/docs/feature-checklists/README.md new file mode 100644 index 0000000..17ba3d4 --- /dev/null +++ b/docs/feature-checklists/README.md @@ -0,0 +1,17 @@ +# Feature Checklists + +This folder tracks the staged feature work for the Kapruka challenge app. + +## Planned iterations + +- `01-gift-advisor.md` - guided gift selection flow +- `02-order-tracking.md` - order tracking tool, prompt flow, and tracking card UI +- `03-pay-link-card.md` - post-checkout order summary and payment-link card +- `04-voice-mode.md` - browser-native voice input and spoken assistant replies +- `05-quick-actions-and-welcome.md` - richer quick actions, onboarding, and category-first prompts +- `06-multilingual-ui.md` - localized UI copy and language-aware helper flows +- `07-reliability-and-debug-surface.md` - provider fallback, MCP debug route, and light request hardening + +## Working rule + +Each file is a per-feature checklist. We update the relevant file as a feature moves from planned to shipped. diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 513e9f7..44a55b6 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -56,6 +56,7 @@ const chatActions = [ { label: "Gift advisor", kind: "advisor" as const }, { label: "Birthday gifts", prompt: "Find 5 birthday gift ideas on Kapruka under Rs. 5,000" }, { label: "Anniversary gifts", prompt: "Show anniversary gift ideas on Kapruka for my partner" }, + { label: "Track order", prompt: "I want to track my order. Please ask me for the Kapruka order number if needed." }, { label: "Show categories", prompt: "Show me Kapruka product categories" }, ]; diff --git a/frontend/src/components/ChatMessage.tsx b/frontend/src/components/ChatMessage.tsx index 9f452d9..63488eb 100644 --- a/frontend/src/components/ChatMessage.tsx +++ b/frontend/src/components/ChatMessage.tsx @@ -1,8 +1,9 @@ "use client"; import ProductCard from "@/components/ProductCard"; +import TrackingCard from "@/components/TrackingCard"; import { Message } from "@/hooks/useChat"; -import type { ProductSummary } from "@/lib/api"; +import type { ProductSummary, TrackingSummary } from "@/lib/api"; function ToolBadge({ tool, args }: { tool: string; args: Record }) { const labels: Record = { @@ -15,6 +16,7 @@ function ToolBadge({ tool, args }: { tool: string; args: Record checkout: "Placing order", list_categories: "Browsing categories", check_delivery: "Checking delivery", + track_order: `Tracking #${args.order_number || "order"}`, }; return ( @@ -56,6 +58,11 @@ function ToolProducts({ ); } +function ToolTracking({ tracking }: { tracking: TrackingSummary | null }) { + if (!tracking) return null; + return ; +} + function normalizeLine(line: string, hasProducts: boolean): string | null { const trimmed = line.trim(); if (!trimmed) return null; @@ -66,7 +73,7 @@ function normalizeLine(line: string, hasProducts: boolean): string | null { /^!\[[^\]]*\]\([^\)]+\)$/.test(trimmed) || /^\[[^\]]+\]\([^\)]+\)$/.test(trimmed) || /^\d+\./.test(trimmed) || - /^[-•*]\s+/.test(trimmed) || + /^[-*]\s+/.test(trimmed) || /(?:\b(?:price|buy here|view product)\b|LKR|Rs\.?)/i.test(trimmed) ) ) { @@ -84,7 +91,7 @@ function normalizeLine(line: string, hasProducts: boolean): string | null { if (orderMatch) { text = `${orderMatch[1]} ${text.replace(/^\d+\.\s+/, "")}`; } else { - text = text.replace(/^[-•*]\s+/, ""); + text = text.replace(/^[-*]\s+/, ""); } text = text.replace(/\s+/g, " ").trim(); @@ -146,6 +153,8 @@ export default function ChatMessage({ ? uniqueProducts.filter((product) => isProductReferenced(product, message.content)).slice(0, 6) : []; const hasProducts = !isUser && productResults.length > 0; + const trackingResult = + message.toolResults?.map((entry) => entry.tracking).find((entry): entry is TrackingSummary => Boolean(entry)) || null; return (
@@ -174,6 +183,8 @@ export default function ChatMessage({ )} + {!hasProducts && trackingResult ? : null} + {!message.content && message.toolCalls && message.toolCalls.length > 0 && !productResults.length ? (
diff --git a/frontend/src/components/TrackingCard.tsx b/frontend/src/components/TrackingCard.tsx new file mode 100644 index 0000000..165ec0a --- /dev/null +++ b/frontend/src/components/TrackingCard.tsx @@ -0,0 +1,102 @@ +"use client"; + +import type { TrackingSummary } from "@/lib/api"; + +function StatusTone(status: string) { + const normalized = status.toLowerCase(); + if (normalized.includes("deliver")) return "bg-emerald-50 text-emerald-800 border-emerald-200"; + if (normalized.includes("ship") || normalized.includes("transit")) return "bg-sky-50 text-sky-800 border-sky-200"; + if (normalized.includes("process") || normalized.includes("pending")) return "bg-amber-50 text-amber-900 border-amber-200"; + if (normalized.includes("cancel")) return "bg-red-50 text-red-800 border-red-200"; + return "bg-surface-2 text-ink-soft border-border"; +} + +export default function TrackingCard({ tracking }: { tracking: TrackingSummary }) { + const hasEvents = tracking.events.length > 0; + const hasItems = tracking.items.length > 0; + + if (!tracking.order_number && !tracking.status && !hasEvents) return null; + + return ( +
+
+
+

Order tracking

+

+ {tracking.order_number ? `#${tracking.order_number}` : "Latest order update"} +

+ {tracking.recipient ? ( +

Recipient: {tracking.recipient}

+ ) : null} +
+ {tracking.status ? ( + + {tracking.status} + + ) : null} +
+ +
+
+ {(tracking.estimated_delivery || tracking.location) ? ( +
+ {tracking.estimated_delivery ? ( +
+

Estimated delivery

+

{tracking.estimated_delivery}

+
+ ) : null} + {tracking.location ? ( +
+

Current location

+

{tracking.location}

+
+ ) : null} +
+ ) : null} + + {hasEvents ? ( +
+

Timeline

+
    + {tracking.events.map((event, index) => ( +
  1. + +

    {event.label}

    + {(event.time || event.location) ? ( +

    + {[event.time, event.location].filter(Boolean).join(" - ")} +

    + ) : null} +
  2. + ))} +
+
+ ) : ( +
+ No event timeline was returned for this order yet. +
+ )} +
+ +
+
+

Items

+ {hasItems ? ( +
    + {tracking.items.map((item, index) => ( +
  • + {item.name} + {item.quantity ? x{item.quantity} : null} +
  • + ))} +
+ ) : ( +

Item details were not included in the tracking response.

+ )} +
+
+
+
+ ); +} diff --git a/frontend/src/hooks/useChat.ts b/frontend/src/hooks/useChat.ts index bf533b4..31fff60 100644 --- a/frontend/src/hooks/useChat.ts +++ b/frontend/src/hooks/useChat.ts @@ -1,14 +1,14 @@ "use client"; import { useState, useCallback, useRef } from "react"; -import { streamChat, type ProductSummary } from "@/lib/api"; +import { streamChat, type ProductSummary, type TrackingSummary } from "@/lib/api"; export interface Message { id: string; role: "user" | "assistant" | "system"; content: string; toolCalls?: { tool: string; args: Record }[]; - toolResults?: { tool: string; result?: string; raw?: string; product?: ProductSummary; products?: ProductSummary[] }[]; + toolResults?: { tool: string; result?: string; raw?: string; product?: ProductSummary; products?: ProductSummary[]; tracking?: TrackingSummary }[]; timestamp: number; } @@ -34,7 +34,7 @@ export function useChat(sessionId: string) { let assistantContent = ""; const toolCalls: { tool: string; args: Record }[] = []; - const toolResults: { tool: string; result?: string; raw?: string; product?: ProductSummary; products?: ProductSummary[] }[] = []; + const toolResults: { tool: string; result?: string; raw?: string; product?: ProductSummary; products?: ProductSummary[]; tracking?: TrackingSummary }[] = []; const assistantId = `assistant-${++idCounter.current}`; try { @@ -69,6 +69,7 @@ export function useChat(sessionId: string) { raw: event.raw, product: event.product, products: event.products, + tracking: event.tracking, }); setMessages((prev) => { const existing = prev.find((m) => m.id === assistantId); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 18d8b7c..d37944d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -40,6 +40,28 @@ export interface ProductSummary { raw?: string; } +export interface TrackingEvent { + label: string; + time?: string; + location?: string; +} + +export interface TrackingItem { + name: string; + quantity?: number | null; +} + +export interface TrackingSummary { + order_number: string; + status: string; + recipient: string; + estimated_delivery: string; + location: string; + items: TrackingItem[]; + events: TrackingEvent[]; + raw?: string; +} + export interface CheckoutInfoPayload { recipient: { name: string; @@ -74,6 +96,7 @@ export interface ChatEvent { cart?: CartState; product?: ProductSummary; products?: ProductSummary[]; + tracking?: TrackingSummary; } export async function* streamChat( From 7c3f53437abd535aad4a227a1ea13dfa6763ecee Mon Sep 17 00:00:00 2001 From: HimanM <67066047+HimanM@users.noreply.github.com> Date: Sun, 21 Jun 2026 19:00:56 +0530 Subject: [PATCH 2/2] Polish tracking entry points on welcome screen --- docs/feature-checklists/02-order-tracking.md | 3 +- frontend/src/app/page.tsx | 35 ++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/feature-checklists/02-order-tracking.md b/docs/feature-checklists/02-order-tracking.md index c1402f0..402c350 100644 --- a/docs/feature-checklists/02-order-tracking.md +++ b/docs/feature-checklists/02-order-tracking.md @@ -18,7 +18,7 @@ Add order-tracking capability inspired by the candidate repos, including a frien - [x] Run `npm run lint` - [x] Run `npm run build` - [x] Verify the flow in a live browser session -- [ ] Commit, push branch, and open draft PR +- [x] Commit, push branch, and open draft PR ## Notes @@ -29,4 +29,5 @@ Add order-tracking capability inspired by the candidate repos, including a frien - Live browser checks covered: - Missing-order-number flow: "I want to track my order" returns a direct request for the Kapruka order number. - Tool path flow: "Track order 12345678" executes the tracking path and returns a graceful not-found response. +- Follow-up UI polish added empty-state hero buttons for tracking and category browsing. - Structured tracking payload normalization is covered by backend unit tests with a representative JSON response shape. diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 44a55b6..19682af 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -52,12 +52,15 @@ const suggestions = [ "Party supplies for 10", ]; +const TRACK_ORDER_PROMPT = "I want to track my order. Please ask me for the Kapruka order number if needed."; +const CATEGORY_PROMPT = "Show me Kapruka product categories"; + const chatActions = [ { label: "Gift advisor", kind: "advisor" as const }, { label: "Birthday gifts", prompt: "Find 5 birthday gift ideas on Kapruka under Rs. 5,000" }, { label: "Anniversary gifts", prompt: "Show anniversary gift ideas on Kapruka for my partner" }, - { label: "Track order", prompt: "I want to track my order. Please ask me for the Kapruka order number if needed." }, - { label: "Show categories", prompt: "Show me Kapruka product categories" }, + { label: "Track order", prompt: TRACK_ORDER_PROMPT }, + { label: "Show categories", prompt: CATEGORY_PROMPT }, ]; function CartIcon({ className = "" }: { className?: string }) { @@ -412,7 +415,7 @@ export default function Home() {