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..909704e 100644 --- a/flight-booking-app/app/page.tsx +++ b/flight-booking-app/app/page.tsx @@ -21,6 +21,7 @@ import { } from "@/components/ai-elements/tool"; import ChatInput from "@/components/chat-input"; import type { MyUIMessage } from "@/schemas/chat"; +import { BookingApproval } from "@/components/booking-approval"; const SUGGESTIONS = [ "Find me flights from San Francisco to Los Angeles", @@ -29,6 +30,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); @@ -38,7 +41,7 @@ export default function ChatPage() { return localStorage.getItem("active-workflow-run-id") ?? undefined; }, []); - const { stop, messages, sendMessage, status, setMessages } = + const { stop, error, messages, sendMessage, status, setMessages } = useChat({ resume: !!activeWorkflowRunId, onError(error) { @@ -63,7 +66,7 @@ export default function ChatPage() { // Update the chat history in `localStorage` to include the latest user message localStorage.setItem( "chat-history", - JSON.stringify(options.messages), + JSON.stringify(options.messages) ); // We'll store the workflow run ID in `localStorage` to allow the client @@ -71,7 +74,7 @@ export default function ChatPage() { const workflowRunId = response.headers.get("x-workflow-run-id"); if (!workflowRunId) { throw new Error( - 'Workflow run ID not found in "x-workflow-run-id" response header', + 'Workflow run ID not found in "x-workflow-run-id" response header' ); } localStorage.setItem("active-workflow-run-id", workflowRunId); @@ -121,6 +124,16 @@ export default function ChatPage() {

Book a flight using workflows

+ {/* Error display */} + {error && ( +
+
+ Error: + {error.message} +
+
+ )} + {messages.length === 0 && (
@@ -154,15 +167,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 +182,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 +218,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 +247,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 +466,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..68a1946 --- /dev/null +++ b/flight-booking-app/components/booking-approval.tsx @@ -0,0 +1,109 @@ +'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); + const [error, setError] = useState(null); + + // 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); + setError(null); + try { + const response = await fetch('/api/hooks/approval', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ toolCallId, approved, comment }), + }); + + if (!response.ok) { + const errorData = await response.text(); + throw new Error(`API error: ${response.status} - ${errorData || response.statusText}`); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Failed to submit approval'; + setError(errorMessage); + setIsSubmitting(false); + return; + } + setIsSubmitting(false); + }; + return ( +
+ {error && ( +
+

{error}

+
+ )} +
+

Approve this booking?

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