[{"data":1,"prerenderedAt":22894},["ShallowReactive",2],{"home-posts":3,"home-projects":22754},[4,3099,10714,18318],{"id":5,"title":6,"body":7,"cover":3083,"date":3084,"description":3085,"draft":3086,"extension":3087,"hashnodeId":3088,"meta":3089,"navigation":291,"path":3090,"seo":3091,"slug":3092,"stem":3092,"tags":3093,"__hash__":3098},"posts\u002Fbuilding-brew-haven-ab-testing-my-coffee-shop-dreams-with-devcycle.md","Building Brew Haven: A\u002FB Testing My Coffee Shop Dreams with DevCycle",{"type":8,"value":9,"toc":3068},"minimark",[10,14,17,23,28,31,36,39,43,53,63,67,70,86,90,98,101,112,115,119,122,206,209,213,216,236,239,243,246,249,253,268,690,697,906,909,913,924,927,1440,1443,2102,2105,2309,2312,2818,2821,3009,3020,3024,3027,3030,3044,3047,3050,3053,3056,3064],[11,12,13],"p",{},"Do you love coffee? As developers, many of us jokingly claim to be \"powered by coffee\", and the thought of opening a quaint coffee shop someday often lingers in the back of our minds — perhaps as a post-dev career dream.",[11,15,16],{},"When brainstorming ideas for a fun project to showcase feature flags, this coffee shop fantasy kept nudging me: \"Pick me! Build me!\". So, I decided to indulge that thought and create Brew Haven, a dummy coffee shop app that explores the power of feature flags.",[18,19,20],"blockquote",{},[11,21,22],{},"This project was originally created as part of a dev challenge to showcase the power of feature flags in an engaging way.",[24,25,27],"h2",{"id":26},"project-overview","Project Overview",[11,29,30],{},"As you might have guessed, I built a playground to explore the power of feature flags, packaged as a coffee shop app. The app features customizable menus, seasonal items, and dynamic A\u002FB testing for promotions using the DevCycle SDK. It also leverages the DevCycle Management APIs to power an admin panel, giving shop managers full control over these features in real time.",[32,33,35],"h3",{"id":34},"demo","Demo",[11,37,38],{},"The following video gives a short walkthrough of the app.",[40,41],"media-embed",{"url":42},"https:\u002F\u002Fyoutu.be\u002FXp6i7GXl4hM",[11,44,45,46],{},"You can try out the live app here: ",[47,48,52],"a",{"href":49,"rel":50},"https:\u002F\u002Fbrewwhaven.netlify.app",[51],"nofollow","Brew Haven",[18,54,55],{},[11,56,57,58,62],{},"Note: The admin page uses a dummy password auth. Use ",[59,60,61],"code",{},"admin@123"," passowrd to view the admin panel and play around with the available feature flags.",[32,64,66],{"id":65},"technologies-used","Technologies Used",[11,68,69],{},"To bring Brew Haven to life, I used the following:",[71,72,73,77,80,83],"ul",{},[74,75,76],"li",{},"React with Vite for a fast and modular frontend.",[74,78,79],{},"Shadcn UI for styling and components.",[74,81,82],{},"Netlify Functions for secure serverless DevCycle Management API calls.",[74,84,85],{},"DevCycle SDK for feature flag integration on the client side.",[24,87,89],{"id":88},"what-is-devcycle","What is DevCycle?",[11,91,92,93,97],{},"DevCycle is a feature management platform that helps developers experiment with new features, run A\u002FB tests, and gradually roll out updates without downtime. It uses ",[94,95,96],"strong",{},"feature flags","—switches in your code that let you turn specific features on or off in real time.",[11,99,100],{},"With DevCycle, you can:",[71,102,103,106,109],{},[74,104,105],{},"Deploy features safely using controlled rollouts.",[74,107,108],{},"Customize user experiences through A\u002FB testing.",[74,110,111],{},"Quickly respond to user feedback without redeploying your code.",[11,113,114],{},"DevCycle integrates seamlessly into your development process, making feature management simple and efficient. In Brew Haven, I used it to power dynamic menu customization, promotions, and more.",[32,116,118],{"id":117},"how-does-devcycle-work","How Does DevCycle Work?",[11,120,121],{},"DevCycle is a powerful tool designed to simplify the management of feature flags and provide advanced functionality for tailoring your software’s behavior. Here’s an overview of how it works:",[123,124,125,164,198],"ol",{},[74,126,127,130,133,134,137,138],{},[94,128,129],{},"Feature Flags and Feature Types",[131,132],"br",{},"\nAt the heart of DevCycle are ",[94,135,136],{},"features",", which can have one or more toggles (or flags). Features are categorized into four types, tailored for specific use cases:",[71,139,140,146,152,158],{},[74,141,142,145],{},[94,143,144],{},"Release",": Designed for separating code deployment from feature release. This enables merging incomplete or in-progress code into production without affecting end users until it’s toggled on.",[74,147,148,151],{},[94,149,150],{},"Ops",": Helps ensure system safety during feature rollouts, with built-in mechanisms for gradual rollouts or emergency kill switches.",[74,153,154,157],{},[94,155,156],{},"Experiment",": Ideal for A\u002FB or multivariate testing. It lets you distribute users across variations and track outcomes to make data-driven decisions.",[74,159,160,163],{},[94,161,162],{},"Permission",": Gating features based on user attributes, such as subscription plans or roles, allowing granular control over feature access.",[74,165,166,169,171,172,175,176,179,180,183,184,186,189,190],{},[94,167,168],{},"Variables and Variations",[131,170],{},"\nEach feature flag can have associated ",[94,173,174],{},"variables",", representing configurable values. For example, a flag for a promotion might include variables like ",[59,177,178],{},"discountPercentage"," or ",[59,181,182],{},"minimumOrderValue",".",[131,185],{},[94,187,188],{},"Variations"," are pre-defined combinations of variable values, which dictate how the feature behaves. For instance:",[71,191,192,195],{},[74,193,194],{},"Variation A might offer a 10% discount.",[74,196,197],{},"Variation B might provide a 15% discount with a higher order value threshold.",[74,199,200,203,205],{},[94,201,202],{},"Targeting Rules and Rollouts",[131,204],{},"\nDevCycle allows you to define rules that control which users see specific variations. These rules can be based on user properties (like location or plan type) or random distribution for A\u002FB testing. Features can also be rolled out gradually, allowing you to monitor performance and ensure stability before scaling.",[11,207,208],{},"This structured approach ensures safe experimentation, seamless feature rollouts, and precise customization—all with minimal risk to your users’ experience.",[32,210,212],{"id":211},"feature-flags-in-brew-haven","Feature Flags in Brew Haven",[11,214,215],{},"Brew Haven uses the following feature flags to make the app dynamic and customizable:",[123,217,218,224,230],{},[74,219,220,223],{},[94,221,222],{},"Coffee Menu",": Feature flags control menu customization, seasonal items, and order personalization.",[74,225,226,229],{},[94,227,228],{},"Payment & Ordering",": Enable or disable online payments, loyalty points, and live order tracking with simple toggles.",[74,231,232,235],{},[94,233,234],{},"A\u002FB Testing Promotions",": Run experiments on discounts and promotional offers to optimize customer engagement.",[11,237,238],{},"The first two are release type feature flags, while the last one is an experiment type feature. These flags not only made development faster but also showcased the versatility of DevCycle.",[24,240,242],{"id":241},"integrating-devcycle","Integrating DevCycle",[11,244,245],{},"The source code of the app is open source, and is available on GitHub. We’ll briefly look at how to integrate and use DevCycle in a React App.",[40,247],{"url":248},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fbrew-haven",[32,250,252],{"id":251},"client-side-usage-of-devcycle-sdk","Client Side Usage of DevCycle SDK",[11,254,255,256,259,260,263,264,267],{},"After adding the ",[59,257,258],{},"DevCycle React SDK"," dependency, we first initialize the ",[59,261,262],{},"DevCycleProvider"," in the ",[59,265,266],{},"App.tsx"," file as shown below:",[269,270,275],"pre",{"className":271,"code":272,"language":273,"meta":274,"style":274},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F src\u002FApp.tsx\n\nimport { withDevCycleProvider } from \"@devcycle\u002Freact-client-sdk\";\n\u002F\u002F ...\n\nfunction App() {\n  return (\n    \u003CThemeProvider defaultTheme=\"system\" storageKey=\"coffee-shop-ui-theme\">\n      \u003CRouter>\n        \u003CLayout>\n          \u003CRoutes>\n            \u003CRoute path=\"\u002F\" element={\u003CHome \u002F>} \u002F>\n            \u003CRoute path=\"\u002Fmenu\" element={\u003CMenu \u002F>} \u002F>\n            \u003CRoute path=\"\u002Fcheckout\" element={\u003CCheckout \u002F>} \u002F>\n            \u003CRoute path=\"\u002Forders\" element={\u003COrders \u002F>} \u002F>\n            \u003CRoute\n              path=\"\u002Fadmin\"\n              element={\n                \u003CProtectedRoute>\n                  \u003CAdmin \u002F>\n                \u003C\u002FProtectedRoute>\n              }\n            \u002F>\n          \u003C\u002FRoutes>\n        \u003C\u002FLayout>\n      \u003C\u002FRouter>\n      \u003CToaster \u002F>\n    \u003C\u002FThemeProvider>\n  );\n}\n\nexport default withDevCycleProvider({\n  sdkKey: import.meta.env.VITE_DEVCYCLE_CLIENT_SDK_KEY,\n  options: {\n    logLevel: \"debug\",\n  },\n})(App);\n","typescript","",[59,276,277,286,293,314,320,325,338,347,373,384,395,406,431,452,473,494,503,514,525,531,537,543,549,555,565,575,585,595,606,612,618,623,638,661,667,678,684],{"__ignoreMap":274},[278,279,282],"span",{"class":280,"line":281},"line",1,[278,283,285],{"class":284},"sJ8bj","\u002F\u002F src\u002FApp.tsx\n",[278,287,289],{"class":280,"line":288},2,[278,290,292],{"emptyLinePlaceholder":291},true,"\n",[278,294,296,300,304,307,311],{"class":280,"line":295},3,[278,297,299],{"class":298},"szBVR","import",[278,301,303],{"class":302},"sVt8B"," { withDevCycleProvider } ",[278,305,306],{"class":298},"from",[278,308,310],{"class":309},"sZZnC"," \"@devcycle\u002Freact-client-sdk\"",[278,312,313],{"class":302},";\n",[278,315,317],{"class":280,"line":316},4,[278,318,319],{"class":284},"\u002F\u002F ...\n",[278,321,323],{"class":280,"line":322},5,[278,324,292],{"emptyLinePlaceholder":291},[278,326,328,331,335],{"class":280,"line":327},6,[278,329,330],{"class":298},"function",[278,332,334],{"class":333},"sScJk"," App",[278,336,337],{"class":302},"() {\n",[278,339,341,344],{"class":280,"line":340},7,[278,342,343],{"class":298},"  return",[278,345,346],{"class":302}," (\n",[278,348,350,353,356,359,362,365,367,370],{"class":280,"line":349},8,[278,351,352],{"class":298},"    \u003C",[278,354,355],{"class":302},"ThemeProvider defaultTheme",[278,357,358],{"class":298},"=",[278,360,361],{"class":309},"\"system\"",[278,363,364],{"class":302}," storageKey",[278,366,358],{"class":298},[278,368,369],{"class":309},"\"coffee-shop-ui-theme\"",[278,371,372],{"class":298},">\n",[278,374,376,379,382],{"class":280,"line":375},9,[278,377,378],{"class":302},"      \u003C",[278,380,381],{"class":333},"Router",[278,383,372],{"class":302},[278,385,387,390,393],{"class":280,"line":386},10,[278,388,389],{"class":302},"        \u003C",[278,391,392],{"class":333},"Layout",[278,394,372],{"class":302},[278,396,398,401,404],{"class":280,"line":397},11,[278,399,400],{"class":302},"          \u003C",[278,402,403],{"class":333},"Routes",[278,405,372],{"class":302},[278,407,409,412,415,417,420,423,425,428],{"class":280,"line":408},12,[278,410,411],{"class":298},"            \u003C",[278,413,414],{"class":302},"Route path",[278,416,358],{"class":298},[278,418,419],{"class":309},"\"\u002F\"",[278,421,422],{"class":302}," element",[278,424,358],{"class":298},[278,426,427],{"class":302},"{\u003CHome \u002F>} ",[278,429,430],{"class":298},"\u002F>\n",[278,432,434,436,438,440,443,445,447,450],{"class":280,"line":433},13,[278,435,411],{"class":298},[278,437,414],{"class":302},[278,439,358],{"class":298},[278,441,442],{"class":309},"\"\u002Fmenu\"",[278,444,422],{"class":302},[278,446,358],{"class":298},[278,448,449],{"class":302},"{\u003CMenu \u002F>} ",[278,451,430],{"class":298},[278,453,455,457,459,461,464,466,468,471],{"class":280,"line":454},14,[278,456,411],{"class":298},[278,458,414],{"class":302},[278,460,358],{"class":298},[278,462,463],{"class":309},"\"\u002Fcheckout\"",[278,465,422],{"class":302},[278,467,358],{"class":298},[278,469,470],{"class":302},"{\u003CCheckout \u002F>} ",[278,472,430],{"class":298},[278,474,476,478,480,482,485,487,489,492],{"class":280,"line":475},15,[278,477,411],{"class":298},[278,479,414],{"class":302},[278,481,358],{"class":298},[278,483,484],{"class":309},"\"\u002Forders\"",[278,486,422],{"class":302},[278,488,358],{"class":298},[278,490,491],{"class":302},"{\u003COrders \u002F>} ",[278,493,430],{"class":298},[278,495,497,499],{"class":280,"line":496},16,[278,498,411],{"class":298},[278,500,502],{"class":501},"s4XuR","Route\n",[278,504,506,509,511],{"class":280,"line":505},17,[278,507,508],{"class":302},"              path",[278,510,358],{"class":298},[278,512,513],{"class":309},"\"\u002Fadmin\"\n",[278,515,517,520,522],{"class":280,"line":516},18,[278,518,519],{"class":302},"              element",[278,521,358],{"class":298},[278,523,524],{"class":302},"{\n",[278,526,528],{"class":280,"line":527},19,[278,529,530],{"class":302},"                \u003CProtectedRoute>\n",[278,532,534],{"class":280,"line":533},20,[278,535,536],{"class":302},"                  \u003CAdmin \u002F>\n",[278,538,540],{"class":280,"line":539},21,[278,541,542],{"class":302},"                \u003C\u002FProtectedRoute>\n",[278,544,546],{"class":280,"line":545},22,[278,547,548],{"class":302},"              }\n",[278,550,552],{"class":280,"line":551},23,[278,553,554],{"class":298},"            \u002F>\n",[278,556,558,561,563],{"class":280,"line":557},24,[278,559,560],{"class":298},"          \u003C\u002F",[278,562,403],{"class":302},[278,564,372],{"class":298},[278,566,568,571,573],{"class":280,"line":567},25,[278,569,570],{"class":298},"        \u003C\u002F",[278,572,392],{"class":302},[278,574,372],{"class":298},[278,576,578,581,583],{"class":280,"line":577},26,[278,579,580],{"class":298},"      \u003C\u002F",[278,582,381],{"class":302},[278,584,372],{"class":298},[278,586,588,590,593],{"class":280,"line":587},27,[278,589,378],{"class":298},[278,591,592],{"class":302},"Toaster ",[278,594,430],{"class":298},[278,596,598,601,604],{"class":280,"line":597},28,[278,599,600],{"class":298},"    \u003C\u002F",[278,602,603],{"class":302},"ThemeProvider",[278,605,372],{"class":298},[278,607,609],{"class":280,"line":608},29,[278,610,611],{"class":302},"  );\n",[278,613,615],{"class":280,"line":614},30,[278,616,617],{"class":302},"}\n",[278,619,621],{"class":280,"line":620},31,[278,622,292],{"emptyLinePlaceholder":291},[278,624,626,629,632,635],{"class":280,"line":625},32,[278,627,628],{"class":298},"export",[278,630,631],{"class":298}," default",[278,633,634],{"class":333}," withDevCycleProvider",[278,636,637],{"class":302},"({\n",[278,639,641,644,646,648,652,655,658],{"class":280,"line":640},33,[278,642,643],{"class":302},"  sdkKey: ",[278,645,299],{"class":298},[278,647,183],{"class":302},[278,649,651],{"class":650},"sj4cs","meta",[278,653,654],{"class":302},".env.",[278,656,657],{"class":650},"VITE_DEVCYCLE_CLIENT_SDK_KEY",[278,659,660],{"class":302},",\n",[278,662,664],{"class":280,"line":663},34,[278,665,666],{"class":302},"  options: {\n",[278,668,670,673,676],{"class":280,"line":669},35,[278,671,672],{"class":302},"    logLevel: ",[278,674,675],{"class":309},"\"debug\"",[278,677,660],{"class":302},[278,679,681],{"class":280,"line":680},36,[278,682,683],{"class":302},"  },\n",[278,685,687],{"class":280,"line":686},37,[278,688,689],{"class":302},"})(App);\n",[11,691,692,693,696],{},"And then we create a ",[59,694,695],{},"useFeatureFlags"," hook to get the various feature flags variables associated with the app as shown below:",[269,698,700],{"className":271,"code":699,"language":273,"meta":274,"style":274},"\u002F\u002F src\u002Fhooks\u002Fuse-feature-flags.ts\n\nimport { useVariableValue } from \"@devcycle\u002Freact-client-sdk\";\nimport { featureKeys } from \"@\u002Flib\u002Fconsts\";\n\nexport function useFeatureFlags() {\n  const showNutritionInfo = useVariableValue(\n    featureKeys.SHOW_NUTRITION_INFO,\n    false,\n  );\n  const enableOnlinePayment = useVariableValue(\n    featureKeys.ENABLE_ONLINE_PAYMENT,\n    false,\n  );\n  const showPromotionalBanner = useVariableValue(\n    featureKeys.SHOW_PROMOTIONAL_BANNER,\n    \"\",\n  );\n\n  \u002F\u002F etc.\n\n  return {\n    showNutritionInfo,\n    enableOnlinePayment,\n    showPromotionalBanner,\n    \u002F\u002F ...\n  };\n}\n",[59,701,702,707,711,724,738,742,754,771,781,788,792,805,814,820,824,837,846,853,857,861,866,870,877,882,887,892,897,902],{"__ignoreMap":274},[278,703,704],{"class":280,"line":281},[278,705,706],{"class":284},"\u002F\u002F src\u002Fhooks\u002Fuse-feature-flags.ts\n",[278,708,709],{"class":280,"line":288},[278,710,292],{"emptyLinePlaceholder":291},[278,712,713,715,718,720,722],{"class":280,"line":295},[278,714,299],{"class":298},[278,716,717],{"class":302}," { useVariableValue } ",[278,719,306],{"class":298},[278,721,310],{"class":309},[278,723,313],{"class":302},[278,725,726,728,731,733,736],{"class":280,"line":316},[278,727,299],{"class":298},[278,729,730],{"class":302}," { featureKeys } ",[278,732,306],{"class":298},[278,734,735],{"class":309}," \"@\u002Flib\u002Fconsts\"",[278,737,313],{"class":302},[278,739,740],{"class":280,"line":322},[278,741,292],{"emptyLinePlaceholder":291},[278,743,744,746,749,752],{"class":280,"line":327},[278,745,628],{"class":298},[278,747,748],{"class":298}," function",[278,750,751],{"class":333}," useFeatureFlags",[278,753,337],{"class":302},[278,755,756,759,762,765,768],{"class":280,"line":340},[278,757,758],{"class":298},"  const",[278,760,761],{"class":650}," showNutritionInfo",[278,763,764],{"class":298}," =",[278,766,767],{"class":333}," useVariableValue",[278,769,770],{"class":302},"(\n",[278,772,773,776,779],{"class":280,"line":349},[278,774,775],{"class":302},"    featureKeys.",[278,777,778],{"class":650},"SHOW_NUTRITION_INFO",[278,780,660],{"class":302},[278,782,783,786],{"class":280,"line":375},[278,784,785],{"class":650},"    false",[278,787,660],{"class":302},[278,789,790],{"class":280,"line":386},[278,791,611],{"class":302},[278,793,794,796,799,801,803],{"class":280,"line":397},[278,795,758],{"class":298},[278,797,798],{"class":650}," enableOnlinePayment",[278,800,764],{"class":298},[278,802,767],{"class":333},[278,804,770],{"class":302},[278,806,807,809,812],{"class":280,"line":408},[278,808,775],{"class":302},[278,810,811],{"class":650},"ENABLE_ONLINE_PAYMENT",[278,813,660],{"class":302},[278,815,816,818],{"class":280,"line":433},[278,817,785],{"class":650},[278,819,660],{"class":302},[278,821,822],{"class":280,"line":454},[278,823,611],{"class":302},[278,825,826,828,831,833,835],{"class":280,"line":475},[278,827,758],{"class":298},[278,829,830],{"class":650}," showPromotionalBanner",[278,832,764],{"class":298},[278,834,767],{"class":333},[278,836,770],{"class":302},[278,838,839,841,844],{"class":280,"line":496},[278,840,775],{"class":302},[278,842,843],{"class":650},"SHOW_PROMOTIONAL_BANNER",[278,845,660],{"class":302},[278,847,848,851],{"class":280,"line":505},[278,849,850],{"class":309},"    \"\"",[278,852,660],{"class":302},[278,854,855],{"class":280,"line":516},[278,856,611],{"class":302},[278,858,859],{"class":280,"line":527},[278,860,292],{"emptyLinePlaceholder":291},[278,862,863],{"class":280,"line":533},[278,864,865],{"class":284},"  \u002F\u002F etc.\n",[278,867,868],{"class":280,"line":539},[278,869,292],{"emptyLinePlaceholder":291},[278,871,872,874],{"class":280,"line":545},[278,873,343],{"class":298},[278,875,876],{"class":302}," {\n",[278,878,879],{"class":280,"line":551},[278,880,881],{"class":302},"    showNutritionInfo,\n",[278,883,884],{"class":280,"line":557},[278,885,886],{"class":302},"    enableOnlinePayment,\n",[278,888,889],{"class":280,"line":567},[278,890,891],{"class":302},"    showPromotionalBanner,\n",[278,893,894],{"class":280,"line":577},[278,895,896],{"class":284},"    \u002F\u002F ...\n",[278,898,899],{"class":280,"line":587},[278,900,901],{"class":302},"  };\n",[278,903,904],{"class":280,"line":597},[278,905,617],{"class":302},[11,907,908],{},"This hook is used in the app to toggle various code sections on\u002Foff to change the UI.",[32,910,912],{"id":911},"interacting-with-devcycle-management-apis","Interacting with DevCycle Management APIs",[11,914,915,916,919,920,923],{},"For securely calling the Management APIs, we use Netlify serverless functions so that the DevCycle API's ",[59,917,918],{},"client_id"," and ",[59,921,922],{},"client_secret"," are not exposed to the client.",[11,925,926],{},"Before we can interact with the DevCycle APIs, we need to generate an auth token using the DevCycle APIs client id and secret. The below code shows how to generate the auth token:",[269,928,932],{"className":929,"code":930,"language":931,"meta":274,"style":274},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F netlify\u002Ffunctions\u002Ffeature-flags.mts\n\ninterface AuthToken {\n  access_token: string;\n  expires_in: number;\n  token_type: string;\n}\n\nlet tokenCache: { token: string; expiresAt: number } | null = null;\n\nasync function getAuthToken() {\n  if (tokenCache && tokenCache.expiresAt > Date.now()) {\n    return tokenCache.token;\n  }\n\n  try {\n    const response = await fetch(\"https:\u002F\u002Fauth.devcycle.com\u002Foauth\u002Ftoken\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application\u002Fx-www-form-urlencoded\",\n      },\n      body: new URLSearchParams({\n        grant_type: \"client_credentials\",\n        audience: \"https:\u002F\u002Fapi.devcycle.com\u002F\",\n        client_id: process.env.DEVCYCLE_API_CLIENT_ID!,\n        client_secret: process.env.DEVCYCLE_API_CLIENT_SECRET!,\n      }),\n    });\n\n    if (!response.ok) {\n      throw new Error(`Auth failed: ${response.status}`);\n    }\n\n    const data: AuthToken = await response.json();\n\n    tokenCache = {\n      token: data.access_token,\n      expiresAt: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n    };\n\n    return data.access_token;\n  } catch (error) {\n    console.error(\"Failed to get auth token:\", error);\n    throw error;\n  }\n}\n","ts",[59,933,934,939,943,953,966,978,989,993,997,1042,1046,1058,1084,1092,1097,1101,1108,1133,1143,1148,1161,1166,1179,1189,1199,1212,1224,1229,1234,1238,1251,1281,1286,1290,1314,1318,1327,1332,1373,1379,1384,1392,1404,1421,1430,1435],{"__ignoreMap":274},[278,935,936],{"class":280,"line":281},[278,937,938],{"class":284},"\u002F\u002F netlify\u002Ffunctions\u002Ffeature-flags.mts\n",[278,940,941],{"class":280,"line":288},[278,942,292],{"emptyLinePlaceholder":291},[278,944,945,948,951],{"class":280,"line":295},[278,946,947],{"class":298},"interface",[278,949,950],{"class":333}," AuthToken",[278,952,876],{"class":302},[278,954,955,958,961,964],{"class":280,"line":316},[278,956,957],{"class":501},"  access_token",[278,959,960],{"class":298},":",[278,962,963],{"class":650}," string",[278,965,313],{"class":302},[278,967,968,971,973,976],{"class":280,"line":322},[278,969,970],{"class":501},"  expires_in",[278,972,960],{"class":298},[278,974,975],{"class":650}," number",[278,977,313],{"class":302},[278,979,980,983,985,987],{"class":280,"line":327},[278,981,982],{"class":501},"  token_type",[278,984,960],{"class":298},[278,986,963],{"class":650},[278,988,313],{"class":302},[278,990,991],{"class":280,"line":340},[278,992,617],{"class":302},[278,994,995],{"class":280,"line":349},[278,996,292],{"emptyLinePlaceholder":291},[278,998,999,1002,1005,1007,1010,1013,1015,1017,1020,1023,1025,1027,1030,1033,1036,1038,1040],{"class":280,"line":375},[278,1000,1001],{"class":298},"let",[278,1003,1004],{"class":302}," tokenCache",[278,1006,960],{"class":298},[278,1008,1009],{"class":302}," { ",[278,1011,1012],{"class":501},"token",[278,1014,960],{"class":298},[278,1016,963],{"class":650},[278,1018,1019],{"class":302},"; ",[278,1021,1022],{"class":501},"expiresAt",[278,1024,960],{"class":298},[278,1026,975],{"class":650},[278,1028,1029],{"class":302}," } ",[278,1031,1032],{"class":298},"|",[278,1034,1035],{"class":650}," null",[278,1037,764],{"class":298},[278,1039,1035],{"class":650},[278,1041,313],{"class":302},[278,1043,1044],{"class":280,"line":386},[278,1045,292],{"emptyLinePlaceholder":291},[278,1047,1048,1051,1053,1056],{"class":280,"line":397},[278,1049,1050],{"class":298},"async",[278,1052,748],{"class":298},[278,1054,1055],{"class":333}," getAuthToken",[278,1057,337],{"class":302},[278,1059,1060,1063,1066,1069,1072,1075,1078,1081],{"class":280,"line":408},[278,1061,1062],{"class":298},"  if",[278,1064,1065],{"class":302}," (tokenCache ",[278,1067,1068],{"class":298},"&&",[278,1070,1071],{"class":302}," tokenCache.expiresAt ",[278,1073,1074],{"class":298},">",[278,1076,1077],{"class":302}," Date.",[278,1079,1080],{"class":333},"now",[278,1082,1083],{"class":302},"()) {\n",[278,1085,1086,1089],{"class":280,"line":433},[278,1087,1088],{"class":298},"    return",[278,1090,1091],{"class":302}," tokenCache.token;\n",[278,1093,1094],{"class":280,"line":454},[278,1095,1096],{"class":302},"  }\n",[278,1098,1099],{"class":280,"line":475},[278,1100,292],{"emptyLinePlaceholder":291},[278,1102,1103,1106],{"class":280,"line":496},[278,1104,1105],{"class":298},"  try",[278,1107,876],{"class":302},[278,1109,1110,1113,1116,1118,1121,1124,1127,1130],{"class":280,"line":505},[278,1111,1112],{"class":298},"    const",[278,1114,1115],{"class":650}," response",[278,1117,764],{"class":298},[278,1119,1120],{"class":298}," await",[278,1122,1123],{"class":333}," fetch",[278,1125,1126],{"class":302},"(",[278,1128,1129],{"class":309},"\"https:\u002F\u002Fauth.devcycle.com\u002Foauth\u002Ftoken\"",[278,1131,1132],{"class":302},", {\n",[278,1134,1135,1138,1141],{"class":280,"line":516},[278,1136,1137],{"class":302},"      method: ",[278,1139,1140],{"class":309},"\"POST\"",[278,1142,660],{"class":302},[278,1144,1145],{"class":280,"line":527},[278,1146,1147],{"class":302},"      headers: {\n",[278,1149,1150,1153,1156,1159],{"class":280,"line":533},[278,1151,1152],{"class":309},"        \"Content-Type\"",[278,1154,1155],{"class":302},": ",[278,1157,1158],{"class":309},"\"application\u002Fx-www-form-urlencoded\"",[278,1160,660],{"class":302},[278,1162,1163],{"class":280,"line":539},[278,1164,1165],{"class":302},"      },\n",[278,1167,1168,1171,1174,1177],{"class":280,"line":545},[278,1169,1170],{"class":302},"      body: ",[278,1172,1173],{"class":298},"new",[278,1175,1176],{"class":333}," URLSearchParams",[278,1178,637],{"class":302},[278,1180,1181,1184,1187],{"class":280,"line":551},[278,1182,1183],{"class":302},"        grant_type: ",[278,1185,1186],{"class":309},"\"client_credentials\"",[278,1188,660],{"class":302},[278,1190,1191,1194,1197],{"class":280,"line":557},[278,1192,1193],{"class":302},"        audience: ",[278,1195,1196],{"class":309},"\"https:\u002F\u002Fapi.devcycle.com\u002F\"",[278,1198,660],{"class":302},[278,1200,1201,1204,1207,1210],{"class":280,"line":567},[278,1202,1203],{"class":302},"        client_id: process.env.",[278,1205,1206],{"class":650},"DEVCYCLE_API_CLIENT_ID",[278,1208,1209],{"class":298},"!",[278,1211,660],{"class":302},[278,1213,1214,1217,1220,1222],{"class":280,"line":577},[278,1215,1216],{"class":302},"        client_secret: process.env.",[278,1218,1219],{"class":650},"DEVCYCLE_API_CLIENT_SECRET",[278,1221,1209],{"class":298},[278,1223,660],{"class":302},[278,1225,1226],{"class":280,"line":587},[278,1227,1228],{"class":302},"      }),\n",[278,1230,1231],{"class":280,"line":597},[278,1232,1233],{"class":302},"    });\n",[278,1235,1236],{"class":280,"line":608},[278,1237,292],{"emptyLinePlaceholder":291},[278,1239,1240,1243,1246,1248],{"class":280,"line":614},[278,1241,1242],{"class":298},"    if",[278,1244,1245],{"class":302}," (",[278,1247,1209],{"class":298},[278,1249,1250],{"class":302},"response.ok) {\n",[278,1252,1253,1256,1259,1262,1264,1267,1270,1272,1275,1278],{"class":280,"line":620},[278,1254,1255],{"class":298},"      throw",[278,1257,1258],{"class":298}," new",[278,1260,1261],{"class":333}," Error",[278,1263,1126],{"class":302},[278,1265,1266],{"class":309},"`Auth failed: ${",[278,1268,1269],{"class":302},"response",[278,1271,183],{"class":309},[278,1273,1274],{"class":302},"status",[278,1276,1277],{"class":309},"}`",[278,1279,1280],{"class":302},");\n",[278,1282,1283],{"class":280,"line":625},[278,1284,1285],{"class":302},"    }\n",[278,1287,1288],{"class":280,"line":640},[278,1289,292],{"emptyLinePlaceholder":291},[278,1291,1292,1294,1297,1299,1301,1303,1305,1308,1311],{"class":280,"line":663},[278,1293,1112],{"class":298},[278,1295,1296],{"class":650}," data",[278,1298,960],{"class":298},[278,1300,950],{"class":333},[278,1302,764],{"class":298},[278,1304,1120],{"class":298},[278,1306,1307],{"class":302}," response.",[278,1309,1310],{"class":333},"json",[278,1312,1313],{"class":302},"();\n",[278,1315,1316],{"class":280,"line":669},[278,1317,292],{"emptyLinePlaceholder":291},[278,1319,1320,1323,1325],{"class":280,"line":680},[278,1321,1322],{"class":302},"    tokenCache ",[278,1324,358],{"class":298},[278,1326,876],{"class":302},[278,1328,1329],{"class":280,"line":686},[278,1330,1331],{"class":302},"      token: data.access_token,\n",[278,1333,1335,1338,1340,1343,1346,1349,1352,1355,1358,1361,1364,1367,1369,1371],{"class":280,"line":1334},38,[278,1336,1337],{"class":302},"      expiresAt: Date.",[278,1339,1080],{"class":333},[278,1341,1342],{"class":302},"() ",[278,1344,1345],{"class":298},"+",[278,1347,1348],{"class":302}," data.expires_in ",[278,1350,1351],{"class":298},"*",[278,1353,1354],{"class":650}," 1000",[278,1356,1357],{"class":298}," -",[278,1359,1360],{"class":650}," 5",[278,1362,1363],{"class":298}," *",[278,1365,1366],{"class":650}," 60",[278,1368,1363],{"class":298},[278,1370,1354],{"class":650},[278,1372,660],{"class":302},[278,1374,1376],{"class":280,"line":1375},39,[278,1377,1378],{"class":302},"    };\n",[278,1380,1382],{"class":280,"line":1381},40,[278,1383,292],{"emptyLinePlaceholder":291},[278,1385,1387,1389],{"class":280,"line":1386},41,[278,1388,1088],{"class":298},[278,1390,1391],{"class":302}," data.access_token;\n",[278,1393,1395,1398,1401],{"class":280,"line":1394},42,[278,1396,1397],{"class":302},"  } ",[278,1399,1400],{"class":298},"catch",[278,1402,1403],{"class":302}," (error) {\n",[278,1405,1407,1410,1413,1415,1418],{"class":280,"line":1406},43,[278,1408,1409],{"class":302},"    console.",[278,1411,1412],{"class":333},"error",[278,1414,1126],{"class":302},[278,1416,1417],{"class":309},"\"Failed to get auth token:\"",[278,1419,1420],{"class":302},", error);\n",[278,1422,1424,1427],{"class":280,"line":1423},44,[278,1425,1426],{"class":298},"    throw",[278,1428,1429],{"class":302}," error;\n",[278,1431,1433],{"class":280,"line":1432},45,[278,1434,1096],{"class":302},[278,1436,1438],{"class":280,"line":1437},46,[278,1439,617],{"class":302},[11,1441,1442],{},"Now we can fetch the available feature flags, and their config (to get the currently active variation) using the below code:",[269,1444,1446],{"className":271,"code":1445,"language":273,"meta":274,"style":274},"\u002F\u002F netlify\u002Ffunctions\u002Ffeature-flags.mts\n\ninterface Distribution {\n  _variation: string;\n  percentage: number;\n}\n\ninterface FeatureConfig {\n  _feature: string;\n  _environment: string;\n  status: string;\n  targets: {\n    _id: string;\n    name: string;\n    distribution: Distribution[];\n    audience: {\n      name: string;\n      filters: {\n        operator: \"and\" | \"or\";\n        filters: { type: string }[];\n      };\n    };\n  }[];\n}\n\nasync function getFeatures(\n  featuresUrl: string,\n  headers: Record\u003Cstring, string>,\n) {\n  const featuresResponse = await fetch(featuresUrl, { headers });\n\n  if (!featuresResponse.ok) {\n    throw new Error(`Failed to fetch flags: ${featuresResponse.status}`);\n  }\n\n  const featuresData = await featuresResponse.json();\n\n  \u002F\u002F Fetch the config for each of the feature flags\n  \u002F\u002F We're only fetching the configs for the development env\n  const configPromises = featuresData.map(async (feature: any) => {\n    const configResponse = await fetch(\n      `${featuresUrl}\u002F${feature._id}\u002Fconfigurations?environment=development`,\n      { headers },\n    );\n\n    if (!configResponse.ok) {\n      console.error(`Failed to fetch config for feature ${feature._id}`);\n      return null;\n    }\n\n    const configs: FeatureConfig[] = await configResponse.json();\n\n    return {\n      ...feature,\n      targets: configs[0].targets,\n      status: configs[0].status,\n    };\n  });\n\n  const featuresWithConfigs = await Promise.all(configPromises);\n  return featuresWithConfigs.filter((f) => f !== null);\n}\n",[59,1447,1448,1452,1456,1465,1476,1487,1491,1495,1504,1515,1526,1537,1546,1557,1568,1580,1589,1600,1609,1627,1646,1651,1655,1660,1664,1668,1679,1690,1714,1719,1735,1739,1750,1774,1778,1782,1800,1804,1809,1814,1851,1866,1889,1894,1899,1903,1914,1937,1947,1952,1957,1983,1988,1995,2004,2016,2027,2032,2038,2043,2066,2097],{"__ignoreMap":274},[278,1449,1450],{"class":280,"line":281},[278,1451,938],{"class":284},[278,1453,1454],{"class":280,"line":288},[278,1455,292],{"emptyLinePlaceholder":291},[278,1457,1458,1460,1463],{"class":280,"line":295},[278,1459,947],{"class":298},[278,1461,1462],{"class":333}," Distribution",[278,1464,876],{"class":302},[278,1466,1467,1470,1472,1474],{"class":280,"line":316},[278,1468,1469],{"class":501},"  _variation",[278,1471,960],{"class":298},[278,1473,963],{"class":650},[278,1475,313],{"class":302},[278,1477,1478,1481,1483,1485],{"class":280,"line":322},[278,1479,1480],{"class":501},"  percentage",[278,1482,960],{"class":298},[278,1484,975],{"class":650},[278,1486,313],{"class":302},[278,1488,1489],{"class":280,"line":327},[278,1490,617],{"class":302},[278,1492,1493],{"class":280,"line":340},[278,1494,292],{"emptyLinePlaceholder":291},[278,1496,1497,1499,1502],{"class":280,"line":349},[278,1498,947],{"class":298},[278,1500,1501],{"class":333}," FeatureConfig",[278,1503,876],{"class":302},[278,1505,1506,1509,1511,1513],{"class":280,"line":375},[278,1507,1508],{"class":501},"  _feature",[278,1510,960],{"class":298},[278,1512,963],{"class":650},[278,1514,313],{"class":302},[278,1516,1517,1520,1522,1524],{"class":280,"line":386},[278,1518,1519],{"class":501},"  _environment",[278,1521,960],{"class":298},[278,1523,963],{"class":650},[278,1525,313],{"class":302},[278,1527,1528,1531,1533,1535],{"class":280,"line":397},[278,1529,1530],{"class":501},"  status",[278,1532,960],{"class":298},[278,1534,963],{"class":650},[278,1536,313],{"class":302},[278,1538,1539,1542,1544],{"class":280,"line":408},[278,1540,1541],{"class":501},"  targets",[278,1543,960],{"class":298},[278,1545,876],{"class":302},[278,1547,1548,1551,1553,1555],{"class":280,"line":433},[278,1549,1550],{"class":501},"    _id",[278,1552,960],{"class":298},[278,1554,963],{"class":650},[278,1556,313],{"class":302},[278,1558,1559,1562,1564,1566],{"class":280,"line":454},[278,1560,1561],{"class":501},"    name",[278,1563,960],{"class":298},[278,1565,963],{"class":650},[278,1567,313],{"class":302},[278,1569,1570,1573,1575,1577],{"class":280,"line":475},[278,1571,1572],{"class":501},"    distribution",[278,1574,960],{"class":298},[278,1576,1462],{"class":333},[278,1578,1579],{"class":302},"[];\n",[278,1581,1582,1585,1587],{"class":280,"line":496},[278,1583,1584],{"class":501},"    audience",[278,1586,960],{"class":298},[278,1588,876],{"class":302},[278,1590,1591,1594,1596,1598],{"class":280,"line":505},[278,1592,1593],{"class":501},"      name",[278,1595,960],{"class":298},[278,1597,963],{"class":650},[278,1599,313],{"class":302},[278,1601,1602,1605,1607],{"class":280,"line":516},[278,1603,1604],{"class":501},"      filters",[278,1606,960],{"class":298},[278,1608,876],{"class":302},[278,1610,1611,1614,1616,1619,1622,1625],{"class":280,"line":527},[278,1612,1613],{"class":501},"        operator",[278,1615,960],{"class":298},[278,1617,1618],{"class":309}," \"and\"",[278,1620,1621],{"class":298}," |",[278,1623,1624],{"class":309}," \"or\"",[278,1626,313],{"class":302},[278,1628,1629,1632,1634,1636,1639,1641,1643],{"class":280,"line":533},[278,1630,1631],{"class":501},"        filters",[278,1633,960],{"class":298},[278,1635,1009],{"class":302},[278,1637,1638],{"class":501},"type",[278,1640,960],{"class":298},[278,1642,963],{"class":650},[278,1644,1645],{"class":302}," }[];\n",[278,1647,1648],{"class":280,"line":539},[278,1649,1650],{"class":302},"      };\n",[278,1652,1653],{"class":280,"line":545},[278,1654,1378],{"class":302},[278,1656,1657],{"class":280,"line":551},[278,1658,1659],{"class":302},"  }[];\n",[278,1661,1662],{"class":280,"line":557},[278,1663,617],{"class":302},[278,1665,1666],{"class":280,"line":567},[278,1667,292],{"emptyLinePlaceholder":291},[278,1669,1670,1672,1674,1677],{"class":280,"line":577},[278,1671,1050],{"class":298},[278,1673,748],{"class":298},[278,1675,1676],{"class":333}," getFeatures",[278,1678,770],{"class":302},[278,1680,1681,1684,1686,1688],{"class":280,"line":587},[278,1682,1683],{"class":501},"  featuresUrl",[278,1685,960],{"class":298},[278,1687,963],{"class":650},[278,1689,660],{"class":302},[278,1691,1692,1695,1697,1700,1703,1706,1709,1711],{"class":280,"line":597},[278,1693,1694],{"class":501},"  headers",[278,1696,960],{"class":298},[278,1698,1699],{"class":333}," Record",[278,1701,1702],{"class":302},"\u003C",[278,1704,1705],{"class":650},"string",[278,1707,1708],{"class":302},", ",[278,1710,1705],{"class":650},[278,1712,1713],{"class":302},">,\n",[278,1715,1716],{"class":280,"line":608},[278,1717,1718],{"class":302},") {\n",[278,1720,1721,1723,1726,1728,1730,1732],{"class":280,"line":614},[278,1722,758],{"class":298},[278,1724,1725],{"class":650}," featuresResponse",[278,1727,764],{"class":298},[278,1729,1120],{"class":298},[278,1731,1123],{"class":333},[278,1733,1734],{"class":302},"(featuresUrl, { headers });\n",[278,1736,1737],{"class":280,"line":620},[278,1738,292],{"emptyLinePlaceholder":291},[278,1740,1741,1743,1745,1747],{"class":280,"line":625},[278,1742,1062],{"class":298},[278,1744,1245],{"class":302},[278,1746,1209],{"class":298},[278,1748,1749],{"class":302},"featuresResponse.ok) {\n",[278,1751,1752,1754,1756,1758,1760,1763,1766,1768,1770,1772],{"class":280,"line":640},[278,1753,1426],{"class":298},[278,1755,1258],{"class":298},[278,1757,1261],{"class":333},[278,1759,1126],{"class":302},[278,1761,1762],{"class":309},"`Failed to fetch flags: ${",[278,1764,1765],{"class":302},"featuresResponse",[278,1767,183],{"class":309},[278,1769,1274],{"class":302},[278,1771,1277],{"class":309},[278,1773,1280],{"class":302},[278,1775,1776],{"class":280,"line":663},[278,1777,1096],{"class":302},[278,1779,1780],{"class":280,"line":669},[278,1781,292],{"emptyLinePlaceholder":291},[278,1783,1784,1786,1789,1791,1793,1796,1798],{"class":280,"line":680},[278,1785,758],{"class":298},[278,1787,1788],{"class":650}," featuresData",[278,1790,764],{"class":298},[278,1792,1120],{"class":298},[278,1794,1795],{"class":302}," featuresResponse.",[278,1797,1310],{"class":333},[278,1799,1313],{"class":302},[278,1801,1802],{"class":280,"line":686},[278,1803,292],{"emptyLinePlaceholder":291},[278,1805,1806],{"class":280,"line":1334},[278,1807,1808],{"class":284},"  \u002F\u002F Fetch the config for each of the feature flags\n",[278,1810,1811],{"class":280,"line":1375},[278,1812,1813],{"class":284},"  \u002F\u002F We're only fetching the configs for the development env\n",[278,1815,1816,1818,1821,1823,1826,1829,1831,1833,1835,1838,1840,1843,1846,1849],{"class":280,"line":1381},[278,1817,758],{"class":298},[278,1819,1820],{"class":650}," configPromises",[278,1822,764],{"class":298},[278,1824,1825],{"class":302}," featuresData.",[278,1827,1828],{"class":333},"map",[278,1830,1126],{"class":302},[278,1832,1050],{"class":298},[278,1834,1245],{"class":302},[278,1836,1837],{"class":501},"feature",[278,1839,960],{"class":298},[278,1841,1842],{"class":650}," any",[278,1844,1845],{"class":302},") ",[278,1847,1848],{"class":298},"=>",[278,1850,876],{"class":302},[278,1852,1853,1855,1858,1860,1862,1864],{"class":280,"line":1386},[278,1854,1112],{"class":298},[278,1856,1857],{"class":650}," configResponse",[278,1859,764],{"class":298},[278,1861,1120],{"class":298},[278,1863,1123],{"class":333},[278,1865,770],{"class":302},[278,1867,1868,1871,1874,1877,1879,1881,1884,1887],{"class":280,"line":1394},[278,1869,1870],{"class":309},"      `${",[278,1872,1873],{"class":302},"featuresUrl",[278,1875,1876],{"class":309},"}\u002F${",[278,1878,1837],{"class":302},[278,1880,183],{"class":309},[278,1882,1883],{"class":302},"_id",[278,1885,1886],{"class":309},"}\u002Fconfigurations?environment=development`",[278,1888,660],{"class":302},[278,1890,1891],{"class":280,"line":1406},[278,1892,1893],{"class":302},"      { headers },\n",[278,1895,1896],{"class":280,"line":1423},[278,1897,1898],{"class":302},"    );\n",[278,1900,1901],{"class":280,"line":1432},[278,1902,292],{"emptyLinePlaceholder":291},[278,1904,1905,1907,1909,1911],{"class":280,"line":1437},[278,1906,1242],{"class":298},[278,1908,1245],{"class":302},[278,1910,1209],{"class":298},[278,1912,1913],{"class":302},"configResponse.ok) {\n",[278,1915,1917,1920,1922,1924,1927,1929,1931,1933,1935],{"class":280,"line":1916},47,[278,1918,1919],{"class":302},"      console.",[278,1921,1412],{"class":333},[278,1923,1126],{"class":302},[278,1925,1926],{"class":309},"`Failed to fetch config for feature ${",[278,1928,1837],{"class":302},[278,1930,183],{"class":309},[278,1932,1883],{"class":302},[278,1934,1277],{"class":309},[278,1936,1280],{"class":302},[278,1938,1940,1943,1945],{"class":280,"line":1939},48,[278,1941,1942],{"class":298},"      return",[278,1944,1035],{"class":650},[278,1946,313],{"class":302},[278,1948,1950],{"class":280,"line":1949},49,[278,1951,1285],{"class":302},[278,1953,1955],{"class":280,"line":1954},50,[278,1956,292],{"emptyLinePlaceholder":291},[278,1958,1960,1962,1965,1967,1969,1972,1974,1976,1979,1981],{"class":280,"line":1959},51,[278,1961,1112],{"class":298},[278,1963,1964],{"class":650}," configs",[278,1966,960],{"class":298},[278,1968,1501],{"class":333},[278,1970,1971],{"class":302},"[] ",[278,1973,358],{"class":298},[278,1975,1120],{"class":298},[278,1977,1978],{"class":302}," configResponse.",[278,1980,1310],{"class":333},[278,1982,1313],{"class":302},[278,1984,1986],{"class":280,"line":1985},52,[278,1987,292],{"emptyLinePlaceholder":291},[278,1989,1991,1993],{"class":280,"line":1990},53,[278,1992,1088],{"class":298},[278,1994,876],{"class":302},[278,1996,1998,2001],{"class":280,"line":1997},54,[278,1999,2000],{"class":298},"      ...",[278,2002,2003],{"class":302},"feature,\n",[278,2005,2007,2010,2013],{"class":280,"line":2006},55,[278,2008,2009],{"class":302},"      targets: configs[",[278,2011,2012],{"class":650},"0",[278,2014,2015],{"class":302},"].targets,\n",[278,2017,2019,2022,2024],{"class":280,"line":2018},56,[278,2020,2021],{"class":302},"      status: configs[",[278,2023,2012],{"class":650},[278,2025,2026],{"class":302},"].status,\n",[278,2028,2030],{"class":280,"line":2029},57,[278,2031,1378],{"class":302},[278,2033,2035],{"class":280,"line":2034},58,[278,2036,2037],{"class":302},"  });\n",[278,2039,2041],{"class":280,"line":2040},59,[278,2042,292],{"emptyLinePlaceholder":291},[278,2044,2046,2048,2051,2053,2055,2058,2060,2063],{"class":280,"line":2045},60,[278,2047,758],{"class":298},[278,2049,2050],{"class":650}," featuresWithConfigs",[278,2052,764],{"class":298},[278,2054,1120],{"class":298},[278,2056,2057],{"class":650}," Promise",[278,2059,183],{"class":302},[278,2061,2062],{"class":333},"all",[278,2064,2065],{"class":302},"(configPromises);\n",[278,2067,2069,2071,2074,2077,2080,2083,2085,2087,2090,2093,2095],{"class":280,"line":2068},61,[278,2070,343],{"class":298},[278,2072,2073],{"class":302}," featuresWithConfigs.",[278,2075,2076],{"class":333},"filter",[278,2078,2079],{"class":302},"((",[278,2081,2082],{"class":501},"f",[278,2084,1845],{"class":302},[278,2086,1848],{"class":298},[278,2088,2089],{"class":302}," f ",[278,2091,2092],{"class":298},"!==",[278,2094,1035],{"class":650},[278,2096,1280],{"class":302},[278,2098,2100],{"class":280,"line":2099},62,[278,2101,617],{"class":302},[11,2103,2104],{},"To update the feature flags config (changing the variation, or toggle the flag altogether), we can use the below function:",[269,2106,2108],{"className":271,"code":2107,"language":273,"meta":274,"style":274},"async function updateFeature(\n  featuresUrl: string,\n  featureId: string,\n  headers: Record\u003Cstring, string>,\n  update: any,\n) {\n  const response = await fetch(\n    `${featuresUrl}\u002F${featureId}\u002Fconfigurations?environment=development`,\n    {\n      method: \"PATCH\",\n      headers,\n      body: JSON.stringify(update),\n    },\n  );\n\n  if (!response.ok) {\n    throw new Error(`Failed to update feature: ${response.status}`);\n  }\n\n  return await response.json();\n}\n",[59,2109,2110,2121,2131,2142,2160,2171,2175,2189,2205,2210,2219,2224,2239,2244,2248,2252,2262,2285,2289,2293,2305],{"__ignoreMap":274},[278,2111,2112,2114,2116,2119],{"class":280,"line":281},[278,2113,1050],{"class":298},[278,2115,748],{"class":298},[278,2117,2118],{"class":333}," updateFeature",[278,2120,770],{"class":302},[278,2122,2123,2125,2127,2129],{"class":280,"line":288},[278,2124,1683],{"class":501},[278,2126,960],{"class":298},[278,2128,963],{"class":650},[278,2130,660],{"class":302},[278,2132,2133,2136,2138,2140],{"class":280,"line":295},[278,2134,2135],{"class":501},"  featureId",[278,2137,960],{"class":298},[278,2139,963],{"class":650},[278,2141,660],{"class":302},[278,2143,2144,2146,2148,2150,2152,2154,2156,2158],{"class":280,"line":316},[278,2145,1694],{"class":501},[278,2147,960],{"class":298},[278,2149,1699],{"class":333},[278,2151,1702],{"class":302},[278,2153,1705],{"class":650},[278,2155,1708],{"class":302},[278,2157,1705],{"class":650},[278,2159,1713],{"class":302},[278,2161,2162,2165,2167,2169],{"class":280,"line":322},[278,2163,2164],{"class":501},"  update",[278,2166,960],{"class":298},[278,2168,1842],{"class":650},[278,2170,660],{"class":302},[278,2172,2173],{"class":280,"line":327},[278,2174,1718],{"class":302},[278,2176,2177,2179,2181,2183,2185,2187],{"class":280,"line":340},[278,2178,758],{"class":298},[278,2180,1115],{"class":650},[278,2182,764],{"class":298},[278,2184,1120],{"class":298},[278,2186,1123],{"class":333},[278,2188,770],{"class":302},[278,2190,2191,2194,2196,2198,2201,2203],{"class":280,"line":349},[278,2192,2193],{"class":309},"    `${",[278,2195,1873],{"class":302},[278,2197,1876],{"class":309},[278,2199,2200],{"class":302},"featureId",[278,2202,1886],{"class":309},[278,2204,660],{"class":302},[278,2206,2207],{"class":280,"line":375},[278,2208,2209],{"class":302},"    {\n",[278,2211,2212,2214,2217],{"class":280,"line":386},[278,2213,1137],{"class":302},[278,2215,2216],{"class":309},"\"PATCH\"",[278,2218,660],{"class":302},[278,2220,2221],{"class":280,"line":397},[278,2222,2223],{"class":302},"      headers,\n",[278,2225,2226,2228,2231,2233,2236],{"class":280,"line":408},[278,2227,1170],{"class":302},[278,2229,2230],{"class":650},"JSON",[278,2232,183],{"class":302},[278,2234,2235],{"class":333},"stringify",[278,2237,2238],{"class":302},"(update),\n",[278,2240,2241],{"class":280,"line":433},[278,2242,2243],{"class":302},"    },\n",[278,2245,2246],{"class":280,"line":454},[278,2247,611],{"class":302},[278,2249,2250],{"class":280,"line":475},[278,2251,292],{"emptyLinePlaceholder":291},[278,2253,2254,2256,2258,2260],{"class":280,"line":496},[278,2255,1062],{"class":298},[278,2257,1245],{"class":302},[278,2259,1209],{"class":298},[278,2261,1250],{"class":302},[278,2263,2264,2266,2268,2270,2272,2275,2277,2279,2281,2283],{"class":280,"line":505},[278,2265,1426],{"class":298},[278,2267,1258],{"class":298},[278,2269,1261],{"class":333},[278,2271,1126],{"class":302},[278,2273,2274],{"class":309},"`Failed to update feature: ${",[278,2276,1269],{"class":302},[278,2278,183],{"class":309},[278,2280,1274],{"class":302},[278,2282,1277],{"class":309},[278,2284,1280],{"class":302},[278,2286,2287],{"class":280,"line":516},[278,2288,1096],{"class":302},[278,2290,2291],{"class":280,"line":527},[278,2292,292],{"emptyLinePlaceholder":291},[278,2294,2295,2297,2299,2301,2303],{"class":280,"line":533},[278,2296,343],{"class":298},[278,2298,1120],{"class":298},[278,2300,1307],{"class":302},[278,2302,1310],{"class":333},[278,2304,1313],{"class":302},[278,2306,2307],{"class":280,"line":539},[278,2308,617],{"class":302},[11,2310,2311],{},"Finally, here is the Netlify function that uses the above functions to serve the client requests:",[269,2313,2315],{"className":271,"code":2314,"language":273,"meta":274,"style":274},"export default async (req: Request) => {\n  try {\n    const { method } = req;\n    const token = await getAuthToken();\n    const featuresBaseUrl = `https:\u002F\u002Fapi.devcycle.com\u002Fv1\u002Fprojects\u002F${process.env.DEVCYCLE_PROJECT_ID}\u002Ffeatures`;\n    const headers = {\n      Authorization: `Bearer ${token}`,\n    };\n\n    if (method === \"GET\") {\n      const features = await getFeatures(featuresBaseUrl, headers);\n\n      return Response.json({ features });\n    }\n\n    if (method === \"PATCH\") {\n      const body = await req.json();\n      const { featureId, update } = body;\n\n      const data = await updateFeature(\n        featuresBaseUrl,\n        featureId,\n        {\n          ...headers,\n          \"Content-Type\": \"application\u002Fjson\",\n        },\n        update,\n      );\n\n      return Response.json({ data });\n    }\n\n    return new Response(JSON.stringify({ error: \"Method not allowed\" }), {\n      status: 405,\n      headers: {\n        \"Content-Type\": \"application\u002Fjson\",\n      },\n    });\n  } catch (error) {\n    console.error(\"Error:\", error);\n    return new Response(\n      JSON.stringify({\n        error: error instanceof Error ? error.message : \"Internal server error\",\n      }),\n      {\n        status: 500,\n        headers: {\n          \"Content-Type\": \"application\u002Fjson\",\n        },\n      },\n    );\n  }\n};\n",[59,2316,2317,2342,2348,2364,2379,2409,2420,2434,2438,2442,2457,2474,2478,2490,2494,2498,2511,2529,2549,2553,2567,2572,2577,2582,2590,2602,2607,2612,2617,2621,2632,2636,2640,2666,2676,2680,2690,2694,2698,2706,2719,2729,2740,2763,2767,2772,2782,2787,2797,2801,2805,2809,2813],{"__ignoreMap":274},[278,2318,2319,2321,2323,2326,2328,2331,2333,2336,2338,2340],{"class":280,"line":281},[278,2320,628],{"class":298},[278,2322,631],{"class":298},[278,2324,2325],{"class":298}," async",[278,2327,1245],{"class":302},[278,2329,2330],{"class":501},"req",[278,2332,960],{"class":298},[278,2334,2335],{"class":333}," Request",[278,2337,1845],{"class":302},[278,2339,1848],{"class":298},[278,2341,876],{"class":302},[278,2343,2344,2346],{"class":280,"line":288},[278,2345,1105],{"class":298},[278,2347,876],{"class":302},[278,2349,2350,2352,2354,2357,2359,2361],{"class":280,"line":295},[278,2351,1112],{"class":298},[278,2353,1009],{"class":302},[278,2355,2356],{"class":650},"method",[278,2358,1029],{"class":302},[278,2360,358],{"class":298},[278,2362,2363],{"class":302}," req;\n",[278,2365,2366,2368,2371,2373,2375,2377],{"class":280,"line":316},[278,2367,1112],{"class":298},[278,2369,2370],{"class":650}," token",[278,2372,764],{"class":298},[278,2374,1120],{"class":298},[278,2376,1055],{"class":333},[278,2378,1313],{"class":302},[278,2380,2381,2383,2386,2388,2391,2394,2396,2399,2401,2404,2407],{"class":280,"line":322},[278,2382,1112],{"class":298},[278,2384,2385],{"class":650}," featuresBaseUrl",[278,2387,764],{"class":298},[278,2389,2390],{"class":309}," `https:\u002F\u002Fapi.devcycle.com\u002Fv1\u002Fprojects\u002F${",[278,2392,2393],{"class":302},"process",[278,2395,183],{"class":309},[278,2397,2398],{"class":302},"env",[278,2400,183],{"class":309},[278,2402,2403],{"class":650},"DEVCYCLE_PROJECT_ID",[278,2405,2406],{"class":309},"}\u002Ffeatures`",[278,2408,313],{"class":302},[278,2410,2411,2413,2416,2418],{"class":280,"line":327},[278,2412,1112],{"class":298},[278,2414,2415],{"class":650}," headers",[278,2417,764],{"class":298},[278,2419,876],{"class":302},[278,2421,2422,2425,2428,2430,2432],{"class":280,"line":340},[278,2423,2424],{"class":302},"      Authorization: ",[278,2426,2427],{"class":309},"`Bearer ${",[278,2429,1012],{"class":302},[278,2431,1277],{"class":309},[278,2433,660],{"class":302},[278,2435,2436],{"class":280,"line":349},[278,2437,1378],{"class":302},[278,2439,2440],{"class":280,"line":375},[278,2441,292],{"emptyLinePlaceholder":291},[278,2443,2444,2446,2449,2452,2455],{"class":280,"line":386},[278,2445,1242],{"class":298},[278,2447,2448],{"class":302}," (method ",[278,2450,2451],{"class":298},"===",[278,2453,2454],{"class":309}," \"GET\"",[278,2456,1718],{"class":302},[278,2458,2459,2462,2465,2467,2469,2471],{"class":280,"line":397},[278,2460,2461],{"class":298},"      const",[278,2463,2464],{"class":650}," features",[278,2466,764],{"class":298},[278,2468,1120],{"class":298},[278,2470,1676],{"class":333},[278,2472,2473],{"class":302},"(featuresBaseUrl, headers);\n",[278,2475,2476],{"class":280,"line":408},[278,2477,292],{"emptyLinePlaceholder":291},[278,2479,2480,2482,2485,2487],{"class":280,"line":433},[278,2481,1942],{"class":298},[278,2483,2484],{"class":302}," Response.",[278,2486,1310],{"class":333},[278,2488,2489],{"class":302},"({ features });\n",[278,2491,2492],{"class":280,"line":454},[278,2493,1285],{"class":302},[278,2495,2496],{"class":280,"line":475},[278,2497,292],{"emptyLinePlaceholder":291},[278,2499,2500,2502,2504,2506,2509],{"class":280,"line":496},[278,2501,1242],{"class":298},[278,2503,2448],{"class":302},[278,2505,2451],{"class":298},[278,2507,2508],{"class":309}," \"PATCH\"",[278,2510,1718],{"class":302},[278,2512,2513,2515,2518,2520,2522,2525,2527],{"class":280,"line":505},[278,2514,2461],{"class":298},[278,2516,2517],{"class":650}," body",[278,2519,764],{"class":298},[278,2521,1120],{"class":298},[278,2523,2524],{"class":302}," req.",[278,2526,1310],{"class":333},[278,2528,1313],{"class":302},[278,2530,2531,2533,2535,2537,2539,2542,2544,2546],{"class":280,"line":516},[278,2532,2461],{"class":298},[278,2534,1009],{"class":302},[278,2536,2200],{"class":650},[278,2538,1708],{"class":302},[278,2540,2541],{"class":650},"update",[278,2543,1029],{"class":302},[278,2545,358],{"class":298},[278,2547,2548],{"class":302}," body;\n",[278,2550,2551],{"class":280,"line":527},[278,2552,292],{"emptyLinePlaceholder":291},[278,2554,2555,2557,2559,2561,2563,2565],{"class":280,"line":533},[278,2556,2461],{"class":298},[278,2558,1296],{"class":650},[278,2560,764],{"class":298},[278,2562,1120],{"class":298},[278,2564,2118],{"class":333},[278,2566,770],{"class":302},[278,2568,2569],{"class":280,"line":539},[278,2570,2571],{"class":302},"        featuresBaseUrl,\n",[278,2573,2574],{"class":280,"line":545},[278,2575,2576],{"class":302},"        featureId,\n",[278,2578,2579],{"class":280,"line":551},[278,2580,2581],{"class":302},"        {\n",[278,2583,2584,2587],{"class":280,"line":557},[278,2585,2586],{"class":298},"          ...",[278,2588,2589],{"class":302},"headers,\n",[278,2591,2592,2595,2597,2600],{"class":280,"line":567},[278,2593,2594],{"class":309},"          \"Content-Type\"",[278,2596,1155],{"class":302},[278,2598,2599],{"class":309},"\"application\u002Fjson\"",[278,2601,660],{"class":302},[278,2603,2604],{"class":280,"line":577},[278,2605,2606],{"class":302},"        },\n",[278,2608,2609],{"class":280,"line":587},[278,2610,2611],{"class":302},"        update,\n",[278,2613,2614],{"class":280,"line":597},[278,2615,2616],{"class":302},"      );\n",[278,2618,2619],{"class":280,"line":608},[278,2620,292],{"emptyLinePlaceholder":291},[278,2622,2623,2625,2627,2629],{"class":280,"line":614},[278,2624,1942],{"class":298},[278,2626,2484],{"class":302},[278,2628,1310],{"class":333},[278,2630,2631],{"class":302},"({ data });\n",[278,2633,2634],{"class":280,"line":620},[278,2635,1285],{"class":302},[278,2637,2638],{"class":280,"line":625},[278,2639,292],{"emptyLinePlaceholder":291},[278,2641,2642,2644,2646,2649,2651,2653,2655,2657,2660,2663],{"class":280,"line":640},[278,2643,1088],{"class":298},[278,2645,1258],{"class":298},[278,2647,2648],{"class":333}," Response",[278,2650,1126],{"class":302},[278,2652,2230],{"class":650},[278,2654,183],{"class":302},[278,2656,2235],{"class":333},[278,2658,2659],{"class":302},"({ error: ",[278,2661,2662],{"class":309},"\"Method not allowed\"",[278,2664,2665],{"class":302}," }), {\n",[278,2667,2668,2671,2674],{"class":280,"line":663},[278,2669,2670],{"class":302},"      status: ",[278,2672,2673],{"class":650},"405",[278,2675,660],{"class":302},[278,2677,2678],{"class":280,"line":669},[278,2679,1147],{"class":302},[278,2681,2682,2684,2686,2688],{"class":280,"line":680},[278,2683,1152],{"class":309},[278,2685,1155],{"class":302},[278,2687,2599],{"class":309},[278,2689,660],{"class":302},[278,2691,2692],{"class":280,"line":686},[278,2693,1165],{"class":302},[278,2695,2696],{"class":280,"line":1334},[278,2697,1233],{"class":302},[278,2699,2700,2702,2704],{"class":280,"line":1375},[278,2701,1397],{"class":302},[278,2703,1400],{"class":298},[278,2705,1403],{"class":302},[278,2707,2708,2710,2712,2714,2717],{"class":280,"line":1381},[278,2709,1409],{"class":302},[278,2711,1412],{"class":333},[278,2713,1126],{"class":302},[278,2715,2716],{"class":309},"\"Error:\"",[278,2718,1420],{"class":302},[278,2720,2721,2723,2725,2727],{"class":280,"line":1386},[278,2722,1088],{"class":298},[278,2724,1258],{"class":298},[278,2726,2648],{"class":333},[278,2728,770],{"class":302},[278,2730,2731,2734,2736,2738],{"class":280,"line":1394},[278,2732,2733],{"class":650},"      JSON",[278,2735,183],{"class":302},[278,2737,2235],{"class":333},[278,2739,637],{"class":302},[278,2741,2742,2745,2748,2750,2753,2756,2758,2761],{"class":280,"line":1406},[278,2743,2744],{"class":302},"        error: error ",[278,2746,2747],{"class":298},"instanceof",[278,2749,1261],{"class":333},[278,2751,2752],{"class":298}," ?",[278,2754,2755],{"class":302}," error.message ",[278,2757,960],{"class":298},[278,2759,2760],{"class":309}," \"Internal server error\"",[278,2762,660],{"class":302},[278,2764,2765],{"class":280,"line":1423},[278,2766,1228],{"class":302},[278,2768,2769],{"class":280,"line":1432},[278,2770,2771],{"class":302},"      {\n",[278,2773,2774,2777,2780],{"class":280,"line":1437},[278,2775,2776],{"class":302},"        status: ",[278,2778,2779],{"class":650},"500",[278,2781,660],{"class":302},[278,2783,2784],{"class":280,"line":1916},[278,2785,2786],{"class":302},"        headers: {\n",[278,2788,2789,2791,2793,2795],{"class":280,"line":1939},[278,2790,2594],{"class":309},[278,2792,1155],{"class":302},[278,2794,2599],{"class":309},[278,2796,660],{"class":302},[278,2798,2799],{"class":280,"line":1949},[278,2800,2606],{"class":302},[278,2802,2803],{"class":280,"line":1954},[278,2804,1165],{"class":302},[278,2806,2807],{"class":280,"line":1959},[278,2808,1898],{"class":302},[278,2810,2811],{"class":280,"line":1985},[278,2812,1096],{"class":302},[278,2814,2815],{"class":280,"line":1990},[278,2816,2817],{"class":302},"};\n",[11,2819,2820],{},"This Netlify function is called by the client in the following way:",[269,2822,2824],{"className":271,"code":2823,"language":273,"meta":274,"style":274},"\u002F\u002F src\u002Flib\u002Fapi.ts\n\nexport async function getFeatureFlags() {\n  try {\n    const response = await fetch(\"\u002F.netlify\u002Ffunctions\u002Ffeature-flags\");\n    if (!response.ok) {\n      throw new Error(\"Failed to fetch flags\");\n    }\n\n    const data = await response.json();\n    return { success: true, data: data.features as Feature[] };\n  } catch (error) {\n    return {\n      success: false,\n      error: error instanceof Error ? error.message : \"Unknown error\",\n    };\n  }\n}\n\n\u002F\u002F and, so on...\n",[59,2825,2826,2831,2835,2848,2854,2873,2883,2898,2902,2906,2922,2944,2952,2958,2968,2988,2992,2996,3000,3004],{"__ignoreMap":274},[278,2827,2828],{"class":280,"line":281},[278,2829,2830],{"class":284},"\u002F\u002F src\u002Flib\u002Fapi.ts\n",[278,2832,2833],{"class":280,"line":288},[278,2834,292],{"emptyLinePlaceholder":291},[278,2836,2837,2839,2841,2843,2846],{"class":280,"line":295},[278,2838,628],{"class":298},[278,2840,2325],{"class":298},[278,2842,748],{"class":298},[278,2844,2845],{"class":333}," getFeatureFlags",[278,2847,337],{"class":302},[278,2849,2850,2852],{"class":280,"line":316},[278,2851,1105],{"class":298},[278,2853,876],{"class":302},[278,2855,2856,2858,2860,2862,2864,2866,2868,2871],{"class":280,"line":322},[278,2857,1112],{"class":298},[278,2859,1115],{"class":650},[278,2861,764],{"class":298},[278,2863,1120],{"class":298},[278,2865,1123],{"class":333},[278,2867,1126],{"class":302},[278,2869,2870],{"class":309},"\"\u002F.netlify\u002Ffunctions\u002Ffeature-flags\"",[278,2872,1280],{"class":302},[278,2874,2875,2877,2879,2881],{"class":280,"line":327},[278,2876,1242],{"class":298},[278,2878,1245],{"class":302},[278,2880,1209],{"class":298},[278,2882,1250],{"class":302},[278,2884,2885,2887,2889,2891,2893,2896],{"class":280,"line":340},[278,2886,1255],{"class":298},[278,2888,1258],{"class":298},[278,2890,1261],{"class":333},[278,2892,1126],{"class":302},[278,2894,2895],{"class":309},"\"Failed to fetch flags\"",[278,2897,1280],{"class":302},[278,2899,2900],{"class":280,"line":349},[278,2901,1285],{"class":302},[278,2903,2904],{"class":280,"line":375},[278,2905,292],{"emptyLinePlaceholder":291},[278,2907,2908,2910,2912,2914,2916,2918,2920],{"class":280,"line":386},[278,2909,1112],{"class":298},[278,2911,1296],{"class":650},[278,2913,764],{"class":298},[278,2915,1120],{"class":298},[278,2917,1307],{"class":302},[278,2919,1310],{"class":333},[278,2921,1313],{"class":302},[278,2923,2924,2926,2929,2932,2935,2938,2941],{"class":280,"line":397},[278,2925,1088],{"class":298},[278,2927,2928],{"class":302}," { success: ",[278,2930,2931],{"class":650},"true",[278,2933,2934],{"class":302},", data: data.features ",[278,2936,2937],{"class":298},"as",[278,2939,2940],{"class":333}," Feature",[278,2942,2943],{"class":302},"[] };\n",[278,2945,2946,2948,2950],{"class":280,"line":408},[278,2947,1397],{"class":302},[278,2949,1400],{"class":298},[278,2951,1403],{"class":302},[278,2953,2954,2956],{"class":280,"line":433},[278,2955,1088],{"class":298},[278,2957,876],{"class":302},[278,2959,2960,2963,2966],{"class":280,"line":454},[278,2961,2962],{"class":302},"      success: ",[278,2964,2965],{"class":650},"false",[278,2967,660],{"class":302},[278,2969,2970,2973,2975,2977,2979,2981,2983,2986],{"class":280,"line":475},[278,2971,2972],{"class":302},"      error: error ",[278,2974,2747],{"class":298},[278,2976,1261],{"class":333},[278,2978,2752],{"class":298},[278,2980,2755],{"class":302},[278,2982,960],{"class":298},[278,2984,2985],{"class":309}," \"Unknown error\"",[278,2987,660],{"class":302},[278,2989,2990],{"class":280,"line":496},[278,2991,1378],{"class":302},[278,2993,2994],{"class":280,"line":505},[278,2995,1096],{"class":302},[278,2997,2998],{"class":280,"line":516},[278,2999,617],{"class":302},[278,3001,3002],{"class":280,"line":527},[278,3003,292],{"emptyLinePlaceholder":291},[278,3005,3006],{"class":280,"line":533},[278,3007,3008],{"class":284},"\u002F\u002F and, so on...\n",[11,3010,3011,3012,3015,3016,3019],{},"The above code snippets capture how the ",[59,3013,3014],{},"DevCycle SDK"," and its ",[59,3017,3018],{},"Management APIs"," are used within the app. You can go through the shared source code to view the complete implementation in more detail.",[24,3021,3023],{"id":3022},"wrapping-up","Wrapping Up",[11,3025,3026],{},"Brew Haven isn’t just a coffee shop app — it’s a showcase of how feature flags can make your projects more dynamic, responsive, and fun.",[11,3028,3029],{},"Feature flags allow you to:",[71,3031,3032,3035,3038,3041],{},[74,3033,3034],{},"Do gradual rollouts.",[74,3036,3037],{},"Reduce deployment risks.",[74,3039,3040],{},"Enable rapid experimentation.",[74,3042,3043],{},"Personalize user experiences.",[11,3045,3046],{},"Next time you’re sipping coffee and dreaming big, think about how feature flags can brew innovation into your projects. ☕",[3048,3049],"hr",{},[11,3051,3052],{},"Thank you for sticking with me until the end! I hope you’ve picked up some new concepts along the way. I’d love to hear what you learned or any thoughts you have in the comments section. Your feedback is not only valuable to me, but to the entire developer community exploring this exciting field.",[11,3054,3055],{},"Until next time.",[18,3057,3058],{},[11,3059,3060],{},[3061,3062,3063],"em",{},"Keep adding the bits, and soon you'll have a lot of bytes to share with the world.",[3065,3066,3067],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":274,"searchDepth":288,"depth":288,"links":3069},[3070,3074,3078,3082],{"id":26,"depth":288,"text":27,"children":3071},[3072,3073],{"id":34,"depth":295,"text":35},{"id":65,"depth":295,"text":66},{"id":88,"depth":288,"text":89,"children":3075},[3076,3077],{"id":117,"depth":295,"text":118},{"id":211,"depth":295,"text":212},{"id":241,"depth":288,"text":242,"children":3079},[3080,3081],{"id":251,"depth":295,"text":252},{"id":911,"depth":295,"text":912},{"id":3022,"depth":288,"text":3023},"\u002Fimages\u002Fposts\u002Fbuilding-brew-haven-ab-testing-my-coffee-shop-dreams-with-devcycle\u002F1b223b14-15c4-401a-b08c-bb3816a5a85a-8d7979e641.png","2024-12-29T11:09:52.011Z","Do you love coffee? As developers, many of us jokingly claim to be \"powered by coffee\", and the thought of opening a quaint coffee shop someday often lingers in the back of our...",false,"md","cm59idr0r003e09kvhj3h7q4c",{},"\u002Fbuilding-brew-haven-ab-testing-my-coffee-shop-dreams-with-devcycle",{"title":6,"description":3085},"building-brew-haven-ab-testing-my-coffee-shop-dreams-with-devcycle",[3094,3095,3096,3097],"webdev","reactjs","feature-flags","devcycle","9x8wgHo3qK6g88SwsaBh4aPCpWdubLWTTnuy1OkW_e0",{"id":3100,"title":3101,"body":3102,"cover":10699,"date":10700,"description":10701,"draft":3086,"extension":3087,"hashnodeId":10702,"meta":10703,"navigation":291,"path":10704,"seo":10705,"slug":10706,"stem":10706,"tags":10707,"__hash__":10713},"posts\u002Fbuilding-voice-notes-app-with-ai-transcription-and-post-processing.md","Building Vhisper: Voice Notes App with AI Transcription and Post-Processing",{"type":8,"value":3103,"toc":10655},[3104,3119,3126,3132,3139,3141,3147,3173,3176,3179,3186,3199,3203,3206,3264,3268,3271,3299,3323,3330,3333,3368,3379,3399,3402,3485,3492,3694,3697,3712,3722,3748,3752,3755,3767,3781,3803,3807,3810,3813,3817,3820,3839,3842,3849,3860,4147,4150,4184,4187,4201,4207,4218,4347,4353,4381,4388,4396,4407,4480,4487,4497,4794,4800,4809,4921,4934,4941,4947,4960,4964,5226,5229,5233,5386,5393,5398,5407,5435,5445,5666,5677,5681,5688,5730,5741,5747,5750,5754,5757,5764,5773,7074,7076,7115,7122,7130,7692,7695,7739,7744,7752,8680,8683,8726,8731,8734,8744,8751,9053,9058,9071,9424,9438,9563,9567,9574,9790,9793,9815,9818,9826,9915,9926,9969,9974,9977,9985,10337,10340,10343,10349,10353,10356,10365,10437,10451,10518,10530,10533,10537,10540,10543,10561,10564,10568,10571,10575,10586,10593,10597,10611,10618,10622,10628,10631,10635,10638,10640,10643,10645,10652],[11,3105,3106,3107,3112,3113,3118],{},"After wrapping up my last project—a ",[47,3108,3111],{"href":3109,"rel":3110},"https:\u002F\u002Frajeev.dev\u002Fbuilding-a-chat-interface-to-search-github",[51],"chat interface to search GitHub","—I found myself searching for the next idea to tackle. As a developer, inspiration often comes unexpectedly, and this time, it struck while scrolling through my GitHub feed. A repo, starred by Daniel Roe (Nuxt Core Team Lead), caught my eye. It was an ",[47,3114,3117],{"href":3115,"rel":3116},"https:\u002F\u002Fgithub.com\u002Fegoist\u002Fwhispo",[51],"Electron-based voice notes app"," designed for macOS.",[11,3120,3121,3122,3125],{},"Something about the simplicity of voice notes combined with the technical challenge intrigued me. Could I take this concept further? Could I build a modern, AI-powered voice notes app using web technologies? That urge to build led me here, to this blog post, where I’ll walk you through building ",[94,3123,3124],{},"Vhisper",", a voice notes app with AI transcription and post-processing, built with the Nuxt ecosystem and powered by Cloudflare.",[11,3127,3128,3129],{},"And before you say it, I must make a confession: ",[3061,3130,3131],{},"“Hi! My name is Rajeev, and I am addicted to talking\u002Fchatting.”.",[11,3133,3134],{},[3135,3136],"img",{"alt":3137,"src":3138},"A bear confessing to its addiction","https:\u002F\u002Fi.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExNDJzcGdtYWJkeHhobDRzYzJ3MzhmZG8zNHczcThobmcxZnVrd3EyZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002FC4CI97S8sKstW\u002Fgiphy.gif",[24,3140,27],{"id":26},[11,3142,3143,3144,3146],{},"Now that the formalities are done, let’s focus on what we’ll be building in this project. The goal is to create ",[94,3145,3124],{},", a web-based voice notes application with the following core features:",[71,3148,3149,3155,3161,3167],{},[74,3150,3151,3154],{},[94,3152,3153],{},"Recording Voice Notes",": Users can record voice notes directly in the browser.",[74,3156,3157,3160],{},[94,3158,3159],{},"AI-Powered Transcription",": Each recording is processed via Cloudflare Workers AI, converting speech to text.",[74,3162,3163,3166],{},[94,3164,3165],{},"Post-Processing with Custom Prompts",": Users can customize how transcriptions are refined using an AI-driven post-processing step.",[74,3168,3169,3172],{},[94,3170,3171],{},"Seamless Data Management (CRUD)",": Notes and audio files are efficiently stored using Cloudflare’s D1 database and R2 storage.",[11,3174,3175],{},"To give you a better sense of what we’re aiming for, here’s a quick demo showcasing Vhisper’s main features:",[40,3177],{"url":3178},"https:\u002F\u002Fyoutu.be\u002F7IcwTIrHIPg",[11,3180,3181,3182],{},"You can experience it live here: ",[47,3183,3184],{"href":3184,"rel":3185},"https:\u002F\u002Fvhisper.nuxt.dev",[51],[11,3187,3188,3189,1708,3192,919,3195,3198],{},"By the end of this guide, you’ll know exactly how to build and deploy this voice notes app using ",[94,3190,3191],{},"Nuxt",[94,3193,3194],{},"NuxtHub",[94,3196,3197],{},"Cloudflare services","—a stack that combines innovation with developer-first simplicity. Ready to build it? Let’s get started!",[24,3200,3202],{"id":3201},"project-setup","Project Setup",[11,3204,3205],{},"Before setting up the project let’s review the technologies used to build this app:",[123,3207,3208,3215,3223,3231,3239,3256],{},[74,3209,3210,3214],{},[47,3211,3191],{"href":3212,"rel":3213},"https:\u002F\u002Fnuxt.com",[51],": Vue.js framework for the application foundation",[74,3216,3217,3222],{},[47,3218,3221],{"href":3219,"rel":3220},"https:\u002F\u002Fui3.nuxt.com",[51],"Nuxt UI (v3)",": For creating a polished and professional frontend",[74,3224,3225,3230],{},[47,3226,3229],{"href":3227,"rel":3228},"https:\u002F\u002Form.drizzle.team",[51],"Drizzle",": Database ORM",[74,3232,3233,3238],{},[47,3234,3237],{"href":3235,"rel":3236},"https:\u002F\u002Fzod.dev",[51],"Zod",": For client\u002Fserver side data validation",[74,3240,3241,3245,3246,1708,3249,1708,3252,3255],{},[47,3242,3194],{"href":3243,"rel":3244},"https:\u002F\u002Fhub.nuxt.com",[51],": Backend (",[59,3247,3248],{},"database",[59,3250,3251],{},"storage",[59,3253,3254],{},"AI"," etc.), deployment and administration platform for Nuxt",[74,3257,3258,3263],{},[47,3259,3262],{"href":3260,"rel":3261},"https:\u002F\u002Fdevelopers.cloudflare.com",[51],"Cloudflare",": Powers NuxtHub to provide various services",[32,3265,3267],{"id":3266},"prerequisites","Prerequisites",[11,3269,3270],{},"To follow along, apart from basic necessities like Node.js, npm, and some Nuxt knowledge, you’ll need:",[123,3272,3273,3287],{},[74,3274,3275,3276,3279,3280,183],{},"A ",[94,3277,3278],{},"Cloudflare account"," to use Workers AI and deploy your project. If you don’t have one, you can set it up ",[47,3281,3284],{"href":3282,"rel":3283},"https:\u002F\u002Fwww.cloudflare.com\u002F",[51],[94,3285,3286],{},"here",[74,3288,3275,3289,3292,3293,183],{},[94,3290,3291],{},"NuxtHub Admin Account"," for managing apps via the NuxtHub dashboard. Sign up ",[47,3294,3297],{"href":3295,"rel":3296},"https:\u002F\u002Fadmin.hub.nuxt.com\u002F",[51],[94,3298,3286],{},[3300,3301,3303,3307],"div",{"dataNodeType":3302},"callout",[3300,3304,3306],{"dataNodeType":3305},"callout-emoji","ℹ",[3300,3308,3310,3313,3314,183],{"dataNodeType":3309},"callout-text",[94,3311,3312],{},"Note:"," Workers AI models will run in your Cloudflare account even during local development. Check out their ",[47,3315,3322],{"target":3316,"rel":3317,"href":3320,"style":3321},"_self",[3318,3319,51],"noopener","noreferrer","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers-ai\u002Fplatform\u002Fpricing","pointer-events: none","pricing and free quota",[32,3324,3326,3327],{"id":3325},"project-init","Project ",[94,3328,3329],{},"Init",[11,3331,3332],{},"We’ll start with the NuxtHub starter template. Run the following command to create and navigate to your new project directory:",[269,3334,3338],{"className":3335,"code":3336,"language":3337,"meta":274,"style":274},"language-bash shiki shiki-themes github-light github-dark","# Create project and change into the project dir\nnpx nuxthub init voice-notes && cd $_\n","bash",[59,3339,3340,3345],{"__ignoreMap":274},[278,3341,3342],{"class":280,"line":281},[278,3343,3344],{"class":284},"# Create project and change into the project dir\n",[278,3346,3347,3350,3353,3356,3359,3362,3365],{"class":280,"line":288},[278,3348,3349],{"class":333},"npx",[278,3351,3352],{"class":309}," nuxthub",[278,3354,3355],{"class":309}," init",[278,3357,3358],{"class":309}," voice-notes",[278,3360,3361],{"class":302}," && ",[278,3363,3364],{"class":650},"cd",[278,3366,3367],{"class":650}," $_\n",[11,3369,3370,3371,3374,3375,3378],{},"If you plan to use ",[94,3372,3373],{},"pnpm"," as your package manager, add a ",[59,3376,3377],{},".npmrc"," file at the root of your project with this line to hoist dependencies:",[269,3380,3382],{"className":3335,"code":3381,"language":3337,"meta":274,"style":274},"# .npmrc\nshamefully-hoist=true\n",[59,3383,3384,3389],{"__ignoreMap":274},[278,3385,3386],{"class":280,"line":281},[278,3387,3388],{"class":284},"# .npmrc\n",[278,3390,3391,3394,3396],{"class":280,"line":288},[278,3392,3393],{"class":302},"shamefully-hoist",[278,3395,358],{"class":298},[278,3397,3398],{"class":309},"true\n",[11,3400,3401],{},"Now, install the dependencies:",[123,3403,3404,3422,3445,3465],{},[74,3405,3406,3407],{},"Nuxt modules:",[269,3408,3410],{"className":3335,"code":3409,"language":3337,"meta":274,"style":274},"pnpm add @nuxt\u002Fui@next\n",[59,3411,3412],{"__ignoreMap":274},[278,3413,3414,3416,3419],{"class":280,"line":281},[278,3415,3373],{"class":333},[278,3417,3418],{"class":309}," add",[278,3420,3421],{"class":309}," @nuxt\u002Fui@next\n",[74,3423,3424,3425],{},"Drizzle and related tools:",[269,3426,3428],{"className":3335,"code":3427,"language":3337,"meta":274,"style":274},"pnpm add drizzle-orm drizzle-zod @vueuse\u002Fcore\n",[59,3429,3430],{"__ignoreMap":274},[278,3431,3432,3434,3436,3439,3442],{"class":280,"line":281},[278,3433,3373],{"class":333},[278,3435,3418],{"class":309},[278,3437,3438],{"class":309}," drizzle-orm",[278,3440,3441],{"class":309}," drizzle-zod",[278,3443,3444],{"class":309}," @vueuse\u002Fcore\n",[74,3446,3447,3448],{},"Icon packs:",[269,3449,3451],{"className":3335,"code":3450,"language":3337,"meta":274,"style":274},"pnpm add @iconify-json\u002Flucide @iconify-json\u002Fsimple-icons\n",[59,3452,3453],{"__ignoreMap":274},[278,3454,3455,3457,3459,3462],{"class":280,"line":281},[278,3456,3373],{"class":333},[278,3458,3418],{"class":309},[278,3460,3461],{"class":309}," @iconify-json\u002Flucide",[278,3463,3464],{"class":309}," @iconify-json\u002Fsimple-icons\n",[74,3466,3467,3468],{},"Dev dependencies:",[269,3469,3471],{"className":3335,"code":3470,"language":3337,"meta":274,"style":274},"pnpm add -D drizzle-kit\n",[59,3472,3473],{"__ignoreMap":274},[278,3474,3475,3477,3479,3482],{"class":280,"line":281},[278,3476,3373],{"class":333},[278,3478,3418],{"class":309},[278,3480,3481],{"class":650}," -D",[278,3483,3484],{"class":309}," drizzle-kit\n",[11,3486,3487,3488,3491],{},"Update your ",[59,3489,3490],{},"nuxt.config.ts"," file as follows:",[269,3493,3495],{"className":929,"code":3494,"language":931,"meta":274,"style":274},"export default defineNuxtConfig({\n  modules: [\"@nuxthub\u002Fcore\", \"@nuxt\u002Feslint\", \"nuxt-auth-utils\", \"@nuxt\u002Fui\"],\n\n  devtools: { enabled: true },\n\n  runtimeConfig: {\n    public: {\n      helloText: \"Hello from the Edge 👋\",\n    },\n  },\n\n  future: { compatibilityVersion: 4 },\n  compatibilityDate: \"2024-07-30\",\n\n  hub: {\n    ai: true,\n    blob: true,\n    database: true,\n  },\n\n  css: [\"~\u002Fassets\u002Fcss\u002Fmain.css\"],\n\n  eslint: {\n    config: {\n      stylistic: false,\n    },\n  },\n});\n",[59,3496,3497,3508,3534,3538,3548,3552,3557,3562,3572,3576,3580,3584,3594,3604,3608,3613,3622,3631,3640,3644,3648,3658,3662,3667,3672,3681,3685,3689],{"__ignoreMap":274},[278,3498,3499,3501,3503,3506],{"class":280,"line":281},[278,3500,628],{"class":298},[278,3502,631],{"class":298},[278,3504,3505],{"class":333}," defineNuxtConfig",[278,3507,637],{"class":302},[278,3509,3510,3513,3516,3518,3521,3523,3526,3528,3531],{"class":280,"line":288},[278,3511,3512],{"class":302},"  modules: [",[278,3514,3515],{"class":309},"\"@nuxthub\u002Fcore\"",[278,3517,1708],{"class":302},[278,3519,3520],{"class":309},"\"@nuxt\u002Feslint\"",[278,3522,1708],{"class":302},[278,3524,3525],{"class":309},"\"nuxt-auth-utils\"",[278,3527,1708],{"class":302},[278,3529,3530],{"class":309},"\"@nuxt\u002Fui\"",[278,3532,3533],{"class":302},"],\n",[278,3535,3536],{"class":280,"line":295},[278,3537,292],{"emptyLinePlaceholder":291},[278,3539,3540,3543,3545],{"class":280,"line":316},[278,3541,3542],{"class":302},"  devtools: { enabled: ",[278,3544,2931],{"class":650},[278,3546,3547],{"class":302}," },\n",[278,3549,3550],{"class":280,"line":322},[278,3551,292],{"emptyLinePlaceholder":291},[278,3553,3554],{"class":280,"line":327},[278,3555,3556],{"class":302},"  runtimeConfig: {\n",[278,3558,3559],{"class":280,"line":340},[278,3560,3561],{"class":302},"    public: {\n",[278,3563,3564,3567,3570],{"class":280,"line":349},[278,3565,3566],{"class":302},"      helloText: ",[278,3568,3569],{"class":309},"\"Hello from the Edge 👋\"",[278,3571,660],{"class":302},[278,3573,3574],{"class":280,"line":375},[278,3575,2243],{"class":302},[278,3577,3578],{"class":280,"line":386},[278,3579,683],{"class":302},[278,3581,3582],{"class":280,"line":397},[278,3583,292],{"emptyLinePlaceholder":291},[278,3585,3586,3589,3592],{"class":280,"line":408},[278,3587,3588],{"class":302},"  future: { compatibilityVersion: ",[278,3590,3591],{"class":650},"4",[278,3593,3547],{"class":302},[278,3595,3596,3599,3602],{"class":280,"line":433},[278,3597,3598],{"class":302},"  compatibilityDate: ",[278,3600,3601],{"class":309},"\"2024-07-30\"",[278,3603,660],{"class":302},[278,3605,3606],{"class":280,"line":454},[278,3607,292],{"emptyLinePlaceholder":291},[278,3609,3610],{"class":280,"line":475},[278,3611,3612],{"class":302},"  hub: {\n",[278,3614,3615,3618,3620],{"class":280,"line":496},[278,3616,3617],{"class":302},"    ai: ",[278,3619,2931],{"class":650},[278,3621,660],{"class":302},[278,3623,3624,3627,3629],{"class":280,"line":505},[278,3625,3626],{"class":302},"    blob: ",[278,3628,2931],{"class":650},[278,3630,660],{"class":302},[278,3632,3633,3636,3638],{"class":280,"line":516},[278,3634,3635],{"class":302},"    database: ",[278,3637,2931],{"class":650},[278,3639,660],{"class":302},[278,3641,3642],{"class":280,"line":527},[278,3643,683],{"class":302},[278,3645,3646],{"class":280,"line":533},[278,3647,292],{"emptyLinePlaceholder":291},[278,3649,3650,3653,3656],{"class":280,"line":539},[278,3651,3652],{"class":302},"  css: [",[278,3654,3655],{"class":309},"\"~\u002Fassets\u002Fcss\u002Fmain.css\"",[278,3657,3533],{"class":302},[278,3659,3660],{"class":280,"line":545},[278,3661,292],{"emptyLinePlaceholder":291},[278,3663,3664],{"class":280,"line":551},[278,3665,3666],{"class":302},"  eslint: {\n",[278,3668,3669],{"class":280,"line":557},[278,3670,3671],{"class":302},"    config: {\n",[278,3673,3674,3677,3679],{"class":280,"line":567},[278,3675,3676],{"class":302},"      stylistic: ",[278,3678,2965],{"class":650},[278,3680,660],{"class":302},[278,3682,3683],{"class":280,"line":577},[278,3684,2243],{"class":302},[278,3686,3687],{"class":280,"line":587},[278,3688,683],{"class":302},[278,3690,3691],{"class":280,"line":597},[278,3692,3693],{"class":302},"});\n",[11,3695,3696],{},"We’ve made the following changes to the Nuxt config file:",[123,3698,3699,3702,3705],{},[74,3700,3701],{},"Updated the Nuxt modules used in the app",[74,3703,3704],{},"Enabled required NuxtHub features",[74,3706,3707,3708,3711],{},"And, added the ",[59,3709,3710],{},"main.css"," file path.",[11,3713,3714,3715,3717,3718,3721],{},"Create the ",[59,3716,3710],{}," file in the ",[59,3719,3720],{},"app\u002Fassets\u002Fcss"," folder with this content:",[269,3723,3727],{"className":3724,"code":3725,"language":3726,"meta":274,"style":274},"language-css shiki shiki-themes github-light github-dark","@import \"tailwindcss\";\n@import \"@nuxt\u002Fui\";\n","css",[59,3728,3729,3739],{"__ignoreMap":274},[278,3730,3731,3734,3737],{"class":280,"line":281},[278,3732,3733],{"class":298},"@import",[278,3735,3736],{"class":309}," \"tailwindcss\"",[278,3738,313],{"class":302},[278,3740,3741,3743,3746],{"class":280,"line":288},[278,3742,3733],{"class":298},[278,3744,3745],{"class":309}," \"@nuxt\u002Fui\"",[278,3747,313],{"class":302},[32,3749,3751],{"id":3750},"testing-the-setup","Testing the Setup",[11,3753,3754],{},"Run the development server:",[269,3756,3758],{"className":3335,"code":3757,"language":3337,"meta":274,"style":274},"pnpm dev\n",[59,3759,3760],{"__ignoreMap":274},[278,3761,3762,3764],{"class":280,"line":281},[278,3763,3373],{"class":333},[278,3765,3766],{"class":309}," dev\n",[11,3768,3769,3770,3776,3777,3780],{},"Visit ",[47,3771,3774],{"href":3772,"rel":3773},"http:\u002F\u002Flocalhost:3000",[51],[59,3775,3772],{}," in your browser. If everything is set up correctly, you’ll see the message: ",[3061,3778,3779],{},"“Hello from the Edge 👋”"," with a refresh button.",[3300,3782,3783,3786],{"dataNodeType":3302},[3300,3784,3785],{"dataNodeType":3305},"💡",[3300,3787,3788,3791,3792,919,3795,3798,3799,3802],{"dataNodeType":3309},[94,3789,3790],{},"Troubleshooting Tip:"," If you encounter issues with TailwindCSS, try deleting ",[59,3793,3794],{},"node_modules",[59,3796,3797],{},"pnpm-lock.yaml",", and then run ",[59,3800,3801],{},"pnpm install"," to re-install the dependecies.",[24,3804,3806],{"id":3805},"building-the-basic-backend","Building the Basic Backend",[11,3808,3809],{},"With the project setup complete, let’s dive into building the backend. We’ll begin by creating API endpoints to handle core functionalities, followed by configuring the database and integrating validation.",[11,3811,3812],{},"But before jumping to code, let’s understand how you’ll interact with various Cloudflare offerings. If you’ve been attentive, you should know the answer, NuxrHub, but what is NuxtHub?",[32,3814,3816],{"id":3815},"what-is-nuxthub","What is NuxtHub?",[11,3818,3819],{},"NuxtHub is a developer-friendly interface built on top of Cloudflare’s robust services. It simplifies the process of creating, binding, and managing services for your project, offering a seamless development experience (DX).",[11,3821,3822,3823,3826,3827,3830,3831,3834,3835,3838],{},"You started with a NuxtHub template, so the project comes preconfigured with the ",[59,3824,3825],{},"@nuxthub\u002Fcore"," module. During the setup, you also enabled the required Cloudflare services: AI, Database, and Blob. The NuxtHub core module exposes these services through interfaces prefixed with ",[59,3828,3829],{},"hub",". For example, ",[59,3832,3833],{},"hubAI"," is used for AI features, ",[59,3836,3837],{},"hubBlob"," for object storage, and so on.",[11,3840,3841],{},"Time is ripe now to work on the first API endpoint.",[32,3843,3845,3848],{"id":3844},"apitranscribe-endpoint",[59,3846,3847],{},"\u002Fapi\u002Ftranscribe"," Endpoint",[11,3850,3851,3852,3855,3856,3859],{},"Create a new file named ",[59,3853,3854],{},"transcribe.post.ts"," inside the ",[59,3857,3858],{},"server\u002Fapi"," directory, and add the following code to it:",[269,3861,3863],{"className":271,"code":3862,"language":273,"meta":274,"style":274},"\u002F\u002F server\u002Fapi\u002Ftranscribe.post.ts \nexport default defineEventHandler(async (event) => {\n  const form = await readFormData(event);\n  const blob = form.get(\"audio\") as Blob;\n  if (!blob) {\n    throw createError({\n      statusCode: 400,\n      message: \"Missing audio blob to transcribe\",\n    });\n  }\n\n  ensureBlob(blob, { maxSize: \"8MB\", types: [\"audio\"] });\n\n  try {\n    const response = await hubAI().run(\"@cf\u002Fopenai\u002Fwhisper\", {\n      audio: [...new Uint8Array(await blob.arrayBuffer())],\n    });\n\n    return response.text;\n  } catch (err) {\n    console.error(\"Error transcribing audio:\", err);\n    throw createError({\n      statusCode: 500,\n      message: \"Failed to transcribe audio. Please try again.\",\n    });\n  }\n});\n",[59,3864,3865,3870,3894,3911,3940,3951,3960,3970,3980,3984,3988,3992,4011,4015,4021,4047,4072,4076,4080,4087,4096,4110,4118,4126,4135,4139,4143],{"__ignoreMap":274},[278,3866,3867],{"class":280,"line":281},[278,3868,3869],{"class":284},"\u002F\u002F server\u002Fapi\u002Ftranscribe.post.ts \n",[278,3871,3872,3874,3876,3879,3881,3883,3885,3888,3890,3892],{"class":280,"line":288},[278,3873,628],{"class":298},[278,3875,631],{"class":298},[278,3877,3878],{"class":333}," defineEventHandler",[278,3880,1126],{"class":302},[278,3882,1050],{"class":298},[278,3884,1245],{"class":302},[278,3886,3887],{"class":501},"event",[278,3889,1845],{"class":302},[278,3891,1848],{"class":298},[278,3893,876],{"class":302},[278,3895,3896,3898,3901,3903,3905,3908],{"class":280,"line":295},[278,3897,758],{"class":298},[278,3899,3900],{"class":650}," form",[278,3902,764],{"class":298},[278,3904,1120],{"class":298},[278,3906,3907],{"class":333}," readFormData",[278,3909,3910],{"class":302},"(event);\n",[278,3912,3913,3915,3918,3920,3923,3926,3928,3931,3933,3935,3938],{"class":280,"line":316},[278,3914,758],{"class":298},[278,3916,3917],{"class":650}," blob",[278,3919,764],{"class":298},[278,3921,3922],{"class":302}," form.",[278,3924,3925],{"class":333},"get",[278,3927,1126],{"class":302},[278,3929,3930],{"class":309},"\"audio\"",[278,3932,1845],{"class":302},[278,3934,2937],{"class":298},[278,3936,3937],{"class":333}," Blob",[278,3939,313],{"class":302},[278,3941,3942,3944,3946,3948],{"class":280,"line":322},[278,3943,1062],{"class":298},[278,3945,1245],{"class":302},[278,3947,1209],{"class":298},[278,3949,3950],{"class":302},"blob) {\n",[278,3952,3953,3955,3958],{"class":280,"line":327},[278,3954,1426],{"class":298},[278,3956,3957],{"class":333}," createError",[278,3959,637],{"class":302},[278,3961,3962,3965,3968],{"class":280,"line":340},[278,3963,3964],{"class":302},"      statusCode: ",[278,3966,3967],{"class":650},"400",[278,3969,660],{"class":302},[278,3971,3972,3975,3978],{"class":280,"line":349},[278,3973,3974],{"class":302},"      message: ",[278,3976,3977],{"class":309},"\"Missing audio blob to transcribe\"",[278,3979,660],{"class":302},[278,3981,3982],{"class":280,"line":375},[278,3983,1233],{"class":302},[278,3985,3986],{"class":280,"line":386},[278,3987,1096],{"class":302},[278,3989,3990],{"class":280,"line":397},[278,3991,292],{"emptyLinePlaceholder":291},[278,3993,3994,3997,4000,4003,4006,4008],{"class":280,"line":408},[278,3995,3996],{"class":333},"  ensureBlob",[278,3998,3999],{"class":302},"(blob, { maxSize: ",[278,4001,4002],{"class":309},"\"8MB\"",[278,4004,4005],{"class":302},", types: [",[278,4007,3930],{"class":309},[278,4009,4010],{"class":302},"] });\n",[278,4012,4013],{"class":280,"line":433},[278,4014,292],{"emptyLinePlaceholder":291},[278,4016,4017,4019],{"class":280,"line":454},[278,4018,1105],{"class":298},[278,4020,876],{"class":302},[278,4022,4023,4025,4027,4029,4031,4034,4037,4040,4042,4045],{"class":280,"line":475},[278,4024,1112],{"class":298},[278,4026,1115],{"class":650},[278,4028,764],{"class":298},[278,4030,1120],{"class":298},[278,4032,4033],{"class":333}," hubAI",[278,4035,4036],{"class":302},"().",[278,4038,4039],{"class":333},"run",[278,4041,1126],{"class":302},[278,4043,4044],{"class":309},"\"@cf\u002Fopenai\u002Fwhisper\"",[278,4046,1132],{"class":302},[278,4048,4049,4052,4055,4058,4060,4063,4066,4069],{"class":280,"line":496},[278,4050,4051],{"class":302},"      audio: [",[278,4053,4054],{"class":298},"...new",[278,4056,4057],{"class":333}," Uint8Array",[278,4059,1126],{"class":302},[278,4061,4062],{"class":298},"await",[278,4064,4065],{"class":302}," blob.",[278,4067,4068],{"class":333},"arrayBuffer",[278,4070,4071],{"class":302},"())],\n",[278,4073,4074],{"class":280,"line":505},[278,4075,1233],{"class":302},[278,4077,4078],{"class":280,"line":516},[278,4079,292],{"emptyLinePlaceholder":291},[278,4081,4082,4084],{"class":280,"line":527},[278,4083,1088],{"class":298},[278,4085,4086],{"class":302}," response.text;\n",[278,4088,4089,4091,4093],{"class":280,"line":533},[278,4090,1397],{"class":302},[278,4092,1400],{"class":298},[278,4094,4095],{"class":302}," (err) {\n",[278,4097,4098,4100,4102,4104,4107],{"class":280,"line":539},[278,4099,1409],{"class":302},[278,4101,1412],{"class":333},[278,4103,1126],{"class":302},[278,4105,4106],{"class":309},"\"Error transcribing audio:\"",[278,4108,4109],{"class":302},", err);\n",[278,4111,4112,4114,4116],{"class":280,"line":545},[278,4113,1426],{"class":298},[278,4115,3957],{"class":333},[278,4117,637],{"class":302},[278,4119,4120,4122,4124],{"class":280,"line":551},[278,4121,3964],{"class":302},[278,4123,2779],{"class":650},[278,4125,660],{"class":302},[278,4127,4128,4130,4133],{"class":280,"line":557},[278,4129,3974],{"class":302},[278,4131,4132],{"class":309},"\"Failed to transcribe audio. Please try again.\"",[278,4134,660],{"class":302},[278,4136,4137],{"class":280,"line":567},[278,4138,1233],{"class":302},[278,4140,4141],{"class":280,"line":577},[278,4142,1096],{"class":302},[278,4144,4145],{"class":280,"line":587},[278,4146,3693],{"class":302},[11,4148,4149],{},"The above code does the following:",[123,4151,4152,4158,4171,4181],{},[74,4153,4154,4155],{},"Parses incoming form data to extract the audio as a ",[59,4156,4157],{},"Blob",[74,4159,4160,4161,4164,4165,4167,4168],{},"Verifies that it’s an audio blob and is less than ",[59,4162,4163],{},"8MB"," in size using a ",[59,4166,3825],{}," utility function ",[59,4169,4170],{},"ensureBlob",[74,4172,4173,4174,4177,4178,4180],{},"Passes on the array buffer to the ",[59,4175,4176],{},"Whisper"," model through ",[59,4179,3833],{}," for transcription",[74,4182,4183],{},"Returns the transcribed text to the client",[11,4185,4186],{},"Before you can use Workers AI in development, you’ll need to link it to your Cloudflare project. As we’re using NuxtHub as the interface, running the following command will create\u002Flink a new or existing NuxtHub project with this project.",[269,4188,4190],{"className":3335,"code":4189,"language":3337,"meta":274,"style":274},"npx nuxthub link\n",[59,4191,4192],{"__ignoreMap":274},[278,4193,4194,4196,4198],{"class":280,"line":281},[278,4195,3349],{"class":333},[278,4197,3352],{"class":309},[278,4199,4200],{"class":309}," link\n",[32,4202,4204,3848],{"id":4203},"apiupload-endpoint",[59,4205,4206],{},"\u002Fapi\u002Fupload",[11,4208,4209,4210,4213,4214,4217],{},"Next, create an endpoint to upload the audio recordings to the R2 storage. Create a new file ",[59,4211,4212],{},"upload.put.ts"," in your ",[59,4215,4216],{},"\u002Fserver\u002Fapi"," folder and add the following code to it:",[269,4219,4221],{"className":271,"code":4220,"language":273,"meta":274,"style":274},"\u002F\u002F server\u002Fapi\u002Fupload.put.ts\nexport default defineEventHandler(async (event) => {\n  return hubBlob().handleUpload(event, {\n    formKey: \"files\",\n    multiple: true,\n    ensure: {\n      maxSize: \"8MB\",\n      types: [\"audio\"],\n    },\n    put: {\n      addRandomSuffix: true,\n      prefix: \"recordings\",\n    },\n  });\n});\n",[59,4222,4223,4228,4250,4265,4275,4284,4289,4298,4307,4311,4316,4325,4335,4339,4343],{"__ignoreMap":274},[278,4224,4225],{"class":280,"line":281},[278,4226,4227],{"class":284},"\u002F\u002F server\u002Fapi\u002Fupload.put.ts\n",[278,4229,4230,4232,4234,4236,4238,4240,4242,4244,4246,4248],{"class":280,"line":288},[278,4231,628],{"class":298},[278,4233,631],{"class":298},[278,4235,3878],{"class":333},[278,4237,1126],{"class":302},[278,4239,1050],{"class":298},[278,4241,1245],{"class":302},[278,4243,3887],{"class":501},[278,4245,1845],{"class":302},[278,4247,1848],{"class":298},[278,4249,876],{"class":302},[278,4251,4252,4254,4257,4259,4262],{"class":280,"line":295},[278,4253,343],{"class":298},[278,4255,4256],{"class":333}," hubBlob",[278,4258,4036],{"class":302},[278,4260,4261],{"class":333},"handleUpload",[278,4263,4264],{"class":302},"(event, {\n",[278,4266,4267,4270,4273],{"class":280,"line":316},[278,4268,4269],{"class":302},"    formKey: ",[278,4271,4272],{"class":309},"\"files\"",[278,4274,660],{"class":302},[278,4276,4277,4280,4282],{"class":280,"line":322},[278,4278,4279],{"class":302},"    multiple: ",[278,4281,2931],{"class":650},[278,4283,660],{"class":302},[278,4285,4286],{"class":280,"line":327},[278,4287,4288],{"class":302},"    ensure: {\n",[278,4290,4291,4294,4296],{"class":280,"line":340},[278,4292,4293],{"class":302},"      maxSize: ",[278,4295,4002],{"class":309},[278,4297,660],{"class":302},[278,4299,4300,4303,4305],{"class":280,"line":349},[278,4301,4302],{"class":302},"      types: [",[278,4304,3930],{"class":309},[278,4306,3533],{"class":302},[278,4308,4309],{"class":280,"line":375},[278,4310,2243],{"class":302},[278,4312,4313],{"class":280,"line":386},[278,4314,4315],{"class":302},"    put: {\n",[278,4317,4318,4321,4323],{"class":280,"line":397},[278,4319,4320],{"class":302},"      addRandomSuffix: ",[278,4322,2931],{"class":650},[278,4324,660],{"class":302},[278,4326,4327,4330,4333],{"class":280,"line":408},[278,4328,4329],{"class":302},"      prefix: ",[278,4331,4332],{"class":309},"\"recordings\"",[278,4334,660],{"class":302},[278,4336,4337],{"class":280,"line":433},[278,4338,2243],{"class":302},[278,4340,4341],{"class":280,"line":454},[278,4342,2037],{"class":302},[278,4344,4345],{"class":280,"line":475},[278,4346,3693],{"class":302},[11,4348,4349,4350,4352],{},"The above code uses another utility method from the NuxtHub core module to upload the incoming audio files to R2. ",[59,4351,4261],{}," does the following:",[123,4354,4355,4362,4365,4371,4378],{},[74,4356,4357,4358,4361],{},"Looks for the ",[59,4359,4360],{},"files"," key in the incoming form data to extract blob data",[74,4363,4364],{},"Supports multiple files per event",[74,4366,4367,4368,4370],{},"Ensures that the files are audio and under ",[59,4369,4163],{}," in size",[74,4372,4373,4374,4377],{},"And, finally uploads them to your R2 bucket inside ",[59,4375,4376],{},"recordings"," folder while also adding a random suffix to the final names",[74,4379,4380],{},"Returns a promise to the client that resolves once all the files are uploaded",[11,4382,4383,4384,4387],{},"Now we just need ",[59,4385,4386],{},"\u002Fnotes"," endpoints to create & fetch notes entries before the basic backend is done. But to do that we need to create the needed tables. Let’s tackle this in next section.",[32,4389,4391,4392,4395],{"id":4390},"defining-the-notes-table-schema","Defining the ",[59,4393,4394],{},"notes"," Table Schema",[11,4397,4398,4399,4402,4403,4406],{},"As we will use ",[59,4400,4401],{},"drizzle"," to manage and interact with the database, we need to configure it first. Create a new file ",[59,4404,4405],{},"drizzle.config.ts"," in the project root, and add the following to it:",[269,4408,4410],{"className":271,"code":4409,"language":273,"meta":274,"style":274},"\u002F\u002F drizzle.config.ts\nimport { defineConfig } from 'drizzle-kit';\n\nexport default defineConfig({\n  dialect: 'sqlite',\n  schema: '.\u002Fserver\u002Fdatabase\u002Fschema.ts',\n  out: '.\u002Fserver\u002Fdatabase\u002Fmigrations',\n});\n",[59,4411,4412,4417,4431,4435,4446,4456,4466,4476],{"__ignoreMap":274},[278,4413,4414],{"class":280,"line":281},[278,4415,4416],{"class":284},"\u002F\u002F drizzle.config.ts\n",[278,4418,4419,4421,4424,4426,4429],{"class":280,"line":288},[278,4420,299],{"class":298},[278,4422,4423],{"class":302}," { defineConfig } ",[278,4425,306],{"class":298},[278,4427,4428],{"class":309}," 'drizzle-kit'",[278,4430,313],{"class":302},[278,4432,4433],{"class":280,"line":295},[278,4434,292],{"emptyLinePlaceholder":291},[278,4436,4437,4439,4441,4444],{"class":280,"line":316},[278,4438,628],{"class":298},[278,4440,631],{"class":298},[278,4442,4443],{"class":333}," defineConfig",[278,4445,637],{"class":302},[278,4447,4448,4451,4454],{"class":280,"line":322},[278,4449,4450],{"class":302},"  dialect: ",[278,4452,4453],{"class":309},"'sqlite'",[278,4455,660],{"class":302},[278,4457,4458,4461,4464],{"class":280,"line":327},[278,4459,4460],{"class":302},"  schema: ",[278,4462,4463],{"class":309},"'.\u002Fserver\u002Fdatabase\u002Fschema.ts'",[278,4465,660],{"class":302},[278,4467,4468,4471,4474],{"class":280,"line":340},[278,4469,4470],{"class":302},"  out: ",[278,4472,4473],{"class":309},"'.\u002Fserver\u002Fdatabase\u002Fmigrations'",[278,4475,660],{"class":302},[278,4477,4478],{"class":280,"line":349},[278,4479,3693],{"class":302},[11,4481,4482,4483,4486],{},"The config above mentions where the database schema is located, and where should the database migrations be generated. The database dialect is set to ",[59,4484,4485],{},"sqlite"," as that is what Cloudflare’s D1 database supports.",[11,4488,4489,4490,263,4493,4496],{},"Next, create a new file ",[59,4491,4492],{},"schema.ts",[59,4494,4495],{},"server\u002Fdatabase"," folder, and add the following to it:",[269,4498,4500],{"className":271,"code":4499,"language":273,"meta":274,"style":274},"\u002F\u002F server\u002Fdatabase\u002Fschema.ts\nimport crypto from \"node:crypto\";\nimport { sql } from \"drizzle-orm\";\nimport { sqliteTable, text } from \"drizzle-orm\u002Fsqlite-core\";\n\nexport const notes = sqliteTable(\"notes\", {\n  id: text(\"id\")\n    .primaryKey()\n    .$defaultFn(() => \"nt_\" + crypto.randomBytes(12).toString(\"hex\")),\n  text: text(\"text\").notNull(),\n  createdAt: text(\"created_at\")\n    .notNull()\n    .default(sql`(CURRENT_TIMESTAMP)`),\n  updatedAt: text(\"updated_at\")\n    .notNull()\n    .default(sql`(CURRENT_TIMESTAMP)`)\n    .$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),\n  audioUrls: text(\"audio_urls\", { mode: \"json\" }).$type\u003Cstring[]>(),\n});\n",[59,4501,4502,4507,4521,4535,4549,4553,4575,4591,4602,4645,4665,4679,4687,4705,4719,4727,4741,4759,4790],{"__ignoreMap":274},[278,4503,4504],{"class":280,"line":281},[278,4505,4506],{"class":284},"\u002F\u002F server\u002Fdatabase\u002Fschema.ts\n",[278,4508,4509,4511,4514,4516,4519],{"class":280,"line":288},[278,4510,299],{"class":298},[278,4512,4513],{"class":302}," crypto ",[278,4515,306],{"class":298},[278,4517,4518],{"class":309}," \"node:crypto\"",[278,4520,313],{"class":302},[278,4522,4523,4525,4528,4530,4533],{"class":280,"line":295},[278,4524,299],{"class":298},[278,4526,4527],{"class":302}," { sql } ",[278,4529,306],{"class":298},[278,4531,4532],{"class":309}," \"drizzle-orm\"",[278,4534,313],{"class":302},[278,4536,4537,4539,4542,4544,4547],{"class":280,"line":316},[278,4538,299],{"class":298},[278,4540,4541],{"class":302}," { sqliteTable, text } ",[278,4543,306],{"class":298},[278,4545,4546],{"class":309}," \"drizzle-orm\u002Fsqlite-core\"",[278,4548,313],{"class":302},[278,4550,4551],{"class":280,"line":322},[278,4552,292],{"emptyLinePlaceholder":291},[278,4554,4555,4557,4560,4563,4565,4568,4570,4573],{"class":280,"line":327},[278,4556,628],{"class":298},[278,4558,4559],{"class":298}," const",[278,4561,4562],{"class":650}," notes",[278,4564,764],{"class":298},[278,4566,4567],{"class":333}," sqliteTable",[278,4569,1126],{"class":302},[278,4571,4572],{"class":309},"\"notes\"",[278,4574,1132],{"class":302},[278,4576,4577,4580,4583,4585,4588],{"class":280,"line":340},[278,4578,4579],{"class":302},"  id: ",[278,4581,4582],{"class":333},"text",[278,4584,1126],{"class":302},[278,4586,4587],{"class":309},"\"id\"",[278,4589,4590],{"class":302},")\n",[278,4592,4593,4596,4599],{"class":280,"line":349},[278,4594,4595],{"class":302},"    .",[278,4597,4598],{"class":333},"primaryKey",[278,4600,4601],{"class":302},"()\n",[278,4603,4604,4606,4609,4612,4614,4617,4620,4623,4626,4628,4631,4634,4637,4639,4642],{"class":280,"line":375},[278,4605,4595],{"class":302},[278,4607,4608],{"class":333},"$defaultFn",[278,4610,4611],{"class":302},"(() ",[278,4613,1848],{"class":298},[278,4615,4616],{"class":309}," \"nt_\"",[278,4618,4619],{"class":298}," +",[278,4621,4622],{"class":302}," crypto.",[278,4624,4625],{"class":333},"randomBytes",[278,4627,1126],{"class":302},[278,4629,4630],{"class":650},"12",[278,4632,4633],{"class":302},").",[278,4635,4636],{"class":333},"toString",[278,4638,1126],{"class":302},[278,4640,4641],{"class":309},"\"hex\"",[278,4643,4644],{"class":302},")),\n",[278,4646,4647,4650,4652,4654,4657,4659,4662],{"class":280,"line":386},[278,4648,4649],{"class":302},"  text: ",[278,4651,4582],{"class":333},[278,4653,1126],{"class":302},[278,4655,4656],{"class":309},"\"text\"",[278,4658,4633],{"class":302},[278,4660,4661],{"class":333},"notNull",[278,4663,4664],{"class":302},"(),\n",[278,4666,4667,4670,4672,4674,4677],{"class":280,"line":397},[278,4668,4669],{"class":302},"  createdAt: ",[278,4671,4582],{"class":333},[278,4673,1126],{"class":302},[278,4675,4676],{"class":309},"\"created_at\"",[278,4678,4590],{"class":302},[278,4680,4681,4683,4685],{"class":280,"line":408},[278,4682,4595],{"class":302},[278,4684,4661],{"class":333},[278,4686,4601],{"class":302},[278,4688,4689,4691,4694,4696,4699,4702],{"class":280,"line":433},[278,4690,4595],{"class":302},[278,4692,4693],{"class":333},"default",[278,4695,1126],{"class":302},[278,4697,4698],{"class":333},"sql",[278,4700,4701],{"class":309},"`(CURRENT_TIMESTAMP)`",[278,4703,4704],{"class":302},"),\n",[278,4706,4707,4710,4712,4714,4717],{"class":280,"line":454},[278,4708,4709],{"class":302},"  updatedAt: ",[278,4711,4582],{"class":333},[278,4713,1126],{"class":302},[278,4715,4716],{"class":309},"\"updated_at\"",[278,4718,4590],{"class":302},[278,4720,4721,4723,4725],{"class":280,"line":475},[278,4722,4595],{"class":302},[278,4724,4661],{"class":333},[278,4726,4601],{"class":302},[278,4728,4729,4731,4733,4735,4737,4739],{"class":280,"line":496},[278,4730,4595],{"class":302},[278,4732,4693],{"class":333},[278,4734,1126],{"class":302},[278,4736,4698],{"class":333},[278,4738,4701],{"class":309},[278,4740,4590],{"class":302},[278,4742,4743,4745,4748,4750,4752,4755,4757],{"class":280,"line":505},[278,4744,4595],{"class":302},[278,4746,4747],{"class":333},"$onUpdate",[278,4749,4611],{"class":302},[278,4751,1848],{"class":298},[278,4753,4754],{"class":333}," sql",[278,4756,4701],{"class":309},[278,4758,4704],{"class":302},[278,4760,4761,4764,4766,4768,4771,4774,4777,4780,4783,4785,4787],{"class":280,"line":516},[278,4762,4763],{"class":302},"  audioUrls: ",[278,4765,4582],{"class":333},[278,4767,1126],{"class":302},[278,4769,4770],{"class":309},"\"audio_urls\"",[278,4772,4773],{"class":302},", { mode: ",[278,4775,4776],{"class":309},"\"json\"",[278,4778,4779],{"class":302}," }).",[278,4781,4782],{"class":333},"$type",[278,4784,1702],{"class":302},[278,4786,1705],{"class":650},[278,4788,4789],{"class":302},"[]>(),\n",[278,4791,4792],{"class":280,"line":527},[278,4793,3693],{"class":302},[11,4795,4796,4797,4799],{},"The ",[59,4798,4394],{}," table schema is straightforward. It includes the note text and optional audio recording URLs stored as a JSON string array.",[11,4801,4802,4803,263,4806,4496],{},"Finally, create a new file ",[59,4804,4805],{},"drizzle.ts",[59,4807,4808],{},"server\u002Futils",[269,4810,4812],{"className":271,"code":4811,"language":273,"meta":274,"style":274},"\u002F\u002F server\u002Futils\u002Fdrizzle.ts\nimport { drizzle } from \"drizzle-orm\u002Fd1\";\nimport * as schema from \"..\u002Fdatabase\u002Fschema\";\n\nexport { sql, eq, and, or, desc } from \"drizzle-orm\";\n\nexport const tables = schema;\n\nexport function useDrizzle() {\n  return drizzle(hubDatabase(), { schema });\n}\n",[59,4813,4814,4819,4833,4852,4856,4869,4873,4887,4891,4902,4917],{"__ignoreMap":274},[278,4815,4816],{"class":280,"line":281},[278,4817,4818],{"class":284},"\u002F\u002F server\u002Futils\u002Fdrizzle.ts\n",[278,4820,4821,4823,4826,4828,4831],{"class":280,"line":288},[278,4822,299],{"class":298},[278,4824,4825],{"class":302}," { drizzle } ",[278,4827,306],{"class":298},[278,4829,4830],{"class":309}," \"drizzle-orm\u002Fd1\"",[278,4832,313],{"class":302},[278,4834,4835,4837,4839,4842,4845,4847,4850],{"class":280,"line":295},[278,4836,299],{"class":298},[278,4838,1363],{"class":650},[278,4840,4841],{"class":298}," as",[278,4843,4844],{"class":302}," schema ",[278,4846,306],{"class":298},[278,4848,4849],{"class":309}," \"..\u002Fdatabase\u002Fschema\"",[278,4851,313],{"class":302},[278,4853,4854],{"class":280,"line":316},[278,4855,292],{"emptyLinePlaceholder":291},[278,4857,4858,4860,4863,4865,4867],{"class":280,"line":322},[278,4859,628],{"class":298},[278,4861,4862],{"class":302}," { sql, eq, and, or, desc } ",[278,4864,306],{"class":298},[278,4866,4532],{"class":309},[278,4868,313],{"class":302},[278,4870,4871],{"class":280,"line":327},[278,4872,292],{"emptyLinePlaceholder":291},[278,4874,4875,4877,4879,4882,4884],{"class":280,"line":340},[278,4876,628],{"class":298},[278,4878,4559],{"class":298},[278,4880,4881],{"class":650}," tables",[278,4883,764],{"class":298},[278,4885,4886],{"class":302}," schema;\n",[278,4888,4889],{"class":280,"line":349},[278,4890,292],{"emptyLinePlaceholder":291},[278,4892,4893,4895,4897,4900],{"class":280,"line":375},[278,4894,628],{"class":298},[278,4896,748],{"class":298},[278,4898,4899],{"class":333}," useDrizzle",[278,4901,337],{"class":302},[278,4903,4904,4906,4909,4911,4914],{"class":280,"line":386},[278,4905,343],{"class":298},[278,4907,4908],{"class":333}," drizzle",[278,4910,1126],{"class":302},[278,4912,4913],{"class":333},"hubDatabase",[278,4915,4916],{"class":302},"(), { schema });\n",[278,4918,4919],{"class":280,"line":397},[278,4920,617],{"class":302},[11,4922,4923,4924,4926,4927,4929,4930,4933],{},"Here we hook up ",[59,4925,4913],{}," with the tables schema through ",[59,4928,4401],{}," and export the server composable ",[59,4931,4932],{},"useDrizzle"," along with the needed operators.",[11,4935,4936,4937,4940],{},"Now we are ready to create the ",[59,4938,4939],{},"\u002Fapi\u002Fnotes"," endpoints which we will be doing in the next section.",[32,4942,4944,4946],{"id":4943},"apinotes-endpoints",[59,4945,4939],{}," Endpoints",[11,4948,4949,4950,919,4953,263,4956,4959],{},"Create two new files ",[59,4951,4952],{},"index.post.ts",[59,4954,4955],{},"index.get.ts",[59,4957,4958],{},"server\u002Fapi\u002Fnotes"," folder and add the respective codes to them as shown below.",[11,4961,4962],{},[94,4963,4952],{},[269,4965,4967],{"className":271,"code":4966,"language":273,"meta":274,"style":274},"\u002F\u002F server\u002Fapi\u002Fnotes\u002Findex.post.ts\nimport { noteSchema } from \"#shared\u002Fschemas\u002Fnote.schema\";\n\nexport default defineEventHandler(async (event) => {\n  const { user } = await requireUserSession(event);\n\n  const { text, audioUrls } = await readValidatedBody(event, noteSchema.parse);\n\n  try {\n    await useDrizzle()\n      .insert(tables.notes)\n      .values({\n        text,\n        audioUrls: audioUrls ? audioUrls.map((url) => `\u002Faudio\u002F${url}`) : null,\n      });\n\n    return setResponseStatus(event, 201);\n  } catch (err) {\n    console.error(\"Error creating note:\", err);\n    throw createError({\n      statusCode: 500,\n      message: \"Failed to create note. Please try again.\",\n    });\n  }\n});\n",[59,4968,4969,4974,4988,4992,5014,5034,5038,5063,5067,5073,5082,5093,5102,5107,5144,5149,5153,5168,5176,5189,5197,5205,5214,5218,5222],{"__ignoreMap":274},[278,4970,4971],{"class":280,"line":281},[278,4972,4973],{"class":284},"\u002F\u002F server\u002Fapi\u002Fnotes\u002Findex.post.ts\n",[278,4975,4976,4978,4981,4983,4986],{"class":280,"line":288},[278,4977,299],{"class":298},[278,4979,4980],{"class":302}," { noteSchema } ",[278,4982,306],{"class":298},[278,4984,4985],{"class":309}," \"#shared\u002Fschemas\u002Fnote.schema\"",[278,4987,313],{"class":302},[278,4989,4990],{"class":280,"line":295},[278,4991,292],{"emptyLinePlaceholder":291},[278,4993,4994,4996,4998,5000,5002,5004,5006,5008,5010,5012],{"class":280,"line":316},[278,4995,628],{"class":298},[278,4997,631],{"class":298},[278,4999,3878],{"class":333},[278,5001,1126],{"class":302},[278,5003,1050],{"class":298},[278,5005,1245],{"class":302},[278,5007,3887],{"class":501},[278,5009,1845],{"class":302},[278,5011,1848],{"class":298},[278,5013,876],{"class":302},[278,5015,5016,5018,5020,5023,5025,5027,5029,5032],{"class":280,"line":322},[278,5017,758],{"class":298},[278,5019,1009],{"class":302},[278,5021,5022],{"class":650},"user",[278,5024,1029],{"class":302},[278,5026,358],{"class":298},[278,5028,1120],{"class":298},[278,5030,5031],{"class":333}," requireUserSession",[278,5033,3910],{"class":302},[278,5035,5036],{"class":280,"line":327},[278,5037,292],{"emptyLinePlaceholder":291},[278,5039,5040,5042,5044,5046,5048,5051,5053,5055,5057,5060],{"class":280,"line":340},[278,5041,758],{"class":298},[278,5043,1009],{"class":302},[278,5045,4582],{"class":650},[278,5047,1708],{"class":302},[278,5049,5050],{"class":650},"audioUrls",[278,5052,1029],{"class":302},[278,5054,358],{"class":298},[278,5056,1120],{"class":298},[278,5058,5059],{"class":333}," readValidatedBody",[278,5061,5062],{"class":302},"(event, noteSchema.parse);\n",[278,5064,5065],{"class":280,"line":349},[278,5066,292],{"emptyLinePlaceholder":291},[278,5068,5069,5071],{"class":280,"line":375},[278,5070,1105],{"class":298},[278,5072,876],{"class":302},[278,5074,5075,5078,5080],{"class":280,"line":386},[278,5076,5077],{"class":298},"    await",[278,5079,4899],{"class":333},[278,5081,4601],{"class":302},[278,5083,5084,5087,5090],{"class":280,"line":397},[278,5085,5086],{"class":302},"      .",[278,5088,5089],{"class":333},"insert",[278,5091,5092],{"class":302},"(tables.notes)\n",[278,5094,5095,5097,5100],{"class":280,"line":408},[278,5096,5086],{"class":302},[278,5098,5099],{"class":333},"values",[278,5101,637],{"class":302},[278,5103,5104],{"class":280,"line":433},[278,5105,5106],{"class":302},"        text,\n",[278,5108,5109,5112,5115,5118,5120,5122,5125,5127,5129,5132,5134,5136,5138,5140,5142],{"class":280,"line":454},[278,5110,5111],{"class":302},"        audioUrls: audioUrls ",[278,5113,5114],{"class":298},"?",[278,5116,5117],{"class":302}," audioUrls.",[278,5119,1828],{"class":333},[278,5121,2079],{"class":302},[278,5123,5124],{"class":501},"url",[278,5126,1845],{"class":302},[278,5128,1848],{"class":298},[278,5130,5131],{"class":309}," `\u002Faudio\u002F${",[278,5133,5124],{"class":302},[278,5135,1277],{"class":309},[278,5137,1845],{"class":302},[278,5139,960],{"class":298},[278,5141,1035],{"class":650},[278,5143,660],{"class":302},[278,5145,5146],{"class":280,"line":475},[278,5147,5148],{"class":302},"      });\n",[278,5150,5151],{"class":280,"line":496},[278,5152,292],{"emptyLinePlaceholder":291},[278,5154,5155,5157,5160,5163,5166],{"class":280,"line":505},[278,5156,1088],{"class":298},[278,5158,5159],{"class":333}," setResponseStatus",[278,5161,5162],{"class":302},"(event, ",[278,5164,5165],{"class":650},"201",[278,5167,1280],{"class":302},[278,5169,5170,5172,5174],{"class":280,"line":516},[278,5171,1397],{"class":302},[278,5173,1400],{"class":298},[278,5175,4095],{"class":302},[278,5177,5178,5180,5182,5184,5187],{"class":280,"line":527},[278,5179,1409],{"class":302},[278,5181,1412],{"class":333},[278,5183,1126],{"class":302},[278,5185,5186],{"class":309},"\"Error creating note:\"",[278,5188,4109],{"class":302},[278,5190,5191,5193,5195],{"class":280,"line":533},[278,5192,1426],{"class":298},[278,5194,3957],{"class":333},[278,5196,637],{"class":302},[278,5198,5199,5201,5203],{"class":280,"line":539},[278,5200,3964],{"class":302},[278,5202,2779],{"class":650},[278,5204,660],{"class":302},[278,5206,5207,5209,5212],{"class":280,"line":545},[278,5208,3974],{"class":302},[278,5210,5211],{"class":309},"\"Failed to create note. Please try again.\"",[278,5213,660],{"class":302},[278,5215,5216],{"class":280,"line":551},[278,5217,1233],{"class":302},[278,5219,5220],{"class":280,"line":557},[278,5221,1096],{"class":302},[278,5223,5224],{"class":280,"line":567},[278,5225,3693],{"class":302},[11,5227,5228],{},"The above code reads the validated event body, and creates a new note entry in the database using the drizzle composable we created earlier. We will get to the validation part in a bit.",[11,5230,5231],{},[94,5232,4955],{},[269,5234,5236],{"className":271,"code":5235,"language":273,"meta":274,"style":274},"\u002F\u002F server\u002Fapi\u002Fnotes\u002Findex.get.ts\nexport default defineEventHandler(async (event) => {\n  try {\n    const notes = await useDrizzle()\n      .select()\n      .from(tables.notes)\n      .orderBy(desc(tables.notes.updatedAt));\n\n    return notes;\n  } catch (err) {\n    console.error(\"Error retrieving note:\", err);\n    throw createError({\n      statusCode: 500,\n      message: \"Failed to get notes. Please try again.\",\n    });\n  }\n});\n",[59,5237,5238,5243,5265,5271,5285,5294,5302,5317,5321,5328,5336,5349,5357,5365,5374,5378,5382],{"__ignoreMap":274},[278,5239,5240],{"class":280,"line":281},[278,5241,5242],{"class":284},"\u002F\u002F server\u002Fapi\u002Fnotes\u002Findex.get.ts\n",[278,5244,5245,5247,5249,5251,5253,5255,5257,5259,5261,5263],{"class":280,"line":288},[278,5246,628],{"class":298},[278,5248,631],{"class":298},[278,5250,3878],{"class":333},[278,5252,1126],{"class":302},[278,5254,1050],{"class":298},[278,5256,1245],{"class":302},[278,5258,3887],{"class":501},[278,5260,1845],{"class":302},[278,5262,1848],{"class":298},[278,5264,876],{"class":302},[278,5266,5267,5269],{"class":280,"line":295},[278,5268,1105],{"class":298},[278,5270,876],{"class":302},[278,5272,5273,5275,5277,5279,5281,5283],{"class":280,"line":316},[278,5274,1112],{"class":298},[278,5276,4562],{"class":650},[278,5278,764],{"class":298},[278,5280,1120],{"class":298},[278,5282,4899],{"class":333},[278,5284,4601],{"class":302},[278,5286,5287,5289,5292],{"class":280,"line":322},[278,5288,5086],{"class":302},[278,5290,5291],{"class":333},"select",[278,5293,4601],{"class":302},[278,5295,5296,5298,5300],{"class":280,"line":327},[278,5297,5086],{"class":302},[278,5299,306],{"class":333},[278,5301,5092],{"class":302},[278,5303,5304,5306,5309,5311,5314],{"class":280,"line":340},[278,5305,5086],{"class":302},[278,5307,5308],{"class":333},"orderBy",[278,5310,1126],{"class":302},[278,5312,5313],{"class":333},"desc",[278,5315,5316],{"class":302},"(tables.notes.updatedAt));\n",[278,5318,5319],{"class":280,"line":349},[278,5320,292],{"emptyLinePlaceholder":291},[278,5322,5323,5325],{"class":280,"line":375},[278,5324,1088],{"class":298},[278,5326,5327],{"class":302}," notes;\n",[278,5329,5330,5332,5334],{"class":280,"line":386},[278,5331,1397],{"class":302},[278,5333,1400],{"class":298},[278,5335,4095],{"class":302},[278,5337,5338,5340,5342,5344,5347],{"class":280,"line":397},[278,5339,1409],{"class":302},[278,5341,1412],{"class":333},[278,5343,1126],{"class":302},[278,5345,5346],{"class":309},"\"Error retrieving note:\"",[278,5348,4109],{"class":302},[278,5350,5351,5353,5355],{"class":280,"line":408},[278,5352,1426],{"class":298},[278,5354,3957],{"class":333},[278,5356,637],{"class":302},[278,5358,5359,5361,5363],{"class":280,"line":433},[278,5360,3964],{"class":302},[278,5362,2779],{"class":650},[278,5364,660],{"class":302},[278,5366,5367,5369,5372],{"class":280,"line":454},[278,5368,3974],{"class":302},[278,5370,5371],{"class":309},"\"Failed to get notes. Please try again.\"",[278,5373,660],{"class":302},[278,5375,5376],{"class":280,"line":475},[278,5377,1233],{"class":302},[278,5379,5380],{"class":280,"line":496},[278,5381,1096],{"class":302},[278,5383,5384],{"class":280,"line":505},[278,5385,3693],{"class":302},[11,5387,5388,5389,5392],{},"Here we fetch the notes entries from the table in descending order of ",[59,5390,5391],{},"updatedAt"," field.",[11,5394,5395],{},[94,5396,5397],{},"Incoming data validation",[11,5399,5400,5401,5403,5404,5406],{},"As mentioned in the beginning, we’ll use ",[59,5402,3237],{}," for data validation. Here is the relevant code from ",[59,5405,4952],{}," that validates the incoming client data.",[269,5408,5410],{"className":271,"code":5409,"language":273,"meta":274,"style":274},"const { text, audioUrls } = await readValidatedBody(event, noteSchema.parse);\n",[59,5411,5412],{"__ignoreMap":274},[278,5413,5414,5417,5419,5421,5423,5425,5427,5429,5431,5433],{"class":280,"line":281},[278,5415,5416],{"class":298},"const",[278,5418,1009],{"class":302},[278,5420,4582],{"class":650},[278,5422,1708],{"class":302},[278,5424,5050],{"class":650},[278,5426,1029],{"class":302},[278,5428,358],{"class":298},[278,5430,1120],{"class":298},[278,5432,5059],{"class":333},[278,5434,5062],{"class":302},[11,5436,5437,5438,263,5441,5444],{},"Create a new file ",[59,5439,5440],{},"note.schema.ts",[59,5442,5443],{},"shared\u002Fschemas"," folder in the project root directory with the following content:",[269,5446,5448],{"className":271,"code":5447,"language":273,"meta":274,"style":274},"\u002F\u002F shared\u002Fschemas\u002Fnote.schema.ts\nimport { createInsertSchema, createSelectSchema } from \"drizzle-zod\";\nimport { z } from \"zod\";\nimport { notes } from \"~~\u002Fserver\u002Fdatabase\u002Fschema\";\n\nexport const noteSchema = createInsertSchema(notes, {\n  text: (schema) =>\n    schema.text\n      .min(3, \"Note must be at least 3 characters long\")\n      .max(5000, \"Note cannot exceed 5000 characters\"),\n  audioUrls: z.string().array().optional(),\n}).pick({\n  text: true,\n  audioUrls: true,\n});\n\nexport const noteSelectSchema = createSelectSchema(notes, {\n  audioUrls: z.string().array().optional(),\n});\n",[59,5449,5450,5455,5469,5483,5497,5501,5518,5534,5539,5558,5577,5596,5606,5614,5622,5626,5630,5646,5662],{"__ignoreMap":274},[278,5451,5452],{"class":280,"line":281},[278,5453,5454],{"class":284},"\u002F\u002F shared\u002Fschemas\u002Fnote.schema.ts\n",[278,5456,5457,5459,5462,5464,5467],{"class":280,"line":288},[278,5458,299],{"class":298},[278,5460,5461],{"class":302}," { createInsertSchema, createSelectSchema } ",[278,5463,306],{"class":298},[278,5465,5466],{"class":309}," \"drizzle-zod\"",[278,5468,313],{"class":302},[278,5470,5471,5473,5476,5478,5481],{"class":280,"line":295},[278,5472,299],{"class":298},[278,5474,5475],{"class":302}," { z } ",[278,5477,306],{"class":298},[278,5479,5480],{"class":309}," \"zod\"",[278,5482,313],{"class":302},[278,5484,5485,5487,5490,5492,5495],{"class":280,"line":316},[278,5486,299],{"class":298},[278,5488,5489],{"class":302}," { notes } ",[278,5491,306],{"class":298},[278,5493,5494],{"class":309}," \"~~\u002Fserver\u002Fdatabase\u002Fschema\"",[278,5496,313],{"class":302},[278,5498,5499],{"class":280,"line":322},[278,5500,292],{"emptyLinePlaceholder":291},[278,5502,5503,5505,5507,5510,5512,5515],{"class":280,"line":327},[278,5504,628],{"class":298},[278,5506,4559],{"class":298},[278,5508,5509],{"class":650}," noteSchema",[278,5511,764],{"class":298},[278,5513,5514],{"class":333}," createInsertSchema",[278,5516,5517],{"class":302},"(notes, {\n",[278,5519,5520,5523,5526,5529,5531],{"class":280,"line":340},[278,5521,5522],{"class":333},"  text",[278,5524,5525],{"class":302},": (",[278,5527,5528],{"class":501},"schema",[278,5530,1845],{"class":302},[278,5532,5533],{"class":298},"=>\n",[278,5535,5536],{"class":280,"line":349},[278,5537,5538],{"class":302},"    schema.text\n",[278,5540,5541,5543,5546,5548,5551,5553,5556],{"class":280,"line":375},[278,5542,5086],{"class":302},[278,5544,5545],{"class":333},"min",[278,5547,1126],{"class":302},[278,5549,5550],{"class":650},"3",[278,5552,1708],{"class":302},[278,5554,5555],{"class":309},"\"Note must be at least 3 characters long\"",[278,5557,4590],{"class":302},[278,5559,5560,5562,5565,5567,5570,5572,5575],{"class":280,"line":386},[278,5561,5086],{"class":302},[278,5563,5564],{"class":333},"max",[278,5566,1126],{"class":302},[278,5568,5569],{"class":650},"5000",[278,5571,1708],{"class":302},[278,5573,5574],{"class":309},"\"Note cannot exceed 5000 characters\"",[278,5576,4704],{"class":302},[278,5578,5579,5582,5584,5586,5589,5591,5594],{"class":280,"line":397},[278,5580,5581],{"class":302},"  audioUrls: z.",[278,5583,1705],{"class":333},[278,5585,4036],{"class":302},[278,5587,5588],{"class":333},"array",[278,5590,4036],{"class":302},[278,5592,5593],{"class":333},"optional",[278,5595,4664],{"class":302},[278,5597,5598,5601,5604],{"class":280,"line":408},[278,5599,5600],{"class":302},"}).",[278,5602,5603],{"class":333},"pick",[278,5605,637],{"class":302},[278,5607,5608,5610,5612],{"class":280,"line":433},[278,5609,4649],{"class":302},[278,5611,2931],{"class":650},[278,5613,660],{"class":302},[278,5615,5616,5618,5620],{"class":280,"line":454},[278,5617,4763],{"class":302},[278,5619,2931],{"class":650},[278,5621,660],{"class":302},[278,5623,5624],{"class":280,"line":475},[278,5625,3693],{"class":302},[278,5627,5628],{"class":280,"line":496},[278,5629,292],{"emptyLinePlaceholder":291},[278,5631,5632,5634,5636,5639,5641,5644],{"class":280,"line":505},[278,5633,628],{"class":298},[278,5635,4559],{"class":298},[278,5637,5638],{"class":650}," noteSelectSchema",[278,5640,764],{"class":298},[278,5642,5643],{"class":333}," createSelectSchema",[278,5645,5517],{"class":302},[278,5647,5648,5650,5652,5654,5656,5658,5660],{"class":280,"line":516},[278,5649,5581],{"class":302},[278,5651,1705],{"class":333},[278,5653,4036],{"class":302},[278,5655,5588],{"class":333},[278,5657,4036],{"class":302},[278,5659,5593],{"class":333},[278,5661,4664],{"class":302},[278,5663,5664],{"class":280,"line":527},[278,5665,3693],{"class":302},[11,5667,5668,5669,5672,5673,5676],{},"The above code uses the ",[59,5670,5671],{},"drizzle-zod"," plugin to create the ",[59,5674,5675],{},"zod"," schema needed for validation (The above validation error messages are more suitable for the client side. Feel free to adapt these validation rules to suit your specific project requirements.).",[32,5678,5680],{"id":5679},"creating-db-migrations","Creating DB Migrations",[11,5682,5683,5684,5687],{},"With the table schema and API endpoints defined, the final step is to create and apply database migrations to bring everything together. Add the following command to your ",[59,5685,5686],{},"package.json","'s scripts:",[269,5689,5692],{"className":5690,"code":5691,"language":1310,"meta":274,"style":274},"language-json shiki shiki-themes github-light github-dark","\u002F\u002F ..\n\"scripts\": {\n  \u002F\u002F ..\n  \"db:generate\": \"drizzle-kit generate\"\n}\n\u002F\u002F ..\n",[59,5693,5694,5699,5707,5712,5722,5726],{"__ignoreMap":274},[278,5695,5696],{"class":280,"line":281},[278,5697,5698],{"class":284},"\u002F\u002F ..\n",[278,5700,5701,5704],{"class":280,"line":288},[278,5702,5703],{"class":309},"\"scripts\"",[278,5705,5706],{"class":302},": {\n",[278,5708,5709],{"class":280,"line":295},[278,5710,5711],{"class":284},"  \u002F\u002F ..\n",[278,5713,5714,5717,5719],{"class":280,"line":316},[278,5715,5716],{"class":650},"  \"db:generate\"",[278,5718,1155],{"class":302},[278,5720,5721],{"class":309},"\"drizzle-kit generate\"\n",[278,5723,5724],{"class":280,"line":322},[278,5725,617],{"class":302},[278,5727,5728],{"class":280,"line":327},[278,5729,5698],{"class":284},[11,5731,5732,5733,5736,5737,5740],{},"Next, run ",[59,5734,5735],{},"pnpm run db:generate"," to create the database migrations. These migrations are auto applied by NuxtHub when you run or deploy your project. You can test it by running ",[59,5738,5739],{},"pnpm dev"," and checking the Nuxt Dev Tools as shown below (this is a local sqlite database that is used in the dev mode).",[11,5742,5743],{},[3135,5744],{"alt":5745,"src":5746},"Nuxt Dev Tools showing empty notes table","\u002Fimages\u002Fposts\u002Fbuilding-voice-notes-app-with-ai-transcription-and-post-processing\u002Fb7106851-cba6-4dd0-b3a8-758f6746ef37-09bcd37401.png",[11,5748,5749],{},"We are done with the basic backend of the project. In the next section, we will code the frontend components and pages to complete the whole thing,",[24,5751,5753],{"id":5752},"creating-the-basic-frontend","Creating the Basic Frontend",[11,5755,5756],{},"We’ll start with the most important feature first: recording the user voice, and then we’ll move on to creating the needed components and pages.",[32,5758,5760,5763],{"id":5759},"usemediarecorder-composable",[59,5761,5762],{},"useMediaRecorder"," Composable",[11,5765,5766,5767,4213,5770,4217],{},"Let’s create a composable to handle the media recording functionality. Create a new file ",[59,5768,5769],{},"useMediaRecorder.ts",[59,5771,5772],{},"app\u002Fcomposables",[269,5774,5776],{"className":271,"code":5775,"language":273,"meta":274,"style":274},"\u002F\u002F app\u002Fcomposables\u002FuseMediaRecorder.ts\ninterface MediaRecorderState {\n  isRecording: boolean;\n  recordingDuration: number;\n  audioData: Uint8Array | null;\n  updateTrigger: number;\n}\n\nconst getSupportedMimeType = () => {\n  const types = [\n    \"audio\u002Fmp4\",\n    \"audio\u002Fmp4;codecs=mp4a\",\n    \"audio\u002Fmpeg\",\n    \"audio\u002Fwebm;codecs=opus\",\n    \"audio\u002Fwebm\",\n  ];\n\n  return (\n    types.find((type) => MediaRecorder.isTypeSupported(type)) || \"audio\u002Fwebm\"\n  );\n};\n\nexport function useMediaRecorder() {\n  const state = ref\u003CMediaRecorderState>({\n    isRecording: false,\n    recordingDuration: 0,\n    audioData: null,\n    updateTrigger: 0,\n  });\n\n  let mediaRecorder: MediaRecorder | null = null;\n  let audioContext: AudioContext | null = null;\n  let analyser: AnalyserNode | null = null;\n  let animationFrame: number | null = null;\n  let audioChunks: Blob[] | undefined = undefined;\n\n  const updateAudioData = () => {\n    if (!analyser || !state.value.isRecording || !state.value.audioData) {\n      if (animationFrame) {\n        cancelAnimationFrame(animationFrame);\n        animationFrame = null;\n      }\n\n      return;\n    }\n\n    analyser.getByteTimeDomainData(state.value.audioData);\n    state.value.updateTrigger += 1;\n    animationFrame = requestAnimationFrame(updateAudioData);\n  };\n\n  const startRecording = async () => {\n    try {\n      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n\n      audioContext = new AudioContext();\n      analyser = audioContext.createAnalyser();\n\n      const source = audioContext.createMediaStreamSource(stream);\n      source.connect(analyser);\n\n      const options = {\n        mimeType: getSupportedMimeType(),\n        audioBitsPerSecond: 64000,\n      };\n\n      mediaRecorder = new MediaRecorder(stream, options);\n      audioChunks = [];\n\n      mediaRecorder.ondataavailable = (e: BlobEvent) => {\n        audioChunks?.push(e.data);\n        state.value.recordingDuration += 1;\n      };\n\n      state.value.audioData = new Uint8Array(analyser.frequencyBinCount);\n      state.value.isRecording = true;\n      state.value.recordingDuration = 0;\n      state.value.updateTrigger = 0;\n      mediaRecorder.start(1000);\n\n      updateAudioData();\n    } catch (err) {\n      console.error(\"Error accessing microphone:\", err);\n      throw err;\n    }\n  };\n\n  const stopRecording = async () => {\n    return await new Promise\u003CBlob>((resolve) => {\n      if (mediaRecorder && state.value.isRecording) {\n        const mimeType = mediaRecorder.mimeType;\n        mediaRecorder.onstop = () => {\n          const blob = new Blob(audioChunks, { type: mimeType });\n          audioChunks = undefined;\n\n          state.value.recordingDuration = 0;\n          state.value.updateTrigger = 0;\n          state.value.audioData = null;\n\n          resolve(blob);\n        };\n\n        state.value.isRecording = false;\n        mediaRecorder.stop();\n        mediaRecorder.stream.getTracks().forEach((track) => track.stop());\n\n        if (animationFrame) {\n          cancelAnimationFrame(animationFrame);\n          animationFrame = null;\n        }\n\n        audioContext?.close();\n        audioContext = null;\n      }\n    });\n  };\n\n  onUnmounted(() => {\n    stopRecording();\n  });\n\n  return {\n    state: readonly(state),\n    startRecording,\n    stopRecording,\n  };\n}\n",[59,5777,5778,5783,5792,5804,5815,5830,5841,5845,5849,5865,5877,5884,5891,5898,5905,5912,5917,5921,5927,5958,5962,5966,5970,5981,6001,6010,6019,6029,6038,6042,6046,6069,6091,6113,6134,6158,6162,6177,6203,6211,6219,6230,6235,6239,6245,6249,6253,6264,6277,6290,6294,6298,6315,6322,6347,6351,6364,6379,6383,6400,6411,6415,6426,6437,6448,6453,6458,6473,6484,6489,6516,6528,6540,6545,6550,6565,6578,6591,6603,6618,6623,6631,6641,6655,6663,6668,6673,6678,6696,6723,6736,6750,6767,6784,6796,6801,6813,6825,6837,6842,6851,6857,6862,6875,6885,6916,6921,6929,6937,6949,6955,6960,6971,6983,6988,6993,6998,7003,7015,7023,7028,7033,7040,7052,7058,7064,7069],{"__ignoreMap":274},[278,5779,5780],{"class":280,"line":281},[278,5781,5782],{"class":284},"\u002F\u002F app\u002Fcomposables\u002FuseMediaRecorder.ts\n",[278,5784,5785,5787,5790],{"class":280,"line":288},[278,5786,947],{"class":298},[278,5788,5789],{"class":333}," MediaRecorderState",[278,5791,876],{"class":302},[278,5793,5794,5797,5799,5802],{"class":280,"line":295},[278,5795,5796],{"class":501},"  isRecording",[278,5798,960],{"class":298},[278,5800,5801],{"class":650}," boolean",[278,5803,313],{"class":302},[278,5805,5806,5809,5811,5813],{"class":280,"line":316},[278,5807,5808],{"class":501},"  recordingDuration",[278,5810,960],{"class":298},[278,5812,975],{"class":650},[278,5814,313],{"class":302},[278,5816,5817,5820,5822,5824,5826,5828],{"class":280,"line":322},[278,5818,5819],{"class":501},"  audioData",[278,5821,960],{"class":298},[278,5823,4057],{"class":333},[278,5825,1621],{"class":298},[278,5827,1035],{"class":650},[278,5829,313],{"class":302},[278,5831,5832,5835,5837,5839],{"class":280,"line":327},[278,5833,5834],{"class":501},"  updateTrigger",[278,5836,960],{"class":298},[278,5838,975],{"class":650},[278,5840,313],{"class":302},[278,5842,5843],{"class":280,"line":340},[278,5844,617],{"class":302},[278,5846,5847],{"class":280,"line":349},[278,5848,292],{"emptyLinePlaceholder":291},[278,5850,5851,5853,5856,5858,5861,5863],{"class":280,"line":375},[278,5852,5416],{"class":298},[278,5854,5855],{"class":333}," getSupportedMimeType",[278,5857,764],{"class":298},[278,5859,5860],{"class":302}," () ",[278,5862,1848],{"class":298},[278,5864,876],{"class":302},[278,5866,5867,5869,5872,5874],{"class":280,"line":386},[278,5868,758],{"class":298},[278,5870,5871],{"class":650}," types",[278,5873,764],{"class":298},[278,5875,5876],{"class":302}," [\n",[278,5878,5879,5882],{"class":280,"line":397},[278,5880,5881],{"class":309},"    \"audio\u002Fmp4\"",[278,5883,660],{"class":302},[278,5885,5886,5889],{"class":280,"line":408},[278,5887,5888],{"class":309},"    \"audio\u002Fmp4;codecs=mp4a\"",[278,5890,660],{"class":302},[278,5892,5893,5896],{"class":280,"line":433},[278,5894,5895],{"class":309},"    \"audio\u002Fmpeg\"",[278,5897,660],{"class":302},[278,5899,5900,5903],{"class":280,"line":454},[278,5901,5902],{"class":309},"    \"audio\u002Fwebm;codecs=opus\"",[278,5904,660],{"class":302},[278,5906,5907,5910],{"class":280,"line":475},[278,5908,5909],{"class":309},"    \"audio\u002Fwebm\"",[278,5911,660],{"class":302},[278,5913,5914],{"class":280,"line":496},[278,5915,5916],{"class":302},"  ];\n",[278,5918,5919],{"class":280,"line":505},[278,5920,292],{"emptyLinePlaceholder":291},[278,5922,5923,5925],{"class":280,"line":516},[278,5924,343],{"class":298},[278,5926,346],{"class":302},[278,5928,5929,5932,5935,5937,5939,5941,5943,5946,5949,5952,5955],{"class":280,"line":527},[278,5930,5931],{"class":302},"    types.",[278,5933,5934],{"class":333},"find",[278,5936,2079],{"class":302},[278,5938,1638],{"class":501},[278,5940,1845],{"class":302},[278,5942,1848],{"class":298},[278,5944,5945],{"class":302}," MediaRecorder.",[278,5947,5948],{"class":333},"isTypeSupported",[278,5950,5951],{"class":302},"(type)) ",[278,5953,5954],{"class":298},"||",[278,5956,5957],{"class":309}," \"audio\u002Fwebm\"\n",[278,5959,5960],{"class":280,"line":533},[278,5961,611],{"class":302},[278,5963,5964],{"class":280,"line":539},[278,5965,2817],{"class":302},[278,5967,5968],{"class":280,"line":545},[278,5969,292],{"emptyLinePlaceholder":291},[278,5971,5972,5974,5976,5979],{"class":280,"line":551},[278,5973,628],{"class":298},[278,5975,748],{"class":298},[278,5977,5978],{"class":333}," useMediaRecorder",[278,5980,337],{"class":302},[278,5982,5983,5985,5988,5990,5993,5995,5998],{"class":280,"line":557},[278,5984,758],{"class":298},[278,5986,5987],{"class":650}," state",[278,5989,764],{"class":298},[278,5991,5992],{"class":333}," ref",[278,5994,1702],{"class":302},[278,5996,5997],{"class":333},"MediaRecorderState",[278,5999,6000],{"class":302},">({\n",[278,6002,6003,6006,6008],{"class":280,"line":567},[278,6004,6005],{"class":302},"    isRecording: ",[278,6007,2965],{"class":650},[278,6009,660],{"class":302},[278,6011,6012,6015,6017],{"class":280,"line":577},[278,6013,6014],{"class":302},"    recordingDuration: ",[278,6016,2012],{"class":650},[278,6018,660],{"class":302},[278,6020,6021,6024,6027],{"class":280,"line":587},[278,6022,6023],{"class":302},"    audioData: ",[278,6025,6026],{"class":650},"null",[278,6028,660],{"class":302},[278,6030,6031,6034,6036],{"class":280,"line":597},[278,6032,6033],{"class":302},"    updateTrigger: ",[278,6035,2012],{"class":650},[278,6037,660],{"class":302},[278,6039,6040],{"class":280,"line":608},[278,6041,2037],{"class":302},[278,6043,6044],{"class":280,"line":614},[278,6045,292],{"emptyLinePlaceholder":291},[278,6047,6048,6051,6054,6056,6059,6061,6063,6065,6067],{"class":280,"line":620},[278,6049,6050],{"class":298},"  let",[278,6052,6053],{"class":302}," mediaRecorder",[278,6055,960],{"class":298},[278,6057,6058],{"class":333}," MediaRecorder",[278,6060,1621],{"class":298},[278,6062,1035],{"class":650},[278,6064,764],{"class":298},[278,6066,1035],{"class":650},[278,6068,313],{"class":302},[278,6070,6071,6073,6076,6078,6081,6083,6085,6087,6089],{"class":280,"line":625},[278,6072,6050],{"class":298},[278,6074,6075],{"class":302}," audioContext",[278,6077,960],{"class":298},[278,6079,6080],{"class":333}," AudioContext",[278,6082,1621],{"class":298},[278,6084,1035],{"class":650},[278,6086,764],{"class":298},[278,6088,1035],{"class":650},[278,6090,313],{"class":302},[278,6092,6093,6095,6098,6100,6103,6105,6107,6109,6111],{"class":280,"line":640},[278,6094,6050],{"class":298},[278,6096,6097],{"class":302}," analyser",[278,6099,960],{"class":298},[278,6101,6102],{"class":333}," AnalyserNode",[278,6104,1621],{"class":298},[278,6106,1035],{"class":650},[278,6108,764],{"class":298},[278,6110,1035],{"class":650},[278,6112,313],{"class":302},[278,6114,6115,6117,6120,6122,6124,6126,6128,6130,6132],{"class":280,"line":663},[278,6116,6050],{"class":298},[278,6118,6119],{"class":302}," animationFrame",[278,6121,960],{"class":298},[278,6123,975],{"class":650},[278,6125,1621],{"class":298},[278,6127,1035],{"class":650},[278,6129,764],{"class":298},[278,6131,1035],{"class":650},[278,6133,313],{"class":302},[278,6135,6136,6138,6141,6143,6145,6147,6149,6152,6154,6156],{"class":280,"line":669},[278,6137,6050],{"class":298},[278,6139,6140],{"class":302}," audioChunks",[278,6142,960],{"class":298},[278,6144,3937],{"class":333},[278,6146,1971],{"class":302},[278,6148,1032],{"class":298},[278,6150,6151],{"class":650}," undefined",[278,6153,764],{"class":298},[278,6155,6151],{"class":650},[278,6157,313],{"class":302},[278,6159,6160],{"class":280,"line":680},[278,6161,292],{"emptyLinePlaceholder":291},[278,6163,6164,6166,6169,6171,6173,6175],{"class":280,"line":686},[278,6165,758],{"class":298},[278,6167,6168],{"class":333}," updateAudioData",[278,6170,764],{"class":298},[278,6172,5860],{"class":302},[278,6174,1848],{"class":298},[278,6176,876],{"class":302},[278,6178,6179,6181,6183,6185,6188,6190,6193,6196,6198,6200],{"class":280,"line":1334},[278,6180,1242],{"class":298},[278,6182,1245],{"class":302},[278,6184,1209],{"class":298},[278,6186,6187],{"class":302},"analyser ",[278,6189,5954],{"class":298},[278,6191,6192],{"class":298}," !",[278,6194,6195],{"class":302},"state.value.isRecording ",[278,6197,5954],{"class":298},[278,6199,6192],{"class":298},[278,6201,6202],{"class":302},"state.value.audioData) {\n",[278,6204,6205,6208],{"class":280,"line":1375},[278,6206,6207],{"class":298},"      if",[278,6209,6210],{"class":302}," (animationFrame) {\n",[278,6212,6213,6216],{"class":280,"line":1381},[278,6214,6215],{"class":333},"        cancelAnimationFrame",[278,6217,6218],{"class":302},"(animationFrame);\n",[278,6220,6221,6224,6226,6228],{"class":280,"line":1386},[278,6222,6223],{"class":302},"        animationFrame ",[278,6225,358],{"class":298},[278,6227,1035],{"class":650},[278,6229,313],{"class":302},[278,6231,6232],{"class":280,"line":1394},[278,6233,6234],{"class":302},"      }\n",[278,6236,6237],{"class":280,"line":1406},[278,6238,292],{"emptyLinePlaceholder":291},[278,6240,6241,6243],{"class":280,"line":1423},[278,6242,1942],{"class":298},[278,6244,313],{"class":302},[278,6246,6247],{"class":280,"line":1432},[278,6248,1285],{"class":302},[278,6250,6251],{"class":280,"line":1437},[278,6252,292],{"emptyLinePlaceholder":291},[278,6254,6255,6258,6261],{"class":280,"line":1916},[278,6256,6257],{"class":302},"    analyser.",[278,6259,6260],{"class":333},"getByteTimeDomainData",[278,6262,6263],{"class":302},"(state.value.audioData);\n",[278,6265,6266,6269,6272,6275],{"class":280,"line":1939},[278,6267,6268],{"class":302},"    state.value.updateTrigger ",[278,6270,6271],{"class":298},"+=",[278,6273,6274],{"class":650}," 1",[278,6276,313],{"class":302},[278,6278,6279,6282,6284,6287],{"class":280,"line":1949},[278,6280,6281],{"class":302},"    animationFrame ",[278,6283,358],{"class":298},[278,6285,6286],{"class":333}," requestAnimationFrame",[278,6288,6289],{"class":302},"(updateAudioData);\n",[278,6291,6292],{"class":280,"line":1954},[278,6293,901],{"class":302},[278,6295,6296],{"class":280,"line":1959},[278,6297,292],{"emptyLinePlaceholder":291},[278,6299,6300,6302,6305,6307,6309,6311,6313],{"class":280,"line":1985},[278,6301,758],{"class":298},[278,6303,6304],{"class":333}," startRecording",[278,6306,764],{"class":298},[278,6308,2325],{"class":298},[278,6310,5860],{"class":302},[278,6312,1848],{"class":298},[278,6314,876],{"class":302},[278,6316,6317,6320],{"class":280,"line":1990},[278,6318,6319],{"class":298},"    try",[278,6321,876],{"class":302},[278,6323,6324,6326,6329,6331,6333,6336,6339,6342,6344],{"class":280,"line":1997},[278,6325,2461],{"class":298},[278,6327,6328],{"class":650}," stream",[278,6330,764],{"class":298},[278,6332,1120],{"class":298},[278,6334,6335],{"class":302}," navigator.mediaDevices.",[278,6337,6338],{"class":333},"getUserMedia",[278,6340,6341],{"class":302},"({ audio: ",[278,6343,2931],{"class":650},[278,6345,6346],{"class":302}," });\n",[278,6348,6349],{"class":280,"line":2006},[278,6350,292],{"emptyLinePlaceholder":291},[278,6352,6353,6356,6358,6360,6362],{"class":280,"line":2018},[278,6354,6355],{"class":302},"      audioContext ",[278,6357,358],{"class":298},[278,6359,1258],{"class":298},[278,6361,6080],{"class":333},[278,6363,1313],{"class":302},[278,6365,6366,6369,6371,6374,6377],{"class":280,"line":2029},[278,6367,6368],{"class":302},"      analyser ",[278,6370,358],{"class":298},[278,6372,6373],{"class":302}," audioContext.",[278,6375,6376],{"class":333},"createAnalyser",[278,6378,1313],{"class":302},[278,6380,6381],{"class":280,"line":2034},[278,6382,292],{"emptyLinePlaceholder":291},[278,6384,6385,6387,6390,6392,6394,6397],{"class":280,"line":2040},[278,6386,2461],{"class":298},[278,6388,6389],{"class":650}," source",[278,6391,764],{"class":298},[278,6393,6373],{"class":302},[278,6395,6396],{"class":333},"createMediaStreamSource",[278,6398,6399],{"class":302},"(stream);\n",[278,6401,6402,6405,6408],{"class":280,"line":2045},[278,6403,6404],{"class":302},"      source.",[278,6406,6407],{"class":333},"connect",[278,6409,6410],{"class":302},"(analyser);\n",[278,6412,6413],{"class":280,"line":2068},[278,6414,292],{"emptyLinePlaceholder":291},[278,6416,6417,6419,6422,6424],{"class":280,"line":2099},[278,6418,2461],{"class":298},[278,6420,6421],{"class":650}," options",[278,6423,764],{"class":298},[278,6425,876],{"class":302},[278,6427,6429,6432,6435],{"class":280,"line":6428},63,[278,6430,6431],{"class":302},"        mimeType: ",[278,6433,6434],{"class":333},"getSupportedMimeType",[278,6436,4664],{"class":302},[278,6438,6440,6443,6446],{"class":280,"line":6439},64,[278,6441,6442],{"class":302},"        audioBitsPerSecond: ",[278,6444,6445],{"class":650},"64000",[278,6447,660],{"class":302},[278,6449,6451],{"class":280,"line":6450},65,[278,6452,1650],{"class":302},[278,6454,6456],{"class":280,"line":6455},66,[278,6457,292],{"emptyLinePlaceholder":291},[278,6459,6461,6464,6466,6468,6470],{"class":280,"line":6460},67,[278,6462,6463],{"class":302},"      mediaRecorder ",[278,6465,358],{"class":298},[278,6467,1258],{"class":298},[278,6469,6058],{"class":333},[278,6471,6472],{"class":302},"(stream, options);\n",[278,6474,6476,6479,6481],{"class":280,"line":6475},68,[278,6477,6478],{"class":302},"      audioChunks ",[278,6480,358],{"class":298},[278,6482,6483],{"class":302}," [];\n",[278,6485,6487],{"class":280,"line":6486},69,[278,6488,292],{"emptyLinePlaceholder":291},[278,6490,6492,6495,6498,6500,6502,6505,6507,6510,6512,6514],{"class":280,"line":6491},70,[278,6493,6494],{"class":302},"      mediaRecorder.",[278,6496,6497],{"class":333},"ondataavailable",[278,6499,764],{"class":298},[278,6501,1245],{"class":302},[278,6503,6504],{"class":501},"e",[278,6506,960],{"class":298},[278,6508,6509],{"class":333}," BlobEvent",[278,6511,1845],{"class":302},[278,6513,1848],{"class":298},[278,6515,876],{"class":302},[278,6517,6519,6522,6525],{"class":280,"line":6518},71,[278,6520,6521],{"class":302},"        audioChunks?.",[278,6523,6524],{"class":333},"push",[278,6526,6527],{"class":302},"(e.data);\n",[278,6529,6531,6534,6536,6538],{"class":280,"line":6530},72,[278,6532,6533],{"class":302},"        state.value.recordingDuration ",[278,6535,6271],{"class":298},[278,6537,6274],{"class":650},[278,6539,313],{"class":302},[278,6541,6543],{"class":280,"line":6542},73,[278,6544,1650],{"class":302},[278,6546,6548],{"class":280,"line":6547},74,[278,6549,292],{"emptyLinePlaceholder":291},[278,6551,6553,6556,6558,6560,6562],{"class":280,"line":6552},75,[278,6554,6555],{"class":302},"      state.value.audioData ",[278,6557,358],{"class":298},[278,6559,1258],{"class":298},[278,6561,4057],{"class":333},[278,6563,6564],{"class":302},"(analyser.frequencyBinCount);\n",[278,6566,6568,6571,6573,6576],{"class":280,"line":6567},76,[278,6569,6570],{"class":302},"      state.value.isRecording ",[278,6572,358],{"class":298},[278,6574,6575],{"class":650}," true",[278,6577,313],{"class":302},[278,6579,6581,6584,6586,6589],{"class":280,"line":6580},77,[278,6582,6583],{"class":302},"      state.value.recordingDuration ",[278,6585,358],{"class":298},[278,6587,6588],{"class":650}," 0",[278,6590,313],{"class":302},[278,6592,6594,6597,6599,6601],{"class":280,"line":6593},78,[278,6595,6596],{"class":302},"      state.value.updateTrigger ",[278,6598,358],{"class":298},[278,6600,6588],{"class":650},[278,6602,313],{"class":302},[278,6604,6606,6608,6611,6613,6616],{"class":280,"line":6605},79,[278,6607,6494],{"class":302},[278,6609,6610],{"class":333},"start",[278,6612,1126],{"class":302},[278,6614,6615],{"class":650},"1000",[278,6617,1280],{"class":302},[278,6619,6621],{"class":280,"line":6620},80,[278,6622,292],{"emptyLinePlaceholder":291},[278,6624,6626,6629],{"class":280,"line":6625},81,[278,6627,6628],{"class":333},"      updateAudioData",[278,6630,1313],{"class":302},[278,6632,6634,6637,6639],{"class":280,"line":6633},82,[278,6635,6636],{"class":302},"    } ",[278,6638,1400],{"class":298},[278,6640,4095],{"class":302},[278,6642,6644,6646,6648,6650,6653],{"class":280,"line":6643},83,[278,6645,1919],{"class":302},[278,6647,1412],{"class":333},[278,6649,1126],{"class":302},[278,6651,6652],{"class":309},"\"Error accessing microphone:\"",[278,6654,4109],{"class":302},[278,6656,6658,6660],{"class":280,"line":6657},84,[278,6659,1255],{"class":298},[278,6661,6662],{"class":302}," err;\n",[278,6664,6666],{"class":280,"line":6665},85,[278,6667,1285],{"class":302},[278,6669,6671],{"class":280,"line":6670},86,[278,6672,901],{"class":302},[278,6674,6676],{"class":280,"line":6675},87,[278,6677,292],{"emptyLinePlaceholder":291},[278,6679,6681,6683,6686,6688,6690,6692,6694],{"class":280,"line":6680},88,[278,6682,758],{"class":298},[278,6684,6685],{"class":333}," stopRecording",[278,6687,764],{"class":298},[278,6689,2325],{"class":298},[278,6691,5860],{"class":302},[278,6693,1848],{"class":298},[278,6695,876],{"class":302},[278,6697,6699,6701,6703,6705,6707,6709,6711,6714,6717,6719,6721],{"class":280,"line":6698},89,[278,6700,1088],{"class":298},[278,6702,1120],{"class":298},[278,6704,1258],{"class":298},[278,6706,2057],{"class":650},[278,6708,1702],{"class":302},[278,6710,4157],{"class":333},[278,6712,6713],{"class":302},">((",[278,6715,6716],{"class":501},"resolve",[278,6718,1845],{"class":302},[278,6720,1848],{"class":298},[278,6722,876],{"class":302},[278,6724,6726,6728,6731,6733],{"class":280,"line":6725},90,[278,6727,6207],{"class":298},[278,6729,6730],{"class":302}," (mediaRecorder ",[278,6732,1068],{"class":298},[278,6734,6735],{"class":302}," state.value.isRecording) {\n",[278,6737,6739,6742,6745,6747],{"class":280,"line":6738},91,[278,6740,6741],{"class":298},"        const",[278,6743,6744],{"class":650}," mimeType",[278,6746,764],{"class":298},[278,6748,6749],{"class":302}," mediaRecorder.mimeType;\n",[278,6751,6753,6756,6759,6761,6763,6765],{"class":280,"line":6752},92,[278,6754,6755],{"class":302},"        mediaRecorder.",[278,6757,6758],{"class":333},"onstop",[278,6760,764],{"class":298},[278,6762,5860],{"class":302},[278,6764,1848],{"class":298},[278,6766,876],{"class":302},[278,6768,6770,6773,6775,6777,6779,6781],{"class":280,"line":6769},93,[278,6771,6772],{"class":298},"          const",[278,6774,3917],{"class":650},[278,6776,764],{"class":298},[278,6778,1258],{"class":298},[278,6780,3937],{"class":333},[278,6782,6783],{"class":302},"(audioChunks, { type: mimeType });\n",[278,6785,6787,6790,6792,6794],{"class":280,"line":6786},94,[278,6788,6789],{"class":302},"          audioChunks ",[278,6791,358],{"class":298},[278,6793,6151],{"class":650},[278,6795,313],{"class":302},[278,6797,6799],{"class":280,"line":6798},95,[278,6800,292],{"emptyLinePlaceholder":291},[278,6802,6804,6807,6809,6811],{"class":280,"line":6803},96,[278,6805,6806],{"class":302},"          state.value.recordingDuration ",[278,6808,358],{"class":298},[278,6810,6588],{"class":650},[278,6812,313],{"class":302},[278,6814,6816,6819,6821,6823],{"class":280,"line":6815},97,[278,6817,6818],{"class":302},"          state.value.updateTrigger ",[278,6820,358],{"class":298},[278,6822,6588],{"class":650},[278,6824,313],{"class":302},[278,6826,6828,6831,6833,6835],{"class":280,"line":6827},98,[278,6829,6830],{"class":302},"          state.value.audioData ",[278,6832,358],{"class":298},[278,6834,1035],{"class":650},[278,6836,313],{"class":302},[278,6838,6840],{"class":280,"line":6839},99,[278,6841,292],{"emptyLinePlaceholder":291},[278,6843,6845,6848],{"class":280,"line":6844},100,[278,6846,6847],{"class":333},"          resolve",[278,6849,6850],{"class":302},"(blob);\n",[278,6852,6854],{"class":280,"line":6853},101,[278,6855,6856],{"class":302},"        };\n",[278,6858,6860],{"class":280,"line":6859},102,[278,6861,292],{"emptyLinePlaceholder":291},[278,6863,6865,6868,6870,6873],{"class":280,"line":6864},103,[278,6866,6867],{"class":302},"        state.value.isRecording ",[278,6869,358],{"class":298},[278,6871,6872],{"class":650}," false",[278,6874,313],{"class":302},[278,6876,6878,6880,6883],{"class":280,"line":6877},104,[278,6879,6755],{"class":302},[278,6881,6882],{"class":333},"stop",[278,6884,1313],{"class":302},[278,6886,6888,6891,6894,6896,6899,6901,6904,6906,6908,6911,6913],{"class":280,"line":6887},105,[278,6889,6890],{"class":302},"        mediaRecorder.stream.",[278,6892,6893],{"class":333},"getTracks",[278,6895,4036],{"class":302},[278,6897,6898],{"class":333},"forEach",[278,6900,2079],{"class":302},[278,6902,6903],{"class":501},"track",[278,6905,1845],{"class":302},[278,6907,1848],{"class":298},[278,6909,6910],{"class":302}," track.",[278,6912,6882],{"class":333},[278,6914,6915],{"class":302},"());\n",[278,6917,6919],{"class":280,"line":6918},106,[278,6920,292],{"emptyLinePlaceholder":291},[278,6922,6924,6927],{"class":280,"line":6923},107,[278,6925,6926],{"class":298},"        if",[278,6928,6210],{"class":302},[278,6930,6932,6935],{"class":280,"line":6931},108,[278,6933,6934],{"class":333},"          cancelAnimationFrame",[278,6936,6218],{"class":302},[278,6938,6940,6943,6945,6947],{"class":280,"line":6939},109,[278,6941,6942],{"class":302},"          animationFrame ",[278,6944,358],{"class":298},[278,6946,1035],{"class":650},[278,6948,313],{"class":302},[278,6950,6952],{"class":280,"line":6951},110,[278,6953,6954],{"class":302},"        }\n",[278,6956,6958],{"class":280,"line":6957},111,[278,6959,292],{"emptyLinePlaceholder":291},[278,6961,6963,6966,6969],{"class":280,"line":6962},112,[278,6964,6965],{"class":302},"        audioContext?.",[278,6967,6968],{"class":333},"close",[278,6970,1313],{"class":302},[278,6972,6974,6977,6979,6981],{"class":280,"line":6973},113,[278,6975,6976],{"class":302},"        audioContext ",[278,6978,358],{"class":298},[278,6980,1035],{"class":650},[278,6982,313],{"class":302},[278,6984,6986],{"class":280,"line":6985},114,[278,6987,6234],{"class":302},[278,6989,6991],{"class":280,"line":6990},115,[278,6992,1233],{"class":302},[278,6994,6996],{"class":280,"line":6995},116,[278,6997,901],{"class":302},[278,6999,7001],{"class":280,"line":7000},117,[278,7002,292],{"emptyLinePlaceholder":291},[278,7004,7006,7009,7011,7013],{"class":280,"line":7005},118,[278,7007,7008],{"class":333},"  onUnmounted",[278,7010,4611],{"class":302},[278,7012,1848],{"class":298},[278,7014,876],{"class":302},[278,7016,7018,7021],{"class":280,"line":7017},119,[278,7019,7020],{"class":333},"    stopRecording",[278,7022,1313],{"class":302},[278,7024,7026],{"class":280,"line":7025},120,[278,7027,2037],{"class":302},[278,7029,7031],{"class":280,"line":7030},121,[278,7032,292],{"emptyLinePlaceholder":291},[278,7034,7036,7038],{"class":280,"line":7035},122,[278,7037,343],{"class":298},[278,7039,876],{"class":302},[278,7041,7043,7046,7049],{"class":280,"line":7042},123,[278,7044,7045],{"class":302},"    state: ",[278,7047,7048],{"class":333},"readonly",[278,7050,7051],{"class":302},"(state),\n",[278,7053,7055],{"class":280,"line":7054},124,[278,7056,7057],{"class":302},"    startRecording,\n",[278,7059,7061],{"class":280,"line":7060},125,[278,7062,7063],{"class":302},"    stopRecording,\n",[278,7065,7067],{"class":280,"line":7066},126,[278,7068,901],{"class":302},[278,7070,7072],{"class":280,"line":7071},127,[278,7073,617],{"class":302},[11,7075,4149],{},[123,7077,7078,7081,7095,7105],{},[74,7079,7080],{},"Exposes recording start\u002Fstop functionality along with the current recording readonly state",[74,7082,7083,7084,7087,7088,7091,7092,7094],{},"Captures user’s voice using the ",[59,7085,7086],{},"MediaRecorder"," API when ",[59,7089,7090],{},"startRecording"," function is invoked. The ",[59,7093,7086],{}," API is a simple and efficient way to handle media capture in modern browsers, making it ideal for our use case.",[74,7096,7097,7098,919,7101,7104],{},"Captures audio visualization data using ",[59,7099,7100],{},"AudioContext",[59,7102,7103],{},"AnalyserNode"," and updates it in real-time using animation frames",[74,7106,7107,7108,7110,7111,7114],{},"Cleans up resources and returns the captured audio as a ",[59,7109,4157],{}," when ",[59,7112,7113],{},"stopRecording"," is called or if the component unmounts",[32,7116,7118,7121],{"id":7117},"noteeditormodal-component",[59,7119,7120],{},"NoteEditorModal"," Component",[11,7123,4489,7124,263,7127,4217],{},[59,7125,7126],{},"NoteEditorModal.vue",[59,7128,7129],{},"app\u002Fcomponents",[269,7131,7135],{"className":7132,"code":7133,"language":7134,"meta":274,"style":274},"language-xml shiki shiki-themes github-light github-dark","\u003C!-- app\u002Fcomponents\u002FNoteEditorModal.vue -->\n\u003Ctemplate>\n  \u003CUModal\n    fullscreen\n    :close=\"{\n      disabled: isSaving || noteRecorder?.isBusy,\n    }\"\n    :prevent-close=\"isSaving || noteRecorder?.isBusy\"\n    title=\"Create Note\"\n    :ui=\"{\n      body: 'flex-1 w-full max-w-7xl mx-auto flex flex-col md:flex-row gap-4 sm:gap-6 overflow-hidden',\n    }\"\n  >\n    \u003Ctemplate #body>\n      \u003CUCard class=\"flex-1 flex flex-col\" :ui=\"{ body: 'flex-1' }\">\n        \u003Ctemplate #header>\n          \u003Ch3 class=\"h-8 font-medium text-gray-600 dark:text-gray-300\">\n            Note transcript\n          \u003C\u002Fh3>\n        \u003C\u002Ftemplate>\n\n        \u003CUTextarea\n          v-model=\"noteText\"\n          placeholder=\"Type your note here, or use voice recording...\"\n          size=\"lg\"\n          :disabled=\"isSaving || noteRecorder?.isBusy\"\n          :ui=\"{ root: 'w-full h-full', base: ['h-full resize-none'] }\"\n        \u002F>\n      \u003C\u002FUCard>\n\n      \u003CNoteRecorder\n        ref=\"recorder\"\n        class=\"md:h-full md:flex md:flex-col md:w-96 shrink-0 order-first md:order-none\"\n        @transcription=\"handleTranscription\"\n      \u002F>\n    \u003C\u002Ftemplate>\n\n    \u003Ctemplate #footer>\n      \u003CUButton\n        icon=\"i-lucide-undo-2\"\n        color=\"neutral\"\n        variant=\"outline\"\n        :disabled=\"isSaving\"\n        @click=\"resetNote\"\n      >\n        Reset\n      \u003C\u002FUButton>\n\n      \u003CUButton\n        icon=\"i-lucide-cloud-upload\"\n        :disabled=\"!noteText.trim() || noteRecorder?.isBusy || isSaving\"\n        :loading=\"isSaving\"\n        @click=\"saveNote\"\n      >\n        Save Note\n      \u003C\u002FUButton>\n    \u003C\u002Ftemplate>\n  \u003C\u002FUModal>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { NoteRecorder } from \"#components\";\n\nconst props = defineProps\u003C{ onNewNote: () => void }>();\n\ntype NoteRecorderType = InstanceType\u003Ctypeof NoteRecorder>;\nconst noteRecorder = useTemplateRef\u003CNoteRecorderType>(\"recorder\");\nconst resetNote = () => {\n  noteText.value = \"\";\n  noteRecorder.value?.resetRecordings();\n};\n\nconst noteText = ref(\"\");\nconst handleTranscription = (text: string) => {\n  noteText.value += noteText.value ? \"\\n\\n\" : \"\";\n  noteText.value += text ?? \"\";\n};\n\nconst modal = useModal();\nconst isSaving = ref(false);\nconst saveNote = async () => {\n  const text = noteText.value.trim();\n  if (!text) return;\n\n  isSaving.value = true;\n\n  const audioUrls = await noteRecorder.value?.uploadRecordings();\n\n  try {\n    await $fetch(\"\u002Fapi\u002Fnotes\", {\n      method: \"POST\",\n      body: { text, audioUrls },\n    });\n\n    useToast().add({\n      title: \"Note Saved\",\n      description: \"Your note was saved successfully.\",\n      color: \"success\",\n    });\n\n    if (props.onNewNote) {\n      props.onNewNote();\n    }\n\n    modal.close();\n  } catch (err) {\n    console.error(\"Error saving note:\", err);\n    useToast().add({\n      title: \"Save Failed\",\n      description: \"Failed to save the note.\",\n      color: \"error\",\n    });\n  }\n\n  isSaving.value = false;\n};\n\u003C\u002Fscript>\n","xml",[59,7136,7137,7142,7147,7152,7157,7162,7167,7172,7177,7182,7187,7192,7196,7201,7206,7211,7216,7221,7226,7231,7236,7240,7245,7250,7255,7260,7265,7270,7275,7280,7284,7289,7294,7299,7304,7309,7314,7318,7323,7328,7333,7338,7343,7348,7353,7358,7363,7368,7372,7376,7381,7386,7391,7396,7400,7405,7409,7413,7418,7423,7427,7432,7437,7441,7446,7450,7455,7460,7465,7470,7475,7479,7483,7488,7493,7498,7503,7507,7511,7516,7521,7526,7531,7536,7540,7545,7549,7554,7558,7563,7568,7573,7578,7582,7586,7591,7596,7601,7606,7610,7614,7619,7624,7628,7632,7637,7642,7647,7651,7656,7661,7666,7670,7674,7678,7683,7687],{"__ignoreMap":274},[278,7138,7139],{"class":280,"line":281},[278,7140,7141],{},"\u003C!-- app\u002Fcomponents\u002FNoteEditorModal.vue -->\n",[278,7143,7144],{"class":280,"line":288},[278,7145,7146],{},"\u003Ctemplate>\n",[278,7148,7149],{"class":280,"line":295},[278,7150,7151],{},"  \u003CUModal\n",[278,7153,7154],{"class":280,"line":316},[278,7155,7156],{},"    fullscreen\n",[278,7158,7159],{"class":280,"line":322},[278,7160,7161],{},"    :close=\"{\n",[278,7163,7164],{"class":280,"line":327},[278,7165,7166],{},"      disabled: isSaving || noteRecorder?.isBusy,\n",[278,7168,7169],{"class":280,"line":340},[278,7170,7171],{},"    }\"\n",[278,7173,7174],{"class":280,"line":349},[278,7175,7176],{},"    :prevent-close=\"isSaving || noteRecorder?.isBusy\"\n",[278,7178,7179],{"class":280,"line":375},[278,7180,7181],{},"    title=\"Create Note\"\n",[278,7183,7184],{"class":280,"line":386},[278,7185,7186],{},"    :ui=\"{\n",[278,7188,7189],{"class":280,"line":397},[278,7190,7191],{},"      body: 'flex-1 w-full max-w-7xl mx-auto flex flex-col md:flex-row gap-4 sm:gap-6 overflow-hidden',\n",[278,7193,7194],{"class":280,"line":408},[278,7195,7171],{},[278,7197,7198],{"class":280,"line":433},[278,7199,7200],{},"  >\n",[278,7202,7203],{"class":280,"line":454},[278,7204,7205],{},"    \u003Ctemplate #body>\n",[278,7207,7208],{"class":280,"line":475},[278,7209,7210],{},"      \u003CUCard class=\"flex-1 flex flex-col\" :ui=\"{ body: 'flex-1' }\">\n",[278,7212,7213],{"class":280,"line":496},[278,7214,7215],{},"        \u003Ctemplate #header>\n",[278,7217,7218],{"class":280,"line":505},[278,7219,7220],{},"          \u003Ch3 class=\"h-8 font-medium text-gray-600 dark:text-gray-300\">\n",[278,7222,7223],{"class":280,"line":516},[278,7224,7225],{},"            Note transcript\n",[278,7227,7228],{"class":280,"line":527},[278,7229,7230],{},"          \u003C\u002Fh3>\n",[278,7232,7233],{"class":280,"line":533},[278,7234,7235],{},"        \u003C\u002Ftemplate>\n",[278,7237,7238],{"class":280,"line":539},[278,7239,292],{"emptyLinePlaceholder":291},[278,7241,7242],{"class":280,"line":545},[278,7243,7244],{},"        \u003CUTextarea\n",[278,7246,7247],{"class":280,"line":551},[278,7248,7249],{},"          v-model=\"noteText\"\n",[278,7251,7252],{"class":280,"line":557},[278,7253,7254],{},"          placeholder=\"Type your note here, or use voice recording...\"\n",[278,7256,7257],{"class":280,"line":567},[278,7258,7259],{},"          size=\"lg\"\n",[278,7261,7262],{"class":280,"line":577},[278,7263,7264],{},"          :disabled=\"isSaving || noteRecorder?.isBusy\"\n",[278,7266,7267],{"class":280,"line":587},[278,7268,7269],{},"          :ui=\"{ root: 'w-full h-full', base: ['h-full resize-none'] }\"\n",[278,7271,7272],{"class":280,"line":597},[278,7273,7274],{},"        \u002F>\n",[278,7276,7277],{"class":280,"line":608},[278,7278,7279],{},"      \u003C\u002FUCard>\n",[278,7281,7282],{"class":280,"line":614},[278,7283,292],{"emptyLinePlaceholder":291},[278,7285,7286],{"class":280,"line":620},[278,7287,7288],{},"      \u003CNoteRecorder\n",[278,7290,7291],{"class":280,"line":625},[278,7292,7293],{},"        ref=\"recorder\"\n",[278,7295,7296],{"class":280,"line":640},[278,7297,7298],{},"        class=\"md:h-full md:flex md:flex-col md:w-96 shrink-0 order-first md:order-none\"\n",[278,7300,7301],{"class":280,"line":663},[278,7302,7303],{},"        @transcription=\"handleTranscription\"\n",[278,7305,7306],{"class":280,"line":669},[278,7307,7308],{},"      \u002F>\n",[278,7310,7311],{"class":280,"line":680},[278,7312,7313],{},"    \u003C\u002Ftemplate>\n",[278,7315,7316],{"class":280,"line":686},[278,7317,292],{"emptyLinePlaceholder":291},[278,7319,7320],{"class":280,"line":1334},[278,7321,7322],{},"    \u003Ctemplate #footer>\n",[278,7324,7325],{"class":280,"line":1375},[278,7326,7327],{},"      \u003CUButton\n",[278,7329,7330],{"class":280,"line":1381},[278,7331,7332],{},"        icon=\"i-lucide-undo-2\"\n",[278,7334,7335],{"class":280,"line":1386},[278,7336,7337],{},"        color=\"neutral\"\n",[278,7339,7340],{"class":280,"line":1394},[278,7341,7342],{},"        variant=\"outline\"\n",[278,7344,7345],{"class":280,"line":1406},[278,7346,7347],{},"        :disabled=\"isSaving\"\n",[278,7349,7350],{"class":280,"line":1423},[278,7351,7352],{},"        @click=\"resetNote\"\n",[278,7354,7355],{"class":280,"line":1432},[278,7356,7357],{},"      >\n",[278,7359,7360],{"class":280,"line":1437},[278,7361,7362],{},"        Reset\n",[278,7364,7365],{"class":280,"line":1916},[278,7366,7367],{},"      \u003C\u002FUButton>\n",[278,7369,7370],{"class":280,"line":1939},[278,7371,292],{"emptyLinePlaceholder":291},[278,7373,7374],{"class":280,"line":1949},[278,7375,7327],{},[278,7377,7378],{"class":280,"line":1954},[278,7379,7380],{},"        icon=\"i-lucide-cloud-upload\"\n",[278,7382,7383],{"class":280,"line":1959},[278,7384,7385],{},"        :disabled=\"!noteText.trim() || noteRecorder?.isBusy || isSaving\"\n",[278,7387,7388],{"class":280,"line":1985},[278,7389,7390],{},"        :loading=\"isSaving\"\n",[278,7392,7393],{"class":280,"line":1990},[278,7394,7395],{},"        @click=\"saveNote\"\n",[278,7397,7398],{"class":280,"line":1997},[278,7399,7357],{},[278,7401,7402],{"class":280,"line":2006},[278,7403,7404],{},"        Save Note\n",[278,7406,7407],{"class":280,"line":2018},[278,7408,7367],{},[278,7410,7411],{"class":280,"line":2029},[278,7412,7313],{},[278,7414,7415],{"class":280,"line":2034},[278,7416,7417],{},"  \u003C\u002FUModal>\n",[278,7419,7420],{"class":280,"line":2040},[278,7421,7422],{},"\u003C\u002Ftemplate>\n",[278,7424,7425],{"class":280,"line":2045},[278,7426,292],{"emptyLinePlaceholder":291},[278,7428,7429],{"class":280,"line":2068},[278,7430,7431],{},"\u003Cscript setup lang=\"ts\">\n",[278,7433,7434],{"class":280,"line":2099},[278,7435,7436],{},"import { NoteRecorder } from \"#components\";\n",[278,7438,7439],{"class":280,"line":6428},[278,7440,292],{"emptyLinePlaceholder":291},[278,7442,7443],{"class":280,"line":6439},[278,7444,7445],{},"const props = defineProps\u003C{ onNewNote: () => void }>();\n",[278,7447,7448],{"class":280,"line":6450},[278,7449,292],{"emptyLinePlaceholder":291},[278,7451,7452],{"class":280,"line":6455},[278,7453,7454],{},"type NoteRecorderType = InstanceType\u003Ctypeof NoteRecorder>;\n",[278,7456,7457],{"class":280,"line":6460},[278,7458,7459],{},"const noteRecorder = useTemplateRef\u003CNoteRecorderType>(\"recorder\");\n",[278,7461,7462],{"class":280,"line":6475},[278,7463,7464],{},"const resetNote = () => {\n",[278,7466,7467],{"class":280,"line":6486},[278,7468,7469],{},"  noteText.value = \"\";\n",[278,7471,7472],{"class":280,"line":6491},[278,7473,7474],{},"  noteRecorder.value?.resetRecordings();\n",[278,7476,7477],{"class":280,"line":6518},[278,7478,2817],{},[278,7480,7481],{"class":280,"line":6530},[278,7482,292],{"emptyLinePlaceholder":291},[278,7484,7485],{"class":280,"line":6542},[278,7486,7487],{},"const noteText = ref(\"\");\n",[278,7489,7490],{"class":280,"line":6547},[278,7491,7492],{},"const handleTranscription = (text: string) => {\n",[278,7494,7495],{"class":280,"line":6552},[278,7496,7497],{},"  noteText.value += noteText.value ? \"\\n\\n\" : \"\";\n",[278,7499,7500],{"class":280,"line":6567},[278,7501,7502],{},"  noteText.value += text ?? \"\";\n",[278,7504,7505],{"class":280,"line":6580},[278,7506,2817],{},[278,7508,7509],{"class":280,"line":6593},[278,7510,292],{"emptyLinePlaceholder":291},[278,7512,7513],{"class":280,"line":6605},[278,7514,7515],{},"const modal = useModal();\n",[278,7517,7518],{"class":280,"line":6620},[278,7519,7520],{},"const isSaving = ref(false);\n",[278,7522,7523],{"class":280,"line":6625},[278,7524,7525],{},"const saveNote = async () => {\n",[278,7527,7528],{"class":280,"line":6633},[278,7529,7530],{},"  const text = noteText.value.trim();\n",[278,7532,7533],{"class":280,"line":6643},[278,7534,7535],{},"  if (!text) return;\n",[278,7537,7538],{"class":280,"line":6657},[278,7539,292],{"emptyLinePlaceholder":291},[278,7541,7542],{"class":280,"line":6665},[278,7543,7544],{},"  isSaving.value = true;\n",[278,7546,7547],{"class":280,"line":6670},[278,7548,292],{"emptyLinePlaceholder":291},[278,7550,7551],{"class":280,"line":6675},[278,7552,7553],{},"  const audioUrls = await noteRecorder.value?.uploadRecordings();\n",[278,7555,7556],{"class":280,"line":6680},[278,7557,292],{"emptyLinePlaceholder":291},[278,7559,7560],{"class":280,"line":6698},[278,7561,7562],{},"  try {\n",[278,7564,7565],{"class":280,"line":6725},[278,7566,7567],{},"    await $fetch(\"\u002Fapi\u002Fnotes\", {\n",[278,7569,7570],{"class":280,"line":6738},[278,7571,7572],{},"      method: \"POST\",\n",[278,7574,7575],{"class":280,"line":6752},[278,7576,7577],{},"      body: { text, audioUrls },\n",[278,7579,7580],{"class":280,"line":6769},[278,7581,1233],{},[278,7583,7584],{"class":280,"line":6786},[278,7585,292],{"emptyLinePlaceholder":291},[278,7587,7588],{"class":280,"line":6798},[278,7589,7590],{},"    useToast().add({\n",[278,7592,7593],{"class":280,"line":6803},[278,7594,7595],{},"      title: \"Note Saved\",\n",[278,7597,7598],{"class":280,"line":6815},[278,7599,7600],{},"      description: \"Your note was saved successfully.\",\n",[278,7602,7603],{"class":280,"line":6827},[278,7604,7605],{},"      color: \"success\",\n",[278,7607,7608],{"class":280,"line":6839},[278,7609,1233],{},[278,7611,7612],{"class":280,"line":6844},[278,7613,292],{"emptyLinePlaceholder":291},[278,7615,7616],{"class":280,"line":6853},[278,7617,7618],{},"    if (props.onNewNote) {\n",[278,7620,7621],{"class":280,"line":6859},[278,7622,7623],{},"      props.onNewNote();\n",[278,7625,7626],{"class":280,"line":6864},[278,7627,1285],{},[278,7629,7630],{"class":280,"line":6877},[278,7631,292],{"emptyLinePlaceholder":291},[278,7633,7634],{"class":280,"line":6887},[278,7635,7636],{},"    modal.close();\n",[278,7638,7639],{"class":280,"line":6918},[278,7640,7641],{},"  } catch (err) {\n",[278,7643,7644],{"class":280,"line":6923},[278,7645,7646],{},"    console.error(\"Error saving note:\", err);\n",[278,7648,7649],{"class":280,"line":6931},[278,7650,7590],{},[278,7652,7653],{"class":280,"line":6939},[278,7654,7655],{},"      title: \"Save Failed\",\n",[278,7657,7658],{"class":280,"line":6951},[278,7659,7660],{},"      description: \"Failed to save the note.\",\n",[278,7662,7663],{"class":280,"line":6957},[278,7664,7665],{},"      color: \"error\",\n",[278,7667,7668],{"class":280,"line":6962},[278,7669,1233],{},[278,7671,7672],{"class":280,"line":6973},[278,7673,1096],{},[278,7675,7676],{"class":280,"line":6985},[278,7677,292],{"emptyLinePlaceholder":291},[278,7679,7680],{"class":280,"line":6990},[278,7681,7682],{},"  isSaving.value = false;\n",[278,7684,7685],{"class":280,"line":6995},[278,7686,2817],{},[278,7688,7689],{"class":280,"line":7000},[278,7690,7691],{},"\u003C\u002Fscript>\n",[11,7693,7694],{},"The above modal component does the following:",[123,7696,7697,7704,7714,7720,7730],{},[74,7698,7699,7700,7703],{},"Displays a ",[59,7701,7702],{},"textarea"," for allowing a manual note entry",[74,7705,7706,7707,7710,7711,7713],{},"The modal integrates the ",[59,7708,7709],{},"NoteRecorder"," component for voice recordings and manages the data flow between the recordings and the ",[59,7712,7702],{}," for user notes.",[74,7715,7716,7717,7719],{},"Whenever a new recording is created, it captures the emitted event from the note recorder component, and appends the transcription text to the ",[59,7718,7702],{}," content",[74,7721,7722,7723,7726,7727,7729],{},"When the user clicks the save note button, its first uploads all recordings (if any) by calling the note recorder’s ",[59,7724,7725],{},"uploadRecordings"," method, and then save the note by calling the ",[59,7728,4394],{}," API endpoint created earlier.",[74,7731,7732,7733,7735,7736,7738],{},"The save note button first uploads all recordings (if any) asynchronously by calling the ",[59,7734,7725],{}," method, then sends the note data to the ",[59,7737,4939],{}," endpoint. Upon success, it notifies the parent by executing the callback passed by it, and then closes the modal.",[32,7740,7742,7121],{"id":7741},"noterecorder-component",[59,7743,7709],{},[11,7745,5437,7746,263,7749,7751],{},[59,7747,7748],{},"NoteRecorder.vue",[59,7750,7129],{}," folder and add the following content to it:",[269,7753,7755],{"className":7132,"code":7754,"language":7134,"meta":274,"style":274},"\u003C!-- app\u002Fcomponents\u002FNoteRecorder.vue --> \n\u003Ctemplate>\n  \u003CUCard\n    :ui=\"{\n      body: 'max-h-36 md:max-h-none md:flex-1 overflow-y-auto',\n    }\"\n  >\n    \u003Ctemplate #header>\n      \u003Ch3 class=\"font-medium text-gray-600 dark:text-gray-300\">Recordings\u003C\u002Fh3>\n\n      \u003Cdiv class=\"flex items-center gap-x-2\">\n        \u003Ctemplate v-if=\"state.isRecording\">\n          \u003Cdiv class=\"w-2 h-2 rounded-full bg-red-500 animate-pulse\" \u002F>\n          \u003Cspan class=\"mr-2 text-sm\">\n            {{ formatDuration(state.recordingDuration) }}\n          \u003C\u002Fspan>\n        \u003C\u002Ftemplate>\n\n        \u003CUButton\n          :icon=\"state.isRecording ? 'i-lucide-circle-stop' : 'i-lucide-mic'\"\n          :color=\"state.isRecording ? 'error' : 'primary'\"\n          :loading=\"isTranscribing\"\n          @click=\"toggleRecording\"\n        \u002F>\n      \u003C\u002Fdiv>\n    \u003C\u002Ftemplate>\n\n    \u003CAudioVisualizer\n      v-if=\"state.isRecording\"\n      class=\"w-full h-14 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2\"\n      :audio-data=\"state.audioData\"\n      :data-update-trigger=\"state.updateTrigger\"\n    \u002F>\n\n    \u003Cdiv\n      v-else-if=\"isTranscribing\"\n      class=\"flex items-center justify-center h-14 gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2 text-gray-500 dark:text-gray-400\"\n    >\n      \u003CUIcon name=\"i-lucide-refresh-cw\" size=\"size-6\" class=\"animate-spin\" \u002F>\n      Transcribing...\n    \u003C\u002Fdiv>\n\n    \u003Cdiv class=\"space-y-2\">\n      \u003Cdiv\n        v-for=\"recording in recordings\"\n        :key=\"recording.id\"\n        class=\"flex items-center gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg\"\n      >\n        \u003Caudio :src=\"recording.url\" controls class=\"w-full h-10\" \u002F>\n\n        \u003CUButton\n          icon=\"i-lucide-trash-2\"\n          color=\"error\"\n          variant=\"ghost\"\n          size=\"sm\"\n          @click=\"removeRecording(recording)\"\n        \u002F>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv\n      v-if=\"!recordings.length && !state.isRecording && !isTranscribing\"\n      class=\"h-full flex flex-col items-center justify-center text-gray-500 dark:text-gray-400\"\n    >\n      \u003Cp>No recordings...!\u003C\u002Fp>\n      \u003Cp class=\"text-sm mt-1\">Tap the mic icon to create one.\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  \u003C\u002FUCard>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst emit = defineEmits\u003C{ transcription: [text: string] }>();\n\nconst { state, startRecording, stopRecording } = useMediaRecorder();\nconst toggleRecording = () => {\n  if (state.value.isRecording) {\n    handleRecordingStop();\n  } else {\n    handleRecordingStart();\n  }\n};\n\nconst handleRecordingStart = async () => {\n  try {\n    await startRecording();\n  } catch (err) {\n    console.error(\"Error accessing microphone:\", err);\n    useToast().add({\n      title: \"Error\",\n      description: \"Could not access microphone. Please check permissions.\",\n      color: \"error\",\n    });\n  }\n};\n\nconst { recordings, addRecording, removeRecording, resetRecordings } =\n  useRecordings();\n\nconst handleRecordingStop = async () => {\n  let blob: Blob | undefined;\n\n  try {\n    blob = await stopRecording();\n  } catch (err) {\n    console.error(\"Error stopping recording:\", err);\n    useToast().add({\n      title: \"Error\",\n      description: \"Failed to record audio. Please try again.\",\n      color: \"error\",\n    });\n  }\n\n  if (blob) {\n    try {\n      const transcription = await transcribeAudio(blob);\n\n      if (transcription) {\n        emit(\"transcription\", transcription);\n\n        addRecording({\n          url: URL.createObjectURL(blob),\n          blob,\n          id: `${Date.now()}`,\n        });\n      }\n    } catch (err) {\n      console.error(\"Error transcribing audio:\", err);\n      useToast().add({\n        title: \"Error\",\n        description: \"Failed to transcribe audio. Please try again.\",\n        color: \"error\",\n      });\n    }\n  }\n};\n\nconst isTranscribing = ref(false);\nconst transcribeAudio = async (blob: Blob) => {\n  try {\n    isTranscribing.value = true;\n    const formData = new FormData();\n    formData.append(\"audio\", blob);\n\n    return await $fetch(\"\u002Fapi\u002Ftranscribe\", {\n      method: \"POST\",\n      body: formData,\n    });\n  } finally {\n    isTranscribing.value = false;\n  }\n};\n\nconst uploadRecordings = async () => {\n  if (!recordings.value.length) return;\n\n  const formData = new FormData();\n  recordings.value.forEach((recording) => {\n    if (recording.blob) {\n      formData.append(\n        \"files\",\n        recording.blob,\n        `${recording.id}.${recording.blob.type.split(\"\u002F\")[1]}`,\n      );\n    }\n  });\n\n  try {\n    const result = await $fetch(\"\u002Fapi\u002Fupload\", {\n      method: \"PUT\",\n      body: formData,\n    });\n\n    return result.map((obj) => obj.pathname);\n  } catch (error) {\n    console.error(\"Failed to upload audio recordings\", error);\n  }\n};\n\nconst isBusy = computed(() => state.value.isRecording || isTranscribing.value);\n\ndefineExpose({ uploadRecordings, resetRecordings, isBusy });\n\nconst formatDuration = (seconds: number) => {\n  const mins = Math.floor(seconds \u002F 60);\n  const secs = seconds % 60;\n  return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n};\n\u003C\u002Fscript>\n",[59,7756,7757,7762,7766,7771,7775,7780,7784,7788,7793,7798,7802,7807,7812,7817,7822,7827,7832,7836,7840,7845,7850,7855,7860,7865,7869,7874,7878,7882,7887,7892,7897,7902,7907,7912,7916,7921,7926,7931,7936,7941,7946,7951,7955,7960,7965,7970,7975,7980,7984,7989,7993,7997,8002,8007,8012,8017,8022,8026,8030,8034,8038,8042,8047,8052,8056,8061,8066,8070,8075,8079,8083,8087,8092,8096,8101,8106,8111,8116,8121,8126,8130,8134,8138,8143,8147,8152,8156,8161,8165,8170,8175,8179,8183,8187,8191,8195,8200,8205,8209,8214,8219,8223,8227,8232,8236,8241,8245,8249,8254,8258,8262,8266,8270,8275,8280,8285,8289,8294,8299,8303,8308,8313,8318,8323,8328,8332,8337,8342,8348,8354,8360,8366,8371,8376,8381,8386,8391,8397,8403,8408,8414,8420,8426,8431,8437,8442,8448,8453,8459,8465,8470,8475,8480,8486,8492,8497,8503,8509,8515,8521,8527,8533,8539,8544,8549,8554,8559,8564,8570,8576,8581,8586,8591,8597,8603,8609,8614,8619,8624,8630,8635,8641,8646,8652,8658,8664,8670,8675],{"__ignoreMap":274},[278,7758,7759],{"class":280,"line":281},[278,7760,7761],{},"\u003C!-- app\u002Fcomponents\u002FNoteRecorder.vue --> \n",[278,7763,7764],{"class":280,"line":288},[278,7765,7146],{},[278,7767,7768],{"class":280,"line":295},[278,7769,7770],{},"  \u003CUCard\n",[278,7772,7773],{"class":280,"line":316},[278,7774,7186],{},[278,7776,7777],{"class":280,"line":322},[278,7778,7779],{},"      body: 'max-h-36 md:max-h-none md:flex-1 overflow-y-auto',\n",[278,7781,7782],{"class":280,"line":327},[278,7783,7171],{},[278,7785,7786],{"class":280,"line":340},[278,7787,7200],{},[278,7789,7790],{"class":280,"line":349},[278,7791,7792],{},"    \u003Ctemplate #header>\n",[278,7794,7795],{"class":280,"line":375},[278,7796,7797],{},"      \u003Ch3 class=\"font-medium text-gray-600 dark:text-gray-300\">Recordings\u003C\u002Fh3>\n",[278,7799,7800],{"class":280,"line":386},[278,7801,292],{"emptyLinePlaceholder":291},[278,7803,7804],{"class":280,"line":397},[278,7805,7806],{},"      \u003Cdiv class=\"flex items-center gap-x-2\">\n",[278,7808,7809],{"class":280,"line":408},[278,7810,7811],{},"        \u003Ctemplate v-if=\"state.isRecording\">\n",[278,7813,7814],{"class":280,"line":433},[278,7815,7816],{},"          \u003Cdiv class=\"w-2 h-2 rounded-full bg-red-500 animate-pulse\" \u002F>\n",[278,7818,7819],{"class":280,"line":454},[278,7820,7821],{},"          \u003Cspan class=\"mr-2 text-sm\">\n",[278,7823,7824],{"class":280,"line":475},[278,7825,7826],{},"            {{ formatDuration(state.recordingDuration) }}\n",[278,7828,7829],{"class":280,"line":496},[278,7830,7831],{},"          \u003C\u002Fspan>\n",[278,7833,7834],{"class":280,"line":505},[278,7835,7235],{},[278,7837,7838],{"class":280,"line":516},[278,7839,292],{"emptyLinePlaceholder":291},[278,7841,7842],{"class":280,"line":527},[278,7843,7844],{},"        \u003CUButton\n",[278,7846,7847],{"class":280,"line":533},[278,7848,7849],{},"          :icon=\"state.isRecording ? 'i-lucide-circle-stop' : 'i-lucide-mic'\"\n",[278,7851,7852],{"class":280,"line":539},[278,7853,7854],{},"          :color=\"state.isRecording ? 'error' : 'primary'\"\n",[278,7856,7857],{"class":280,"line":545},[278,7858,7859],{},"          :loading=\"isTranscribing\"\n",[278,7861,7862],{"class":280,"line":551},[278,7863,7864],{},"          @click=\"toggleRecording\"\n",[278,7866,7867],{"class":280,"line":557},[278,7868,7274],{},[278,7870,7871],{"class":280,"line":567},[278,7872,7873],{},"      \u003C\u002Fdiv>\n",[278,7875,7876],{"class":280,"line":577},[278,7877,7313],{},[278,7879,7880],{"class":280,"line":587},[278,7881,292],{"emptyLinePlaceholder":291},[278,7883,7884],{"class":280,"line":597},[278,7885,7886],{},"    \u003CAudioVisualizer\n",[278,7888,7889],{"class":280,"line":608},[278,7890,7891],{},"      v-if=\"state.isRecording\"\n",[278,7893,7894],{"class":280,"line":614},[278,7895,7896],{},"      class=\"w-full h-14 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2\"\n",[278,7898,7899],{"class":280,"line":620},[278,7900,7901],{},"      :audio-data=\"state.audioData\"\n",[278,7903,7904],{"class":280,"line":625},[278,7905,7906],{},"      :data-update-trigger=\"state.updateTrigger\"\n",[278,7908,7909],{"class":280,"line":640},[278,7910,7911],{},"    \u002F>\n",[278,7913,7914],{"class":280,"line":663},[278,7915,292],{"emptyLinePlaceholder":291},[278,7917,7918],{"class":280,"line":669},[278,7919,7920],{},"    \u003Cdiv\n",[278,7922,7923],{"class":280,"line":680},[278,7924,7925],{},"      v-else-if=\"isTranscribing\"\n",[278,7927,7928],{"class":280,"line":686},[278,7929,7930],{},"      class=\"flex items-center justify-center h-14 gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2 text-gray-500 dark:text-gray-400\"\n",[278,7932,7933],{"class":280,"line":1334},[278,7934,7935],{},"    >\n",[278,7937,7938],{"class":280,"line":1375},[278,7939,7940],{},"      \u003CUIcon name=\"i-lucide-refresh-cw\" size=\"size-6\" class=\"animate-spin\" \u002F>\n",[278,7942,7943],{"class":280,"line":1381},[278,7944,7945],{},"      Transcribing...\n",[278,7947,7948],{"class":280,"line":1386},[278,7949,7950],{},"    \u003C\u002Fdiv>\n",[278,7952,7953],{"class":280,"line":1394},[278,7954,292],{"emptyLinePlaceholder":291},[278,7956,7957],{"class":280,"line":1406},[278,7958,7959],{},"    \u003Cdiv class=\"space-y-2\">\n",[278,7961,7962],{"class":280,"line":1423},[278,7963,7964],{},"      \u003Cdiv\n",[278,7966,7967],{"class":280,"line":1432},[278,7968,7969],{},"        v-for=\"recording in recordings\"\n",[278,7971,7972],{"class":280,"line":1437},[278,7973,7974],{},"        :key=\"recording.id\"\n",[278,7976,7977],{"class":280,"line":1916},[278,7978,7979],{},"        class=\"flex items-center gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg\"\n",[278,7981,7982],{"class":280,"line":1939},[278,7983,7357],{},[278,7985,7986],{"class":280,"line":1949},[278,7987,7988],{},"        \u003Caudio :src=\"recording.url\" controls class=\"w-full h-10\" \u002F>\n",[278,7990,7991],{"class":280,"line":1954},[278,7992,292],{"emptyLinePlaceholder":291},[278,7994,7995],{"class":280,"line":1959},[278,7996,7844],{},[278,7998,7999],{"class":280,"line":1985},[278,8000,8001],{},"          icon=\"i-lucide-trash-2\"\n",[278,8003,8004],{"class":280,"line":1990},[278,8005,8006],{},"          color=\"error\"\n",[278,8008,8009],{"class":280,"line":1997},[278,8010,8011],{},"          variant=\"ghost\"\n",[278,8013,8014],{"class":280,"line":2006},[278,8015,8016],{},"          size=\"sm\"\n",[278,8018,8019],{"class":280,"line":2018},[278,8020,8021],{},"          @click=\"removeRecording(recording)\"\n",[278,8023,8024],{"class":280,"line":2029},[278,8025,7274],{},[278,8027,8028],{"class":280,"line":2034},[278,8029,7873],{},[278,8031,8032],{"class":280,"line":2040},[278,8033,7950],{},[278,8035,8036],{"class":280,"line":2045},[278,8037,292],{"emptyLinePlaceholder":291},[278,8039,8040],{"class":280,"line":2068},[278,8041,7920],{},[278,8043,8044],{"class":280,"line":2099},[278,8045,8046],{},"      v-if=\"!recordings.length && !state.isRecording && !isTranscribing\"\n",[278,8048,8049],{"class":280,"line":6428},[278,8050,8051],{},"      class=\"h-full flex flex-col items-center justify-center text-gray-500 dark:text-gray-400\"\n",[278,8053,8054],{"class":280,"line":6439},[278,8055,7935],{},[278,8057,8058],{"class":280,"line":6450},[278,8059,8060],{},"      \u003Cp>No recordings...!\u003C\u002Fp>\n",[278,8062,8063],{"class":280,"line":6455},[278,8064,8065],{},"      \u003Cp class=\"text-sm mt-1\">Tap the mic icon to create one.\u003C\u002Fp>\n",[278,8067,8068],{"class":280,"line":6460},[278,8069,7950],{},[278,8071,8072],{"class":280,"line":6475},[278,8073,8074],{},"  \u003C\u002FUCard>\n",[278,8076,8077],{"class":280,"line":6486},[278,8078,7422],{},[278,8080,8081],{"class":280,"line":6491},[278,8082,292],{"emptyLinePlaceholder":291},[278,8084,8085],{"class":280,"line":6518},[278,8086,7431],{},[278,8088,8089],{"class":280,"line":6530},[278,8090,8091],{},"const emit = defineEmits\u003C{ transcription: [text: string] }>();\n",[278,8093,8094],{"class":280,"line":6542},[278,8095,292],{"emptyLinePlaceholder":291},[278,8097,8098],{"class":280,"line":6547},[278,8099,8100],{},"const { state, startRecording, stopRecording } = useMediaRecorder();\n",[278,8102,8103],{"class":280,"line":6552},[278,8104,8105],{},"const toggleRecording = () => {\n",[278,8107,8108],{"class":280,"line":6567},[278,8109,8110],{},"  if (state.value.isRecording) {\n",[278,8112,8113],{"class":280,"line":6580},[278,8114,8115],{},"    handleRecordingStop();\n",[278,8117,8118],{"class":280,"line":6593},[278,8119,8120],{},"  } else {\n",[278,8122,8123],{"class":280,"line":6605},[278,8124,8125],{},"    handleRecordingStart();\n",[278,8127,8128],{"class":280,"line":6620},[278,8129,1096],{},[278,8131,8132],{"class":280,"line":6625},[278,8133,2817],{},[278,8135,8136],{"class":280,"line":6633},[278,8137,292],{"emptyLinePlaceholder":291},[278,8139,8140],{"class":280,"line":6643},[278,8141,8142],{},"const handleRecordingStart = async () => {\n",[278,8144,8145],{"class":280,"line":6657},[278,8146,7562],{},[278,8148,8149],{"class":280,"line":6665},[278,8150,8151],{},"    await startRecording();\n",[278,8153,8154],{"class":280,"line":6670},[278,8155,7641],{},[278,8157,8158],{"class":280,"line":6675},[278,8159,8160],{},"    console.error(\"Error accessing microphone:\", err);\n",[278,8162,8163],{"class":280,"line":6680},[278,8164,7590],{},[278,8166,8167],{"class":280,"line":6698},[278,8168,8169],{},"      title: \"Error\",\n",[278,8171,8172],{"class":280,"line":6725},[278,8173,8174],{},"      description: \"Could not access microphone. Please check permissions.\",\n",[278,8176,8177],{"class":280,"line":6738},[278,8178,7665],{},[278,8180,8181],{"class":280,"line":6752},[278,8182,1233],{},[278,8184,8185],{"class":280,"line":6769},[278,8186,1096],{},[278,8188,8189],{"class":280,"line":6786},[278,8190,2817],{},[278,8192,8193],{"class":280,"line":6798},[278,8194,292],{"emptyLinePlaceholder":291},[278,8196,8197],{"class":280,"line":6803},[278,8198,8199],{},"const { recordings, addRecording, removeRecording, resetRecordings } =\n",[278,8201,8202],{"class":280,"line":6815},[278,8203,8204],{},"  useRecordings();\n",[278,8206,8207],{"class":280,"line":6827},[278,8208,292],{"emptyLinePlaceholder":291},[278,8210,8211],{"class":280,"line":6839},[278,8212,8213],{},"const handleRecordingStop = async () => {\n",[278,8215,8216],{"class":280,"line":6844},[278,8217,8218],{},"  let blob: Blob | undefined;\n",[278,8220,8221],{"class":280,"line":6853},[278,8222,292],{"emptyLinePlaceholder":291},[278,8224,8225],{"class":280,"line":6859},[278,8226,7562],{},[278,8228,8229],{"class":280,"line":6864},[278,8230,8231],{},"    blob = await stopRecording();\n",[278,8233,8234],{"class":280,"line":6877},[278,8235,7641],{},[278,8237,8238],{"class":280,"line":6887},[278,8239,8240],{},"    console.error(\"Error stopping recording:\", err);\n",[278,8242,8243],{"class":280,"line":6918},[278,8244,7590],{},[278,8246,8247],{"class":280,"line":6923},[278,8248,8169],{},[278,8250,8251],{"class":280,"line":6931},[278,8252,8253],{},"      description: \"Failed to record audio. Please try again.\",\n",[278,8255,8256],{"class":280,"line":6939},[278,8257,7665],{},[278,8259,8260],{"class":280,"line":6951},[278,8261,1233],{},[278,8263,8264],{"class":280,"line":6957},[278,8265,1096],{},[278,8267,8268],{"class":280,"line":6962},[278,8269,292],{"emptyLinePlaceholder":291},[278,8271,8272],{"class":280,"line":6973},[278,8273,8274],{},"  if (blob) {\n",[278,8276,8277],{"class":280,"line":6985},[278,8278,8279],{},"    try {\n",[278,8281,8282],{"class":280,"line":6990},[278,8283,8284],{},"      const transcription = await transcribeAudio(blob);\n",[278,8286,8287],{"class":280,"line":6995},[278,8288,292],{"emptyLinePlaceholder":291},[278,8290,8291],{"class":280,"line":7000},[278,8292,8293],{},"      if (transcription) {\n",[278,8295,8296],{"class":280,"line":7005},[278,8297,8298],{},"        emit(\"transcription\", transcription);\n",[278,8300,8301],{"class":280,"line":7017},[278,8302,292],{"emptyLinePlaceholder":291},[278,8304,8305],{"class":280,"line":7025},[278,8306,8307],{},"        addRecording({\n",[278,8309,8310],{"class":280,"line":7030},[278,8311,8312],{},"          url: URL.createObjectURL(blob),\n",[278,8314,8315],{"class":280,"line":7035},[278,8316,8317],{},"          blob,\n",[278,8319,8320],{"class":280,"line":7042},[278,8321,8322],{},"          id: `${Date.now()}`,\n",[278,8324,8325],{"class":280,"line":7054},[278,8326,8327],{},"        });\n",[278,8329,8330],{"class":280,"line":7060},[278,8331,6234],{},[278,8333,8334],{"class":280,"line":7066},[278,8335,8336],{},"    } catch (err) {\n",[278,8338,8339],{"class":280,"line":7071},[278,8340,8341],{},"      console.error(\"Error transcribing audio:\", err);\n",[278,8343,8345],{"class":280,"line":8344},128,[278,8346,8347],{},"      useToast().add({\n",[278,8349,8351],{"class":280,"line":8350},129,[278,8352,8353],{},"        title: \"Error\",\n",[278,8355,8357],{"class":280,"line":8356},130,[278,8358,8359],{},"        description: \"Failed to transcribe audio. Please try again.\",\n",[278,8361,8363],{"class":280,"line":8362},131,[278,8364,8365],{},"        color: \"error\",\n",[278,8367,8369],{"class":280,"line":8368},132,[278,8370,5148],{},[278,8372,8374],{"class":280,"line":8373},133,[278,8375,1285],{},[278,8377,8379],{"class":280,"line":8378},134,[278,8380,1096],{},[278,8382,8384],{"class":280,"line":8383},135,[278,8385,2817],{},[278,8387,8389],{"class":280,"line":8388},136,[278,8390,292],{"emptyLinePlaceholder":291},[278,8392,8394],{"class":280,"line":8393},137,[278,8395,8396],{},"const isTranscribing = ref(false);\n",[278,8398,8400],{"class":280,"line":8399},138,[278,8401,8402],{},"const transcribeAudio = async (blob: Blob) => {\n",[278,8404,8406],{"class":280,"line":8405},139,[278,8407,7562],{},[278,8409,8411],{"class":280,"line":8410},140,[278,8412,8413],{},"    isTranscribing.value = true;\n",[278,8415,8417],{"class":280,"line":8416},141,[278,8418,8419],{},"    const formData = new FormData();\n",[278,8421,8423],{"class":280,"line":8422},142,[278,8424,8425],{},"    formData.append(\"audio\", blob);\n",[278,8427,8429],{"class":280,"line":8428},143,[278,8430,292],{"emptyLinePlaceholder":291},[278,8432,8434],{"class":280,"line":8433},144,[278,8435,8436],{},"    return await $fetch(\"\u002Fapi\u002Ftranscribe\", {\n",[278,8438,8440],{"class":280,"line":8439},145,[278,8441,7572],{},[278,8443,8445],{"class":280,"line":8444},146,[278,8446,8447],{},"      body: formData,\n",[278,8449,8451],{"class":280,"line":8450},147,[278,8452,1233],{},[278,8454,8456],{"class":280,"line":8455},148,[278,8457,8458],{},"  } finally {\n",[278,8460,8462],{"class":280,"line":8461},149,[278,8463,8464],{},"    isTranscribing.value = false;\n",[278,8466,8468],{"class":280,"line":8467},150,[278,8469,1096],{},[278,8471,8473],{"class":280,"line":8472},151,[278,8474,2817],{},[278,8476,8478],{"class":280,"line":8477},152,[278,8479,292],{"emptyLinePlaceholder":291},[278,8481,8483],{"class":280,"line":8482},153,[278,8484,8485],{},"const uploadRecordings = async () => {\n",[278,8487,8489],{"class":280,"line":8488},154,[278,8490,8491],{},"  if (!recordings.value.length) return;\n",[278,8493,8495],{"class":280,"line":8494},155,[278,8496,292],{"emptyLinePlaceholder":291},[278,8498,8500],{"class":280,"line":8499},156,[278,8501,8502],{},"  const formData = new FormData();\n",[278,8504,8506],{"class":280,"line":8505},157,[278,8507,8508],{},"  recordings.value.forEach((recording) => {\n",[278,8510,8512],{"class":280,"line":8511},158,[278,8513,8514],{},"    if (recording.blob) {\n",[278,8516,8518],{"class":280,"line":8517},159,[278,8519,8520],{},"      formData.append(\n",[278,8522,8524],{"class":280,"line":8523},160,[278,8525,8526],{},"        \"files\",\n",[278,8528,8530],{"class":280,"line":8529},161,[278,8531,8532],{},"        recording.blob,\n",[278,8534,8536],{"class":280,"line":8535},162,[278,8537,8538],{},"        `${recording.id}.${recording.blob.type.split(\"\u002F\")[1]}`,\n",[278,8540,8542],{"class":280,"line":8541},163,[278,8543,2616],{},[278,8545,8547],{"class":280,"line":8546},164,[278,8548,1285],{},[278,8550,8552],{"class":280,"line":8551},165,[278,8553,2037],{},[278,8555,8557],{"class":280,"line":8556},166,[278,8558,292],{"emptyLinePlaceholder":291},[278,8560,8562],{"class":280,"line":8561},167,[278,8563,7562],{},[278,8565,8567],{"class":280,"line":8566},168,[278,8568,8569],{},"    const result = await $fetch(\"\u002Fapi\u002Fupload\", {\n",[278,8571,8573],{"class":280,"line":8572},169,[278,8574,8575],{},"      method: \"PUT\",\n",[278,8577,8579],{"class":280,"line":8578},170,[278,8580,8447],{},[278,8582,8584],{"class":280,"line":8583},171,[278,8585,1233],{},[278,8587,8589],{"class":280,"line":8588},172,[278,8590,292],{"emptyLinePlaceholder":291},[278,8592,8594],{"class":280,"line":8593},173,[278,8595,8596],{},"    return result.map((obj) => obj.pathname);\n",[278,8598,8600],{"class":280,"line":8599},174,[278,8601,8602],{},"  } catch (error) {\n",[278,8604,8606],{"class":280,"line":8605},175,[278,8607,8608],{},"    console.error(\"Failed to upload audio recordings\", error);\n",[278,8610,8612],{"class":280,"line":8611},176,[278,8613,1096],{},[278,8615,8617],{"class":280,"line":8616},177,[278,8618,2817],{},[278,8620,8622],{"class":280,"line":8621},178,[278,8623,292],{"emptyLinePlaceholder":291},[278,8625,8627],{"class":280,"line":8626},179,[278,8628,8629],{},"const isBusy = computed(() => state.value.isRecording || isTranscribing.value);\n",[278,8631,8633],{"class":280,"line":8632},180,[278,8634,292],{"emptyLinePlaceholder":291},[278,8636,8638],{"class":280,"line":8637},181,[278,8639,8640],{},"defineExpose({ uploadRecordings, resetRecordings, isBusy });\n",[278,8642,8644],{"class":280,"line":8643},182,[278,8645,292],{"emptyLinePlaceholder":291},[278,8647,8649],{"class":280,"line":8648},183,[278,8650,8651],{},"const formatDuration = (seconds: number) => {\n",[278,8653,8655],{"class":280,"line":8654},184,[278,8656,8657],{},"  const mins = Math.floor(seconds \u002F 60);\n",[278,8659,8661],{"class":280,"line":8660},185,[278,8662,8663],{},"  const secs = seconds % 60;\n",[278,8665,8667],{"class":280,"line":8666},186,[278,8668,8669],{},"  return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n",[278,8671,8673],{"class":280,"line":8672},187,[278,8674,2817],{},[278,8676,8678],{"class":280,"line":8677},188,[278,8679,7691],{},[11,8681,8682],{},"This component does the following:",[123,8684,8685,8695,8702,8717],{},[74,8686,8687,8688,8690,8691,8694],{},"Allows recording the user’s voice with the help of ",[59,8689,5762],{}," composable created earlier. It also integrates the ",[59,8692,8693],{},"AudioVisualizer"," component to enhance the user experience by providing real-time audio feedback during recordings.",[74,8696,8697,8698,8701],{},"On a new recording, sends the recorded blob for transcription to the ",[59,8699,8700],{},"transcribe"," API endpoint, and emits the transcription text on success",[74,8703,8704,8705,8708,8709,8712,8713,8716],{},"Displays all recordings as ",[59,8706,8707],{},"audio"," elements for users perusal (using ",[59,8710,8711],{},"URL.createObjectURL(blob)","). It utilizes the ",[59,8714,8715],{},"useRecordings"," composable to manage the recordings",[74,8718,8719,8720,8722,8723,8725],{},"Uploads the final recordings to R2 (the local disk in dev mode) using the ",[59,8721,4206],{}," endpoint, and returns the pathnames of these recordings to the caller (the ",[59,8724,7120],{}," component)",[32,8727,8729,7121],{"id":8728},"audiovisualizer-component",[59,8730,8693],{},[11,8732,8733],{},"This component uses an HTML canvas element to represent the audio waveform along a horizontal line. The canvas element is used for its flexibility and efficiency in rendering real-time visualizations, making it suitable for audio waveforms.",[11,8735,8736,8737,8740,8741,8743],{},"The visualization dynamically adjusts based on the amplitude of the captured audio, providing a real-time feedback loop for the user during recording. To do that, it watches the ",[59,8738,8739],{},"updateTrigger"," state variable exposed by ",[59,8742,5762],{}," to redraw the canvas on audio data changes.",[11,8745,5437,8746,263,8749,4217],{},[59,8747,8748],{},"AudioVisualizer.vue",[59,8750,7129],{},[269,8752,8754],{"className":7132,"code":8753,"language":7134,"meta":274,"style":274},"\u003C!-- app\u002Fcomponents\u002FAudioVisualizer.vue -->\n\u003Ctemplate>\n  \u003Ccanvas ref=\"canvas\" width=\"640\" height=\"100\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst props = defineProps\u003C{\n  audioData: Uint8Array | null;\n  dataUpdateTrigger: number;\n}>();\n\nlet width = 0;\nlet height = 0;\nconst audioCanvas = useTemplateRef\u003CHTMLCanvasElement>(\"canvas\");\nconst canvasCtx = ref\u003CCanvasRenderingContext2D | null>(null);\n\nonMounted(() => {\n  if (audioCanvas.value) {\n    canvasCtx.value = audioCanvas.value.getContext(\"2d\");\n    width = audioCanvas.value.width;\n    height = audioCanvas.value.height;\n  }\n});\n\nconst drawCanvas = () => {\n  if (!canvasCtx.value || !props.audioData) {\n    return;\n  }\n\n  const data = props.audioData;\n  const ctx = canvasCtx.value;\n  const sliceWidth = width \u002F data.length;\n\n  ctx.clearRect(0, 0, width, height);\n  ctx.lineWidth = 2;\n  ctx.strokeStyle = \"rgb(221, 72, 49)\";\n  ctx.beginPath();\n\n  let x = 0;\n  for (let i = 0; i \u003C data.length; i++) {\n    const v = (data[i] ?? 0) \u002F 128.0;\n    const y = (v * height) \u002F 2;\n\n    if (i === 0) {\n      ctx.moveTo(x, y);\n    } else {\n      ctx.lineTo(x, y);\n    }\n\n    x += sliceWidth;\n  }\n\n  ctx.lineTo(width, height \u002F 2);\n  ctx.stroke();\n};\n\nwatch(\n  () => props.dataUpdateTrigger,\n  () => {\n    drawCanvas();\n  },\n  { immediate: true },\n);\n\u003C\u002Fscript>\n",[59,8755,8756,8761,8765,8770,8774,8778,8782,8787,8792,8797,8802,8806,8811,8816,8821,8826,8830,8835,8840,8845,8850,8855,8859,8863,8867,8872,8877,8882,8886,8890,8895,8900,8905,8909,8914,8919,8924,8929,8933,8938,8943,8948,8953,8957,8962,8967,8972,8977,8981,8985,8990,8994,8998,9003,9008,9012,9016,9021,9026,9031,9036,9040,9045,9049],{"__ignoreMap":274},[278,8757,8758],{"class":280,"line":281},[278,8759,8760],{},"\u003C!-- app\u002Fcomponents\u002FAudioVisualizer.vue -->\n",[278,8762,8763],{"class":280,"line":288},[278,8764,7146],{},[278,8766,8767],{"class":280,"line":295},[278,8768,8769],{},"  \u003Ccanvas ref=\"canvas\" width=\"640\" height=\"100\" \u002F>\n",[278,8771,8772],{"class":280,"line":316},[278,8773,7422],{},[278,8775,8776],{"class":280,"line":322},[278,8777,292],{"emptyLinePlaceholder":291},[278,8779,8780],{"class":280,"line":327},[278,8781,7431],{},[278,8783,8784],{"class":280,"line":340},[278,8785,8786],{},"const props = defineProps\u003C{\n",[278,8788,8789],{"class":280,"line":349},[278,8790,8791],{},"  audioData: Uint8Array | null;\n",[278,8793,8794],{"class":280,"line":375},[278,8795,8796],{},"  dataUpdateTrigger: number;\n",[278,8798,8799],{"class":280,"line":386},[278,8800,8801],{},"}>();\n",[278,8803,8804],{"class":280,"line":397},[278,8805,292],{"emptyLinePlaceholder":291},[278,8807,8808],{"class":280,"line":408},[278,8809,8810],{},"let width = 0;\n",[278,8812,8813],{"class":280,"line":433},[278,8814,8815],{},"let height = 0;\n",[278,8817,8818],{"class":280,"line":454},[278,8819,8820],{},"const audioCanvas = useTemplateRef\u003CHTMLCanvasElement>(\"canvas\");\n",[278,8822,8823],{"class":280,"line":475},[278,8824,8825],{},"const canvasCtx = ref\u003CCanvasRenderingContext2D | null>(null);\n",[278,8827,8828],{"class":280,"line":496},[278,8829,292],{"emptyLinePlaceholder":291},[278,8831,8832],{"class":280,"line":505},[278,8833,8834],{},"onMounted(() => {\n",[278,8836,8837],{"class":280,"line":516},[278,8838,8839],{},"  if (audioCanvas.value) {\n",[278,8841,8842],{"class":280,"line":527},[278,8843,8844],{},"    canvasCtx.value = audioCanvas.value.getContext(\"2d\");\n",[278,8846,8847],{"class":280,"line":533},[278,8848,8849],{},"    width = audioCanvas.value.width;\n",[278,8851,8852],{"class":280,"line":539},[278,8853,8854],{},"    height = audioCanvas.value.height;\n",[278,8856,8857],{"class":280,"line":545},[278,8858,1096],{},[278,8860,8861],{"class":280,"line":551},[278,8862,3693],{},[278,8864,8865],{"class":280,"line":557},[278,8866,292],{"emptyLinePlaceholder":291},[278,8868,8869],{"class":280,"line":567},[278,8870,8871],{},"const drawCanvas = () => {\n",[278,8873,8874],{"class":280,"line":577},[278,8875,8876],{},"  if (!canvasCtx.value || !props.audioData) {\n",[278,8878,8879],{"class":280,"line":587},[278,8880,8881],{},"    return;\n",[278,8883,8884],{"class":280,"line":597},[278,8885,1096],{},[278,8887,8888],{"class":280,"line":608},[278,8889,292],{"emptyLinePlaceholder":291},[278,8891,8892],{"class":280,"line":614},[278,8893,8894],{},"  const data = props.audioData;\n",[278,8896,8897],{"class":280,"line":620},[278,8898,8899],{},"  const ctx = canvasCtx.value;\n",[278,8901,8902],{"class":280,"line":625},[278,8903,8904],{},"  const sliceWidth = width \u002F data.length;\n",[278,8906,8907],{"class":280,"line":640},[278,8908,292],{"emptyLinePlaceholder":291},[278,8910,8911],{"class":280,"line":663},[278,8912,8913],{},"  ctx.clearRect(0, 0, width, height);\n",[278,8915,8916],{"class":280,"line":669},[278,8917,8918],{},"  ctx.lineWidth = 2;\n",[278,8920,8921],{"class":280,"line":680},[278,8922,8923],{},"  ctx.strokeStyle = \"rgb(221, 72, 49)\";\n",[278,8925,8926],{"class":280,"line":686},[278,8927,8928],{},"  ctx.beginPath();\n",[278,8930,8931],{"class":280,"line":1334},[278,8932,292],{"emptyLinePlaceholder":291},[278,8934,8935],{"class":280,"line":1375},[278,8936,8937],{},"  let x = 0;\n",[278,8939,8940],{"class":280,"line":1381},[278,8941,8942],{},"  for (let i = 0; i \u003C data.length; i++) {\n",[278,8944,8945],{"class":280,"line":1386},[278,8946,8947],{},"    const v = (data[i] ?? 0) \u002F 128.0;\n",[278,8949,8950],{"class":280,"line":1394},[278,8951,8952],{},"    const y = (v * height) \u002F 2;\n",[278,8954,8955],{"class":280,"line":1406},[278,8956,292],{"emptyLinePlaceholder":291},[278,8958,8959],{"class":280,"line":1423},[278,8960,8961],{},"    if (i === 0) {\n",[278,8963,8964],{"class":280,"line":1432},[278,8965,8966],{},"      ctx.moveTo(x, y);\n",[278,8968,8969],{"class":280,"line":1437},[278,8970,8971],{},"    } else {\n",[278,8973,8974],{"class":280,"line":1916},[278,8975,8976],{},"      ctx.lineTo(x, y);\n",[278,8978,8979],{"class":280,"line":1939},[278,8980,1285],{},[278,8982,8983],{"class":280,"line":1949},[278,8984,292],{"emptyLinePlaceholder":291},[278,8986,8987],{"class":280,"line":1954},[278,8988,8989],{},"    x += sliceWidth;\n",[278,8991,8992],{"class":280,"line":1959},[278,8993,1096],{},[278,8995,8996],{"class":280,"line":1985},[278,8997,292],{"emptyLinePlaceholder":291},[278,8999,9000],{"class":280,"line":1990},[278,9001,9002],{},"  ctx.lineTo(width, height \u002F 2);\n",[278,9004,9005],{"class":280,"line":1997},[278,9006,9007],{},"  ctx.stroke();\n",[278,9009,9010],{"class":280,"line":2006},[278,9011,2817],{},[278,9013,9014],{"class":280,"line":2018},[278,9015,292],{"emptyLinePlaceholder":291},[278,9017,9018],{"class":280,"line":2029},[278,9019,9020],{},"watch(\n",[278,9022,9023],{"class":280,"line":2034},[278,9024,9025],{},"  () => props.dataUpdateTrigger,\n",[278,9027,9028],{"class":280,"line":2040},[278,9029,9030],{},"  () => {\n",[278,9032,9033],{"class":280,"line":2045},[278,9034,9035],{},"    drawCanvas();\n",[278,9037,9038],{"class":280,"line":2068},[278,9039,683],{},[278,9041,9042],{"class":280,"line":2099},[278,9043,9044],{},"  { immediate: true },\n",[278,9046,9047],{"class":280,"line":6428},[278,9048,1280],{},[278,9050,9051],{"class":280,"line":6439},[278,9052,7691],{},[32,9054,9056,5763],{"id":9055},"userecordings-composable",[59,9057,8715],{},[11,9059,4796,9060,9062,9063,9065,9066,263,9069,4217],{},[59,9061,7709],{}," component uses the ",[59,9064,8715],{}," composable to manage the list of recordings, and to clear any used resources. Create a new file ",[59,9067,9068],{},"useRecordings.ts",[59,9070,5772],{},[269,9072,9074],{"className":271,"code":9073,"language":273,"meta":274,"style":274},"\u002F\u002F app\u002Fcomposables\u002FuseRecordings.ts\nexport const useRecordings = () => {\n  const recordings = ref\u003CRecording[]>([]);\n\n  const cleanupResource = (recording: Recording) => {\n    if (recording.blob) {\n      URL.revokeObjectURL(recording.url);\n    }\n  };\n\n  const cleanupResources = () => {\n    recordings.value.forEach((recording) => {\n      cleanupResource(recording);\n    });\n  };\n\n  const addRecording = (recording: Recording) => {\n    recordings.value.unshift(recording);\n  };\n\n  const removeRecording = (recording: Recording) => {\n    recordings.value = recordings.value.filter((r) => r.id !== recording.id);\n    cleanupResource(recording);\n  };\n\n  const resetRecordings = () => {\n    cleanupResources();\n\n    recordings.value = [];\n  };\n\n  onUnmounted(cleanupResources);\n\n  return {\n    recordings,\n    addRecording,\n    removeRecording,\n    resetRecordings,\n  };\n};\n",[59,9075,9076,9081,9098,9117,9121,9146,9153,9166,9170,9174,9178,9193,9210,9218,9222,9226,9230,9253,9262,9266,9270,9293,9322,9329,9333,9337,9352,9359,9363,9371,9375,9379,9386,9390,9396,9401,9406,9411,9416,9420],{"__ignoreMap":274},[278,9077,9078],{"class":280,"line":281},[278,9079,9080],{"class":284},"\u002F\u002F app\u002Fcomposables\u002FuseRecordings.ts\n",[278,9082,9083,9085,9087,9090,9092,9094,9096],{"class":280,"line":288},[278,9084,628],{"class":298},[278,9086,4559],{"class":298},[278,9088,9089],{"class":333}," useRecordings",[278,9091,764],{"class":298},[278,9093,5860],{"class":302},[278,9095,1848],{"class":298},[278,9097,876],{"class":302},[278,9099,9100,9102,9105,9107,9109,9111,9114],{"class":280,"line":295},[278,9101,758],{"class":298},[278,9103,9104],{"class":650}," recordings",[278,9106,764],{"class":298},[278,9108,5992],{"class":333},[278,9110,1702],{"class":302},[278,9112,9113],{"class":333},"Recording",[278,9115,9116],{"class":302},"[]>([]);\n",[278,9118,9119],{"class":280,"line":316},[278,9120,292],{"emptyLinePlaceholder":291},[278,9122,9123,9125,9128,9130,9132,9135,9137,9140,9142,9144],{"class":280,"line":322},[278,9124,758],{"class":298},[278,9126,9127],{"class":333}," cleanupResource",[278,9129,764],{"class":298},[278,9131,1245],{"class":302},[278,9133,9134],{"class":501},"recording",[278,9136,960],{"class":298},[278,9138,9139],{"class":333}," Recording",[278,9141,1845],{"class":302},[278,9143,1848],{"class":298},[278,9145,876],{"class":302},[278,9147,9148,9150],{"class":280,"line":327},[278,9149,1242],{"class":298},[278,9151,9152],{"class":302}," (recording.blob) {\n",[278,9154,9155,9158,9160,9163],{"class":280,"line":340},[278,9156,9157],{"class":650},"      URL",[278,9159,183],{"class":302},[278,9161,9162],{"class":333},"revokeObjectURL",[278,9164,9165],{"class":302},"(recording.url);\n",[278,9167,9168],{"class":280,"line":349},[278,9169,1285],{"class":302},[278,9171,9172],{"class":280,"line":375},[278,9173,901],{"class":302},[278,9175,9176],{"class":280,"line":386},[278,9177,292],{"emptyLinePlaceholder":291},[278,9179,9180,9182,9185,9187,9189,9191],{"class":280,"line":397},[278,9181,758],{"class":298},[278,9183,9184],{"class":333}," cleanupResources",[278,9186,764],{"class":298},[278,9188,5860],{"class":302},[278,9190,1848],{"class":298},[278,9192,876],{"class":302},[278,9194,9195,9198,9200,9202,9204,9206,9208],{"class":280,"line":408},[278,9196,9197],{"class":302},"    recordings.value.",[278,9199,6898],{"class":333},[278,9201,2079],{"class":302},[278,9203,9134],{"class":501},[278,9205,1845],{"class":302},[278,9207,1848],{"class":298},[278,9209,876],{"class":302},[278,9211,9212,9215],{"class":280,"line":433},[278,9213,9214],{"class":333},"      cleanupResource",[278,9216,9217],{"class":302},"(recording);\n",[278,9219,9220],{"class":280,"line":454},[278,9221,1233],{"class":302},[278,9223,9224],{"class":280,"line":475},[278,9225,901],{"class":302},[278,9227,9228],{"class":280,"line":496},[278,9229,292],{"emptyLinePlaceholder":291},[278,9231,9232,9234,9237,9239,9241,9243,9245,9247,9249,9251],{"class":280,"line":505},[278,9233,758],{"class":298},[278,9235,9236],{"class":333}," addRecording",[278,9238,764],{"class":298},[278,9240,1245],{"class":302},[278,9242,9134],{"class":501},[278,9244,960],{"class":298},[278,9246,9139],{"class":333},[278,9248,1845],{"class":302},[278,9250,1848],{"class":298},[278,9252,876],{"class":302},[278,9254,9255,9257,9260],{"class":280,"line":516},[278,9256,9197],{"class":302},[278,9258,9259],{"class":333},"unshift",[278,9261,9217],{"class":302},[278,9263,9264],{"class":280,"line":527},[278,9265,901],{"class":302},[278,9267,9268],{"class":280,"line":533},[278,9269,292],{"emptyLinePlaceholder":291},[278,9271,9272,9274,9277,9279,9281,9283,9285,9287,9289,9291],{"class":280,"line":539},[278,9273,758],{"class":298},[278,9275,9276],{"class":333}," removeRecording",[278,9278,764],{"class":298},[278,9280,1245],{"class":302},[278,9282,9134],{"class":501},[278,9284,960],{"class":298},[278,9286,9139],{"class":333},[278,9288,1845],{"class":302},[278,9290,1848],{"class":298},[278,9292,876],{"class":302},[278,9294,9295,9298,9300,9303,9305,9307,9310,9312,9314,9317,9319],{"class":280,"line":545},[278,9296,9297],{"class":302},"    recordings.value ",[278,9299,358],{"class":298},[278,9301,9302],{"class":302}," recordings.value.",[278,9304,2076],{"class":333},[278,9306,2079],{"class":302},[278,9308,9309],{"class":501},"r",[278,9311,1845],{"class":302},[278,9313,1848],{"class":298},[278,9315,9316],{"class":302}," r.id ",[278,9318,2092],{"class":298},[278,9320,9321],{"class":302}," recording.id);\n",[278,9323,9324,9327],{"class":280,"line":551},[278,9325,9326],{"class":333},"    cleanupResource",[278,9328,9217],{"class":302},[278,9330,9331],{"class":280,"line":557},[278,9332,901],{"class":302},[278,9334,9335],{"class":280,"line":567},[278,9336,292],{"emptyLinePlaceholder":291},[278,9338,9339,9341,9344,9346,9348,9350],{"class":280,"line":577},[278,9340,758],{"class":298},[278,9342,9343],{"class":333}," resetRecordings",[278,9345,764],{"class":298},[278,9347,5860],{"class":302},[278,9349,1848],{"class":298},[278,9351,876],{"class":302},[278,9353,9354,9357],{"class":280,"line":587},[278,9355,9356],{"class":333},"    cleanupResources",[278,9358,1313],{"class":302},[278,9360,9361],{"class":280,"line":597},[278,9362,292],{"emptyLinePlaceholder":291},[278,9364,9365,9367,9369],{"class":280,"line":608},[278,9366,9297],{"class":302},[278,9368,358],{"class":298},[278,9370,6483],{"class":302},[278,9372,9373],{"class":280,"line":614},[278,9374,901],{"class":302},[278,9376,9377],{"class":280,"line":620},[278,9378,292],{"emptyLinePlaceholder":291},[278,9380,9381,9383],{"class":280,"line":625},[278,9382,7008],{"class":333},[278,9384,9385],{"class":302},"(cleanupResources);\n",[278,9387,9388],{"class":280,"line":640},[278,9389,292],{"emptyLinePlaceholder":291},[278,9391,9392,9394],{"class":280,"line":663},[278,9393,343],{"class":298},[278,9395,876],{"class":302},[278,9397,9398],{"class":280,"line":669},[278,9399,9400],{"class":302},"    recordings,\n",[278,9402,9403],{"class":280,"line":680},[278,9404,9405],{"class":302},"    addRecording,\n",[278,9407,9408],{"class":280,"line":686},[278,9409,9410],{"class":302},"    removeRecording,\n",[278,9412,9413],{"class":280,"line":1334},[278,9414,9415],{"class":302},"    resetRecordings,\n",[278,9417,9418],{"class":280,"line":1375},[278,9419,901],{"class":302},[278,9421,9422],{"class":280,"line":1381},[278,9423,2817],{"class":302},[11,9425,9426,9427,9429,9430,9433,9434,9437],{},"You can define the ",[59,9428,9113],{}," type definition in the ",[59,9431,9432],{},"shared\u002Ftypes\u002Findex.ts"," file. This allows for auto import of type definitions in both client & server sides (The intended purpose of the shared folder is for sharing common types & utils between the app & server). Also, while you’re at it, you can also define the ",[59,9435,9436],{},"Note"," type.",[269,9439,9441],{"className":271,"code":9440,"language":273,"meta":274,"style":274},"\u002F\u002F shared\u002Ftypes\u002Findex.ts\nimport type { z } from \"zod\";\nimport type { noteSelectSchema } from \"#shared\u002Fschemas\u002Fnote.schema\";\n\nexport type Recording = {\n  url: string;\n  blob?: Blob;\n  id: string;\n};\n\nexport type Note = z.output\u003Ctypeof noteSelectSchema>;\n",[59,9442,9443,9448,9463,9478,9482,9494,9505,9517,9528,9532,9536],{"__ignoreMap":274},[278,9444,9445],{"class":280,"line":281},[278,9446,9447],{"class":284},"\u002F\u002F shared\u002Ftypes\u002Findex.ts\n",[278,9449,9450,9452,9455,9457,9459,9461],{"class":280,"line":288},[278,9451,299],{"class":298},[278,9453,9454],{"class":298}," type",[278,9456,5475],{"class":302},[278,9458,306],{"class":298},[278,9460,5480],{"class":309},[278,9462,313],{"class":302},[278,9464,9465,9467,9469,9472,9474,9476],{"class":280,"line":295},[278,9466,299],{"class":298},[278,9468,9454],{"class":298},[278,9470,9471],{"class":302}," { noteSelectSchema } ",[278,9473,306],{"class":298},[278,9475,4985],{"class":309},[278,9477,313],{"class":302},[278,9479,9480],{"class":280,"line":316},[278,9481,292],{"emptyLinePlaceholder":291},[278,9483,9484,9486,9488,9490,9492],{"class":280,"line":322},[278,9485,628],{"class":298},[278,9487,9454],{"class":298},[278,9489,9139],{"class":333},[278,9491,764],{"class":298},[278,9493,876],{"class":302},[278,9495,9496,9499,9501,9503],{"class":280,"line":327},[278,9497,9498],{"class":501},"  url",[278,9500,960],{"class":298},[278,9502,963],{"class":650},[278,9504,313],{"class":302},[278,9506,9507,9510,9513,9515],{"class":280,"line":340},[278,9508,9509],{"class":501},"  blob",[278,9511,9512],{"class":298},"?:",[278,9514,3937],{"class":333},[278,9516,313],{"class":302},[278,9518,9519,9522,9524,9526],{"class":280,"line":349},[278,9520,9521],{"class":501},"  id",[278,9523,960],{"class":298},[278,9525,963],{"class":650},[278,9527,313],{"class":302},[278,9529,9530],{"class":280,"line":375},[278,9531,2817],{"class":302},[278,9533,9534],{"class":280,"line":386},[278,9535,292],{"emptyLinePlaceholder":291},[278,9537,9538,9540,9542,9545,9547,9550,9552,9555,9557,9560],{"class":280,"line":397},[278,9539,628],{"class":298},[278,9541,9454],{"class":298},[278,9543,9544],{"class":333}," Note",[278,9546,764],{"class":298},[278,9548,9549],{"class":333}," z",[278,9551,183],{"class":302},[278,9553,9554],{"class":333},"output",[278,9556,1702],{"class":302},[278,9558,9559],{"class":298},"typeof",[278,9561,9562],{"class":302}," noteSelectSchema>;\n",[32,9564,9566],{"id":9565},"creating-the-home-page","Creating the Home Page",[11,9568,9569,9570,9573],{},"Now that we have all the pieces ready for the basic app, it is time to put everything together in a page. Delete the content of the home page (",[59,9571,9572],{},"app\u002Fpages\u002Findex.vue","), and put the following content to it:",[269,9575,9577],{"className":7132,"code":9576,"language":7134,"meta":274,"style":274},"\u003C!-- app\u002Fpages\u002Findex.vue -->\n\u003Ctemplate>\n  \u003CUContainer class=\"h-screen flex justify-center items-center\">\n    \u003CUCard\n      class=\"w-full max-h-full overflow-hidden max-w-4xl mx-auto\"\n      :ui=\"{ body: 'h-[calc(100vh-4rem)] overflow-y-auto' }\"\n    >\n      \u003Ctemplate #header>\n        \u003Cspan class=\"font-bold text-xl md:text-2xl\">Voice Notes\u003C\u002Fspan>\n        \u003CUButton icon=\"i-lucide-plus\" @click=\"showNoteModal\">\n          New Note\n        \u003C\u002FUButton>\n      \u003C\u002Ftemplate>\n\n      \u003Cdiv v-if=\"notes?.length\" class=\"space-y-4\">\n        \u003CNoteCard v-for=\"note in notes\" :key=\"note.id\" :note=\"note\" \u002F>\n      \u003C\u002Fdiv>\n      \u003Cdiv\n        v-else\n        class=\"my-12 text-center text-gray-500 dark:text-gray-400 space-y-2\"\n      >\n        \u003Ch2 class=\"text-2xl md:text-3xl\">No notes created\u003C\u002Fh2>\n        \u003Cp>Get started by creating your first note\u003C\u002Fp>\n      \u003C\u002Fdiv>\n    \u003C\u002FUCard>\n  \u003C\u002FUContainer>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { LazyNoteEditorModal } from \"#components\";\n\nconst { data: notes, refresh } = await useFetch(\"\u002Fapi\u002Fnotes\");\n\nconst modal = useModal();\nconst showNoteModal = () => {\n  modal.open(LazyNoteEditorModal, {\n    onNewNote: refresh,\n  });\n};\n\nwatch(modal.isOpen, (newState) => {\n  if (!newState) {\n    modal.reset();\n  }\n});\n\u003C\u002Fscript>\n",[59,9578,9579,9584,9588,9593,9598,9603,9608,9612,9617,9622,9627,9632,9637,9642,9646,9651,9656,9660,9664,9669,9674,9678,9683,9688,9692,9697,9702,9706,9710,9714,9719,9723,9728,9732,9736,9741,9746,9751,9755,9759,9763,9768,9773,9778,9782,9786],{"__ignoreMap":274},[278,9580,9581],{"class":280,"line":281},[278,9582,9583],{},"\u003C!-- app\u002Fpages\u002Findex.vue -->\n",[278,9585,9586],{"class":280,"line":288},[278,9587,7146],{},[278,9589,9590],{"class":280,"line":295},[278,9591,9592],{},"  \u003CUContainer class=\"h-screen flex justify-center items-center\">\n",[278,9594,9595],{"class":280,"line":316},[278,9596,9597],{},"    \u003CUCard\n",[278,9599,9600],{"class":280,"line":322},[278,9601,9602],{},"      class=\"w-full max-h-full overflow-hidden max-w-4xl mx-auto\"\n",[278,9604,9605],{"class":280,"line":327},[278,9606,9607],{},"      :ui=\"{ body: 'h-[calc(100vh-4rem)] overflow-y-auto' }\"\n",[278,9609,9610],{"class":280,"line":340},[278,9611,7935],{},[278,9613,9614],{"class":280,"line":349},[278,9615,9616],{},"      \u003Ctemplate #header>\n",[278,9618,9619],{"class":280,"line":375},[278,9620,9621],{},"        \u003Cspan class=\"font-bold text-xl md:text-2xl\">Voice Notes\u003C\u002Fspan>\n",[278,9623,9624],{"class":280,"line":386},[278,9625,9626],{},"        \u003CUButton icon=\"i-lucide-plus\" @click=\"showNoteModal\">\n",[278,9628,9629],{"class":280,"line":397},[278,9630,9631],{},"          New Note\n",[278,9633,9634],{"class":280,"line":408},[278,9635,9636],{},"        \u003C\u002FUButton>\n",[278,9638,9639],{"class":280,"line":433},[278,9640,9641],{},"      \u003C\u002Ftemplate>\n",[278,9643,9644],{"class":280,"line":454},[278,9645,292],{"emptyLinePlaceholder":291},[278,9647,9648],{"class":280,"line":475},[278,9649,9650],{},"      \u003Cdiv v-if=\"notes?.length\" class=\"space-y-4\">\n",[278,9652,9653],{"class":280,"line":496},[278,9654,9655],{},"        \u003CNoteCard v-for=\"note in notes\" :key=\"note.id\" :note=\"note\" \u002F>\n",[278,9657,9658],{"class":280,"line":505},[278,9659,7873],{},[278,9661,9662],{"class":280,"line":516},[278,9663,7964],{},[278,9665,9666],{"class":280,"line":527},[278,9667,9668],{},"        v-else\n",[278,9670,9671],{"class":280,"line":533},[278,9672,9673],{},"        class=\"my-12 text-center text-gray-500 dark:text-gray-400 space-y-2\"\n",[278,9675,9676],{"class":280,"line":539},[278,9677,7357],{},[278,9679,9680],{"class":280,"line":545},[278,9681,9682],{},"        \u003Ch2 class=\"text-2xl md:text-3xl\">No notes created\u003C\u002Fh2>\n",[278,9684,9685],{"class":280,"line":551},[278,9686,9687],{},"        \u003Cp>Get started by creating your first note\u003C\u002Fp>\n",[278,9689,9690],{"class":280,"line":557},[278,9691,7873],{},[278,9693,9694],{"class":280,"line":567},[278,9695,9696],{},"    \u003C\u002FUCard>\n",[278,9698,9699],{"class":280,"line":577},[278,9700,9701],{},"  \u003C\u002FUContainer>\n",[278,9703,9704],{"class":280,"line":587},[278,9705,7422],{},[278,9707,9708],{"class":280,"line":597},[278,9709,292],{"emptyLinePlaceholder":291},[278,9711,9712],{"class":280,"line":608},[278,9713,7431],{},[278,9715,9716],{"class":280,"line":614},[278,9717,9718],{},"import { LazyNoteEditorModal } from \"#components\";\n",[278,9720,9721],{"class":280,"line":620},[278,9722,292],{"emptyLinePlaceholder":291},[278,9724,9725],{"class":280,"line":625},[278,9726,9727],{},"const { data: notes, refresh } = await useFetch(\"\u002Fapi\u002Fnotes\");\n",[278,9729,9730],{"class":280,"line":640},[278,9731,292],{"emptyLinePlaceholder":291},[278,9733,9734],{"class":280,"line":663},[278,9735,7515],{},[278,9737,9738],{"class":280,"line":669},[278,9739,9740],{},"const showNoteModal = () => {\n",[278,9742,9743],{"class":280,"line":680},[278,9744,9745],{},"  modal.open(LazyNoteEditorModal, {\n",[278,9747,9748],{"class":280,"line":686},[278,9749,9750],{},"    onNewNote: refresh,\n",[278,9752,9753],{"class":280,"line":1334},[278,9754,2037],{},[278,9756,9757],{"class":280,"line":1375},[278,9758,2817],{},[278,9760,9761],{"class":280,"line":1381},[278,9762,292],{"emptyLinePlaceholder":291},[278,9764,9765],{"class":280,"line":1386},[278,9766,9767],{},"watch(modal.isOpen, (newState) => {\n",[278,9769,9770],{"class":280,"line":1394},[278,9771,9772],{},"  if (!newState) {\n",[278,9774,9775],{"class":280,"line":1406},[278,9776,9777],{},"    modal.reset();\n",[278,9779,9780],{"class":280,"line":1423},[278,9781,1096],{},[278,9783,9784],{"class":280,"line":1432},[278,9785,3693],{},[278,9787,9788],{"class":280,"line":1437},[278,9789,7691],{},[11,9791,9792],{},"On this page we’re doing the following:",[123,9794,9795,9802,9812],{},[74,9796,9797,9798,9801],{},"Fetch the list of existing notes from the database and display them using the ",[59,9799,9800],{},"NoteCard"," component",[74,9803,9804,9805,9807,9808,9811],{},"Shows a new note button which when clicked opens the ",[59,9806,7120],{},". On successful note creation the ",[59,9809,9810],{},"refresh"," function is called to refetch the notes",[74,9813,9814],{},"The modal state is reset on closure to ensure a clean slate for the next note creation",[11,9816,9817],{},"The cards and modals headers\u002Ffooters used in the app follow a global style that is defined in the app config file. Centralizing styles in the app configuration ensures consistent theming and reduces redundancy across components.",[11,9819,5437,9820,3855,9823,4496],{},[59,9821,9822],{},"app.config.ts",[59,9824,9825],{},"app",[269,9827,9829],{"className":271,"code":9828,"language":273,"meta":274,"style":274},"\u002F\u002F app\u002Fapp.config.ts\nexport default defineAppConfig({\n  ui: {\n    card: {\n      slots: {\n        header: \"flex items-center justify-between gap-3 flex-wrap\",\n      },\n    },\n    modal: {\n      slots: {\n        footer: \"justify-end gap-x-3\",\n      },\n    },\n  },\n});\n",[59,9830,9831,9836,9847,9852,9857,9862,9872,9876,9880,9885,9889,9899,9903,9907,9911],{"__ignoreMap":274},[278,9832,9833],{"class":280,"line":281},[278,9834,9835],{"class":284},"\u002F\u002F app\u002Fapp.config.ts\n",[278,9837,9838,9840,9842,9845],{"class":280,"line":288},[278,9839,628],{"class":298},[278,9841,631],{"class":298},[278,9843,9844],{"class":333}," defineAppConfig",[278,9846,637],{"class":302},[278,9848,9849],{"class":280,"line":295},[278,9850,9851],{"class":302},"  ui: {\n",[278,9853,9854],{"class":280,"line":316},[278,9855,9856],{"class":302},"    card: {\n",[278,9858,9859],{"class":280,"line":322},[278,9860,9861],{"class":302},"      slots: {\n",[278,9863,9864,9867,9870],{"class":280,"line":327},[278,9865,9866],{"class":302},"        header: ",[278,9868,9869],{"class":309},"\"flex items-center justify-between gap-3 flex-wrap\"",[278,9871,660],{"class":302},[278,9873,9874],{"class":280,"line":340},[278,9875,1165],{"class":302},[278,9877,9878],{"class":280,"line":349},[278,9879,2243],{"class":302},[278,9881,9882],{"class":280,"line":375},[278,9883,9884],{"class":302},"    modal: {\n",[278,9886,9887],{"class":280,"line":386},[278,9888,9861],{"class":302},[278,9890,9891,9894,9897],{"class":280,"line":397},[278,9892,9893],{"class":302},"        footer: ",[278,9895,9896],{"class":309},"\"justify-end gap-x-3\"",[278,9898,660],{"class":302},[278,9900,9901],{"class":280,"line":408},[278,9902,1165],{"class":302},[278,9904,9905],{"class":280,"line":433},[278,9906,2243],{"class":302},[278,9908,9909],{"class":280,"line":454},[278,9910,683],{"class":302},[278,9912,9913],{"class":280,"line":475},[278,9914,3693],{"class":302},[11,9916,9917,9918,9921,9922,9925],{},"You’ll also need to wrap your ",[59,9919,9920],{},"NuxtPage"," component with the ",[59,9923,9924],{},"UApp"," component for the modals and toast notifications to work as shown below:",[269,9927,9929],{"className":7132,"code":9928,"language":7134,"meta":274,"style":274},"\u003C!-- app\u002Fapp.vue -->\n\u003Ctemplate>\n  \u003CNuxtRouteAnnouncer \u002F>\n  \u003CNuxtLoadingIndicator \u002F>\n  \u003CUApp>\n    \u003CNuxtPage \u002F>\n  \u003C\u002FUApp>\n\u003C\u002Ftemplate>\n",[59,9930,9931,9936,9940,9945,9950,9955,9960,9965],{"__ignoreMap":274},[278,9932,9933],{"class":280,"line":281},[278,9934,9935],{},"\u003C!-- app\u002Fapp.vue -->\n",[278,9937,9938],{"class":280,"line":288},[278,9939,7146],{},[278,9941,9942],{"class":280,"line":295},[278,9943,9944],{},"  \u003CNuxtRouteAnnouncer \u002F>\n",[278,9946,9947],{"class":280,"line":316},[278,9948,9949],{},"  \u003CNuxtLoadingIndicator \u002F>\n",[278,9951,9952],{"class":280,"line":322},[278,9953,9954],{},"  \u003CUApp>\n",[278,9956,9957],{"class":280,"line":327},[278,9958,9959],{},"    \u003CNuxtPage \u002F>\n",[278,9961,9962],{"class":280,"line":340},[278,9963,9964],{},"  \u003C\u002FUApp>\n",[278,9966,9967],{"class":280,"line":349},[278,9968,7422],{},[32,9970,9972,9801],{"id":9971},"notecard-component",[59,9973,9800],{},[11,9975,9976],{},"This component displays the note text and the attached audio recordings of a note. The note text is clamped to 3 lines with a show more\u002Fless button to show\u002Fhide rest of the text. Text clamping ensures that the UI remains clean and uncluttered, while the show more\u002Fless button gives users full control over note visibility.",[11,9978,5437,9979,263,9982,9984],{},[59,9980,9981],{},"NoteCard.vue",[59,9983,7129],{}," folder, and add the following code to it:",[269,9986,9988],{"className":7132,"code":9987,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003CUCard class=\"hover:shadow-lg transition-shadow\">\n    \u003Cdiv class=\"flex-1\">\n      \u003Cp\n        ref=\"text\"\n        :class=\"['whitespace-pre-wrap', !showFullText && 'line-clamp-3']\"\n      >\n        {{ note.text }}\n      \u003C\u002Fp>\n      \u003CUButton\n        v-if=\"shouldShowExpandBtn\"\n        variant=\"link\"\n        :padded=\"false\"\n        @click=\"showFullText = !showFullText\"\n      >\n        {{ showFullText ? \"Show less\" : \"Show more\" }}\n      \u003C\u002FUButton>\n    \u003C\u002Fdiv>\n\n    \u003Cdiv\n      v-if=\"note.audioUrls && note.audioUrls.length > 0\"\n      class=\"mt-4 flex gap-x-2 overflow-x-auto\"\n    >\n      \u003Caudio\n        v-for=\"url in note.audioUrls\"\n        :key=\"url\"\n        :src=\"url\"\n        controls\n        class=\"w-60 shrink-0 h-10\"\n      \u002F>\n    \u003C\u002Fdiv>\n\n    \u003Cp\n      class=\"flex items-center text-sm text-gray-500 dark:text-gray-400 gap-x-2 mt-6\"\n    >\n      \u003CUIcon name=\"i-lucide-clock\" size=\"size-4\" \u002F>\n      \u003Cspan>\n        {{\n          note.updatedAt && note.updatedAt !== note.createdAt\n            ? `Updated ${updated}`\n            : `Created ${created}`\n        }}\n      \u003C\u002Fspan>\n    \u003C\u002Fp>\n  \u003C\u002FUCard>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { useTimeAgo } from \"@vueuse\u002Fcore\";\n\nconst props = defineProps\u003C{ note: Note }>();\n\nconst createdAt = computed(() => props.note.createdAt + \"Z\");\nconst updatedAt = computed(() => props.note.updatedAt + \"Z\");\n\nconst created = useTimeAgo(createdAt);\nconst updated = useTimeAgo(updatedAt);\n\nconst showFullText = ref(false);\n\nconst shouldShowExpandBtn = ref(false);\nconst noteText = useTemplateRef\u003CHTMLParagraphElement>(\"text\");\nconst checkTextExpansion = () => {\n  nextTick(() => {\n    if (noteText.value) {\n      shouldShowExpandBtn.value =\n        noteText.value.scrollHeight > noteText.value.clientHeight;\n    }\n  });\n};\n\nonMounted(checkTextExpansion);\n\nwatch(() => props.note.text, checkTextExpansion);\n\u003C\u002Fscript>\n",[59,9989,9990,9994,9999,10004,10009,10014,10019,10023,10028,10033,10037,10042,10047,10052,10057,10061,10066,10070,10074,10078,10082,10087,10092,10096,10101,10106,10111,10116,10121,10126,10130,10134,10138,10143,10148,10152,10157,10162,10167,10172,10177,10182,10187,10192,10197,10201,10205,10209,10213,10218,10222,10227,10231,10236,10241,10245,10250,10255,10259,10264,10268,10273,10278,10283,10288,10293,10298,10303,10307,10311,10315,10319,10324,10328,10333],{"__ignoreMap":274},[278,9991,9992],{"class":280,"line":281},[278,9993,7146],{},[278,9995,9996],{"class":280,"line":288},[278,9997,9998],{},"  \u003CUCard class=\"hover:shadow-lg transition-shadow\">\n",[278,10000,10001],{"class":280,"line":295},[278,10002,10003],{},"    \u003Cdiv class=\"flex-1\">\n",[278,10005,10006],{"class":280,"line":316},[278,10007,10008],{},"      \u003Cp\n",[278,10010,10011],{"class":280,"line":322},[278,10012,10013],{},"        ref=\"text\"\n",[278,10015,10016],{"class":280,"line":327},[278,10017,10018],{},"        :class=\"['whitespace-pre-wrap', !showFullText && 'line-clamp-3']\"\n",[278,10020,10021],{"class":280,"line":340},[278,10022,7357],{},[278,10024,10025],{"class":280,"line":349},[278,10026,10027],{},"        {{ note.text }}\n",[278,10029,10030],{"class":280,"line":375},[278,10031,10032],{},"      \u003C\u002Fp>\n",[278,10034,10035],{"class":280,"line":386},[278,10036,7327],{},[278,10038,10039],{"class":280,"line":397},[278,10040,10041],{},"        v-if=\"shouldShowExpandBtn\"\n",[278,10043,10044],{"class":280,"line":408},[278,10045,10046],{},"        variant=\"link\"\n",[278,10048,10049],{"class":280,"line":433},[278,10050,10051],{},"        :padded=\"false\"\n",[278,10053,10054],{"class":280,"line":454},[278,10055,10056],{},"        @click=\"showFullText = !showFullText\"\n",[278,10058,10059],{"class":280,"line":475},[278,10060,7357],{},[278,10062,10063],{"class":280,"line":496},[278,10064,10065],{},"        {{ showFullText ? \"Show less\" : \"Show more\" }}\n",[278,10067,10068],{"class":280,"line":505},[278,10069,7367],{},[278,10071,10072],{"class":280,"line":516},[278,10073,7950],{},[278,10075,10076],{"class":280,"line":527},[278,10077,292],{"emptyLinePlaceholder":291},[278,10079,10080],{"class":280,"line":533},[278,10081,7920],{},[278,10083,10084],{"class":280,"line":539},[278,10085,10086],{},"      v-if=\"note.audioUrls && note.audioUrls.length > 0\"\n",[278,10088,10089],{"class":280,"line":545},[278,10090,10091],{},"      class=\"mt-4 flex gap-x-2 overflow-x-auto\"\n",[278,10093,10094],{"class":280,"line":551},[278,10095,7935],{},[278,10097,10098],{"class":280,"line":557},[278,10099,10100],{},"      \u003Caudio\n",[278,10102,10103],{"class":280,"line":567},[278,10104,10105],{},"        v-for=\"url in note.audioUrls\"\n",[278,10107,10108],{"class":280,"line":577},[278,10109,10110],{},"        :key=\"url\"\n",[278,10112,10113],{"class":280,"line":587},[278,10114,10115],{},"        :src=\"url\"\n",[278,10117,10118],{"class":280,"line":597},[278,10119,10120],{},"        controls\n",[278,10122,10123],{"class":280,"line":608},[278,10124,10125],{},"        class=\"w-60 shrink-0 h-10\"\n",[278,10127,10128],{"class":280,"line":614},[278,10129,7308],{},[278,10131,10132],{"class":280,"line":620},[278,10133,7950],{},[278,10135,10136],{"class":280,"line":625},[278,10137,292],{"emptyLinePlaceholder":291},[278,10139,10140],{"class":280,"line":640},[278,10141,10142],{},"    \u003Cp\n",[278,10144,10145],{"class":280,"line":663},[278,10146,10147],{},"      class=\"flex items-center text-sm text-gray-500 dark:text-gray-400 gap-x-2 mt-6\"\n",[278,10149,10150],{"class":280,"line":669},[278,10151,7935],{},[278,10153,10154],{"class":280,"line":680},[278,10155,10156],{},"      \u003CUIcon name=\"i-lucide-clock\" size=\"size-4\" \u002F>\n",[278,10158,10159],{"class":280,"line":686},[278,10160,10161],{},"      \u003Cspan>\n",[278,10163,10164],{"class":280,"line":1334},[278,10165,10166],{},"        {{\n",[278,10168,10169],{"class":280,"line":1375},[278,10170,10171],{},"          note.updatedAt && note.updatedAt !== note.createdAt\n",[278,10173,10174],{"class":280,"line":1381},[278,10175,10176],{},"            ? `Updated ${updated}`\n",[278,10178,10179],{"class":280,"line":1386},[278,10180,10181],{},"            : `Created ${created}`\n",[278,10183,10184],{"class":280,"line":1394},[278,10185,10186],{},"        }}\n",[278,10188,10189],{"class":280,"line":1406},[278,10190,10191],{},"      \u003C\u002Fspan>\n",[278,10193,10194],{"class":280,"line":1423},[278,10195,10196],{},"    \u003C\u002Fp>\n",[278,10198,10199],{"class":280,"line":1432},[278,10200,8074],{},[278,10202,10203],{"class":280,"line":1437},[278,10204,7422],{},[278,10206,10207],{"class":280,"line":1916},[278,10208,292],{"emptyLinePlaceholder":291},[278,10210,10211],{"class":280,"line":1939},[278,10212,7431],{},[278,10214,10215],{"class":280,"line":1949},[278,10216,10217],{},"import { useTimeAgo } from \"@vueuse\u002Fcore\";\n",[278,10219,10220],{"class":280,"line":1954},[278,10221,292],{"emptyLinePlaceholder":291},[278,10223,10224],{"class":280,"line":1959},[278,10225,10226],{},"const props = defineProps\u003C{ note: Note }>();\n",[278,10228,10229],{"class":280,"line":1985},[278,10230,292],{"emptyLinePlaceholder":291},[278,10232,10233],{"class":280,"line":1990},[278,10234,10235],{},"const createdAt = computed(() => props.note.createdAt + \"Z\");\n",[278,10237,10238],{"class":280,"line":1997},[278,10239,10240],{},"const updatedAt = computed(() => props.note.updatedAt + \"Z\");\n",[278,10242,10243],{"class":280,"line":2006},[278,10244,292],{"emptyLinePlaceholder":291},[278,10246,10247],{"class":280,"line":2018},[278,10248,10249],{},"const created = useTimeAgo(createdAt);\n",[278,10251,10252],{"class":280,"line":2029},[278,10253,10254],{},"const updated = useTimeAgo(updatedAt);\n",[278,10256,10257],{"class":280,"line":2034},[278,10258,292],{"emptyLinePlaceholder":291},[278,10260,10261],{"class":280,"line":2040},[278,10262,10263],{},"const showFullText = ref(false);\n",[278,10265,10266],{"class":280,"line":2045},[278,10267,292],{"emptyLinePlaceholder":291},[278,10269,10270],{"class":280,"line":2068},[278,10271,10272],{},"const shouldShowExpandBtn = ref(false);\n",[278,10274,10275],{"class":280,"line":2099},[278,10276,10277],{},"const noteText = useTemplateRef\u003CHTMLParagraphElement>(\"text\");\n",[278,10279,10280],{"class":280,"line":6428},[278,10281,10282],{},"const checkTextExpansion = () => {\n",[278,10284,10285],{"class":280,"line":6439},[278,10286,10287],{},"  nextTick(() => {\n",[278,10289,10290],{"class":280,"line":6450},[278,10291,10292],{},"    if (noteText.value) {\n",[278,10294,10295],{"class":280,"line":6455},[278,10296,10297],{},"      shouldShowExpandBtn.value =\n",[278,10299,10300],{"class":280,"line":6460},[278,10301,10302],{},"        noteText.value.scrollHeight > noteText.value.clientHeight;\n",[278,10304,10305],{"class":280,"line":6475},[278,10306,1285],{},[278,10308,10309],{"class":280,"line":6486},[278,10310,2037],{},[278,10312,10313],{"class":280,"line":6491},[278,10314,2817],{},[278,10316,10317],{"class":280,"line":6518},[278,10318,292],{"emptyLinePlaceholder":291},[278,10320,10321],{"class":280,"line":6530},[278,10322,10323],{},"onMounted(checkTextExpansion);\n",[278,10325,10326],{"class":280,"line":6542},[278,10327,292],{"emptyLinePlaceholder":291},[278,10329,10330],{"class":280,"line":6547},[278,10331,10332],{},"watch(() => props.note.text, checkTextExpansion);\n",[278,10334,10335],{"class":280,"line":6552},[278,10336,7691],{},[11,10338,10339],{},"And we are done here. Try running the application and create some notes. You should be able to create notes, add multiple recordings to the same note etc. Everything should be working now, or is it?",[11,10341,10342],{},"Try playing the audio recordings of the saved notes, are these playable?",[11,10344,10345],{},[3135,10346],{"alt":10347,"src":10348},"Houston, we have a problem","https:\u002F\u002Fi.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExbTFvbTQ4Z3h1aXlyNHhvOW9ibW9oM2M5OWE2Y3MyczRxazRqN3E2YSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002F3oEjHWzZQaCrZW2aWs\u002Fgiphy.gif",[32,10350,10352],{"id":10351},"serving-the-audio-recordings","Serving the Audio Recordings",[11,10354,10355],{},"We can’t play the audio recordings because these are saved in R2 (local disk in dev mode), and nowhere we are serving these files. It is time to fix that.",[11,10357,10358,10359,10361,10362,10364],{},"If you look at the ",[59,10360,4939],{}," code, we save the audio urls\u002Fpathnames with an ",[59,10363,8707],{}," prefix",[269,10366,10368],{"className":271,"code":10367,"language":273,"meta":274,"style":274},"await useDrizzle()\n  .insert(tables.notes)\n  .values({\n    text,\n    audioUrls: audioUrls ? audioUrls.map((url) => `\u002Faudio\u002F${url}`) : null,\n  });\n",[59,10369,10370,10378,10387,10395,10400,10433],{"__ignoreMap":274},[278,10371,10372,10374,10376],{"class":280,"line":281},[278,10373,4062],{"class":298},[278,10375,4899],{"class":333},[278,10377,4601],{"class":302},[278,10379,10380,10383,10385],{"class":280,"line":288},[278,10381,10382],{"class":302},"  .",[278,10384,5089],{"class":333},[278,10386,5092],{"class":302},[278,10388,10389,10391,10393],{"class":280,"line":295},[278,10390,10382],{"class":302},[278,10392,5099],{"class":333},[278,10394,637],{"class":302},[278,10396,10397],{"class":280,"line":316},[278,10398,10399],{"class":302},"    text,\n",[278,10401,10402,10405,10407,10409,10411,10413,10415,10417,10419,10421,10423,10425,10427,10429,10431],{"class":280,"line":322},[278,10403,10404],{"class":302},"    audioUrls: audioUrls ",[278,10406,5114],{"class":298},[278,10408,5117],{"class":302},[278,10410,1828],{"class":333},[278,10412,2079],{"class":302},[278,10414,5124],{"class":501},[278,10416,1845],{"class":302},[278,10418,1848],{"class":298},[278,10420,5131],{"class":309},[278,10422,5124],{"class":302},[278,10424,1277],{"class":309},[278,10426,1845],{"class":302},[278,10428,960],{"class":298},[278,10430,1035],{"class":650},[278,10432,660],{"class":302},[278,10434,10435],{"class":280,"line":327},[278,10436,2037],{"class":302},[11,10438,10439,10440,10443,10444,263,10447,10450],{},"The reason to do so was to serve all audio recordings through an ",[59,10441,10442],{},"\u002Faudio"," path. Create a new file ",[59,10445,10446],{},"[…pathname].get.ts",[59,10448,10449],{},"server\u002Froutes\u002Faudio"," folder and add the following to it:",[269,10452,10454],{"className":271,"code":10453,"language":273,"meta":274,"style":274},"export default defineEventHandler(async (event) => {\n  const { pathname } = getRouterParams(event);\n\n  return hubBlob().serve(event, pathname);\n});\n",[59,10455,10456,10478,10496,10500,10514],{"__ignoreMap":274},[278,10457,10458,10460,10462,10464,10466,10468,10470,10472,10474,10476],{"class":280,"line":281},[278,10459,628],{"class":298},[278,10461,631],{"class":298},[278,10463,3878],{"class":333},[278,10465,1126],{"class":302},[278,10467,1050],{"class":298},[278,10469,1245],{"class":302},[278,10471,3887],{"class":501},[278,10473,1845],{"class":302},[278,10475,1848],{"class":298},[278,10477,876],{"class":302},[278,10479,10480,10482,10484,10487,10489,10491,10494],{"class":280,"line":288},[278,10481,758],{"class":298},[278,10483,1009],{"class":302},[278,10485,10486],{"class":650},"pathname",[278,10488,1029],{"class":302},[278,10490,358],{"class":298},[278,10492,10493],{"class":333}," getRouterParams",[278,10495,3910],{"class":302},[278,10497,10498],{"class":280,"line":295},[278,10499,292],{"emptyLinePlaceholder":291},[278,10501,10502,10504,10506,10508,10511],{"class":280,"line":316},[278,10503,343],{"class":298},[278,10505,4256],{"class":333},[278,10507,4036],{"class":302},[278,10509,10510],{"class":333},"serve",[278,10512,10513],{"class":302},"(event, pathname);\n",[278,10515,10516],{"class":280,"line":322},[278,10517,3693],{"class":302},[11,10519,10520,10521,10523,10524,10527,10528,183],{},"What we’ve done above is to catch all requests to the ",[59,10522,10442],{}," path (by using the wildcard ",[59,10525,10526],{},"[…pathname]"," in the filename), and serve the requested recording from the storage using ",[59,10529,3837],{},[11,10531,10532],{},"With this, the frontend is complete, and all functionalities should now work seamlessly.",[24,10534,10536],{"id":10535},"further-enhancements","Further Enhancements",[11,10538,10539],{},"What you’ve created here is a basic version of the application—with all must-have features—that you saw in the beginning of the article. You can further refine the app and take it closer to the demo by:",[11,10541,10542],{},"What you’ve created here is a solid foundation for the application, complete with the core features introduced earlier. To further enhance the app and bring it closer to the full demo version, consider implementing the following features:",[123,10544,10545,10548,10555,10558],{},[74,10546,10547],{},"Adding a settings page to save post processing settings",[74,10549,10550,10551,10554],{},"Handle post processing in the ",[59,10552,10553],{},"\u002Ftranscribe"," api route",[74,10556,10557],{},"Allowing edit\u002Fdelete of saved notes",[74,10559,10560],{},"Experimenting with additional features that fit your use case or user needs.",[11,10562,10563],{},"If you get stuck while implementing these features, do not hesitate to look at the application source code. The complete source code of the final application is shared at the end of the article.",[24,10565,10567],{"id":10566},"deploying-the-application","Deploying the Application",[11,10569,10570],{},"You can deploy the application using either the NuxtHub admin dashboard or through the NuxtHub CLI.",[32,10572,10574],{"id":10573},"deploy-via-nuxthub-admin","Deploy via NuxtHub Admin",[71,10576,10577,10580,10583],{},[74,10578,10579],{},"Push your code to a GitHub repository.",[74,10581,10582],{},"Link the repository with NuxtHub.",[74,10584,10585],{},"Deploy from the Admin console.",[11,10587,10588],{},[47,10589,10592],{"href":10590,"rel":10591},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fdeploy#cloudflare-pages-ci",[51],"Learn more about NuxtHub Git integration",[32,10594,10596],{"id":10595},"deploy-via-nuxthub-cli","Deploy via NuxtHub CLI",[269,10598,10600],{"className":3335,"code":10599,"language":3337,"meta":274,"style":274},"npx nuxthub deploy\n",[59,10601,10602],{"__ignoreMap":274},[278,10603,10604,10606,10608],{"class":280,"line":281},[278,10605,3349],{"class":333},[278,10607,3352],{"class":309},[278,10609,10610],{"class":309}," deploy\n",[11,10612,10613],{},[47,10614,10617],{"href":10615,"rel":10616},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fdeploy#nuxthub-cli",[51],"Learn more about CLI deployment",[24,10619,10621],{"id":10620},"source-code","Source Code",[11,10623,10624,10625,10627],{},"You can find the source code of ",[59,10626,3124],{}," application on GitHub. The source code includes all the features discussed in this article, along with additional configurations and optimizations shown in the demo.",[40,10629],{"url":10630},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fvhisper",[24,10632,10634],{"id":10633},"conclusion","Conclusion",[11,10636,10637],{},"Congratulations! You've built a powerful application that records and transcribes audio, stores recordings, and manages notes with an intuitive interface. Along the way you’ve touched upon various aspects of Nuxt, NuxtHub and Cloudflare services. As you continue to refine and expand Vhisper, consider exploring additional features and optimizations to further enhance its functionality and user experience. Keep experimenting and innovating, and let this project be a stepping stone to even more ambitious endeavors.",[11,10639,3052],{},[11,10641,10642],{},"Until next time!",[3048,10644],{},[18,10646,10647],{},[11,10648,10649],{},[3061,10650,10651],{},"Keep adding the bits and soon you'll have a lot of bytes to share with the world.",[3065,10653,10654],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":274,"searchDepth":288,"depth":288,"links":10656},[10657,10658,10664,10676,10692,10693,10697,10698],{"id":26,"depth":288,"text":27},{"id":3201,"depth":288,"text":3202,"children":10659},[10660,10661,10663],{"id":3266,"depth":295,"text":3267},{"id":3325,"depth":295,"text":10662},"Project Init",{"id":3750,"depth":295,"text":3751},{"id":3805,"depth":288,"text":3806,"children":10665},[10666,10667,10669,10671,10673,10675],{"id":3815,"depth":295,"text":3816},{"id":3844,"depth":295,"text":10668},"\u002Fapi\u002Ftranscribe Endpoint",{"id":4203,"depth":295,"text":10670},"\u002Fapi\u002Fupload Endpoint",{"id":4390,"depth":295,"text":10672},"Defining the notes Table Schema",{"id":4943,"depth":295,"text":10674},"\u002Fapi\u002Fnotes Endpoints",{"id":5679,"depth":295,"text":5680},{"id":5752,"depth":288,"text":5753,"children":10677},[10678,10680,10682,10684,10686,10688,10689,10691],{"id":5759,"depth":295,"text":10679},"useMediaRecorder Composable",{"id":7117,"depth":295,"text":10681},"NoteEditorModal Component",{"id":7741,"depth":295,"text":10683},"NoteRecorder Component",{"id":8728,"depth":295,"text":10685},"AudioVisualizer Component",{"id":9055,"depth":295,"text":10687},"useRecordings Composable",{"id":9565,"depth":295,"text":9566},{"id":9971,"depth":295,"text":10690},"NoteCard component",{"id":10351,"depth":295,"text":10352},{"id":10535,"depth":288,"text":10536},{"id":10566,"depth":288,"text":10567,"children":10694},[10695,10696],{"id":10573,"depth":295,"text":10574},{"id":10595,"depth":295,"text":10596},{"id":10620,"depth":288,"text":10621},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fbuilding-voice-notes-app-with-ai-transcription-and-post-processing\u002F9f989586-15d7-4cba-862d-edc11882aee4-9c9c98a588.png","2024-11-28T19:28:53.835Z","After wrapping up my last project—a chat interface to search GitHub—I found myself searching for the next idea to tackle. As a developer, inspiration often comes unexpectedly, a...","cm41pk3m3000k09l8g5kre7ep",{},"\u002Fbuilding-voice-notes-app-with-ai-transcription-and-post-processing",{"title":3101,"description":10701},"building-voice-notes-app-with-ai-transcription-and-post-processing",[10708,10709,10710,10711,10712],"cloudflare","ai","nuxt","nuxthub","nuxtui","pnPFG6mwX2Fz0aU3ywPx2OY-NAm9rfeTaTq-8Sp0rIU",{"id":10715,"title":10716,"body":10717,"cover":18306,"date":18307,"description":18308,"draft":3086,"extension":3087,"hashnodeId":18309,"meta":18310,"navigation":291,"path":18311,"seo":18312,"slug":18313,"stem":18313,"tags":18314,"__hash__":18317},"posts\u002Fbuilding-a-chat-interface-to-search-github.md","Rethinking GitHub Search: How I built a Chat Interface to search GitHub",{"type":8,"value":10718,"toc":18272},[10719,10728,10731,10735,10746,10753,10762,10766,10769,10786,10789,10795,10803,10806,10808,10827,10831,10834,10881,10883,10907,10913,10926,10972,10978,10982,10992,11003,11041,11047,11095,11101,11156,11171,11174,11177,11181,11184,11188,11191,11211,11214,11405,11432,11438,11715,11718,11738,11741,12172,12176,12187,12691,12705,12709,12712,12868,12871,12875,12878,12890,13567,13587,13626,13645,13648,13652,13658,13668,13679,13685,13689,13692,14502,14509,14563,14567,14577,15062,15072,15125,15128,15159,15163,15170,15955,15958,16004,16007,16011,16018,16024,16028,16039,16049,16212,16228,16239,16431,16437,16491,16495,16505,16508,16743,16754,16761,16765,16768,16772,16781,16787,16882,16892,17553,17556,17928,17932,17935,18154,18158,18161,18167,18170,18173,18175,18178,18181,18185,18193,18196,18206,18208,18226,18229,18243,18246,18248,18251,18254,18257,18259,18261,18269],[11,10720,10721,10722,10727],{},"Here I am again, exploring the world of chat interfaces. If you've been following my journey (I mean my ",[47,10723,10726],{"href":10724,"rel":10725},"https:\u002F\u002Frajeev.dev\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui",[51],"last post","), you might think, 'Rajeev, you're becoming obsessed with chatting!' And you wouldn't be entirely wrong. While I'm not always the most talkative person in a room, give me the right topic - like simplifying how we interact with technology - and my excitement becomes difficult to contain.",[11,10729,10730],{},"This time, my enthusiasm has led me to tackle a challenge many developers face often: searching GitHub efficiently. Let me introduce Chat GitHub, a tool where the complexity of code repositories meets the simplicity of conversation.",[24,10732,10734],{"id":10733},"why-chat-github","Why Chat GitHub?",[11,10736,10737,10738,10741,10742,10745],{},"So, why a chat interface for GitHub search? Well, GitHub search, as powerful as it is with all its filters, can’t beat the simplicity of natural language queries. Let’s be real—it won’t answer simple questions like, ",[3061,10739,10740],{},"“When did I make my first commit?”"," (What filters would you even use to find this information with the current GitHub Search?). GitHub certainly knows (reminds me of ",[3061,10743,10744],{},"“I Know What You Did Last Summer”","), but it just won’t answer you directly.",[11,10747,10748,10749,10752],{},"Natural language makes the experience frictionless. Instead of crafting complex queries, you can just ",[3061,10750,10751],{},"ask"," GitHub, like you would a colleague.",[11,10754,10755,10756,10761],{},"When ",[47,10757,10760],{"href":10758,"rel":10759},"https:\u002F\u002Fhashnode.com\u002F@atinuxt",[51],"Sébastien Chopin"," mentioned the idea to me, I was onboard immediately (also because I was itching to build something…).",[24,10763,10765],{"id":10764},"how-it-works","How it Works?",[11,10767,10768],{},"At its core, Chat GitHub leverages the power of OpenAI's language models to interpret your natural language queries and translate them into GitHub's search syntax. Here's a simplified breakdown of the process:",[123,10770,10771,10774,10777,10780,10783],{},[74,10772,10773],{},"You enter a query in plain English",[74,10775,10776],{},"The AI model interprets your request",[74,10778,10779],{},"It then selects the appropriate search endpoint, and generates the necessary GitHub API query parameters",[74,10781,10782],{},"We send a request to GitHub's API with these details",[74,10784,10785],{},"The results are fetched and presented to you in a clean, easy-to-digest chat interface",[11,10787,10788],{},"Here's a sneak peek of what the chat interface looks like:",[11,10790,10791],{},[3135,10792],{"alt":10793,"src":10794},"chat github's chatting interface","\u002Fimages\u002Fposts\u002Fbuilding-a-chat-interface-to-search-github\u002F2b8c0fe0-0564-4f0f-8609-19d249549829-662eee868c.png",[11,10796,10797,10798],{},"You can try it out live here: ",[47,10799,10802],{"href":10800,"rel":10801},"https:\u002F\u002Fchat-github.nuxt.dev\u002F",[51],"https:\u002F\u002Fchat-github.nuxt.dev",[11,10804,10805],{},"We’ll explore the key aspects of this process in the sections to follow. Ready? Let’s get started.",[24,10807,3202],{"id":3201},[11,10809,10810,10811,1245,10816,10821,10822,10826],{},"This project follows a similar setup to my last one ",[47,10812,10815],{"href":10813,"rel":10814},"https:\u002F\u002Fhub-chat.nuxt.dev",[51],"Hub Chat",[47,10817,10820],{"href":10818,"rel":10819},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fhub-chat",[51],"GitHub link","), and I’ve reused several components with some slight modifications. I won't bore you by repeating the same details, but if you’re new here, feel free to follow along with both posts (",[47,10823,10825],{"href":10724,"rel":10824},[51],"previous post",") for a more complete picture.",[32,10828,10830],{"id":10829},"tech-stack","Tech Stack",[11,10832,10833],{},"Here’s a quick breakdown of the tech stack:",[71,10835,10836,10842,10848,10858,10864,10870,10876],{},[74,10837,10838,10841],{},[94,10839,10840],{},"Nuxt 3",": For the overall framework and routing.",[74,10843,10844,10847],{},[94,10845,10846],{},"OpenAI APIs",": To handle the natural language processing.",[74,10849,10850,10853,10854,10857],{},[94,10851,10852],{},"GitHub API",": To fetch the data you’re looking for—remember? Only ",[3061,10855,10856],{},"it"," knows what you did last summer.",[74,10859,10860,10863],{},[94,10861,10862],{},"NuxtUI",": To make sure the UI is smooth and responsive.",[74,10865,10866,10869],{},[94,10867,10868],{},"Nuxt-Auth-Utils",": For user authentication and handling GitHub login (Only authenticated users can start a chat).",[74,10871,10872,10875],{},[94,10873,10874],{},"Nuxt MDC:"," For parsing and displaying the markdown responses",[74,10877,10878,10880],{},[94,10879,3194],{},": For deployment, database, and caching (all powered by Cloudflare).",[32,10882,3267],{"id":3266},[71,10884,10885,10895,10901],{},[74,10886,10887,10890,10891,10894],{},[94,10888,10889],{},"GitHub account:"," You'll need this to generate a ",[59,10892,10893],{},"GITHUB_TOKEN"," for API queries, and to create an OAuth App for authentication.",[74,10896,10897,10900],{},[94,10898,10899],{},"OpenAI account:"," For creating an OpenAI API key, so the app can process your queries.",[74,10902,10903,10906],{},[94,10904,10905],{},"Optional",": If you're looking to deploy the project yourself, you'll need Cloudflare and a NuxtHub account.",[32,10908,10910],{"id":10909},"setting-up-the-project",[94,10911,10912],{},"Setting up the project",[11,10914,10915,10916,1708,10919,919,10922,10925],{},"Follow the setup process from my previous article. Just remember to add the ",[59,10917,10918],{},"nuxt-auth-utils",[59,10920,10921],{},"@octokit\u002Frest",[59,10923,10924],{},"OpenAI"," dependencies.",[269,10927,10929],{"className":3335,"code":10928,"language":3337,"meta":274,"style":274},"# Add the nuxt-auth-utils module\nnpx nuxi module add auth-utils\n\n# Add the Octokit Rest & OpenAI libraries\npnpm add @octokit\u002Frest openai\n",[59,10930,10931,10936,10951,10955,10960],{"__ignoreMap":274},[278,10932,10933],{"class":280,"line":281},[278,10934,10935],{"class":284},"# Add the nuxt-auth-utils module\n",[278,10937,10938,10940,10943,10946,10948],{"class":280,"line":288},[278,10939,3349],{"class":333},[278,10941,10942],{"class":309}," nuxi",[278,10944,10945],{"class":309}," module",[278,10947,3418],{"class":309},[278,10949,10950],{"class":309}," auth-utils\n",[278,10952,10953],{"class":280,"line":295},[278,10954,292],{"emptyLinePlaceholder":291},[278,10956,10957],{"class":280,"line":316},[278,10958,10959],{"class":284},"# Add the Octokit Rest & OpenAI libraries\n",[278,10961,10962,10964,10966,10969],{"class":280,"line":322},[278,10963,3373],{"class":333},[278,10965,3418],{"class":309},[278,10967,10968],{"class":309}," @octokit\u002Frest",[278,10970,10971],{"class":309}," openai\n",[11,10973,10974,10975,10977],{},"Once the dependencies are set up, you should be able to run the project with ",[59,10976,5739],{}," and see the default app UI at localhost:3000.",[32,10979,10981],{"id":10980},"configs-and-environment-variables","Configs and Environment Variables",[11,10983,10984,10985,10987,10988,10991],{},"At this point, you can enable the hub database and cache in the ",[59,10986,3490],{}," file for later use, as well as create the necessary API tokens and keys to place in the ",[59,10989,10990],{},".env"," file.",[11,10993,10994,10995,919,10997,11000,11001,960],{},"Enabling ",[59,10996,3248],{},[59,10998,10999],{},"cache"," in ",[59,11002,3490],{},[269,11004,11006],{"className":271,"code":11005,"language":273,"meta":274,"style":274},"hub: {\n  cache: true,\n  database: true,\n},\n",[59,11007,11008,11014,11025,11036],{"__ignoreMap":274},[278,11009,11010,11012],{"class":280,"line":281},[278,11011,3829],{"class":333},[278,11013,5706],{"class":302},[278,11015,11016,11019,11021,11023],{"class":280,"line":288},[278,11017,11018],{"class":333},"  cache",[278,11020,1155],{"class":302},[278,11022,2931],{"class":650},[278,11024,660],{"class":302},[278,11026,11027,11030,11032,11034],{"class":280,"line":295},[278,11028,11029],{"class":333},"  database",[278,11031,1155],{"class":302},[278,11033,2931],{"class":650},[278,11035,660],{"class":302},[278,11037,11038],{"class":280,"line":316},[278,11039,11040],{"class":302},"},\n",[11,11042,11043,11044,11046],{},"Next, generate the following tokens and keys, and store them in your ",[59,11045,10990],{}," file (located in the root of your project):",[71,11048,11049,11061,11072],{},[74,11050,11051,11054,11055,11060],{},[94,11052,11053],{},"GitHub Token",": Create a ",[47,11056,11059],{"href":11057,"rel":11058},"https:\u002F\u002Fgithub.com\u002Fsettings\u002Fpersonal-access-tokens\u002Fnew",[51],"GitHub token"," (no special scope required) for making API calls.",[74,11062,11063,11066,11067,183],{},[94,11064,11065],{},"OpenAI API Key",": Generate a key from your ",[47,11068,11071],{"href":11069,"rel":11070},"https:\u002F\u002Fplatform.openai.com\u002Fapi-keys",[51],"OpenAI dashboard",[74,11073,11074,11054,11077,11082,11083,919,11086,11089,11090],{},[94,11075,11076],{},"GitHub OAuth App",[47,11078,11081],{"href":11079,"rel":11080},"https:\u002F\u002Fgithub.com\u002Fsettings\u002Fapplications\u002Fnew",[51],"GitHub OAuth app"," and get its ",[59,11084,11085],{},"CLIENT_ID",[59,11087,11088],{},"CLIENT_SECRET",". This will be used to authenticate users (only authenticated users can start a chat). For more information on how to create a GitHub OAuth app, refer to the ",[47,11091,11094],{"href":11092,"rel":11093},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fdevelopers\u002Fapps\u002Fbuilding-oauth-apps\u002Fcreating-an-oauth-app",[51],"GitHub documentation.",[11,11096,11097,11098,11100],{},"Your ",[59,11099,10990],{}," should contain the following entries:",[269,11102,11104],{"className":3335,"code":11103,"language":3337,"meta":274,"style":274},"NUXT_SESSION_PASSWORD=at_least_32_chars_string\nNUXT_OAUTH_GITHUB_CLIENT_ID=github_oauth_client_id\nNUXT_OAUTH_GITHUB_CLIENT_SECRET=github_oauth_client_secret\nNUXT_GITHUB_TOKEN=your_personal_access_token\nOPENAI_API_KEY=your_openai_api_key\n",[59,11105,11106,11116,11126,11136,11146],{"__ignoreMap":274},[278,11107,11108,11111,11113],{"class":280,"line":281},[278,11109,11110],{"class":302},"NUXT_SESSION_PASSWORD",[278,11112,358],{"class":298},[278,11114,11115],{"class":309},"at_least_32_chars_string\n",[278,11117,11118,11121,11123],{"class":280,"line":288},[278,11119,11120],{"class":302},"NUXT_OAUTH_GITHUB_CLIENT_ID",[278,11122,358],{"class":298},[278,11124,11125],{"class":309},"github_oauth_client_id\n",[278,11127,11128,11131,11133],{"class":280,"line":295},[278,11129,11130],{"class":302},"NUXT_OAUTH_GITHUB_CLIENT_SECRET",[278,11132,358],{"class":298},[278,11134,11135],{"class":309},"github_oauth_client_secret\n",[278,11137,11138,11141,11143],{"class":280,"line":316},[278,11139,11140],{"class":302},"NUXT_GITHUB_TOKEN",[278,11142,358],{"class":298},[278,11144,11145],{"class":309},"your_personal_access_token\n",[278,11147,11148,11151,11153],{"class":280,"line":322},[278,11149,11150],{"class":302},"OPENAI_API_KEY",[278,11152,358],{"class":298},[278,11154,11155],{"class":309},"your_openai_api_key\n",[11,11157,11158,11160,11161,11160,11163,11160,11166,11160,11168],{},[3061,11159,3312],{}," ",[59,11162,11110],{},[3061,11164,11165],{},"will automatically be created by",[59,11167,10918],{},[3061,11169,11170],{},"in development if you haven’t set it manually.",[11,11172,11173],{},"And that’s it for the setup—phew!",[11,11175,11176],{},"In the next section, we’ll explore the core of the chat process: making sense of the user query, converting it to a GitHub API call amd generating a final response.",[24,11178,11180],{"id":11179},"from-user-query-to-github-api-call","From User Query to GitHub API Call",[11,11182,11183],{},"Now, let’s break down how Chat GitHub processes your query, identifies the necessary actions, and makes the appropriate GitHub API call. This involves three main steps: interpreting the user query, calling the necessary tools (the GitHub API), and generating the final response.",[32,11185,11187],{"id":11186},"_1-interpreting-the-user-query","1. Interpreting the User Query",[11,11189,11190],{},"The first challenge is understanding what the user is asking for. To do this, the system relies on OpenAI’s language models to parse natural language inputs. It takes into account several factors:",[71,11192,11193,11199,11205],{},[74,11194,11195,11198],{},[94,11196,11197],{},"Intent Recognition",": What is the user trying to achieve? Are they searching for commits, issues, repositories, or user profiles?",[74,11200,11201,11204],{},[94,11202,11203],{},"Parameter Extraction",": Once the intent is clear, the model extracts necessary parameters like repo name, user, dates, and other filters. These details are critical for constructing a meaningful API call.",[74,11206,11207,11210],{},[94,11208,11209],{},"Tool Selection",": Based on the query, the AI determines if a GitHub API tool needs to be invoked (for example, searching commits or issues).",[11,11212,11213],{},"To guide the AI, I created a detailed system prompt to provide context. Here’s a portion of that prompt:",[269,11215,11217],{"className":271,"code":11216,"language":273,"meta":274,"style":274},"const systemPropmt = `You are a concise assistant who helps \\\nusers find information on GitHub. Use the supplied tools to \\\nfind information when asked.\n\nAvailable endpoints and key parameters:\n\n\u002F\u002F ...\n\n2. issues (Also searches PRs):\n  - Sort options: comments, reactions, reactions-+1, ...\n  - Query qualifiers: type, is, state, author, assignee, ...\n  - use \"type\" or \"is\" qualifier to search issues or PRs (type:issue\u002Ftype:pr)\n\n\u002F\u002F ...\n\nWhen using searchGithub function:\n1. Choose the appropriate search endpoint.\n2. Formulate a concise query (q) as per the user's request.\n3. Add any relevant sort or order parameters if needed.\n4. Always use appropriate per_page value to limit the number of results.\n\n\u002F\u002F ...\n\nExamples: \n\u002F\u002F ...\n\n2. Find the total number of repositories of a user\narguments: \n{\n  \"endpoint\": \"repositories\",\n  \"q\": \"user:\u003Cuser_login>\",\n  \"per_page\": 1\n}\n\nSummarize final response concisely using markdown when appropriate \\\n(for all links add {target=\"_blank\"} at the end). Do not include \\\nimages, commit SHA or hashes etc. in your summary.`\n",[59,11218,11219,11234,11241,11246,11250,11255,11259,11263,11267,11272,11277,11282,11287,11291,11295,11299,11304,11309,11314,11319,11324,11328,11332,11336,11341,11345,11349,11354,11359,11363,11368,11373,11378,11382,11386,11393,11400],{"__ignoreMap":274},[278,11220,11221,11223,11226,11228,11231],{"class":280,"line":281},[278,11222,5416],{"class":298},[278,11224,11225],{"class":650}," systemPropmt",[278,11227,764],{"class":298},[278,11229,11230],{"class":309}," `You are a concise assistant who helps ",[278,11232,11233],{"class":650},"\\\n",[278,11235,11236,11239],{"class":280,"line":288},[278,11237,11238],{"class":309},"users find information on GitHub. Use the supplied tools to ",[278,11240,11233],{"class":650},[278,11242,11243],{"class":280,"line":295},[278,11244,11245],{"class":309},"find information when asked.\n",[278,11247,11248],{"class":280,"line":316},[278,11249,292],{"emptyLinePlaceholder":291},[278,11251,11252],{"class":280,"line":322},[278,11253,11254],{"class":309},"Available endpoints and key parameters:\n",[278,11256,11257],{"class":280,"line":327},[278,11258,292],{"emptyLinePlaceholder":291},[278,11260,11261],{"class":280,"line":340},[278,11262,319],{"class":309},[278,11264,11265],{"class":280,"line":349},[278,11266,292],{"emptyLinePlaceholder":291},[278,11268,11269],{"class":280,"line":375},[278,11270,11271],{"class":309},"2. issues (Also searches PRs):\n",[278,11273,11274],{"class":280,"line":386},[278,11275,11276],{"class":309},"  - Sort options: comments, reactions, reactions-+1, ...\n",[278,11278,11279],{"class":280,"line":397},[278,11280,11281],{"class":309},"  - Query qualifiers: type, is, state, author, assignee, ...\n",[278,11283,11284],{"class":280,"line":408},[278,11285,11286],{"class":309},"  - use \"type\" or \"is\" qualifier to search issues or PRs (type:issue\u002Ftype:pr)\n",[278,11288,11289],{"class":280,"line":433},[278,11290,292],{"emptyLinePlaceholder":291},[278,11292,11293],{"class":280,"line":454},[278,11294,319],{"class":309},[278,11296,11297],{"class":280,"line":475},[278,11298,292],{"emptyLinePlaceholder":291},[278,11300,11301],{"class":280,"line":496},[278,11302,11303],{"class":309},"When using searchGithub function:\n",[278,11305,11306],{"class":280,"line":505},[278,11307,11308],{"class":309},"1. Choose the appropriate search endpoint.\n",[278,11310,11311],{"class":280,"line":516},[278,11312,11313],{"class":309},"2. Formulate a concise query (q) as per the user's request.\n",[278,11315,11316],{"class":280,"line":527},[278,11317,11318],{"class":309},"3. Add any relevant sort or order parameters if needed.\n",[278,11320,11321],{"class":280,"line":533},[278,11322,11323],{"class":309},"4. Always use appropriate per_page value to limit the number of results.\n",[278,11325,11326],{"class":280,"line":539},[278,11327,292],{"emptyLinePlaceholder":291},[278,11329,11330],{"class":280,"line":545},[278,11331,319],{"class":309},[278,11333,11334],{"class":280,"line":551},[278,11335,292],{"emptyLinePlaceholder":291},[278,11337,11338],{"class":280,"line":557},[278,11339,11340],{"class":309},"Examples: \n",[278,11342,11343],{"class":280,"line":567},[278,11344,319],{"class":309},[278,11346,11347],{"class":280,"line":577},[278,11348,292],{"emptyLinePlaceholder":291},[278,11350,11351],{"class":280,"line":587},[278,11352,11353],{"class":309},"2. Find the total number of repositories of a user\n",[278,11355,11356],{"class":280,"line":597},[278,11357,11358],{"class":309},"arguments: \n",[278,11360,11361],{"class":280,"line":608},[278,11362,524],{"class":309},[278,11364,11365],{"class":280,"line":614},[278,11366,11367],{"class":309},"  \"endpoint\": \"repositories\",\n",[278,11369,11370],{"class":280,"line":620},[278,11371,11372],{"class":309},"  \"q\": \"user:\u003Cuser_login>\",\n",[278,11374,11375],{"class":280,"line":625},[278,11376,11377],{"class":309},"  \"per_page\": 1\n",[278,11379,11380],{"class":280,"line":640},[278,11381,617],{"class":309},[278,11383,11384],{"class":280,"line":663},[278,11385,292],{"emptyLinePlaceholder":291},[278,11387,11388,11391],{"class":280,"line":669},[278,11389,11390],{"class":309},"Summarize final response concisely using markdown when appropriate ",[278,11392,11233],{"class":650},[278,11394,11395,11398],{"class":280,"line":680},[278,11396,11397],{"class":309},"(for all links add {target=\"_blank\"} at the end). Do not include ",[278,11399,11233],{"class":650},[278,11401,11402],{"class":280,"line":686},[278,11403,11404],{"class":309},"images, commit SHA or hashes etc. in your summary.`\n",[11,11406,11407,11160,11410,11413,11160,11416,11419,11160,11421,11160,11424,11160,11427,11430],{},[3061,11408,11409],{},"Note: I restricted the AI to only the most important GitHub search endpoints",[59,11411,11412],{},"\u002Fsearch\u002Fcommits",[3061,11414,11415],{},",",[59,11417,11418],{},"\u002Fsearch\u002Fissues",[3061,11420,11415],{},[59,11422,11423],{},"\u002Fsearch\u002Frepositories",[3061,11425,11426],{},"and",[59,11428,11429],{},"\u002Fsearch\u002Fusers",[3061,11431,183],{},[11,11433,11434,11435,4633],{},"In addition to the system prompt, we create tools definitions that lists the types of tools, their names, and their specific parameters (in this case I only create one function tool, ",[59,11436,11437],{},"searchGithub",[269,11439,11441],{"className":271,"code":11440,"language":273,"meta":274,"style":274},"const tools: OpenAI.ChatCompletionTool[] = [\n  {\n    type: 'function',\n    function: {\n      name: 'searchGithub',\n      description:\n        'Searches GitHub for information using the GitHub API. Call this if you need to find information on GitHub.',\n      parameters: {\n        type: 'object',\n        properties: {\n          endpoint: {\n            type: 'string',\n            description: `The specific search endpoint to use. One of ['commits', 'issues', 'repositories', 'users']`,\n          },\n          q: {\n            type: 'string',\n            description: 'the search query using applicable qualifiers',\n          },\n          sort: {\n            type: 'string',\n            description: 'The sort field (optional, depends on the endpoint)',\n          },\n          order: {\n            type: 'string',\n            description: 'The sort order (optional, asc or desc)',\n          },\n          per_page: {\n            type: 'string',\n            description:\n              'Number of results to fetch per page (max 25)',\n          },\n        },\n        required: ['endpoint', 'q', 'per_page'],\n        additionalProperties: false,\n      },\n    },\n  },\n];\n",[59,11442,11443,11466,11471,11481,11486,11496,11501,11508,11513,11523,11528,11533,11543,11553,11558,11563,11571,11580,11584,11589,11597,11606,11610,11615,11623,11632,11636,11641,11649,11654,11661,11665,11669,11689,11698,11702,11706,11710],{"__ignoreMap":274},[278,11444,11445,11447,11450,11452,11455,11457,11460,11462,11464],{"class":280,"line":281},[278,11446,5416],{"class":298},[278,11448,11449],{"class":650}," tools",[278,11451,960],{"class":298},[278,11453,11454],{"class":333}," OpenAI",[278,11456,183],{"class":302},[278,11458,11459],{"class":333},"ChatCompletionTool",[278,11461,1971],{"class":302},[278,11463,358],{"class":298},[278,11465,5876],{"class":302},[278,11467,11468],{"class":280,"line":288},[278,11469,11470],{"class":302},"  {\n",[278,11472,11473,11476,11479],{"class":280,"line":295},[278,11474,11475],{"class":302},"    type: ",[278,11477,11478],{"class":309},"'function'",[278,11480,660],{"class":302},[278,11482,11483],{"class":280,"line":316},[278,11484,11485],{"class":302},"    function: {\n",[278,11487,11488,11491,11494],{"class":280,"line":322},[278,11489,11490],{"class":302},"      name: ",[278,11492,11493],{"class":309},"'searchGithub'",[278,11495,660],{"class":302},[278,11497,11498],{"class":280,"line":327},[278,11499,11500],{"class":302},"      description:\n",[278,11502,11503,11506],{"class":280,"line":340},[278,11504,11505],{"class":309},"        'Searches GitHub for information using the GitHub API. Call this if you need to find information on GitHub.'",[278,11507,660],{"class":302},[278,11509,11510],{"class":280,"line":349},[278,11511,11512],{"class":302},"      parameters: {\n",[278,11514,11515,11518,11521],{"class":280,"line":375},[278,11516,11517],{"class":302},"        type: ",[278,11519,11520],{"class":309},"'object'",[278,11522,660],{"class":302},[278,11524,11525],{"class":280,"line":386},[278,11526,11527],{"class":302},"        properties: {\n",[278,11529,11530],{"class":280,"line":397},[278,11531,11532],{"class":302},"          endpoint: {\n",[278,11534,11535,11538,11541],{"class":280,"line":408},[278,11536,11537],{"class":302},"            type: ",[278,11539,11540],{"class":309},"'string'",[278,11542,660],{"class":302},[278,11544,11545,11548,11551],{"class":280,"line":433},[278,11546,11547],{"class":302},"            description: ",[278,11549,11550],{"class":309},"`The specific search endpoint to use. One of ['commits', 'issues', 'repositories', 'users']`",[278,11552,660],{"class":302},[278,11554,11555],{"class":280,"line":454},[278,11556,11557],{"class":302},"          },\n",[278,11559,11560],{"class":280,"line":475},[278,11561,11562],{"class":302},"          q: {\n",[278,11564,11565,11567,11569],{"class":280,"line":496},[278,11566,11537],{"class":302},[278,11568,11540],{"class":309},[278,11570,660],{"class":302},[278,11572,11573,11575,11578],{"class":280,"line":505},[278,11574,11547],{"class":302},[278,11576,11577],{"class":309},"'the search query using applicable qualifiers'",[278,11579,660],{"class":302},[278,11581,11582],{"class":280,"line":516},[278,11583,11557],{"class":302},[278,11585,11586],{"class":280,"line":527},[278,11587,11588],{"class":302},"          sort: {\n",[278,11590,11591,11593,11595],{"class":280,"line":533},[278,11592,11537],{"class":302},[278,11594,11540],{"class":309},[278,11596,660],{"class":302},[278,11598,11599,11601,11604],{"class":280,"line":539},[278,11600,11547],{"class":302},[278,11602,11603],{"class":309},"'The sort field (optional, depends on the endpoint)'",[278,11605,660],{"class":302},[278,11607,11608],{"class":280,"line":545},[278,11609,11557],{"class":302},[278,11611,11612],{"class":280,"line":551},[278,11613,11614],{"class":302},"          order: {\n",[278,11616,11617,11619,11621],{"class":280,"line":557},[278,11618,11537],{"class":302},[278,11620,11540],{"class":309},[278,11622,660],{"class":302},[278,11624,11625,11627,11630],{"class":280,"line":567},[278,11626,11547],{"class":302},[278,11628,11629],{"class":309},"'The sort order (optional, asc or desc)'",[278,11631,660],{"class":302},[278,11633,11634],{"class":280,"line":577},[278,11635,11557],{"class":302},[278,11637,11638],{"class":280,"line":587},[278,11639,11640],{"class":302},"          per_page: {\n",[278,11642,11643,11645,11647],{"class":280,"line":597},[278,11644,11537],{"class":302},[278,11646,11540],{"class":309},[278,11648,660],{"class":302},[278,11650,11651],{"class":280,"line":608},[278,11652,11653],{"class":302},"            description:\n",[278,11655,11656,11659],{"class":280,"line":614},[278,11657,11658],{"class":309},"              'Number of results to fetch per page (max 25)'",[278,11660,660],{"class":302},[278,11662,11663],{"class":280,"line":620},[278,11664,11557],{"class":302},[278,11666,11667],{"class":280,"line":625},[278,11668,2606],{"class":302},[278,11670,11671,11674,11677,11679,11682,11684,11687],{"class":280,"line":640},[278,11672,11673],{"class":302},"        required: [",[278,11675,11676],{"class":309},"'endpoint'",[278,11678,1708],{"class":302},[278,11680,11681],{"class":309},"'q'",[278,11683,1708],{"class":302},[278,11685,11686],{"class":309},"'per_page'",[278,11688,3533],{"class":302},[278,11690,11691,11694,11696],{"class":280,"line":663},[278,11692,11693],{"class":302},"        additionalProperties: ",[278,11695,2965],{"class":650},[278,11697,660],{"class":302},[278,11699,11700],{"class":280,"line":669},[278,11701,1165],{"class":302},[278,11703,11704],{"class":280,"line":680},[278,11705,2243],{"class":302},[278,11707,11708],{"class":280,"line":686},[278,11709,683],{"class":302},[278,11711,11712],{"class":280,"line":1334},[278,11713,11714],{"class":302},"];\n",[11,11716,11717],{},"A couple of key design decisions we made:",[71,11719,11720,11726,11732],{},[74,11721,11722,11725],{},[94,11723,11724],{},"Complimentary System Prompt & Tool Definition",": The system prompt gives context, while the tool definition ensures the API queries are correctly structured.",[74,11727,11728,11731],{},[94,11729,11730],{},"Result Limit",": While GitHub allows more items per response, we restrict results to 25 to keep things manageable.",[74,11733,11734,11737],{},[94,11735,11736],{},"No Pagination",": We decided to skip pagination, keeping things simple and avoiding unnecessary complexity.",[11,11739,11740],{},"Now, the AI is ready to handle the user query and transform it into a structured format that the system can use. Here is the relevant code:",[269,11742,11744],{"className":271,"code":11743,"language":273,"meta":274,"style":274},"let _openai: OpenAI;\nfunction useOpenAI() {\n  if (!_openai) {\n    _openai = new OpenAI({\n      apiKey: process.env.OPENAI_API_KEY,\n    });\n  }\n\n  return _openai;\n}\n\nexport const handleMessageWithOpenAI = async (\n  event: H3Event,\n  messages: OpenAI.Chat.ChatCompletionMessageParam[]\n) => {\n  const openai = useOpenAI();\n  const response = await openai.chat.completions.create({\n    model: MODEL, \u002F\u002F used gpt-4o\n    messages, \u002F\u002F contains system prompt and the complete chat history\n    tools, \u002F\u002F defined above\n  });\n    \n  const responseMessage = response.choices[0].message;\n  const toolCalls = responseMessage.tool_calls;\n    \n  if (toolCalls) {\n    messages.push(responseMessage);\n    \n    for (const toolCall of toolCalls) {\n      const functionName = toolCall.function.name;\n      if (functionName === 'searchGithub') {\n        const functionArgs = JSON.parse(toolCall.function.arguments);\n        const functionResponse = await searchGithub(\n          functionArgs.endpoint,\n          {\n            q: functionArgs.q,\n            sort: functionArgs.sort,\n            order: functionArgs.order,\n            per_page: functionArgs.per_page,\n          }\n        );\n    \n        \u002F\u002F ..\n      }\n    }\n    \n    \u002F\u002F ...\n  }\n    \n  return responseMessage.content;\n}\n",[59,11745,11746,11759,11768,11779,11792,11801,11805,11809,11813,11820,11824,11828,11843,11855,11877,11885,11898,11916,11929,11937,11945,11949,11954,11971,11983,11987,11994,12004,12008,12026,12038,12052,12072,12088,12093,12098,12103,12108,12113,12118,12123,12128,12132,12137,12141,12145,12149,12153,12157,12161,12168],{"__ignoreMap":274},[278,11747,11748,11750,11753,11755,11757],{"class":280,"line":281},[278,11749,1001],{"class":298},[278,11751,11752],{"class":302}," _openai",[278,11754,960],{"class":298},[278,11756,11454],{"class":333},[278,11758,313],{"class":302},[278,11760,11761,11763,11766],{"class":280,"line":288},[278,11762,330],{"class":298},[278,11764,11765],{"class":333}," useOpenAI",[278,11767,337],{"class":302},[278,11769,11770,11772,11774,11776],{"class":280,"line":295},[278,11771,1062],{"class":298},[278,11773,1245],{"class":302},[278,11775,1209],{"class":298},[278,11777,11778],{"class":302},"_openai) {\n",[278,11780,11781,11784,11786,11788,11790],{"class":280,"line":316},[278,11782,11783],{"class":302},"    _openai ",[278,11785,358],{"class":298},[278,11787,1258],{"class":298},[278,11789,11454],{"class":333},[278,11791,637],{"class":302},[278,11793,11794,11797,11799],{"class":280,"line":322},[278,11795,11796],{"class":302},"      apiKey: process.env.",[278,11798,11150],{"class":650},[278,11800,660],{"class":302},[278,11802,11803],{"class":280,"line":327},[278,11804,1233],{"class":302},[278,11806,11807],{"class":280,"line":340},[278,11808,1096],{"class":302},[278,11810,11811],{"class":280,"line":349},[278,11812,292],{"emptyLinePlaceholder":291},[278,11814,11815,11817],{"class":280,"line":375},[278,11816,343],{"class":298},[278,11818,11819],{"class":302}," _openai;\n",[278,11821,11822],{"class":280,"line":386},[278,11823,617],{"class":302},[278,11825,11826],{"class":280,"line":397},[278,11827,292],{"emptyLinePlaceholder":291},[278,11829,11830,11832,11834,11837,11839,11841],{"class":280,"line":408},[278,11831,628],{"class":298},[278,11833,4559],{"class":298},[278,11835,11836],{"class":333}," handleMessageWithOpenAI",[278,11838,764],{"class":298},[278,11840,2325],{"class":298},[278,11842,346],{"class":302},[278,11844,11845,11848,11850,11853],{"class":280,"line":433},[278,11846,11847],{"class":501},"  event",[278,11849,960],{"class":298},[278,11851,11852],{"class":333}," H3Event",[278,11854,660],{"class":302},[278,11856,11857,11860,11862,11864,11866,11869,11871,11874],{"class":280,"line":454},[278,11858,11859],{"class":501},"  messages",[278,11861,960],{"class":298},[278,11863,11454],{"class":333},[278,11865,183],{"class":302},[278,11867,11868],{"class":333},"Chat",[278,11870,183],{"class":302},[278,11872,11873],{"class":333},"ChatCompletionMessageParam",[278,11875,11876],{"class":302},"[]\n",[278,11878,11879,11881,11883],{"class":280,"line":475},[278,11880,1845],{"class":302},[278,11882,1848],{"class":298},[278,11884,876],{"class":302},[278,11886,11887,11889,11892,11894,11896],{"class":280,"line":496},[278,11888,758],{"class":298},[278,11890,11891],{"class":650}," openai",[278,11893,764],{"class":298},[278,11895,11765],{"class":333},[278,11897,1313],{"class":302},[278,11899,11900,11902,11904,11906,11908,11911,11914],{"class":280,"line":505},[278,11901,758],{"class":298},[278,11903,1115],{"class":650},[278,11905,764],{"class":298},[278,11907,1120],{"class":298},[278,11909,11910],{"class":302}," openai.chat.completions.",[278,11912,11913],{"class":333},"create",[278,11915,637],{"class":302},[278,11917,11918,11921,11924,11926],{"class":280,"line":516},[278,11919,11920],{"class":302},"    model: ",[278,11922,11923],{"class":650},"MODEL",[278,11925,1708],{"class":302},[278,11927,11928],{"class":284},"\u002F\u002F used gpt-4o\n",[278,11930,11931,11934],{"class":280,"line":527},[278,11932,11933],{"class":302},"    messages, ",[278,11935,11936],{"class":284},"\u002F\u002F contains system prompt and the complete chat history\n",[278,11938,11939,11942],{"class":280,"line":533},[278,11940,11941],{"class":302},"    tools, ",[278,11943,11944],{"class":284},"\u002F\u002F defined above\n",[278,11946,11947],{"class":280,"line":539},[278,11948,2037],{"class":302},[278,11950,11951],{"class":280,"line":545},[278,11952,11953],{"class":302},"    \n",[278,11955,11956,11958,11961,11963,11966,11968],{"class":280,"line":551},[278,11957,758],{"class":298},[278,11959,11960],{"class":650}," responseMessage",[278,11962,764],{"class":298},[278,11964,11965],{"class":302}," response.choices[",[278,11967,2012],{"class":650},[278,11969,11970],{"class":302},"].message;\n",[278,11972,11973,11975,11978,11980],{"class":280,"line":557},[278,11974,758],{"class":298},[278,11976,11977],{"class":650}," toolCalls",[278,11979,764],{"class":298},[278,11981,11982],{"class":302}," responseMessage.tool_calls;\n",[278,11984,11985],{"class":280,"line":567},[278,11986,11953],{"class":302},[278,11988,11989,11991],{"class":280,"line":577},[278,11990,1062],{"class":298},[278,11992,11993],{"class":302}," (toolCalls) {\n",[278,11995,11996,11999,12001],{"class":280,"line":587},[278,11997,11998],{"class":302},"    messages.",[278,12000,6524],{"class":333},[278,12002,12003],{"class":302},"(responseMessage);\n",[278,12005,12006],{"class":280,"line":597},[278,12007,11953],{"class":302},[278,12009,12010,12013,12015,12017,12020,12023],{"class":280,"line":608},[278,12011,12012],{"class":298},"    for",[278,12014,1245],{"class":302},[278,12016,5416],{"class":298},[278,12018,12019],{"class":650}," toolCall",[278,12021,12022],{"class":298}," of",[278,12024,12025],{"class":302}," toolCalls) {\n",[278,12027,12028,12030,12033,12035],{"class":280,"line":614},[278,12029,2461],{"class":298},[278,12031,12032],{"class":650}," functionName",[278,12034,764],{"class":298},[278,12036,12037],{"class":302}," toolCall.function.name;\n",[278,12039,12040,12042,12045,12047,12050],{"class":280,"line":620},[278,12041,6207],{"class":298},[278,12043,12044],{"class":302}," (functionName ",[278,12046,2451],{"class":298},[278,12048,12049],{"class":309}," 'searchGithub'",[278,12051,1718],{"class":302},[278,12053,12054,12056,12059,12061,12064,12066,12069],{"class":280,"line":625},[278,12055,6741],{"class":298},[278,12057,12058],{"class":650}," functionArgs",[278,12060,764],{"class":298},[278,12062,12063],{"class":650}," JSON",[278,12065,183],{"class":302},[278,12067,12068],{"class":333},"parse",[278,12070,12071],{"class":302},"(toolCall.function.arguments);\n",[278,12073,12074,12076,12079,12081,12083,12086],{"class":280,"line":640},[278,12075,6741],{"class":298},[278,12077,12078],{"class":650}," functionResponse",[278,12080,764],{"class":298},[278,12082,1120],{"class":298},[278,12084,12085],{"class":333}," searchGithub",[278,12087,770],{"class":302},[278,12089,12090],{"class":280,"line":663},[278,12091,12092],{"class":302},"          functionArgs.endpoint,\n",[278,12094,12095],{"class":280,"line":669},[278,12096,12097],{"class":302},"          {\n",[278,12099,12100],{"class":280,"line":680},[278,12101,12102],{"class":302},"            q: functionArgs.q,\n",[278,12104,12105],{"class":280,"line":686},[278,12106,12107],{"class":302},"            sort: functionArgs.sort,\n",[278,12109,12110],{"class":280,"line":1334},[278,12111,12112],{"class":302},"            order: functionArgs.order,\n",[278,12114,12115],{"class":280,"line":1375},[278,12116,12117],{"class":302},"            per_page: functionArgs.per_page,\n",[278,12119,12120],{"class":280,"line":1381},[278,12121,12122],{"class":302},"          }\n",[278,12124,12125],{"class":280,"line":1386},[278,12126,12127],{"class":302},"        );\n",[278,12129,12130],{"class":280,"line":1394},[278,12131,11953],{"class":302},[278,12133,12134],{"class":280,"line":1406},[278,12135,12136],{"class":284},"        \u002F\u002F ..\n",[278,12138,12139],{"class":280,"line":1423},[278,12140,6234],{"class":302},[278,12142,12143],{"class":280,"line":1432},[278,12144,1285],{"class":302},[278,12146,12147],{"class":280,"line":1437},[278,12148,11953],{"class":302},[278,12150,12151],{"class":280,"line":1916},[278,12152,896],{"class":284},[278,12154,12155],{"class":280,"line":1939},[278,12156,1096],{"class":302},[278,12158,12159],{"class":280,"line":1949},[278,12160,11953],{"class":302},[278,12162,12163,12165],{"class":280,"line":1954},[278,12164,343],{"class":298},[278,12166,12167],{"class":302}," responseMessage.content;\n",[278,12169,12170],{"class":280,"line":1959},[278,12171,617],{"class":302},[32,12173,12175],{"id":12174},"_2-using-tool-calls-for-github-search","2. Using Tool Calls for GitHub Search",[11,12177,12178,12179,12182,12183,12186],{},"Once the AI determines the need for a GitHub API call, it generates a ",[59,12180,12181],{},"tool_call"," with the necessary parameters. Here’s how we handle this with the ",[59,12184,12185],{},"searchGitHub"," function:",[269,12188,12190],{"className":271,"code":12189,"language":273,"meta":274,"style":274},"export type SearchParams = {\n  endpoint: string;\n  q: string;\n  order?: string;\n  sort?: string;\n  per_page: string;\n};\n\nconst allowedEndpoints = {\n  commits: 'GET \u002Fsearch\u002Fcommits',\n  issues: 'GET \u002Fsearch\u002Fissues',\n  repositories: 'GET \u002Fsearch\u002Frepositories',\n  users: 'GET \u002Fsearch\u002Fusers',\n} as const;\n\ntype EndpointType = keyof typeof allowedEndpoints;\n\nlet _octokit: Octokit;\n\nfunction useOctokit() {\n  if (!_octokit) {\n    _octokit = new Octokit({\n      auth: process.env.NUXT_GITHUB_TOKEN,\n    });\n  }\n\n  return _octokit;\n}\n\nexport const searchGithub = async (\n  endpoint: string,\n  params: Omit\u003CSearchParams, 'endpoint'>\n) => {\n  if (!endpoint || !allowedEndpoints[endpoint as EndpointType]) {\n    throw createError({\n      statusCode: 404,\n      message: 'Endpoint not supported',\n    });\n  }\n\n  const octokit = useOctokit();\n  const endpointToUse = allowedEndpoints[endpoint as EndpointType];\n\n  try {\n    const response = await octokit.request(endpointToUse as string, params);\n\n    return response.data;\n  } catch (error) {\n    console.error(error);\n    throw createError({\n      statusCode: 500,\n      message: 'Error searching GitHub',\n    });\n  }\n};\n",[59,12191,12192,12205,12216,12227,12238,12249,12260,12264,12268,12279,12289,12299,12309,12319,12330,12334,12352,12356,12370,12374,12383,12394,12407,12416,12420,12424,12428,12435,12439,12443,12457,12467,12488,12496,12521,12529,12538,12547,12551,12555,12559,12572,12590,12594,12600,12626,12630,12637,12645,12654,12662,12670,12679,12683,12687],{"__ignoreMap":274},[278,12193,12194,12196,12198,12201,12203],{"class":280,"line":281},[278,12195,628],{"class":298},[278,12197,9454],{"class":298},[278,12199,12200],{"class":333}," SearchParams",[278,12202,764],{"class":298},[278,12204,876],{"class":302},[278,12206,12207,12210,12212,12214],{"class":280,"line":288},[278,12208,12209],{"class":501},"  endpoint",[278,12211,960],{"class":298},[278,12213,963],{"class":650},[278,12215,313],{"class":302},[278,12217,12218,12221,12223,12225],{"class":280,"line":295},[278,12219,12220],{"class":501},"  q",[278,12222,960],{"class":298},[278,12224,963],{"class":650},[278,12226,313],{"class":302},[278,12228,12229,12232,12234,12236],{"class":280,"line":316},[278,12230,12231],{"class":501},"  order",[278,12233,9512],{"class":298},[278,12235,963],{"class":650},[278,12237,313],{"class":302},[278,12239,12240,12243,12245,12247],{"class":280,"line":322},[278,12241,12242],{"class":501},"  sort",[278,12244,9512],{"class":298},[278,12246,963],{"class":650},[278,12248,313],{"class":302},[278,12250,12251,12254,12256,12258],{"class":280,"line":327},[278,12252,12253],{"class":501},"  per_page",[278,12255,960],{"class":298},[278,12257,963],{"class":650},[278,12259,313],{"class":302},[278,12261,12262],{"class":280,"line":340},[278,12263,2817],{"class":302},[278,12265,12266],{"class":280,"line":349},[278,12267,292],{"emptyLinePlaceholder":291},[278,12269,12270,12272,12275,12277],{"class":280,"line":375},[278,12271,5416],{"class":298},[278,12273,12274],{"class":650}," allowedEndpoints",[278,12276,764],{"class":298},[278,12278,876],{"class":302},[278,12280,12281,12284,12287],{"class":280,"line":386},[278,12282,12283],{"class":302},"  commits: ",[278,12285,12286],{"class":309},"'GET \u002Fsearch\u002Fcommits'",[278,12288,660],{"class":302},[278,12290,12291,12294,12297],{"class":280,"line":397},[278,12292,12293],{"class":302},"  issues: ",[278,12295,12296],{"class":309},"'GET \u002Fsearch\u002Fissues'",[278,12298,660],{"class":302},[278,12300,12301,12304,12307],{"class":280,"line":408},[278,12302,12303],{"class":302},"  repositories: ",[278,12305,12306],{"class":309},"'GET \u002Fsearch\u002Frepositories'",[278,12308,660],{"class":302},[278,12310,12311,12314,12317],{"class":280,"line":433},[278,12312,12313],{"class":302},"  users: ",[278,12315,12316],{"class":309},"'GET \u002Fsearch\u002Fusers'",[278,12318,660],{"class":302},[278,12320,12321,12324,12326,12328],{"class":280,"line":454},[278,12322,12323],{"class":302},"} ",[278,12325,2937],{"class":298},[278,12327,4559],{"class":298},[278,12329,313],{"class":302},[278,12331,12332],{"class":280,"line":475},[278,12333,292],{"emptyLinePlaceholder":291},[278,12335,12336,12338,12341,12343,12346,12349],{"class":280,"line":496},[278,12337,1638],{"class":298},[278,12339,12340],{"class":333}," EndpointType",[278,12342,764],{"class":298},[278,12344,12345],{"class":298}," keyof",[278,12347,12348],{"class":298}," typeof",[278,12350,12351],{"class":302}," allowedEndpoints;\n",[278,12353,12354],{"class":280,"line":505},[278,12355,292],{"emptyLinePlaceholder":291},[278,12357,12358,12360,12363,12365,12368],{"class":280,"line":516},[278,12359,1001],{"class":298},[278,12361,12362],{"class":302}," _octokit",[278,12364,960],{"class":298},[278,12366,12367],{"class":333}," Octokit",[278,12369,313],{"class":302},[278,12371,12372],{"class":280,"line":527},[278,12373,292],{"emptyLinePlaceholder":291},[278,12375,12376,12378,12381],{"class":280,"line":533},[278,12377,330],{"class":298},[278,12379,12380],{"class":333}," useOctokit",[278,12382,337],{"class":302},[278,12384,12385,12387,12389,12391],{"class":280,"line":539},[278,12386,1062],{"class":298},[278,12388,1245],{"class":302},[278,12390,1209],{"class":298},[278,12392,12393],{"class":302},"_octokit) {\n",[278,12395,12396,12399,12401,12403,12405],{"class":280,"line":545},[278,12397,12398],{"class":302},"    _octokit ",[278,12400,358],{"class":298},[278,12402,1258],{"class":298},[278,12404,12367],{"class":333},[278,12406,637],{"class":302},[278,12408,12409,12412,12414],{"class":280,"line":551},[278,12410,12411],{"class":302},"      auth: process.env.",[278,12413,11140],{"class":650},[278,12415,660],{"class":302},[278,12417,12418],{"class":280,"line":557},[278,12419,1233],{"class":302},[278,12421,12422],{"class":280,"line":567},[278,12423,1096],{"class":302},[278,12425,12426],{"class":280,"line":577},[278,12427,292],{"emptyLinePlaceholder":291},[278,12429,12430,12432],{"class":280,"line":587},[278,12431,343],{"class":298},[278,12433,12434],{"class":302}," _octokit;\n",[278,12436,12437],{"class":280,"line":597},[278,12438,617],{"class":302},[278,12440,12441],{"class":280,"line":608},[278,12442,292],{"emptyLinePlaceholder":291},[278,12444,12445,12447,12449,12451,12453,12455],{"class":280,"line":614},[278,12446,628],{"class":298},[278,12448,4559],{"class":298},[278,12450,12085],{"class":333},[278,12452,764],{"class":298},[278,12454,2325],{"class":298},[278,12456,346],{"class":302},[278,12458,12459,12461,12463,12465],{"class":280,"line":620},[278,12460,12209],{"class":501},[278,12462,960],{"class":298},[278,12464,963],{"class":650},[278,12466,660],{"class":302},[278,12468,12469,12472,12474,12477,12479,12482,12484,12486],{"class":280,"line":625},[278,12470,12471],{"class":501},"  params",[278,12473,960],{"class":298},[278,12475,12476],{"class":333}," Omit",[278,12478,1702],{"class":302},[278,12480,12481],{"class":333},"SearchParams",[278,12483,1708],{"class":302},[278,12485,11676],{"class":309},[278,12487,372],{"class":302},[278,12489,12490,12492,12494],{"class":280,"line":640},[278,12491,1845],{"class":302},[278,12493,1848],{"class":298},[278,12495,876],{"class":302},[278,12497,12498,12500,12502,12504,12507,12509,12511,12514,12516,12518],{"class":280,"line":663},[278,12499,1062],{"class":298},[278,12501,1245],{"class":302},[278,12503,1209],{"class":298},[278,12505,12506],{"class":302},"endpoint ",[278,12508,5954],{"class":298},[278,12510,6192],{"class":298},[278,12512,12513],{"class":302},"allowedEndpoints[endpoint ",[278,12515,2937],{"class":298},[278,12517,12340],{"class":333},[278,12519,12520],{"class":302},"]) {\n",[278,12522,12523,12525,12527],{"class":280,"line":669},[278,12524,1426],{"class":298},[278,12526,3957],{"class":333},[278,12528,637],{"class":302},[278,12530,12531,12533,12536],{"class":280,"line":680},[278,12532,3964],{"class":302},[278,12534,12535],{"class":650},"404",[278,12537,660],{"class":302},[278,12539,12540,12542,12545],{"class":280,"line":686},[278,12541,3974],{"class":302},[278,12543,12544],{"class":309},"'Endpoint not supported'",[278,12546,660],{"class":302},[278,12548,12549],{"class":280,"line":1334},[278,12550,1233],{"class":302},[278,12552,12553],{"class":280,"line":1375},[278,12554,1096],{"class":302},[278,12556,12557],{"class":280,"line":1381},[278,12558,292],{"emptyLinePlaceholder":291},[278,12560,12561,12563,12566,12568,12570],{"class":280,"line":1386},[278,12562,758],{"class":298},[278,12564,12565],{"class":650}," octokit",[278,12567,764],{"class":298},[278,12569,12380],{"class":333},[278,12571,1313],{"class":302},[278,12573,12574,12576,12579,12581,12584,12586,12588],{"class":280,"line":1394},[278,12575,758],{"class":298},[278,12577,12578],{"class":650}," endpointToUse",[278,12580,764],{"class":298},[278,12582,12583],{"class":302}," allowedEndpoints[endpoint ",[278,12585,2937],{"class":298},[278,12587,12340],{"class":333},[278,12589,11714],{"class":302},[278,12591,12592],{"class":280,"line":1406},[278,12593,292],{"emptyLinePlaceholder":291},[278,12595,12596,12598],{"class":280,"line":1423},[278,12597,1105],{"class":298},[278,12599,876],{"class":302},[278,12601,12602,12604,12606,12608,12610,12613,12616,12619,12621,12623],{"class":280,"line":1432},[278,12603,1112],{"class":298},[278,12605,1115],{"class":650},[278,12607,764],{"class":298},[278,12609,1120],{"class":298},[278,12611,12612],{"class":302}," octokit.",[278,12614,12615],{"class":333},"request",[278,12617,12618],{"class":302},"(endpointToUse ",[278,12620,2937],{"class":298},[278,12622,963],{"class":650},[278,12624,12625],{"class":302},", params);\n",[278,12627,12628],{"class":280,"line":1437},[278,12629,292],{"emptyLinePlaceholder":291},[278,12631,12632,12634],{"class":280,"line":1916},[278,12633,1088],{"class":298},[278,12635,12636],{"class":302}," response.data;\n",[278,12638,12639,12641,12643],{"class":280,"line":1939},[278,12640,1397],{"class":302},[278,12642,1400],{"class":298},[278,12644,1403],{"class":302},[278,12646,12647,12649,12651],{"class":280,"line":1949},[278,12648,1409],{"class":302},[278,12650,1412],{"class":333},[278,12652,12653],{"class":302},"(error);\n",[278,12655,12656,12658,12660],{"class":280,"line":1954},[278,12657,1426],{"class":298},[278,12659,3957],{"class":333},[278,12661,637],{"class":302},[278,12663,12664,12666,12668],{"class":280,"line":1959},[278,12665,3964],{"class":302},[278,12667,2779],{"class":650},[278,12669,660],{"class":302},[278,12671,12672,12674,12677],{"class":280,"line":1985},[278,12673,3974],{"class":302},[278,12675,12676],{"class":309},"'Error searching GitHub'",[278,12678,660],{"class":302},[278,12680,12681],{"class":280,"line":1990},[278,12682,1233],{"class":302},[278,12684,12685],{"class":280,"line":1997},[278,12686,1096],{"class":302},[278,12688,12689],{"class":280,"line":2006},[278,12690,2817],{"class":302},[11,12692,12693,12694,12696,12697,12699,12700,183],{},"The function is a straightforward GitHub API search using the ",[59,12695,10921],{}," library. The endpoint and parameters are passed from the ",[59,12698,12181],{},", and we make the request to GitHub’s API, fetching the appropriate data. You can learn more about the GitHub search APIs from the official ",[47,12701,12704],{"href":12702,"rel":12703},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Frest\u002Fsearch\u002Fsearch?apiVersion=2022-11-28",[51],"GitHub Docs",[32,12706,12708],{"id":12707},"_3-generating-the-final-response","3. Generating the Final Response",[11,12710,12711],{},"Once the GitHub API returns its results, the final step is to package them into a user-friendly response. The AI processes the raw data—whether commits, issues, repos, or users—and generates readable summaries. Here’s the relevant code snippet:",[269,12713,12715],{"className":271,"code":12714,"language":273,"meta":274,"style":274},"if (tool_calls) {\n  \u002F\u002F ...\n\n  for (const toolCall of toolCalls) {\n    \u002F\u002F ...\n\n    messages.push({\n      tool_call_id: toolCall.id,\n      role: 'tool',\n      content: JSON.stringify(functionResponse),\n    });\n  }\n\n  const finalResponse = await openai.chat.completions.create({\n    model: MODEL,\n    messages: messages,\n  });\n\n  return finalResponse.choices[0].message.content;\n}\n\n\u002F\u002F ..\n",[59,12716,12717,12725,12730,12734,12749,12753,12757,12765,12770,12780,12794,12798,12802,12806,12823,12831,12836,12840,12844,12856,12860,12864],{"__ignoreMap":274},[278,12718,12719,12722],{"class":280,"line":281},[278,12720,12721],{"class":298},"if",[278,12723,12724],{"class":302}," (tool_calls) {\n",[278,12726,12727],{"class":280,"line":288},[278,12728,12729],{"class":284},"  \u002F\u002F ...\n",[278,12731,12732],{"class":280,"line":295},[278,12733,292],{"emptyLinePlaceholder":291},[278,12735,12736,12739,12741,12743,12745,12747],{"class":280,"line":316},[278,12737,12738],{"class":298},"  for",[278,12740,1245],{"class":302},[278,12742,5416],{"class":298},[278,12744,12019],{"class":650},[278,12746,12022],{"class":298},[278,12748,12025],{"class":302},[278,12750,12751],{"class":280,"line":322},[278,12752,896],{"class":284},[278,12754,12755],{"class":280,"line":327},[278,12756,292],{"emptyLinePlaceholder":291},[278,12758,12759,12761,12763],{"class":280,"line":340},[278,12760,11998],{"class":302},[278,12762,6524],{"class":333},[278,12764,637],{"class":302},[278,12766,12767],{"class":280,"line":349},[278,12768,12769],{"class":302},"      tool_call_id: toolCall.id,\n",[278,12771,12772,12775,12778],{"class":280,"line":375},[278,12773,12774],{"class":302},"      role: ",[278,12776,12777],{"class":309},"'tool'",[278,12779,660],{"class":302},[278,12781,12782,12785,12787,12789,12791],{"class":280,"line":386},[278,12783,12784],{"class":302},"      content: ",[278,12786,2230],{"class":650},[278,12788,183],{"class":302},[278,12790,2235],{"class":333},[278,12792,12793],{"class":302},"(functionResponse),\n",[278,12795,12796],{"class":280,"line":397},[278,12797,1233],{"class":302},[278,12799,12800],{"class":280,"line":408},[278,12801,1096],{"class":302},[278,12803,12804],{"class":280,"line":433},[278,12805,292],{"emptyLinePlaceholder":291},[278,12807,12808,12810,12813,12815,12817,12819,12821],{"class":280,"line":454},[278,12809,758],{"class":298},[278,12811,12812],{"class":650}," finalResponse",[278,12814,764],{"class":298},[278,12816,1120],{"class":298},[278,12818,11910],{"class":302},[278,12820,11913],{"class":333},[278,12822,637],{"class":302},[278,12824,12825,12827,12829],{"class":280,"line":475},[278,12826,11920],{"class":302},[278,12828,11923],{"class":650},[278,12830,660],{"class":302},[278,12832,12833],{"class":280,"line":496},[278,12834,12835],{"class":302},"    messages: messages,\n",[278,12837,12838],{"class":280,"line":505},[278,12839,2037],{"class":302},[278,12841,12842],{"class":280,"line":516},[278,12843,292],{"emptyLinePlaceholder":291},[278,12845,12846,12848,12851,12853],{"class":280,"line":527},[278,12847,343],{"class":298},[278,12849,12850],{"class":302}," finalResponse.choices[",[278,12852,2012],{"class":650},[278,12854,12855],{"class":302},"].message.content;\n",[278,12857,12858],{"class":280,"line":533},[278,12859,617],{"class":302},[278,12861,12862],{"class":280,"line":539},[278,12863,292],{"emptyLinePlaceholder":291},[278,12865,12866],{"class":280,"line":545},[278,12867,5698],{"class":284},[11,12869,12870],{},"In the code above, you can see how we take the API response and push it to the messages array, preparing it for the AI to format into a concise response that’s easy for the user to understand.",[24,12872,12874],{"id":12873},"managing-github-api-rate-limits","Managing GitHub API Rate Limits",[11,12876,12877],{},"If you’ve used the GitHub API (or any third-party API), you’ll know that most of them come with rate limits. So, how can we minimize GitHub API calls? The answer is simple: we avoid making duplicate calls by caching every GitHub response. If a user requests the same information that another user (or even themselves) asked for earlier, we pull the data from the cache instead of making another API call.",[11,12879,12880,12881,12886,12887,12889],{},"Now, the question is: how do we implement this in Nuxt? It’s actually quite easy, thanks to ",[47,12882,12885],{"href":12883,"rel":12884},"https:\u002F\u002Fnitro.unjs.io\u002Fguide\u002Fcache#cached-functions",[51],"Nitro’s Cached Functions"," (Nitro is an open source framework to build web servers which Nuxt uses internally). Let’s take a look at the revised ",[59,12888,11437],{}," function below:",[269,12891,12893],{"className":271,"code":12892,"language":273,"meta":274,"style":274},"export const searchGithub = defineCachedFunction(\n  async (\n    event: H3Event,\n    endpoint: string,\n    params: Omit\u003CSearchParams, 'endpoint'>\n  ) => {\n    if (!endpoint || !allowedEndpoints[endpoint as EndpointType]) {\n      throw createError({\n        statusCode: 404,\n        message: 'Endpoint not supported',\n      });\n    }\n\n    const octokit = useOctokit();\n    const endpointToUse = allowedEndpoints[endpoint as EndpointType];\n\n    try {\n      const response = await octokit.request(endpointToUse as string, params);\n\n      return response.data;\n    } catch (error) {\n      console.error(error);\n      throw createError({\n        statusCode: 500,\n        message: 'Error searching GitHub',\n      });\n    }\n  },\n  {\n    maxAge: 60 * 60, \u002F\u002F 1 hour\n    group: 'github',\n    name: 'search',\n    getKey: (\n      event: H3Event,\n      endpoint: string,\n      params: Omit\u003CSearchParams, 'endpoint'>\n    ) => {\n      const q = params.q.trim().toLowerCase().split(' ');\n\n      const mainQuery = q.filter((term) => !term.includes(':')).join(' ');\n      const qualifiers = q.filter((term) => term.includes(':')).sort();\n\n      const finalQuery = [...(mainQuery ? [mainQuery] : []), ...qualifiers]\n        .join(' ')\n        .replace(\u002F[\\s:]\u002Fg, '_');\n\n      let key = endpoint + '_q_' + finalQuery;\n\n      if (params.per_page) {\n        key += '_per_page_' + params.per_page;\n      }\n\n      if (params.order) {\n        key += '_order_' + params.order;\n      }\n\n      if (params.sort) {\n        key += '_sort_' + params.sort;\n      }\n\n      return key;\n    },\n  }\n);\n",[59,12894,12895,12910,12917,12922,12927,12941,12950,12972,12980,12989,12998,13002,13006,13010,13022,13038,13042,13048,13070,13074,13080,13088,13096,13104,13112,13120,13124,13128,13132,13136,13153,13163,13173,13181,13192,13203,13222,13231,13263,13267,13315,13352,13356,13389,13402,13429,13433,13456,13460,13467,13482,13486,13490,13497,13511,13515,13519,13526,13540,13544,13548,13555,13559,13563],{"__ignoreMap":274},[278,12896,12897,12899,12901,12903,12905,12908],{"class":280,"line":281},[278,12898,628],{"class":298},[278,12900,4559],{"class":298},[278,12902,12085],{"class":650},[278,12904,764],{"class":298},[278,12906,12907],{"class":333}," defineCachedFunction",[278,12909,770],{"class":302},[278,12911,12912,12915],{"class":280,"line":288},[278,12913,12914],{"class":333},"  async",[278,12916,346],{"class":302},[278,12918,12919],{"class":280,"line":295},[278,12920,12921],{"class":302},"    event: H3Event,\n",[278,12923,12924],{"class":280,"line":316},[278,12925,12926],{"class":302},"    endpoint: string,\n",[278,12928,12929,12932,12934,12937,12939],{"class":280,"line":322},[278,12930,12931],{"class":302},"    params: Omit",[278,12933,1702],{"class":298},[278,12935,12936],{"class":302},"SearchParams, ",[278,12938,11676],{"class":309},[278,12940,372],{"class":298},[278,12942,12943,12946,12948],{"class":280,"line":327},[278,12944,12945],{"class":302},"  ) ",[278,12947,1848],{"class":298},[278,12949,876],{"class":302},[278,12951,12952,12954,12956,12958,12960,12962,12964,12966,12968,12970],{"class":280,"line":340},[278,12953,1242],{"class":298},[278,12955,1245],{"class":302},[278,12957,1209],{"class":298},[278,12959,12506],{"class":302},[278,12961,5954],{"class":298},[278,12963,6192],{"class":298},[278,12965,12513],{"class":302},[278,12967,2937],{"class":298},[278,12969,12340],{"class":333},[278,12971,12520],{"class":302},[278,12973,12974,12976,12978],{"class":280,"line":349},[278,12975,1255],{"class":298},[278,12977,3957],{"class":333},[278,12979,637],{"class":302},[278,12981,12982,12985,12987],{"class":280,"line":375},[278,12983,12984],{"class":302},"        statusCode: ",[278,12986,12535],{"class":650},[278,12988,660],{"class":302},[278,12990,12991,12994,12996],{"class":280,"line":386},[278,12992,12993],{"class":302},"        message: ",[278,12995,12544],{"class":309},[278,12997,660],{"class":302},[278,12999,13000],{"class":280,"line":397},[278,13001,5148],{"class":302},[278,13003,13004],{"class":280,"line":408},[278,13005,1285],{"class":302},[278,13007,13008],{"class":280,"line":433},[278,13009,292],{"emptyLinePlaceholder":291},[278,13011,13012,13014,13016,13018,13020],{"class":280,"line":454},[278,13013,1112],{"class":298},[278,13015,12565],{"class":650},[278,13017,764],{"class":298},[278,13019,12380],{"class":333},[278,13021,1313],{"class":302},[278,13023,13024,13026,13028,13030,13032,13034,13036],{"class":280,"line":475},[278,13025,1112],{"class":298},[278,13027,12578],{"class":650},[278,13029,764],{"class":298},[278,13031,12583],{"class":302},[278,13033,2937],{"class":298},[278,13035,12340],{"class":333},[278,13037,11714],{"class":302},[278,13039,13040],{"class":280,"line":496},[278,13041,292],{"emptyLinePlaceholder":291},[278,13043,13044,13046],{"class":280,"line":505},[278,13045,6319],{"class":298},[278,13047,876],{"class":302},[278,13049,13050,13052,13054,13056,13058,13060,13062,13064,13066,13068],{"class":280,"line":516},[278,13051,2461],{"class":298},[278,13053,1115],{"class":650},[278,13055,764],{"class":298},[278,13057,1120],{"class":298},[278,13059,12612],{"class":302},[278,13061,12615],{"class":333},[278,13063,12618],{"class":302},[278,13065,2937],{"class":298},[278,13067,963],{"class":650},[278,13069,12625],{"class":302},[278,13071,13072],{"class":280,"line":527},[278,13073,292],{"emptyLinePlaceholder":291},[278,13075,13076,13078],{"class":280,"line":533},[278,13077,1942],{"class":298},[278,13079,12636],{"class":302},[278,13081,13082,13084,13086],{"class":280,"line":539},[278,13083,6636],{"class":302},[278,13085,1400],{"class":298},[278,13087,1403],{"class":302},[278,13089,13090,13092,13094],{"class":280,"line":545},[278,13091,1919],{"class":302},[278,13093,1412],{"class":333},[278,13095,12653],{"class":302},[278,13097,13098,13100,13102],{"class":280,"line":551},[278,13099,1255],{"class":298},[278,13101,3957],{"class":333},[278,13103,637],{"class":302},[278,13105,13106,13108,13110],{"class":280,"line":557},[278,13107,12984],{"class":302},[278,13109,2779],{"class":650},[278,13111,660],{"class":302},[278,13113,13114,13116,13118],{"class":280,"line":567},[278,13115,12993],{"class":302},[278,13117,12676],{"class":309},[278,13119,660],{"class":302},[278,13121,13122],{"class":280,"line":577},[278,13123,5148],{"class":302},[278,13125,13126],{"class":280,"line":587},[278,13127,1285],{"class":302},[278,13129,13130],{"class":280,"line":597},[278,13131,683],{"class":302},[278,13133,13134],{"class":280,"line":608},[278,13135,11470],{"class":302},[278,13137,13138,13141,13144,13146,13148,13150],{"class":280,"line":614},[278,13139,13140],{"class":302},"    maxAge: ",[278,13142,13143],{"class":650},"60",[278,13145,1363],{"class":298},[278,13147,1366],{"class":650},[278,13149,1708],{"class":302},[278,13151,13152],{"class":284},"\u002F\u002F 1 hour\n",[278,13154,13155,13158,13161],{"class":280,"line":620},[278,13156,13157],{"class":302},"    group: ",[278,13159,13160],{"class":309},"'github'",[278,13162,660],{"class":302},[278,13164,13165,13168,13171],{"class":280,"line":625},[278,13166,13167],{"class":302},"    name: ",[278,13169,13170],{"class":309},"'search'",[278,13172,660],{"class":302},[278,13174,13175,13178],{"class":280,"line":640},[278,13176,13177],{"class":333},"    getKey",[278,13179,13180],{"class":302},": (\n",[278,13182,13183,13186,13188,13190],{"class":280,"line":663},[278,13184,13185],{"class":501},"      event",[278,13187,960],{"class":298},[278,13189,11852],{"class":333},[278,13191,660],{"class":302},[278,13193,13194,13197,13199,13201],{"class":280,"line":669},[278,13195,13196],{"class":501},"      endpoint",[278,13198,960],{"class":298},[278,13200,963],{"class":650},[278,13202,660],{"class":302},[278,13204,13205,13208,13210,13212,13214,13216,13218,13220],{"class":280,"line":680},[278,13206,13207],{"class":501},"      params",[278,13209,960],{"class":298},[278,13211,12476],{"class":333},[278,13213,1702],{"class":302},[278,13215,12481],{"class":333},[278,13217,1708],{"class":302},[278,13219,11676],{"class":309},[278,13221,372],{"class":302},[278,13223,13224,13227,13229],{"class":280,"line":686},[278,13225,13226],{"class":302},"    ) ",[278,13228,1848],{"class":298},[278,13230,876],{"class":302},[278,13232,13233,13235,13238,13240,13243,13246,13248,13251,13253,13256,13258,13261],{"class":280,"line":1334},[278,13234,2461],{"class":298},[278,13236,13237],{"class":650}," q",[278,13239,764],{"class":298},[278,13241,13242],{"class":302}," params.q.",[278,13244,13245],{"class":333},"trim",[278,13247,4036],{"class":302},[278,13249,13250],{"class":333},"toLowerCase",[278,13252,4036],{"class":302},[278,13254,13255],{"class":333},"split",[278,13257,1126],{"class":302},[278,13259,13260],{"class":309},"' '",[278,13262,1280],{"class":302},[278,13264,13265],{"class":280,"line":1375},[278,13266,292],{"emptyLinePlaceholder":291},[278,13268,13269,13271,13274,13276,13279,13281,13283,13286,13288,13290,13292,13295,13298,13300,13303,13306,13309,13311,13313],{"class":280,"line":1381},[278,13270,2461],{"class":298},[278,13272,13273],{"class":650}," mainQuery",[278,13275,764],{"class":298},[278,13277,13278],{"class":302}," q.",[278,13280,2076],{"class":333},[278,13282,2079],{"class":302},[278,13284,13285],{"class":501},"term",[278,13287,1845],{"class":302},[278,13289,1848],{"class":298},[278,13291,6192],{"class":298},[278,13293,13294],{"class":302},"term.",[278,13296,13297],{"class":333},"includes",[278,13299,1126],{"class":302},[278,13301,13302],{"class":309},"':'",[278,13304,13305],{"class":302},")).",[278,13307,13308],{"class":333},"join",[278,13310,1126],{"class":302},[278,13312,13260],{"class":309},[278,13314,1280],{"class":302},[278,13316,13317,13319,13322,13324,13326,13328,13330,13332,13334,13336,13339,13341,13343,13345,13347,13350],{"class":280,"line":1386},[278,13318,2461],{"class":298},[278,13320,13321],{"class":650}," qualifiers",[278,13323,764],{"class":298},[278,13325,13278],{"class":302},[278,13327,2076],{"class":333},[278,13329,2079],{"class":302},[278,13331,13285],{"class":501},[278,13333,1845],{"class":302},[278,13335,1848],{"class":298},[278,13337,13338],{"class":302}," term.",[278,13340,13297],{"class":333},[278,13342,1126],{"class":302},[278,13344,13302],{"class":309},[278,13346,13305],{"class":302},[278,13348,13349],{"class":333},"sort",[278,13351,1313],{"class":302},[278,13353,13354],{"class":280,"line":1394},[278,13355,292],{"emptyLinePlaceholder":291},[278,13357,13358,13360,13363,13365,13368,13371,13374,13376,13379,13381,13384,13386],{"class":280,"line":1406},[278,13359,2461],{"class":298},[278,13361,13362],{"class":650}," finalQuery",[278,13364,764],{"class":298},[278,13366,13367],{"class":302}," [",[278,13369,13370],{"class":298},"...",[278,13372,13373],{"class":302},"(mainQuery ",[278,13375,5114],{"class":298},[278,13377,13378],{"class":302}," [mainQuery] ",[278,13380,960],{"class":298},[278,13382,13383],{"class":302}," []), ",[278,13385,13370],{"class":298},[278,13387,13388],{"class":302},"qualifiers]\n",[278,13390,13391,13394,13396,13398,13400],{"class":280,"line":1423},[278,13392,13393],{"class":302},"        .",[278,13395,13308],{"class":333},[278,13397,1126],{"class":302},[278,13399,13260],{"class":309},[278,13401,4590],{"class":302},[278,13403,13404,13406,13409,13411,13414,13417,13419,13422,13424,13427],{"class":280,"line":1432},[278,13405,13393],{"class":302},[278,13407,13408],{"class":333},"replace",[278,13410,1126],{"class":302},[278,13412,13413],{"class":309},"\u002F",[278,13415,13416],{"class":650},"[\\s:]",[278,13418,13413],{"class":309},[278,13420,13421],{"class":298},"g",[278,13423,1708],{"class":302},[278,13425,13426],{"class":309},"'_'",[278,13428,1280],{"class":302},[278,13430,13431],{"class":280,"line":1437},[278,13432,292],{"emptyLinePlaceholder":291},[278,13434,13435,13438,13441,13443,13446,13448,13451,13453],{"class":280,"line":1916},[278,13436,13437],{"class":298},"      let",[278,13439,13440],{"class":302}," key ",[278,13442,358],{"class":298},[278,13444,13445],{"class":302}," endpoint ",[278,13447,1345],{"class":298},[278,13449,13450],{"class":309}," '_q_'",[278,13452,4619],{"class":298},[278,13454,13455],{"class":302}," finalQuery;\n",[278,13457,13458],{"class":280,"line":1939},[278,13459,292],{"emptyLinePlaceholder":291},[278,13461,13462,13464],{"class":280,"line":1949},[278,13463,6207],{"class":298},[278,13465,13466],{"class":302}," (params.per_page) {\n",[278,13468,13469,13472,13474,13477,13479],{"class":280,"line":1954},[278,13470,13471],{"class":302},"        key ",[278,13473,6271],{"class":298},[278,13475,13476],{"class":309}," '_per_page_'",[278,13478,4619],{"class":298},[278,13480,13481],{"class":302}," params.per_page;\n",[278,13483,13484],{"class":280,"line":1959},[278,13485,6234],{"class":302},[278,13487,13488],{"class":280,"line":1985},[278,13489,292],{"emptyLinePlaceholder":291},[278,13491,13492,13494],{"class":280,"line":1990},[278,13493,6207],{"class":298},[278,13495,13496],{"class":302}," (params.order) {\n",[278,13498,13499,13501,13503,13506,13508],{"class":280,"line":1997},[278,13500,13471],{"class":302},[278,13502,6271],{"class":298},[278,13504,13505],{"class":309}," '_order_'",[278,13507,4619],{"class":298},[278,13509,13510],{"class":302}," params.order;\n",[278,13512,13513],{"class":280,"line":2006},[278,13514,6234],{"class":302},[278,13516,13517],{"class":280,"line":2018},[278,13518,292],{"emptyLinePlaceholder":291},[278,13520,13521,13523],{"class":280,"line":2029},[278,13522,6207],{"class":298},[278,13524,13525],{"class":302}," (params.sort) {\n",[278,13527,13528,13530,13532,13535,13537],{"class":280,"line":2034},[278,13529,13471],{"class":302},[278,13531,6271],{"class":298},[278,13533,13534],{"class":309}," '_sort_'",[278,13536,4619],{"class":298},[278,13538,13539],{"class":302}," params.sort;\n",[278,13541,13542],{"class":280,"line":2040},[278,13543,6234],{"class":302},[278,13545,13546],{"class":280,"line":2045},[278,13547,292],{"emptyLinePlaceholder":291},[278,13549,13550,13552],{"class":280,"line":2068},[278,13551,1942],{"class":298},[278,13553,13554],{"class":302}," key;\n",[278,13556,13557],{"class":280,"line":2099},[278,13558,2243],{"class":302},[278,13560,13561],{"class":280,"line":6428},[278,13562,1096],{"class":302},[278,13564,13565],{"class":280,"line":6439},[278,13566,1280],{"class":302},[11,13568,13569,13570,13573,13574,13577,13578,13581,13582,13586],{},"We’ve modified our earlier function to use ",[59,13571,13572],{},"cachedFunction",", and added ",[59,13575,13576],{},"H3Event"," (from the ",[59,13579,13580],{},"\u002Fchat"," API endpoint call) as the first parameter—this is needed because the app is deployed on the edge with Cloudflare (more details ",[47,13583,3286],{"href":13584,"rel":13585},"https:\u002F\u002Fnitro.unjs.io\u002Fguide\u002Fcache#edge-workers",[51],"). The most important part here is how we create a unique cache key. Here’s a breakdown:",[123,13588,13589,13610,13620],{},[74,13590,13591,13594,13595,13598,13599,13602,13603,13605,13606,13609],{},[94,13592,13593],{},"Query Qualifiers",": First, we break down the ",[59,13596,13597],{},"q"," parameter into its qualifier pairs (e.g., for finding the first PR of a GitHub user like ",[59,13600,13601],{},"ra-jeev","—yes, that’s me—the ",[59,13604,13597],{}," value would be ",[59,13607,13608],{},"author:ra-jeev type:pr","). Sometimes, it may contain the direct query string too, and the code accounts for this.",[74,13611,13612,13615,13616,13619],{},[94,13613,13614],{},"Sorting Qualifiers",": Next, we sort the qualifiers alphabetically. This is because the AI could create the same query as ",[59,13617,13618],{},"type:pr author:ra-jeev",". We could handle this in the system prompt, but why over-complicate things for the AI?",[74,13621,13622,13625],{},[94,13623,13624],{},"Creating the Cache Key",": Finally, we combine all the qualifiers and other parameters into a single string, separated by underscores.",[11,13627,13628,13629,13632,13633,13635,13636,13638,13639,13642,13643,183],{},"We set the cache duration to 1 hour, as seen in the ",[59,13630,13631],{},"maxAge"," setting, which means all ",[59,13634,12185],{}," responses are stored for that time. To use cache in ",[59,13637,3194],{}," production we’d already enabled ",[59,13640,13641],{},"cache: true"," in our ",[59,13644,3490],{},[11,13646,13647],{},"Now that we’ve tackled rate limiting, it’s time to shift our focus to response streaming. In the next section, we’ll explore how to implement streaming for a more seamless and efficient user experience.",[24,13649,13651],{"id":13650},"adding-ai-response-streaming","Adding AI Response Streaming",[11,13653,13654,13655,13657],{},"Enabling AI response streaming is usually straightforward: you pass a parameter when making the API call, and the AI returns the response as a stream. In our ",[94,13656,10815],{}," project, for example, we handled the stream chunks directly client-side, ensuring that responses trickled in smoothly for the user.",[11,13659,13660,13661,13664,13665,13667],{},"But in the case of ",[94,13662,13663],{},"Chat GitHub",", there’s an additional complexity—",[94,13666,12181],{}," responses. So, we have two approaches to manage:",[123,13669,13670,13673],{},[74,13671,13672],{},"Enable streaming only for the final response generation, but this limits us when there’s no tool_call, wasting an opportunity to stream from the start.",[74,13674,13675,13676,13678],{},"Enable stream response for both the OpenAI calls, but this requires us to manage a stream more carefully on the server side (as we need to handle the ",[59,13677,12181],{},", if any, there itself).",[11,13680,13681,13682],{},"I took the seemingly complex path and went ahead with the second approach—",[3061,13683,13684],{},"”Two roads diverged in a wood, and I—I took the one less traveled by, And that has made all the difference.”",[32,13686,13688],{"id":13687},"enable-streaming-in-openai-calls","Enable Streaming in OpenAI Calls",[11,13690,13691],{},"Here’s how the OpenAI API call was modified to enable streaming:",[269,13693,13695],{"className":271,"code":13694,"language":273,"meta":274,"style":274},"export const handleMessageWithOpenAI = async function* (\n  event: H3Event,\n  messages: OpenAI.ChatCompletionMessageParam[],\n) {\n  const openai = useOpenAI();\n  const responseStream = await openai.chat.completions.create({\n    model: MODEL,\n    messages,\n    tools,\n    stream: true, \u002F\u002F enable streaming\n  });\n\n  const currentToolCalls: OpenAI.ChatCompletionMessageToolCall[] = [];\n\n  for await (const chunk of responseStream) {\n    const choice = chunk.choices[0];\n\n    \u002F\u002F if it is normal text chunk just yield it\n    if (choice.delta.content) {\n      yield choice.delta.content;\n    }\n\n    \u002F\u002F if the delta contains tool_calls, then collect all chunks\n    if (choice.delta.tool_calls) {\n      for (const toolCall of choice.delta.tool_calls) {\n        if (toolCall.index !== undefined) {\n          if (!currentToolCalls[toolCall.index]) {\n            currentToolCalls[toolCall.index] = {\n              id: toolCall.id || '',\n              type: 'function' as const,\n              function: {\n                name: toolCall.function?.name || '',\n                arguments: '',\n              },\n            };\n          }\n\n          if (toolCall.function?.arguments) {\n            currentToolCalls[toolCall.index].function.arguments +=\n              toolCall.function.arguments;\n          }\n        }\n      }\n    }\n\n    \u002F\u002F once it has returned all chunks with tool_calls, we will\n    \u002F\u002F get the final finish_reason as 'tool_calls'. We can call\n    \u002F\u002F the mentioned tool now\n    if (choice.finish_reason === 'tool_calls') {\n      messages.push({\n        role: 'assistant',\n        tool_calls: currentToolCalls,\n      });\n\n      for (const toolCall of currentToolCalls) {\n        if (toolCall.function.name === 'searchGithub') {\n          try {\n            const functionArgs = JSON.parse(toolCall.function.arguments);\n            const toolResult = await searchGithub(\n              event,\n              functionArgs.endpoint,\n              {\n                q: functionArgs.q,\n                sort: functionArgs.sort,\n                order: functionArgs.order,\n                per_page: functionArgs.per_page,\n              }\n            );\n\n            messages.push({\n              role: 'tool',\n              tool_call_id: toolCall.id,\n              content: JSON.stringify(toolResult),\n            });\n          } catch (error) {\n            console.error('Error parsing tool call arguments:', error);\n            throw error;\n          }\n        }\n      }\n\n      try {\n        const finalResponse = await openai.chat.completions.create({\n          model: MODEL,\n          messages,\n          stream: true,\n        });\n\n        for await (const chunk of finalResponse) {\n          if (chunk.choices[0].delta.content) {\n            yield chunk.choices[0].delta.content;\n          }\n        }\n      } catch (error) {\n        console.error(\n          'Error generating final response or saving user query :',\n          error\n        );\n\n        throw error;\n      }\n    }\n  }\n};\n",[59,13696,13697,13714,13724,13739,13743,13755,13772,13780,13785,13790,13802,13806,13810,13832,13836,13854,13870,13874,13879,13886,13894,13898,13902,13907,13914,13930,13943,13955,13964,13976,13989,13994,14005,14015,14020,14025,14029,14033,14040,14048,14053,14057,14061,14065,14069,14073,14078,14083,14088,14102,14111,14121,14126,14130,14134,14149,14162,14169,14186,14201,14206,14211,14216,14221,14226,14231,14236,14240,14245,14249,14258,14267,14272,14286,14291,14300,14314,14321,14325,14329,14333,14337,14344,14360,14369,14374,14383,14387,14391,14409,14421,14433,14437,14441,14450,14459,14466,14471,14475,14479,14486,14490,14494,14498],{"__ignoreMap":274},[278,13698,13699,13701,13703,13705,13707,13709,13712],{"class":280,"line":281},[278,13700,628],{"class":298},[278,13702,4559],{"class":298},[278,13704,11836],{"class":333},[278,13706,764],{"class":298},[278,13708,2325],{"class":298},[278,13710,13711],{"class":298}," function*",[278,13713,346],{"class":302},[278,13715,13716,13718,13720,13722],{"class":280,"line":288},[278,13717,11847],{"class":501},[278,13719,960],{"class":298},[278,13721,11852],{"class":333},[278,13723,660],{"class":302},[278,13725,13726,13728,13730,13732,13734,13736],{"class":280,"line":295},[278,13727,11859],{"class":501},[278,13729,960],{"class":298},[278,13731,11454],{"class":333},[278,13733,183],{"class":302},[278,13735,11873],{"class":333},[278,13737,13738],{"class":302},"[],\n",[278,13740,13741],{"class":280,"line":316},[278,13742,1718],{"class":302},[278,13744,13745,13747,13749,13751,13753],{"class":280,"line":322},[278,13746,758],{"class":298},[278,13748,11891],{"class":650},[278,13750,764],{"class":298},[278,13752,11765],{"class":333},[278,13754,1313],{"class":302},[278,13756,13757,13759,13762,13764,13766,13768,13770],{"class":280,"line":327},[278,13758,758],{"class":298},[278,13760,13761],{"class":650}," responseStream",[278,13763,764],{"class":298},[278,13765,1120],{"class":298},[278,13767,11910],{"class":302},[278,13769,11913],{"class":333},[278,13771,637],{"class":302},[278,13773,13774,13776,13778],{"class":280,"line":340},[278,13775,11920],{"class":302},[278,13777,11923],{"class":650},[278,13779,660],{"class":302},[278,13781,13782],{"class":280,"line":349},[278,13783,13784],{"class":302},"    messages,\n",[278,13786,13787],{"class":280,"line":375},[278,13788,13789],{"class":302},"    tools,\n",[278,13791,13792,13795,13797,13799],{"class":280,"line":386},[278,13793,13794],{"class":302},"    stream: ",[278,13796,2931],{"class":650},[278,13798,1708],{"class":302},[278,13800,13801],{"class":284},"\u002F\u002F enable streaming\n",[278,13803,13804],{"class":280,"line":397},[278,13805,2037],{"class":302},[278,13807,13808],{"class":280,"line":408},[278,13809,292],{"emptyLinePlaceholder":291},[278,13811,13812,13814,13817,13819,13821,13823,13826,13828,13830],{"class":280,"line":433},[278,13813,758],{"class":298},[278,13815,13816],{"class":650}," currentToolCalls",[278,13818,960],{"class":298},[278,13820,11454],{"class":333},[278,13822,183],{"class":302},[278,13824,13825],{"class":333},"ChatCompletionMessageToolCall",[278,13827,1971],{"class":302},[278,13829,358],{"class":298},[278,13831,6483],{"class":302},[278,13833,13834],{"class":280,"line":454},[278,13835,292],{"emptyLinePlaceholder":291},[278,13837,13838,13840,13842,13844,13846,13849,13851],{"class":280,"line":475},[278,13839,12738],{"class":298},[278,13841,1120],{"class":298},[278,13843,1245],{"class":302},[278,13845,5416],{"class":298},[278,13847,13848],{"class":650}," chunk",[278,13850,12022],{"class":298},[278,13852,13853],{"class":302}," responseStream) {\n",[278,13855,13856,13858,13861,13863,13866,13868],{"class":280,"line":496},[278,13857,1112],{"class":298},[278,13859,13860],{"class":650}," choice",[278,13862,764],{"class":298},[278,13864,13865],{"class":302}," chunk.choices[",[278,13867,2012],{"class":650},[278,13869,11714],{"class":302},[278,13871,13872],{"class":280,"line":505},[278,13873,292],{"emptyLinePlaceholder":291},[278,13875,13876],{"class":280,"line":516},[278,13877,13878],{"class":284},"    \u002F\u002F if it is normal text chunk just yield it\n",[278,13880,13881,13883],{"class":280,"line":527},[278,13882,1242],{"class":298},[278,13884,13885],{"class":302}," (choice.delta.content) {\n",[278,13887,13888,13891],{"class":280,"line":533},[278,13889,13890],{"class":298},"      yield",[278,13892,13893],{"class":302}," choice.delta.content;\n",[278,13895,13896],{"class":280,"line":539},[278,13897,1285],{"class":302},[278,13899,13900],{"class":280,"line":545},[278,13901,292],{"emptyLinePlaceholder":291},[278,13903,13904],{"class":280,"line":551},[278,13905,13906],{"class":284},"    \u002F\u002F if the delta contains tool_calls, then collect all chunks\n",[278,13908,13909,13911],{"class":280,"line":557},[278,13910,1242],{"class":298},[278,13912,13913],{"class":302}," (choice.delta.tool_calls) {\n",[278,13915,13916,13919,13921,13923,13925,13927],{"class":280,"line":567},[278,13917,13918],{"class":298},"      for",[278,13920,1245],{"class":302},[278,13922,5416],{"class":298},[278,13924,12019],{"class":650},[278,13926,12022],{"class":298},[278,13928,13929],{"class":302}," choice.delta.tool_calls) {\n",[278,13931,13932,13934,13937,13939,13941],{"class":280,"line":577},[278,13933,6926],{"class":298},[278,13935,13936],{"class":302}," (toolCall.index ",[278,13938,2092],{"class":298},[278,13940,6151],{"class":650},[278,13942,1718],{"class":302},[278,13944,13945,13948,13950,13952],{"class":280,"line":587},[278,13946,13947],{"class":298},"          if",[278,13949,1245],{"class":302},[278,13951,1209],{"class":298},[278,13953,13954],{"class":302},"currentToolCalls[toolCall.index]) {\n",[278,13956,13957,13960,13962],{"class":280,"line":597},[278,13958,13959],{"class":302},"            currentToolCalls[toolCall.index] ",[278,13961,358],{"class":298},[278,13963,876],{"class":302},[278,13965,13966,13969,13971,13974],{"class":280,"line":608},[278,13967,13968],{"class":302},"              id: toolCall.id ",[278,13970,5954],{"class":298},[278,13972,13973],{"class":309}," ''",[278,13975,660],{"class":302},[278,13977,13978,13981,13983,13985,13987],{"class":280,"line":614},[278,13979,13980],{"class":302},"              type: ",[278,13982,11478],{"class":309},[278,13984,4841],{"class":298},[278,13986,4559],{"class":298},[278,13988,660],{"class":302},[278,13990,13991],{"class":280,"line":620},[278,13992,13993],{"class":302},"              function: {\n",[278,13995,13996,13999,14001,14003],{"class":280,"line":625},[278,13997,13998],{"class":302},"                name: toolCall.function?.name ",[278,14000,5954],{"class":298},[278,14002,13973],{"class":309},[278,14004,660],{"class":302},[278,14006,14007,14010,14013],{"class":280,"line":640},[278,14008,14009],{"class":302},"                arguments: ",[278,14011,14012],{"class":309},"''",[278,14014,660],{"class":302},[278,14016,14017],{"class":280,"line":663},[278,14018,14019],{"class":302},"              },\n",[278,14021,14022],{"class":280,"line":669},[278,14023,14024],{"class":302},"            };\n",[278,14026,14027],{"class":280,"line":680},[278,14028,12122],{"class":302},[278,14030,14031],{"class":280,"line":686},[278,14032,292],{"emptyLinePlaceholder":291},[278,14034,14035,14037],{"class":280,"line":1334},[278,14036,13947],{"class":298},[278,14038,14039],{"class":302}," (toolCall.function?.arguments) {\n",[278,14041,14042,14045],{"class":280,"line":1375},[278,14043,14044],{"class":302},"            currentToolCalls[toolCall.index].function.arguments ",[278,14046,14047],{"class":298},"+=\n",[278,14049,14050],{"class":280,"line":1381},[278,14051,14052],{"class":302},"              toolCall.function.arguments;\n",[278,14054,14055],{"class":280,"line":1386},[278,14056,12122],{"class":302},[278,14058,14059],{"class":280,"line":1394},[278,14060,6954],{"class":302},[278,14062,14063],{"class":280,"line":1406},[278,14064,6234],{"class":302},[278,14066,14067],{"class":280,"line":1423},[278,14068,1285],{"class":302},[278,14070,14071],{"class":280,"line":1432},[278,14072,292],{"emptyLinePlaceholder":291},[278,14074,14075],{"class":280,"line":1437},[278,14076,14077],{"class":284},"    \u002F\u002F once it has returned all chunks with tool_calls, we will\n",[278,14079,14080],{"class":280,"line":1916},[278,14081,14082],{"class":284},"    \u002F\u002F get the final finish_reason as 'tool_calls'. We can call\n",[278,14084,14085],{"class":280,"line":1939},[278,14086,14087],{"class":284},"    \u002F\u002F the mentioned tool now\n",[278,14089,14090,14092,14095,14097,14100],{"class":280,"line":1949},[278,14091,1242],{"class":298},[278,14093,14094],{"class":302}," (choice.finish_reason ",[278,14096,2451],{"class":298},[278,14098,14099],{"class":309}," 'tool_calls'",[278,14101,1718],{"class":302},[278,14103,14104,14107,14109],{"class":280,"line":1954},[278,14105,14106],{"class":302},"      messages.",[278,14108,6524],{"class":333},[278,14110,637],{"class":302},[278,14112,14113,14116,14119],{"class":280,"line":1959},[278,14114,14115],{"class":302},"        role: ",[278,14117,14118],{"class":309},"'assistant'",[278,14120,660],{"class":302},[278,14122,14123],{"class":280,"line":1985},[278,14124,14125],{"class":302},"        tool_calls: currentToolCalls,\n",[278,14127,14128],{"class":280,"line":1990},[278,14129,5148],{"class":302},[278,14131,14132],{"class":280,"line":1997},[278,14133,292],{"emptyLinePlaceholder":291},[278,14135,14136,14138,14140,14142,14144,14146],{"class":280,"line":2006},[278,14137,13918],{"class":298},[278,14139,1245],{"class":302},[278,14141,5416],{"class":298},[278,14143,12019],{"class":650},[278,14145,12022],{"class":298},[278,14147,14148],{"class":302}," currentToolCalls) {\n",[278,14150,14151,14153,14156,14158,14160],{"class":280,"line":2018},[278,14152,6926],{"class":298},[278,14154,14155],{"class":302}," (toolCall.function.name ",[278,14157,2451],{"class":298},[278,14159,12049],{"class":309},[278,14161,1718],{"class":302},[278,14163,14164,14167],{"class":280,"line":2029},[278,14165,14166],{"class":298},"          try",[278,14168,876],{"class":302},[278,14170,14171,14174,14176,14178,14180,14182,14184],{"class":280,"line":2034},[278,14172,14173],{"class":298},"            const",[278,14175,12058],{"class":650},[278,14177,764],{"class":298},[278,14179,12063],{"class":650},[278,14181,183],{"class":302},[278,14183,12068],{"class":333},[278,14185,12071],{"class":302},[278,14187,14188,14190,14193,14195,14197,14199],{"class":280,"line":2040},[278,14189,14173],{"class":298},[278,14191,14192],{"class":650}," toolResult",[278,14194,764],{"class":298},[278,14196,1120],{"class":298},[278,14198,12085],{"class":333},[278,14200,770],{"class":302},[278,14202,14203],{"class":280,"line":2045},[278,14204,14205],{"class":302},"              event,\n",[278,14207,14208],{"class":280,"line":2068},[278,14209,14210],{"class":302},"              functionArgs.endpoint,\n",[278,14212,14213],{"class":280,"line":2099},[278,14214,14215],{"class":302},"              {\n",[278,14217,14218],{"class":280,"line":6428},[278,14219,14220],{"class":302},"                q: functionArgs.q,\n",[278,14222,14223],{"class":280,"line":6439},[278,14224,14225],{"class":302},"                sort: functionArgs.sort,\n",[278,14227,14228],{"class":280,"line":6450},[278,14229,14230],{"class":302},"                order: functionArgs.order,\n",[278,14232,14233],{"class":280,"line":6455},[278,14234,14235],{"class":302},"                per_page: functionArgs.per_page,\n",[278,14237,14238],{"class":280,"line":6460},[278,14239,548],{"class":302},[278,14241,14242],{"class":280,"line":6475},[278,14243,14244],{"class":302},"            );\n",[278,14246,14247],{"class":280,"line":6486},[278,14248,292],{"emptyLinePlaceholder":291},[278,14250,14251,14254,14256],{"class":280,"line":6491},[278,14252,14253],{"class":302},"            messages.",[278,14255,6524],{"class":333},[278,14257,637],{"class":302},[278,14259,14260,14263,14265],{"class":280,"line":6518},[278,14261,14262],{"class":302},"              role: ",[278,14264,12777],{"class":309},[278,14266,660],{"class":302},[278,14268,14269],{"class":280,"line":6530},[278,14270,14271],{"class":302},"              tool_call_id: toolCall.id,\n",[278,14273,14274,14277,14279,14281,14283],{"class":280,"line":6542},[278,14275,14276],{"class":302},"              content: ",[278,14278,2230],{"class":650},[278,14280,183],{"class":302},[278,14282,2235],{"class":333},[278,14284,14285],{"class":302},"(toolResult),\n",[278,14287,14288],{"class":280,"line":6547},[278,14289,14290],{"class":302},"            });\n",[278,14292,14293,14296,14298],{"class":280,"line":6552},[278,14294,14295],{"class":302},"          } ",[278,14297,1400],{"class":298},[278,14299,1403],{"class":302},[278,14301,14302,14305,14307,14309,14312],{"class":280,"line":6567},[278,14303,14304],{"class":302},"            console.",[278,14306,1412],{"class":333},[278,14308,1126],{"class":302},[278,14310,14311],{"class":309},"'Error parsing tool call arguments:'",[278,14313,1420],{"class":302},[278,14315,14316,14319],{"class":280,"line":6580},[278,14317,14318],{"class":298},"            throw",[278,14320,1429],{"class":302},[278,14322,14323],{"class":280,"line":6593},[278,14324,12122],{"class":302},[278,14326,14327],{"class":280,"line":6605},[278,14328,6954],{"class":302},[278,14330,14331],{"class":280,"line":6620},[278,14332,6234],{"class":302},[278,14334,14335],{"class":280,"line":6625},[278,14336,292],{"emptyLinePlaceholder":291},[278,14338,14339,14342],{"class":280,"line":6633},[278,14340,14341],{"class":298},"      try",[278,14343,876],{"class":302},[278,14345,14346,14348,14350,14352,14354,14356,14358],{"class":280,"line":6643},[278,14347,6741],{"class":298},[278,14349,12812],{"class":650},[278,14351,764],{"class":298},[278,14353,1120],{"class":298},[278,14355,11910],{"class":302},[278,14357,11913],{"class":333},[278,14359,637],{"class":302},[278,14361,14362,14365,14367],{"class":280,"line":6657},[278,14363,14364],{"class":302},"          model: ",[278,14366,11923],{"class":650},[278,14368,660],{"class":302},[278,14370,14371],{"class":280,"line":6665},[278,14372,14373],{"class":302},"          messages,\n",[278,14375,14376,14379,14381],{"class":280,"line":6670},[278,14377,14378],{"class":302},"          stream: ",[278,14380,2931],{"class":650},[278,14382,660],{"class":302},[278,14384,14385],{"class":280,"line":6675},[278,14386,8327],{"class":302},[278,14388,14389],{"class":280,"line":6680},[278,14390,292],{"emptyLinePlaceholder":291},[278,14392,14393,14396,14398,14400,14402,14404,14406],{"class":280,"line":6698},[278,14394,14395],{"class":298},"        for",[278,14397,1120],{"class":298},[278,14399,1245],{"class":302},[278,14401,5416],{"class":298},[278,14403,13848],{"class":650},[278,14405,12022],{"class":298},[278,14407,14408],{"class":302}," finalResponse) {\n",[278,14410,14411,14413,14416,14418],{"class":280,"line":6725},[278,14412,13947],{"class":298},[278,14414,14415],{"class":302}," (chunk.choices[",[278,14417,2012],{"class":650},[278,14419,14420],{"class":302},"].delta.content) {\n",[278,14422,14423,14426,14428,14430],{"class":280,"line":6738},[278,14424,14425],{"class":298},"            yield",[278,14427,13865],{"class":302},[278,14429,2012],{"class":650},[278,14431,14432],{"class":302},"].delta.content;\n",[278,14434,14435],{"class":280,"line":6752},[278,14436,12122],{"class":302},[278,14438,14439],{"class":280,"line":6769},[278,14440,6954],{"class":302},[278,14442,14443,14446,14448],{"class":280,"line":6786},[278,14444,14445],{"class":302},"      } ",[278,14447,1400],{"class":298},[278,14449,1403],{"class":302},[278,14451,14452,14455,14457],{"class":280,"line":6798},[278,14453,14454],{"class":302},"        console.",[278,14456,1412],{"class":333},[278,14458,770],{"class":302},[278,14460,14461,14464],{"class":280,"line":6803},[278,14462,14463],{"class":309},"          'Error generating final response or saving user query :'",[278,14465,660],{"class":302},[278,14467,14468],{"class":280,"line":6815},[278,14469,14470],{"class":302},"          error\n",[278,14472,14473],{"class":280,"line":6827},[278,14474,12127],{"class":302},[278,14476,14477],{"class":280,"line":6839},[278,14478,292],{"emptyLinePlaceholder":291},[278,14480,14481,14484],{"class":280,"line":6844},[278,14482,14483],{"class":298},"        throw",[278,14485,1429],{"class":302},[278,14487,14488],{"class":280,"line":6853},[278,14489,6234],{"class":302},[278,14491,14492],{"class":280,"line":6859},[278,14493,1285],{"class":302},[278,14495,14496],{"class":280,"line":6864},[278,14497,1096],{"class":302},[278,14499,14500],{"class":280,"line":6877},[278,14501,2817],{"class":302},[11,14503,14504,14505,14508],{},"The revised ",[59,14506,14507],{},"handleMessageWithOpenAI"," function works like this:",[71,14510,14511,14517,14526,14536,14554],{},[74,14512,14513,14516],{},[94,14514,14515],{},"Converted it to an AsyncGenerator",": This allows the function to yield data chunks progressively as they are received.",[74,14518,14519,11160,14522,14525],{},[94,14520,14521],{},"Added",[59,14523,14524],{},"stream: true"," to both OpenAI API calls: This tells OpenAI to stream the response back to us.",[74,14527,14528,14531,14532,14535],{},[94,14529,14530],{},"Yielding Response Chunks",": For each chunk of text that we get from the stream, we simply ",[59,14533,14534],{},"yield"," it to the caller.",[74,14537,14538,14541,14542,14544,14545,14548,14549,14551,14552,183],{},[94,14539,14540],{},"Handling tool_calls",": Since streaming is enabled, the ",[59,14543,12181],{}," information also arrives in chunks. We collect these chunks until the OpenAI API signals the completion of this part (",[59,14546,14547],{},"finish_reason === 'tool_calls'","), and then invoke the ",[59,14550,12185],{}," function using the parameters provided by the ",[59,14553,12181],{},[74,14555,14556,14559,14560,14562],{},[94,14557,14558],{},"Final Response",": After the GitHub search is done, we ",[59,14561,14534],{}," the response in chunks in the same way.",[32,14564,14566],{"id":14565},"converting-to-a-readablestream","Converting to a ReadableStream",[11,14568,14569,14570,14572,14573,14576],{},"To complete the process, the chunks from ",[59,14571,14507],{}," are converted into a ",[94,14574,14575],{},"ReadableStream"," format, which is then returned to the client (not shown here). Here’s how that works:",[269,14578,14580],{"className":271,"code":14579,"language":273,"meta":274,"style":274},"export const asyncGeneratorToStream = (\n  asyncGenerator: AsyncGenerator\u003Cstring, void, unknown>\n) => {\n  let cancelled = false;\n  const encoder = new TextEncoder();\n  const stream = new ReadableStream({\n    async start(controller) {\n      try {\n        for await (const value of asyncGenerator) {\n          if (cancelled) {\n            break;\n          }\n\n          controller.enqueue(\n            encoder.encode(`data: ${JSON.stringify({ response: value })}\\n\\n`)\n          );\n        }\n\n        \u002F\u002F Send done to signal end of stream\n        controller.enqueue(encoder.encode(`data: [DONE]\\n\\n`));\n\n        controller.close();\n      } catch (err) {\n        console.log('Error in stream:', err);\n\n        \u002F* eslint-disable @stylistic\u002Foperator-linebreak *\u002F\n        const errorMessage =\n          err instanceof Error\n            ? err.message\n            : 'An error occurred in the stream';\n        \u002F* eslint-enable @stylistic\u002Foperator-linebreak *\u002F\n\n        controller.enqueue(\n          encoder.encode(\n            `event: error\\ndata: ${JSON.stringify({\n              message: errorMessage,\n            })}\\n\\n`\n          )\n        );\n\n        controller.close();\n      }\n    },\n    cancel(reason) {\n      console.log('Client closed connection. Reason:', reason);\n      cancelled = true;\n    },\n  });\n\n  return stream;\n};\n",[59,14581,14582,14595,14621,14629,14642,14658,14673,14688,14694,14712,14719,14726,14730,14734,14744,14783,14788,14792,14796,14801,14825,14829,14837,14845,14859,14863,14868,14878,14888,14896,14906,14911,14915,14923,14932,14951,14961,14973,14978,14982,14986,14994,14998,15002,15014,15028,15039,15043,15047,15051,15058],{"__ignoreMap":274},[278,14583,14584,14586,14588,14591,14593],{"class":280,"line":281},[278,14585,628],{"class":298},[278,14587,4559],{"class":298},[278,14589,14590],{"class":333}," asyncGeneratorToStream",[278,14592,764],{"class":298},[278,14594,346],{"class":302},[278,14596,14597,14600,14602,14605,14607,14609,14611,14614,14616,14619],{"class":280,"line":288},[278,14598,14599],{"class":501},"  asyncGenerator",[278,14601,960],{"class":298},[278,14603,14604],{"class":333}," AsyncGenerator",[278,14606,1702],{"class":302},[278,14608,1705],{"class":650},[278,14610,1708],{"class":302},[278,14612,14613],{"class":650},"void",[278,14615,1708],{"class":302},[278,14617,14618],{"class":650},"unknown",[278,14620,372],{"class":302},[278,14622,14623,14625,14627],{"class":280,"line":295},[278,14624,1845],{"class":302},[278,14626,1848],{"class":298},[278,14628,876],{"class":302},[278,14630,14631,14633,14636,14638,14640],{"class":280,"line":316},[278,14632,6050],{"class":298},[278,14634,14635],{"class":302}," cancelled ",[278,14637,358],{"class":298},[278,14639,6872],{"class":650},[278,14641,313],{"class":302},[278,14643,14644,14646,14649,14651,14653,14656],{"class":280,"line":322},[278,14645,758],{"class":298},[278,14647,14648],{"class":650}," encoder",[278,14650,764],{"class":298},[278,14652,1258],{"class":298},[278,14654,14655],{"class":333}," TextEncoder",[278,14657,1313],{"class":302},[278,14659,14660,14662,14664,14666,14668,14671],{"class":280,"line":327},[278,14661,758],{"class":298},[278,14663,6328],{"class":650},[278,14665,764],{"class":298},[278,14667,1258],{"class":298},[278,14669,14670],{"class":333}," ReadableStream",[278,14672,637],{"class":302},[278,14674,14675,14678,14681,14683,14686],{"class":280,"line":340},[278,14676,14677],{"class":298},"    async",[278,14679,14680],{"class":333}," start",[278,14682,1126],{"class":302},[278,14684,14685],{"class":501},"controller",[278,14687,1718],{"class":302},[278,14689,14690,14692],{"class":280,"line":349},[278,14691,14341],{"class":298},[278,14693,876],{"class":302},[278,14695,14696,14698,14700,14702,14704,14707,14709],{"class":280,"line":375},[278,14697,14395],{"class":298},[278,14699,1120],{"class":298},[278,14701,1245],{"class":302},[278,14703,5416],{"class":298},[278,14705,14706],{"class":650}," value",[278,14708,12022],{"class":298},[278,14710,14711],{"class":302}," asyncGenerator) {\n",[278,14713,14714,14716],{"class":280,"line":386},[278,14715,13947],{"class":298},[278,14717,14718],{"class":302}," (cancelled) {\n",[278,14720,14721,14724],{"class":280,"line":397},[278,14722,14723],{"class":298},"            break",[278,14725,313],{"class":302},[278,14727,14728],{"class":280,"line":408},[278,14729,12122],{"class":302},[278,14731,14732],{"class":280,"line":433},[278,14733,292],{"emptyLinePlaceholder":291},[278,14735,14736,14739,14742],{"class":280,"line":454},[278,14737,14738],{"class":302},"          controller.",[278,14740,14741],{"class":333},"enqueue",[278,14743,770],{"class":302},[278,14745,14746,14749,14752,14754,14757,14759,14761,14763,14766,14769,14772,14775,14778,14781],{"class":280,"line":475},[278,14747,14748],{"class":302},"            encoder.",[278,14750,14751],{"class":333},"encode",[278,14753,1126],{"class":302},[278,14755,14756],{"class":309},"`data: ${",[278,14758,2230],{"class":650},[278,14760,183],{"class":309},[278,14762,2235],{"class":333},[278,14764,14765],{"class":309},"({ response: ",[278,14767,14768],{"class":302},"value",[278,14770,14771],{"class":309}," })",[278,14773,14774],{"class":309},"}",[278,14776,14777],{"class":650},"\\n\\n",[278,14779,14780],{"class":309},"`",[278,14782,4590],{"class":302},[278,14784,14785],{"class":280,"line":496},[278,14786,14787],{"class":302},"          );\n",[278,14789,14790],{"class":280,"line":505},[278,14791,6954],{"class":302},[278,14793,14794],{"class":280,"line":516},[278,14795,292],{"emptyLinePlaceholder":291},[278,14797,14798],{"class":280,"line":527},[278,14799,14800],{"class":284},"        \u002F\u002F Send done to signal end of stream\n",[278,14802,14803,14806,14808,14811,14813,14815,14818,14820,14822],{"class":280,"line":533},[278,14804,14805],{"class":302},"        controller.",[278,14807,14741],{"class":333},[278,14809,14810],{"class":302},"(encoder.",[278,14812,14751],{"class":333},[278,14814,1126],{"class":302},[278,14816,14817],{"class":309},"`data: [DONE]",[278,14819,14777],{"class":650},[278,14821,14780],{"class":309},[278,14823,14824],{"class":302},"));\n",[278,14826,14827],{"class":280,"line":539},[278,14828,292],{"emptyLinePlaceholder":291},[278,14830,14831,14833,14835],{"class":280,"line":545},[278,14832,14805],{"class":302},[278,14834,6968],{"class":333},[278,14836,1313],{"class":302},[278,14838,14839,14841,14843],{"class":280,"line":551},[278,14840,14445],{"class":302},[278,14842,1400],{"class":298},[278,14844,4095],{"class":302},[278,14846,14847,14849,14852,14854,14857],{"class":280,"line":557},[278,14848,14454],{"class":302},[278,14850,14851],{"class":333},"log",[278,14853,1126],{"class":302},[278,14855,14856],{"class":309},"'Error in stream:'",[278,14858,4109],{"class":302},[278,14860,14861],{"class":280,"line":567},[278,14862,292],{"emptyLinePlaceholder":291},[278,14864,14865],{"class":280,"line":577},[278,14866,14867],{"class":284},"        \u002F* eslint-disable @stylistic\u002Foperator-linebreak *\u002F\n",[278,14869,14870,14872,14875],{"class":280,"line":587},[278,14871,6741],{"class":298},[278,14873,14874],{"class":650}," errorMessage",[278,14876,14877],{"class":298}," =\n",[278,14879,14880,14883,14885],{"class":280,"line":597},[278,14881,14882],{"class":302},"          err ",[278,14884,2747],{"class":298},[278,14886,14887],{"class":333}," Error\n",[278,14889,14890,14893],{"class":280,"line":608},[278,14891,14892],{"class":298},"            ?",[278,14894,14895],{"class":302}," err.message\n",[278,14897,14898,14901,14904],{"class":280,"line":614},[278,14899,14900],{"class":298},"            :",[278,14902,14903],{"class":309}," 'An error occurred in the stream'",[278,14905,313],{"class":302},[278,14907,14908],{"class":280,"line":620},[278,14909,14910],{"class":284},"        \u002F* eslint-enable @stylistic\u002Foperator-linebreak *\u002F\n",[278,14912,14913],{"class":280,"line":625},[278,14914,292],{"emptyLinePlaceholder":291},[278,14916,14917,14919,14921],{"class":280,"line":640},[278,14918,14805],{"class":302},[278,14920,14741],{"class":333},[278,14922,770],{"class":302},[278,14924,14925,14928,14930],{"class":280,"line":663},[278,14926,14927],{"class":302},"          encoder.",[278,14929,14751],{"class":333},[278,14931,770],{"class":302},[278,14933,14934,14937,14940,14943,14945,14947,14949],{"class":280,"line":669},[278,14935,14936],{"class":309},"            `event: error",[278,14938,14939],{"class":650},"\\n",[278,14941,14942],{"class":309},"data: ${",[278,14944,2230],{"class":650},[278,14946,183],{"class":309},[278,14948,2235],{"class":333},[278,14950,637],{"class":309},[278,14952,14953,14956,14959],{"class":280,"line":680},[278,14954,14955],{"class":309},"              message: ",[278,14957,14958],{"class":302},"errorMessage",[278,14960,660],{"class":309},[278,14962,14963,14966,14968,14970],{"class":280,"line":686},[278,14964,14965],{"class":309},"            })",[278,14967,14774],{"class":309},[278,14969,14777],{"class":650},[278,14971,14972],{"class":309},"`\n",[278,14974,14975],{"class":280,"line":1334},[278,14976,14977],{"class":302},"          )\n",[278,14979,14980],{"class":280,"line":1375},[278,14981,12127],{"class":302},[278,14983,14984],{"class":280,"line":1381},[278,14985,292],{"emptyLinePlaceholder":291},[278,14987,14988,14990,14992],{"class":280,"line":1386},[278,14989,14805],{"class":302},[278,14991,6968],{"class":333},[278,14993,1313],{"class":302},[278,14995,14996],{"class":280,"line":1394},[278,14997,6234],{"class":302},[278,14999,15000],{"class":280,"line":1406},[278,15001,2243],{"class":302},[278,15003,15004,15007,15009,15012],{"class":280,"line":1423},[278,15005,15006],{"class":333},"    cancel",[278,15008,1126],{"class":302},[278,15010,15011],{"class":501},"reason",[278,15013,1718],{"class":302},[278,15015,15016,15018,15020,15022,15025],{"class":280,"line":1432},[278,15017,1919],{"class":302},[278,15019,14851],{"class":333},[278,15021,1126],{"class":302},[278,15023,15024],{"class":309},"'Client closed connection. Reason:'",[278,15026,15027],{"class":302},", reason);\n",[278,15029,15030,15033,15035,15037],{"class":280,"line":1437},[278,15031,15032],{"class":302},"      cancelled ",[278,15034,358],{"class":298},[278,15036,6575],{"class":650},[278,15038,313],{"class":302},[278,15040,15041],{"class":280,"line":1916},[278,15042,2243],{"class":302},[278,15044,15045],{"class":280,"line":1939},[278,15046,2037],{"class":302},[278,15048,15049],{"class":280,"line":1949},[278,15050,292],{"emptyLinePlaceholder":291},[278,15052,15053,15055],{"class":280,"line":1954},[278,15054,343],{"class":298},[278,15056,15057],{"class":302}," stream;\n",[278,15059,15060],{"class":280,"line":1959},[278,15061,2817],{"class":302},[11,15063,15064,15065,15068,15069,15071],{},"This code transforms the ",[59,15066,15067],{},"AsyncGenerator"," we created earlier into a ",[94,15070,14575],{},". Here’s a breakdown of what’s happening:",[71,15073,15074,15082,15091,15105,15115],{},[74,15075,15076,15078,15079,15081],{},[94,15077,15067],{},": We pass the AsyncGenerator from ",[59,15080,14507],{}," as an argument to this function.",[74,15083,15084,15087,15088,15090],{},[94,15085,15086],{},"Creating a ReadableStream",": Inside the ",[59,15089,6610],{}," method of the ReadableStream, we wait for chunks from the AsyncGenerator.",[74,15092,15093,15096,15097,15101,15102,183],{},[94,15094,15095],{},"Formatting Chunks",": For each text chunk received, we format it according to the Server-Sent Events (SSE) convention (You can read more about SSE in my ",[47,15098,10825],{"href":15099,"rel":15100},"https:\u002F\u002Frajeev.dev\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui#heading-consuming-server-sent-events",[51],"). Each chunk is wrapped in this format: ",[59,15103,15104],{},"data: ${JSON.stringify({ response: value })}\\n\\n",[74,15106,15107,15110,15111,15114],{},[94,15108,15109],{},"Encoding the Stream",": Using ",[59,15112,15113],{},"TextEncoder",", we encode the chunks before sending them to the client. This encoding step is crucial, especially in production environments, as it ensures that the client correctly receives the stream in real-time.",[74,15116,15117,15120,15121,15124],{},[94,15118,15119],{},"Error Handling",": If an error occurs (as we throw errors from the AsyncGenerator), we send an ",[59,15122,15123],{},"event: error"," message to the client, signaling that something went wrong, and then close the stream to terminate the connection cleanly.",[11,15126,15127],{},"And then this stream is returned to the client from the API endpoint",[269,15129,15131],{"className":271,"code":15130,"language":273,"meta":274,"style":274},"\u002F\u002F inside \u002Fapi\u002Fchat event handler\nreturn asyncGeneratorToStream(\n  handleMessageWithOpenAI(event, llmMessages)\n);\n",[59,15132,15133,15138,15147,15155],{"__ignoreMap":274},[278,15134,15135],{"class":280,"line":281},[278,15136,15137],{"class":284},"\u002F\u002F inside \u002Fapi\u002Fchat event handler\n",[278,15139,15140,15143,15145],{"class":280,"line":288},[278,15141,15142],{"class":298},"return",[278,15144,14590],{"class":333},[278,15146,770],{"class":302},[278,15148,15149,15152],{"class":280,"line":295},[278,15150,15151],{"class":333},"  handleMessageWithOpenAI",[278,15153,15154],{"class":302},"(event, llmMessages)\n",[278,15156,15157],{"class":280,"line":316},[278,15158,1280],{"class":302},[32,15160,15162],{"id":15161},"handling-the-stream-on-the-client-side","Handling the Stream on the Client-Side",[11,15164,15165,15166,15169],{},"To handle the streamed response, we’ll use a refactored version of the ",[59,15167,15168],{},"useChat"," composable, initially created for the Hub Chat project. This time, we’ll extend it to handle error events. Here's the updated code:",[269,15171,15173],{"className":271,"code":15172,"language":273,"meta":274,"style":274},"export function useChat(apiBase: string, body: Record\u003Cstring, unknown>) {\n  async function* chat(): AsyncGenerator\u003Cstring, void, unknown> {\n    try {\n      const response = await $fetch(apiBase, {\n        method: 'POST',\n        body,\n        responseType: 'stream',\n      });\n\n      let buffer = '';\n      const reader = (response as ReadableStream)\n        .pipeThrough(new TextDecoderStream())\n        .getReader();\n\n      while (true) {\n        const { value, done } = await reader.read();\n\n        if (done) {\n          if (buffer.trim()) {\n            console.warn('Stream ended with unparsed data:', buffer);\n          }\n\n          return;\n        }\n\n        buffer += value;\n        const messages = buffer.split('\\n\\n');\n        buffer = messages.pop() || '';\n\n        for (const message of messages) {\n          const lines = message.split('\\n');\n          let event = '';\n          let data = '';\n\n          for (const line of lines) {\n            if (line.startsWith('event:')) {\n              event = line.slice('event:'.length).trim();\n            } else if (line.startsWith('data:')) {\n              data = line.slice('data:'.length).trim();\n            }\n          }\n\n          if (event === 'error') {\n            const parsedError = JSON.parse(data);\n            console.error('Stream error:', parsedError);\n\n            throw new Error(\n              parsedError.message ?? 'Failed to generate response'\n            );\n          } else if (data) {\n            if (data === '[DONE]') return;\n\n            try {\n              const jsonData = JSON.parse(data);\n              if (jsonData.response) {\n                yield jsonData.response;\n              }\n            } catch (parseError) {\n              console.warn('Error parsing JSON:', parseError);\n            }\n          }\n        }\n      }\n    } catch (error) {\n      console.error('Error sending message:', error);\n      throw error;\n    }\n  }\n\n  return chat;\n}\n",[59,15174,15175,15213,15244,15250,15266,15276,15281,15291,15295,15299,15312,15330,15347,15356,15360,15371,15398,15402,15409,15420,15435,15439,15443,15450,15454,15458,15468,15493,15513,15517,15533,15557,15571,15584,15588,15605,15624,15652,15674,15699,15704,15708,15712,15726,15744,15758,15762,15772,15783,15787,15798,15816,15820,15827,15845,15853,15861,15865,15874,15889,15893,15897,15901,15905,15913,15926,15932,15936,15940,15944,15951],{"__ignoreMap":274},[278,15176,15177,15179,15181,15184,15186,15189,15191,15193,15195,15198,15200,15202,15204,15206,15208,15210],{"class":280,"line":281},[278,15178,628],{"class":298},[278,15180,748],{"class":298},[278,15182,15183],{"class":333}," useChat",[278,15185,1126],{"class":302},[278,15187,15188],{"class":501},"apiBase",[278,15190,960],{"class":298},[278,15192,963],{"class":650},[278,15194,1708],{"class":302},[278,15196,15197],{"class":501},"body",[278,15199,960],{"class":298},[278,15201,1699],{"class":333},[278,15203,1702],{"class":302},[278,15205,1705],{"class":650},[278,15207,1708],{"class":302},[278,15209,14618],{"class":650},[278,15211,15212],{"class":302},">) {\n",[278,15214,15215,15217,15219,15222,15225,15227,15229,15231,15233,15235,15237,15239,15241],{"class":280,"line":288},[278,15216,12914],{"class":298},[278,15218,13711],{"class":298},[278,15220,15221],{"class":333}," chat",[278,15223,15224],{"class":302},"()",[278,15226,960],{"class":298},[278,15228,14604],{"class":333},[278,15230,1702],{"class":302},[278,15232,1705],{"class":650},[278,15234,1708],{"class":302},[278,15236,14613],{"class":650},[278,15238,1708],{"class":302},[278,15240,14618],{"class":650},[278,15242,15243],{"class":302},"> {\n",[278,15245,15246,15248],{"class":280,"line":295},[278,15247,6319],{"class":298},[278,15249,876],{"class":302},[278,15251,15252,15254,15256,15258,15260,15263],{"class":280,"line":316},[278,15253,2461],{"class":298},[278,15255,1115],{"class":650},[278,15257,764],{"class":298},[278,15259,1120],{"class":298},[278,15261,15262],{"class":333}," $fetch",[278,15264,15265],{"class":302},"(apiBase, {\n",[278,15267,15268,15271,15274],{"class":280,"line":322},[278,15269,15270],{"class":302},"        method: ",[278,15272,15273],{"class":309},"'POST'",[278,15275,660],{"class":302},[278,15277,15278],{"class":280,"line":327},[278,15279,15280],{"class":302},"        body,\n",[278,15282,15283,15286,15289],{"class":280,"line":340},[278,15284,15285],{"class":302},"        responseType: ",[278,15287,15288],{"class":309},"'stream'",[278,15290,660],{"class":302},[278,15292,15293],{"class":280,"line":349},[278,15294,5148],{"class":302},[278,15296,15297],{"class":280,"line":375},[278,15298,292],{"emptyLinePlaceholder":291},[278,15300,15301,15303,15306,15308,15310],{"class":280,"line":386},[278,15302,13437],{"class":298},[278,15304,15305],{"class":302}," buffer ",[278,15307,358],{"class":298},[278,15309,13973],{"class":309},[278,15311,313],{"class":302},[278,15313,15314,15316,15319,15321,15324,15326,15328],{"class":280,"line":397},[278,15315,2461],{"class":298},[278,15317,15318],{"class":650}," reader",[278,15320,764],{"class":298},[278,15322,15323],{"class":302}," (response ",[278,15325,2937],{"class":298},[278,15327,14670],{"class":333},[278,15329,4590],{"class":302},[278,15331,15332,15334,15337,15339,15341,15344],{"class":280,"line":408},[278,15333,13393],{"class":302},[278,15335,15336],{"class":333},"pipeThrough",[278,15338,1126],{"class":302},[278,15340,1173],{"class":298},[278,15342,15343],{"class":333}," TextDecoderStream",[278,15345,15346],{"class":302},"())\n",[278,15348,15349,15351,15354],{"class":280,"line":433},[278,15350,13393],{"class":302},[278,15352,15353],{"class":333},"getReader",[278,15355,1313],{"class":302},[278,15357,15358],{"class":280,"line":454},[278,15359,292],{"emptyLinePlaceholder":291},[278,15361,15362,15365,15367,15369],{"class":280,"line":475},[278,15363,15364],{"class":298},"      while",[278,15366,1245],{"class":302},[278,15368,2931],{"class":650},[278,15370,1718],{"class":302},[278,15372,15373,15375,15377,15379,15381,15384,15386,15388,15390,15393,15396],{"class":280,"line":496},[278,15374,6741],{"class":298},[278,15376,1009],{"class":302},[278,15378,14768],{"class":650},[278,15380,1708],{"class":302},[278,15382,15383],{"class":650},"done",[278,15385,1029],{"class":302},[278,15387,358],{"class":298},[278,15389,1120],{"class":298},[278,15391,15392],{"class":302}," reader.",[278,15394,15395],{"class":333},"read",[278,15397,1313],{"class":302},[278,15399,15400],{"class":280,"line":505},[278,15401,292],{"emptyLinePlaceholder":291},[278,15403,15404,15406],{"class":280,"line":516},[278,15405,6926],{"class":298},[278,15407,15408],{"class":302}," (done) {\n",[278,15410,15411,15413,15416,15418],{"class":280,"line":527},[278,15412,13947],{"class":298},[278,15414,15415],{"class":302}," (buffer.",[278,15417,13245],{"class":333},[278,15419,1083],{"class":302},[278,15421,15422,15424,15427,15429,15432],{"class":280,"line":533},[278,15423,14304],{"class":302},[278,15425,15426],{"class":333},"warn",[278,15428,1126],{"class":302},[278,15430,15431],{"class":309},"'Stream ended with unparsed data:'",[278,15433,15434],{"class":302},", buffer);\n",[278,15436,15437],{"class":280,"line":539},[278,15438,12122],{"class":302},[278,15440,15441],{"class":280,"line":545},[278,15442,292],{"emptyLinePlaceholder":291},[278,15444,15445,15448],{"class":280,"line":551},[278,15446,15447],{"class":298},"          return",[278,15449,313],{"class":302},[278,15451,15452],{"class":280,"line":557},[278,15453,6954],{"class":302},[278,15455,15456],{"class":280,"line":567},[278,15457,292],{"emptyLinePlaceholder":291},[278,15459,15460,15463,15465],{"class":280,"line":577},[278,15461,15462],{"class":302},"        buffer ",[278,15464,6271],{"class":298},[278,15466,15467],{"class":302}," value;\n",[278,15469,15470,15472,15475,15477,15480,15482,15484,15487,15489,15491],{"class":280,"line":587},[278,15471,6741],{"class":298},[278,15473,15474],{"class":650}," messages",[278,15476,764],{"class":298},[278,15478,15479],{"class":302}," buffer.",[278,15481,13255],{"class":333},[278,15483,1126],{"class":302},[278,15485,15486],{"class":309},"'",[278,15488,14777],{"class":650},[278,15490,15486],{"class":309},[278,15492,1280],{"class":302},[278,15494,15495,15497,15499,15502,15505,15507,15509,15511],{"class":280,"line":597},[278,15496,15462],{"class":302},[278,15498,358],{"class":298},[278,15500,15501],{"class":302}," messages.",[278,15503,15504],{"class":333},"pop",[278,15506,1342],{"class":302},[278,15508,5954],{"class":298},[278,15510,13973],{"class":309},[278,15512,313],{"class":302},[278,15514,15515],{"class":280,"line":608},[278,15516,292],{"emptyLinePlaceholder":291},[278,15518,15519,15521,15523,15525,15528,15530],{"class":280,"line":614},[278,15520,14395],{"class":298},[278,15522,1245],{"class":302},[278,15524,5416],{"class":298},[278,15526,15527],{"class":650}," message",[278,15529,12022],{"class":298},[278,15531,15532],{"class":302}," messages) {\n",[278,15534,15535,15537,15540,15542,15545,15547,15549,15551,15553,15555],{"class":280,"line":620},[278,15536,6772],{"class":298},[278,15538,15539],{"class":650}," lines",[278,15541,764],{"class":298},[278,15543,15544],{"class":302}," message.",[278,15546,13255],{"class":333},[278,15548,1126],{"class":302},[278,15550,15486],{"class":309},[278,15552,14939],{"class":650},[278,15554,15486],{"class":309},[278,15556,1280],{"class":302},[278,15558,15559,15562,15565,15567,15569],{"class":280,"line":625},[278,15560,15561],{"class":298},"          let",[278,15563,15564],{"class":302}," event ",[278,15566,358],{"class":298},[278,15568,13973],{"class":309},[278,15570,313],{"class":302},[278,15572,15573,15575,15578,15580,15582],{"class":280,"line":640},[278,15574,15561],{"class":298},[278,15576,15577],{"class":302}," data ",[278,15579,358],{"class":298},[278,15581,13973],{"class":309},[278,15583,313],{"class":302},[278,15585,15586],{"class":280,"line":663},[278,15587,292],{"emptyLinePlaceholder":291},[278,15589,15590,15593,15595,15597,15600,15602],{"class":280,"line":669},[278,15591,15592],{"class":298},"          for",[278,15594,1245],{"class":302},[278,15596,5416],{"class":298},[278,15598,15599],{"class":650}," line",[278,15601,12022],{"class":298},[278,15603,15604],{"class":302}," lines) {\n",[278,15606,15607,15610,15613,15616,15618,15621],{"class":280,"line":680},[278,15608,15609],{"class":298},"            if",[278,15611,15612],{"class":302}," (line.",[278,15614,15615],{"class":333},"startsWith",[278,15617,1126],{"class":302},[278,15619,15620],{"class":309},"'event:'",[278,15622,15623],{"class":302},")) {\n",[278,15625,15626,15629,15631,15634,15637,15639,15641,15643,15646,15648,15650],{"class":280,"line":686},[278,15627,15628],{"class":302},"              event ",[278,15630,358],{"class":298},[278,15632,15633],{"class":302}," line.",[278,15635,15636],{"class":333},"slice",[278,15638,1126],{"class":302},[278,15640,15620],{"class":309},[278,15642,183],{"class":302},[278,15644,15645],{"class":650},"length",[278,15647,4633],{"class":302},[278,15649,13245],{"class":333},[278,15651,1313],{"class":302},[278,15653,15654,15657,15660,15663,15665,15667,15669,15672],{"class":280,"line":1334},[278,15655,15656],{"class":302},"            } ",[278,15658,15659],{"class":298},"else",[278,15661,15662],{"class":298}," if",[278,15664,15612],{"class":302},[278,15666,15615],{"class":333},[278,15668,1126],{"class":302},[278,15670,15671],{"class":309},"'data:'",[278,15673,15623],{"class":302},[278,15675,15676,15679,15681,15683,15685,15687,15689,15691,15693,15695,15697],{"class":280,"line":1375},[278,15677,15678],{"class":302},"              data ",[278,15680,358],{"class":298},[278,15682,15633],{"class":302},[278,15684,15636],{"class":333},[278,15686,1126],{"class":302},[278,15688,15671],{"class":309},[278,15690,183],{"class":302},[278,15692,15645],{"class":650},[278,15694,4633],{"class":302},[278,15696,13245],{"class":333},[278,15698,1313],{"class":302},[278,15700,15701],{"class":280,"line":1381},[278,15702,15703],{"class":302},"            }\n",[278,15705,15706],{"class":280,"line":1386},[278,15707,12122],{"class":302},[278,15709,15710],{"class":280,"line":1394},[278,15711,292],{"emptyLinePlaceholder":291},[278,15713,15714,15716,15719,15721,15724],{"class":280,"line":1406},[278,15715,13947],{"class":298},[278,15717,15718],{"class":302}," (event ",[278,15720,2451],{"class":298},[278,15722,15723],{"class":309}," 'error'",[278,15725,1718],{"class":302},[278,15727,15728,15730,15733,15735,15737,15739,15741],{"class":280,"line":1423},[278,15729,14173],{"class":298},[278,15731,15732],{"class":650}," parsedError",[278,15734,764],{"class":298},[278,15736,12063],{"class":650},[278,15738,183],{"class":302},[278,15740,12068],{"class":333},[278,15742,15743],{"class":302},"(data);\n",[278,15745,15746,15748,15750,15752,15755],{"class":280,"line":1432},[278,15747,14304],{"class":302},[278,15749,1412],{"class":333},[278,15751,1126],{"class":302},[278,15753,15754],{"class":309},"'Stream error:'",[278,15756,15757],{"class":302},", parsedError);\n",[278,15759,15760],{"class":280,"line":1437},[278,15761,292],{"emptyLinePlaceholder":291},[278,15763,15764,15766,15768,15770],{"class":280,"line":1916},[278,15765,14318],{"class":298},[278,15767,1258],{"class":298},[278,15769,1261],{"class":333},[278,15771,770],{"class":302},[278,15773,15774,15777,15780],{"class":280,"line":1939},[278,15775,15776],{"class":302},"              parsedError.message ",[278,15778,15779],{"class":298},"??",[278,15781,15782],{"class":309}," 'Failed to generate response'\n",[278,15784,15785],{"class":280,"line":1949},[278,15786,14244],{"class":302},[278,15788,15789,15791,15793,15795],{"class":280,"line":1954},[278,15790,14295],{"class":302},[278,15792,15659],{"class":298},[278,15794,15662],{"class":298},[278,15796,15797],{"class":302}," (data) {\n",[278,15799,15800,15802,15805,15807,15810,15812,15814],{"class":280,"line":1959},[278,15801,15609],{"class":298},[278,15803,15804],{"class":302}," (data ",[278,15806,2451],{"class":298},[278,15808,15809],{"class":309}," '[DONE]'",[278,15811,1845],{"class":302},[278,15813,15142],{"class":298},[278,15815,313],{"class":302},[278,15817,15818],{"class":280,"line":1985},[278,15819,292],{"emptyLinePlaceholder":291},[278,15821,15822,15825],{"class":280,"line":1990},[278,15823,15824],{"class":298},"            try",[278,15826,876],{"class":302},[278,15828,15829,15832,15835,15837,15839,15841,15843],{"class":280,"line":1997},[278,15830,15831],{"class":298},"              const",[278,15833,15834],{"class":650}," jsonData",[278,15836,764],{"class":298},[278,15838,12063],{"class":650},[278,15840,183],{"class":302},[278,15842,12068],{"class":333},[278,15844,15743],{"class":302},[278,15846,15847,15850],{"class":280,"line":2006},[278,15848,15849],{"class":298},"              if",[278,15851,15852],{"class":302}," (jsonData.response) {\n",[278,15854,15855,15858],{"class":280,"line":2018},[278,15856,15857],{"class":298},"                yield",[278,15859,15860],{"class":302}," jsonData.response;\n",[278,15862,15863],{"class":280,"line":2029},[278,15864,548],{"class":302},[278,15866,15867,15869,15871],{"class":280,"line":2034},[278,15868,15656],{"class":302},[278,15870,1400],{"class":298},[278,15872,15873],{"class":302}," (parseError) {\n",[278,15875,15876,15879,15881,15883,15886],{"class":280,"line":2040},[278,15877,15878],{"class":302},"              console.",[278,15880,15426],{"class":333},[278,15882,1126],{"class":302},[278,15884,15885],{"class":309},"'Error parsing JSON:'",[278,15887,15888],{"class":302},", parseError);\n",[278,15890,15891],{"class":280,"line":2045},[278,15892,15703],{"class":302},[278,15894,15895],{"class":280,"line":2068},[278,15896,12122],{"class":302},[278,15898,15899],{"class":280,"line":2099},[278,15900,6954],{"class":302},[278,15902,15903],{"class":280,"line":6428},[278,15904,6234],{"class":302},[278,15906,15907,15909,15911],{"class":280,"line":6439},[278,15908,6636],{"class":302},[278,15910,1400],{"class":298},[278,15912,1403],{"class":302},[278,15914,15915,15917,15919,15921,15924],{"class":280,"line":6450},[278,15916,1919],{"class":302},[278,15918,1412],{"class":333},[278,15920,1126],{"class":302},[278,15922,15923],{"class":309},"'Error sending message:'",[278,15925,1420],{"class":302},[278,15927,15928,15930],{"class":280,"line":6455},[278,15929,1255],{"class":298},[278,15931,1429],{"class":302},[278,15933,15934],{"class":280,"line":6460},[278,15935,1285],{"class":302},[278,15937,15938],{"class":280,"line":6475},[278,15939,1096],{"class":302},[278,15941,15942],{"class":280,"line":6486},[278,15943,292],{"emptyLinePlaceholder":291},[278,15945,15946,15948],{"class":280,"line":6491},[278,15947,343],{"class":298},[278,15949,15950],{"class":302}," chat;\n",[278,15952,15953],{"class":280,"line":6518},[278,15954,617],{"class":302},[11,15956,15957],{},"Here’s a breakdown of what the code does:",[71,15959,15960,15975,15989,15998],{},[74,15961,15962,15963,15966,15967,15970,15971,15974],{},"We use Nuxt’s ",[59,15964,15965],{},"$fetch"," to make a ",[59,15968,15969],{},"POST"," request to the API endpoint, passing the ",[59,15972,15973],{},"responseType: 'stream'",". This tells the client to expect a streaming response.",[74,15976,15977,15978,15980,15981,15984,15985,15988],{},"Once we receive the ",[59,15979,14575],{},", we create a ",[59,15982,15983],{},"streamReader"," for it. This allows us to process the chunks one at a time as they arrive. We also pass the chunks through a ",[59,15986,15987],{},"TextDecoder"," to convert the raw bytes into readable text.",[74,15990,15991,15992,919,15994,15997],{},"The stream is in Server-Sent Events (SSE) format, so we parse and handle the ",[59,15993,3887],{},[59,15995,15996],{},"data"," parts appropriately. Each data chunk is parsed and returned to the client component for rendering.",[74,15999,16000,16001,16003],{},"The code also listens for and handles any ",[59,16002,1412],{}," events that may occur, ensuring a smoother user experience by gracefully handling stream interruptions or API errors.",[11,16005,16006],{},"And this concludes the road less traveled that we took earlier. In the next section, we’ll cover how we can authenticate our users.",[24,16008,16010],{"id":16009},"user-authentication-with-github-oauth","User Authentication with GitHub OAuth",[11,16012,16013,16014,16017],{},"Now that our backend is ready to handle client requests, how do we restrict access to authenticated users? And how do we provide context to the AI, like answering a query such as, ",[3061,16015,16016],{},"“When did I make my first ever commit?”"," To control who can access the backend, we use authentication. And to give the AI context about the user, we rely on GitHub OAuth for authentication.",[11,16019,16020,16021,16023],{},"We’re leveraging ",[59,16022,10918],{},", which simplifies integrating GitHub OAuth into our Nuxt app. This allows us to authenticate users with their GitHub accounts and manage sessions effortlessly.",[32,16025,16027],{"id":16026},"server-side-implementation","Server-Side Implementation",[11,16029,16030,16031,919,16033,16035,16036,16038],{},"On the server side, we need to create a route that handles the GitHub access token when the user logs in. Make sure to create a GitHub OAuth app and add the ",[59,16032,11085],{},[59,16034,11088],{}," to the ",[59,16037,10990],{}," as mentioned in the setup section.",[11,16040,16041,16042,3717,16045,16048],{},"To implement this, create a ",[59,16043,16044],{},"github.get.ts",[59,16046,16047],{},"route\u002Fauth"," folder of the server directory with the following content:",[269,16050,16052],{"className":271,"code":16051,"language":273,"meta":274,"style":274},"export default oauthGitHubEventHandler({\n  async onSuccess(event, { user }) {\n    await setUserSession(event, {\n      user: {\n        id: user.id,\n        login: user.login,\n        name: user.name,\n        avatarUrl: user.avatar_url,\n        htmlUrl: user.html_url,\n        publicRepos: user.public_repos,\n      },\n    });\n\n    return sendRedirect(event, '\u002Fchat');\n  },\n  \u002F\u002F Optional, will return a json error and 401 status code by default\n  onError(event, error) {\n    console.error('GitHub OAuth error:', error);\n    return sendRedirect(event, '\u002F');\n  },\n});\n",[59,16053,16054,16065,16084,16093,16098,16103,16108,16113,16118,16123,16128,16132,16136,16140,16154,16158,16163,16178,16191,16204,16208],{"__ignoreMap":274},[278,16055,16056,16058,16060,16063],{"class":280,"line":281},[278,16057,628],{"class":298},[278,16059,631],{"class":298},[278,16061,16062],{"class":333}," oauthGitHubEventHandler",[278,16064,637],{"class":302},[278,16066,16067,16069,16072,16074,16076,16079,16081],{"class":280,"line":288},[278,16068,12914],{"class":298},[278,16070,16071],{"class":333}," onSuccess",[278,16073,1126],{"class":302},[278,16075,3887],{"class":501},[278,16077,16078],{"class":302},", { ",[278,16080,5022],{"class":501},[278,16082,16083],{"class":302}," }) {\n",[278,16085,16086,16088,16091],{"class":280,"line":295},[278,16087,5077],{"class":298},[278,16089,16090],{"class":333}," setUserSession",[278,16092,4264],{"class":302},[278,16094,16095],{"class":280,"line":316},[278,16096,16097],{"class":302},"      user: {\n",[278,16099,16100],{"class":280,"line":322},[278,16101,16102],{"class":302},"        id: user.id,\n",[278,16104,16105],{"class":280,"line":327},[278,16106,16107],{"class":302},"        login: user.login,\n",[278,16109,16110],{"class":280,"line":340},[278,16111,16112],{"class":302},"        name: user.name,\n",[278,16114,16115],{"class":280,"line":349},[278,16116,16117],{"class":302},"        avatarUrl: user.avatar_url,\n",[278,16119,16120],{"class":280,"line":375},[278,16121,16122],{"class":302},"        htmlUrl: user.html_url,\n",[278,16124,16125],{"class":280,"line":386},[278,16126,16127],{"class":302},"        publicRepos: user.public_repos,\n",[278,16129,16130],{"class":280,"line":397},[278,16131,1165],{"class":302},[278,16133,16134],{"class":280,"line":408},[278,16135,1233],{"class":302},[278,16137,16138],{"class":280,"line":433},[278,16139,292],{"emptyLinePlaceholder":291},[278,16141,16142,16144,16147,16149,16152],{"class":280,"line":454},[278,16143,1088],{"class":298},[278,16145,16146],{"class":333}," sendRedirect",[278,16148,5162],{"class":302},[278,16150,16151],{"class":309},"'\u002Fchat'",[278,16153,1280],{"class":302},[278,16155,16156],{"class":280,"line":475},[278,16157,683],{"class":302},[278,16159,16160],{"class":280,"line":496},[278,16161,16162],{"class":284},"  \u002F\u002F Optional, will return a json error and 401 status code by default\n",[278,16164,16165,16168,16170,16172,16174,16176],{"class":280,"line":505},[278,16166,16167],{"class":333},"  onError",[278,16169,1126],{"class":302},[278,16171,3887],{"class":501},[278,16173,1708],{"class":302},[278,16175,1412],{"class":501},[278,16177,1718],{"class":302},[278,16179,16180,16182,16184,16186,16189],{"class":280,"line":516},[278,16181,1409],{"class":302},[278,16183,1412],{"class":333},[278,16185,1126],{"class":302},[278,16187,16188],{"class":309},"'GitHub OAuth error:'",[278,16190,1420],{"class":302},[278,16192,16193,16195,16197,16199,16202],{"class":280,"line":527},[278,16194,1088],{"class":298},[278,16196,16146],{"class":333},[278,16198,5162],{"class":302},[278,16200,16201],{"class":309},"'\u002F'",[278,16203,1280],{"class":302},[278,16205,16206],{"class":280,"line":533},[278,16207,683],{"class":302},[278,16209,16210],{"class":280,"line":539},[278,16211,3693],{"class":302},[11,16213,16214,16215,16218,16219,16223,16224,16227],{},"The event handler name is important and should be ",[59,16216,16217],{},"oauthGitHubEventHandler"," (more details can be found ",[47,16220,3286],{"href":16221,"rel":16222},"https:\u002F\u002Fgithub.com\u002Fatinux\u002Fnuxt-auth-utils?tab=readme-ov-file#oauth-event-handlers",[51],"). On successful login, we call the ",[59,16225,16226],{},"setUserSession"," utility function to store the user details in an HTTP cookie and redirect them to the chat page.",[11,16229,16230,16231,16234,16235,16238],{},"For our API routes, we can then call the ",[59,16232,16233],{},"requireUserSession"," utility to ensure only authenticated users can make requests. Below is the full ",[59,16236,16237],{},"\u002Fapi\u002Fchat"," endpoint handler:",[269,16240,16242],{"className":271,"code":16241,"language":273,"meta":274,"style":274},"export default defineEventHandler(async (event) => {\n  const userSession = await requireUserSession(event);\n\n  const { messages } = await readBody(event);\n  if (!messages) {\n    throw createError({\n      statusCode: 400,\n      message: 'User messages are required',\n    });\n  }\n\n  const llmMessages = [\n    {\n      role: 'system',\n      content: getSystemPrompt(userSession.user.login),\n    },\n    ...messages,\n  ];\n\n  return asyncGeneratorToStream(\n    handleMessageWithOpenAI(event, llmMessages, userSession.user.login)\n  );\n});\n",[59,16243,16244,16266,16281,16285,16305,16316,16324,16332,16341,16345,16349,16353,16364,16368,16377,16387,16391,16399,16403,16407,16415,16423,16427],{"__ignoreMap":274},[278,16245,16246,16248,16250,16252,16254,16256,16258,16260,16262,16264],{"class":280,"line":281},[278,16247,628],{"class":298},[278,16249,631],{"class":298},[278,16251,3878],{"class":333},[278,16253,1126],{"class":302},[278,16255,1050],{"class":298},[278,16257,1245],{"class":302},[278,16259,3887],{"class":501},[278,16261,1845],{"class":302},[278,16263,1848],{"class":298},[278,16265,876],{"class":302},[278,16267,16268,16270,16273,16275,16277,16279],{"class":280,"line":288},[278,16269,758],{"class":298},[278,16271,16272],{"class":650}," userSession",[278,16274,764],{"class":298},[278,16276,1120],{"class":298},[278,16278,5031],{"class":333},[278,16280,3910],{"class":302},[278,16282,16283],{"class":280,"line":295},[278,16284,292],{"emptyLinePlaceholder":291},[278,16286,16287,16289,16291,16294,16296,16298,16300,16303],{"class":280,"line":316},[278,16288,758],{"class":298},[278,16290,1009],{"class":302},[278,16292,16293],{"class":650},"messages",[278,16295,1029],{"class":302},[278,16297,358],{"class":298},[278,16299,1120],{"class":298},[278,16301,16302],{"class":333}," readBody",[278,16304,3910],{"class":302},[278,16306,16307,16309,16311,16313],{"class":280,"line":322},[278,16308,1062],{"class":298},[278,16310,1245],{"class":302},[278,16312,1209],{"class":298},[278,16314,16315],{"class":302},"messages) {\n",[278,16317,16318,16320,16322],{"class":280,"line":327},[278,16319,1426],{"class":298},[278,16321,3957],{"class":333},[278,16323,637],{"class":302},[278,16325,16326,16328,16330],{"class":280,"line":340},[278,16327,3964],{"class":302},[278,16329,3967],{"class":650},[278,16331,660],{"class":302},[278,16333,16334,16336,16339],{"class":280,"line":349},[278,16335,3974],{"class":302},[278,16337,16338],{"class":309},"'User messages are required'",[278,16340,660],{"class":302},[278,16342,16343],{"class":280,"line":375},[278,16344,1233],{"class":302},[278,16346,16347],{"class":280,"line":386},[278,16348,1096],{"class":302},[278,16350,16351],{"class":280,"line":397},[278,16352,292],{"emptyLinePlaceholder":291},[278,16354,16355,16357,16360,16362],{"class":280,"line":408},[278,16356,758],{"class":298},[278,16358,16359],{"class":650}," llmMessages",[278,16361,764],{"class":298},[278,16363,5876],{"class":302},[278,16365,16366],{"class":280,"line":433},[278,16367,2209],{"class":302},[278,16369,16370,16372,16375],{"class":280,"line":454},[278,16371,12774],{"class":302},[278,16373,16374],{"class":309},"'system'",[278,16376,660],{"class":302},[278,16378,16379,16381,16384],{"class":280,"line":475},[278,16380,12784],{"class":302},[278,16382,16383],{"class":333},"getSystemPrompt",[278,16385,16386],{"class":302},"(userSession.user.login),\n",[278,16388,16389],{"class":280,"line":496},[278,16390,2243],{"class":302},[278,16392,16393,16396],{"class":280,"line":505},[278,16394,16395],{"class":298},"    ...",[278,16397,16398],{"class":302},"messages,\n",[278,16400,16401],{"class":280,"line":516},[278,16402,5916],{"class":302},[278,16404,16405],{"class":280,"line":527},[278,16406,292],{"emptyLinePlaceholder":291},[278,16408,16409,16411,16413],{"class":280,"line":533},[278,16410,343],{"class":298},[278,16412,14590],{"class":333},[278,16414,770],{"class":302},[278,16416,16417,16420],{"class":280,"line":539},[278,16418,16419],{"class":333},"    handleMessageWithOpenAI",[278,16421,16422],{"class":302},"(event, llmMessages, userSession.user.login)\n",[278,16424,16425],{"class":280,"line":545},[278,16426,611],{"class":302},[278,16428,16429],{"class":280,"line":551},[278,16430,3693],{"class":302},[11,16432,16433,16434,16436],{},"As you can see, we retrieve the currently logged-in GitHub user’s details and pass the login info into the system prompt. This gives OpenAI the context it needs to answer queries like, “When did I make my first commit?” The ",[59,16435,16383],{}," utility helps with that:",[269,16438,16440],{"className":271,"code":16439,"language":273,"meta":274,"style":274},"export const getSystemPrompt = (loggedInUserName: string) =>\n  systemPrompt +\n  `\\n\\nNote: The currently logged in github user is \"${loggedInUserName}\".`;\n",[59,16441,16442,16466,16474],{"__ignoreMap":274},[278,16443,16444,16446,16448,16451,16453,16455,16458,16460,16462,16464],{"class":280,"line":281},[278,16445,628],{"class":298},[278,16447,4559],{"class":298},[278,16449,16450],{"class":333}," getSystemPrompt",[278,16452,764],{"class":298},[278,16454,1245],{"class":302},[278,16456,16457],{"class":501},"loggedInUserName",[278,16459,960],{"class":298},[278,16461,963],{"class":650},[278,16463,1845],{"class":302},[278,16465,5533],{"class":298},[278,16467,16468,16471],{"class":280,"line":288},[278,16469,16470],{"class":302},"  systemPrompt ",[278,16472,16473],{"class":298},"+\n",[278,16475,16476,16479,16481,16484,16486,16489],{"class":280,"line":295},[278,16477,16478],{"class":309},"  `",[278,16480,14777],{"class":650},[278,16482,16483],{"class":309},"Note: The currently logged in github user is \"${",[278,16485,16457],{"class":302},[278,16487,16488],{"class":309},"}\".`",[278,16490,313],{"class":302},[32,16492,16494],{"id":16493},"client-side-handling","Client-Side Handling",[11,16496,16497,16498,16501,16502,16504],{},"On the client side, we use the built-in ",[59,16499,16500],{},"AuthState"," component from ",[59,16503,10918],{}," to manage authentication flows, like logging in and checking if a user is signed in.",[11,16506,16507],{},"Here’s how to handle sign-in:",[269,16509,16511],{"className":271,"code":16510,"language":273,"meta":274,"style":274},"\u003CAuthState v-slot=\"{ loggedIn }\">\n  \u003CUButton\n    v-if=\"loggedIn\"\n    size=\"lg\"\n    trailing-icon=\"i-heroicons-arrow-right-16-solid\"\n    to=\"\u002Fchat\"\n  >\n    Go to Chat\n  \u003C\u002FUButton>\n\n  \u003Cdiv v-else class=\"flex flex-col items-center justify-center gap-y-2\">\n    \u003CUButton\n      size=\"lg\"\n      icon=\"i-simple-icons-github\"\n      to=\"\u002Fauth\u002Fgithub\"\n      external\n    >\n      Sign in with GitHub\n    \u003C\u002FUButton>\n    \u003Cp class=\"text-sm text-gray-600 dark:text-gray-300 text-center\">\n      Start Chatting Now!\n    \u003C\u002Fp>\n  \u003C\u002Fdiv>\n\u003C\u002FAuthState>\n",[59,16512,16513,16533,16541,16552,16562,16577,16587,16591,16596,16606,16610,16630,16636,16645,16655,16665,16670,16674,16688,16696,16710,16718,16726,16734],{"__ignoreMap":274},[278,16514,16515,16517,16520,16523,16526,16528,16531],{"class":280,"line":281},[278,16516,1702],{"class":298},[278,16518,16519],{"class":302},"AuthState v",[278,16521,16522],{"class":298},"-",[278,16524,16525],{"class":302},"slot",[278,16527,358],{"class":298},[278,16529,16530],{"class":309},"\"{ loggedIn }\"",[278,16532,372],{"class":298},[278,16534,16535,16538],{"class":280,"line":288},[278,16536,16537],{"class":298},"  \u003C",[278,16539,16540],{"class":302},"UButton\n",[278,16542,16543,16546,16549],{"class":280,"line":295},[278,16544,16545],{"class":302},"    v",[278,16547,16548],{"class":298},"-if=",[278,16550,16551],{"class":309},"\"loggedIn\"\n",[278,16553,16554,16557,16559],{"class":280,"line":316},[278,16555,16556],{"class":302},"    size",[278,16558,358],{"class":298},[278,16560,16561],{"class":309},"\"lg\"\n",[278,16563,16564,16567,16569,16572,16574],{"class":280,"line":322},[278,16565,16566],{"class":302},"    trailing",[278,16568,16522],{"class":298},[278,16570,16571],{"class":302},"icon",[278,16573,358],{"class":298},[278,16575,16576],{"class":309},"\"i-heroicons-arrow-right-16-solid\"\n",[278,16578,16579,16582,16584],{"class":280,"line":327},[278,16580,16581],{"class":302},"    to",[278,16583,358],{"class":298},[278,16585,16586],{"class":309},"\"\u002Fchat\"\n",[278,16588,16589],{"class":280,"line":340},[278,16590,7200],{"class":298},[278,16592,16593],{"class":280,"line":349},[278,16594,16595],{"class":302},"    Go to Chat\n",[278,16597,16598,16601,16604],{"class":280,"line":375},[278,16599,16600],{"class":298},"  \u003C\u002F",[278,16602,16603],{"class":302},"UButton",[278,16605,372],{"class":298},[278,16607,16608],{"class":280,"line":386},[278,16609,292],{"emptyLinePlaceholder":291},[278,16611,16612,16614,16617,16620,16623,16625,16628],{"class":280,"line":397},[278,16613,16537],{"class":298},[278,16615,16616],{"class":302},"div v",[278,16618,16619],{"class":298},"-else",[278,16621,16622],{"class":302}," class",[278,16624,358],{"class":298},[278,16626,16627],{"class":309},"\"flex flex-col items-center justify-center gap-y-2\"",[278,16629,372],{"class":298},[278,16631,16632,16634],{"class":280,"line":408},[278,16633,352],{"class":298},[278,16635,16540],{"class":302},[278,16637,16638,16641,16643],{"class":280,"line":433},[278,16639,16640],{"class":302},"      size",[278,16642,358],{"class":298},[278,16644,16561],{"class":309},[278,16646,16647,16650,16652],{"class":280,"line":454},[278,16648,16649],{"class":302},"      icon",[278,16651,358],{"class":298},[278,16653,16654],{"class":309},"\"i-simple-icons-github\"\n",[278,16656,16657,16660,16662],{"class":280,"line":475},[278,16658,16659],{"class":302},"      to",[278,16661,358],{"class":298},[278,16663,16664],{"class":309},"\"\u002Fauth\u002Fgithub\"\n",[278,16666,16667],{"class":280,"line":496},[278,16668,16669],{"class":302},"      external\n",[278,16671,16672],{"class":280,"line":505},[278,16673,7935],{"class":298},[278,16675,16676,16679,16682,16685],{"class":280,"line":516},[278,16677,16678],{"class":302},"      Sign ",[278,16680,16681],{"class":298},"in",[278,16683,16684],{"class":298}," with",[278,16686,16687],{"class":302}," GitHub\n",[278,16689,16690,16692,16694],{"class":280,"line":527},[278,16691,600],{"class":298},[278,16693,16603],{"class":302},[278,16695,372],{"class":298},[278,16697,16698,16700,16703,16705,16708],{"class":280,"line":533},[278,16699,352],{"class":298},[278,16701,16702],{"class":302},"p class",[278,16704,358],{"class":298},[278,16706,16707],{"class":309},"\"text-sm text-gray-600 dark:text-gray-300 text-center\"",[278,16709,372],{"class":298},[278,16711,16712,16715],{"class":280,"line":539},[278,16713,16714],{"class":302},"      Start Chatting Now",[278,16716,16717],{"class":298},"!\n",[278,16719,16720,16722,16724],{"class":280,"line":545},[278,16721,600],{"class":298},[278,16723,11],{"class":302},[278,16725,372],{"class":298},[278,16727,16728,16730,16732],{"class":280,"line":551},[278,16729,16600],{"class":298},[278,16731,3300],{"class":302},[278,16733,372],{"class":298},[278,16735,16736,16739,16741],{"class":280,"line":557},[278,16737,16738],{"class":298},"\u003C\u002F",[278,16740,16500],{"class":302},[278,16742,372],{"class":298},[11,16744,16745,16746,16749,16750,16753],{},"Notice the ",[59,16747,16748],{},"external"," attribute on the Sign-In button. This attribute is essential—it tells the framework to treat the GitHub authentication as an external process. Without it, the framework will try to redirect you to the ",[59,16751,16752],{},"\u002Fauth\u002Fgithub"," route on the client side, causing errors (It did get me for sure).",[11,16755,16756,16757,16760],{},"This completes the server-side and client-side setup for user authentication. You can go through the shared GitHub repo to see how we also restrict navigation on the client side by using an ",[59,16758,16759],{},"auth"," middleware, ensuring that only authenticated users can access the chat page.",[24,16762,16764],{"id":16763},"bonus-enhancing-engagement","Bonus: Enhancing Engagement",[11,16766,16767],{},"As the project neared completion, I kept thinking about how to make it more engaging. I wanted to show what users were querying about and who they were querying for. However, I didn’t want to save every type of query—especially those like “When did I make my first commit?”—as they were both pointless and numerous. Instead, I decided to save only the queries made about other users, repositories, and other entities, excluding self-referential queries.",[32,16769,16771],{"id":16770},"database-setup","Database Setup",[11,16773,16774,16775,13642,16778,16780],{},"To achieve this, we needed a database, which is why we enabled ",[59,16776,16777],{},"database: true",[59,16779,3490],{}," file. This setting binds Cloudflare’s D1 database in production and uses its platform proxy during development.",[11,16782,16783,16784,16786],{},"First, we create the necessary database tables using the ",[59,16785,4913],{}," server composable:",[269,16788,16790],{"className":271,"code":16789,"language":273,"meta":274,"style":274},"await hubDatabase().exec(\n  `CREATE TABLE IF NOT EXISTS queries (\n    id INTEGER PRIMARY KEY AUTOINCREMENT,\n    text TEXT NOT NULL,\n    response TEXT NOT NULL,\n    github_request TEXT NOT NULL,\n    github_response TEXT NOT NULL,\n    queried_at DATETIME DEFAULT CURRENT_TIMESTAMP\n  );\n`.replace(\u002F\\n\u002Fg, '')\n);\n\n\u002F\u002F ... other tables and indexes\n",[59,16791,16792,16806,16811,16816,16821,16826,16831,16836,16841,16845,16869,16873,16877],{"__ignoreMap":274},[278,16793,16794,16796,16799,16801,16804],{"class":280,"line":281},[278,16795,4062],{"class":298},[278,16797,16798],{"class":333}," hubDatabase",[278,16800,4036],{"class":302},[278,16802,16803],{"class":333},"exec",[278,16805,770],{"class":302},[278,16807,16808],{"class":280,"line":288},[278,16809,16810],{"class":309},"  `CREATE TABLE IF NOT EXISTS queries (\n",[278,16812,16813],{"class":280,"line":295},[278,16814,16815],{"class":309},"    id INTEGER PRIMARY KEY AUTOINCREMENT,\n",[278,16817,16818],{"class":280,"line":316},[278,16819,16820],{"class":309},"    text TEXT NOT NULL,\n",[278,16822,16823],{"class":280,"line":322},[278,16824,16825],{"class":309},"    response TEXT NOT NULL,\n",[278,16827,16828],{"class":280,"line":327},[278,16829,16830],{"class":309},"    github_request TEXT NOT NULL,\n",[278,16832,16833],{"class":280,"line":340},[278,16834,16835],{"class":309},"    github_response TEXT NOT NULL,\n",[278,16837,16838],{"class":280,"line":349},[278,16839,16840],{"class":309},"    queried_at DATETIME DEFAULT CURRENT_TIMESTAMP\n",[278,16842,16843],{"class":280,"line":375},[278,16844,611],{"class":309},[278,16846,16847,16849,16851,16853,16855,16857,16859,16861,16863,16865,16867],{"class":280,"line":386},[278,16848,14780],{"class":309},[278,16850,183],{"class":302},[278,16852,13408],{"class":333},[278,16854,1126],{"class":302},[278,16856,13413],{"class":309},[278,16858,14939],{"class":650},[278,16860,13413],{"class":309},[278,16862,13421],{"class":298},[278,16864,1708],{"class":302},[278,16866,14012],{"class":309},[278,16868,4590],{"class":302},[278,16870,16871],{"class":280,"line":397},[278,16872,1280],{"class":302},[278,16874,16875],{"class":280,"line":408},[278,16876,292],{"emptyLinePlaceholder":291},[278,16878,16879],{"class":280,"line":433},[278,16880,16881],{"class":284},"\u002F\u002F ... other tables and indexes\n",[11,16883,16884,16885,16888,16889,16891],{},"Next, we utilize utility functions to save the queries. The ",[59,16886,16887],{},"saveUserQuery"," function is invoked only if a tool call was made from the ",[59,16890,14507],{}," function we discussed earlier.",[269,16893,16895],{"className":271,"code":16894,"language":273,"meta":274,"style":274},"export type ToolCallDetails = {\n  \u002F\u002F eslint-disable-next-line @typescript-eslint\u002Fno-explicit-any\n  response: any;\n  request: SearchParams;\n};\n\nexport type UserQuery = {\n  userMessage: string;\n  toolCalls: ToolCallDetails[];\n  assistantReply: string;\n};\n\nconst getAvatarUrl = (toolCall: ToolCallDetails) => {\n  let avatarUrl;\n  const responseItem = toolCall.response.items[0];\n\n  if (responseItem) {\n    if (responseItem.author) {\n      avatarUrl = responseItem.author.avatar_url;\n    } else if (responseItem.user) {\n      avatarUrl = responseItem.user.avatar_url;\n    } else if (responseItem.owner) {\n      avatarUrl = responseItem.owner.avatar_url;\n    } else if (responseItem.avatar_url) {\n      avatarUrl = responseItem.avatar_url;\n    }\n  }\n\n  return avatarUrl;\n};\n\nconst shouldSaveUserQuery = (\n  toolCall: ToolCallDetails,\n  loggedInUser: string\n) => {\n  const responseItem = toolCall.response.items[0];\n  if (\n    responseItem &&\n    ((responseItem.author && responseItem.author.login === loggedInUser) ||\n      (responseItem.user && responseItem.user.login === loggedInUser) ||\n      (responseItem.owner && responseItem.owner.login === loggedInUser) ||\n      responseItem.login === loggedInUser)\n  ) {\n    return false;\n  }\n\n  return true;\n};\n\nexport const saveUserQuery = async (\n  loggedInUser: string,\n  userQuery: UserQuery\n) => {\n  const toolCall = userQuery.toolCalls[0];\n  const matchedUser = toolCall.request.q.match(\u002F(?:author:|user:)(\\S+)\u002F);\n  if (matchedUser) {\n    const queriedUser = matchedUser[1].toLowerCase();\n    if (queriedUser !== loggedInUser) {\n      const avatarUrl = getAvatarUrl(toolCall);\n\n      await storeQuery(\n        userQuery.userMessage,\n        userQuery.assistantReply,\n        toolCall,\n        { login: queriedUser, avatarUrl }\n      );\n    }\n  } else if (shouldSaveUserQuery(toolCall, loggedInUser)) {\n    await storeQuery(userQuery.userMessage, userQuery.assistantReply, toolCall);\n  }\n};\n",[59,16896,16897,16910,16915,16926,16937,16941,16945,16958,16969,16980,16991,16995,16999,17023,17030,17046,17050,17057,17064,17074,17085,17094,17105,17114,17125,17134,17138,17142,17146,17152,17156,17160,17171,17182,17192,17200,17214,17220,17228,17246,17262,17278,17288,17293,17301,17305,17309,17317,17321,17325,17340,17350,17360,17368,17383,17423,17430,17452,17464,17478,17482,17492,17497,17502,17507,17512,17516,17520,17536,17545,17549],{"__ignoreMap":274},[278,16898,16899,16901,16903,16906,16908],{"class":280,"line":281},[278,16900,628],{"class":298},[278,16902,9454],{"class":298},[278,16904,16905],{"class":333}," ToolCallDetails",[278,16907,764],{"class":298},[278,16909,876],{"class":302},[278,16911,16912],{"class":280,"line":288},[278,16913,16914],{"class":284},"  \u002F\u002F eslint-disable-next-line @typescript-eslint\u002Fno-explicit-any\n",[278,16916,16917,16920,16922,16924],{"class":280,"line":295},[278,16918,16919],{"class":501},"  response",[278,16921,960],{"class":298},[278,16923,1842],{"class":650},[278,16925,313],{"class":302},[278,16927,16928,16931,16933,16935],{"class":280,"line":316},[278,16929,16930],{"class":501},"  request",[278,16932,960],{"class":298},[278,16934,12200],{"class":333},[278,16936,313],{"class":302},[278,16938,16939],{"class":280,"line":322},[278,16940,2817],{"class":302},[278,16942,16943],{"class":280,"line":327},[278,16944,292],{"emptyLinePlaceholder":291},[278,16946,16947,16949,16951,16954,16956],{"class":280,"line":340},[278,16948,628],{"class":298},[278,16950,9454],{"class":298},[278,16952,16953],{"class":333}," UserQuery",[278,16955,764],{"class":298},[278,16957,876],{"class":302},[278,16959,16960,16963,16965,16967],{"class":280,"line":349},[278,16961,16962],{"class":501},"  userMessage",[278,16964,960],{"class":298},[278,16966,963],{"class":650},[278,16968,313],{"class":302},[278,16970,16971,16974,16976,16978],{"class":280,"line":375},[278,16972,16973],{"class":501},"  toolCalls",[278,16975,960],{"class":298},[278,16977,16905],{"class":333},[278,16979,1579],{"class":302},[278,16981,16982,16985,16987,16989],{"class":280,"line":386},[278,16983,16984],{"class":501},"  assistantReply",[278,16986,960],{"class":298},[278,16988,963],{"class":650},[278,16990,313],{"class":302},[278,16992,16993],{"class":280,"line":397},[278,16994,2817],{"class":302},[278,16996,16997],{"class":280,"line":408},[278,16998,292],{"emptyLinePlaceholder":291},[278,17000,17001,17003,17006,17008,17010,17013,17015,17017,17019,17021],{"class":280,"line":433},[278,17002,5416],{"class":298},[278,17004,17005],{"class":333}," getAvatarUrl",[278,17007,764],{"class":298},[278,17009,1245],{"class":302},[278,17011,17012],{"class":501},"toolCall",[278,17014,960],{"class":298},[278,17016,16905],{"class":333},[278,17018,1845],{"class":302},[278,17020,1848],{"class":298},[278,17022,876],{"class":302},[278,17024,17025,17027],{"class":280,"line":454},[278,17026,6050],{"class":298},[278,17028,17029],{"class":302}," avatarUrl;\n",[278,17031,17032,17034,17037,17039,17042,17044],{"class":280,"line":475},[278,17033,758],{"class":298},[278,17035,17036],{"class":650}," responseItem",[278,17038,764],{"class":298},[278,17040,17041],{"class":302}," toolCall.response.items[",[278,17043,2012],{"class":650},[278,17045,11714],{"class":302},[278,17047,17048],{"class":280,"line":496},[278,17049,292],{"emptyLinePlaceholder":291},[278,17051,17052,17054],{"class":280,"line":505},[278,17053,1062],{"class":298},[278,17055,17056],{"class":302}," (responseItem) {\n",[278,17058,17059,17061],{"class":280,"line":516},[278,17060,1242],{"class":298},[278,17062,17063],{"class":302}," (responseItem.author) {\n",[278,17065,17066,17069,17071],{"class":280,"line":527},[278,17067,17068],{"class":302},"      avatarUrl ",[278,17070,358],{"class":298},[278,17072,17073],{"class":302}," responseItem.author.avatar_url;\n",[278,17075,17076,17078,17080,17082],{"class":280,"line":533},[278,17077,6636],{"class":302},[278,17079,15659],{"class":298},[278,17081,15662],{"class":298},[278,17083,17084],{"class":302}," (responseItem.user) {\n",[278,17086,17087,17089,17091],{"class":280,"line":539},[278,17088,17068],{"class":302},[278,17090,358],{"class":298},[278,17092,17093],{"class":302}," responseItem.user.avatar_url;\n",[278,17095,17096,17098,17100,17102],{"class":280,"line":545},[278,17097,6636],{"class":302},[278,17099,15659],{"class":298},[278,17101,15662],{"class":298},[278,17103,17104],{"class":302}," (responseItem.owner) {\n",[278,17106,17107,17109,17111],{"class":280,"line":551},[278,17108,17068],{"class":302},[278,17110,358],{"class":298},[278,17112,17113],{"class":302}," responseItem.owner.avatar_url;\n",[278,17115,17116,17118,17120,17122],{"class":280,"line":557},[278,17117,6636],{"class":302},[278,17119,15659],{"class":298},[278,17121,15662],{"class":298},[278,17123,17124],{"class":302}," (responseItem.avatar_url) {\n",[278,17126,17127,17129,17131],{"class":280,"line":567},[278,17128,17068],{"class":302},[278,17130,358],{"class":298},[278,17132,17133],{"class":302}," responseItem.avatar_url;\n",[278,17135,17136],{"class":280,"line":577},[278,17137,1285],{"class":302},[278,17139,17140],{"class":280,"line":587},[278,17141,1096],{"class":302},[278,17143,17144],{"class":280,"line":597},[278,17145,292],{"emptyLinePlaceholder":291},[278,17147,17148,17150],{"class":280,"line":608},[278,17149,343],{"class":298},[278,17151,17029],{"class":302},[278,17153,17154],{"class":280,"line":614},[278,17155,2817],{"class":302},[278,17157,17158],{"class":280,"line":620},[278,17159,292],{"emptyLinePlaceholder":291},[278,17161,17162,17164,17167,17169],{"class":280,"line":625},[278,17163,5416],{"class":298},[278,17165,17166],{"class":333}," shouldSaveUserQuery",[278,17168,764],{"class":298},[278,17170,346],{"class":302},[278,17172,17173,17176,17178,17180],{"class":280,"line":640},[278,17174,17175],{"class":501},"  toolCall",[278,17177,960],{"class":298},[278,17179,16905],{"class":333},[278,17181,660],{"class":302},[278,17183,17184,17187,17189],{"class":280,"line":663},[278,17185,17186],{"class":501},"  loggedInUser",[278,17188,960],{"class":298},[278,17190,17191],{"class":650}," string\n",[278,17193,17194,17196,17198],{"class":280,"line":669},[278,17195,1845],{"class":302},[278,17197,1848],{"class":298},[278,17199,876],{"class":302},[278,17201,17202,17204,17206,17208,17210,17212],{"class":280,"line":680},[278,17203,758],{"class":298},[278,17205,17036],{"class":650},[278,17207,764],{"class":298},[278,17209,17041],{"class":302},[278,17211,2012],{"class":650},[278,17213,11714],{"class":302},[278,17215,17216,17218],{"class":280,"line":686},[278,17217,1062],{"class":298},[278,17219,346],{"class":302},[278,17221,17222,17225],{"class":280,"line":1334},[278,17223,17224],{"class":302},"    responseItem ",[278,17226,17227],{"class":298},"&&\n",[278,17229,17230,17233,17235,17238,17240,17243],{"class":280,"line":1375},[278,17231,17232],{"class":302},"    ((responseItem.author ",[278,17234,1068],{"class":298},[278,17236,17237],{"class":302}," responseItem.author.login ",[278,17239,2451],{"class":298},[278,17241,17242],{"class":302}," loggedInUser) ",[278,17244,17245],{"class":298},"||\n",[278,17247,17248,17251,17253,17256,17258,17260],{"class":280,"line":1381},[278,17249,17250],{"class":302},"      (responseItem.user ",[278,17252,1068],{"class":298},[278,17254,17255],{"class":302}," responseItem.user.login ",[278,17257,2451],{"class":298},[278,17259,17242],{"class":302},[278,17261,17245],{"class":298},[278,17263,17264,17267,17269,17272,17274,17276],{"class":280,"line":1386},[278,17265,17266],{"class":302},"      (responseItem.owner ",[278,17268,1068],{"class":298},[278,17270,17271],{"class":302}," responseItem.owner.login ",[278,17273,2451],{"class":298},[278,17275,17242],{"class":302},[278,17277,17245],{"class":298},[278,17279,17280,17283,17285],{"class":280,"line":1394},[278,17281,17282],{"class":302},"      responseItem.login ",[278,17284,2451],{"class":298},[278,17286,17287],{"class":302}," loggedInUser)\n",[278,17289,17290],{"class":280,"line":1406},[278,17291,17292],{"class":302},"  ) {\n",[278,17294,17295,17297,17299],{"class":280,"line":1423},[278,17296,1088],{"class":298},[278,17298,6872],{"class":650},[278,17300,313],{"class":302},[278,17302,17303],{"class":280,"line":1432},[278,17304,1096],{"class":302},[278,17306,17307],{"class":280,"line":1437},[278,17308,292],{"emptyLinePlaceholder":291},[278,17310,17311,17313,17315],{"class":280,"line":1916},[278,17312,343],{"class":298},[278,17314,6575],{"class":650},[278,17316,313],{"class":302},[278,17318,17319],{"class":280,"line":1939},[278,17320,2817],{"class":302},[278,17322,17323],{"class":280,"line":1949},[278,17324,292],{"emptyLinePlaceholder":291},[278,17326,17327,17329,17331,17334,17336,17338],{"class":280,"line":1954},[278,17328,628],{"class":298},[278,17330,4559],{"class":298},[278,17332,17333],{"class":333}," saveUserQuery",[278,17335,764],{"class":298},[278,17337,2325],{"class":298},[278,17339,346],{"class":302},[278,17341,17342,17344,17346,17348],{"class":280,"line":1959},[278,17343,17186],{"class":501},[278,17345,960],{"class":298},[278,17347,963],{"class":650},[278,17349,660],{"class":302},[278,17351,17352,17355,17357],{"class":280,"line":1985},[278,17353,17354],{"class":501},"  userQuery",[278,17356,960],{"class":298},[278,17358,17359],{"class":333}," UserQuery\n",[278,17361,17362,17364,17366],{"class":280,"line":1990},[278,17363,1845],{"class":302},[278,17365,1848],{"class":298},[278,17367,876],{"class":302},[278,17369,17370,17372,17374,17376,17379,17381],{"class":280,"line":1997},[278,17371,758],{"class":298},[278,17373,12019],{"class":650},[278,17375,764],{"class":298},[278,17377,17378],{"class":302}," userQuery.toolCalls[",[278,17380,2012],{"class":650},[278,17382,11714],{"class":302},[278,17384,17385,17387,17390,17392,17395,17398,17400,17402,17406,17408,17411,17414,17416,17419,17421],{"class":280,"line":2006},[278,17386,758],{"class":298},[278,17388,17389],{"class":650}," matchedUser",[278,17391,764],{"class":298},[278,17393,17394],{"class":302}," toolCall.request.q.",[278,17396,17397],{"class":333},"match",[278,17399,1126],{"class":302},[278,17401,13413],{"class":309},[278,17403,17405],{"class":17404},"sA_wV","(?:author:",[278,17407,1032],{"class":298},[278,17409,17410],{"class":17404},"user:)(",[278,17412,17413],{"class":650},"\\S",[278,17415,1345],{"class":298},[278,17417,17418],{"class":17404},")",[278,17420,13413],{"class":309},[278,17422,1280],{"class":302},[278,17424,17425,17427],{"class":280,"line":2018},[278,17426,1062],{"class":298},[278,17428,17429],{"class":302}," (matchedUser) {\n",[278,17431,17432,17434,17437,17439,17442,17445,17448,17450],{"class":280,"line":2029},[278,17433,1112],{"class":298},[278,17435,17436],{"class":650}," queriedUser",[278,17438,764],{"class":298},[278,17440,17441],{"class":302}," matchedUser[",[278,17443,17444],{"class":650},"1",[278,17446,17447],{"class":302},"].",[278,17449,13250],{"class":333},[278,17451,1313],{"class":302},[278,17453,17454,17456,17459,17461],{"class":280,"line":2034},[278,17455,1242],{"class":298},[278,17457,17458],{"class":302}," (queriedUser ",[278,17460,2092],{"class":298},[278,17462,17463],{"class":302}," loggedInUser) {\n",[278,17465,17466,17468,17471,17473,17475],{"class":280,"line":2040},[278,17467,2461],{"class":298},[278,17469,17470],{"class":650}," avatarUrl",[278,17472,764],{"class":298},[278,17474,17005],{"class":333},[278,17476,17477],{"class":302},"(toolCall);\n",[278,17479,17480],{"class":280,"line":2045},[278,17481,292],{"emptyLinePlaceholder":291},[278,17483,17484,17487,17490],{"class":280,"line":2068},[278,17485,17486],{"class":298},"      await",[278,17488,17489],{"class":333}," storeQuery",[278,17491,770],{"class":302},[278,17493,17494],{"class":280,"line":2099},[278,17495,17496],{"class":302},"        userQuery.userMessage,\n",[278,17498,17499],{"class":280,"line":6428},[278,17500,17501],{"class":302},"        userQuery.assistantReply,\n",[278,17503,17504],{"class":280,"line":6439},[278,17505,17506],{"class":302},"        toolCall,\n",[278,17508,17509],{"class":280,"line":6450},[278,17510,17511],{"class":302},"        { login: queriedUser, avatarUrl }\n",[278,17513,17514],{"class":280,"line":6455},[278,17515,2616],{"class":302},[278,17517,17518],{"class":280,"line":6460},[278,17519,1285],{"class":302},[278,17521,17522,17524,17526,17528,17530,17533],{"class":280,"line":6475},[278,17523,1397],{"class":302},[278,17525,15659],{"class":298},[278,17527,15662],{"class":298},[278,17529,1245],{"class":302},[278,17531,17532],{"class":333},"shouldSaveUserQuery",[278,17534,17535],{"class":302},"(toolCall, loggedInUser)) {\n",[278,17537,17538,17540,17542],{"class":280,"line":6486},[278,17539,5077],{"class":298},[278,17541,17489],{"class":333},[278,17543,17544],{"class":302},"(userQuery.userMessage, userQuery.assistantReply, toolCall);\n",[278,17546,17547],{"class":280,"line":6491},[278,17548,1096],{"class":302},[278,17550,17551],{"class":280,"line":6518},[278,17552,2817],{"class":302},[11,17554,17555],{},"It checks if the query involves the currently logged-in user. If it does, we simply return without logging anything; otherwise, we store the query details in the database using:",[269,17557,17559],{"className":271,"code":17558,"language":273,"meta":274,"style":274},"const storeQuery = async (\n  queryText: string,\n  assistantReply: string,\n  toolCall: ToolCallDetails,\n  queriedUser?: { login: string; avatarUrl?: string }\n) => {\n  try {\n    const db = hubDatabase();\n\n    const queryStmt = db\n      .prepare(\n        'INSERT INTO queries (text, response, github_request, github_response) VALUES (?1, ?2, ?3, ?4)'\n      )\n      .bind(\n        queryText,\n        assistantReply,\n        JSON.stringify(toolCall.request),\n        JSON.stringify(toolCall.response)\n      );\n    if (queriedUser) {\n      const [batchRes1, batchRes2] = await db.batch([\n        queryStmt,\n        db\n          .prepare(\n            `INSERT INTO trending_users (username, search_count, last_searched, avatar_url)\n              VALUES (?1, 1, CURRENT_TIMESTAMP, ?2)\n              ON CONFLICT(username)\n              DO UPDATE SET search_count = search_count + 1, last_searched = CURRENT_TIMESTAMP, avatar_url = COALESCE(?2, avatar_url)`\n          )\n          .bind(queriedUser.login, queriedUser.avatarUrl),\n      ]);\n\n      console.log('storeQuery: ', batchRes1, batchRes2);\n    } else {\n      const res = await queryStmt.run();\n\n      console.log('storeQuery: ', res);\n    }\n  } catch (error) {\n    console.error('Failed to store query: ', error);\n  }\n};\n",[59,17560,17561,17573,17584,17594,17604,17632,17640,17646,17659,17663,17675,17684,17689,17694,17703,17708,17713,17725,17736,17740,17747,17777,17782,17787,17796,17801,17806,17811,17816,17820,17829,17834,17838,17852,17860,17878,17882,17895,17899,17907,17920,17924],{"__ignoreMap":274},[278,17562,17563,17565,17567,17569,17571],{"class":280,"line":281},[278,17564,5416],{"class":298},[278,17566,17489],{"class":333},[278,17568,764],{"class":298},[278,17570,2325],{"class":298},[278,17572,346],{"class":302},[278,17574,17575,17578,17580,17582],{"class":280,"line":288},[278,17576,17577],{"class":501},"  queryText",[278,17579,960],{"class":298},[278,17581,963],{"class":650},[278,17583,660],{"class":302},[278,17585,17586,17588,17590,17592],{"class":280,"line":295},[278,17587,16984],{"class":501},[278,17589,960],{"class":298},[278,17591,963],{"class":650},[278,17593,660],{"class":302},[278,17595,17596,17598,17600,17602],{"class":280,"line":316},[278,17597,17175],{"class":501},[278,17599,960],{"class":298},[278,17601,16905],{"class":333},[278,17603,660],{"class":302},[278,17605,17606,17609,17611,17613,17616,17618,17620,17622,17625,17627,17629],{"class":280,"line":322},[278,17607,17608],{"class":501},"  queriedUser",[278,17610,9512],{"class":298},[278,17612,1009],{"class":302},[278,17614,17615],{"class":501},"login",[278,17617,960],{"class":298},[278,17619,963],{"class":650},[278,17621,1019],{"class":302},[278,17623,17624],{"class":501},"avatarUrl",[278,17626,9512],{"class":298},[278,17628,963],{"class":650},[278,17630,17631],{"class":302}," }\n",[278,17633,17634,17636,17638],{"class":280,"line":327},[278,17635,1845],{"class":302},[278,17637,1848],{"class":298},[278,17639,876],{"class":302},[278,17641,17642,17644],{"class":280,"line":340},[278,17643,1105],{"class":298},[278,17645,876],{"class":302},[278,17647,17648,17650,17653,17655,17657],{"class":280,"line":349},[278,17649,1112],{"class":298},[278,17651,17652],{"class":650}," db",[278,17654,764],{"class":298},[278,17656,16798],{"class":333},[278,17658,1313],{"class":302},[278,17660,17661],{"class":280,"line":375},[278,17662,292],{"emptyLinePlaceholder":291},[278,17664,17665,17667,17670,17672],{"class":280,"line":386},[278,17666,1112],{"class":298},[278,17668,17669],{"class":650}," queryStmt",[278,17671,764],{"class":298},[278,17673,17674],{"class":302}," db\n",[278,17676,17677,17679,17682],{"class":280,"line":397},[278,17678,5086],{"class":302},[278,17680,17681],{"class":333},"prepare",[278,17683,770],{"class":302},[278,17685,17686],{"class":280,"line":408},[278,17687,17688],{"class":309},"        'INSERT INTO queries (text, response, github_request, github_response) VALUES (?1, ?2, ?3, ?4)'\n",[278,17690,17691],{"class":280,"line":433},[278,17692,17693],{"class":302},"      )\n",[278,17695,17696,17698,17701],{"class":280,"line":454},[278,17697,5086],{"class":302},[278,17699,17700],{"class":333},"bind",[278,17702,770],{"class":302},[278,17704,17705],{"class":280,"line":475},[278,17706,17707],{"class":302},"        queryText,\n",[278,17709,17710],{"class":280,"line":496},[278,17711,17712],{"class":302},"        assistantReply,\n",[278,17714,17715,17718,17720,17722],{"class":280,"line":505},[278,17716,17717],{"class":650},"        JSON",[278,17719,183],{"class":302},[278,17721,2235],{"class":333},[278,17723,17724],{"class":302},"(toolCall.request),\n",[278,17726,17727,17729,17731,17733],{"class":280,"line":516},[278,17728,17717],{"class":650},[278,17730,183],{"class":302},[278,17732,2235],{"class":333},[278,17734,17735],{"class":302},"(toolCall.response)\n",[278,17737,17738],{"class":280,"line":527},[278,17739,2616],{"class":302},[278,17741,17742,17744],{"class":280,"line":533},[278,17743,1242],{"class":298},[278,17745,17746],{"class":302}," (queriedUser) {\n",[278,17748,17749,17751,17753,17756,17758,17761,17764,17766,17768,17771,17774],{"class":280,"line":539},[278,17750,2461],{"class":298},[278,17752,13367],{"class":302},[278,17754,17755],{"class":650},"batchRes1",[278,17757,1708],{"class":302},[278,17759,17760],{"class":650},"batchRes2",[278,17762,17763],{"class":302},"] ",[278,17765,358],{"class":298},[278,17767,1120],{"class":298},[278,17769,17770],{"class":302}," db.",[278,17772,17773],{"class":333},"batch",[278,17775,17776],{"class":302},"([\n",[278,17778,17779],{"class":280,"line":545},[278,17780,17781],{"class":302},"        queryStmt,\n",[278,17783,17784],{"class":280,"line":551},[278,17785,17786],{"class":302},"        db\n",[278,17788,17789,17792,17794],{"class":280,"line":557},[278,17790,17791],{"class":302},"          .",[278,17793,17681],{"class":333},[278,17795,770],{"class":302},[278,17797,17798],{"class":280,"line":567},[278,17799,17800],{"class":309},"            `INSERT INTO trending_users (username, search_count, last_searched, avatar_url)\n",[278,17802,17803],{"class":280,"line":577},[278,17804,17805],{"class":309},"              VALUES (?1, 1, CURRENT_TIMESTAMP, ?2)\n",[278,17807,17808],{"class":280,"line":587},[278,17809,17810],{"class":309},"              ON CONFLICT(username)\n",[278,17812,17813],{"class":280,"line":597},[278,17814,17815],{"class":309},"              DO UPDATE SET search_count = search_count + 1, last_searched = CURRENT_TIMESTAMP, avatar_url = COALESCE(?2, avatar_url)`\n",[278,17817,17818],{"class":280,"line":608},[278,17819,14977],{"class":302},[278,17821,17822,17824,17826],{"class":280,"line":614},[278,17823,17791],{"class":302},[278,17825,17700],{"class":333},[278,17827,17828],{"class":302},"(queriedUser.login, queriedUser.avatarUrl),\n",[278,17830,17831],{"class":280,"line":620},[278,17832,17833],{"class":302},"      ]);\n",[278,17835,17836],{"class":280,"line":625},[278,17837,292],{"emptyLinePlaceholder":291},[278,17839,17840,17842,17844,17846,17849],{"class":280,"line":640},[278,17841,1919],{"class":302},[278,17843,14851],{"class":333},[278,17845,1126],{"class":302},[278,17847,17848],{"class":309},"'storeQuery: '",[278,17850,17851],{"class":302},", batchRes1, batchRes2);\n",[278,17853,17854,17856,17858],{"class":280,"line":663},[278,17855,6636],{"class":302},[278,17857,15659],{"class":298},[278,17859,876],{"class":302},[278,17861,17862,17864,17867,17869,17871,17874,17876],{"class":280,"line":669},[278,17863,2461],{"class":298},[278,17865,17866],{"class":650}," res",[278,17868,764],{"class":298},[278,17870,1120],{"class":298},[278,17872,17873],{"class":302}," queryStmt.",[278,17875,4039],{"class":333},[278,17877,1313],{"class":302},[278,17879,17880],{"class":280,"line":680},[278,17881,292],{"emptyLinePlaceholder":291},[278,17883,17884,17886,17888,17890,17892],{"class":280,"line":686},[278,17885,1919],{"class":302},[278,17887,14851],{"class":333},[278,17889,1126],{"class":302},[278,17891,17848],{"class":309},[278,17893,17894],{"class":302},", res);\n",[278,17896,17897],{"class":280,"line":1334},[278,17898,1285],{"class":302},[278,17900,17901,17903,17905],{"class":280,"line":1375},[278,17902,1397],{"class":302},[278,17904,1400],{"class":298},[278,17906,1403],{"class":302},[278,17908,17909,17911,17913,17915,17918],{"class":280,"line":1381},[278,17910,1409],{"class":302},[278,17912,1412],{"class":333},[278,17914,1126],{"class":302},[278,17916,17917],{"class":309},"'Failed to store query: '",[278,17919,1420],{"class":302},[278,17921,17922],{"class":280,"line":1386},[278,17923,1096],{"class":302},[278,17925,17926],{"class":280,"line":1394},[278,17927,2817],{"class":302},[32,17929,17931],{"id":17930},"api-endpoints","API Endpoints",[11,17933,17934],{},"We then create API endpoints to retrieve the trending users and recent queries, as shown below (note the use of endpoint caching):",[269,17936,17938],{"className":271,"code":17937,"language":273,"meta":274,"style":274},"export const getRecentQueries = async () => {\n  const db = hubDatabase();\n  console.log('getRecentQueries');\n  const result = await db\n    .prepare(\n      'SELECT id, text, response, queried_at FROM queries ORDER BY queried_at DESC LIMIT ?'\n    )\n    .bind(10)\n    .all\u003CRecentQuery>();\n\n  console.log('getRecentQueries: ', result);\n  return result.results;\n};\n\nexport default defineCachedEventHandler(\n  async () => {\n    const results = await getRecentQueries();\n\n    return results;\n  },\n  {\n    maxAge: 10 * 60, \u002F\u002F 10 minutes\n  }\n);\n",[59,17939,17940,17959,17971,17985,17998,18006,18011,18016,18029,18043,18047,18061,18068,18072,18076,18087,18097,18112,18116,18123,18127,18131,18146,18150],{"__ignoreMap":274},[278,17941,17942,17944,17946,17949,17951,17953,17955,17957],{"class":280,"line":281},[278,17943,628],{"class":298},[278,17945,4559],{"class":298},[278,17947,17948],{"class":333}," getRecentQueries",[278,17950,764],{"class":298},[278,17952,2325],{"class":298},[278,17954,5860],{"class":302},[278,17956,1848],{"class":298},[278,17958,876],{"class":302},[278,17960,17961,17963,17965,17967,17969],{"class":280,"line":288},[278,17962,758],{"class":298},[278,17964,17652],{"class":650},[278,17966,764],{"class":298},[278,17968,16798],{"class":333},[278,17970,1313],{"class":302},[278,17972,17973,17976,17978,17980,17983],{"class":280,"line":295},[278,17974,17975],{"class":302},"  console.",[278,17977,14851],{"class":333},[278,17979,1126],{"class":302},[278,17981,17982],{"class":309},"'getRecentQueries'",[278,17984,1280],{"class":302},[278,17986,17987,17989,17992,17994,17996],{"class":280,"line":316},[278,17988,758],{"class":298},[278,17990,17991],{"class":650}," result",[278,17993,764],{"class":298},[278,17995,1120],{"class":298},[278,17997,17674],{"class":302},[278,17999,18000,18002,18004],{"class":280,"line":322},[278,18001,4595],{"class":302},[278,18003,17681],{"class":333},[278,18005,770],{"class":302},[278,18007,18008],{"class":280,"line":327},[278,18009,18010],{"class":309},"      'SELECT id, text, response, queried_at FROM queries ORDER BY queried_at DESC LIMIT ?'\n",[278,18012,18013],{"class":280,"line":340},[278,18014,18015],{"class":302},"    )\n",[278,18017,18018,18020,18022,18024,18027],{"class":280,"line":349},[278,18019,4595],{"class":302},[278,18021,17700],{"class":333},[278,18023,1126],{"class":302},[278,18025,18026],{"class":650},"10",[278,18028,4590],{"class":302},[278,18030,18031,18033,18035,18037,18040],{"class":280,"line":375},[278,18032,4595],{"class":302},[278,18034,2062],{"class":333},[278,18036,1702],{"class":302},[278,18038,18039],{"class":333},"RecentQuery",[278,18041,18042],{"class":302},">();\n",[278,18044,18045],{"class":280,"line":386},[278,18046,292],{"emptyLinePlaceholder":291},[278,18048,18049,18051,18053,18055,18058],{"class":280,"line":397},[278,18050,17975],{"class":302},[278,18052,14851],{"class":333},[278,18054,1126],{"class":302},[278,18056,18057],{"class":309},"'getRecentQueries: '",[278,18059,18060],{"class":302},", result);\n",[278,18062,18063,18065],{"class":280,"line":408},[278,18064,343],{"class":298},[278,18066,18067],{"class":302}," result.results;\n",[278,18069,18070],{"class":280,"line":433},[278,18071,2817],{"class":302},[278,18073,18074],{"class":280,"line":454},[278,18075,292],{"emptyLinePlaceholder":291},[278,18077,18078,18080,18082,18085],{"class":280,"line":475},[278,18079,628],{"class":298},[278,18081,631],{"class":298},[278,18083,18084],{"class":333}," defineCachedEventHandler",[278,18086,770],{"class":302},[278,18088,18089,18091,18093,18095],{"class":280,"line":496},[278,18090,12914],{"class":298},[278,18092,5860],{"class":302},[278,18094,1848],{"class":298},[278,18096,876],{"class":302},[278,18098,18099,18101,18104,18106,18108,18110],{"class":280,"line":505},[278,18100,1112],{"class":298},[278,18102,18103],{"class":650}," results",[278,18105,764],{"class":298},[278,18107,1120],{"class":298},[278,18109,17948],{"class":333},[278,18111,1313],{"class":302},[278,18113,18114],{"class":280,"line":516},[278,18115,292],{"emptyLinePlaceholder":291},[278,18117,18118,18120],{"class":280,"line":527},[278,18119,1088],{"class":298},[278,18121,18122],{"class":302}," results;\n",[278,18124,18125],{"class":280,"line":533},[278,18126,683],{"class":302},[278,18128,18129],{"class":280,"line":539},[278,18130,11470],{"class":302},[278,18132,18133,18135,18137,18139,18141,18143],{"class":280,"line":545},[278,18134,13140],{"class":302},[278,18136,18026],{"class":650},[278,18138,1363],{"class":298},[278,18140,1366],{"class":650},[278,18142,1708],{"class":302},[278,18144,18145],{"class":284},"\u002F\u002F 10 minutes\n",[278,18147,18148],{"class":280,"line":551},[278,18149,1096],{"class":302},[278,18151,18152],{"class":280,"line":557},[278,18153,1280],{"class":302},[32,18155,18157],{"id":18156},"frontend-integration","Frontend Integration",[11,18159,18160],{},"This setup allows us to display the data in the frontend, providing users with insights into trending queries and recently searched users, as illustrated in the screenshot below.",[11,18162,18163],{},[3135,18164],{"alt":18165,"src":18166},"Chat GitHub home page showing recent queries","\u002Fimages\u002Fposts\u002Fbuilding-a-chat-interface-to-search-github\u002F2944e305-f533-4eec-8e7f-02a33daf79a2-fd33705e89.png",[11,18168,18169],{},"You can go through the shared GitHub repo to see the whole implementation in detail.",[11,18171,18172],{},"And with that, now you can also know what you did last summer—phew!",[24,18174,10621],{"id":10620},[11,18176,18177],{},"The project’s code is open source and can be checked here",[40,18179],{"url":18180},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fchat-github",[24,18182,18184],{"id":18183},"deployment","Deployment",[11,18186,18187,18188,183],{},"You can deploy the app using your NuxtHub admin panel or manually through the NuxtHub CLI. For more details on deploying an app through NuxtHub, refer to the ",[47,18189,18192],{"href":18190,"rel":18191},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fdeploy",[51],"official documentation",[11,18194,18195],{},"The best part is that this project is now listed as a template on the NuxtHub Templates page. So, if you already have a NuxtHub account, you can deploy this project in one click using the button below (Just remember to add the necessary environment variables in the panel).",[11,18197,18198],{},[47,18199,18202],{"href":18200,"rel":18201},"https:\u002F\u002Fhub.nuxt.com\u002Fnew?template=chat-github",[51],[3135,18203],{"alt":18204,"src":18205},"One click NuxtHub deploy button","https:\u002F\u002Fhub.nuxt.com\u002Fbutton.svg",[24,18207,10536],{"id":10535},[11,18209,18210,18211,919,18214,18217,18218,18221,18222,18225],{},"Currently, we rely on the AI's ability to generate GitHub API queries from natural language input. While we’ve provided it with details about various qualifiers, it can still struggle with more complex or intricate queries. I also experimented with tool-calling models from ",[59,18212,18213],{},"Cloudflare’s Workers AI",[59,18215,18216],{},"Groq API",", and found that ",[59,18219,18220],{},"gpt-4o"," performed better for these tasks. ",[59,18223,18224],{},"Claude 3.5 Sonnet"," should definitely offer even better results, but it tends to be more expensive.",[11,18227,18228],{},"Here are a few alternative approaches we could explore to improve query accuracy and results:",[71,18230,18231,18237],{},[74,18232,18233,18236],{},[94,18234,18235],{},"Fine-tune\u002Ftrain a tool-calling model"," specifically on the GitHub Search API documentation.",[74,18238,18239,18242],{},[94,18240,18241],{},"Create embeddings"," from the GitHub Search documentation and store them in a vector database. Cloudflare offers a free tier for its vector database now, which we could leverage. When a user query is made, we could retrieve relevant information from the embeddings and include it in the system prompt.",[11,18244,18245],{},"If you have any other ideas for improving this project—or any feedback—please feel free to share them in the comments!",[24,18247,10634],{"id":10633},[11,18249,18250],{},"And there you have it—a GitHub search tool, powered by OpenAI, wrapped in a chat interface, and ready for action. We’ve gone through setting up the project, streaming responses, authenticating users, and even added a sprinkle of engagement to make things interesting.",[11,18252,18253],{},"While the project has plenty of room for improvements, it’s a solid foundation to build upon. And who knows—maybe with a few tweaks, it’ll be predicting your next GitHub repo before you even think of it :-).",[11,18255,18256],{},"May all your commits be bug-free!",[11,18258,10642],{},[3048,18260],{},[18,18262,18263],{},[11,18264,18265],{},[3061,18266,18267],{},[94,18268,10651],{},[3065,18270,18271],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":274,"searchDepth":288,"depth":288,"links":18273},[18274,18275,18276,18282,18287,18288,18293,18297,18302,18303,18304,18305],{"id":10733,"depth":288,"text":10734},{"id":10764,"depth":288,"text":10765},{"id":3201,"depth":288,"text":3202,"children":18277},[18278,18279,18280,18281],{"id":10829,"depth":295,"text":10830},{"id":3266,"depth":295,"text":3267},{"id":10909,"depth":295,"text":10912},{"id":10980,"depth":295,"text":10981},{"id":11179,"depth":288,"text":11180,"children":18283},[18284,18285,18286],{"id":11186,"depth":295,"text":11187},{"id":12174,"depth":295,"text":12175},{"id":12707,"depth":295,"text":12708},{"id":12873,"depth":288,"text":12874},{"id":13650,"depth":288,"text":13651,"children":18289},[18290,18291,18292],{"id":13687,"depth":295,"text":13688},{"id":14565,"depth":295,"text":14566},{"id":15161,"depth":295,"text":15162},{"id":16009,"depth":288,"text":16010,"children":18294},[18295,18296],{"id":16026,"depth":295,"text":16027},{"id":16493,"depth":295,"text":16494},{"id":16763,"depth":288,"text":16764,"children":18298},[18299,18300,18301],{"id":16770,"depth":295,"text":16771},{"id":17930,"depth":295,"text":17931},{"id":18156,"depth":295,"text":18157},{"id":10620,"depth":288,"text":10621},{"id":18183,"depth":288,"text":18184},{"id":10535,"depth":288,"text":10536},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fbuilding-a-chat-interface-to-search-github\u002F5f866b99-3c0b-4c3d-859b-fa98566ea7fd-ab6fef7095.png","2024-10-04T11:53:21.011Z","Explore how to create a chat interface for GitHub searches, utilizing AI to simplify natural language queries and boost search efficiency","cm1uo2esz002o09jk7n6y1xhz",{},"\u002Fbuilding-a-chat-interface-to-search-github",{"title":10716,"description":18308},"building-a-chat-interface-to-search-github",[10708,18315,10710,18316,10711],"github","openai","-2egEZImq_Vrq8QQFK4c6eHS7RL5yNrfdTeGk_vyJdo",{"id":18319,"title":18320,"body":18321,"cover":22743,"date":22744,"description":22745,"draft":3086,"extension":3087,"hashnodeId":22746,"meta":22747,"navigation":291,"path":22748,"seo":22749,"slug":22750,"stem":22750,"tags":22751,"__hash__":22753},"posts\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui.md","Create Your Own Cloudflare Workers AI LLM Playground Using NuxtHub and NuxtUI",{"type":8,"value":18322,"toc":22714},[18323,18331,18334,18336,18351,18371,18374,18380,18386,18389,18391,18394,18398,18429,18431,18434,18454,18456,18459,18542,18552,18555,18559,18565,18569,18572,18750,18757,18761,18764,18767,19272,19276,19279,19284,19287,19460,19465,19468,19625,19639,19644,19647,19758,19769,19772,19776,19782,19974,19998,20001,20013,20016,20020,20037,20409,20414,20421,20424,20428,20431,20435,20438,20446,20453,20456,20467,20474,20477,20480,20484,20493,20685,20703,20706,20736,20748,21307,21313,21319,21323,21329,21781,21784,21792,21795,21799,21802,21806,21815,21839,21862,21962,21983,21991,22103,22109,22122,22126,22136,22145,22151,22154,22363,22366,22370,22376,22428,22435,22471,22481,22614,22620,22623,22627,22630,22633,22645,22652,22659,22661,22664,22667,22669,22672,22683,22686,22697,22700,22702,22704,22711],[11,18324,18325,18326,18330],{},"You might be wondering, 'Another LLM (Large Language Model) playground? Aren't there plenty of these already?' Fair question. But here's the thing: the world of AI is constantly evolving, and every day one new tool or another pops up. One such AI offering from ",[47,18327,3194],{"href":18328,"rel":18329},"https:\u002F\u002Fhub.nuxt.com\u002F",[51]," was launched recently, and I couldn’t resist taking it for a spin. And let’s be honest—some of us just can't resist adding a 'dark mode', something the Cloudflare Workers AI models playground hasn’t embraced yet.",[11,18332,18333],{},"But beyond these practical reasons, there’s something even more satisfying: the joy of creation and learning. So, if you're ready to dive in, maybe you'll pick up some new concepts like Server-Sent Events, response streaming, markdown parsing, and, of course, deploying your own chat playground without any of the usual fuss. So, get comfy—however you like—and let's get started!",[24,18335,27],{"id":26},[11,18337,18338,18339,18344,18345,18350],{},"Alright, so what exactly are we going to create here? As you might have guessed already, we will be creating a chat interface to talk to different ",[47,18340,18343],{"href":18341,"rel":18342},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers-ai\u002Fmodels#text-generation",[51],"text generation models"," supported by ",[47,18346,18349],{"href":18347,"rel":18348},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers-ai\u002F",[51],"Cloudflare Workers AI",". Below is a brief list of capabilities we will build along the way:",[71,18352,18353,18356,18359,18362,18365,18368],{},[74,18354,18355],{},"Ability to set different LLM params, like temperature, max tokens, system prompt, top_p, top_k etc while keeping some of these optional",[74,18357,18358],{},"Ability to turn LLM response streaming on\u002Foff",[74,18360,18361],{},"Handle streaming\u002Fnon-streaming LLM responses on both, the server and the client side",[74,18363,18364],{},"Parsing LLM responses for markdown and display it appropriately",[74,18366,18367],{},"Auto-scrolling the chat container as the response is streamed from the LLM endpoint",[74,18369,18370],{},"Adding the dark mode (this one is trivial but let's add it here for completeness)",[11,18372,18373],{},"This is how the interface will look when we are through this article:",[11,18375,18376],{},[3135,18377],{"alt":18378,"src":18379},"LLM playground chat interface ","\u002Fimages\u002Fposts\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui\u002F4dc571f5-3c82-4496-a75b-6a70d4dbbd0c-f1a7f42690.png",[11,18381,10797,18382],{},[47,18383,18384],{"href":18384,"rel":18385},"https:\u002F\u002Fhub-chat.nuxt.dev\u002F",[51],[11,18387,18388],{},"We will cover each of the tasks in detail in the following sections.",[24,18390,3202],{"id":3201},[11,18392,18393],{},"Now that we've set the stage for our LLM playground project, let's dive into the technologies we'll be using and get our development environment ready.",[32,18395,18397],{"id":18396},"technologies-well-use","Technologies we'll use",[123,18399,18400,18407,18415,18421],{},[74,18401,18402,18406],{},[47,18403,10840],{"href":18404,"rel":18405},"https:\u002F\u002Fnuxt.com\u002F",[51],": Nuxt 3 is a powerful Vue.js framework that will serve as the foundation of our application.",[74,18408,18409,18414],{},[47,18410,18413],{"href":18411,"rel":18412},"https:\u002F\u002Fui.nuxt.com\u002F",[51],"Nuxt UI",": A Nuxt module that will help us create a sleek and responsive interface.",[74,18416,18417,18420],{},[47,18418,3194],{"href":18328,"rel":18419},[51],": NuxtHub is a deployment and administration platform for Nuxt, powered by Cloudflare. We can use NuxtHub to access different Cloudflare offerings like D1 database, Workers KV, R2 Storage, Workers AI etc. In this project, we'll use it to access the LLMs as well as to deploy our project",[74,18422,18423,18428],{},[47,18424,18427],{"href":18425,"rel":18426},"https:\u002F\u002Fgithub.com\u002Fnuxt-modules\u002Fmdc",[51],"Nuxt MDC",": For parsing and displaying the chat messages",[32,18430,3267],{"id":3266},[11,18432,18433],{},"Apart from the basic prerequisites like Node\u002FNpm, Code Editors, and some VueJs\u002FNuxt knowledge, you'll need the following to follow along:",[123,18435,18436,18445],{},[74,18437,18438,18441,18442,183],{},[94,18439,18440],{},"A Cloudflare account:"," To use the Workers AI models, as well as to deploy your project on Cloudflare Pages for free. If you don't have it already, you can set it up ",[47,18443,3286],{"href":3282,"rel":18444},[51],[74,18446,18447,18450,18451,183],{},[94,18448,18449],{},"A NuxtHub Admin Account:"," NuxtHub admin is a web based dashboard to manage NuxtHub apps. You can create your account ",[47,18452,3286],{"href":3295,"rel":18453},[51],[32,18455,10912],{"id":10909},[11,18457,18458],{},"We can either start with a Nuxt (or Nuxt UI) template and add NuxtHub on top of it, or we can use the NuxtHub starter template and add Nuxt UI to it. We'll take the second approach:",[123,18460,18461,18500,18521],{},[74,18462,18463,18464],{},"Create a new NuxtHub project",[269,18465,18467],{"className":3335,"code":18466,"language":3337,"meta":274,"style":274},"# Init the project and install dependencies\nnpx nuxthub init cf-playground\n\n# Change into the created dir\ncd cf-playground\n",[59,18468,18469,18474,18485,18489,18494],{"__ignoreMap":274},[278,18470,18471],{"class":280,"line":281},[278,18472,18473],{"class":284},"# Init the project and install dependencies\n",[278,18475,18476,18478,18480,18482],{"class":280,"line":288},[278,18477,3349],{"class":333},[278,18479,3352],{"class":309},[278,18481,3355],{"class":309},[278,18483,18484],{"class":309}," cf-playground\n",[278,18486,18487],{"class":280,"line":295},[278,18488,292],{"emptyLinePlaceholder":291},[278,18490,18491],{"class":280,"line":316},[278,18492,18493],{"class":284},"# Change into the created dir\n",[278,18495,18496,18498],{"class":280,"line":322},[278,18497,3364],{"class":650},[278,18499,18484],{"class":309},[74,18501,18502,18503],{},"Add the Nuxt UI module. The below command will install the @nuxt\u002Fui dependency as well as add it as a module in your nuxt config file",[269,18504,18506],{"className":3335,"code":18505,"language":3337,"meta":274,"style":274},"npx nuxi module add ui\n",[59,18507,18508],{"__ignoreMap":274},[278,18509,18510,18512,18514,18516,18518],{"class":280,"line":281},[278,18511,3349],{"class":333},[278,18513,10942],{"class":309},[278,18515,10945],{"class":309},[278,18517,3418],{"class":309},[278,18519,18520],{"class":309}," ui\n",[74,18522,18523,18524],{},"Similarly, add the Nuxt MDC module",[269,18525,18527],{"className":3335,"code":18526,"language":3337,"meta":274,"style":274},"npx nuxi module add mdc\n",[59,18528,18529],{"__ignoreMap":274},[278,18530,18531,18533,18535,18537,18539],{"class":280,"line":281},[278,18532,3349],{"class":333},[278,18534,10942],{"class":309},[278,18536,10945],{"class":309},[278,18538,3418],{"class":309},[278,18540,18541],{"class":309}," mdc\n",[11,18543,18544,18545,18547,18548,18551],{},"Now we have setup everything that we need for this project. You can try running the project with ",[59,18546,5739],{}," (or an equivalent command if you're using a different package manager) and visit ",[47,18549,3772],{"href":3772,"rel":18550},[51]," in your browser. If you've dark mode enabled on your system then you'll notice that dark mode is already up and running in the project (We just need a way to toggle it).",[11,18553,18554],{},"With our development environment set up, we're ready to start building our LLM playground. In the next section, we'll begin implementing our user interface using NuxtUI components.",[24,18556,18558],{"id":18557},"creating-the-ui-components","Creating the UI Components",[11,18560,18561,18562,183],{},"First, we will create a component that will let us set numerical values for LLM settings using either a range slider or an input box. Let's call it ",[59,18563,18564],{},"RangeInput",[32,18566,18568],{"id":18567},"the-rangeinput-component","The RangeInput Component",[11,18570,18571],{},"We utilize a FormGroup component from NuxtUI to create this component. We put a Range slider in the default slot and an input in the hint slot to achieve the desired outcome. Here is the complete component code",[269,18573,18575],{"className":7132,"code":18574,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003CUFormGroup :label=\"label\" :ui=\"{ container: 'mt-2' }\">\n    \u003Ctemplate #hint>\n      \u003CUInput\n        v-model=\"model\"\n        class=\"w-[72px]\"\n        type=\"number\"\n        :min=\"min\"\n        :max=\"max\"\n        :step=\"step\"\n      \u002F>\n    \u003C\u002Ftemplate>\n    \u003CURange v-model=\"model\" :min=\"min\" :max=\"max\" :step=\"step\" size=\"sm\" \u002F>\n  \u003C\u002FUFormGroup>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst model = defineModel({ type: Number, default: undefined });\n\ndefineProps({\n  label: {\n    type: String,\n    required: true,\n  },\n  min: {\n    type: Number,\n    default: undefined,\n  },\n  max: {\n    type: Number,\n    default: undefined,\n  },\n  step: {\n    type: Number,\n    default: undefined,\n  },\n});\n\u003C\u002Fscript>\n",[59,18576,18577,18581,18586,18591,18596,18601,18606,18611,18616,18621,18626,18630,18634,18639,18644,18648,18652,18656,18661,18665,18670,18675,18680,18685,18689,18694,18699,18704,18708,18713,18717,18721,18725,18730,18734,18738,18742,18746],{"__ignoreMap":274},[278,18578,18579],{"class":280,"line":281},[278,18580,7146],{},[278,18582,18583],{"class":280,"line":288},[278,18584,18585],{},"  \u003CUFormGroup :label=\"label\" :ui=\"{ container: 'mt-2' }\">\n",[278,18587,18588],{"class":280,"line":295},[278,18589,18590],{},"    \u003Ctemplate #hint>\n",[278,18592,18593],{"class":280,"line":316},[278,18594,18595],{},"      \u003CUInput\n",[278,18597,18598],{"class":280,"line":322},[278,18599,18600],{},"        v-model=\"model\"\n",[278,18602,18603],{"class":280,"line":327},[278,18604,18605],{},"        class=\"w-[72px]\"\n",[278,18607,18608],{"class":280,"line":340},[278,18609,18610],{},"        type=\"number\"\n",[278,18612,18613],{"class":280,"line":349},[278,18614,18615],{},"        :min=\"min\"\n",[278,18617,18618],{"class":280,"line":375},[278,18619,18620],{},"        :max=\"max\"\n",[278,18622,18623],{"class":280,"line":386},[278,18624,18625],{},"        :step=\"step\"\n",[278,18627,18628],{"class":280,"line":397},[278,18629,7308],{},[278,18631,18632],{"class":280,"line":408},[278,18633,7313],{},[278,18635,18636],{"class":280,"line":433},[278,18637,18638],{},"    \u003CURange v-model=\"model\" :min=\"min\" :max=\"max\" :step=\"step\" size=\"sm\" \u002F>\n",[278,18640,18641],{"class":280,"line":454},[278,18642,18643],{},"  \u003C\u002FUFormGroup>\n",[278,18645,18646],{"class":280,"line":475},[278,18647,7422],{},[278,18649,18650],{"class":280,"line":496},[278,18651,292],{"emptyLinePlaceholder":291},[278,18653,18654],{"class":280,"line":505},[278,18655,7431],{},[278,18657,18658],{"class":280,"line":516},[278,18659,18660],{},"const model = defineModel({ type: Number, default: undefined });\n",[278,18662,18663],{"class":280,"line":527},[278,18664,292],{"emptyLinePlaceholder":291},[278,18666,18667],{"class":280,"line":533},[278,18668,18669],{},"defineProps({\n",[278,18671,18672],{"class":280,"line":539},[278,18673,18674],{},"  label: {\n",[278,18676,18677],{"class":280,"line":545},[278,18678,18679],{},"    type: String,\n",[278,18681,18682],{"class":280,"line":551},[278,18683,18684],{},"    required: true,\n",[278,18686,18687],{"class":280,"line":557},[278,18688,683],{},[278,18690,18691],{"class":280,"line":567},[278,18692,18693],{},"  min: {\n",[278,18695,18696],{"class":280,"line":577},[278,18697,18698],{},"    type: Number,\n",[278,18700,18701],{"class":280,"line":587},[278,18702,18703],{},"    default: undefined,\n",[278,18705,18706],{"class":280,"line":597},[278,18707,683],{},[278,18709,18710],{"class":280,"line":608},[278,18711,18712],{},"  max: {\n",[278,18714,18715],{"class":280,"line":614},[278,18716,18698],{},[278,18718,18719],{"class":280,"line":620},[278,18720,18703],{},[278,18722,18723],{"class":280,"line":625},[278,18724,683],{},[278,18726,18727],{"class":280,"line":640},[278,18728,18729],{},"  step: {\n",[278,18731,18732],{"class":280,"line":663},[278,18733,18698],{},[278,18735,18736],{"class":280,"line":669},[278,18737,18703],{},[278,18739,18740],{"class":280,"line":680},[278,18741,683],{},[278,18743,18744],{"class":280,"line":686},[278,18745,3693],{},[278,18747,18748],{"class":280,"line":1334},[278,18749,7691],{},[11,18751,18752,18753,18756],{},"Instead of taking the model value as a prop and then emitting the changes manually, we use the ",[59,18754,18755],{},"defineModel"," macro to achieve two way binding. If the initial model value is undefined the input box will be empty. This will help in making some of the LLM params optional.",[32,18758,18760],{"id":18759},"the-llmsettings-component","The LLMSettings Component",[11,18762,18763],{},"LLMSettings component utilizes many RangeInputs that we created above as most of the settings are numerical values. Apart from RangeInputs we use a textarea for the system prompt, a toggle for enabling\u002Fdisabling response streaming, a select menu for choosing the LLM model, and an accordion for hiding the optional params.",[11,18765,18766],{},"Here are the relevant parts of the component",[269,18768,18770],{"className":7132,"code":18769,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003Cdiv class=\"h-full flex flex-col overflow-hidden\">\n    \u003C!-- Settings Header Code -->\n    \u003CUDivider \u002F>\n    \u003Cdiv class=\"p-4 flex-1 space-y-6 overflow-y-auto\">\n      \u003CUFormGroup label=\"Model\">\n        \u003CUSelectMenu\n          v-model=\"llmParams.model\"\n          size=\"md\"\n          :options=\"models\"\n          value-attribute=\"id\"\n          option-attribute=\"name\"\n        \u002F>\n      \u003C\u002FUFormGroup>\n\n      \u003CRangeInput\n        v-model=\"llmParams.temperature\"\n        label=\"Temperature\"\n        :min=\"0\"\n        :max=\"5\"\n        :step=\"0.1\"\n      \u002F>\n\n      \u003CRangeInput\n        v-model=\"llmParams.maxTokens\"\n        label=\"Max Tokens\"\n        :min=\"1\"\n        :max=\"4096\"\n      \u002F>\n\n      \u003CUFormGroup label=\"System Prompt\">\n        \u003CUTextarea\n          v-model=\"llmParams.systemPrompt\"\n          :rows=\"3\"\n          :maxrows=\"8\"\n          autoresize\n        \u002F>\n      \u003C\u002FUFormGroup>\n\n      \u003Cdiv class=\"flex items-center justify-between\">\n        \u003Cspan>Stream Response\u003C\u002Fspan>\n        \u003CUToggle v-model=\"llmParams.stream\" \u002F>\n      \u003C\u002Fdiv>\n\n      \u003CUAccordion\n        :items=\"accordionItems\"\n        color=\"white\"\n        variant=\"solid\"\n        size=\"md\"\n      >\n        \u003Ctemplate #item>\n          \u003CUCard :ui=\"{ body: { base: 'space-y-6', padding: 'p-4 sm:p-4' } }\">\n            \u003CRangeInput\n              v-model=\"llmParams.topP\"\n              label=\"Top P\"\n              :min=\"0\"\n              :max=\"2\"\n              :step=\"0.1\"\n            \u002F>\n\n            \u003C!-- Other optional params -->\n          \u003C\u002FUCard>\n        \u003C\u002Ftemplate>\n      \u003C\u002FUAccordion>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ntype LlmParams = {\n  model: string;\n  temperature: number;\n  maxTokens: number;\n  topP?: number;\n  topK?: number;\n  frequencyPenalty?: number;\n  presencePenalty?: number;\n  systemPrompt: string;\n  stream: boolean;\n};\n\nconst llmParams = defineModel('llmParams', {\n  type: Object as () => LlmParams,\n  required: true,\n});\n\ndefineEmits(['hideDrawer', 'reset']);\n\nconst accordionItems = [\n  {\n    label: 'Advanced Settings',\n    defaultOpen: false,\n  },\n];\n\nconst models = [\n  {\n    name: 'deepseek-coder-6.7b-base-awq',\n    id: '@hf\u002Fthebloke\u002Fdeepseek-coder-6.7b-base-awq',\n  },\n  { \n    name: 'llama-3-8b-instruct', \n    id: '@cf\u002Fmeta\u002Fllama-3-8b-instruct', \n  },\n  \u002F\u002F ...other models\n]\n\u003C\u002Fscript>\n",[59,18771,18772,18776,18781,18786,18791,18796,18801,18806,18811,18816,18821,18826,18831,18835,18840,18844,18849,18854,18859,18864,18869,18874,18878,18882,18886,18891,18896,18901,18906,18910,18914,18919,18923,18928,18933,18938,18943,18947,18951,18955,18960,18965,18970,18974,18978,18983,18988,18993,18998,19003,19007,19012,19017,19022,19027,19032,19037,19042,19047,19051,19055,19060,19065,19069,19074,19078,19083,19087,19091,19095,19100,19105,19110,19115,19120,19125,19130,19135,19140,19145,19149,19153,19158,19163,19168,19172,19176,19181,19185,19190,19194,19199,19204,19208,19212,19216,19221,19225,19230,19235,19239,19244,19249,19254,19258,19263,19268],{"__ignoreMap":274},[278,18773,18774],{"class":280,"line":281},[278,18775,7146],{},[278,18777,18778],{"class":280,"line":288},[278,18779,18780],{},"  \u003Cdiv class=\"h-full flex flex-col overflow-hidden\">\n",[278,18782,18783],{"class":280,"line":295},[278,18784,18785],{},"    \u003C!-- Settings Header Code -->\n",[278,18787,18788],{"class":280,"line":316},[278,18789,18790],{},"    \u003CUDivider \u002F>\n",[278,18792,18793],{"class":280,"line":322},[278,18794,18795],{},"    \u003Cdiv class=\"p-4 flex-1 space-y-6 overflow-y-auto\">\n",[278,18797,18798],{"class":280,"line":327},[278,18799,18800],{},"      \u003CUFormGroup label=\"Model\">\n",[278,18802,18803],{"class":280,"line":340},[278,18804,18805],{},"        \u003CUSelectMenu\n",[278,18807,18808],{"class":280,"line":349},[278,18809,18810],{},"          v-model=\"llmParams.model\"\n",[278,18812,18813],{"class":280,"line":375},[278,18814,18815],{},"          size=\"md\"\n",[278,18817,18818],{"class":280,"line":386},[278,18819,18820],{},"          :options=\"models\"\n",[278,18822,18823],{"class":280,"line":397},[278,18824,18825],{},"          value-attribute=\"id\"\n",[278,18827,18828],{"class":280,"line":408},[278,18829,18830],{},"          option-attribute=\"name\"\n",[278,18832,18833],{"class":280,"line":433},[278,18834,7274],{},[278,18836,18837],{"class":280,"line":454},[278,18838,18839],{},"      \u003C\u002FUFormGroup>\n",[278,18841,18842],{"class":280,"line":475},[278,18843,292],{"emptyLinePlaceholder":291},[278,18845,18846],{"class":280,"line":496},[278,18847,18848],{},"      \u003CRangeInput\n",[278,18850,18851],{"class":280,"line":505},[278,18852,18853],{},"        v-model=\"llmParams.temperature\"\n",[278,18855,18856],{"class":280,"line":516},[278,18857,18858],{},"        label=\"Temperature\"\n",[278,18860,18861],{"class":280,"line":527},[278,18862,18863],{},"        :min=\"0\"\n",[278,18865,18866],{"class":280,"line":533},[278,18867,18868],{},"        :max=\"5\"\n",[278,18870,18871],{"class":280,"line":539},[278,18872,18873],{},"        :step=\"0.1\"\n",[278,18875,18876],{"class":280,"line":545},[278,18877,7308],{},[278,18879,18880],{"class":280,"line":551},[278,18881,292],{"emptyLinePlaceholder":291},[278,18883,18884],{"class":280,"line":557},[278,18885,18848],{},[278,18887,18888],{"class":280,"line":567},[278,18889,18890],{},"        v-model=\"llmParams.maxTokens\"\n",[278,18892,18893],{"class":280,"line":577},[278,18894,18895],{},"        label=\"Max Tokens\"\n",[278,18897,18898],{"class":280,"line":587},[278,18899,18900],{},"        :min=\"1\"\n",[278,18902,18903],{"class":280,"line":597},[278,18904,18905],{},"        :max=\"4096\"\n",[278,18907,18908],{"class":280,"line":608},[278,18909,7308],{},[278,18911,18912],{"class":280,"line":614},[278,18913,292],{"emptyLinePlaceholder":291},[278,18915,18916],{"class":280,"line":620},[278,18917,18918],{},"      \u003CUFormGroup label=\"System Prompt\">\n",[278,18920,18921],{"class":280,"line":625},[278,18922,7244],{},[278,18924,18925],{"class":280,"line":640},[278,18926,18927],{},"          v-model=\"llmParams.systemPrompt\"\n",[278,18929,18930],{"class":280,"line":663},[278,18931,18932],{},"          :rows=\"3\"\n",[278,18934,18935],{"class":280,"line":669},[278,18936,18937],{},"          :maxrows=\"8\"\n",[278,18939,18940],{"class":280,"line":680},[278,18941,18942],{},"          autoresize\n",[278,18944,18945],{"class":280,"line":686},[278,18946,7274],{},[278,18948,18949],{"class":280,"line":1334},[278,18950,18839],{},[278,18952,18953],{"class":280,"line":1375},[278,18954,292],{"emptyLinePlaceholder":291},[278,18956,18957],{"class":280,"line":1381},[278,18958,18959],{},"      \u003Cdiv class=\"flex items-center justify-between\">\n",[278,18961,18962],{"class":280,"line":1386},[278,18963,18964],{},"        \u003Cspan>Stream Response\u003C\u002Fspan>\n",[278,18966,18967],{"class":280,"line":1394},[278,18968,18969],{},"        \u003CUToggle v-model=\"llmParams.stream\" \u002F>\n",[278,18971,18972],{"class":280,"line":1406},[278,18973,7873],{},[278,18975,18976],{"class":280,"line":1423},[278,18977,292],{"emptyLinePlaceholder":291},[278,18979,18980],{"class":280,"line":1432},[278,18981,18982],{},"      \u003CUAccordion\n",[278,18984,18985],{"class":280,"line":1437},[278,18986,18987],{},"        :items=\"accordionItems\"\n",[278,18989,18990],{"class":280,"line":1916},[278,18991,18992],{},"        color=\"white\"\n",[278,18994,18995],{"class":280,"line":1939},[278,18996,18997],{},"        variant=\"solid\"\n",[278,18999,19000],{"class":280,"line":1949},[278,19001,19002],{},"        size=\"md\"\n",[278,19004,19005],{"class":280,"line":1954},[278,19006,7357],{},[278,19008,19009],{"class":280,"line":1959},[278,19010,19011],{},"        \u003Ctemplate #item>\n",[278,19013,19014],{"class":280,"line":1985},[278,19015,19016],{},"          \u003CUCard :ui=\"{ body: { base: 'space-y-6', padding: 'p-4 sm:p-4' } }\">\n",[278,19018,19019],{"class":280,"line":1990},[278,19020,19021],{},"            \u003CRangeInput\n",[278,19023,19024],{"class":280,"line":1997},[278,19025,19026],{},"              v-model=\"llmParams.topP\"\n",[278,19028,19029],{"class":280,"line":2006},[278,19030,19031],{},"              label=\"Top P\"\n",[278,19033,19034],{"class":280,"line":2018},[278,19035,19036],{},"              :min=\"0\"\n",[278,19038,19039],{"class":280,"line":2029},[278,19040,19041],{},"              :max=\"2\"\n",[278,19043,19044],{"class":280,"line":2034},[278,19045,19046],{},"              :step=\"0.1\"\n",[278,19048,19049],{"class":280,"line":2040},[278,19050,554],{},[278,19052,19053],{"class":280,"line":2045},[278,19054,292],{"emptyLinePlaceholder":291},[278,19056,19057],{"class":280,"line":2068},[278,19058,19059],{},"            \u003C!-- Other optional params -->\n",[278,19061,19062],{"class":280,"line":2099},[278,19063,19064],{},"          \u003C\u002FUCard>\n",[278,19066,19067],{"class":280,"line":6428},[278,19068,7235],{},[278,19070,19071],{"class":280,"line":6439},[278,19072,19073],{},"      \u003C\u002FUAccordion>\n",[278,19075,19076],{"class":280,"line":6450},[278,19077,7950],{},[278,19079,19080],{"class":280,"line":6455},[278,19081,19082],{},"  \u003C\u002Fdiv>\n",[278,19084,19085],{"class":280,"line":6460},[278,19086,7422],{},[278,19088,19089],{"class":280,"line":6475},[278,19090,292],{"emptyLinePlaceholder":291},[278,19092,19093],{"class":280,"line":6486},[278,19094,7431],{},[278,19096,19097],{"class":280,"line":6491},[278,19098,19099],{},"type LlmParams = {\n",[278,19101,19102],{"class":280,"line":6518},[278,19103,19104],{},"  model: string;\n",[278,19106,19107],{"class":280,"line":6530},[278,19108,19109],{},"  temperature: number;\n",[278,19111,19112],{"class":280,"line":6542},[278,19113,19114],{},"  maxTokens: number;\n",[278,19116,19117],{"class":280,"line":6547},[278,19118,19119],{},"  topP?: number;\n",[278,19121,19122],{"class":280,"line":6552},[278,19123,19124],{},"  topK?: number;\n",[278,19126,19127],{"class":280,"line":6567},[278,19128,19129],{},"  frequencyPenalty?: number;\n",[278,19131,19132],{"class":280,"line":6580},[278,19133,19134],{},"  presencePenalty?: number;\n",[278,19136,19137],{"class":280,"line":6593},[278,19138,19139],{},"  systemPrompt: string;\n",[278,19141,19142],{"class":280,"line":6605},[278,19143,19144],{},"  stream: boolean;\n",[278,19146,19147],{"class":280,"line":6620},[278,19148,2817],{},[278,19150,19151],{"class":280,"line":6625},[278,19152,292],{"emptyLinePlaceholder":291},[278,19154,19155],{"class":280,"line":6633},[278,19156,19157],{},"const llmParams = defineModel('llmParams', {\n",[278,19159,19160],{"class":280,"line":6643},[278,19161,19162],{},"  type: Object as () => LlmParams,\n",[278,19164,19165],{"class":280,"line":6657},[278,19166,19167],{},"  required: true,\n",[278,19169,19170],{"class":280,"line":6665},[278,19171,3693],{},[278,19173,19174],{"class":280,"line":6670},[278,19175,292],{"emptyLinePlaceholder":291},[278,19177,19178],{"class":280,"line":6675},[278,19179,19180],{},"defineEmits(['hideDrawer', 'reset']);\n",[278,19182,19183],{"class":280,"line":6680},[278,19184,292],{"emptyLinePlaceholder":291},[278,19186,19187],{"class":280,"line":6698},[278,19188,19189],{},"const accordionItems = [\n",[278,19191,19192],{"class":280,"line":6725},[278,19193,11470],{},[278,19195,19196],{"class":280,"line":6738},[278,19197,19198],{},"    label: 'Advanced Settings',\n",[278,19200,19201],{"class":280,"line":6752},[278,19202,19203],{},"    defaultOpen: false,\n",[278,19205,19206],{"class":280,"line":6769},[278,19207,683],{},[278,19209,19210],{"class":280,"line":6786},[278,19211,11714],{},[278,19213,19214],{"class":280,"line":6798},[278,19215,292],{"emptyLinePlaceholder":291},[278,19217,19218],{"class":280,"line":6803},[278,19219,19220],{},"const models = [\n",[278,19222,19223],{"class":280,"line":6815},[278,19224,11470],{},[278,19226,19227],{"class":280,"line":6827},[278,19228,19229],{},"    name: 'deepseek-coder-6.7b-base-awq',\n",[278,19231,19232],{"class":280,"line":6839},[278,19233,19234],{},"    id: '@hf\u002Fthebloke\u002Fdeepseek-coder-6.7b-base-awq',\n",[278,19236,19237],{"class":280,"line":6844},[278,19238,683],{},[278,19240,19241],{"class":280,"line":6853},[278,19242,19243],{},"  { \n",[278,19245,19246],{"class":280,"line":6859},[278,19247,19248],{},"    name: 'llama-3-8b-instruct', \n",[278,19250,19251],{"class":280,"line":6864},[278,19252,19253],{},"    id: '@cf\u002Fmeta\u002Fllama-3-8b-instruct', \n",[278,19255,19256],{"class":280,"line":6877},[278,19257,683],{},[278,19259,19260],{"class":280,"line":6887},[278,19261,19262],{},"  \u002F\u002F ...other models\n",[278,19264,19265],{"class":280,"line":6918},[278,19266,19267],{},"]\n",[278,19269,19270],{"class":280,"line":6923},[278,19271,7691],{},[32,19273,19275],{"id":19274},"the-chatpanel-component","The ChatPanel Component",[11,19277,19278],{},"This is the most important component of all as it handles the core chat functionality of the app. It consists of three parts:",[11,19280,19281],{},[94,19282,19283],{},"Chat Header",[11,19285,19286],{},"It displays the app name\u002Flabel apart from some global buttons for clearing chats, dark mode toggling and for showing the settings drawer on mobile devices",[269,19288,19290],{"className":7132,"code":19289,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center justify-between p-4\">\n    \u003Cdiv class=\"flex items-center gap-x-4\">\n      \u003Ch2 class=\"text-xl md:text-2xl text-primary font-bold\">Hub Chat\u003C\u002Fh2>\n      \u003CUTooltip text=\"Clear chat\">\n        \u003CUButton\n          color=\"gray\"\n          icon=\"i-heroicons-trash\"\n          size=\"xs\"\n          :disabled=\"clearDisabled\"\n          @click=\"$emit('clear')\"\n        \u002F>\n      \u003C\u002FUTooltip>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"flex items-center gap-x-4\">\n      \u003CColorMode \u002F>\n      \u003CUButton\n        icon=\"i-heroicons-cog-6-tooth\"\n        color=\"gray\"\n        variant=\"ghost\"\n        class=\"md:hidden\"\n        @click=\"$emit('showDrawer')\"\n      \u002F>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ndefineEmits(['clear', 'showDrawer']);\n\ndefineProps({\n  clearDisabled: {\n    type: Boolean,\n    default: true,\n  },\n});\n\u003C\u002Fscript>\n",[59,19291,19292,19296,19301,19306,19311,19316,19320,19325,19330,19335,19340,19345,19349,19354,19358,19362,19367,19371,19376,19381,19386,19391,19396,19400,19404,19408,19412,19416,19420,19425,19429,19433,19438,19443,19448,19452,19456],{"__ignoreMap":274},[278,19293,19294],{"class":280,"line":281},[278,19295,7146],{},[278,19297,19298],{"class":280,"line":288},[278,19299,19300],{},"  \u003Cdiv class=\"flex items-center justify-between p-4\">\n",[278,19302,19303],{"class":280,"line":295},[278,19304,19305],{},"    \u003Cdiv class=\"flex items-center gap-x-4\">\n",[278,19307,19308],{"class":280,"line":316},[278,19309,19310],{},"      \u003Ch2 class=\"text-xl md:text-2xl text-primary font-bold\">Hub Chat\u003C\u002Fh2>\n",[278,19312,19313],{"class":280,"line":322},[278,19314,19315],{},"      \u003CUTooltip text=\"Clear chat\">\n",[278,19317,19318],{"class":280,"line":327},[278,19319,7844],{},[278,19321,19322],{"class":280,"line":340},[278,19323,19324],{},"          color=\"gray\"\n",[278,19326,19327],{"class":280,"line":349},[278,19328,19329],{},"          icon=\"i-heroicons-trash\"\n",[278,19331,19332],{"class":280,"line":375},[278,19333,19334],{},"          size=\"xs\"\n",[278,19336,19337],{"class":280,"line":386},[278,19338,19339],{},"          :disabled=\"clearDisabled\"\n",[278,19341,19342],{"class":280,"line":397},[278,19343,19344],{},"          @click=\"$emit('clear')\"\n",[278,19346,19347],{"class":280,"line":408},[278,19348,7274],{},[278,19350,19351],{"class":280,"line":433},[278,19352,19353],{},"      \u003C\u002FUTooltip>\n",[278,19355,19356],{"class":280,"line":454},[278,19357,7950],{},[278,19359,19360],{"class":280,"line":475},[278,19361,19305],{},[278,19363,19364],{"class":280,"line":496},[278,19365,19366],{},"      \u003CColorMode \u002F>\n",[278,19368,19369],{"class":280,"line":505},[278,19370,7327],{},[278,19372,19373],{"class":280,"line":516},[278,19374,19375],{},"        icon=\"i-heroicons-cog-6-tooth\"\n",[278,19377,19378],{"class":280,"line":527},[278,19379,19380],{},"        color=\"gray\"\n",[278,19382,19383],{"class":280,"line":533},[278,19384,19385],{},"        variant=\"ghost\"\n",[278,19387,19388],{"class":280,"line":539},[278,19389,19390],{},"        class=\"md:hidden\"\n",[278,19392,19393],{"class":280,"line":545},[278,19394,19395],{},"        @click=\"$emit('showDrawer')\"\n",[278,19397,19398],{"class":280,"line":551},[278,19399,7308],{},[278,19401,19402],{"class":280,"line":557},[278,19403,7950],{},[278,19405,19406],{"class":280,"line":567},[278,19407,19082],{},[278,19409,19410],{"class":280,"line":577},[278,19411,7422],{},[278,19413,19414],{"class":280,"line":587},[278,19415,292],{"emptyLinePlaceholder":291},[278,19417,19418],{"class":280,"line":597},[278,19419,7431],{},[278,19421,19422],{"class":280,"line":608},[278,19423,19424],{},"defineEmits(['clear', 'showDrawer']);\n",[278,19426,19427],{"class":280,"line":614},[278,19428,292],{"emptyLinePlaceholder":291},[278,19430,19431],{"class":280,"line":620},[278,19432,18669],{},[278,19434,19435],{"class":280,"line":625},[278,19436,19437],{},"  clearDisabled: {\n",[278,19439,19440],{"class":280,"line":640},[278,19441,19442],{},"    type: Boolean,\n",[278,19444,19445],{"class":280,"line":663},[278,19446,19447],{},"    default: true,\n",[278,19449,19450],{"class":280,"line":669},[278,19451,683],{},[278,19453,19454],{"class":280,"line":680},[278,19455,3693],{},[278,19457,19458],{"class":280,"line":686},[278,19459,7691],{},[11,19461,19462],{},[94,19463,19464],{},"Chats Container",[11,19466,19467],{},"For parsing and displaying the chat messages. It also shows a message loading skeleton when streaming is off, as well as a NoChats placeholder when needed.",[269,19469,19471],{"className":7132,"code":19470,"language":7134,"meta":274,"style":274},"\u003Cdiv ref=\"chatContainer\" class=\"flex-1 overflow-y-auto p-4 space-y-5\">\n  \u003Cdiv\n    v-for=\"(message, index) in chatHistory\"\n    :key=\"`message-${index}`\"\n    class=\"flex items-start gap-x-4\"\n  >\n    \u003Cdiv\n      class=\"w-12 h-12 p-2 rounded-full\"\n      :class=\"`${\n        message.role === 'user' ? 'bg-primary\u002F20' : 'bg-blue-500\u002F20'\n      }`\"\n    >\n      \u003CUIcon\n        :name=\"`${\n          message.role === 'user'\n            ? 'i-mdi-user'\n            : 'i-heroicons-sparkles-solid'\n        }`\"\n        class=\"w-8 h-8\"\n        :class=\"`${\n          message.role === 'user' ? 'text-primary-400' : 'text-blue-400'\n        }`\"\n      \u002F>\n    \u003C\u002Fdiv>\n    \u003Cdiv v-if=\"message.role === 'user'\">\n      {{ message.content }}\n    \u003C\u002Fdiv>\n    \u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n  \u003C\u002Fdiv>\n  \u003CChatLoadingSkeleton v-if=\"loading === 'message'\" \u002F>\n  \u003CNoChats v-if=\"chatHistory.length === 0\" class=\"h-full\" \u002F>\n\u003C\u002Fdiv>\n",[59,19472,19473,19478,19483,19488,19493,19498,19502,19506,19511,19516,19521,19526,19530,19535,19540,19545,19550,19555,19560,19565,19570,19575,19579,19583,19587,19592,19597,19601,19606,19610,19615,19620],{"__ignoreMap":274},[278,19474,19475],{"class":280,"line":281},[278,19476,19477],{},"\u003Cdiv ref=\"chatContainer\" class=\"flex-1 overflow-y-auto p-4 space-y-5\">\n",[278,19479,19480],{"class":280,"line":288},[278,19481,19482],{},"  \u003Cdiv\n",[278,19484,19485],{"class":280,"line":295},[278,19486,19487],{},"    v-for=\"(message, index) in chatHistory\"\n",[278,19489,19490],{"class":280,"line":316},[278,19491,19492],{},"    :key=\"`message-${index}`\"\n",[278,19494,19495],{"class":280,"line":322},[278,19496,19497],{},"    class=\"flex items-start gap-x-4\"\n",[278,19499,19500],{"class":280,"line":327},[278,19501,7200],{},[278,19503,19504],{"class":280,"line":340},[278,19505,7920],{},[278,19507,19508],{"class":280,"line":349},[278,19509,19510],{},"      class=\"w-12 h-12 p-2 rounded-full\"\n",[278,19512,19513],{"class":280,"line":375},[278,19514,19515],{},"      :class=\"`${\n",[278,19517,19518],{"class":280,"line":386},[278,19519,19520],{},"        message.role === 'user' ? 'bg-primary\u002F20' : 'bg-blue-500\u002F20'\n",[278,19522,19523],{"class":280,"line":397},[278,19524,19525],{},"      }`\"\n",[278,19527,19528],{"class":280,"line":408},[278,19529,7935],{},[278,19531,19532],{"class":280,"line":433},[278,19533,19534],{},"      \u003CUIcon\n",[278,19536,19537],{"class":280,"line":454},[278,19538,19539],{},"        :name=\"`${\n",[278,19541,19542],{"class":280,"line":475},[278,19543,19544],{},"          message.role === 'user'\n",[278,19546,19547],{"class":280,"line":496},[278,19548,19549],{},"            ? 'i-mdi-user'\n",[278,19551,19552],{"class":280,"line":505},[278,19553,19554],{},"            : 'i-heroicons-sparkles-solid'\n",[278,19556,19557],{"class":280,"line":516},[278,19558,19559],{},"        }`\"\n",[278,19561,19562],{"class":280,"line":527},[278,19563,19564],{},"        class=\"w-8 h-8\"\n",[278,19566,19567],{"class":280,"line":533},[278,19568,19569],{},"        :class=\"`${\n",[278,19571,19572],{"class":280,"line":539},[278,19573,19574],{},"          message.role === 'user' ? 'text-primary-400' : 'text-blue-400'\n",[278,19576,19577],{"class":280,"line":545},[278,19578,19559],{},[278,19580,19581],{"class":280,"line":551},[278,19582,7308],{},[278,19584,19585],{"class":280,"line":557},[278,19586,7950],{},[278,19588,19589],{"class":280,"line":567},[278,19590,19591],{},"    \u003Cdiv v-if=\"message.role === 'user'\">\n",[278,19593,19594],{"class":280,"line":577},[278,19595,19596],{},"      {{ message.content }}\n",[278,19598,19599],{"class":280,"line":587},[278,19600,7950],{},[278,19602,19603],{"class":280,"line":597},[278,19604,19605],{},"    \u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n",[278,19607,19608],{"class":280,"line":608},[278,19609,19082],{},[278,19611,19612],{"class":280,"line":614},[278,19613,19614],{},"  \u003CChatLoadingSkeleton v-if=\"loading === 'message'\" \u002F>\n",[278,19616,19617],{"class":280,"line":620},[278,19618,19619],{},"  \u003CNoChats v-if=\"chatHistory.length === 0\" class=\"h-full\" \u002F>\n",[278,19621,19622],{"class":280,"line":625},[278,19623,19624],{},"\u003C\u002Fdiv>\n",[11,19626,19627,19628,1708,19631,19634,19635,19638],{},"To keep the user informed of the progress, we show a message loading skeleton when streaming is off. To do this we utilize a loading variable which can have three states: ",[59,19629,19630],{},"idle",[59,19632,19633],{},"message"," & ",[59,19636,19637],{},"stream",". So when a non-streaming request is made we set the ref to message and the loading skeleton is shown.",[11,19640,19641],{},[94,19642,19643],{},"User Message Textbox",[11,19645,19646],{},"For entering user message. It shows up as a single line textarea that resizes automatically when needed.",[269,19648,19650],{"className":7132,"code":19649,"language":7134,"meta":274,"style":274},"\u003Cdiv class=\"flex items-start p-3.5 relative\">\n  \u003CUTextarea\n    v-model=\"userMessage\"\n    placeholder=\"How can I help you today?\"\n    class=\"w-full\"\n    :ui=\"{ padding: { xl: 'pr-11' } }\"\n    :rows=\"1\"\n    :maxrows=\"5\"\n    :disabled=\"loading !== 'idle'\"\n    autoresize\n    size=\"xl\"\n    @keydown.enter.exact.prevent=\"sendMessage\"\n    @keydown.enter.shift.exact.prevent=\"userMessage += '\\n'\"\n  \u002F>\n\n  \u003CUButton\n    icon=\"i-heroicons-arrow-up-20-solid\"\n    class=\"absolute top-5 right-5\"\n    :disabled=\"loading !== 'idle'\"\n    @click=\"sendMessage\"\n  \u002F>\n\u003C\u002Fdiv>\n",[59,19651,19652,19657,19662,19667,19672,19677,19682,19687,19692,19697,19702,19707,19712,19717,19722,19726,19731,19736,19741,19745,19750,19754],{"__ignoreMap":274},[278,19653,19654],{"class":280,"line":281},[278,19655,19656],{},"\u003Cdiv class=\"flex items-start p-3.5 relative\">\n",[278,19658,19659],{"class":280,"line":288},[278,19660,19661],{},"  \u003CUTextarea\n",[278,19663,19664],{"class":280,"line":295},[278,19665,19666],{},"    v-model=\"userMessage\"\n",[278,19668,19669],{"class":280,"line":316},[278,19670,19671],{},"    placeholder=\"How can I help you today?\"\n",[278,19673,19674],{"class":280,"line":322},[278,19675,19676],{},"    class=\"w-full\"\n",[278,19678,19679],{"class":280,"line":327},[278,19680,19681],{},"    :ui=\"{ padding: { xl: 'pr-11' } }\"\n",[278,19683,19684],{"class":280,"line":340},[278,19685,19686],{},"    :rows=\"1\"\n",[278,19688,19689],{"class":280,"line":349},[278,19690,19691],{},"    :maxrows=\"5\"\n",[278,19693,19694],{"class":280,"line":375},[278,19695,19696],{},"    :disabled=\"loading !== 'idle'\"\n",[278,19698,19699],{"class":280,"line":386},[278,19700,19701],{},"    autoresize\n",[278,19703,19704],{"class":280,"line":397},[278,19705,19706],{},"    size=\"xl\"\n",[278,19708,19709],{"class":280,"line":408},[278,19710,19711],{},"    @keydown.enter.exact.prevent=\"sendMessage\"\n",[278,19713,19714],{"class":280,"line":433},[278,19715,19716],{},"    @keydown.enter.shift.exact.prevent=\"userMessage += '\\n'\"\n",[278,19718,19719],{"class":280,"line":454},[278,19720,19721],{},"  \u002F>\n",[278,19723,19724],{"class":280,"line":475},[278,19725,292],{"emptyLinePlaceholder":291},[278,19727,19728],{"class":280,"line":496},[278,19729,19730],{},"  \u003CUButton\n",[278,19732,19733],{"class":280,"line":505},[278,19734,19735],{},"    icon=\"i-heroicons-arrow-up-20-solid\"\n",[278,19737,19738],{"class":280,"line":516},[278,19739,19740],{},"    class=\"absolute top-5 right-5\"\n",[278,19742,19743],{"class":280,"line":527},[278,19744,19696],{},[278,19746,19747],{"class":280,"line":533},[278,19748,19749],{},"    @click=\"sendMessage\"\n",[278,19751,19752],{"class":280,"line":539},[278,19753,19721],{},[278,19755,19756],{"class":280,"line":545},[278,19757,19624],{},[11,19759,19760,19761,19764,19765,19768],{},"For allowing the user to send message on hitting enter, we use the keydown event listener with the needed modifiers (",[59,19762,19763],{},"@keydown.enter.exact.prevent","). Similarly, for adding a newline we use ",[59,19766,19767],{},"enter + shift"," keys.",[11,19770,19771],{},"Now that we have our UI components in place, let's shift our focus to the backend. We'll set up the AI integration and create an API endpoint to handle our chat requests. This will bridge the gap between our frontend interface and the AI model.",[24,19773,19775],{"id":19774},"setting-up-ai-and-api-endpoint","Setting up AI and API Endpoint",[11,19777,19778,19779,19781],{},"Integrating AI into our project is very simple thanks to NuxtHub. Let's look at our ",[59,19780,3490],{}," file in the root dir of the project.",[269,19783,19785],{"className":271,"code":19784,"language":273,"meta":274,"style":274},"\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config\nexport default defineNuxtConfig({\n  compatibilityDate: '2024-07-30',\n  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fupgrade#testing-nuxt-4\n  future: { compatibilityVersion: 4 },\n\n  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fmodules\n  modules: ['@nuxthub\u002Fcore', '@nuxt\u002Feslint', '@nuxtjs\u002Fmdc', \"@nuxt\u002Fui\"],\n\n  \u002F\u002F https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Finstallation#options\n  hub: {},\n\n  \u002F\u002F Env variables - https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fconfiguration#environment-variables-and-private-tokens\n  runtimeConfig: {\n    public: {\n      \u002F\u002F Can be overridden by NUXT_PUBLIC_HELLO_TEXT environment variable\n      helloText: 'Hello from the Edge 👋',\n    },\n  },\n\n  \u002F\u002F https:\u002F\u002Feslint.nuxt.com\n  eslint: {\n    config: {\n      stylistic: {\n        quotes: 'single',\n      },\n    },\n  },\n\n  \u002F\u002F https:\u002F\u002Fdevtools.nuxt.com\n  devtools: { enabled: true },\n});\n",[59,19786,19787,19792,19802,19811,19816,19824,19828,19833,19856,19860,19865,19870,19874,19879,19883,19887,19892,19901,19905,19909,19913,19918,19922,19926,19931,19941,19945,19949,19953,19957,19962,19970],{"__ignoreMap":274},[278,19788,19789],{"class":280,"line":281},[278,19790,19791],{"class":284},"\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config\n",[278,19793,19794,19796,19798,19800],{"class":280,"line":288},[278,19795,628],{"class":298},[278,19797,631],{"class":298},[278,19799,3505],{"class":333},[278,19801,637],{"class":302},[278,19803,19804,19806,19809],{"class":280,"line":295},[278,19805,3598],{"class":302},[278,19807,19808],{"class":309},"'2024-07-30'",[278,19810,660],{"class":302},[278,19812,19813],{"class":280,"line":316},[278,19814,19815],{"class":284},"  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fupgrade#testing-nuxt-4\n",[278,19817,19818,19820,19822],{"class":280,"line":322},[278,19819,3588],{"class":302},[278,19821,3591],{"class":650},[278,19823,3547],{"class":302},[278,19825,19826],{"class":280,"line":327},[278,19827,292],{"emptyLinePlaceholder":291},[278,19829,19830],{"class":280,"line":340},[278,19831,19832],{"class":284},"  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fmodules\n",[278,19834,19835,19837,19840,19842,19845,19847,19850,19852,19854],{"class":280,"line":349},[278,19836,3512],{"class":302},[278,19838,19839],{"class":309},"'@nuxthub\u002Fcore'",[278,19841,1708],{"class":302},[278,19843,19844],{"class":309},"'@nuxt\u002Feslint'",[278,19846,1708],{"class":302},[278,19848,19849],{"class":309},"'@nuxtjs\u002Fmdc'",[278,19851,1708],{"class":302},[278,19853,3530],{"class":309},[278,19855,3533],{"class":302},[278,19857,19858],{"class":280,"line":375},[278,19859,292],{"emptyLinePlaceholder":291},[278,19861,19862],{"class":280,"line":386},[278,19863,19864],{"class":284},"  \u002F\u002F https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Finstallation#options\n",[278,19866,19867],{"class":280,"line":397},[278,19868,19869],{"class":302},"  hub: {},\n",[278,19871,19872],{"class":280,"line":408},[278,19873,292],{"emptyLinePlaceholder":291},[278,19875,19876],{"class":280,"line":433},[278,19877,19878],{"class":284},"  \u002F\u002F Env variables - https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fconfiguration#environment-variables-and-private-tokens\n",[278,19880,19881],{"class":280,"line":454},[278,19882,3556],{"class":302},[278,19884,19885],{"class":280,"line":475},[278,19886,3561],{"class":302},[278,19888,19889],{"class":280,"line":496},[278,19890,19891],{"class":284},"      \u002F\u002F Can be overridden by NUXT_PUBLIC_HELLO_TEXT environment variable\n",[278,19893,19894,19896,19899],{"class":280,"line":505},[278,19895,3566],{"class":302},[278,19897,19898],{"class":309},"'Hello from the Edge 👋'",[278,19900,660],{"class":302},[278,19902,19903],{"class":280,"line":516},[278,19904,2243],{"class":302},[278,19906,19907],{"class":280,"line":527},[278,19908,683],{"class":302},[278,19910,19911],{"class":280,"line":533},[278,19912,292],{"emptyLinePlaceholder":291},[278,19914,19915],{"class":280,"line":539},[278,19916,19917],{"class":284},"  \u002F\u002F https:\u002F\u002Feslint.nuxt.com\n",[278,19919,19920],{"class":280,"line":545},[278,19921,3666],{"class":302},[278,19923,19924],{"class":280,"line":551},[278,19925,3671],{"class":302},[278,19927,19928],{"class":280,"line":557},[278,19929,19930],{"class":302},"      stylistic: {\n",[278,19932,19933,19936,19939],{"class":280,"line":567},[278,19934,19935],{"class":302},"        quotes: ",[278,19937,19938],{"class":309},"'single'",[278,19940,660],{"class":302},[278,19942,19943],{"class":280,"line":577},[278,19944,1165],{"class":302},[278,19946,19947],{"class":280,"line":587},[278,19948,2243],{"class":302},[278,19950,19951],{"class":280,"line":597},[278,19952,683],{"class":302},[278,19954,19955],{"class":280,"line":608},[278,19956,292],{"emptyLinePlaceholder":291},[278,19958,19959],{"class":280,"line":614},[278,19960,19961],{"class":284},"  \u002F\u002F https:\u002F\u002Fdevtools.nuxt.com\n",[278,19963,19964,19966,19968],{"class":280,"line":620},[278,19965,3542],{"class":302},[278,19967,2931],{"class":650},[278,19969,3547],{"class":302},[278,19971,19972],{"class":280,"line":625},[278,19973,3693],{"class":302},[11,19975,19976,19977,19980,19981,1245,19984,19986,19987,1245,19990,19993,19994,19997],{},"To enable AI, we just need to add the ",[59,19978,19979],{},"ai: true"," flag under hub config options above. If needed, we can enable other NuxtHub Cloudflare integrations like ",[59,19982,19983],{},"D1 database",[59,19985,16777],{},"), ",[59,19988,19989],{},"Workers KV",[59,19991,19992],{},"kv: true",") etc. While we are here, we can remove runtimeConfig as we don't need it. (You can also remove\u002Fmodify the ",[59,19995,19996],{},"eslint"," config as per your code editor settings).",[11,19999,20000],{},"If we want to use AI in dev mode (which we definitely do), we need to link this project to a Cloudflare project. This is needed as we will be talking directly to the Cloudflare APIs as no workers are running yet (and also because the AI usage will be linked to your Cloudflare account). This is a straight forward task, just run the following command",[269,20002,20003],{"className":3335,"code":4189,"language":3337,"meta":274,"style":274},[59,20004,20005],{"__ignoreMap":274},[278,20006,20007,20009,20011],{"class":280,"line":281},[278,20008,3349],{"class":333},[278,20010,3352],{"class":309},[278,20012,4200],{"class":309},[11,20014,20015],{},"Running the link command will make sure that we are logged into to our NuxtHub account, and will allow us to create a new NuxtHub project (backed by Cloudflare) or choose an existing one.",[32,20017,20019],{"id":20018},"creating-chat-api-endpoint","Creating Chat API Endpoint",[11,20021,20022,20023,1245,20026,20029,20030,20033,20034,20036],{},"Let's create a chat api endpoint. Create a new file ",[59,20024,20025],{},"chat.post.ts",[59,20027,20028],{},"\"post\""," in the file name signifies that this endpoint will only accept ",[59,20031,20032],{},"HTTP POST"," requests) in the ",[59,20035,3858],{}," directory and add the following code to it",[269,20038,20040],{"className":271,"code":20039,"language":273,"meta":274,"style":274},"export default defineEventHandler(async (event) => {\n  const { messages, params } = await readBody(event);\n  if (!messages || messages.length === 0 || !params) {\n    throw createError({\n      statusCode: 400,\n      statusMessage: 'Missing messages or LLM params',\n    });\n  }\n\n  const config = {\n    max_tokens: params.maxTokens,\n    temperature: params.temperature,\n    top_p: params.topP,\n    top_k: params.topK,\n    frequency_penalty: params.frequencyPenalty,\n    presence_penalty: params.presencePenalty,\n    stream: params.stream,\n  };\n\n  const ai = hubAI();\n\n  try {\n    const result = await ai.run(params.model, {\n      messages: params.systemPrompt\n        ? [{ role: 'system', content: params.systemPrompt }, ...messages]\n        : messages,\n      ...config,\n    });\n\n    return params.stream\n      ? sendStream(event, result as ReadableStream)\n      : (\n          result as {\n            response: string;\n          }\n        ).response;\n  } catch (error) {\n    console.error(error);\n    throw createError({\n      statusCode: 500,\n      statusMessage: 'Error processing request',\n    });\n  }\n});\n",[59,20041,20042,20064,20087,20117,20125,20133,20143,20147,20151,20155,20166,20171,20176,20181,20186,20191,20196,20201,20205,20209,20222,20226,20232,20250,20255,20273,20281,20288,20292,20296,20303,20320,20327,20336,20347,20351,20356,20364,20372,20380,20388,20397,20401,20405],{"__ignoreMap":274},[278,20043,20044,20046,20048,20050,20052,20054,20056,20058,20060,20062],{"class":280,"line":281},[278,20045,628],{"class":298},[278,20047,631],{"class":298},[278,20049,3878],{"class":333},[278,20051,1126],{"class":302},[278,20053,1050],{"class":298},[278,20055,1245],{"class":302},[278,20057,3887],{"class":501},[278,20059,1845],{"class":302},[278,20061,1848],{"class":298},[278,20063,876],{"class":302},[278,20065,20066,20068,20070,20072,20074,20077,20079,20081,20083,20085],{"class":280,"line":288},[278,20067,758],{"class":298},[278,20069,1009],{"class":302},[278,20071,16293],{"class":650},[278,20073,1708],{"class":302},[278,20075,20076],{"class":650},"params",[278,20078,1029],{"class":302},[278,20080,358],{"class":298},[278,20082,1120],{"class":298},[278,20084,16302],{"class":333},[278,20086,3910],{"class":302},[278,20088,20089,20091,20093,20095,20098,20100,20102,20104,20107,20109,20112,20114],{"class":280,"line":295},[278,20090,1062],{"class":298},[278,20092,1245],{"class":302},[278,20094,1209],{"class":298},[278,20096,20097],{"class":302},"messages ",[278,20099,5954],{"class":298},[278,20101,15501],{"class":302},[278,20103,15645],{"class":650},[278,20105,20106],{"class":298}," ===",[278,20108,6588],{"class":650},[278,20110,20111],{"class":298}," ||",[278,20113,6192],{"class":298},[278,20115,20116],{"class":302},"params) {\n",[278,20118,20119,20121,20123],{"class":280,"line":316},[278,20120,1426],{"class":298},[278,20122,3957],{"class":333},[278,20124,637],{"class":302},[278,20126,20127,20129,20131],{"class":280,"line":322},[278,20128,3964],{"class":302},[278,20130,3967],{"class":650},[278,20132,660],{"class":302},[278,20134,20135,20138,20141],{"class":280,"line":327},[278,20136,20137],{"class":302},"      statusMessage: ",[278,20139,20140],{"class":309},"'Missing messages or LLM params'",[278,20142,660],{"class":302},[278,20144,20145],{"class":280,"line":340},[278,20146,1233],{"class":302},[278,20148,20149],{"class":280,"line":349},[278,20150,1096],{"class":302},[278,20152,20153],{"class":280,"line":375},[278,20154,292],{"emptyLinePlaceholder":291},[278,20156,20157,20159,20162,20164],{"class":280,"line":386},[278,20158,758],{"class":298},[278,20160,20161],{"class":650}," config",[278,20163,764],{"class":298},[278,20165,876],{"class":302},[278,20167,20168],{"class":280,"line":397},[278,20169,20170],{"class":302},"    max_tokens: params.maxTokens,\n",[278,20172,20173],{"class":280,"line":408},[278,20174,20175],{"class":302},"    temperature: params.temperature,\n",[278,20177,20178],{"class":280,"line":433},[278,20179,20180],{"class":302},"    top_p: params.topP,\n",[278,20182,20183],{"class":280,"line":454},[278,20184,20185],{"class":302},"    top_k: params.topK,\n",[278,20187,20188],{"class":280,"line":475},[278,20189,20190],{"class":302},"    frequency_penalty: params.frequencyPenalty,\n",[278,20192,20193],{"class":280,"line":496},[278,20194,20195],{"class":302},"    presence_penalty: params.presencePenalty,\n",[278,20197,20198],{"class":280,"line":505},[278,20199,20200],{"class":302},"    stream: params.stream,\n",[278,20202,20203],{"class":280,"line":516},[278,20204,901],{"class":302},[278,20206,20207],{"class":280,"line":527},[278,20208,292],{"emptyLinePlaceholder":291},[278,20210,20211,20213,20216,20218,20220],{"class":280,"line":533},[278,20212,758],{"class":298},[278,20214,20215],{"class":650}," ai",[278,20217,764],{"class":298},[278,20219,4033],{"class":333},[278,20221,1313],{"class":302},[278,20223,20224],{"class":280,"line":539},[278,20225,292],{"emptyLinePlaceholder":291},[278,20227,20228,20230],{"class":280,"line":545},[278,20229,1105],{"class":298},[278,20231,876],{"class":302},[278,20233,20234,20236,20238,20240,20242,20245,20247],{"class":280,"line":551},[278,20235,1112],{"class":298},[278,20237,17991],{"class":650},[278,20239,764],{"class":298},[278,20241,1120],{"class":298},[278,20243,20244],{"class":302}," ai.",[278,20246,4039],{"class":333},[278,20248,20249],{"class":302},"(params.model, {\n",[278,20251,20252],{"class":280,"line":557},[278,20253,20254],{"class":302},"      messages: params.systemPrompt\n",[278,20256,20257,20260,20263,20265,20268,20270],{"class":280,"line":567},[278,20258,20259],{"class":298},"        ?",[278,20261,20262],{"class":302}," [{ role: ",[278,20264,16374],{"class":309},[278,20266,20267],{"class":302},", content: params.systemPrompt }, ",[278,20269,13370],{"class":298},[278,20271,20272],{"class":302},"messages]\n",[278,20274,20275,20278],{"class":280,"line":577},[278,20276,20277],{"class":298},"        :",[278,20279,20280],{"class":302}," messages,\n",[278,20282,20283,20285],{"class":280,"line":587},[278,20284,2000],{"class":298},[278,20286,20287],{"class":302},"config,\n",[278,20289,20290],{"class":280,"line":597},[278,20291,1233],{"class":302},[278,20293,20294],{"class":280,"line":608},[278,20295,292],{"emptyLinePlaceholder":291},[278,20297,20298,20300],{"class":280,"line":614},[278,20299,1088],{"class":298},[278,20301,20302],{"class":302}," params.stream\n",[278,20304,20305,20308,20311,20314,20316,20318],{"class":280,"line":620},[278,20306,20307],{"class":298},"      ?",[278,20309,20310],{"class":333}," sendStream",[278,20312,20313],{"class":302},"(event, result ",[278,20315,2937],{"class":298},[278,20317,14670],{"class":333},[278,20319,4590],{"class":302},[278,20321,20322,20325],{"class":280,"line":625},[278,20323,20324],{"class":298},"      :",[278,20326,346],{"class":302},[278,20328,20329,20332,20334],{"class":280,"line":640},[278,20330,20331],{"class":302},"          result ",[278,20333,2937],{"class":298},[278,20335,876],{"class":302},[278,20337,20338,20341,20343,20345],{"class":280,"line":663},[278,20339,20340],{"class":501},"            response",[278,20342,960],{"class":298},[278,20344,963],{"class":650},[278,20346,313],{"class":302},[278,20348,20349],{"class":280,"line":669},[278,20350,12122],{"class":302},[278,20352,20353],{"class":280,"line":680},[278,20354,20355],{"class":302},"        ).response;\n",[278,20357,20358,20360,20362],{"class":280,"line":686},[278,20359,1397],{"class":302},[278,20361,1400],{"class":298},[278,20363,1403],{"class":302},[278,20365,20366,20368,20370],{"class":280,"line":1334},[278,20367,1409],{"class":302},[278,20369,1412],{"class":333},[278,20371,12653],{"class":302},[278,20373,20374,20376,20378],{"class":280,"line":1375},[278,20375,1426],{"class":298},[278,20377,3957],{"class":333},[278,20379,637],{"class":302},[278,20381,20382,20384,20386],{"class":280,"line":1381},[278,20383,3964],{"class":302},[278,20385,2779],{"class":650},[278,20387,660],{"class":302},[278,20389,20390,20392,20395],{"class":280,"line":1386},[278,20391,20137],{"class":302},[278,20393,20394],{"class":309},"'Error processing request'",[278,20396,660],{"class":302},[278,20398,20399],{"class":280,"line":1394},[278,20400,1233],{"class":302},[278,20402,20403],{"class":280,"line":1406},[278,20404,1096],{"class":302},[278,20406,20407],{"class":280,"line":1423},[278,20408,3693],{"class":302},[11,20410,20411,20413],{},[59,20412,3833],{}," is a server composable that returns a Workers AI client for interacting with the LLMs. We pass the messages as well as the LLM params that we received from the frontend and run the requested model.",[11,20415,20416,20417,20420],{},"If you look at the above code carefully you'll notice that this endpoint supports both: streaming and non-streaming responses. To return a stream from the endpoint we just need to call the ",[59,20418,20419],{},"sendStream"," utility function.",[11,20422,20423],{},"We are through with our server side code. Let's look at how to handle the stream response on the client side in the next section.",[24,20425,20427],{"id":20426},"consuming-server-sent-events","Consuming Server Sent Events",[11,20429,20430],{},"Before diving into how to handle stream responses, let's understand why streaming is crucial for LLM interactions and how Server Sent Events (SSE) facilitate this process.",[32,20432,20434],{"id":20433},"why-stream-llm-responses-with-server-sent-events","Why Stream LLM Responses with Server Sent Events?",[11,20436,20437],{},"Large Language Models (LLMs) can take considerable time to generate complete responses, especially for complex queries. Traditionally, web applications wait for the entire response before displaying anything to the user. However, this approach can lead to long waiting times and a less engaging user experience.",[11,20439,20440,20445],{},[47,20441,20444],{"href":20442,"rel":20443},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events",[51],"Server Sent Events (SSE)"," offer a solution to this problem.",[3300,20447,20448,20450],{"dataNodeType":3302},[3300,20449,3785],{"dataNodeType":3305},[3300,20451,20452],{"dataNodeType":3309},"SSE is a technology that allows a server to push data to a web page at any time, enabling real-time updates without the need for the client to constantly request new information.",[11,20454,20455],{},"Here's how SSE benefits LLM responses:",[123,20457,20458,20461,20464],{},[74,20459,20460],{},"Immediate Feedback: As soon as the LLM starts generating content, it can be sent to the client and displayed, providing immediate feedback to the user.",[74,20462,20463],{},"Improved Perceived Performance: Users see content appearing progressively, giving the impression of a faster, more responsive system.",[74,20465,20466],{},"Real-time Interaction: The gradual appearance of text mimics human typing, creating a more natural and engaging conversational experience.",[3300,20468,20469,20471],{"dataNodeType":3302},[3300,20470,3785],{"dataNodeType":3305},[3300,20472,20473],{"dataNodeType":3309},"Think of it like ordering at a restaurant: Instead of waiting for all dishes to be prepared before serving, SSE allows the \"kitchen\" (server) to send out each \"dish\" (piece of the response) as soon as it's ready. This way, you start \"eating\" (reading and processing) sooner, and the overall experience feels faster and more satisfying.",[11,20475,20476],{},"Technically, SSE works by establishing a unidirectional channel from the server to the client. The server sends text data encoded in UTF-8, separated by newline characters. This simple yet effective mechanism allows for real-time updates in web applications, making it ideal for streaming LLM responses.",[11,20478,20479],{},"Now that we understand the importance of streaming for LLM responses and how SSE enables this, let's look at how to implement this in our Nuxt 3 application.",[32,20481,20483],{"id":20482},"handling-server-sent-events-with-nuxt-3-post-requests","Handling Server Sent Events with Nuxt 3 POST Requests",[11,20485,20486,20487,20492],{},"Since ours is a POST request, we need handle it differently. ",[47,20488,20491],{"href":20489,"rel":20490},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fdata-fetching#consuming-sse-server-sent-events-via-post-request",[51],"Nuxt 3 docs"," gives you an excellent starting point to do this. Reproducing the code from the docs",[269,20494,20496],{"className":271,"code":20495,"language":273,"meta":274,"style":274},"\u002F\u002F Make a POST request to the SSE endpoint\nconst response = await $fetch\u003CReadableStream>('\u002Fchats\u002Fask-ai', {\n  method: 'POST',\n  body: {\n    query: \"Hello AI, how are you?\",\n  },\n  responseType: 'stream',\n})\n\n\u002F\u002F Create a new ReadableStream from the response with TextDecoderStream to get the data as text\nconst reader = response.pipeThrough(new TextDecoderStream()).getReader()\n\n\u002F\u002F Read the chunk of data as we get it\nwhile (true) {\n  const { value, done } = await reader.read()\n\n  if (done)\n    break\n\n  console.log('Received:', value)\n}\n",[59,20497,20498,20503,20527,20536,20541,20551,20555,20564,20569,20573,20578,20603,20607,20612,20623,20647,20651,20658,20663,20667,20681],{"__ignoreMap":274},[278,20499,20500],{"class":280,"line":281},[278,20501,20502],{"class":284},"\u002F\u002F Make a POST request to the SSE endpoint\n",[278,20504,20505,20507,20509,20511,20513,20515,20517,20519,20522,20525],{"class":280,"line":288},[278,20506,5416],{"class":298},[278,20508,1115],{"class":650},[278,20510,764],{"class":298},[278,20512,1120],{"class":298},[278,20514,15262],{"class":333},[278,20516,1702],{"class":302},[278,20518,14575],{"class":333},[278,20520,20521],{"class":302},">(",[278,20523,20524],{"class":309},"'\u002Fchats\u002Fask-ai'",[278,20526,1132],{"class":302},[278,20528,20529,20532,20534],{"class":280,"line":295},[278,20530,20531],{"class":302},"  method: ",[278,20533,15273],{"class":309},[278,20535,660],{"class":302},[278,20537,20538],{"class":280,"line":316},[278,20539,20540],{"class":302},"  body: {\n",[278,20542,20543,20546,20549],{"class":280,"line":322},[278,20544,20545],{"class":302},"    query: ",[278,20547,20548],{"class":309},"\"Hello AI, how are you?\"",[278,20550,660],{"class":302},[278,20552,20553],{"class":280,"line":327},[278,20554,683],{"class":302},[278,20556,20557,20560,20562],{"class":280,"line":340},[278,20558,20559],{"class":302},"  responseType: ",[278,20561,15288],{"class":309},[278,20563,660],{"class":302},[278,20565,20566],{"class":280,"line":349},[278,20567,20568],{"class":302},"})\n",[278,20570,20571],{"class":280,"line":375},[278,20572,292],{"emptyLinePlaceholder":291},[278,20574,20575],{"class":280,"line":386},[278,20576,20577],{"class":284},"\u002F\u002F Create a new ReadableStream from the response with TextDecoderStream to get the data as text\n",[278,20579,20580,20582,20584,20586,20588,20590,20592,20594,20596,20599,20601],{"class":280,"line":397},[278,20581,5416],{"class":298},[278,20583,15318],{"class":650},[278,20585,764],{"class":298},[278,20587,1307],{"class":302},[278,20589,15336],{"class":333},[278,20591,1126],{"class":302},[278,20593,1173],{"class":298},[278,20595,15343],{"class":333},[278,20597,20598],{"class":302},"()).",[278,20600,15353],{"class":333},[278,20602,4601],{"class":302},[278,20604,20605],{"class":280,"line":408},[278,20606,292],{"emptyLinePlaceholder":291},[278,20608,20609],{"class":280,"line":433},[278,20610,20611],{"class":284},"\u002F\u002F Read the chunk of data as we get it\n",[278,20613,20614,20617,20619,20621],{"class":280,"line":454},[278,20615,20616],{"class":298},"while",[278,20618,1245],{"class":302},[278,20620,2931],{"class":650},[278,20622,1718],{"class":302},[278,20624,20625,20627,20629,20631,20633,20635,20637,20639,20641,20643,20645],{"class":280,"line":475},[278,20626,758],{"class":298},[278,20628,1009],{"class":302},[278,20630,14768],{"class":650},[278,20632,1708],{"class":302},[278,20634,15383],{"class":650},[278,20636,1029],{"class":302},[278,20638,358],{"class":298},[278,20640,1120],{"class":298},[278,20642,15392],{"class":302},[278,20644,15395],{"class":333},[278,20646,4601],{"class":302},[278,20648,20649],{"class":280,"line":496},[278,20650,292],{"emptyLinePlaceholder":291},[278,20652,20653,20655],{"class":280,"line":505},[278,20654,1062],{"class":298},[278,20656,20657],{"class":302}," (done)\n",[278,20659,20660],{"class":280,"line":516},[278,20661,20662],{"class":298},"    break\n",[278,20664,20665],{"class":280,"line":527},[278,20666,292],{"emptyLinePlaceholder":291},[278,20668,20669,20671,20673,20675,20678],{"class":280,"line":533},[278,20670,17975],{"class":302},[278,20672,14851],{"class":333},[278,20674,1126],{"class":302},[278,20676,20677],{"class":309},"'Received:'",[278,20679,20680],{"class":302},", value)\n",[278,20682,20683],{"class":280,"line":539},[278,20684,617],{"class":302},[11,20686,20687,20688,20691,20692,20694,20695,20697,20698,20700,20701,183],{},"We need to set the request ",[59,20689,20690],{},"responseType"," flag as ",[59,20693,19637],{},", and set the type of ",[59,20696,15965],{}," response as ",[59,20699,14575],{},". Then we create a stream reader while decoding the received chunks by piping the events through a ",[59,20702,15987],{},[11,20704,20705],{},"Below is a glimpse of the events data received from our chat endpoint (sent by the LLM)",[269,20707,20711],{"className":20708,"code":20709,"language":20710,"meta":274,"style":274},"language-plaintext shiki shiki-themes github-light github-dark","data: {\"response\":\"Hello\",\"p\":\"abcdefghijklmnopqrstuvwxyz0123456789abcdefghij\"}\n\ndata: {\"response\":\"!\",\"p\":\"abcdefgh\"}\n\ndata: [DONE]\n","plaintext",[59,20712,20713,20718,20722,20727,20731],{"__ignoreMap":274},[278,20714,20715],{"class":280,"line":281},[278,20716,20717],{},"data: {\"response\":\"Hello\",\"p\":\"abcdefghijklmnopqrstuvwxyz0123456789abcdefghij\"}\n",[278,20719,20720],{"class":280,"line":288},[278,20721,292],{"emptyLinePlaceholder":291},[278,20723,20724],{"class":280,"line":295},[278,20725,20726],{},"data: {\"response\":\"!\",\"p\":\"abcdefgh\"}\n",[278,20728,20729],{"class":280,"line":316},[278,20730,292],{"emptyLinePlaceholder":291},[278,20732,20733],{"class":280,"line":322},[278,20734,20735],{},"data: [DONE]\n",[11,20737,20738,20739,11160,20742,20747],{},"These events may contain more than one data line per event, and some events may not contain a complete JSON object (rest of the object will arrive with the next event). To handle this stream we can create a composable with a ",[59,20740,20741],{},"streamResponse",[47,20743,20746],{"href":20744,"rel":20745},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FStatements\u002Ffunction*",[51],"generator function"," as shown below",[269,20749,20751],{"className":271,"code":20750,"language":273,"meta":274,"style":274},"export function useChat() {\n  async function* streamResponse(\n    url: string,\n    messages: ChatMessage[],\n    llmParams: LlmParams\n  ) {\n    let buffer = '';\n\n    try {\n      const response = await $fetch\u003CReadableStream>(url, {\n        method: 'POST',\n        body: {\n          messages,\n          params: llmParams,\n        },\n        responseType: 'stream',\n      });\n\n      const reader = response.pipeThrough(new TextDecoderStream()).getReader();\n\n      while (true) {\n        const { value, done } = await reader.read();\n\n        if (done) {\n          if (buffer.trim()) {\n            console.warn('Stream ended with unparsed data:', buffer);\n          }\n\n          return;\n        }\n\n        buffer += value;\n        const lines = buffer.split('\\n');\n        buffer = lines.pop() || '';\n\n        for (const line of lines) {\n          if (line.startsWith('data: ')) {\n            const data = line.slice('data: '.length).trim();\n            if (data === '[DONE]') {\n              return;\n            }\n\n            try {\n              const jsonData = JSON.parse(data);\n              if (jsonData.response) {\n                yield jsonData.response;\n              }\n            } catch (parseError) {\n              console.warn('Error parsing JSON:', parseError);\n            }\n          }\n        }\n      }\n    } catch (error) {\n      console.error('Error sending message:', error);\n\n      throw error;\n    }\n  }\n\n  \u002F\u002F For handling non-streaming responses\n  async function getResponse() {}\n\n  return {\n    getResponse,\n    streamResponse,\n  };\n}\n",[59,20752,20753,20763,20774,20785,20797,20807,20811,20824,20828,20834,20853,20861,20866,20870,20875,20879,20887,20891,20895,20919,20923,20933,20957,20961,20967,20977,20989,20993,20997,21003,21007,21011,21019,21041,21060,21064,21078,21093,21119,21131,21138,21142,21146,21152,21168,21174,21180,21184,21192,21204,21208,21212,21216,21220,21228,21240,21244,21250,21254,21258,21262,21267,21279,21283,21289,21294,21299,21303],{"__ignoreMap":274},[278,20754,20755,20757,20759,20761],{"class":280,"line":281},[278,20756,628],{"class":298},[278,20758,748],{"class":298},[278,20760,15183],{"class":333},[278,20762,337],{"class":302},[278,20764,20765,20767,20769,20772],{"class":280,"line":288},[278,20766,12914],{"class":298},[278,20768,13711],{"class":298},[278,20770,20771],{"class":333}," streamResponse",[278,20773,770],{"class":302},[278,20775,20776,20779,20781,20783],{"class":280,"line":295},[278,20777,20778],{"class":501},"    url",[278,20780,960],{"class":298},[278,20782,963],{"class":650},[278,20784,660],{"class":302},[278,20786,20787,20790,20792,20795],{"class":280,"line":316},[278,20788,20789],{"class":501},"    messages",[278,20791,960],{"class":298},[278,20793,20794],{"class":333}," ChatMessage",[278,20796,13738],{"class":302},[278,20798,20799,20802,20804],{"class":280,"line":322},[278,20800,20801],{"class":501},"    llmParams",[278,20803,960],{"class":298},[278,20805,20806],{"class":333}," LlmParams\n",[278,20808,20809],{"class":280,"line":327},[278,20810,17292],{"class":302},[278,20812,20813,20816,20818,20820,20822],{"class":280,"line":340},[278,20814,20815],{"class":298},"    let",[278,20817,15305],{"class":302},[278,20819,358],{"class":298},[278,20821,13973],{"class":309},[278,20823,313],{"class":302},[278,20825,20826],{"class":280,"line":349},[278,20827,292],{"emptyLinePlaceholder":291},[278,20829,20830,20832],{"class":280,"line":375},[278,20831,6319],{"class":298},[278,20833,876],{"class":302},[278,20835,20836,20838,20840,20842,20844,20846,20848,20850],{"class":280,"line":386},[278,20837,2461],{"class":298},[278,20839,1115],{"class":650},[278,20841,764],{"class":298},[278,20843,1120],{"class":298},[278,20845,15262],{"class":333},[278,20847,1702],{"class":302},[278,20849,14575],{"class":333},[278,20851,20852],{"class":302},">(url, {\n",[278,20854,20855,20857,20859],{"class":280,"line":397},[278,20856,15270],{"class":302},[278,20858,15273],{"class":309},[278,20860,660],{"class":302},[278,20862,20863],{"class":280,"line":408},[278,20864,20865],{"class":302},"        body: {\n",[278,20867,20868],{"class":280,"line":433},[278,20869,14373],{"class":302},[278,20871,20872],{"class":280,"line":454},[278,20873,20874],{"class":302},"          params: llmParams,\n",[278,20876,20877],{"class":280,"line":475},[278,20878,2606],{"class":302},[278,20880,20881,20883,20885],{"class":280,"line":496},[278,20882,15285],{"class":302},[278,20884,15288],{"class":309},[278,20886,660],{"class":302},[278,20888,20889],{"class":280,"line":505},[278,20890,5148],{"class":302},[278,20892,20893],{"class":280,"line":516},[278,20894,292],{"emptyLinePlaceholder":291},[278,20896,20897,20899,20901,20903,20905,20907,20909,20911,20913,20915,20917],{"class":280,"line":527},[278,20898,2461],{"class":298},[278,20900,15318],{"class":650},[278,20902,764],{"class":298},[278,20904,1307],{"class":302},[278,20906,15336],{"class":333},[278,20908,1126],{"class":302},[278,20910,1173],{"class":298},[278,20912,15343],{"class":333},[278,20914,20598],{"class":302},[278,20916,15353],{"class":333},[278,20918,1313],{"class":302},[278,20920,20921],{"class":280,"line":533},[278,20922,292],{"emptyLinePlaceholder":291},[278,20924,20925,20927,20929,20931],{"class":280,"line":539},[278,20926,15364],{"class":298},[278,20928,1245],{"class":302},[278,20930,2931],{"class":650},[278,20932,1718],{"class":302},[278,20934,20935,20937,20939,20941,20943,20945,20947,20949,20951,20953,20955],{"class":280,"line":545},[278,20936,6741],{"class":298},[278,20938,1009],{"class":302},[278,20940,14768],{"class":650},[278,20942,1708],{"class":302},[278,20944,15383],{"class":650},[278,20946,1029],{"class":302},[278,20948,358],{"class":298},[278,20950,1120],{"class":298},[278,20952,15392],{"class":302},[278,20954,15395],{"class":333},[278,20956,1313],{"class":302},[278,20958,20959],{"class":280,"line":551},[278,20960,292],{"emptyLinePlaceholder":291},[278,20962,20963,20965],{"class":280,"line":557},[278,20964,6926],{"class":298},[278,20966,15408],{"class":302},[278,20968,20969,20971,20973,20975],{"class":280,"line":567},[278,20970,13947],{"class":298},[278,20972,15415],{"class":302},[278,20974,13245],{"class":333},[278,20976,1083],{"class":302},[278,20978,20979,20981,20983,20985,20987],{"class":280,"line":577},[278,20980,14304],{"class":302},[278,20982,15426],{"class":333},[278,20984,1126],{"class":302},[278,20986,15431],{"class":309},[278,20988,15434],{"class":302},[278,20990,20991],{"class":280,"line":587},[278,20992,12122],{"class":302},[278,20994,20995],{"class":280,"line":597},[278,20996,292],{"emptyLinePlaceholder":291},[278,20998,20999,21001],{"class":280,"line":608},[278,21000,15447],{"class":298},[278,21002,313],{"class":302},[278,21004,21005],{"class":280,"line":614},[278,21006,6954],{"class":302},[278,21008,21009],{"class":280,"line":620},[278,21010,292],{"emptyLinePlaceholder":291},[278,21012,21013,21015,21017],{"class":280,"line":625},[278,21014,15462],{"class":302},[278,21016,6271],{"class":298},[278,21018,15467],{"class":302},[278,21020,21021,21023,21025,21027,21029,21031,21033,21035,21037,21039],{"class":280,"line":640},[278,21022,6741],{"class":298},[278,21024,15539],{"class":650},[278,21026,764],{"class":298},[278,21028,15479],{"class":302},[278,21030,13255],{"class":333},[278,21032,1126],{"class":302},[278,21034,15486],{"class":309},[278,21036,14939],{"class":650},[278,21038,15486],{"class":309},[278,21040,1280],{"class":302},[278,21042,21043,21045,21047,21050,21052,21054,21056,21058],{"class":280,"line":663},[278,21044,15462],{"class":302},[278,21046,358],{"class":298},[278,21048,21049],{"class":302}," lines.",[278,21051,15504],{"class":333},[278,21053,1342],{"class":302},[278,21055,5954],{"class":298},[278,21057,13973],{"class":309},[278,21059,313],{"class":302},[278,21061,21062],{"class":280,"line":669},[278,21063,292],{"emptyLinePlaceholder":291},[278,21065,21066,21068,21070,21072,21074,21076],{"class":280,"line":680},[278,21067,14395],{"class":298},[278,21069,1245],{"class":302},[278,21071,5416],{"class":298},[278,21073,15599],{"class":650},[278,21075,12022],{"class":298},[278,21077,15604],{"class":302},[278,21079,21080,21082,21084,21086,21088,21091],{"class":280,"line":686},[278,21081,13947],{"class":298},[278,21083,15612],{"class":302},[278,21085,15615],{"class":333},[278,21087,1126],{"class":302},[278,21089,21090],{"class":309},"'data: '",[278,21092,15623],{"class":302},[278,21094,21095,21097,21099,21101,21103,21105,21107,21109,21111,21113,21115,21117],{"class":280,"line":1334},[278,21096,14173],{"class":298},[278,21098,1296],{"class":650},[278,21100,764],{"class":298},[278,21102,15633],{"class":302},[278,21104,15636],{"class":333},[278,21106,1126],{"class":302},[278,21108,21090],{"class":309},[278,21110,183],{"class":302},[278,21112,15645],{"class":650},[278,21114,4633],{"class":302},[278,21116,13245],{"class":333},[278,21118,1313],{"class":302},[278,21120,21121,21123,21125,21127,21129],{"class":280,"line":1375},[278,21122,15609],{"class":298},[278,21124,15804],{"class":302},[278,21126,2451],{"class":298},[278,21128,15809],{"class":309},[278,21130,1718],{"class":302},[278,21132,21133,21136],{"class":280,"line":1381},[278,21134,21135],{"class":298},"              return",[278,21137,313],{"class":302},[278,21139,21140],{"class":280,"line":1386},[278,21141,15703],{"class":302},[278,21143,21144],{"class":280,"line":1394},[278,21145,292],{"emptyLinePlaceholder":291},[278,21147,21148,21150],{"class":280,"line":1406},[278,21149,15824],{"class":298},[278,21151,876],{"class":302},[278,21153,21154,21156,21158,21160,21162,21164,21166],{"class":280,"line":1423},[278,21155,15831],{"class":298},[278,21157,15834],{"class":650},[278,21159,764],{"class":298},[278,21161,12063],{"class":650},[278,21163,183],{"class":302},[278,21165,12068],{"class":333},[278,21167,15743],{"class":302},[278,21169,21170,21172],{"class":280,"line":1432},[278,21171,15849],{"class":298},[278,21173,15852],{"class":302},[278,21175,21176,21178],{"class":280,"line":1437},[278,21177,15857],{"class":298},[278,21179,15860],{"class":302},[278,21181,21182],{"class":280,"line":1916},[278,21183,548],{"class":302},[278,21185,21186,21188,21190],{"class":280,"line":1939},[278,21187,15656],{"class":302},[278,21189,1400],{"class":298},[278,21191,15873],{"class":302},[278,21193,21194,21196,21198,21200,21202],{"class":280,"line":1949},[278,21195,15878],{"class":302},[278,21197,15426],{"class":333},[278,21199,1126],{"class":302},[278,21201,15885],{"class":309},[278,21203,15888],{"class":302},[278,21205,21206],{"class":280,"line":1954},[278,21207,15703],{"class":302},[278,21209,21210],{"class":280,"line":1959},[278,21211,12122],{"class":302},[278,21213,21214],{"class":280,"line":1985},[278,21215,6954],{"class":302},[278,21217,21218],{"class":280,"line":1990},[278,21219,6234],{"class":302},[278,21221,21222,21224,21226],{"class":280,"line":1997},[278,21223,6636],{"class":302},[278,21225,1400],{"class":298},[278,21227,1403],{"class":302},[278,21229,21230,21232,21234,21236,21238],{"class":280,"line":2006},[278,21231,1919],{"class":302},[278,21233,1412],{"class":333},[278,21235,1126],{"class":302},[278,21237,15923],{"class":309},[278,21239,1420],{"class":302},[278,21241,21242],{"class":280,"line":2018},[278,21243,292],{"emptyLinePlaceholder":291},[278,21245,21246,21248],{"class":280,"line":2029},[278,21247,1255],{"class":298},[278,21249,1429],{"class":302},[278,21251,21252],{"class":280,"line":2034},[278,21253,1285],{"class":302},[278,21255,21256],{"class":280,"line":2040},[278,21257,1096],{"class":302},[278,21259,21260],{"class":280,"line":2045},[278,21261,292],{"emptyLinePlaceholder":291},[278,21263,21264],{"class":280,"line":2068},[278,21265,21266],{"class":284},"  \u002F\u002F For handling non-streaming responses\n",[278,21268,21269,21271,21273,21276],{"class":280,"line":2099},[278,21270,12914],{"class":298},[278,21272,748],{"class":298},[278,21274,21275],{"class":333}," getResponse",[278,21277,21278],{"class":302},"() {}\n",[278,21280,21281],{"class":280,"line":6428},[278,21282,292],{"emptyLinePlaceholder":291},[278,21284,21285,21287],{"class":280,"line":6439},[278,21286,343],{"class":298},[278,21288,876],{"class":302},[278,21290,21291],{"class":280,"line":6450},[278,21292,21293],{"class":302},"    getResponse,\n",[278,21295,21296],{"class":280,"line":6455},[278,21297,21298],{"class":302},"    streamResponse,\n",[278,21300,21301],{"class":280,"line":6460},[278,21302,901],{"class":302},[278,21304,21305],{"class":280,"line":6475},[278,21306,617],{"class":302},[11,21308,21309,21310,21312],{},"To handle non streaming responses we can also create a simple ",[59,21311,15965],{}," call wrapper function in the same composable (check out the code in the linked Github Repo).",[11,21314,21315,21316,21318],{},"Now that we understand how to consume Server Sent Events and handle streaming responses, let's put all the pieces together in our main chat interface. We'll integrate the components we've built, set up the chat API call, and use our ",[59,21317,15168],{}," composable to manage the LLM responses.",[32,21320,21322],{"id":21321},"final-chat-interface-page","Final Chat Interface Page",[11,21324,21325,21326,21328],{},"The below is the final code of the index page (our app has only one page). Here we use all the components that we created before, and call the chat api endpoint. Then we use the ",[59,21327,15168],{}," composable to handle the LLM responses.",[269,21330,21332],{"className":7132,"code":21331,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003Cdiv class=\"h-screen flex flex-col md:flex-row\">\n    \u003CUSlideover\n      v-model=\"isDrawerOpen\"\n      class=\"md:hidden\"\n      :ui=\"{ width: 'max-w-xs' }\"\n    >\n      \u003CLlmSettings\n        v-model:llmParams=\"llmParams\"\n        @hide-drawer=\"isDrawerOpen = false\"\n        @reset=\"resetSettings\"\n      \u002F>\n    \u003C\u002FUSlideover>\n\n    \u003Cdiv class=\"hidden md:block md:w-1\u002F3 lg:w-1\u002F4\">\n      \u003CLlmSettings v-model:llmParams=\"llmParams\" @reset=\"resetSettings\" \u002F>\n    \u003C\u002Fdiv>\n\n    \u003CUDivider orientation=\"vertical\" class=\"hidden md:block\" \u002F>\n\n    \u003Cdiv class=\"flex-grow md:w-2\u002F3 lg:w-3\u002F4\">\n      \u003CChatPanel\n        :chat-history=\"chatHistory\"\n        :loading=\"loading\"\n        @clear=\"chatHistory = []\"\n        @message=\"sendMessage\"\n        @show-drawer=\"isDrawerOpen = true\"\n      \u002F>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport type { ChatMessage, LlmParams, LoadingType } from '~~\u002Ftypes';\n\nconst isDrawerOpen = ref(false);\n\nconst defaultSettings: LlmParams = {\n  model: '@cf\u002Fmeta\u002Fllama-3.1-8b-instruct',\n  temperature: 0.6,\n  maxTokens: 512,\n  systemPrompt: 'You are a helpful assistant.',\n  stream: true,\n};\n\nconst llmParams = reactive\u003CLlmParams>({ ...defaultSettings });\nconst resetSettings = () => {\n  Object.assign(llmParams, defaultSettings);\n};\n\nconst { getResponse, streamResponse } = useChat();\nconst chatHistory = ref\u003CChatMessage[]>([]);\nconst loading = ref\u003CLoadingType>('idle');\nasync function sendMessage(message: string) {\n  chatHistory.value.push({ role: 'user', content: message });\n\n  try {\n    if (llmParams.stream) {\n      loading.value = 'stream';\n      const messageGenerator = streamResponse(\n        '\u002Fapi\u002Fchat',\n        chatHistory.value,\n        llmParams\n      );\n\n      let responseAdded = false;\n      for await (const chunk of messageGenerator) {\n        if (responseAdded) {\n          \u002F\u002F add the response to the current message\n          chatHistory.value[chatHistory.value.length - 1]!.content += chunk;\n        } else {\n          \u002F\u002F add a new message to the chat history\n          chatHistory.value.push({\n            role: 'assistant',\n            content: chunk,\n          });\n\n          responseAdded = true;\n        }\n      }\n    } else {\n      loading.value = 'message';\n      const response = await getResponse(\n        '\u002Fapi\u002Fchat',\n        chatHistory.value,\n        llmParams\n      );\n\n      chatHistory.value.push({ role: 'assistant', content: response });\n    }\n  } catch (error) {\n    console.error('Error sending message:', error);\n  } finally {\n    loading.value = 'idle';\n  }\n}\n\u003C\u002Fscript>\n",[59,21333,21334,21338,21343,21348,21353,21358,21363,21367,21372,21377,21382,21387,21391,21396,21400,21405,21410,21414,21418,21423,21427,21432,21437,21442,21447,21452,21457,21462,21466,21470,21474,21478,21482,21486,21491,21495,21500,21504,21509,21514,21519,21524,21529,21534,21538,21542,21547,21552,21557,21561,21565,21570,21575,21580,21585,21590,21594,21598,21603,21608,21613,21618,21623,21628,21632,21636,21641,21646,21651,21656,21661,21666,21671,21676,21681,21686,21691,21695,21700,21704,21708,21712,21717,21722,21726,21730,21734,21738,21742,21747,21751,21755,21760,21764,21769,21773,21777],{"__ignoreMap":274},[278,21335,21336],{"class":280,"line":281},[278,21337,7146],{},[278,21339,21340],{"class":280,"line":288},[278,21341,21342],{},"  \u003Cdiv class=\"h-screen flex flex-col md:flex-row\">\n",[278,21344,21345],{"class":280,"line":295},[278,21346,21347],{},"    \u003CUSlideover\n",[278,21349,21350],{"class":280,"line":316},[278,21351,21352],{},"      v-model=\"isDrawerOpen\"\n",[278,21354,21355],{"class":280,"line":322},[278,21356,21357],{},"      class=\"md:hidden\"\n",[278,21359,21360],{"class":280,"line":327},[278,21361,21362],{},"      :ui=\"{ width: 'max-w-xs' }\"\n",[278,21364,21365],{"class":280,"line":340},[278,21366,7935],{},[278,21368,21369],{"class":280,"line":349},[278,21370,21371],{},"      \u003CLlmSettings\n",[278,21373,21374],{"class":280,"line":375},[278,21375,21376],{},"        v-model:llmParams=\"llmParams\"\n",[278,21378,21379],{"class":280,"line":386},[278,21380,21381],{},"        @hide-drawer=\"isDrawerOpen = false\"\n",[278,21383,21384],{"class":280,"line":397},[278,21385,21386],{},"        @reset=\"resetSettings\"\n",[278,21388,21389],{"class":280,"line":408},[278,21390,7308],{},[278,21392,21393],{"class":280,"line":433},[278,21394,21395],{},"    \u003C\u002FUSlideover>\n",[278,21397,21398],{"class":280,"line":454},[278,21399,292],{"emptyLinePlaceholder":291},[278,21401,21402],{"class":280,"line":475},[278,21403,21404],{},"    \u003Cdiv class=\"hidden md:block md:w-1\u002F3 lg:w-1\u002F4\">\n",[278,21406,21407],{"class":280,"line":496},[278,21408,21409],{},"      \u003CLlmSettings v-model:llmParams=\"llmParams\" @reset=\"resetSettings\" \u002F>\n",[278,21411,21412],{"class":280,"line":505},[278,21413,7950],{},[278,21415,21416],{"class":280,"line":516},[278,21417,292],{"emptyLinePlaceholder":291},[278,21419,21420],{"class":280,"line":527},[278,21421,21422],{},"    \u003CUDivider orientation=\"vertical\" class=\"hidden md:block\" \u002F>\n",[278,21424,21425],{"class":280,"line":533},[278,21426,292],{"emptyLinePlaceholder":291},[278,21428,21429],{"class":280,"line":539},[278,21430,21431],{},"    \u003Cdiv class=\"flex-grow md:w-2\u002F3 lg:w-3\u002F4\">\n",[278,21433,21434],{"class":280,"line":545},[278,21435,21436],{},"      \u003CChatPanel\n",[278,21438,21439],{"class":280,"line":551},[278,21440,21441],{},"        :chat-history=\"chatHistory\"\n",[278,21443,21444],{"class":280,"line":557},[278,21445,21446],{},"        :loading=\"loading\"\n",[278,21448,21449],{"class":280,"line":567},[278,21450,21451],{},"        @clear=\"chatHistory = []\"\n",[278,21453,21454],{"class":280,"line":577},[278,21455,21456],{},"        @message=\"sendMessage\"\n",[278,21458,21459],{"class":280,"line":587},[278,21460,21461],{},"        @show-drawer=\"isDrawerOpen = true\"\n",[278,21463,21464],{"class":280,"line":597},[278,21465,7308],{},[278,21467,21468],{"class":280,"line":608},[278,21469,7950],{},[278,21471,21472],{"class":280,"line":614},[278,21473,19082],{},[278,21475,21476],{"class":280,"line":620},[278,21477,7422],{},[278,21479,21480],{"class":280,"line":625},[278,21481,292],{"emptyLinePlaceholder":291},[278,21483,21484],{"class":280,"line":640},[278,21485,7431],{},[278,21487,21488],{"class":280,"line":663},[278,21489,21490],{},"import type { ChatMessage, LlmParams, LoadingType } from '~~\u002Ftypes';\n",[278,21492,21493],{"class":280,"line":669},[278,21494,292],{"emptyLinePlaceholder":291},[278,21496,21497],{"class":280,"line":680},[278,21498,21499],{},"const isDrawerOpen = ref(false);\n",[278,21501,21502],{"class":280,"line":686},[278,21503,292],{"emptyLinePlaceholder":291},[278,21505,21506],{"class":280,"line":1334},[278,21507,21508],{},"const defaultSettings: LlmParams = {\n",[278,21510,21511],{"class":280,"line":1375},[278,21512,21513],{},"  model: '@cf\u002Fmeta\u002Fllama-3.1-8b-instruct',\n",[278,21515,21516],{"class":280,"line":1381},[278,21517,21518],{},"  temperature: 0.6,\n",[278,21520,21521],{"class":280,"line":1386},[278,21522,21523],{},"  maxTokens: 512,\n",[278,21525,21526],{"class":280,"line":1394},[278,21527,21528],{},"  systemPrompt: 'You are a helpful assistant.',\n",[278,21530,21531],{"class":280,"line":1406},[278,21532,21533],{},"  stream: true,\n",[278,21535,21536],{"class":280,"line":1423},[278,21537,2817],{},[278,21539,21540],{"class":280,"line":1432},[278,21541,292],{"emptyLinePlaceholder":291},[278,21543,21544],{"class":280,"line":1437},[278,21545,21546],{},"const llmParams = reactive\u003CLlmParams>({ ...defaultSettings });\n",[278,21548,21549],{"class":280,"line":1916},[278,21550,21551],{},"const resetSettings = () => {\n",[278,21553,21554],{"class":280,"line":1939},[278,21555,21556],{},"  Object.assign(llmParams, defaultSettings);\n",[278,21558,21559],{"class":280,"line":1949},[278,21560,2817],{},[278,21562,21563],{"class":280,"line":1954},[278,21564,292],{"emptyLinePlaceholder":291},[278,21566,21567],{"class":280,"line":1959},[278,21568,21569],{},"const { getResponse, streamResponse } = useChat();\n",[278,21571,21572],{"class":280,"line":1985},[278,21573,21574],{},"const chatHistory = ref\u003CChatMessage[]>([]);\n",[278,21576,21577],{"class":280,"line":1990},[278,21578,21579],{},"const loading = ref\u003CLoadingType>('idle');\n",[278,21581,21582],{"class":280,"line":1997},[278,21583,21584],{},"async function sendMessage(message: string) {\n",[278,21586,21587],{"class":280,"line":2006},[278,21588,21589],{},"  chatHistory.value.push({ role: 'user', content: message });\n",[278,21591,21592],{"class":280,"line":2018},[278,21593,292],{"emptyLinePlaceholder":291},[278,21595,21596],{"class":280,"line":2029},[278,21597,7562],{},[278,21599,21600],{"class":280,"line":2034},[278,21601,21602],{},"    if (llmParams.stream) {\n",[278,21604,21605],{"class":280,"line":2040},[278,21606,21607],{},"      loading.value = 'stream';\n",[278,21609,21610],{"class":280,"line":2045},[278,21611,21612],{},"      const messageGenerator = streamResponse(\n",[278,21614,21615],{"class":280,"line":2068},[278,21616,21617],{},"        '\u002Fapi\u002Fchat',\n",[278,21619,21620],{"class":280,"line":2099},[278,21621,21622],{},"        chatHistory.value,\n",[278,21624,21625],{"class":280,"line":6428},[278,21626,21627],{},"        llmParams\n",[278,21629,21630],{"class":280,"line":6439},[278,21631,2616],{},[278,21633,21634],{"class":280,"line":6450},[278,21635,292],{"emptyLinePlaceholder":291},[278,21637,21638],{"class":280,"line":6455},[278,21639,21640],{},"      let responseAdded = false;\n",[278,21642,21643],{"class":280,"line":6460},[278,21644,21645],{},"      for await (const chunk of messageGenerator) {\n",[278,21647,21648],{"class":280,"line":6475},[278,21649,21650],{},"        if (responseAdded) {\n",[278,21652,21653],{"class":280,"line":6486},[278,21654,21655],{},"          \u002F\u002F add the response to the current message\n",[278,21657,21658],{"class":280,"line":6491},[278,21659,21660],{},"          chatHistory.value[chatHistory.value.length - 1]!.content += chunk;\n",[278,21662,21663],{"class":280,"line":6518},[278,21664,21665],{},"        } else {\n",[278,21667,21668],{"class":280,"line":6530},[278,21669,21670],{},"          \u002F\u002F add a new message to the chat history\n",[278,21672,21673],{"class":280,"line":6542},[278,21674,21675],{},"          chatHistory.value.push({\n",[278,21677,21678],{"class":280,"line":6547},[278,21679,21680],{},"            role: 'assistant',\n",[278,21682,21683],{"class":280,"line":6552},[278,21684,21685],{},"            content: chunk,\n",[278,21687,21688],{"class":280,"line":6567},[278,21689,21690],{},"          });\n",[278,21692,21693],{"class":280,"line":6580},[278,21694,292],{"emptyLinePlaceholder":291},[278,21696,21697],{"class":280,"line":6593},[278,21698,21699],{},"          responseAdded = true;\n",[278,21701,21702],{"class":280,"line":6605},[278,21703,6954],{},[278,21705,21706],{"class":280,"line":6620},[278,21707,6234],{},[278,21709,21710],{"class":280,"line":6625},[278,21711,8971],{},[278,21713,21714],{"class":280,"line":6633},[278,21715,21716],{},"      loading.value = 'message';\n",[278,21718,21719],{"class":280,"line":6643},[278,21720,21721],{},"      const response = await getResponse(\n",[278,21723,21724],{"class":280,"line":6657},[278,21725,21617],{},[278,21727,21728],{"class":280,"line":6665},[278,21729,21622],{},[278,21731,21732],{"class":280,"line":6670},[278,21733,21627],{},[278,21735,21736],{"class":280,"line":6675},[278,21737,2616],{},[278,21739,21740],{"class":280,"line":6680},[278,21741,292],{"emptyLinePlaceholder":291},[278,21743,21744],{"class":280,"line":6698},[278,21745,21746],{},"      chatHistory.value.push({ role: 'assistant', content: response });\n",[278,21748,21749],{"class":280,"line":6725},[278,21750,1285],{},[278,21752,21753],{"class":280,"line":6738},[278,21754,8602],{},[278,21756,21757],{"class":280,"line":6752},[278,21758,21759],{},"    console.error('Error sending message:', error);\n",[278,21761,21762],{"class":280,"line":6769},[278,21763,8458],{},[278,21765,21766],{"class":280,"line":6786},[278,21767,21768],{},"    loading.value = 'idle';\n",[278,21770,21771],{"class":280,"line":6798},[278,21772,1096],{},[278,21774,21775],{"class":280,"line":6803},[278,21776,617],{},[278,21778,21779],{"class":280,"line":6815},[278,21780,7691],{},[11,21782,21783],{},"This completes bulk of the coding. The only things remaining are:",[123,21785,21786,21789],{},[74,21787,21788],{},"Response parsing for markdown and display",[74,21790,21791],{},"Auto scrolling the chat container",[11,21793,21794],{},"Let's tackle these in the next section.",[24,21796,21798],{"id":21797},"polishing-the-chat-interface","Polishing the Chat Interface",[11,21800,21801],{},"We will finish the remaining items in our task list now and call it a day. Stay with me, it won't take long now.",[32,21803,21805],{"id":21804},"using-nuxt-mdc-to-parse-display-messages","Using Nuxt MDC to Parse & Display Messages",[11,21807,10358,21808,21810,21811,21814],{},[59,21809,19464],{}," code in one of the previous sections you'll notice a component ",[59,21812,21813],{},"AssistantMessage"," for displaying the response. Reproducing the relevant code here",[269,21816,21818],{"className":7132,"code":21817,"language":7134,"meta":274,"style":274},"\u003Cdiv v-if=\"message.role === 'user'\">\n  {{ message.content }}\n\u003C\u002Fdiv>\n\u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n",[59,21819,21820,21825,21830,21834],{"__ignoreMap":274},[278,21821,21822],{"class":280,"line":281},[278,21823,21824],{},"\u003Cdiv v-if=\"message.role === 'user'\">\n",[278,21826,21827],{"class":280,"line":288},[278,21828,21829],{},"  {{ message.content }}\n",[278,21831,21832],{"class":280,"line":295},[278,21833,19624],{},[278,21835,21836],{"class":280,"line":316},[278,21837,21838],{},"\u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n",[11,21840,21841,21842,21845,21846,21849,21850,21853,21854,21857,21858,21861],{},"We use the ",[59,21843,21844],{},"parseMarkdown"," utility function from the ",[59,21847,21848],{},"MDC module"," that we included earlier to parse the content, and then use the ",[59,21851,21852],{},"MDCRenderer"," component from the same module for displaying it. To take care of streaming response we add a ",[59,21855,21856],{},"watch"," for the message content and redo the parsing with ",[59,21859,21860],{},"useAsyncData"," composable.",[269,21863,21865],{"className":7132,"code":21864,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003CMDCRenderer class=\"flex-1 prose dark:prose-invert\" :body=\"ast?.body\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { parseMarkdown } from '#imports';\n\nconst props = defineProps\u003C{\n  content: string;\n}>();\n\nconst { data: ast, refresh } = await useAsyncData(useId(), () =>\n  parseMarkdown(props.content)\n);\n\nwatch(\n  () => props.content,\n  () => {\n    refresh();\n  }\n);\n\u003C\u002Fscript>\n",[59,21866,21867,21871,21876,21880,21884,21888,21893,21897,21901,21906,21910,21914,21919,21924,21928,21932,21936,21941,21945,21950,21954,21958],{"__ignoreMap":274},[278,21868,21869],{"class":280,"line":281},[278,21870,7146],{},[278,21872,21873],{"class":280,"line":288},[278,21874,21875],{},"  \u003CMDCRenderer class=\"flex-1 prose dark:prose-invert\" :body=\"ast?.body\" \u002F>\n",[278,21877,21878],{"class":280,"line":295},[278,21879,7422],{},[278,21881,21882],{"class":280,"line":316},[278,21883,292],{"emptyLinePlaceholder":291},[278,21885,21886],{"class":280,"line":322},[278,21887,7431],{},[278,21889,21890],{"class":280,"line":327},[278,21891,21892],{},"import { parseMarkdown } from '#imports';\n",[278,21894,21895],{"class":280,"line":340},[278,21896,292],{"emptyLinePlaceholder":291},[278,21898,21899],{"class":280,"line":349},[278,21900,8786],{},[278,21902,21903],{"class":280,"line":375},[278,21904,21905],{},"  content: string;\n",[278,21907,21908],{"class":280,"line":386},[278,21909,8801],{},[278,21911,21912],{"class":280,"line":397},[278,21913,292],{"emptyLinePlaceholder":291},[278,21915,21916],{"class":280,"line":408},[278,21917,21918],{},"const { data: ast, refresh } = await useAsyncData(useId(), () =>\n",[278,21920,21921],{"class":280,"line":433},[278,21922,21923],{},"  parseMarkdown(props.content)\n",[278,21925,21926],{"class":280,"line":454},[278,21927,1280],{},[278,21929,21930],{"class":280,"line":475},[278,21931,292],{"emptyLinePlaceholder":291},[278,21933,21934],{"class":280,"line":496},[278,21935,9020],{},[278,21937,21938],{"class":280,"line":505},[278,21939,21940],{},"  () => props.content,\n",[278,21942,21943],{"class":280,"line":516},[278,21944,9030],{},[278,21946,21947],{"class":280,"line":527},[278,21948,21949],{},"    refresh();\n",[278,21951,21952],{"class":280,"line":533},[278,21953,1096],{},[278,21955,21956],{"class":280,"line":539},[278,21957,1280],{},[278,21959,21960],{"class":280,"line":545},[278,21961,7691],{},[3300,21963,21964,21966],{"dataNodeType":3302},[3300,21965,3785],{"dataNodeType":3305},[3300,21967,21968,21969,21972,21973,21976,21977,21979,21980,21982],{"dataNodeType":3309},"We could have used the ",[59,21970,21971],{},"\u003CMDC>"," component instead of using ",[59,21974,21975],{},"parseMarkdown + \u003CMDCRenderer>"," combination. ",[59,21978,21971],{}," handles the parsing internally using the same ",[59,21981,21844],{}," function, then why we didn't use it?",[11,21984,4796,21985,21987,21988,21990],{},[59,21986,21971],{}," component was causing overwriting of previous messages that were similar (the start of the message) to the latest message from the API endpoint. This happens because we don't have a unique key in our messages. Here is the relevant code from the ",[59,21989,21971],{}," component.",[269,21992,21994],{"className":271,"code":21993,"language":273,"meta":274,"style":274},"const key = computed(() => hash(props.value))\n\nconst { data, refresh, error } = await useAsyncData(key.value, async () => {\n  if (typeof props.value !== 'string') {\n    return props.value\n  }\n  return await parseMarkdown(props.value, props.parserOptions)\n})\n",[59,21995,21996,22018,22022,22058,22076,22083,22087,22099],{"__ignoreMap":274},[278,21997,21998,22000,22003,22005,22008,22010,22012,22015],{"class":280,"line":281},[278,21999,5416],{"class":298},[278,22001,22002],{"class":650}," key",[278,22004,764],{"class":298},[278,22006,22007],{"class":333}," computed",[278,22009,4611],{"class":302},[278,22011,1848],{"class":298},[278,22013,22014],{"class":333}," hash",[278,22016,22017],{"class":302},"(props.value))\n",[278,22019,22020],{"class":280,"line":288},[278,22021,292],{"emptyLinePlaceholder":291},[278,22023,22024,22026,22028,22030,22032,22034,22036,22038,22040,22042,22044,22047,22050,22052,22054,22056],{"class":280,"line":295},[278,22025,5416],{"class":298},[278,22027,1009],{"class":302},[278,22029,15996],{"class":650},[278,22031,1708],{"class":302},[278,22033,9810],{"class":650},[278,22035,1708],{"class":302},[278,22037,1412],{"class":650},[278,22039,1029],{"class":302},[278,22041,358],{"class":298},[278,22043,1120],{"class":298},[278,22045,22046],{"class":333}," useAsyncData",[278,22048,22049],{"class":302},"(key.value, ",[278,22051,1050],{"class":298},[278,22053,5860],{"class":302},[278,22055,1848],{"class":298},[278,22057,876],{"class":302},[278,22059,22060,22062,22064,22066,22069,22071,22074],{"class":280,"line":316},[278,22061,1062],{"class":298},[278,22063,1245],{"class":302},[278,22065,9559],{"class":298},[278,22067,22068],{"class":302}," props.value ",[278,22070,2092],{"class":298},[278,22072,22073],{"class":309}," 'string'",[278,22075,1718],{"class":302},[278,22077,22078,22080],{"class":280,"line":322},[278,22079,1088],{"class":298},[278,22081,22082],{"class":302}," props.value\n",[278,22084,22085],{"class":280,"line":327},[278,22086,1096],{"class":302},[278,22088,22089,22091,22093,22096],{"class":280,"line":340},[278,22090,343],{"class":298},[278,22092,1120],{"class":298},[278,22094,22095],{"class":333}," parseMarkdown",[278,22097,22098],{"class":302},"(props.value, props.parserOptions)\n",[278,22100,22101],{"class":280,"line":349},[278,22102,20568],{"class":302},[11,22104,22105,22106,22108],{},"As you can see, the key for ",[59,22107,21860],{}," is generated based on the content props value. So if the server streaming responses start with the same letters (e.g., Here's a joke..., Here's another...), the key will be same, and all similar previous responses from the server gets overwritten in the UI.",[11,22110,22111,22112,22114,22115,22118,22119,22121],{},"In the ",[59,22113,21813],{}," component we are using ",[59,22116,22117],{},"useId"," composable to generate a unique id for each ",[59,22120,21860],{}," call, so the issue doesn't occur.",[32,22123,22125],{"id":22124},"using-mutationobserver-to-auto-scroll-the-chats-container","Using MutationObserver to Auto Scroll the Chats Container",[11,22127,22128,22129,183],{},"In the ChatPanel component, the only scrolling part is the chats container. There can be other ways to observe the changes in the container but the simplest one I found is a ",[47,22130,22133],{"href":22131,"rel":22132},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FMutationObserver",[51],[59,22134,22135],{},"MutationObserver",[3300,22137,22138,22140],{"dataNodeType":3302},[3300,22139,3785],{"dataNodeType":3305},[3300,22141,4796,22142,22144],{"dataNodeType":3309},[59,22143,22135],{}," interface provides the ability to watch for changes being made to the DOM tree.",[11,22146,22147,22148,4633],{},"In the case of streaming, we keep appending new content to the latest message that is being displayed. So to handle this effectively, we create a MutationObserver, give it a target to watch (the ChatsContainer), and some criteria to watch for (",[59,22149,22150],{},"childList, subtree & characterData",[11,22152,22153],{},"Here is the relevant code to do this",[269,22155,22157],{"className":271,"code":22156,"language":273,"meta":274,"style":274},"const chatContainer = ref\u003CHTMLElement | null>(null);\nlet observer: MutationObserver | null = null;\n\nonMounted(() => {\n  if (chatContainer.value) {\n    observer = new MutationObserver(() => {\n      if (chatContainer.value) {\n        chatContainer.value.scrollTop = chatContainer.value.scrollHeight;\n      }\n    });\n\n    observer.observe(chatContainer.value, {\n      childList: true,\n      subtree: true,\n      characterData: true,\n    });\n  }\n});\n\nonUnmounted(() => {\n  if (observer) {\n    observer.disconnect();\n  }\n});\n",[59,22158,22159,22185,22207,22211,22222,22229,22246,22252,22262,22266,22270,22274,22285,22294,22303,22312,22316,22320,22324,22328,22339,22346,22355,22359],{"__ignoreMap":274},[278,22160,22161,22163,22166,22168,22170,22172,22175,22177,22179,22181,22183],{"class":280,"line":281},[278,22162,5416],{"class":298},[278,22164,22165],{"class":650}," chatContainer",[278,22167,764],{"class":298},[278,22169,5992],{"class":333},[278,22171,1702],{"class":302},[278,22173,22174],{"class":333},"HTMLElement",[278,22176,1621],{"class":298},[278,22178,1035],{"class":650},[278,22180,20521],{"class":302},[278,22182,6026],{"class":650},[278,22184,1280],{"class":302},[278,22186,22187,22189,22192,22194,22197,22199,22201,22203,22205],{"class":280,"line":288},[278,22188,1001],{"class":298},[278,22190,22191],{"class":302}," observer",[278,22193,960],{"class":298},[278,22195,22196],{"class":333}," MutationObserver",[278,22198,1621],{"class":298},[278,22200,1035],{"class":650},[278,22202,764],{"class":298},[278,22204,1035],{"class":650},[278,22206,313],{"class":302},[278,22208,22209],{"class":280,"line":295},[278,22210,292],{"emptyLinePlaceholder":291},[278,22212,22213,22216,22218,22220],{"class":280,"line":316},[278,22214,22215],{"class":333},"onMounted",[278,22217,4611],{"class":302},[278,22219,1848],{"class":298},[278,22221,876],{"class":302},[278,22223,22224,22226],{"class":280,"line":322},[278,22225,1062],{"class":298},[278,22227,22228],{"class":302}," (chatContainer.value) {\n",[278,22230,22231,22234,22236,22238,22240,22242,22244],{"class":280,"line":327},[278,22232,22233],{"class":302},"    observer ",[278,22235,358],{"class":298},[278,22237,1258],{"class":298},[278,22239,22196],{"class":333},[278,22241,4611],{"class":302},[278,22243,1848],{"class":298},[278,22245,876],{"class":302},[278,22247,22248,22250],{"class":280,"line":340},[278,22249,6207],{"class":298},[278,22251,22228],{"class":302},[278,22253,22254,22257,22259],{"class":280,"line":349},[278,22255,22256],{"class":302},"        chatContainer.value.scrollTop ",[278,22258,358],{"class":298},[278,22260,22261],{"class":302}," chatContainer.value.scrollHeight;\n",[278,22263,22264],{"class":280,"line":375},[278,22265,6234],{"class":302},[278,22267,22268],{"class":280,"line":386},[278,22269,1233],{"class":302},[278,22271,22272],{"class":280,"line":397},[278,22273,292],{"emptyLinePlaceholder":291},[278,22275,22276,22279,22282],{"class":280,"line":408},[278,22277,22278],{"class":302},"    observer.",[278,22280,22281],{"class":333},"observe",[278,22283,22284],{"class":302},"(chatContainer.value, {\n",[278,22286,22287,22290,22292],{"class":280,"line":433},[278,22288,22289],{"class":302},"      childList: ",[278,22291,2931],{"class":650},[278,22293,660],{"class":302},[278,22295,22296,22299,22301],{"class":280,"line":454},[278,22297,22298],{"class":302},"      subtree: ",[278,22300,2931],{"class":650},[278,22302,660],{"class":302},[278,22304,22305,22308,22310],{"class":280,"line":475},[278,22306,22307],{"class":302},"      characterData: ",[278,22309,2931],{"class":650},[278,22311,660],{"class":302},[278,22313,22314],{"class":280,"line":496},[278,22315,1233],{"class":302},[278,22317,22318],{"class":280,"line":505},[278,22319,1096],{"class":302},[278,22321,22322],{"class":280,"line":516},[278,22323,3693],{"class":302},[278,22325,22326],{"class":280,"line":527},[278,22327,292],{"emptyLinePlaceholder":291},[278,22329,22330,22333,22335,22337],{"class":280,"line":533},[278,22331,22332],{"class":333},"onUnmounted",[278,22334,4611],{"class":302},[278,22336,1848],{"class":298},[278,22338,876],{"class":302},[278,22340,22341,22343],{"class":280,"line":539},[278,22342,1062],{"class":298},[278,22344,22345],{"class":302}," (observer) {\n",[278,22347,22348,22350,22353],{"class":280,"line":545},[278,22349,22278],{"class":302},[278,22351,22352],{"class":333},"disconnect",[278,22354,1313],{"class":302},[278,22356,22357],{"class":280,"line":551},[278,22358,1096],{"class":302},[278,22360,22361],{"class":280,"line":557},[278,22362,3693],{"class":302},[11,22364,22365],{},"When the ChatsPanel gets mounted we create a new MutationObserver which when based on the given criteria, we make the chats container scroll to its fullest.",[32,22367,22369],{"id":22368},"bonus-handling-the-dark-mode","Bonus: Handling the Dark Mode",[11,22371,22372,22373,22375],{},"As previously mentioned, the dark mode is already working; we just need a way to toggle it. We can also change the flavor of gray we want in the app. This can be done by adding an ",[59,22374,9822],{}," file in the app directory.",[269,22377,22379],{"className":271,"code":22378,"language":273,"meta":274,"style":274},"\u002F\u002F app.config.ts\nexport default defineAppConfig({\n  ui: {\n    primary: 'orange',\n    gray: 'slate',\n  },\n});\n",[59,22380,22381,22386,22396,22400,22410,22420,22424],{"__ignoreMap":274},[278,22382,22383],{"class":280,"line":281},[278,22384,22385],{"class":284},"\u002F\u002F app.config.ts\n",[278,22387,22388,22390,22392,22394],{"class":280,"line":288},[278,22389,628],{"class":298},[278,22391,631],{"class":298},[278,22393,9844],{"class":333},[278,22395,637],{"class":302},[278,22397,22398],{"class":280,"line":295},[278,22399,9851],{"class":302},[278,22401,22402,22405,22408],{"class":280,"line":316},[278,22403,22404],{"class":302},"    primary: ",[278,22406,22407],{"class":309},"'orange'",[278,22409,660],{"class":302},[278,22411,22412,22415,22418],{"class":280,"line":322},[278,22413,22414],{"class":302},"    gray: ",[278,22416,22417],{"class":309},"'slate'",[278,22419,660],{"class":302},[278,22421,22422],{"class":280,"line":327},[278,22423,683],{"class":302},[278,22425,22426],{"class":280,"line":340},[278,22427,3693],{"class":302},[11,22429,22430,22431,22434],{},"Add the following in your ",[59,22432,22433],{},"app.vue"," file for setting the required background colors.",[269,22436,22438],{"className":7132,"code":22437,"language":7134,"meta":274,"style":274},"\u003Cscript setup lang=\"ts\">\nuseHead({\n  bodyAttrs: {\n    class: 'bg-white dark:bg-gray-900',\n  },\n});\n\u003C\u002Fscript>\n",[59,22439,22440,22444,22449,22454,22459,22463,22467],{"__ignoreMap":274},[278,22441,22442],{"class":280,"line":281},[278,22443,7431],{},[278,22445,22446],{"class":280,"line":288},[278,22447,22448],{},"useHead({\n",[278,22450,22451],{"class":280,"line":295},[278,22452,22453],{},"  bodyAttrs: {\n",[278,22455,22456],{"class":280,"line":316},[278,22457,22458],{},"    class: 'bg-white dark:bg-gray-900',\n",[278,22460,22461],{"class":280,"line":322},[278,22462,683],{},[278,22464,22465],{"class":280,"line":327},[278,22466,3693],{},[278,22468,22469],{"class":280,"line":340},[278,22470,7691],{},[11,22472,22473,22474,22477,22478,22480],{},"And add a new ",[59,22475,22476],{},"ColorMode"," component in your ",[59,22479,7129],{}," directory.",[269,22482,22484],{"className":7132,"code":22483,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003CClientOnly>\n    \u003CUButton\n      :icon=\"isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'\"\n      color=\"gray\"\n      variant=\"ghost\"\n      aria-label=\"Theme\"\n      @click=\"isDark = !isDark\"\n    \u002F>\n\n    \u003Ctemplate #fallback>\n      \u003Cdiv class=\"w-8 h-8\" \u002F>\n    \u003C\u002Ftemplate>\n  \u003C\u002FClientOnly>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst colorMode = useColorMode();\n\nconst isDark = computed({\n  get() {\n    return colorMode.value === 'dark';\n  },\n  set() {\n    colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';\n  },\n});\n\u003C\u002Fscript>\n",[59,22485,22486,22490,22495,22500,22505,22510,22515,22520,22525,22529,22533,22538,22543,22547,22552,22556,22560,22564,22569,22573,22578,22583,22588,22592,22597,22602,22606,22610],{"__ignoreMap":274},[278,22487,22488],{"class":280,"line":281},[278,22489,7146],{},[278,22491,22492],{"class":280,"line":288},[278,22493,22494],{},"  \u003CClientOnly>\n",[278,22496,22497],{"class":280,"line":295},[278,22498,22499],{},"    \u003CUButton\n",[278,22501,22502],{"class":280,"line":316},[278,22503,22504],{},"      :icon=\"isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'\"\n",[278,22506,22507],{"class":280,"line":322},[278,22508,22509],{},"      color=\"gray\"\n",[278,22511,22512],{"class":280,"line":327},[278,22513,22514],{},"      variant=\"ghost\"\n",[278,22516,22517],{"class":280,"line":340},[278,22518,22519],{},"      aria-label=\"Theme\"\n",[278,22521,22522],{"class":280,"line":349},[278,22523,22524],{},"      @click=\"isDark = !isDark\"\n",[278,22526,22527],{"class":280,"line":375},[278,22528,7911],{},[278,22530,22531],{"class":280,"line":386},[278,22532,292],{"emptyLinePlaceholder":291},[278,22534,22535],{"class":280,"line":397},[278,22536,22537],{},"    \u003Ctemplate #fallback>\n",[278,22539,22540],{"class":280,"line":408},[278,22541,22542],{},"      \u003Cdiv class=\"w-8 h-8\" \u002F>\n",[278,22544,22545],{"class":280,"line":433},[278,22546,7313],{},[278,22548,22549],{"class":280,"line":454},[278,22550,22551],{},"  \u003C\u002FClientOnly>\n",[278,22553,22554],{"class":280,"line":475},[278,22555,7422],{},[278,22557,22558],{"class":280,"line":496},[278,22559,292],{"emptyLinePlaceholder":291},[278,22561,22562],{"class":280,"line":505},[278,22563,7431],{},[278,22565,22566],{"class":280,"line":516},[278,22567,22568],{},"const colorMode = useColorMode();\n",[278,22570,22571],{"class":280,"line":527},[278,22572,292],{"emptyLinePlaceholder":291},[278,22574,22575],{"class":280,"line":533},[278,22576,22577],{},"const isDark = computed({\n",[278,22579,22580],{"class":280,"line":539},[278,22581,22582],{},"  get() {\n",[278,22584,22585],{"class":280,"line":545},[278,22586,22587],{},"    return colorMode.value === 'dark';\n",[278,22589,22590],{"class":280,"line":551},[278,22591,683],{},[278,22593,22594],{"class":280,"line":557},[278,22595,22596],{},"  set() {\n",[278,22598,22599],{"class":280,"line":567},[278,22600,22601],{},"    colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';\n",[278,22603,22604],{"class":280,"line":577},[278,22605,683],{},[278,22607,22608],{"class":280,"line":587},[278,22609,3693],{},[278,22611,22612],{"class":280,"line":597},[278,22613,7691],{},[11,22615,22616,22617,21990],{},"Now you can use this color mode toggle button anywhere you like. In our app we've added it in the ",[59,22618,22619],{},"ChatHeader",[11,22621,22622],{},"Phew! And we have completed all the tasks we had set out to complete at the beginning of this article.",[24,22624,22626],{"id":22625},"deploying-the-app","Deploying the App",[11,22628,22629],{},"You can deploy the project in multiple ways. I would recommend to do it through the NuxtHub Admin console. Push your code to a Github repository, link this repository with NuxtHub and then deploy from the Admin console.",[11,22631,22632],{},"But if you want to see it live right away then you can use the below command",[269,22634,22635],{"className":3335,"code":10599,"language":3337,"meta":274,"style":274},[59,22636,22637],{"__ignoreMap":274},[278,22638,22639,22641,22643],{"class":280,"line":281},[278,22640,3349],{"class":333},[278,22642,3352],{"class":309},[278,22644,10610],{"class":309},[3300,22646,22647,22649],{"dataNodeType":3302},[3300,22648,3785],{"dataNodeType":3305},[3300,22650,22651],{"dataNodeType":3309},"If you do your first deployment with the NuxtHub CLI, you won't be able to attach your GitHub\u002FGitLab repository later on due to a Cloudflare limitation.",[11,22653,22654,22655,183],{},"For more details on deployment you can visit the ",[47,22656,22658],{"href":18190,"rel":22657},[51],"NuxtHub Docs",[24,22660,10621],{"id":10620},[11,22662,22663],{},"I only covered the most important parts of the source code here. You can visit the linked GIthub Repo for the complete source code. It should be self explanatory, but if you have question then please feel free to drop a comment below.",[40,22665],{"url":22666},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fhub-chat\u002F",[24,22668,10634],{"id":10633},[11,22670,22671],{},"Congratulations! You've successfully built a feature-rich LLM playground using Nuxt 3, NuxtUI, and Cloudflare Workers AI through NuxtHub. We've covered a wide range of topics, including:",[71,22673,22674,22677,22680],{},[74,22675,22676],{},"Handling streaming responses using Server Sent Events",[74,22678,22679],{},"Parsing and displaying markdown content in chat messages",[74,22681,22682],{},"Implementing auto-scrolling for a better user experience etc.",[11,22684,22685],{},"You can take this project as the starting point and improve it further by:",[71,22687,22688,22691,22694],{},[74,22689,22690],{},"Adding the ability to talks to other types of LLMs, e.g. text-to-image, image-to-text, speech recognition etc.",[74,22692,22693],{},"Implementing user authentication and session management",[74,22695,22696],{},"Adding support for multiple conversation threads etc.",[11,22698,22699],{},"Thank you for staying till the end. I hope you learned some new concepts from this article. Do let me know your learnings in the comments section. Your feedback and experiences are valuable not just to me, but to the entire community of developers exploring this fascinating field.",[11,22701,10642],{},[3048,22703],{},[18,22705,22706],{},[11,22707,22708,183],{},[3061,22709,22710],{},"Keep adding the bits and soon you'll have a lot of bytes to share with the world",[3065,22712,22713],{},"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 .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":274,"searchDepth":288,"depth":288,"links":22715},[22716,22717,22722,22727,22730,22735,22740,22741,22742],{"id":26,"depth":288,"text":27},{"id":3201,"depth":288,"text":3202,"children":22718},[22719,22720,22721],{"id":18396,"depth":295,"text":18397},{"id":3266,"depth":295,"text":3267},{"id":10909,"depth":295,"text":10912},{"id":18557,"depth":288,"text":18558,"children":22723},[22724,22725,22726],{"id":18567,"depth":295,"text":18568},{"id":18759,"depth":295,"text":18760},{"id":19274,"depth":295,"text":19275},{"id":19774,"depth":288,"text":19775,"children":22728},[22729],{"id":20018,"depth":295,"text":20019},{"id":20426,"depth":288,"text":20427,"children":22731},[22732,22733,22734],{"id":20433,"depth":295,"text":20434},{"id":20482,"depth":295,"text":20483},{"id":21321,"depth":295,"text":21322},{"id":21797,"depth":288,"text":21798,"children":22736},[22737,22738,22739],{"id":21804,"depth":295,"text":21805},{"id":22124,"depth":295,"text":22125},{"id":22368,"depth":295,"text":22369},{"id":22625,"depth":288,"text":22626},{"id":10620,"depth":288,"text":10621},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui\u002F6ae3905c-72e8-4e71-8ce0-b22680856f0d-aa3090c597.png","2024-08-29T07:46:37.681Z","You might be wondering, 'Another LLM (Large Language Model) playground? Aren't there plenty of these already?' Fair question. But here's the thing: the world of AI is constantly...","cm0ezeghd00110ama05jhaghe",{},"\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui",{"title":18320,"description":22745},"create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui",[10708,10710,10711,22752,10712],"workers-ai","zHfhudcPj8zZvHTxFxdi-yoSYdk9-qT6ZTDwoDySks8",[22755,22767,22776,22784,22799,22812,22825,22836,22848,22858,22871,22881],{"id":22756,"title":10815,"category":22757,"date":22758,"description":22759,"extension":22760,"featured":291,"liveUrl":22761,"meta":22762,"order":281,"repoUrl":10818,"status":22763,"stem":22764,"tags":22765,"writeupUrl":22748,"__hash__":22766},"projects\u002Fprojects\u002Fhub-chat.yml","experiment","2024-08-29","Cloudflare Workers AI playground for trying LLM prompts through a NuxtHub app.","yml","https:\u002F\u002Fhub-chat.rajeevs.workers.dev",{},"live","projects\u002Fhub-chat",[3194,3262,3254],"sYiah2ILQfMNQm_xOp9uB2bPmR0zzrmAK0sB8yTtWUo",{"id":22768,"title":13663,"category":22757,"date":22769,"description":22770,"extension":22760,"featured":291,"liveUrl":10802,"meta":22771,"order":288,"repoUrl":18180,"status":22763,"stem":22772,"tags":22773,"writeupUrl":18311,"__hash__":22775},"projects\u002Fprojects\u002Fchat-github.yml","2024-10-04","Natural-language GitHub search powered by OpenAI, wrapped in a conversational UI.",{},"projects\u002Fchat-github",[3191,22774,3254],"GitHub","m2Zu9woFU1rz7JJVOsntL1uyRCjwU5-yFvkVug9S8Mk",{"id":22777,"title":3124,"category":22757,"date":22778,"description":22779,"extension":22760,"featured":291,"liveUrl":3184,"meta":22780,"order":295,"repoUrl":10630,"status":22763,"stem":22781,"tags":22782,"writeupUrl":10704,"__hash__":22783},"projects\u002Fprojects\u002Fvhisper.yml","2024-11-29","Voice notes app that turns quick recordings into AI-transcribed, cleaned-up notes.",{},"projects\u002Fvhisper",[3191,3262,3254],"re4IY-AAsKYtet3NHnC849ajcVZUkBiK6khaO59E2f4",{"id":22785,"title":22786,"category":22787,"date":22788,"description":22789,"extension":22760,"featured":291,"liveUrl":22790,"meta":22791,"order":316,"repoUrl":22792,"status":22763,"stem":22793,"tags":22794,"writeupUrl":22797,"__hash__":22798},"projects\u002Fprojects\u002Fwrite-assist-ai.yml","Write Assist AI","shipped","2023-06-20","VS Code extension for rewriting, summarizing, expanding, and polishing prose with OpenAI-compatible models.","https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=ra-jeev.write-assist-ai",{},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fwrite-assist-ai","projects\u002Fwrite-assist-ai",[22795,22796,10924],"VS Code","TypeScript","\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension","djJ6a_y1SSftTRTiNWk3HEd_vLAq3tgMGKeKpIre79s",{"id":22800,"title":22801,"category":22757,"date":22802,"description":22803,"extension":22760,"featured":3086,"liveUrl":22804,"meta":22805,"order":322,"repoUrl":22806,"status":22763,"stem":22807,"tags":22808,"writeupUrl":22810,"__hash__":22811},"projects\u002Fprojects\u002Fname-insights.yml","Name Insights","2024-07-29","AI-assisted domain name scoring and comparison tool for choosing better project names.","https:\u002F\u002Fname-insights.pages.dev",{},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fdomain-name-insights","projects\u002Fname-insights",[3191,3254,22809],"Domains","\u002Fleveraging-ai-for-smarter-domain-name-decisions","g6sjv5nSOfumf-vmCnx2g_UfHwJ5zCBHoOdCZktykro",{"id":22813,"title":22814,"category":22757,"date":22815,"description":22816,"extension":22760,"featured":3086,"liveUrl":22815,"meta":22817,"order":327,"repoUrl":22818,"status":22757,"stem":22819,"tags":22820,"writeupUrl":22815,"__hash__":22824},"projects\u002Fprojects\u002Fexpense-buddy.yml","Expense Buddy",null,"Basic expense tracker built with React and MongoDB Atlas App Services.",{},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fexpense-buddy","projects\u002Fexpense-buddy",[22821,22822,22823],"React","MongoDB","Finance","5Np_ZrPfecIH-DUlNYVh9uRfXCZdFYg73rsNPLDs75w",{"id":22826,"title":22827,"category":22787,"date":22815,"description":22828,"extension":22760,"featured":3086,"liveUrl":22829,"meta":22830,"order":386,"repoUrl":22815,"status":22763,"stem":22831,"tags":22832,"writeupUrl":22815,"__hash__":22835},"projects\u002Fprojects\u002Fshri-ram-shalaka.yml","Shri Ram Shalaka","Bilingual Shri Ram Shalaka Prashnavali app for devotional guidance, with PWA support and Play Store presence.","https:\u002F\u002Fshriramshalaka.com\u002Fen",{},"projects\u002Fshri-ram-shalaka",[3191,22833,22834],"PWA","Devotional","0ovkO2hufLl5AizBy6KRZuBbylKVK_If4D4wjFVlsM4",{"id":22837,"title":22838,"category":22787,"date":22815,"description":22839,"extension":22760,"featured":3086,"liveUrl":22840,"meta":22841,"order":397,"repoUrl":22815,"status":22763,"stem":22842,"tags":22843,"writeupUrl":22815,"__hash__":22847},"projects\u002Fprojects\u002Fsubhashitams.yml","Subhashitams","Curated Sanskrit subhashitams with Devanagari text, transliteration, and Hindi\u002FEnglish meanings.","https:\u002F\u002Fsubhashitams.com\u002Fen",{},"projects\u002Fsubhashitams",[22844,22845,22846],"Sanskrit","Reading","Personal","8wpYlzzHSNJgqts6k5DrADs9cp2M0ST87eYkW1tZYX4",{"id":22849,"title":22850,"category":22787,"date":22815,"description":22851,"extension":22760,"featured":3086,"liveUrl":22852,"meta":22853,"order":408,"repoUrl":22815,"status":22763,"stem":22854,"tags":22855,"writeupUrl":22815,"__hash__":22857},"projects\u002Fprojects\u002Framcharitmanas.yml","Sri Ram Charit Manas","Clean, ad-free PWA for reading the complete Sri Ram Charit Manas with search and offline support.","https:\u002F\u002Framcharithmanas.com\u002Fen",{},"projects\u002Framcharitmanas",[22845,22856,22846],"Devotional Texts","lUJCmD2OQ4R3x6z3TFLtBAdeTKJtKq7RjCogH8o9en4",{"id":22859,"title":22860,"category":22787,"date":22861,"description":22862,"extension":22760,"featured":3086,"liveUrl":22863,"meta":22864,"order":433,"repoUrl":22865,"status":22763,"stem":22866,"tags":22867,"writeupUrl":22869,"__hash__":22870},"projects\u002Fprojects\u002Fgoldroad.yml","GoldRoad","2022-12-13","Daily puzzle game where everyone gets the same challenge, backed by React, MongoDB, and Cloud Run.","https:\u002F\u002Fplaygoldroad.com",{},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fgoldroad","projects\u002Fgoldroad",[22821,22822,22868],"Game","\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run","xR7zvcJK8mk2MIZJ1g2uLaRT5I5Wz6s2P1PTJATnwv4",{"id":22872,"title":52,"category":22757,"date":22873,"description":22874,"extension":22760,"featured":3086,"liveUrl":49,"meta":22875,"order":614,"repoUrl":248,"status":22763,"stem":22876,"tags":22877,"writeupUrl":3090,"__hash__":22880},"projects\u002Fprojects\u002Fbrew-haven.yml","2024-12-29","Coffee shop customization demo built to explore DevCycle feature flags and A\u002FB testing.",{},"projects\u002Fbrew-haven",[22821,22878,22879],"Feature Flags","Netlify","pMI1lwpF26NxyUp2j9lhY1FZP_YESjy23ssrrnrdIV0",{"id":22882,"title":22883,"category":22757,"date":22884,"description":22885,"extension":22760,"featured":3086,"liveUrl":22886,"meta":22887,"order":620,"repoUrl":22888,"status":22763,"stem":22889,"tags":22890,"writeupUrl":22892,"__hash__":22893},"projects\u002Fprojects\u002Fmy-piggy-jar.yml","My Piggy Jar","2022-10-01","Family piggy bank tracker that started as a promise to make saving visible for kids.","https:\u002F\u002Fwww.mypiggyjar.com",{},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fkids-piggy-app","projects\u002Fmy-piggy-jar",[22821,22891,22846],"Amplify","\u002Fnew-years-promise-to-my-son","x2RZJS0RCVfU7CP18lGkYfXuqWF9iv7azEJEf922E_c",1780400659931]