From 2e3200272d40620d2c0b6a502e71f647ecf55ecb Mon Sep 17 00:00:00 2001 From: Peter Wielander Date: Mon, 12 Jan 2026 16:24:49 +0100 Subject: [PATCH 1/5] [flight-booking-app] Add loading and error UI, add sleep and approval tools Signed-off-by: Peter Wielander --- .../app/api/hooks/approval/route.ts | 12 +++ flight-booking-app/app/page.tsx | 85 ++++++++++++++--- .../components/booking-approval.tsx | 93 +++++++++++++++++++ flight-booking-app/instrumentation.ts | 2 +- .../workflows/chat/hooks/approval.ts | 9 ++ .../workflows/chat/steps/tools.ts | 43 ++++++++- 6 files changed, 231 insertions(+), 13 deletions(-) create mode 100644 flight-booking-app/app/api/hooks/approval/route.ts create mode 100644 flight-booking-app/components/booking-approval.tsx create mode 100644 flight-booking-app/workflows/chat/hooks/approval.ts diff --git a/flight-booking-app/app/api/hooks/approval/route.ts b/flight-booking-app/app/api/hooks/approval/route.ts new file mode 100644 index 0000000..51e4e89 --- /dev/null +++ b/flight-booking-app/app/api/hooks/approval/route.ts @@ -0,0 +1,12 @@ +import { bookingApprovalHook } from '@/workflows/chat/hooks/approval'; + +export async function POST(request: Request) { + const { toolCallId, approved, comment } = await request.json(); + // Schema validation happens automatically + // Can throw a zod schema validation error, or a + await bookingApprovalHook.resume(toolCallId, { + approved, + comment, + }); + return Response.json({ success: true }); +} diff --git a/flight-booking-app/app/page.tsx b/flight-booking-app/app/page.tsx index d32cc73..c24dbae 100644 --- a/flight-booking-app/app/page.tsx +++ b/flight-booking-app/app/page.tsx @@ -21,6 +21,8 @@ import { } from "@/components/ai-elements/tool"; import ChatInput from "@/components/chat-input"; import type { MyUIMessage } from "@/schemas/chat"; +import { BookingApproval } from "@/components/booking-approval"; +import { useMultiTurnChat } from "@/components/use-multi-turn-chat"; const SUGGESTIONS = [ "Find me flights from San Francisco to Los Angeles", @@ -29,6 +31,8 @@ const SUGGESTIONS = [ "What's the baggage allowance for United Airlines economy?", "Book a flight from New York to Miami", ]; +const FULL_EXAMPLE_PROMPT = + "Book me the cheapest flight from San Francisco to Los Angeles for July 27 2025. My name is Pranay Prakash. I like window seats. Don't ask me for approval."; export default function ChatPage() { const textareaRef = useRef(null); @@ -121,6 +125,16 @@ export default function ChatPage() {

Book a flight using workflows

+ {/* Error display */} + {error && ( +
+
+ Error: + {error.message} +
+
+ )} + {messages.length === 0 && (
@@ -154,15 +168,13 @@ export default function ChatPage() { type="button" onClick={() => { sendMessage({ - text: "Book me the cheapest flight from San Francisco to Los Angeles for July 27 2025. My name is Pranay Prakash. I like window seats. Don't ask me for confirmation.", + text: FULL_EXAMPLE_PROMPT, metadata: { createdAt: Date.now() }, }); }} className="text-sm border px-3 py-2 rounded-md bg-muted/50 text-left hover:bg-muted/75 transition-colors cursor-pointer" > - Book me the cheapest flight from San Francisco to Los Angeles for - July 27 2025. My name is Pranay Prakash. I like window seats. - Don't ask me for confirmation. + {FULL_EXAMPLE_PROMPT}
@@ -171,15 +183,10 @@ export default function ChatPage() { {messages.map((message, index) => { const hasText = message.parts.some((part) => part.type === "text"); + const isLastMessage = index === messages.length - 1; return (
- {message.role === "assistant" && - index === messages.length - 1 && - (status === "submitted" || status === "streaming") && - !hasText && ( - Thinking... - )} {message.parts.map((part, partIndex) => { @@ -212,7 +219,8 @@ export default function ChatPage() { part.type === "tool-checkFlightStatus" || part.type === "tool-getAirportInfo" || part.type === "tool-bookFlight" || - part.type === "tool-checkBaggageAllowance" + part.type === "tool-checkBaggageAllowance" || + part.type === "tool-sleep" ) { // Additional type guard to ensure we have the required properties if (!("toolCallId" in part) || !("state" in part)) { @@ -240,13 +248,58 @@ export default function ChatPage() { ); } + if (part.type === "tool-bookingApproval") { + return ( + + ); + } return null; })} + {/* Loading indicators */} + {message.role === "assistant" && + isLastMessage && + !hasText && ( + <> + {status === "submitted" && ( + + Sending message... + + )} + {status === "streaming" && ( + + Waiting for response... + + )} + + )}
); })} + {/* Show loading indicator when message is sent but no assistant response yet */} + {messages.length > 0 && + messages[messages.length - 1].role === "user" && + status === "submitted" && ( + + + + Processing your request... + + + + )}
@@ -414,6 +467,16 @@ function renderToolOutput(part: any) { ); } + case "tool-sleep": { + return ( +
+

+ Sleeping for {part.input.durationMs}ms... +

+
+ ); + } + default: return null; } diff --git a/flight-booking-app/components/booking-approval.tsx b/flight-booking-app/components/booking-approval.tsx new file mode 100644 index 0000000..b3d8a5d --- /dev/null +++ b/flight-booking-app/components/booking-approval.tsx @@ -0,0 +1,93 @@ +'use client'; +import { useState } from 'react'; +interface BookingApprovalProps { + toolCallId: string; + input?: { + flightNumber: string; + passengerName: string; + price: number; + }; + output?: string; +} +export function BookingApproval({ + toolCallId, + input, + output, +}: BookingApprovalProps) { + const [comment, setComment] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + // If we have output, the approval has been processed + if (output) { + try { + const json = JSON.parse(output) as { output: { value: string } }; + return ( +
+

{json.output.value}

+
+ ); + } catch (error) { + return ( +
+

+ Error parsing approval result: {(error as Error).message} +

+
+ ); + } + } + + const handleSubmit = async (approved: boolean) => { + setIsSubmitting(true); + try { + await fetch('/api/hooks/approval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ toolCallId, approved, comment }), + }); + } finally { + setIsSubmitting(false); + } + }; + return ( +
+
+

Approve this booking?

+
+ {input && ( + <> +
Flight: {input.flightNumber}
+
Passenger: {input.passengerName}
+
Price: ${input.price}
+ + )} +
+
+