[{"data":1,"prerenderedAt":2786},["ShallowReactive",2],{"navigation":3,"page:\u002Fvercel-ai-sdk":45},{"title":4,"links":5},"API reference",[6,9,12,15,18,21,24,27,30,33,36,39,42],{"title":7,"href":8},"Accounts","\u002Freference\u002Faccounts",{"title":10,"href":11},"Connect","\u002Freference\u002Fconnect",{"title":13,"href":14},"Calendars","\u002Freference\u002Fcalendars",{"title":16,"href":17},"Events","\u002Freference\u002Fevents",{"title":19,"href":20},"Availability","\u002Freference\u002Favailability",{"title":22,"href":23},"Meetings","\u002Freference\u002Fmeetings",{"title":25,"href":26},"Invitations","\u002Freference\u002Finvitations",{"title":28,"href":29},"Webhooks","\u002Freference\u002Fwebhooks",{"title":31,"href":32},"Metadata","\u002Freference\u002Fmetadata",{"title":34,"href":35},"Keys","\u002Freference\u002Fkeys",{"title":37,"href":38},"Sandbox","\u002Freference\u002Fsandbox",{"title":40,"href":41},"Usage","\u002Freference\u002Fusage",{"title":43,"href":44},"Logs","\u002Freference\u002Flogs",{"kind":46,"title":47,"description":48,"sections":49,"doc":65},"guide","Vercel AI SDK","Add a calendar assistant to a Next.js app with the Vercel AI SDK and Pyas. It reads each user's Google and Outlook calendars, and asks them before it books, moves or deletes anything.",[50,53,56,59,62],{"id":51,"title":52},"install","Install",{"id":54,"title":55},"the-chat-route","The chat route",{"id":57,"title":58},"the-chat-page","The chat page",{"id":60,"title":61},"show-the-change","Show the change",{"id":63,"title":64},"try-it","Try it",{"id":66,"title":47,"body":67,"description":48,"extension":2778,"hero":2779,"meta":2780,"navigation":321,"path":2781,"rawbody":2782,"sections":49,"seo":2783,"stem":73,"v1":2784,"__hash__":2785},"docs\u002Fvercel-ai-sdk.md",{"type":68,"value":69,"toc":2771},"minimark",[70,74,81,95,98,132,155,158,183,186,218,233,235,701,707,714,716,1489,1500,1507,1509,1516,1677,1697,2717,2720,2728,2730,2760,2767],[71,72,47],"h1",{"id":73},"vercel-ai-sdk",[75,76,77],"lead",{},[78,79,80],"p",{},"This guide builds a chat in a Next.js app where your users ask about their calendars and book meetings. The assistant uses Pyas's tools for the signed-in user only, and each change waits for them to approve it.",[78,82,83,84,88,89,94],{},"You need the AI SDK 6 or later. The code here was tested with Next.js 16, the AI SDK 7 and OpenAI's ",[85,86,87],"code",{},"gpt-5.5",". Any model that calls tools works. See ",[90,91,93],"a",{"href":92},"\u002Fai-in-your-app","AI in your app"," for how the tools work.",[78,96,97],{},"To start from a working app with the code on this page, create it with the Pyas CLI:",[99,100,105],"pre",{"className":101,"code":102,"language":103,"meta":104,"style":104},"language-bash shiki shiki-themes css-variables","npx @pyas\u002Fcli init --template nextjs-ai-chat my-chat\n","bash","",[85,106,107],{"__ignoreMap":104},[108,109,112,116,120,123,126,129],"span",{"class":110,"line":111},"line",1,[108,113,115],{"class":114},"s1IGz","npx",[108,117,119],{"class":118},"sTB4W"," @pyas\u002Fcli",[108,121,122],{"class":118}," init",[108,124,125],{"class":118}," --template",[108,127,128],{"class":118}," nextjs-ai-chat",[108,130,131],{"class":118}," my-chat\n",[78,133,134,135,138,139,142,143,146,147,150,151,154],{},"Set ",[85,136,137],{},"OPENAI_API_KEY"," in its ",[85,140,141],{},".env.local",", then run ",[85,144,145],{},"npm install"," and ",[85,148,149],{},"npm run dev",". If you're signed in with ",[85,152,153],{},"pyas login",", your test key is already there.",[156,157,52],"h2",{"id":51},[99,159,161],{"className":101,"code":160,"language":103,"meta":104,"style":104},"npm install @pyas\u002Fai-tools ai @ai-sdk\u002Freact @ai-sdk\u002Fopenai\n",[85,162,163],{"__ignoreMap":104},[108,164,165,168,171,174,177,180],{"class":110,"line":111},[108,166,167],{"class":114},"npm",[108,169,170],{"class":118}," install",[108,172,173],{"class":118}," @pyas\u002Fai-tools",[108,175,176],{"class":118}," ai",[108,178,179],{"class":118}," @ai-sdk\u002Freact",[108,181,182],{"class":118}," @ai-sdk\u002Fopenai\n",[78,184,185],{},"Set these on your server:",[99,187,189],{"className":101,"code":188,"language":103,"meta":104,"style":104},"PYAS_API_KEY=pyas_sk_test_...   # a test key from API keys in the Pyas dashboard\nOPENAI_API_KEY=sk-...\n",[85,190,191,208],{"__ignoreMap":104},[108,192,193,197,201,204],{"class":110,"line":111},[108,194,196],{"class":195},"sqSWz","PYAS_API_KEY",[108,198,200],{"class":199},"sag7P","=",[108,202,203],{"class":118},"pyas_sk_test_...",[108,205,207],{"class":206},"ssgCD","   # a test key from API keys in the Pyas dashboard\n",[108,209,211,213,215],{"class":110,"line":210},2,[108,212,137],{"class":195},[108,214,200],{"class":199},[108,216,217],{"class":118},"sk-...\n",[78,219,220,221,224,225,228,229,232],{},"The examples use a ",[85,222,223],{},"currentUserId"," function for your sign-in. It returns the signed-in person's ID in your app, the same ID you saved as ",[85,226,227],{},"subjectId"," when they connected their calendar. To try the guide in the sandbox, have it return ",[85,230,231],{},"'sandbox_user_1'",", the sandbox person Avery Brooks.",[156,234,55],{"id":54},[99,236,241],{"className":237,"code":238,"filename":239,"language":240,"meta":104,"style":104},"language-ts shiki shiki-themes css-variables","import { openai } from '@ai-sdk\u002Fopenai'\nimport { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai'\nimport { pyasTools } from '@pyas\u002Fai-tools\u002Fvercel'\nimport { currentUserId } from '@\u002Flib\u002Fuser'\n\nexport async function POST(req: Request) {\n    const userId = await currentUserId(req)\n    if (!userId) return new Response('Sign in to use your calendar.', { status: 401 })\n\n    const { messages }: { messages: UIMessage[] } = await req.json()\n    \u002F\u002F Today's date lets the model work out \"tomorrow\".\n    const today = new Date().toISOString().slice(0, 10)\n    const result = streamText({\n        model: openai('gpt-5.5'),\n        system: `You help the user manage their calendar. Today is ${today}.`,\n        messages: await convertToModelMessages(messages),\n        tools: pyasTools({ subjectId: userId }),\n        stopWhen: stepCountIs(10),\n        \u002F\u002F With OpenAI: each request carries the model's reasoning, so a chat that goes on after\n        \u002F\u002F an approval never points at reasoning OpenAI no longer has.\n        providerOptions: { openai: { store: false, include: ['reasoning.encrypted_content'] } },\n    })\n    return result.toUIMessageStreamResponse()\n}\n","app\u002Fapi\u002Fchat\u002Froute.ts","ts",[85,242,243,258,290,303,316,323,350,372,415,420,460,466,505,520,542,567,585,606,625,631,637,676,682,695],{"__ignoreMap":104},[108,244,245,248,251,254],{"class":110,"line":111},[108,246,247],{"class":199},"import",[108,249,250],{"class":195}," { openai } ",[108,252,253],{"class":199},"from",[108,255,257],{"class":256},"sk1Au"," '@ai-sdk\u002Fopenai'\n",[108,259,260,262,265,269,272,274,277,279,282,285,287],{"class":110,"line":210},[108,261,247],{"class":199},[108,263,264],{"class":195}," { convertToModelMessages",[108,266,268],{"class":267},"sEEt9",",",[108,270,271],{"class":195}," stepCountIs",[108,273,268],{"class":267},[108,275,276],{"class":195}," streamText",[108,278,268],{"class":267},[108,280,281],{"class":199}," type",[108,283,284],{"class":195}," UIMessage } ",[108,286,253],{"class":199},[108,288,289],{"class":256}," 'ai'\n",[108,291,293,295,298,300],{"class":110,"line":292},3,[108,294,247],{"class":199},[108,296,297],{"class":195}," { pyasTools } ",[108,299,253],{"class":199},[108,301,302],{"class":256}," '@pyas\u002Fai-tools\u002Fvercel'\n",[108,304,306,308,311,313],{"class":110,"line":305},4,[108,307,247],{"class":199},[108,309,310],{"class":195}," { currentUserId } ",[108,312,253],{"class":199},[108,314,315],{"class":256}," '@\u002Flib\u002Fuser'\n",[108,317,319],{"class":110,"line":318},5,[108,320,322],{"emptyLinePlaceholder":321},true,"\n",[108,324,326,329,332,335,338,341,344,347],{"class":110,"line":325},6,[108,327,328],{"class":199},"export",[108,330,331],{"class":199}," async",[108,333,334],{"class":199}," function",[108,336,337],{"class":114}," POST",[108,339,340],{"class":195},"(req",[108,342,343],{"class":199},":",[108,345,346],{"class":114}," Request",[108,348,349],{"class":195},") {\n",[108,351,353,356,360,363,366,369],{"class":110,"line":352},7,[108,354,355],{"class":199},"    const",[108,357,359],{"class":358},"sXULy"," userId",[108,361,362],{"class":199}," =",[108,364,365],{"class":199}," await",[108,367,368],{"class":114}," currentUserId",[108,370,371],{"class":195},"(req)\n",[108,373,375,378,381,384,387,390,393,396,399,402,404,407,409,412],{"class":110,"line":374},8,[108,376,377],{"class":199},"    if",[108,379,380],{"class":195}," (",[108,382,383],{"class":199},"!",[108,385,386],{"class":195},"userId) ",[108,388,389],{"class":199},"return",[108,391,392],{"class":199}," new",[108,394,395],{"class":114}," Response",[108,397,398],{"class":195},"(",[108,400,401],{"class":256},"'Sign in to use your calendar.'",[108,403,268],{"class":267},[108,405,406],{"class":195}," { status",[108,408,343],{"class":199},[108,410,411],{"class":358}," 401",[108,413,414],{"class":195}," })\n",[108,416,418],{"class":110,"line":417},9,[108,419,322],{"emptyLinePlaceholder":321},[108,421,423,425,428,431,434,436,439,441,444,447,449,451,454,457],{"class":110,"line":422},10,[108,424,355],{"class":199},[108,426,427],{"class":195}," { ",[108,429,430],{"class":358},"messages",[108,432,433],{"class":195}," }",[108,435,343],{"class":199},[108,437,438],{"class":195}," { messages",[108,440,343],{"class":199},[108,442,443],{"class":114}," UIMessage",[108,445,446],{"class":195},"[] } ",[108,448,200],{"class":199},[108,450,365],{"class":199},[108,452,453],{"class":358}," req",[108,455,456],{"class":114},".json",[108,458,459],{"class":195},"()\n",[108,461,463],{"class":110,"line":462},11,[108,464,465],{"class":206},"    \u002F\u002F Today's date lets the model work out \"tomorrow\".\n",[108,467,469,471,474,476,478,481,484,487,489,492,494,497,499,502],{"class":110,"line":468},12,[108,470,355],{"class":199},[108,472,473],{"class":358}," today",[108,475,362],{"class":199},[108,477,392],{"class":199},[108,479,480],{"class":114}," Date",[108,482,483],{"class":195},"()",[108,485,486],{"class":114},".toISOString",[108,488,483],{"class":195},[108,490,491],{"class":114},".slice",[108,493,398],{"class":195},[108,495,496],{"class":358},"0",[108,498,268],{"class":267},[108,500,501],{"class":358}," 10",[108,503,504],{"class":195},")\n",[108,506,508,510,513,515,517],{"class":110,"line":507},13,[108,509,355],{"class":199},[108,511,512],{"class":358}," result",[108,514,362],{"class":199},[108,516,276],{"class":114},[108,518,519],{"class":195},"({\n",[108,521,523,526,528,531,533,536,539],{"class":110,"line":522},14,[108,524,525],{"class":195},"        model",[108,527,343],{"class":199},[108,529,530],{"class":114}," openai",[108,532,398],{"class":195},[108,534,535],{"class":256},"'gpt-5.5'",[108,537,538],{"class":195},")",[108,540,541],{"class":267},",\n",[108,543,545,548,550,553,556,559,562,565],{"class":110,"line":544},15,[108,546,547],{"class":195},"        system",[108,549,343],{"class":199},[108,551,552],{"class":256}," `You help the user manage their calendar. Today is ",[108,554,555],{"class":199},"${",[108,557,558],{"class":195},"today",[108,560,561],{"class":199},"}",[108,563,564],{"class":256},".`",[108,566,541],{"class":267},[108,568,570,573,575,577,580,583],{"class":110,"line":569},16,[108,571,572],{"class":195},"        messages",[108,574,343],{"class":199},[108,576,365],{"class":199},[108,578,579],{"class":114}," convertToModelMessages",[108,581,582],{"class":195},"(messages)",[108,584,541],{"class":267},[108,586,588,591,593,596,599,601,604],{"class":110,"line":587},17,[108,589,590],{"class":195},"        tools",[108,592,343],{"class":199},[108,594,595],{"class":114}," pyasTools",[108,597,598],{"class":195},"({ subjectId",[108,600,343],{"class":199},[108,602,603],{"class":195}," userId })",[108,605,541],{"class":267},[108,607,609,612,614,616,618,621,623],{"class":110,"line":608},18,[108,610,611],{"class":195},"        stopWhen",[108,613,343],{"class":199},[108,615,271],{"class":114},[108,617,398],{"class":195},[108,619,620],{"class":358},"10",[108,622,538],{"class":195},[108,624,541],{"class":267},[108,626,628],{"class":110,"line":627},19,[108,629,630],{"class":206},"        \u002F\u002F With OpenAI: each request carries the model's reasoning, so a chat that goes on after\n",[108,632,634],{"class":110,"line":633},20,[108,635,636],{"class":206},"        \u002F\u002F an approval never points at reasoning OpenAI no longer has.\n",[108,638,640,643,645,648,650,653,655,658,660,663,665,668,671,674],{"class":110,"line":639},21,[108,641,642],{"class":195},"        providerOptions",[108,644,343],{"class":199},[108,646,647],{"class":195}," { openai",[108,649,343],{"class":199},[108,651,652],{"class":195}," { store",[108,654,343],{"class":199},[108,656,657],{"class":358}," false",[108,659,268],{"class":267},[108,661,662],{"class":195}," include",[108,664,343],{"class":199},[108,666,667],{"class":195}," [",[108,669,670],{"class":256},"'reasoning.encrypted_content'",[108,672,673],{"class":195},"] } }",[108,675,541],{"class":267},[108,677,679],{"class":110,"line":678},22,[108,680,681],{"class":195},"    })\n",[108,683,685,688,690,693],{"class":110,"line":684},23,[108,686,687],{"class":199},"    return",[108,689,512],{"class":358},[108,691,692],{"class":114},".toUIMessageStreamResponse",[108,694,459],{"class":195},[108,696,698],{"class":110,"line":697},24,[108,699,700],{"class":195},"}\n",[78,702,703,706],{},[85,704,705],{},"pyasTools({ subjectId: userId })"," gives the model the tools for this person's calendars only. Each tool that changes a calendar is marked as needing approval, so the AI SDK stops at it and sends the change to the page instead of running it.",[78,708,709,710,713],{},"With a model from another provider, leave out ",[85,711,712],{},"providerOptions",".",[156,715,58],{"id":57},[99,717,722],{"className":718,"code":719,"filename":720,"language":721,"meta":104,"style":104},"language-tsx shiki shiki-themes css-variables","'use client'\n\nimport { useChat } from '@ai-sdk\u002Freact'\nimport { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'\nimport { useState } from 'react'\nimport { Approval } from '.\u002Fapproval'\n\nexport default function Chat() {\n    const [input, setInput] = useState('')\n    const { messages, sendMessage, addToolApprovalResponse } = useChat({\n        \u002F\u002F Once the person has answered every change, the chat carries on by itself.\n        sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,\n    })\n\n    \u002F\u002F A declined change never runs. The reason tells the model not to ask for it again.\n    function answer(id: string, approved: boolean) {\n        const reason = approved ? undefined : \"The user declined this change. Leave it as it is, and don't ask again unless they do.\"\n        addToolApprovalResponse({ id, approved, reason })\n    }\n\n    function send(event: React.FormEvent) {\n        event.preventDefault()\n        sendMessage({ text: input })\n        setInput('')\n    }\n\n    return (\n        \u003Cmain>\n            {messages.map((message) => (\n                \u003Cdiv key={message.id} className={`message ${message.role}`}>\n                    {message.parts.map((part, index) => {\n                        if (part.type === 'text') return \u003Cdiv key={index} className=\"bubble\">{part.text}\u003C\u002Fdiv>\n                        if (part.type.startsWith('tool-') && 'state' in part && part.state === 'approval-requested') {\n                            return \u003CApproval key={index} tool={part.type.slice(5)} input={part.input as Record\u003Cstring, unknown>} onAnswer={(approved) => answer(part.approval.id, approved)} \u002F>\n                        }\n                        return null\n                    })}\n                \u003C\u002Fdiv>\n            ))}\n            \u003Cform onSubmit={send}>\n                \u003Cinput value={input} onChange={(event) => setInput(event.target.value)} placeholder=\"Ask about your calendar\" \u002F>\n            \u003C\u002Fform>\n        \u003C\u002Fmain>\n    )\n}\n","app\u002Fpage.tsx","tsx",[85,723,724,729,733,745,756,768,780,784,799,828,856,861,873,877,881,886,914,939,956,961,965,987,997,1010,1021,1026,1031,1039,1051,1070,1118,1146,1202,1253,1353,1359,1368,1374,1384,1390,1407,1458,1468,1478,1484],{"__ignoreMap":104},[108,725,726],{"class":110,"line":111},[108,727,728],{"class":256},"'use client'\n",[108,730,731],{"class":110,"line":210},[108,732,322],{"emptyLinePlaceholder":321},[108,734,735,737,740,742],{"class":110,"line":292},[108,736,247],{"class":199},[108,738,739],{"class":195}," { useChat } ",[108,741,253],{"class":199},[108,743,744],{"class":256}," '@ai-sdk\u002Freact'\n",[108,746,747,749,752,754],{"class":110,"line":305},[108,748,247],{"class":199},[108,750,751],{"class":195}," { lastAssistantMessageIsCompleteWithApprovalResponses } ",[108,753,253],{"class":199},[108,755,289],{"class":256},[108,757,758,760,763,765],{"class":110,"line":318},[108,759,247],{"class":199},[108,761,762],{"class":195}," { useState } ",[108,764,253],{"class":199},[108,766,767],{"class":256}," 'react'\n",[108,769,770,772,775,777],{"class":110,"line":325},[108,771,247],{"class":199},[108,773,774],{"class":195}," { Approval } ",[108,776,253],{"class":199},[108,778,779],{"class":256}," '.\u002Fapproval'\n",[108,781,782],{"class":110,"line":352},[108,783,322],{"emptyLinePlaceholder":321},[108,785,786,788,791,793,796],{"class":110,"line":374},[108,787,328],{"class":199},[108,789,790],{"class":199}," default",[108,792,334],{"class":199},[108,794,795],{"class":114}," Chat",[108,797,798],{"class":195},"() {\n",[108,800,801,803,805,808,810,813,816,818,821,823,826],{"class":110,"line":417},[108,802,355],{"class":199},[108,804,667],{"class":195},[108,806,807],{"class":358},"input",[108,809,268],{"class":267},[108,811,812],{"class":358}," setInput",[108,814,815],{"class":195},"] ",[108,817,200],{"class":199},[108,819,820],{"class":114}," useState",[108,822,398],{"class":195},[108,824,825],{"class":256},"''",[108,827,504],{"class":195},[108,829,830,832,834,836,838,841,843,846,849,851,854],{"class":110,"line":422},[108,831,355],{"class":199},[108,833,427],{"class":195},[108,835,430],{"class":358},[108,837,268],{"class":267},[108,839,840],{"class":358}," sendMessage",[108,842,268],{"class":267},[108,844,845],{"class":358}," addToolApprovalResponse",[108,847,848],{"class":195}," } ",[108,850,200],{"class":199},[108,852,853],{"class":114}," useChat",[108,855,519],{"class":195},[108,857,858],{"class":110,"line":462},[108,859,860],{"class":206},"        \u002F\u002F Once the person has answered every change, the chat carries on by itself.\n",[108,862,863,866,868,871],{"class":110,"line":468},[108,864,865],{"class":195},"        sendAutomaticallyWhen",[108,867,343],{"class":199},[108,869,870],{"class":195}," lastAssistantMessageIsCompleteWithApprovalResponses",[108,872,541],{"class":267},[108,874,875],{"class":110,"line":507},[108,876,681],{"class":195},[108,878,879],{"class":110,"line":522},[108,880,322],{"emptyLinePlaceholder":321},[108,882,883],{"class":110,"line":544},[108,884,885],{"class":206},"    \u002F\u002F A declined change never runs. The reason tells the model not to ask for it again.\n",[108,887,888,891,894,897,899,902,904,907,909,912],{"class":110,"line":569},[108,889,890],{"class":199},"    function",[108,892,893],{"class":114}," answer",[108,895,896],{"class":195},"(id",[108,898,343],{"class":199},[108,900,901],{"class":358}," string",[108,903,268],{"class":267},[108,905,906],{"class":195}," approved",[108,908,343],{"class":199},[108,910,911],{"class":358}," boolean",[108,913,349],{"class":195},[108,915,916,919,922,924,927,930,933,936],{"class":110,"line":587},[108,917,918],{"class":199},"        const",[108,920,921],{"class":358}," reason",[108,923,362],{"class":199},[108,925,926],{"class":195}," approved ",[108,928,929],{"class":199},"?",[108,931,932],{"class":358}," undefined",[108,934,935],{"class":199}," :",[108,937,938],{"class":256}," \"The user declined this change. Leave it as it is, and don't ask again unless they do.\"\n",[108,940,941,944,947,949,951,953],{"class":110,"line":608},[108,942,943],{"class":114},"        addToolApprovalResponse",[108,945,946],{"class":195},"({ id",[108,948,268],{"class":267},[108,950,906],{"class":195},[108,952,268],{"class":267},[108,954,955],{"class":195}," reason })\n",[108,957,958],{"class":110,"line":627},[108,959,960],{"class":195},"    }\n",[108,962,963],{"class":110,"line":633},[108,964,322],{"emptyLinePlaceholder":321},[108,966,967,969,972,975,977,980,982,985],{"class":110,"line":639},[108,968,890],{"class":199},[108,970,971],{"class":114}," send",[108,973,974],{"class":195},"(event",[108,976,343],{"class":199},[108,978,979],{"class":114}," React",[108,981,713],{"class":195},[108,983,984],{"class":114},"FormEvent",[108,986,349],{"class":195},[108,988,989,992,995],{"class":110,"line":678},[108,990,991],{"class":358},"        event",[108,993,994],{"class":114},".preventDefault",[108,996,459],{"class":195},[108,998,999,1002,1005,1007],{"class":110,"line":684},[108,1000,1001],{"class":114},"        sendMessage",[108,1003,1004],{"class":195},"({ text",[108,1006,343],{"class":199},[108,1008,1009],{"class":195}," input })\n",[108,1011,1012,1015,1017,1019],{"class":110,"line":697},[108,1013,1014],{"class":114},"        setInput",[108,1016,398],{"class":195},[108,1018,825],{"class":256},[108,1020,504],{"class":195},[108,1022,1024],{"class":110,"line":1023},25,[108,1025,960],{"class":195},[108,1027,1029],{"class":110,"line":1028},26,[108,1030,322],{"emptyLinePlaceholder":321},[108,1032,1034,1036],{"class":110,"line":1033},27,[108,1035,687],{"class":199},[108,1037,1038],{"class":195}," (\n",[108,1040,1042,1045,1048],{"class":110,"line":1041},28,[108,1043,1044],{"class":195},"        \u003C",[108,1046,1047],{"class":256},"main",[108,1049,1050],{"class":195},">\n",[108,1052,1054,1057,1059,1062,1065,1068],{"class":110,"line":1053},29,[108,1055,1056],{"class":195},"            {",[108,1058,430],{"class":358},[108,1060,1061],{"class":114},".map",[108,1063,1064],{"class":195},"((message) ",[108,1066,1067],{"class":199},"=>",[108,1069,1038],{"class":195},[108,1071,1073,1076,1079,1082,1084,1087,1090,1093,1096,1098,1100,1103,1105,1107,1110,1112,1115],{"class":110,"line":1072},30,[108,1074,1075],{"class":195},"                \u003C",[108,1077,1078],{"class":256},"div",[108,1080,1081],{"class":114}," key",[108,1083,200],{"class":199},[108,1085,1086],{"class":195},"{",[108,1088,1089],{"class":358},"message",[108,1091,1092],{"class":195},".id} ",[108,1094,1095],{"class":114},"className",[108,1097,200],{"class":199},[108,1099,1086],{"class":195},[108,1101,1102],{"class":256},"`message ",[108,1104,555],{"class":199},[108,1106,1089],{"class":358},[108,1108,1109],{"class":195},".role",[108,1111,561],{"class":199},[108,1113,1114],{"class":256},"`",[108,1116,1117],{"class":195},"}>\n",[108,1119,1121,1124,1126,1128,1131,1133,1136,1138,1141,1143],{"class":110,"line":1120},31,[108,1122,1123],{"class":195},"                    {",[108,1125,1089],{"class":358},[108,1127,713],{"class":114},[108,1129,1130],{"class":358},"parts",[108,1132,1061],{"class":114},[108,1134,1135],{"class":195},"((part",[108,1137,268],{"class":267},[108,1139,1140],{"class":195}," index) ",[108,1142,1067],{"class":199},[108,1144,1145],{"class":195}," {\n",[108,1147,1149,1152,1154,1157,1160,1163,1166,1169,1171,1174,1176,1178,1180,1183,1185,1187,1190,1193,1195,1198,1200],{"class":110,"line":1148},32,[108,1150,1151],{"class":199},"                        if",[108,1153,380],{"class":195},[108,1155,1156],{"class":358},"part",[108,1158,1159],{"class":195},".type ",[108,1161,1162],{"class":199},"===",[108,1164,1165],{"class":256}," 'text'",[108,1167,1168],{"class":195},") ",[108,1170,389],{"class":199},[108,1172,1173],{"class":195}," \u003C",[108,1175,1078],{"class":256},[108,1177,1081],{"class":114},[108,1179,200],{"class":199},[108,1181,1182],{"class":195},"{index} ",[108,1184,1095],{"class":114},[108,1186,200],{"class":199},[108,1188,1189],{"class":256},"\"bubble\"",[108,1191,1192],{"class":195},">{",[108,1194,1156],{"class":358},[108,1196,1197],{"class":195},".text}\u003C\u002F",[108,1199,1078],{"class":256},[108,1201,1050],{"class":195},[108,1203,1205,1207,1209,1211,1213,1216,1219,1221,1224,1226,1229,1232,1235,1238,1240,1243,1246,1248,1251],{"class":110,"line":1204},33,[108,1206,1151],{"class":199},[108,1208,380],{"class":195},[108,1210,1156],{"class":358},[108,1212,713],{"class":114},[108,1214,1215],{"class":358},"type",[108,1217,1218],{"class":114},".startsWith",[108,1220,398],{"class":195},[108,1222,1223],{"class":256},"'tool-'",[108,1225,1168],{"class":195},[108,1227,1228],{"class":199},"&&",[108,1230,1231],{"class":256}," 'state'",[108,1233,1234],{"class":199}," in",[108,1236,1237],{"class":195}," part ",[108,1239,1228],{"class":199},[108,1241,1242],{"class":358}," part",[108,1244,1245],{"class":195},".state ",[108,1247,1162],{"class":199},[108,1249,1250],{"class":256}," 'approval-requested'",[108,1252,349],{"class":195},[108,1254,1256,1259,1261,1264,1266,1268,1270,1273,1275,1277,1279,1281,1283,1285,1287,1290,1293,1295,1297,1299,1301,1304,1307,1310,1313,1316,1318,1321,1324,1327,1329,1332,1334,1336,1338,1340,1342,1345,1348,1350],{"class":110,"line":1255},34,[108,1257,1258],{"class":199},"                            return",[108,1260,1173],{"class":195},[108,1262,1263],{"class":358},"Approval",[108,1265,1081],{"class":114},[108,1267,200],{"class":199},[108,1269,1182],{"class":195},[108,1271,1272],{"class":114},"tool",[108,1274,200],{"class":199},[108,1276,1086],{"class":195},[108,1278,1156],{"class":358},[108,1280,713],{"class":114},[108,1282,1215],{"class":358},[108,1284,491],{"class":114},[108,1286,398],{"class":195},[108,1288,1289],{"class":358},"5",[108,1291,1292],{"class":195},")} ",[108,1294,807],{"class":114},[108,1296,200],{"class":199},[108,1298,1086],{"class":195},[108,1300,1156],{"class":358},[108,1302,1303],{"class":195},".input ",[108,1305,1306],{"class":199},"as",[108,1308,1309],{"class":114}," Record",[108,1311,1312],{"class":195},"\u003C",[108,1314,1315],{"class":358},"string",[108,1317,268],{"class":267},[108,1319,1320],{"class":358}," unknown",[108,1322,1323],{"class":195},">} ",[108,1325,1326],{"class":114},"onAnswer",[108,1328,200],{"class":199},[108,1330,1331],{"class":195},"{(approved) ",[108,1333,1067],{"class":199},[108,1335,893],{"class":114},[108,1337,398],{"class":195},[108,1339,1156],{"class":358},[108,1341,713],{"class":195},[108,1343,1344],{"class":358},"approval",[108,1346,1347],{"class":195},".id",[108,1349,268],{"class":267},[108,1351,1352],{"class":195}," approved)} \u002F>\n",[108,1354,1356],{"class":110,"line":1355},35,[108,1357,1358],{"class":195},"                        }\n",[108,1360,1362,1365],{"class":110,"line":1361},36,[108,1363,1364],{"class":199},"                        return",[108,1366,1367],{"class":358}," null\n",[108,1369,1371],{"class":110,"line":1370},37,[108,1372,1373],{"class":195},"                    })}\n",[108,1375,1377,1380,1382],{"class":110,"line":1376},38,[108,1378,1379],{"class":195},"                \u003C\u002F",[108,1381,1078],{"class":256},[108,1383,1050],{"class":195},[108,1385,1387],{"class":110,"line":1386},39,[108,1388,1389],{"class":195},"            ))}\n",[108,1391,1393,1396,1399,1402,1404],{"class":110,"line":1392},40,[108,1394,1395],{"class":195},"            \u003C",[108,1397,1398],{"class":256},"form",[108,1400,1401],{"class":114}," onSubmit",[108,1403,200],{"class":199},[108,1405,1406],{"class":195},"{send}>\n",[108,1408,1410,1412,1414,1417,1419,1422,1425,1427,1430,1432,1434,1436,1439,1441,1444,1447,1450,1452,1455],{"class":110,"line":1409},41,[108,1411,1075],{"class":195},[108,1413,807],{"class":256},[108,1415,1416],{"class":114}," value",[108,1418,200],{"class":199},[108,1420,1421],{"class":195},"{input} ",[108,1423,1424],{"class":114},"onChange",[108,1426,200],{"class":199},[108,1428,1429],{"class":195},"{(event) ",[108,1431,1067],{"class":199},[108,1433,812],{"class":114},[108,1435,398],{"class":195},[108,1437,1438],{"class":358},"event",[108,1440,713],{"class":195},[108,1442,1443],{"class":358},"target",[108,1445,1446],{"class":195},".value)} ",[108,1448,1449],{"class":114},"placeholder",[108,1451,200],{"class":199},[108,1453,1454],{"class":256},"\"Ask about your calendar\"",[108,1456,1457],{"class":195}," \u002F>\n",[108,1459,1461,1464,1466],{"class":110,"line":1460},42,[108,1462,1463],{"class":195},"            \u003C\u002F",[108,1465,1398],{"class":256},[108,1467,1050],{"class":195},[108,1469,1471,1474,1476],{"class":110,"line":1470},43,[108,1472,1473],{"class":195},"        \u003C\u002F",[108,1475,1047],{"class":256},[108,1477,1050],{"class":195},[108,1479,1481],{"class":110,"line":1480},44,[108,1482,1483],{"class":195},"    )\n",[108,1485,1487],{"class":110,"line":1486},45,[108,1488,700],{"class":195},[78,1490,1491,1492,1495,1496,1499],{},"A tool part in the ",[85,1493,1494],{},"approval-requested"," state is a change waiting for the person. When they choose, ",[85,1497,1498],{},"addToolApprovalResponse"," records it, and the chat sends it to your route, where the AI SDK runs the approved change or tells the model it was declined.",[78,1501,1502,1503,1506],{},"Pass a ",[85,1504,1505],{},"reason"," when the person declines. Without one, the model may ask for the same change again.",[156,1508,61],{"id":60},[78,1510,1511,1512,1515],{},"The tool part holds what the model asked for, such as an event ID to delete. To show the person the event itself, ask your server. ",[85,1513,1514],{},"describePyasChange"," reads the event from Pyas, with the same checks as the tools:",[99,1517,1520],{"className":237,"code":1518,"filename":1519,"language":240,"meta":104,"style":104},"import { describePyasChange } from '@pyas\u002Fai-tools\u002Fvercel'\nimport { currentUserId } from '@\u002Flib\u002Fuser'\n\n\u002F** What a change waiting for approval is about, from Pyas's own data, for the approval card. *\u002F\nexport async function POST(req: Request) {\n    const userId = await currentUserId(req)\n    if (!userId) return new Response('Sign in to use your calendar.', { status: 401 })\n    const { tool, input } = await req.json()\n    return Response.json(await describePyasChange({ tool, input }, { subjectId: userId }))\n}\n","app\u002Fapi\u002Fpyas-change\u002Froute.ts",[85,1521,1522,1533,1543,1547,1552,1570,1584,1614,1639,1673],{"__ignoreMap":104},[108,1523,1524,1526,1529,1531],{"class":110,"line":111},[108,1525,247],{"class":199},[108,1527,1528],{"class":195}," { describePyasChange } ",[108,1530,253],{"class":199},[108,1532,302],{"class":256},[108,1534,1535,1537,1539,1541],{"class":110,"line":210},[108,1536,247],{"class":199},[108,1538,310],{"class":195},[108,1540,253],{"class":199},[108,1542,315],{"class":256},[108,1544,1545],{"class":110,"line":292},[108,1546,322],{"emptyLinePlaceholder":321},[108,1548,1549],{"class":110,"line":305},[108,1550,1551],{"class":206},"\u002F** What a change waiting for approval is about, from Pyas's own data, for the approval card. *\u002F\n",[108,1553,1554,1556,1558,1560,1562,1564,1566,1568],{"class":110,"line":318},[108,1555,328],{"class":199},[108,1557,331],{"class":199},[108,1559,334],{"class":199},[108,1561,337],{"class":114},[108,1563,340],{"class":195},[108,1565,343],{"class":199},[108,1567,346],{"class":114},[108,1569,349],{"class":195},[108,1571,1572,1574,1576,1578,1580,1582],{"class":110,"line":325},[108,1573,355],{"class":199},[108,1575,359],{"class":358},[108,1577,362],{"class":199},[108,1579,365],{"class":199},[108,1581,368],{"class":114},[108,1583,371],{"class":195},[108,1585,1586,1588,1590,1592,1594,1596,1598,1600,1602,1604,1606,1608,1610,1612],{"class":110,"line":352},[108,1587,377],{"class":199},[108,1589,380],{"class":195},[108,1591,383],{"class":199},[108,1593,386],{"class":195},[108,1595,389],{"class":199},[108,1597,392],{"class":199},[108,1599,395],{"class":114},[108,1601,398],{"class":195},[108,1603,401],{"class":256},[108,1605,268],{"class":267},[108,1607,406],{"class":195},[108,1609,343],{"class":199},[108,1611,411],{"class":358},[108,1613,414],{"class":195},[108,1615,1616,1618,1620,1622,1624,1627,1629,1631,1633,1635,1637],{"class":110,"line":374},[108,1617,355],{"class":199},[108,1619,427],{"class":195},[108,1621,1272],{"class":358},[108,1623,268],{"class":267},[108,1625,1626],{"class":358}," input",[108,1628,848],{"class":195},[108,1630,200],{"class":199},[108,1632,365],{"class":199},[108,1634,453],{"class":358},[108,1636,456],{"class":114},[108,1638,459],{"class":195},[108,1640,1641,1643,1645,1647,1649,1652,1655,1658,1660,1663,1665,1668,1670],{"class":110,"line":417},[108,1642,687],{"class":199},[108,1644,395],{"class":358},[108,1646,456],{"class":114},[108,1648,398],{"class":195},[108,1650,1651],{"class":199},"await",[108,1653,1654],{"class":114}," describePyasChange",[108,1656,1657],{"class":195},"({ tool",[108,1659,268],{"class":267},[108,1661,1662],{"class":195}," input }",[108,1664,268],{"class":267},[108,1666,1667],{"class":195}," { subjectId",[108,1669,343],{"class":199},[108,1671,1672],{"class":195}," userId }))\n",[108,1674,1675],{"class":110,"line":422},[108,1676,700],{"class":195},[78,1678,1679,1680,1683,1684,1687,1688,1690,1691,1693,1694,713],{},"It returns ",[85,1681,1682],{},"{ ok: true, title, event }",", where ",[85,1685,1686],{},"title"," says what the change does, such as \"Delete an event\", and ",[85,1689,1438],{}," is the event as it is now in Pyas. A new event has no ",[85,1692,1438],{},": show the title and times from the input. When the change could never run, such as for an event that doesn't exist, it returns ",[85,1695,1696],{},"{ ok: false, error }",[99,1698,1701],{"className":718,"code":1699,"filename":1700,"language":721,"meta":104,"style":104},"'use client'\n\nimport { useEffect, useState } from 'react'\n\ninterface PyasEvent { title: string; startsAtLocal: string; endsAtLocal: string }\n\ninterface Change {\n    ok: boolean\n    title?: string\n    event?: PyasEvent\n    error?: { message: string }\n}\n\nconst when = (startsAt: string, endsAt: string) => {\n    const start = new Date(startsAt)\n    const time = (date: Date) => date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })\n    return `${start.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}, ${time(start)} to ${time(new Date(endsAt))}`\n}\n\n\u002F** One change waiting for the person: what it is, from Pyas, and their answer. *\u002F\nexport function Approval({ tool, input, onAnswer }: { tool: string; input: Record\u003Cstring, any>; onAnswer: (approved: boolean) => void }) {\n    const [change, setChange] = useState\u003CChange | null>(null)\n    useEffect(() => {\n        fetch('\u002Fapi\u002Fpyas-change', { method: 'POST', body: JSON.stringify({ tool, input }) })\n            .then((res) => res.json())\n            .then(setChange)\n    }, [tool, input])\n\n    if (!change) return \u003Cdiv className=\"approval\">Checking this change...\u003C\u002Fdiv>\n    if (!change.ok) return \u003Cdiv className=\"approval\">This change can't be made: {change.error?.message}\u003C\u002Fdiv>\n\n    const { event } = change\n    const moves = event && input.startsAt && input.endsAt\n    return (\n        \u003Cdiv className=\"approval\">\n            \u003Cdiv className=\"approval-title\">{change.title}\u003C\u002Fdiv>\n            {event && \u003Cdiv className=\"approval-event\">{event.title}\u003C\u002Fdiv>}\n            {event && \u003Cdiv className=\"approval-when\">{moves ? 'Now: ' : ''}{when(event.startsAtLocal, event.endsAtLocal)}\u003C\u002Fdiv>}\n            {moves && \u003Cdiv className=\"approval-when approval-new\">New: {when(input.startsAt, input.endsAt)}\u003C\u002Fdiv>}\n            {!event && input.title && \u003Cdiv className=\"approval-event\">{input.title}\u003C\u002Fdiv>}\n            {!event && input.startsAt && \u003Cdiv className=\"approval-when\">{when(input.startsAt, input.endsAt)}\u003C\u002Fdiv>}\n            \u003Cdiv className=\"buttons\">\n                \u003Cbutton className=\"primary\" onClick={() => onAnswer(true)}>Approve\u003C\u002Fbutton>\n                \u003Cbutton onClick={() => onAnswer(false)}>Decline\u003C\u002Fbutton>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n","app\u002Fapproval.tsx",[85,1702,1703,1707,1711,1727,1731,1763,1767,1776,1786,1797,1807,1823,1827,1831,1863,1879,1932,2019,2023,2027,2032,2101,2140,2152,2192,2210,2217,2232,2236,2268,2310,2314,2329,2355,2361,2375,2399,2428,2483,2524,2562,2608,2623,2662,2690,2698,2707,2712],{"__ignoreMap":104},[108,1704,1705],{"class":110,"line":111},[108,1706,728],{"class":256},[108,1708,1709],{"class":110,"line":210},[108,1710,322],{"emptyLinePlaceholder":321},[108,1712,1713,1715,1718,1720,1723,1725],{"class":110,"line":292},[108,1714,247],{"class":199},[108,1716,1717],{"class":195}," { useEffect",[108,1719,268],{"class":267},[108,1721,1722],{"class":195}," useState } ",[108,1724,253],{"class":199},[108,1726,767],{"class":256},[108,1728,1729],{"class":110,"line":305},[108,1730,322],{"emptyLinePlaceholder":321},[108,1732,1733,1736,1739,1742,1744,1746,1749,1751,1753,1756,1758,1760],{"class":110,"line":318},[108,1734,1735],{"class":199},"interface",[108,1737,1738],{"class":114}," PyasEvent",[108,1740,1741],{"class":195}," { title",[108,1743,343],{"class":199},[108,1745,901],{"class":358},[108,1747,1748],{"class":195},"; startsAtLocal",[108,1750,343],{"class":199},[108,1752,901],{"class":358},[108,1754,1755],{"class":195},"; endsAtLocal",[108,1757,343],{"class":199},[108,1759,901],{"class":358},[108,1761,1762],{"class":195}," }\n",[108,1764,1765],{"class":110,"line":325},[108,1766,322],{"emptyLinePlaceholder":321},[108,1768,1769,1771,1774],{"class":110,"line":352},[108,1770,1735],{"class":199},[108,1772,1773],{"class":114}," Change",[108,1775,1145],{"class":195},[108,1777,1778,1781,1783],{"class":110,"line":374},[108,1779,1780],{"class":195},"    ok",[108,1782,343],{"class":199},[108,1784,1785],{"class":358}," boolean\n",[108,1787,1788,1791,1794],{"class":110,"line":417},[108,1789,1790],{"class":195},"    title",[108,1792,1793],{"class":199},"?:",[108,1795,1796],{"class":358}," string\n",[108,1798,1799,1802,1804],{"class":110,"line":422},[108,1800,1801],{"class":195},"    event",[108,1803,1793],{"class":199},[108,1805,1806],{"class":114}," PyasEvent\n",[108,1808,1809,1812,1814,1817,1819,1821],{"class":110,"line":462},[108,1810,1811],{"class":195},"    error",[108,1813,1793],{"class":199},[108,1815,1816],{"class":195}," { message",[108,1818,343],{"class":199},[108,1820,901],{"class":358},[108,1822,1762],{"class":195},[108,1824,1825],{"class":110,"line":468},[108,1826,700],{"class":195},[108,1828,1829],{"class":110,"line":507},[108,1830,322],{"emptyLinePlaceholder":321},[108,1832,1833,1836,1839,1841,1844,1846,1848,1850,1853,1855,1857,1859,1861],{"class":110,"line":522},[108,1834,1835],{"class":199},"const",[108,1837,1838],{"class":114}," when",[108,1840,362],{"class":199},[108,1842,1843],{"class":195}," (startsAt",[108,1845,343],{"class":199},[108,1847,901],{"class":358},[108,1849,268],{"class":267},[108,1851,1852],{"class":195}," endsAt",[108,1854,343],{"class":199},[108,1856,901],{"class":358},[108,1858,1168],{"class":195},[108,1860,1067],{"class":199},[108,1862,1145],{"class":195},[108,1864,1865,1867,1870,1872,1874,1876],{"class":110,"line":544},[108,1866,355],{"class":199},[108,1868,1869],{"class":358}," start",[108,1871,362],{"class":199},[108,1873,392],{"class":199},[108,1875,480],{"class":114},[108,1877,1878],{"class":195},"(startsAt)\n",[108,1880,1881,1883,1886,1888,1891,1893,1895,1897,1899,1902,1905,1907,1910,1912,1915,1917,1920,1922,1925,1927,1930],{"class":110,"line":569},[108,1882,355],{"class":199},[108,1884,1885],{"class":114}," time",[108,1887,362],{"class":199},[108,1889,1890],{"class":195}," (date",[108,1892,343],{"class":199},[108,1894,480],{"class":114},[108,1896,1168],{"class":195},[108,1898,1067],{"class":199},[108,1900,1901],{"class":358}," date",[108,1903,1904],{"class":114},".toLocaleTimeString",[108,1906,398],{"class":195},[108,1908,1909],{"class":256},"'en-US'",[108,1911,268],{"class":267},[108,1913,1914],{"class":195}," { hour",[108,1916,343],{"class":199},[108,1918,1919],{"class":256}," 'numeric'",[108,1921,268],{"class":267},[108,1923,1924],{"class":195}," minute",[108,1926,343],{"class":199},[108,1928,1929],{"class":256}," '2-digit'",[108,1931,414],{"class":195},[108,1933,1934,1936,1939,1941,1944,1947,1949,1951,1953,1956,1958,1961,1963,1966,1968,1970,1972,1975,1977,1979,1982,1984,1987,1989,1992,1995,1997,2000,2002,2004,2006,2009,2011,2014,2016],{"class":110,"line":587},[108,1935,687],{"class":199},[108,1937,1938],{"class":256}," `",[108,1940,555],{"class":199},[108,1942,1943],{"class":358},"start",[108,1945,1946],{"class":114},".toLocaleDateString",[108,1948,398],{"class":195},[108,1950,1909],{"class":256},[108,1952,268],{"class":267},[108,1954,1955],{"class":195}," { weekday",[108,1957,343],{"class":199},[108,1959,1960],{"class":256}," 'short'",[108,1962,268],{"class":267},[108,1964,1965],{"class":195}," month",[108,1967,343],{"class":199},[108,1969,1960],{"class":256},[108,1971,268],{"class":267},[108,1973,1974],{"class":195}," day",[108,1976,343],{"class":199},[108,1978,1919],{"class":256},[108,1980,1981],{"class":195}," })",[108,1983,561],{"class":199},[108,1985,1986],{"class":256},", ",[108,1988,555],{"class":199},[108,1990,1991],{"class":114},"time",[108,1993,1994],{"class":195},"(start)",[108,1996,561],{"class":199},[108,1998,1999],{"class":256}," to ",[108,2001,555],{"class":199},[108,2003,1991],{"class":114},[108,2005,398],{"class":195},[108,2007,2008],{"class":199},"new",[108,2010,480],{"class":114},[108,2012,2013],{"class":195},"(endsAt))",[108,2015,561],{"class":199},[108,2017,2018],{"class":256},"`\n",[108,2020,2021],{"class":110,"line":608},[108,2022,700],{"class":195},[108,2024,2025],{"class":110,"line":627},[108,2026,322],{"emptyLinePlaceholder":321},[108,2028,2029],{"class":110,"line":633},[108,2030,2031],{"class":206},"\u002F** One change waiting for the person: what it is, from Pyas, and their answer. *\u002F\n",[108,2033,2034,2036,2038,2041,2043,2045,2047,2049,2052,2054,2057,2059,2061,2064,2066,2068,2070,2072,2074,2077,2080,2082,2084,2087,2089,2091,2093,2095,2098],{"class":110,"line":639},[108,2035,328],{"class":199},[108,2037,334],{"class":199},[108,2039,2040],{"class":114}," Approval",[108,2042,1657],{"class":195},[108,2044,268],{"class":267},[108,2046,1626],{"class":195},[108,2048,268],{"class":267},[108,2050,2051],{"class":195}," onAnswer }",[108,2053,343],{"class":199},[108,2055,2056],{"class":195}," { tool",[108,2058,343],{"class":199},[108,2060,901],{"class":358},[108,2062,2063],{"class":195},"; input",[108,2065,343],{"class":199},[108,2067,1309],{"class":114},[108,2069,1312],{"class":195},[108,2071,1315],{"class":358},[108,2073,268],{"class":267},[108,2075,2076],{"class":358}," any",[108,2078,2079],{"class":195},">; ",[108,2081,1326],{"class":114},[108,2083,343],{"class":199},[108,2085,2086],{"class":195}," (approved",[108,2088,343],{"class":199},[108,2090,911],{"class":358},[108,2092,1168],{"class":195},[108,2094,1067],{"class":199},[108,2096,2097],{"class":358}," void",[108,2099,2100],{"class":195}," }) {\n",[108,2102,2103,2105,2107,2110,2112,2115,2117,2119,2121,2123,2126,2129,2132,2135,2138],{"class":110,"line":678},[108,2104,355],{"class":199},[108,2106,667],{"class":195},[108,2108,2109],{"class":358},"change",[108,2111,268],{"class":267},[108,2113,2114],{"class":358}," setChange",[108,2116,815],{"class":195},[108,2118,200],{"class":199},[108,2120,820],{"class":114},[108,2122,1312],{"class":195},[108,2124,2125],{"class":114},"Change",[108,2127,2128],{"class":199}," |",[108,2130,2131],{"class":358}," null",[108,2133,2134],{"class":195},">(",[108,2136,2137],{"class":358},"null",[108,2139,504],{"class":195},[108,2141,2142,2145,2148,2150],{"class":110,"line":684},[108,2143,2144],{"class":114},"    useEffect",[108,2146,2147],{"class":195},"(() ",[108,2149,1067],{"class":199},[108,2151,1145],{"class":195},[108,2153,2154,2157,2159,2162,2164,2167,2169,2172,2174,2177,2179,2182,2185,2187,2189],{"class":110,"line":697},[108,2155,2156],{"class":114},"        fetch",[108,2158,398],{"class":195},[108,2160,2161],{"class":256},"'\u002Fapi\u002Fpyas-change'",[108,2163,268],{"class":267},[108,2165,2166],{"class":195}," { method",[108,2168,343],{"class":199},[108,2170,2171],{"class":256}," 'POST'",[108,2173,268],{"class":267},[108,2175,2176],{"class":195}," body",[108,2178,343],{"class":199},[108,2180,2181],{"class":358}," JSON",[108,2183,2184],{"class":114},".stringify",[108,2186,1657],{"class":195},[108,2188,268],{"class":267},[108,2190,2191],{"class":195}," input }) })\n",[108,2193,2194,2197,2200,2202,2205,2207],{"class":110,"line":1023},[108,2195,2196],{"class":114},"            .then",[108,2198,2199],{"class":195},"((res) ",[108,2201,1067],{"class":199},[108,2203,2204],{"class":358}," res",[108,2206,456],{"class":114},[108,2208,2209],{"class":195},"())\n",[108,2211,2212,2214],{"class":110,"line":1028},[108,2213,2196],{"class":114},[108,2215,2216],{"class":195},"(setChange)\n",[108,2218,2219,2222,2224,2227,2229],{"class":110,"line":1033},[108,2220,2221],{"class":195},"    }",[108,2223,268],{"class":267},[108,2225,2226],{"class":195}," [tool",[108,2228,268],{"class":267},[108,2230,2231],{"class":195}," input])\n",[108,2233,2234],{"class":110,"line":1041},[108,2235,322],{"emptyLinePlaceholder":321},[108,2237,2238,2240,2242,2244,2247,2249,2251,2253,2256,2258,2261,2264,2266],{"class":110,"line":1053},[108,2239,377],{"class":199},[108,2241,380],{"class":195},[108,2243,383],{"class":199},[108,2245,2246],{"class":195},"change) ",[108,2248,389],{"class":199},[108,2250,1173],{"class":195},[108,2252,1078],{"class":256},[108,2254,2255],{"class":114}," className",[108,2257,200],{"class":199},[108,2259,2260],{"class":256},"\"approval\"",[108,2262,2263],{"class":195},">Checking this change...\u003C\u002F",[108,2265,1078],{"class":256},[108,2267,1050],{"class":195},[108,2269,2270,2272,2274,2276,2278,2281,2283,2285,2287,2289,2291,2293,2296,2298,2300,2303,2306,2308],{"class":110,"line":1072},[108,2271,377],{"class":199},[108,2273,380],{"class":195},[108,2275,383],{"class":199},[108,2277,2109],{"class":358},[108,2279,2280],{"class":195},".ok) ",[108,2282,389],{"class":199},[108,2284,1173],{"class":195},[108,2286,1078],{"class":256},[108,2288,2255],{"class":114},[108,2290,200],{"class":199},[108,2292,2260],{"class":256},[108,2294,2295],{"class":195},">This change can't be made: {",[108,2297,2109],{"class":358},[108,2299,713],{"class":195},[108,2301,2302],{"class":358},"error",[108,2304,2305],{"class":195},"?.message}\u003C\u002F",[108,2307,1078],{"class":256},[108,2309,1050],{"class":195},[108,2311,2312],{"class":110,"line":1120},[108,2313,322],{"emptyLinePlaceholder":321},[108,2315,2316,2318,2320,2322,2324,2326],{"class":110,"line":1148},[108,2317,355],{"class":199},[108,2319,427],{"class":195},[108,2321,1438],{"class":358},[108,2323,848],{"class":195},[108,2325,200],{"class":199},[108,2327,2328],{"class":195}," change\n",[108,2330,2331,2333,2336,2338,2341,2343,2345,2348,2350,2352],{"class":110,"line":1204},[108,2332,355],{"class":199},[108,2334,2335],{"class":358}," moves",[108,2337,362],{"class":199},[108,2339,2340],{"class":195}," event ",[108,2342,1228],{"class":199},[108,2344,1626],{"class":358},[108,2346,2347],{"class":195},".startsAt ",[108,2349,1228],{"class":199},[108,2351,1626],{"class":358},[108,2353,2354],{"class":195},".endsAt\n",[108,2356,2357,2359],{"class":110,"line":1255},[108,2358,687],{"class":199},[108,2360,1038],{"class":195},[108,2362,2363,2365,2367,2369,2371,2373],{"class":110,"line":1355},[108,2364,1044],{"class":195},[108,2366,1078],{"class":256},[108,2368,2255],{"class":114},[108,2370,200],{"class":199},[108,2372,2260],{"class":256},[108,2374,1050],{"class":195},[108,2376,2377,2379,2381,2383,2385,2388,2390,2392,2395,2397],{"class":110,"line":1361},[108,2378,1395],{"class":195},[108,2380,1078],{"class":256},[108,2382,2255],{"class":114},[108,2384,200],{"class":199},[108,2386,2387],{"class":256},"\"approval-title\"",[108,2389,1192],{"class":195},[108,2391,2109],{"class":358},[108,2393,2394],{"class":195},".title}\u003C\u002F",[108,2396,1078],{"class":256},[108,2398,1050],{"class":195},[108,2400,2401,2404,2406,2408,2410,2412,2414,2417,2419,2421,2423,2425],{"class":110,"line":1370},[108,2402,2403],{"class":195},"            {event ",[108,2405,1228],{"class":199},[108,2407,1173],{"class":195},[108,2409,1078],{"class":256},[108,2411,2255],{"class":114},[108,2413,200],{"class":199},[108,2415,2416],{"class":256},"\"approval-event\"",[108,2418,1192],{"class":195},[108,2420,1438],{"class":358},[108,2422,2394],{"class":195},[108,2424,1078],{"class":256},[108,2426,2427],{"class":195},">}\n",[108,2429,2430,2432,2434,2436,2438,2440,2442,2445,2448,2450,2453,2455,2458,2461,2464,2466,2468,2471,2473,2476,2479,2481],{"class":110,"line":1376},[108,2431,2403],{"class":195},[108,2433,1228],{"class":199},[108,2435,1173],{"class":195},[108,2437,1078],{"class":256},[108,2439,2255],{"class":114},[108,2441,200],{"class":199},[108,2443,2444],{"class":256},"\"approval-when\"",[108,2446,2447],{"class":195},">{moves ",[108,2449,929],{"class":199},[108,2451,2452],{"class":256}," 'Now: '",[108,2454,935],{"class":199},[108,2456,2457],{"class":256}," ''",[108,2459,2460],{"class":195},"}{",[108,2462,2463],{"class":114},"when",[108,2465,398],{"class":195},[108,2467,1438],{"class":358},[108,2469,2470],{"class":195},".startsAtLocal",[108,2472,268],{"class":267},[108,2474,2475],{"class":358}," event",[108,2477,2478],{"class":195},".endsAtLocal)}\u003C\u002F",[108,2480,1078],{"class":256},[108,2482,2427],{"class":195},[108,2484,2485,2488,2490,2492,2494,2496,2498,2501,2504,2506,2508,2510,2513,2515,2517,2520,2522],{"class":110,"line":1386},[108,2486,2487],{"class":195},"            {moves ",[108,2489,1228],{"class":199},[108,2491,1173],{"class":195},[108,2493,1078],{"class":256},[108,2495,2255],{"class":114},[108,2497,200],{"class":199},[108,2499,2500],{"class":256},"\"approval-when approval-new\"",[108,2502,2503],{"class":195},">New: {",[108,2505,2463],{"class":114},[108,2507,398],{"class":195},[108,2509,807],{"class":358},[108,2511,2512],{"class":195},".startsAt",[108,2514,268],{"class":267},[108,2516,1626],{"class":358},[108,2518,2519],{"class":195},".endsAt)}\u003C\u002F",[108,2521,1078],{"class":256},[108,2523,2427],{"class":195},[108,2525,2526,2528,2530,2533,2535,2537,2540,2542,2544,2546,2548,2550,2552,2554,2556,2558,2560],{"class":110,"line":1392},[108,2527,1056],{"class":195},[108,2529,383],{"class":199},[108,2531,2532],{"class":195},"event ",[108,2534,1228],{"class":199},[108,2536,1626],{"class":358},[108,2538,2539],{"class":195},".title ",[108,2541,1228],{"class":199},[108,2543,1173],{"class":195},[108,2545,1078],{"class":256},[108,2547,2255],{"class":114},[108,2549,200],{"class":199},[108,2551,2416],{"class":256},[108,2553,1192],{"class":195},[108,2555,807],{"class":358},[108,2557,2394],{"class":195},[108,2559,1078],{"class":256},[108,2561,2427],{"class":195},[108,2563,2564,2566,2568,2570,2572,2574,2576,2578,2580,2582,2584,2586,2588,2590,2592,2594,2596,2598,2600,2602,2604,2606],{"class":110,"line":1409},[108,2565,1056],{"class":195},[108,2567,383],{"class":199},[108,2569,2532],{"class":195},[108,2571,1228],{"class":199},[108,2573,1626],{"class":358},[108,2575,2347],{"class":195},[108,2577,1228],{"class":199},[108,2579,1173],{"class":195},[108,2581,1078],{"class":256},[108,2583,2255],{"class":114},[108,2585,200],{"class":199},[108,2587,2444],{"class":256},[108,2589,1192],{"class":195},[108,2591,2463],{"class":114},[108,2593,398],{"class":195},[108,2595,807],{"class":358},[108,2597,2512],{"class":195},[108,2599,268],{"class":267},[108,2601,1626],{"class":358},[108,2603,2519],{"class":195},[108,2605,1078],{"class":256},[108,2607,2427],{"class":195},[108,2609,2610,2612,2614,2616,2618,2621],{"class":110,"line":1460},[108,2611,1395],{"class":195},[108,2613,1078],{"class":256},[108,2615,2255],{"class":114},[108,2617,200],{"class":199},[108,2619,2620],{"class":256},"\"buttons\"",[108,2622,1050],{"class":195},[108,2624,2625,2627,2630,2632,2634,2637,2640,2642,2645,2647,2650,2652,2655,2658,2660],{"class":110,"line":1470},[108,2626,1075],{"class":195},[108,2628,2629],{"class":256},"button",[108,2631,2255],{"class":114},[108,2633,200],{"class":199},[108,2635,2636],{"class":256},"\"primary\"",[108,2638,2639],{"class":114}," onClick",[108,2641,200],{"class":199},[108,2643,2644],{"class":195},"{() ",[108,2646,1067],{"class":199},[108,2648,2649],{"class":114}," onAnswer",[108,2651,398],{"class":195},[108,2653,2654],{"class":358},"true",[108,2656,2657],{"class":195},")}>Approve\u003C\u002F",[108,2659,2629],{"class":256},[108,2661,1050],{"class":195},[108,2663,2664,2666,2668,2670,2672,2674,2676,2678,2680,2683,2686,2688],{"class":110,"line":1480},[108,2665,1075],{"class":195},[108,2667,2629],{"class":256},[108,2669,2639],{"class":114},[108,2671,200],{"class":199},[108,2673,2644],{"class":195},[108,2675,1067],{"class":199},[108,2677,2649],{"class":114},[108,2679,398],{"class":195},[108,2681,2682],{"class":358},"false",[108,2684,2685],{"class":195},")}>Decline\u003C\u002F",[108,2687,2629],{"class":256},[108,2689,1050],{"class":195},[108,2691,2692,2694,2696],{"class":110,"line":1486},[108,2693,1463],{"class":195},[108,2695,1078],{"class":256},[108,2697,1050],{"class":195},[108,2699,2701,2703,2705],{"class":110,"line":2700},46,[108,2702,1473],{"class":195},[108,2704,1078],{"class":256},[108,2706,1050],{"class":195},[108,2708,2710],{"class":110,"line":2709},47,[108,2711,1483],{"class":195},[108,2713,2715],{"class":110,"line":2714},48,[108,2716,700],{"class":195},[78,2718,2719],{},"The class names are for your own styles. With some styling, a request to move a meeting looks like this:",[2721,2722],"screenshot",{"alt":2723,"caption":2724,"height":2725,"src":2726,"width":2727},"A chat. The person asked to move their 1:1 with Sarah to 4:30 PM. A card shows the event's current time and the new time, with Approve and Decline buttons.","The current time comes from Pyas. The new time is what the model asked for.",399,"\u002Fimages\u002Fapproval-in-your-app.png",660,[156,2729,64],{"id":63},[2731,2732,2733,2742,2745,2753],"ol",{},[2734,2735,2736,2737,2739,2740,713],"li",{},"Run your app with a test key, and with ",[85,2738,223],{}," returning ",[85,2741,231],{},[2734,2743,2744],{},"Ask \"What's on my calendar tomorrow?\". The assistant reads Avery's calendar without asking.",[2734,2746,2747,2748,2752],{},"Ask \"Book a 30 minute meeting tomorrow afternoon when I'm free.\" A card asks you to approve the new event. Select ",[2749,2750,2751],"strong",{},"Approve",", and the event is on the calendar.",[2734,2754,2755,2756,2759],{},"Ask it to delete that meeting, and select ",[2749,2757,2758],{},"Decline",". The meeting stays, and the assistant says it left it as it was.",[78,2761,2762,2763,713],{},"To go live, see ",[90,2764,2766],{"href":2765},"\u002Fai-in-your-app#go-live","Go live",[2768,2769,2770],"style",{},"html pre.shiki code .s1IGz, html code.shiki .s1IGz{--shiki-default:var(--shiki-token-function)}html pre.shiki code .sTB4W, html code.shiki .sTB4W{--shiki-default:var(--shiki-token-string)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sqSWz, html code.shiki .sqSWz{--shiki-default:var(--shiki-foreground)}html pre.shiki code .sag7P, html code.shiki .sag7P{--shiki-default:var(--shiki-token-keyword)}html pre.shiki code .ssgCD, html code.shiki .ssgCD{--shiki-default:var(--shiki-token-comment)}html pre.shiki code .sk1Au, html code.shiki .sk1Au{--shiki-default:var(--shiki-token-string-expression)}html pre.shiki code .sEEt9, html code.shiki .sEEt9{--shiki-default:var(--shiki-token-punctuation)}html pre.shiki code .sXULy, html code.shiki .sXULy{--shiki-default:var(--shiki-token-constant)}",{"title":104,"searchDepth":292,"depth":292,"links":2772},[2773,2774,2775,2776,2777],{"id":51,"depth":210,"text":52},{"id":54,"depth":210,"text":55},{"id":57,"depth":210,"text":58},{"id":60,"depth":210,"text":61},{"id":63,"depth":210,"text":64},"md",false,{},"\u002Fvercel-ai-sdk","---\ntitle: Vercel AI SDK\ndescription: Add a calendar assistant to a Next.js app with the Vercel AI SDK and Pyas. It reads each user's Google and Outlook calendars, and asks them before it books, moves or deletes anything.\nsections:\n  - { id: install, title: Install }\n  - { id: the-chat-route, title: The chat route }\n  - { id: the-chat-page, title: The chat page }\n  - { id: show-the-change, title: Show the change }\n  - { id: try-it, title: Try it }\n---\n\n# Vercel AI SDK\n\n::lead\nThis guide builds a chat in a Next.js app where your users ask about their calendars and book meetings. The assistant uses Pyas's tools for the signed-in user only, and each change waits for them to approve it.\n::\n\nYou need the AI SDK 6 or later. The code here was tested with Next.js 16, the AI SDK 7 and OpenAI's `gpt-5.5`. Any model that calls tools works. See [AI in your app](\u002Fai-in-your-app) for how the tools work.\n\nTo start from a working app with the code on this page, create it with the Pyas CLI:\n\n```bash\nnpx @pyas\u002Fcli init --template nextjs-ai-chat my-chat\n```\n\nSet `OPENAI_API_KEY` in its `.env.local`, then run `npm install` and `npm run dev`. If you're signed in with `pyas login`, your test key is already there.\n\n## Install\n\n```bash\nnpm install @pyas\u002Fai-tools ai @ai-sdk\u002Freact @ai-sdk\u002Fopenai\n```\n\nSet these on your server:\n\n```bash\nPYAS_API_KEY=pyas_sk_test_...   # a test key from API keys in the Pyas dashboard\nOPENAI_API_KEY=sk-...\n```\n\nThe examples use a `currentUserId` function for your sign-in. It returns the signed-in person's ID in your app, the same ID you saved as `subjectId` when they connected their calendar. To try the guide in the sandbox, have it return `'sandbox_user_1'`, the sandbox person Avery Brooks.\n\n## The chat route\n\n```ts [app\u002Fapi\u002Fchat\u002Froute.ts]\nimport { openai } from '@ai-sdk\u002Fopenai'\nimport { convertToModelMessages, stepCountIs, streamText, type UIMessage } from 'ai'\nimport { pyasTools } from '@pyas\u002Fai-tools\u002Fvercel'\nimport { currentUserId } from '@\u002Flib\u002Fuser'\n\nexport async function POST(req: Request) {\n    const userId = await currentUserId(req)\n    if (!userId) return new Response('Sign in to use your calendar.', { status: 401 })\n\n    const { messages }: { messages: UIMessage[] } = await req.json()\n    \u002F\u002F Today's date lets the model work out \"tomorrow\".\n    const today = new Date().toISOString().slice(0, 10)\n    const result = streamText({\n        model: openai('gpt-5.5'),\n        system: `You help the user manage their calendar. Today is ${today}.`,\n        messages: await convertToModelMessages(messages),\n        tools: pyasTools({ subjectId: userId }),\n        stopWhen: stepCountIs(10),\n        \u002F\u002F With OpenAI: each request carries the model's reasoning, so a chat that goes on after\n        \u002F\u002F an approval never points at reasoning OpenAI no longer has.\n        providerOptions: { openai: { store: false, include: ['reasoning.encrypted_content'] } },\n    })\n    return result.toUIMessageStreamResponse()\n}\n```\n\n`pyasTools({ subjectId: userId })` gives the model the tools for this person's calendars only. Each tool that changes a calendar is marked as needing approval, so the AI SDK stops at it and sends the change to the page instead of running it.\n\nWith a model from another provider, leave out `providerOptions`.\n\n## The chat page\n\n```tsx [app\u002Fpage.tsx]\n'use client'\n\nimport { useChat } from '@ai-sdk\u002Freact'\nimport { lastAssistantMessageIsCompleteWithApprovalResponses } from 'ai'\nimport { useState } from 'react'\nimport { Approval } from '.\u002Fapproval'\n\nexport default function Chat() {\n    const [input, setInput] = useState('')\n    const { messages, sendMessage, addToolApprovalResponse } = useChat({\n        \u002F\u002F Once the person has answered every change, the chat carries on by itself.\n        sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,\n    })\n\n    \u002F\u002F A declined change never runs. The reason tells the model not to ask for it again.\n    function answer(id: string, approved: boolean) {\n        const reason = approved ? undefined : \"The user declined this change. Leave it as it is, and don't ask again unless they do.\"\n        addToolApprovalResponse({ id, approved, reason })\n    }\n\n    function send(event: React.FormEvent) {\n        event.preventDefault()\n        sendMessage({ text: input })\n        setInput('')\n    }\n\n    return (\n        \u003Cmain>\n            {messages.map((message) => (\n                \u003Cdiv key={message.id} className={`message ${message.role}`}>\n                    {message.parts.map((part, index) => {\n                        if (part.type === 'text') return \u003Cdiv key={index} className=\"bubble\">{part.text}\u003C\u002Fdiv>\n                        if (part.type.startsWith('tool-') && 'state' in part && part.state === 'approval-requested') {\n                            return \u003CApproval key={index} tool={part.type.slice(5)} input={part.input as Record\u003Cstring, unknown>} onAnswer={(approved) => answer(part.approval.id, approved)} \u002F>\n                        }\n                        return null\n                    })}\n                \u003C\u002Fdiv>\n            ))}\n            \u003Cform onSubmit={send}>\n                \u003Cinput value={input} onChange={(event) => setInput(event.target.value)} placeholder=\"Ask about your calendar\" \u002F>\n            \u003C\u002Fform>\n        \u003C\u002Fmain>\n    )\n}\n```\n\nA tool part in the `approval-requested` state is a change waiting for the person. When they choose, `addToolApprovalResponse` records it, and the chat sends it to your route, where the AI SDK runs the approved change or tells the model it was declined.\n\nPass a `reason` when the person declines. Without one, the model may ask for the same change again.\n\n## Show the change\n\nThe tool part holds what the model asked for, such as an event ID to delete. To show the person the event itself, ask your server. `describePyasChange` reads the event from Pyas, with the same checks as the tools:\n\n```ts [app\u002Fapi\u002Fpyas-change\u002Froute.ts]\nimport { describePyasChange } from '@pyas\u002Fai-tools\u002Fvercel'\nimport { currentUserId } from '@\u002Flib\u002Fuser'\n\n\u002F** What a change waiting for approval is about, from Pyas's own data, for the approval card. *\u002F\nexport async function POST(req: Request) {\n    const userId = await currentUserId(req)\n    if (!userId) return new Response('Sign in to use your calendar.', { status: 401 })\n    const { tool, input } = await req.json()\n    return Response.json(await describePyasChange({ tool, input }, { subjectId: userId }))\n}\n```\n\nIt returns `{ ok: true, title, event }`, where `title` says what the change does, such as \"Delete an event\", and `event` is the event as it is now in Pyas. A new event has no `event`: show the title and times from the input. When the change could never run, such as for an event that doesn't exist, it returns `{ ok: false, error }`.\n\n```tsx [app\u002Fapproval.tsx]\n'use client'\n\nimport { useEffect, useState } from 'react'\n\ninterface PyasEvent { title: string; startsAtLocal: string; endsAtLocal: string }\n\ninterface Change {\n    ok: boolean\n    title?: string\n    event?: PyasEvent\n    error?: { message: string }\n}\n\nconst when = (startsAt: string, endsAt: string) => {\n    const start = new Date(startsAt)\n    const time = (date: Date) => date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })\n    return `${start.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })}, ${time(start)} to ${time(new Date(endsAt))}`\n}\n\n\u002F** One change waiting for the person: what it is, from Pyas, and their answer. *\u002F\nexport function Approval({ tool, input, onAnswer }: { tool: string; input: Record\u003Cstring, any>; onAnswer: (approved: boolean) => void }) {\n    const [change, setChange] = useState\u003CChange | null>(null)\n    useEffect(() => {\n        fetch('\u002Fapi\u002Fpyas-change', { method: 'POST', body: JSON.stringify({ tool, input }) })\n            .then((res) => res.json())\n            .then(setChange)\n    }, [tool, input])\n\n    if (!change) return \u003Cdiv className=\"approval\">Checking this change...\u003C\u002Fdiv>\n    if (!change.ok) return \u003Cdiv className=\"approval\">This change can't be made: {change.error?.message}\u003C\u002Fdiv>\n\n    const { event } = change\n    const moves = event && input.startsAt && input.endsAt\n    return (\n        \u003Cdiv className=\"approval\">\n            \u003Cdiv className=\"approval-title\">{change.title}\u003C\u002Fdiv>\n            {event && \u003Cdiv className=\"approval-event\">{event.title}\u003C\u002Fdiv>}\n            {event && \u003Cdiv className=\"approval-when\">{moves ? 'Now: ' : ''}{when(event.startsAtLocal, event.endsAtLocal)}\u003C\u002Fdiv>}\n            {moves && \u003Cdiv className=\"approval-when approval-new\">New: {when(input.startsAt, input.endsAt)}\u003C\u002Fdiv>}\n            {!event && input.title && \u003Cdiv className=\"approval-event\">{input.title}\u003C\u002Fdiv>}\n            {!event && input.startsAt && \u003Cdiv className=\"approval-when\">{when(input.startsAt, input.endsAt)}\u003C\u002Fdiv>}\n            \u003Cdiv className=\"buttons\">\n                \u003Cbutton className=\"primary\" onClick={() => onAnswer(true)}>Approve\u003C\u002Fbutton>\n                \u003Cbutton onClick={() => onAnswer(false)}>Decline\u003C\u002Fbutton>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n```\n\nThe class names are for your own styles. With some styling, a request to move a meeting looks like this:\n\n::screenshot{src=\"\u002Fimages\u002Fapproval-in-your-app.png\" alt=\"A chat. The person asked to move their 1:1 with Sarah to 4:30 PM. A card shows the event's current time and the new time, with Approve and Decline buttons.\" width=\"660\" height=\"399\" caption=\"The current time comes from Pyas. The new time is what the model asked for.\"}\n::\n\n## Try it\n\n1. Run your app with a test key, and with `currentUserId` returning `'sandbox_user_1'`.\n2. Ask \"What's on my calendar tomorrow?\". The assistant reads Avery's calendar without asking.\n3. Ask \"Book a 30 minute meeting tomorrow afternoon when I'm free.\" A card asks you to approve the new event. Select **Approve**, and the event is on the calendar.\n4. Ask it to delete that meeting, and select **Decline**. The meeting stays, and the assistant says it left it as it was.\n\nTo go live, see [Go live](\u002Fai-in-your-app#go-live).\n",{"title":47,"description":48},null,"4cXOoL1LqtyYWwUUBUzIQgQFRU4xedoqSkY5WkeEaEQ",1794697271138]