[{"data":1,"prerenderedAt":55254},["ShallowReactive",2],{"blog-posts":3},[4,3099,10714,18318,22754,23918,24421,26203,32010,34834,37438,38599,40302,40889,44742,45225,45746,49711,50911,52289,54701],{"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",{"id":22755,"title":22756,"body":22757,"cover":23905,"date":23906,"description":23907,"draft":3086,"extension":3087,"hashnodeId":23908,"meta":23909,"navigation":291,"path":23910,"seo":23911,"slug":23912,"stem":23912,"tags":23913,"__hash__":23917},"posts\u002Fleveraging-ai-for-smarter-domain-name-decisions.md","Beyond Gut Feeling: Leveraging AI for Smarter Domain Name Decisions",{"type":8,"value":22758,"toc":23891},[22759,22762,22769,22773,22776,22779,22782,22786,22789,22793,22796,22830,22834,22837,22851,22855,22858,22872,22876,22881,22887,22892,22898,22903,22909,22914,22920,22925,22931,22936,22942,22947,22952,22956,22959,22989,22995,23202,23205,23508,23511,23518,23626,23632,23809,23812,23816,23823,23826,23829,23833,23836,23839,23841,23844,23855,23857,23860,23871,23874,23876,23879,23882,23888],[11,22760,22761],{},"If you work in tech-related fields, sooner or later there will come a time when you have to pick a domain name for the idea you've been working on. Whether it's an app or a service, it still needs a name. And if you're anything like me, this is one of the trickiest parts of building that idea. The perfect domain name can make your project more memorable, boost SEO, and even influence user trust. But how do you know if you're making the right choice?",[11,22763,22764,22765,22768],{},"Traditionally, selecting a domain name has been a mix of creativity, gut feeling, and perhaps a quick poll among people in your circle of influence. The post-GPT era might have added another ingredient to the mix: consulting your trusted AI\u002FLLM. But if you have talked to this advisor, it generally throws some names at you without revealing why it chose these names. And this is where ",[94,22766,22767],{},"\"Name Insights\""," steps in, offering not just suggestions, but insights into the 'why' behind those domain name choices.",[24,22770,22772],{"id":22771},"introduction","Introduction",[11,22774,22775],{},"I have been thinking about domain names (hoarding to be frank) for quite some time now. Maybe because I have bought more domains in the recent past than my usual appetite allows me to. And the first step to this whole process is: knowing the name that you want to buy.",[11,22777,22778],{},"After the usual availability checks, you wonder whether that weird-looking name is right for the purpose you're buying it for. Let's be honest, all the good ones are already taken (why does it feel like I'm not writing a tech article?) or the price is simply beyond your reach. Sometimes you zero down on more than one name, so which one should you go ahead with? These were the guiding questions behind building Name Insights.",[11,22780,22781],{},"Name Insights aims to address these challenges by providing detailed, contextual answers to help you make informed decisions about domain names.",[24,22783,22785],{"id":22784},"app-features","App Features",[11,22787,22788],{},"Name Insights offers three powerful services to assist in your domain name decision-making process. Each service utilizes an AI-driven scoring system that evaluates domains based on six key aspects of what makes a good domain name. Let's explore each feature:",[32,22790,22792],{"id":22791},"name-insights-scoring","Name Insights & Scoring",[11,22794,22795],{},"This feature provides an unbiased view of what kind of app or service is best suited for a given domain name. It:",[71,22797,22798,22821,22824,22827],{},[74,22799,22800,22801],{},"Evaluates the domain on 6 different parameters, namely:",[123,22802,22803,22806,22809,22812,22815,22818],{},[74,22804,22805],{},"Brand Impact: Memorability, brandability, uniqueness, emotional appeal.",[74,22807,22808],{},"Usability: Length, spelling simplicity, pronunciation clarity, absence of numbers\u002Fhyphens.",[74,22810,22811],{},"Relevance and SEO: Relevance to purpose, keyword inclusion, extension potential.",[74,22813,22814],{},"Technical Considerations: TLD appropriateness, potential social media availability.",[74,22816,22817],{},"Legal and Cultural Factors: Potential trademark risks, cultural\u002Flinguistic considerations.",[74,22819,22820],{},"Market Potential: Ability to target desired audience, scalability for business growth.",[74,22822,22823],{},"Provides an overall score based on weighted parameters.",[74,22825,22826],{},"Offers a brief explanation for each score.",[74,22828,22829],{},"Highlights pros and cons of the domain name.",[32,22831,22833],{"id":22832},"domain-names-comparison","Domain Names Comparison",[11,22835,22836],{},"This feature helps you choose between different options you might have. It:",[71,22838,22839,22842,22845,22848],{},[74,22840,22841],{},"Compares two different domain names",[74,22843,22844],{},"Uses the same scoring strategy as the insights feature",[74,22846,22847],{},"Determines a \"winning\" name",[74,22849,22850],{},"Provides a brief summary explaining the choice",[32,22852,22854],{"id":22853},"domain-name-ideas","Domain Name Ideas",[11,22856,22857],{},"This feature is most useful when you're starting from scratch. It:",[71,22859,22860,22863,22866,22869],{},[74,22861,22862],{},"Builds upon the same domain name scoring strategy, and offers five distinct domain name suggestions based on your app\u002Fservice idea",[74,22864,22865],{},"Provides an overall score for each suggestion",[74,22867,22868],{},"Breaks down different category scores",[74,22870,22871],{},"Offers a brief summary of strengths and weaknesses for each name",[24,22873,22875],{"id":22874},"app-demoscreenshots","App Demo\u002FScreenshots",[11,22877,22878],{},[94,22879,22880],{},"Home Page",[11,22882,22883],{},[3135,22884],{"alt":22885,"src":22886},"name insights home page","\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002Fb95c841b-c291-4044-8bbc-4f85e1193588-f906d486e1.png",[11,22888,22889],{},[94,22890,22891],{},"Name insights loading animation",[11,22893,22894],{},[3135,22895],{"alt":22896,"src":22897},"name insights loading animation","\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002F7724153f-acb7-45f0-9da1-e3f9a1301357-65859f0c09.png",[11,22899,22900],{},[94,22901,22902],{},"Name Insights Page (for hashnode.com domain)",[11,22904,22905],{},[3135,22906],{"alt":22907,"src":22908},"name insights for hashnode.com","\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002Feb979fdb-c7f5-4faf-b03b-41cce3c63f8f-ad8760b883.png",[11,22910,22911],{},[94,22912,22913],{},"Name comparison page",[11,22915,22916],{},[3135,22917],{"alt":22918,"src":22919},"name comparison page","\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002F15478b0f-18f2-4772-883c-aafc5cb3cfd0-24ff97e0c4.png",[11,22921,22922],{},[94,22923,22924],{},"Comparison loading animation",[11,22926,22927],{},[3135,22928],{"alt":22929,"src":22930},"comparison loading animation","\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002F1a06b546-b53e-4325-a829-89f24f3af4e1-b7d5d1280c.png",[11,22932,22933],{},[94,22934,22935],{},"Comparison results for fitnawake.com & fitandawake.com",[11,22937,22938],{},[3135,22939],{"alt":22940,"src":22941},"comparison results page","\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002Fd376887e-c449-4241-95e1-4ae18be14259-0dd492a1cb.png",[11,22943,22944],{},[94,22945,22946],{},"Name ideas results page",[11,22948,22949],{},[3135,22950],{"alt":274,"src":22951},"\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002Fe2e0353e-1d35-474e-a3ce-51f801740dea-bf0f13c4cf.png",[24,22953,22955],{"id":22954},"technical-details","Technical Details",[11,22957,22958],{},"Here is a brief overview of the app stack and approach",[123,22960,22961,22967,22977,22983],{},[74,22962,22963,22966],{},[94,22964,22965],{},"Nuxt3:"," Name Insights uses the full-stack capabilities of Nuxt3 as its backbone for a faster turnaround time. You can achieve the same results using another framework of your choice, but for me it made more sense because of where it is hosted (see point 2).",[74,22968,22969,22972,22973,22976],{},[94,22970,22971],{},"NuxtHub:"," The app is hosted on Cloudflare Pages using ",[47,22974,3194],{"href":18328,"rel":22975},[51],". NuxtHub offers a great DX for hosting serverless Nuxt apps, and for talking to various Cloudflare services. At the time of writing this article it supports Cloudflare features such as KV, D1, and R2.",[74,22978,22979,22982],{},[94,22980,22981],{},"NuxtUI:"," The app UI is built using NuxtUI, a collection of prebuilt components based on HeadlessUI & TailwindCSS.",[74,22984,22985,22988],{},[94,22986,22987],{},"Claude 3.5 Sonnet \u002F Anthropic AI:"," The core features of the app are thanks to the Anthropic AI SDK, using the Claude 3.5 Sonnet model. The best in class reasoning capabilities of the model allows for a good overall experience of the app features.",[11,22990,22991,22992,22994],{},"At the heart of the app features is the domain scoring strategy. At present, the app relies on the advanced reasoning capabilities of the ",[94,22993,18224],{}," model to grade a domain name. The key to the implementation lies in carefully crafted system prompts (which you call Prompt Engineering). These prompts instruct the AI on how to analyze domain names, what factors to consider, and how to present the results. E.g. the below is the core part of the system prompt for scoring a domain and generating insights:",[269,22996,22998],{"className":271,"code":22997,"language":273,"meta":274,"style":274},"const nameScorePrompt = `You are an AI assistant specialized in \nevaluating and scoring domain names. Analyze the given domain name \nas if you're seeing it for the very first time, without any prior \nknowledge of its actual use or purpose.\n\nEvaluate the domain based on these categories and weights:\n1. Brand Impact (30%)\n2. Usability (20%)\n3. Relevance and SEO (20%)\n4. Technical Considerations (15%)\n5. Legal and Cultural Factors (10%)\n6. Market Potential (5%)\n\nProvide a score out of 100 for each category, along with a brief, \nunbiased explanation. Calculate the weighted overall score out of 100 \n(rounded to the nearest integer). Include concise lists of strengths \nand weaknesses of the domain name based solely on its characteristics, \nnot its known use. \n`\n\nconst response = await this.anthropic.messages.create({\n  model: \"claude-3-5-sonnet-20240620\",\n  max_tokens: 1000,\n  temperature: 0.4,\n  system: systemPrompt,\n  messages: [\n    {\n      role: \"user\",\n      content: `Analyze the domain name: ${domainName}`,\n    },\n  ],\n});\n",[59,22999,23000,23012,23017,23022,23027,23031,23036,23041,23046,23051,23056,23061,23066,23070,23075,23080,23085,23090,23095,23099,23103,23123,23133,23142,23152,23157,23162,23166,23175,23189,23193,23198],{"__ignoreMap":274},[278,23001,23002,23004,23007,23009],{"class":280,"line":281},[278,23003,5416],{"class":298},[278,23005,23006],{"class":650}," nameScorePrompt",[278,23008,764],{"class":298},[278,23010,23011],{"class":309}," `You are an AI assistant specialized in \n",[278,23013,23014],{"class":280,"line":288},[278,23015,23016],{"class":309},"evaluating and scoring domain names. Analyze the given domain name \n",[278,23018,23019],{"class":280,"line":295},[278,23020,23021],{"class":309},"as if you're seeing it for the very first time, without any prior \n",[278,23023,23024],{"class":280,"line":316},[278,23025,23026],{"class":309},"knowledge of its actual use or purpose.\n",[278,23028,23029],{"class":280,"line":322},[278,23030,292],{"emptyLinePlaceholder":291},[278,23032,23033],{"class":280,"line":327},[278,23034,23035],{"class":309},"Evaluate the domain based on these categories and weights:\n",[278,23037,23038],{"class":280,"line":340},[278,23039,23040],{"class":309},"1. Brand Impact (30%)\n",[278,23042,23043],{"class":280,"line":349},[278,23044,23045],{"class":309},"2. Usability (20%)\n",[278,23047,23048],{"class":280,"line":375},[278,23049,23050],{"class":309},"3. Relevance and SEO (20%)\n",[278,23052,23053],{"class":280,"line":386},[278,23054,23055],{"class":309},"4. Technical Considerations (15%)\n",[278,23057,23058],{"class":280,"line":397},[278,23059,23060],{"class":309},"5. Legal and Cultural Factors (10%)\n",[278,23062,23063],{"class":280,"line":408},[278,23064,23065],{"class":309},"6. Market Potential (5%)\n",[278,23067,23068],{"class":280,"line":433},[278,23069,292],{"emptyLinePlaceholder":291},[278,23071,23072],{"class":280,"line":454},[278,23073,23074],{"class":309},"Provide a score out of 100 for each category, along with a brief, \n",[278,23076,23077],{"class":280,"line":475},[278,23078,23079],{"class":309},"unbiased explanation. Calculate the weighted overall score out of 100 \n",[278,23081,23082],{"class":280,"line":496},[278,23083,23084],{"class":309},"(rounded to the nearest integer). Include concise lists of strengths \n",[278,23086,23087],{"class":280,"line":505},[278,23088,23089],{"class":309},"and weaknesses of the domain name based solely on its characteristics, \n",[278,23091,23092],{"class":280,"line":516},[278,23093,23094],{"class":309},"not its known use. \n",[278,23096,23097],{"class":280,"line":527},[278,23098,14972],{"class":309},[278,23100,23101],{"class":280,"line":533},[278,23102,292],{"emptyLinePlaceholder":291},[278,23104,23105,23107,23109,23111,23113,23116,23119,23121],{"class":280,"line":539},[278,23106,5416],{"class":298},[278,23108,1115],{"class":650},[278,23110,764],{"class":298},[278,23112,1120],{"class":298},[278,23114,23115],{"class":650}," this",[278,23117,23118],{"class":302},".anthropic.messages.",[278,23120,11913],{"class":333},[278,23122,637],{"class":302},[278,23124,23125,23128,23131],{"class":280,"line":545},[278,23126,23127],{"class":302},"  model: ",[278,23129,23130],{"class":309},"\"claude-3-5-sonnet-20240620\"",[278,23132,660],{"class":302},[278,23134,23135,23138,23140],{"class":280,"line":551},[278,23136,23137],{"class":302},"  max_tokens: ",[278,23139,6615],{"class":650},[278,23141,660],{"class":302},[278,23143,23144,23147,23150],{"class":280,"line":557},[278,23145,23146],{"class":302},"  temperature: ",[278,23148,23149],{"class":650},"0.4",[278,23151,660],{"class":302},[278,23153,23154],{"class":280,"line":567},[278,23155,23156],{"class":302},"  system: systemPrompt,\n",[278,23158,23159],{"class":280,"line":577},[278,23160,23161],{"class":302},"  messages: [\n",[278,23163,23164],{"class":280,"line":587},[278,23165,2209],{"class":302},[278,23167,23168,23170,23173],{"class":280,"line":597},[278,23169,12774],{"class":302},[278,23171,23172],{"class":309},"\"user\"",[278,23174,660],{"class":302},[278,23176,23177,23179,23182,23185,23187],{"class":280,"line":608},[278,23178,12784],{"class":302},[278,23180,23181],{"class":309},"`Analyze the domain name: ${",[278,23183,23184],{"class":302},"domainName",[278,23186,1277],{"class":309},[278,23188,660],{"class":302},[278,23190,23191],{"class":280,"line":614},[278,23192,2243],{"class":302},[278,23194,23195],{"class":280,"line":620},[278,23196,23197],{"class":302},"  ],\n",[278,23199,23200],{"class":280,"line":625},[278,23201,3693],{"class":302},[11,23203,23204],{},"The prompt is intentionally detailed and specific to get the desired results from the LLM. We incorporate the required JSON structure into the prompt to receive the output in JSON format. This output is then parsed using the following function:",[269,23206,23208],{"className":271,"code":23207,"language":273,"meta":274,"style":274},"export function extractAndParseJson\u003CT>(text: string): T {\n  try {\n    return JSON.parse(text) as T;\n  } catch (error) {\n    console.error(\"Error parsing JSON:\", error);\n  }\n\n  const backtickPattern = \u002F```(?:json)?\\s*([\\s\\S]*?)\\s*```\u002Fg;\n  const matches = text.match(backtickPattern);\n\n  if (matches) {\n    for (const match of matches) {\n      const content = match.replace(\u002F```(?:json)?\\s*|\\s*```\u002Fg, \"\").trim();\n      try {\n        return JSON.parse(content) as T;\n      } catch (error) {\n        continue;\n      }\n    }\n  }\n\n  throw new Error(\"No valid JSON found in the text\");\n}\n",[59,23209,23210,23241,23247,23266,23274,23287,23291,23295,23340,23357,23361,23368,23384,23431,23437,23457,23465,23472,23476,23480,23484,23488,23504],{"__ignoreMap":274},[278,23211,23212,23214,23216,23219,23221,23224,23226,23228,23230,23232,23234,23236,23239],{"class":280,"line":281},[278,23213,628],{"class":298},[278,23215,748],{"class":298},[278,23217,23218],{"class":333}," extractAndParseJson",[278,23220,1702],{"class":302},[278,23222,23223],{"class":333},"T",[278,23225,20521],{"class":302},[278,23227,4582],{"class":501},[278,23229,960],{"class":298},[278,23231,963],{"class":650},[278,23233,17418],{"class":302},[278,23235,960],{"class":298},[278,23237,23238],{"class":333}," T",[278,23240,876],{"class":302},[278,23242,23243,23245],{"class":280,"line":288},[278,23244,1105],{"class":298},[278,23246,876],{"class":302},[278,23248,23249,23251,23253,23255,23257,23260,23262,23264],{"class":280,"line":295},[278,23250,1088],{"class":298},[278,23252,12063],{"class":650},[278,23254,183],{"class":302},[278,23256,12068],{"class":333},[278,23258,23259],{"class":302},"(text) ",[278,23261,2937],{"class":298},[278,23263,23238],{"class":333},[278,23265,313],{"class":302},[278,23267,23268,23270,23272],{"class":280,"line":316},[278,23269,1397],{"class":302},[278,23271,1400],{"class":298},[278,23273,1403],{"class":302},[278,23275,23276,23278,23280,23282,23285],{"class":280,"line":322},[278,23277,1409],{"class":302},[278,23279,1412],{"class":333},[278,23281,1126],{"class":302},[278,23283,23284],{"class":309},"\"Error parsing JSON:\"",[278,23286,1420],{"class":302},[278,23288,23289],{"class":280,"line":327},[278,23290,1096],{"class":302},[278,23292,23293],{"class":280,"line":340},[278,23294,292],{"emptyLinePlaceholder":291},[278,23296,23297,23299,23302,23304,23307,23310,23312,23315,23317,23319,23322,23325,23327,23329,23331,23334,23336,23338],{"class":280,"line":349},[278,23298,758],{"class":298},[278,23300,23301],{"class":650}," backtickPattern",[278,23303,764],{"class":298},[278,23305,23306],{"class":309}," \u002F",[278,23308,23309],{"class":17404},"```(?:json)",[278,23311,5114],{"class":298},[278,23313,23314],{"class":650},"\\s",[278,23316,1351],{"class":298},[278,23318,1126],{"class":17404},[278,23320,23321],{"class":650},"[\\s\\S]",[278,23323,23324],{"class":298},"*?",[278,23326,17418],{"class":17404},[278,23328,23314],{"class":650},[278,23330,1351],{"class":298},[278,23332,23333],{"class":17404},"```",[278,23335,13413],{"class":309},[278,23337,13421],{"class":298},[278,23339,313],{"class":302},[278,23341,23342,23344,23347,23349,23352,23354],{"class":280,"line":375},[278,23343,758],{"class":298},[278,23345,23346],{"class":650}," matches",[278,23348,764],{"class":298},[278,23350,23351],{"class":302}," text.",[278,23353,17397],{"class":333},[278,23355,23356],{"class":302},"(backtickPattern);\n",[278,23358,23359],{"class":280,"line":386},[278,23360,292],{"emptyLinePlaceholder":291},[278,23362,23363,23365],{"class":280,"line":397},[278,23364,1062],{"class":298},[278,23366,23367],{"class":302}," (matches) {\n",[278,23369,23370,23372,23374,23376,23379,23381],{"class":280,"line":408},[278,23371,12012],{"class":298},[278,23373,1245],{"class":302},[278,23375,5416],{"class":298},[278,23377,23378],{"class":650}," match",[278,23380,12022],{"class":298},[278,23382,23383],{"class":302}," matches) {\n",[278,23385,23386,23388,23390,23392,23395,23397,23399,23401,23403,23405,23407,23410,23412,23414,23416,23418,23420,23422,23425,23427,23429],{"class":280,"line":433},[278,23387,2461],{"class":298},[278,23389,7719],{"class":650},[278,23391,764],{"class":298},[278,23393,23394],{"class":302}," match.",[278,23396,13408],{"class":333},[278,23398,1126],{"class":302},[278,23400,13413],{"class":309},[278,23402,23309],{"class":17404},[278,23404,5114],{"class":298},[278,23406,23314],{"class":650},[278,23408,23409],{"class":298},"*|",[278,23411,23314],{"class":650},[278,23413,1351],{"class":298},[278,23415,23333],{"class":17404},[278,23417,13413],{"class":309},[278,23419,13421],{"class":298},[278,23421,1708],{"class":302},[278,23423,23424],{"class":309},"\"\"",[278,23426,4633],{"class":302},[278,23428,13245],{"class":333},[278,23430,1313],{"class":302},[278,23432,23433,23435],{"class":280,"line":454},[278,23434,14341],{"class":298},[278,23436,876],{"class":302},[278,23438,23439,23442,23444,23446,23448,23451,23453,23455],{"class":280,"line":475},[278,23440,23441],{"class":298},"        return",[278,23443,12063],{"class":650},[278,23445,183],{"class":302},[278,23447,12068],{"class":333},[278,23449,23450],{"class":302},"(content) ",[278,23452,2937],{"class":298},[278,23454,23238],{"class":333},[278,23456,313],{"class":302},[278,23458,23459,23461,23463],{"class":280,"line":496},[278,23460,14445],{"class":302},[278,23462,1400],{"class":298},[278,23464,1403],{"class":302},[278,23466,23467,23470],{"class":280,"line":505},[278,23468,23469],{"class":298},"        continue",[278,23471,313],{"class":302},[278,23473,23474],{"class":280,"line":516},[278,23475,6234],{"class":302},[278,23477,23478],{"class":280,"line":527},[278,23479,1285],{"class":302},[278,23481,23482],{"class":280,"line":533},[278,23483,1096],{"class":302},[278,23485,23486],{"class":280,"line":539},[278,23487,292],{"emptyLinePlaceholder":291},[278,23489,23490,23493,23495,23497,23499,23502],{"class":280,"line":545},[278,23491,23492],{"class":298},"  throw",[278,23494,1258],{"class":298},[278,23496,1261],{"class":333},[278,23498,1126],{"class":302},[278,23500,23501],{"class":309},"\"No valid JSON found in the text\"",[278,23503,1280],{"class":302},[278,23505,23506],{"class":280,"line":551},[278,23507,617],{"class":302},[11,23509,23510],{},"To try out different LLMs from other AI services, the backend implementation is kept generic using interfaces which every AI service needs to implement and extend.",[11,23512,23513,23514,23517],{},"Here is the ",[94,23515,23516],{},"AIService"," interface:",[269,23519,23521],{"className":271,"code":23520,"language":273,"meta":274,"style":274},"export interface AIService {\n  getDomainScore(domainName: string): Promise\u003Cstring>;\n  compareDomains(firstDomain: string, secondDomain: string): Promise\u003Cstring>;\n  getDomainSuggestions(purpose: string): Promise\u003Cstring>;\n}\n",[59,23522,23523,23535,23561,23596,23622],{"__ignoreMap":274},[278,23524,23525,23527,23530,23533],{"class":280,"line":281},[278,23526,628],{"class":298},[278,23528,23529],{"class":298}," interface",[278,23531,23532],{"class":333}," AIService",[278,23534,876],{"class":302},[278,23536,23537,23540,23542,23544,23546,23548,23550,23552,23554,23556,23558],{"class":280,"line":288},[278,23538,23539],{"class":333},"  getDomainScore",[278,23541,1126],{"class":302},[278,23543,23184],{"class":501},[278,23545,960],{"class":298},[278,23547,963],{"class":650},[278,23549,17418],{"class":302},[278,23551,960],{"class":298},[278,23553,2057],{"class":333},[278,23555,1702],{"class":302},[278,23557,1705],{"class":650},[278,23559,23560],{"class":302},">;\n",[278,23562,23563,23566,23568,23571,23573,23575,23577,23580,23582,23584,23586,23588,23590,23592,23594],{"class":280,"line":295},[278,23564,23565],{"class":333},"  compareDomains",[278,23567,1126],{"class":302},[278,23569,23570],{"class":501},"firstDomain",[278,23572,960],{"class":298},[278,23574,963],{"class":650},[278,23576,1708],{"class":302},[278,23578,23579],{"class":501},"secondDomain",[278,23581,960],{"class":298},[278,23583,963],{"class":650},[278,23585,17418],{"class":302},[278,23587,960],{"class":298},[278,23589,2057],{"class":333},[278,23591,1702],{"class":302},[278,23593,1705],{"class":650},[278,23595,23560],{"class":302},[278,23597,23598,23601,23603,23606,23608,23610,23612,23614,23616,23618,23620],{"class":280,"line":316},[278,23599,23600],{"class":333},"  getDomainSuggestions",[278,23602,1126],{"class":302},[278,23604,23605],{"class":501},"purpose",[278,23607,960],{"class":298},[278,23609,963],{"class":650},[278,23611,17418],{"class":302},[278,23613,960],{"class":298},[278,23615,2057],{"class":333},[278,23617,1702],{"class":302},[278,23619,1705],{"class":650},[278,23621,23560],{"class":302},[278,23623,23624],{"class":280,"line":322},[278,23625,617],{"class":302},[11,23627,23628,23629],{},"And the ",[94,23630,23631],{},"BaseAIService",[269,23633,23635],{"className":271,"code":23634,"language":273,"meta":274,"style":274},"export abstract class BaseAIService implements AIService {\n  protected getSystemPrompt(promptType: SystemPromptType): string {\n    return getSystemPrompt(promptType);\n  }\n\n  abstract getDomainScore(domainName: string): Promise\u003Cstring>;\n\n  abstract compareDomains(\n    firstDomain: string,\n    secondDomain: string\n  ): Promise\u003Cstring>;\n\n  abstract getDomainSuggestions(purpose: string): Promise\u003Cstring>;\n}\n",[59,23636,23637,23656,23681,23690,23694,23698,23726,23730,23739,23750,23759,23774,23778,23805],{"__ignoreMap":274},[278,23638,23639,23641,23644,23646,23649,23652,23654],{"class":280,"line":281},[278,23640,628],{"class":298},[278,23642,23643],{"class":298}," abstract",[278,23645,16622],{"class":298},[278,23647,23648],{"class":333}," BaseAIService",[278,23650,23651],{"class":298}," implements",[278,23653,23532],{"class":333},[278,23655,876],{"class":302},[278,23657,23658,23661,23663,23665,23668,23670,23673,23675,23677,23679],{"class":280,"line":288},[278,23659,23660],{"class":298},"  protected",[278,23662,16450],{"class":333},[278,23664,1126],{"class":302},[278,23666,23667],{"class":501},"promptType",[278,23669,960],{"class":298},[278,23671,23672],{"class":333}," SystemPromptType",[278,23674,17418],{"class":302},[278,23676,960],{"class":298},[278,23678,963],{"class":650},[278,23680,876],{"class":302},[278,23682,23683,23685,23687],{"class":280,"line":295},[278,23684,1088],{"class":298},[278,23686,16450],{"class":333},[278,23688,23689],{"class":302},"(promptType);\n",[278,23691,23692],{"class":280,"line":316},[278,23693,1096],{"class":302},[278,23695,23696],{"class":280,"line":322},[278,23697,292],{"emptyLinePlaceholder":291},[278,23699,23700,23703,23706,23708,23710,23712,23714,23716,23718,23720,23722,23724],{"class":280,"line":327},[278,23701,23702],{"class":298},"  abstract",[278,23704,23705],{"class":333}," getDomainScore",[278,23707,1126],{"class":302},[278,23709,23184],{"class":501},[278,23711,960],{"class":298},[278,23713,963],{"class":650},[278,23715,17418],{"class":302},[278,23717,960],{"class":298},[278,23719,2057],{"class":333},[278,23721,1702],{"class":302},[278,23723,1705],{"class":650},[278,23725,23560],{"class":302},[278,23727,23728],{"class":280,"line":340},[278,23729,292],{"emptyLinePlaceholder":291},[278,23731,23732,23734,23737],{"class":280,"line":349},[278,23733,23702],{"class":298},[278,23735,23736],{"class":333}," compareDomains",[278,23738,770],{"class":302},[278,23740,23741,23744,23746,23748],{"class":280,"line":375},[278,23742,23743],{"class":501},"    firstDomain",[278,23745,960],{"class":298},[278,23747,963],{"class":650},[278,23749,660],{"class":302},[278,23751,23752,23755,23757],{"class":280,"line":386},[278,23753,23754],{"class":501},"    secondDomain",[278,23756,960],{"class":298},[278,23758,17191],{"class":650},[278,23760,23761,23764,23766,23768,23770,23772],{"class":280,"line":397},[278,23762,23763],{"class":302},"  )",[278,23765,960],{"class":298},[278,23767,2057],{"class":333},[278,23769,1702],{"class":302},[278,23771,1705],{"class":650},[278,23773,23560],{"class":302},[278,23775,23776],{"class":280,"line":408},[278,23777,292],{"emptyLinePlaceholder":291},[278,23779,23780,23782,23785,23787,23789,23791,23793,23795,23797,23799,23801,23803],{"class":280,"line":433},[278,23781,23702],{"class":298},[278,23783,23784],{"class":333}," getDomainSuggestions",[278,23786,1126],{"class":302},[278,23788,23605],{"class":501},[278,23790,960],{"class":298},[278,23792,963],{"class":650},[278,23794,17418],{"class":302},[278,23796,960],{"class":298},[278,23798,2057],{"class":333},[278,23800,1702],{"class":302},[278,23802,1705],{"class":650},[278,23804,23560],{"class":302},[278,23806,23807],{"class":280,"line":454},[278,23808,617],{"class":302},[11,23810,23811],{},"This approach provides a common system prompt for all AI services while allowing individual services the flexibility to override and define their own custom system prompts if needed.",[24,23813,23815],{"id":23814},"app-repo-links","App & Repo Links",[11,23817,23818,23819],{},"You can try out the app live at ",[47,23820,23821],{"href":23821,"rel":23822},"https:\u002F\u002Fname-insights.nuxt.dev\u002F",[51],[11,23824,23825],{},"The complete app code can be found here:",[40,23827],{"url":23828},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fdomain-name-insights",[24,23830,23832],{"id":23831},"limitations","Limitations",[11,23834,23835],{},"As the app relies entirely on an LLM for its core functionality, the output is not deterministic. This means that if you analyze the same domain name multiple times, you may receive slightly different scores. This variability is inherent to large language models, which can produce different outputs based on subtle differences in how they process the input each time.",[11,23837,23838],{},"However, in my testing, I've found that this limitation doesn't significantly impact the app's utility. The variance in scores tends to be low, and the overall insights remain consistent. Name Insights is an excellent sounding board for giving you a head start in your domain name search.",[24,23840,10536],{"id":10535},[11,23842,23843],{},"This is just the beginning. Below are some of the actions items that can further enhance the app outcome and usefulness:",[123,23845,23846,23849,23852],{},[74,23847,23848],{},"Integrating other LLMs and utilize multiple models simultaneously for arriving at a score and generating insights",[74,23850,23851],{},"Adding real world data to the mix. This will make the scoring more trustworthy and accurate.",[74,23853,23854],{},"Adding real time domain name availability checks, and possibly the social media handles.",[24,23856,10634],{"id":10633},[11,23858,23859],{},"Selecting the perfect domain name is a critical step in establishing your online presence. With its three distinct features Name Insights aims to make the process easier at various stages of the domain name search journey. It helps you to:",[71,23861,23862,23865,23868],{},[74,23863,23864],{},"Gain objective insights into the strengths and weaknesses of potential domain names",[74,23866,23867],{},"Make informed comparisons between different options",[74,23869,23870],{},"Generate fresh ideas tailored to your specific needs",[11,23872,23873],{},"Irrespective of the slight variability in results, the overall consistency and depth of analysis provided by Name Insights make it an invaluable resource for smarter domain name decisions.",[3048,23875],{},[11,23877,23878],{},"I hope you liked reading the article. Do try out the app and it would mean the world to me if you can share your feedback with me.",[11,23880,23881],{},"Until next time...!",[18,23883,23884],{},[11,23885,23886,183],{},[3061,23887,22710],{},[3065,23889,23890],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .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":23892},[23893,23894,23899,23900,23901,23902,23903,23904],{"id":22771,"depth":288,"text":22772},{"id":22784,"depth":288,"text":22785,"children":23895},[23896,23897,23898],{"id":22791,"depth":295,"text":22792},{"id":22832,"depth":295,"text":22833},{"id":22853,"depth":295,"text":22854},{"id":22874,"depth":288,"text":22875},{"id":22954,"depth":288,"text":22955},{"id":23814,"depth":288,"text":23815},{"id":23831,"depth":288,"text":23832},{"id":10535,"depth":288,"text":10536},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fleveraging-ai-for-smarter-domain-name-decisions\u002F894ac1a2-3eb8-423f-99fb-5afa1c592581-1b67034bd5.png","2024-07-28T19:32:27.124Z","If you work in tech-related fields, sooner or later there will come a time when you have to pick a domain name for the idea you've been working on. Whether it's an app or a serv...","clz5yiw6r000308md8h4u35i7",{},"\u002Fleveraging-ai-for-smarter-domain-name-decisions",{"title":22756,"description":23907},"leveraging-ai-for-smarter-domain-name-decisions",[10710,23914,23915,23916],"anthropic","claudeai","aifortomorrow","3rUFszXrj3ueBYr1-h0Gm-_GR6rOmoyTzo_e3UpVhY4",{"id":23919,"title":23920,"body":23921,"cover":24408,"date":24409,"description":24410,"draft":3086,"extension":3087,"hashnodeId":24411,"meta":24412,"navigation":291,"path":24413,"seo":24414,"slug":24415,"stem":24415,"tags":24416,"__hash__":24420},"posts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension.md","Creating a Quiz Generator using the ChatGPT Firebase Extension",{"type":8,"value":23922,"toc":24394},[23923,23926,23928,23931,23934,23938,23941,23944,23955,23959,23965,23969,23978,23984,23991,23997,24014,24020,24033,24039,24042,24084,24091,24095,24102,24108,24114,24117,24142,24146,24149,24152,24157,24160,24200,24203,24233,24236,24240,24243,24268,24271,24281,24285,24288,24292,24307,24313,24328,24334,24337,24343,24354,24357,24363,24366,24372,24375,24377,24380,24383,24386,24391],[11,23924,23925],{},"You often want to test your knowledge whenever you're learning a new topic. You can get ready-made quizzes on the subject, but generally, they tend to be too broad. What if you only want to test yourself on the content you've just read? This is the problem statement we will tackle in this article: given some content, how can you create a quiz based on it?",[24,23927,22772],{"id":22771},[11,23929,23930],{},"While helping my son with his studies, I often struggle with creating questions to ask him to test his knowledge. It has more to do with the idea of reading the content myself first, which I find tedious. I can ask the questions at the chapter's end, but that doesn't feel comprehensive enough. In these situations, I often think, \"If only someone could read the chapter for me, comprehend the content, and generate relevant questions along with the answer keys\".",[11,23932,23933],{},"With the advancements in AI and the recent interest in its generative capabilities (ChatGPT, Bard, etc.), I wondered if such a solution is possible using AI. To keep the experiment short, I looked for a low-code solution to achieve this, and Firebase extensions seemed like the right choice.",[24,23935,23937],{"id":23936},"what-is-generative-ai","What is generative AI?",[11,23939,23940],{},"As the name suggests, generative AI is a type of artificial intelligence that can create new content or data based on its learning from existing data. This new content or data can be similar to the existing data or be entirely original. It usually works by giving some content to the AI along with your instructions (generally called a prompt) on how to use that content, and the AI gives back an appropriate response.",[11,23942,23943],{},"Now that you know about generative AI, you should have a basic idea about the approach you'll need to take to get the desired results. Let's break it down into steps",[123,23945,23946,23949,23952],{},[74,23947,23948],{},"Get the text content from the user for which they want to generate questions",[74,23950,23951],{},"Pass the content along with a suitable instruction set to the AI",[74,23953,23954],{},"Parse the response from the AI and present it to the user in a suitable format",[24,23956,23958],{"id":23957},"implementation","Implementation",[11,23960,23961,23962,23964],{},"The implementation of the quiz generator is quite simple: use a ",[59,23963,7702],{}," to get the text content from the user and pass it to the configured ChatGPT Firebase extension. You can configure the said extension as shown below.",[32,23966,23968],{"id":23967},"configuring-the-chatgpt-extension","Configuring the ChatGPT extension",[11,23970,23971,23972,23977],{},"Go to the ",[47,23973,23976],{"href":23974,"rel":23975},"https:\u002F\u002Fextensions.dev\u002F",[51],"Firebase extensions hub",", search for chat in the search box, and click \"Chatbot with ChatGPT\" from the search results.",[11,23979,23980],{},[3135,23981],{"alt":23982,"src":23983},"Searching chat in the Firebase extensions hub","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F21516aa4-ae33-4701-be5d-19db6f15d8a5-3e83b1553d.png",[11,23985,23986,23987,23990],{},"You can install this extension from the extension page in one of your Firebase projects. Click on the ",[94,23988,23989],{},"\"Install in Firebase console\""," button, select your project from the new tab that opens up, and complete the steps for installing the extension into the project.",[11,23992,23993],{},[3135,23994],{"alt":23995,"src":23996},"installing the ChatGPT extension","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002Fb5af5ab3-06a2-4817-bb97-b1408bf74556-6493d4696e.png",[11,23998,23999,24000,24003,24004,24007,24008,24011,24012],{},"Click the ",[94,24001,24002],{},"Next"," button. This extension creates a cloud function called to generate a response, and we need to enable the ",[94,24005,24006],{},"Secrets Manager API"," to store our OpenAI API Key securely. Based on the current state of your project, you may need to enable other APIs (Cloud Functions, Artifact Registry, etc.). Click ",[94,24009,24010],{},"Enable"," and then click ",[94,24013,24002],{},[11,24015,24016],{},[3135,24017],{"alt":24018,"src":24019},"ChatGPT extension prerequisites","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002Fc10fe156-c127-4784-a024-353c06430e7a-f5d4c5312f.png",[11,24021,24022,24023,24026,24027,24030,24031],{},"Next, the extension needs to use ",[94,24024,24025],{},"Cloud Firestore"," (If you haven't created a Cloud Firestore yet, it will create one for you in datastore mode. For using it with Firebase, you'll need to change it back to the native mode from Google Cloud Console) and ",[94,24028,24029],{},"Secrets Manager"," to process the input text, the OpenAI API response, and the stored OpenAI API key. Click ",[94,24032,24002],{},[11,24034,24035],{},[3135,24036],{"alt":24037,"src":24038},"ChatGPT extension permissions","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F3763bc1c-7eca-4ae2-952f-f81cd3d0d6f4-4e99a6bb06.png",[11,24040,24041],{},"Finally, we need to configure the extension. You need to configure the following",[123,24043,24044,24058,24064,24078,24081],{},[74,24045,24046,24047,24051,24052,24055,24056],{},"Add your OpenAI API Key (you can create it ",[47,24048,3286],{"href":24049,"rel":24050},"https:\u002F\u002Fplatform.openai.com\u002Faccount\u002Fapi-keys",[51],"). Make sure to click on the ",[94,24053,24054],{},"Create Secret"," button to create a secret in the ",[94,24057,24029],{},[74,24059,24060,24061,17418],{},"Select the desired model (",[94,24062,24063],{},"GPT 3.5\u002F4",[74,24065,24066,24067,24070,24071,24074,24075,183],{},"Set the ",[94,24068,24069],{},"temperature"," to a low value (",[94,24072,24073],{},"0.2-0.4","). The temperature value (allowed range 0-2) dictates the randomness of the AI response; higher values make the output more random. I used a temperature of ",[94,24076,24077],{},"0.3",[74,24079,24080],{},"Set the cloud function location (ideally set it to the exact location where your Cloud Firestore is located)",[74,24082,24083],{},"You'll also need to configure the collection path and the field names where the input text and the API response will be stored. For this test, I used requests as the collection name and left the field names as it is.",[11,24085,24086,24087,24090],{},"We can leave the rest of the settings as it is and click on the ",[94,24088,24089],{},"Install extension"," button. Wait for the installation to complete; now we're ready to test it.",[32,24092,24094],{"id":24093},"testing-the-extension","Testing the extension",[11,24096,24097,24098,24101],{},"To test the extension you need to create the collection that you configured in the last section (",[94,24099,24100],{},"requests"," in my case) and add a prompt field to it.",[11,24103,24104],{},[3135,24105],{"alt":24106,"src":24107},"Creating the requests collection","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002Fead22a09-dfc9-481e-9e48-35db121f0d7a-319d7c24a0.png",[11,24109,24110],{},[3135,24111],{"alt":24112,"src":24113},"Adding a request to the requests collection","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F520e6731-6cdc-48b3-81e6-86c4dcac21e3-68edc95d08.png",[11,24115,24116],{},"The way this extension works is:",[123,24118,24119,24122,24129,24135],{},[74,24120,24121],{},"You add the input text and your instructions to the prompt field",[74,24123,24124,24125,24128],{},"The cloud function (",[59,24126,24127],{},"generateAIResponse",") created by the extension gets invoked automatically",[74,24130,24131,24132],{},"The function sends your prompt to the OpenAI API and creates a status field (of type map) with its state key set to ",[59,24133,24134],{},"PROCESSING",[74,24136,24137,24138,24141],{},"On a successful response from the API call, the state is changed to ",[59,24139,24140],{},"COMPLETED"," and the API call output is stored in a new field named response",[32,24143,24145],{"id":24144},"prompt-structure","Prompt Structure",[11,24147,24148],{},"How do you structure your prompt? A good prompt should start by setting a context so that ChatGPT behaves accordingly. Then, you state your requirement, followed by the input text, and finally, you give a response cue to the AI.",[11,24150,24151],{},"To summarize, you can use the below prompt structure",[11,24153,24154],{},[59,24155,24156],{},"Context + Instruction + Input Text + Response Cue",[11,24158,24159],{},"For the quiz generator, you can use the following initial prompt",[269,24161,24165],{"className":24162,"code":24163,"language":24164,"meta":274,"style":274},"language-markdown shiki shiki-themes github-light github-dark","“You're a teacher who needs to create quizzes to test your student's\nknowledge. Using the given text content create several distinct MCQs for the\npurpose.\n\nThe content: {{text_content}}\n\nYour response:”\n","markdown",[59,24166,24167,24172,24177,24182,24186,24191,24195],{"__ignoreMap":274},[278,24168,24169],{"class":280,"line":281},[278,24170,24171],{},"“You're a teacher who needs to create quizzes to test your student's\n",[278,24173,24174],{"class":280,"line":288},[278,24175,24176],{},"knowledge. Using the given text content create several distinct MCQs for the\n",[278,24178,24179],{"class":280,"line":295},[278,24180,24181],{},"purpose.\n",[278,24183,24184],{"class":280,"line":316},[278,24185,292],{"emptyLinePlaceholder":291},[278,24187,24188],{"class":280,"line":322},[278,24189,24190],{},"The content: {{text_content}}\n",[278,24192,24193],{"class":280,"line":327},[278,24194,292],{"emptyLinePlaceholder":291},[278,24196,24197],{"class":280,"line":340},[278,24198,24199],{},"Your response:”\n",[11,24201,24202],{},"The above prompt generates a response in a format similar to the one below. Please note that the format may vary slightly for you.",[269,24204,24206],{"className":24162,"code":24205,"language":24164,"meta":274,"style":274},"1. question text\n    a) Option 1\n    b) Option 2\n    ...\n...\n",[59,24207,24208,24213,24218,24223,24228],{"__ignoreMap":274},[278,24209,24210],{"class":280,"line":281},[278,24211,24212],{},"1. question text\n",[278,24214,24215],{"class":280,"line":288},[278,24216,24217],{},"    a) Option 1\n",[278,24219,24220],{"class":280,"line":295},[278,24221,24222],{},"    b) Option 2\n",[278,24224,24225],{"class":280,"line":316},[278,24226,24227],{},"    ...\n",[278,24229,24230],{"class":280,"line":322},[278,24231,24232],{},"...\n",[11,24234,24235],{},"This is a very good start, but the API is not returning the answers to these questions. Also, it is difficult to use it as a quiz as the response lacks structure.",[32,24237,24239],{"id":24238},"structuring-the-response","Structuring the response",[11,24241,24242],{},"For better control over the whole thing, you need a structured response, such as a JSON formatted output, from the ChatGPT API call. Tweak the instruction part of your prompt to the following:",[269,24244,24246],{"className":24162,"code":24245,"language":24164,"meta":274,"style":274},"Using the given text content creates several distinct MCQs for the purpose. \nReturn your response as an RFC8259-compliant JSON array using the structure\n[{\"question\": \"the question\", \"choices\": [\"choice 1\", \"choice 2\", ...],\n\"answer\": \"the correct choice\"}, {...}]\n",[59,24247,24248,24253,24258,24263],{"__ignoreMap":274},[278,24249,24250],{"class":280,"line":281},[278,24251,24252],{},"Using the given text content creates several distinct MCQs for the purpose. \n",[278,24254,24255],{"class":280,"line":288},[278,24256,24257],{},"Return your response as an RFC8259-compliant JSON array using the structure\n",[278,24259,24260],{"class":280,"line":295},[278,24261,24262],{},"[{\"question\": \"the question\", \"choices\": [\"choice 1\", \"choice 2\", ...],\n",[278,24264,24265],{"class":280,"line":316},[278,24266,24267],{},"\"answer\": \"the correct choice\"}, {...}]\n",[11,24269,24270],{},"Here I’ve intentionally removed any spaces from the JSON to save some tokens from the response. Rerunning the same test with the changed instruction gives us the desired structured result. Now, you can display the questions & the choices in any way you want using your favourite technology. As you have the answer key, you can also convert these questions into a quiz.",[3300,24272,24273,24275],{"dataNodeType":3302},[3300,24274,3785],{"dataNodeType":3305},[3300,24276,24277,24278],{"dataNodeType":3309},"Once in a while, you might get invalid API responses, like extra text in the response, and so on. ",[3061,24279,24280],{},"You can further play around with your instructions to eliminate or minimize such cases.",[24,24282,24284],{"id":24283},"using-images-as-content","Using images as content",[11,24286,24287],{},"You can extend it further and use images that contain text (say, a snapshot of a book's pages) as the starting point of the quiz generator. The underlying approach of the solution remains the same; you're just adding a step at the beginning. Using another Firebase extension, you can extract the text from an image and use that as input like before.",[32,24289,24291],{"id":24290},"configuring-the-cloud-vision-extension","Configuring the Cloud Vision extension",[11,24293,24294,24295,24298,24299,24302,24303,24306],{},"Go back to the ",[47,24296,23976],{"href":23974,"rel":24297},[51],", search for ",[94,24300,24301],{},"vision,"," and click on \"",[94,24304,24305],{},"Extract Image Text with Cloud Vision AI","\" from the search results. Install the extension by following the steps you completed for the previous extension.",[11,24308,24309],{},[3135,24310],{"alt":24311,"src":24312},"Installing the Cloud Vision extension","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F8126899d-0d7b-41fc-a327-7edf18dfe370-09f376bb2a.png",[11,24314,24315,24316,24319,24320,24323,24324,24327],{},"This extension enables Google's Cloud Vision API and creates a new cloud function named ",[59,24317,24318],{},"extractText",". Apart from the ",[94,24321,24322],{},"Cloud Firestore User"," permission, it also needs the ",[94,24325,24326],{},"Storage Admin"," permission (the extension reads the images from Cloud Storage).",[11,24329,24330],{},[3135,24331],{"alt":24332,"src":24333},"Cloud Vision extension permissions","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F94f08ce3-7c10-4ca8-ac27-2d329108d1a7-f42fc916d7.png",[11,24335,24336],{},"Finally, configure the extension as follows (you should keep your cloud function's location near your Cloud Storage\u002FFirestore's location). You can leave the rest of the settings as it is and install the extension",[11,24338,24339],{},[3135,24340],{"alt":24341,"src":24342},"Configuring the cloud vision extension","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F083ad8da-7066-461c-97a7-7e761719f3b6-67b6897678.png",[11,24344,24345,24346,24349,24350,24353],{},"Once the extension has finished installing, you can test it out by uploading an image to your cloud storage bucket under the ",[94,24347,24348],{},"snapshots"," folder. A few seconds after adding the image, you should see a new collection, ",[59,24351,24352],{},"extractedText"," with a single document containing the text that was extracted from the image.",[11,24355,24356],{},"For my test, I used the following screenshot",[11,24358,24359],{},[3135,24360],{"alt":24361,"src":24362},"test screenshot","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F8b06015f-7484-4d05-b06f-84819c33e4d5-a62c797bc8.png",[11,24364,24365],{},"And this is the result of the Cloud Vision extension",[11,24367,24368],{},[3135,24369],{"alt":24370,"src":24371},"result from Cloud Vision API","\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002Fcde08310-7d9c-4e6c-8afc-3d49859ee972-b030e8a444.png",[11,24373,24374],{},"You can use this text as input to the ChatGPT extension and generate questions as before.",[24,24376,10634],{"id":10633},[11,24378,24379],{},"Firebase extensions are powerful low-code options for many different use cases. As we saw in this article, leveraging the power of ChatGPT and Cloud Vision extensions could make the basic quiz generator functionality work without writing a single line of code.",[11,24381,24382],{},"Now, you can integrate Cloud Storage and Firestore into your front end to create a personalized and engaging quiz generator that generates relevant questions based on the given content.",[11,24384,24385],{},"I hope you liked reading the article. If you've questions or just want to say hi, just drop a message in the comments section.",[11,24387,24388],{},[3061,24389,24390],{},"-- Keep adding the bits, soon you'll have more bytes than you may need. :-)",[3065,24392,24393],{},"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":24395},[24396,24397,24398,24404,24407],{"id":22771,"depth":288,"text":22772},{"id":23936,"depth":288,"text":23937},{"id":23957,"depth":288,"text":23958,"children":24399},[24400,24401,24402,24403],{"id":23967,"depth":295,"text":23968},{"id":24093,"depth":295,"text":24094},{"id":24144,"depth":295,"text":24145},{"id":24238,"depth":295,"text":24239},{"id":24283,"depth":288,"text":24284,"children":24405},[24406],{"id":24290,"depth":295,"text":24291},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension\u002F0ec05d12ee0ef77ab9794fffc5e7674a-bf553ba957.jpeg","2023-11-28T15:00:10.628Z","You often want to test your knowledge whenever you're learning a new topic. You can get ready-made quizzes on the subject, but generally, they tend to be too broad. What if you...","clpigsqv8000c08jx6i4sh9kc",{},"\u002Fcreating-a-quiz-generator-using-the-chatgpt-firebase-extension",{"title":23920,"description":24410},"creating-a-quiz-generator-using-the-chatgpt-firebase-extension",[10709,24417,24418,24419],"firebase","chatgpt","firebaseextensions","oI7GaeNPFYlqH0ONRTtjkpJg2cPz7IskvlLfD8r72oM",{"id":24422,"title":24423,"body":24424,"cover":26191,"date":26192,"description":26193,"draft":3086,"extension":3087,"hashnodeId":26194,"meta":26195,"navigation":291,"path":26196,"seo":26197,"slug":26198,"stem":26198,"tags":26199,"__hash__":26202},"posts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt.md","To Outerbase with Bun, ToastUI Editor and ChatGPT",{"type":8,"value":24425,"toc":26175},[24426,24447,24449,24463,24466,24477,24480,24483,24487,24490,24494,24505,24519,24525,24532,24577,24580,24588,24595,24777,24780,24804,24808,24811,24822,24880,24883,24889,24893,24896,24899,24942,24953,24978,24981,24992,25025,25028,25043,25048,25054,25061,25065,25072,25075,25081,25084,25178,25181,25185,25188,25207,25221,25355,25362,25366,25369,25513,25516,25609,25612,25784,25787,25847,25851,25854,25857,25937,25940,26025,26028,26123,26126,26130,26141,26145,26148,26151,26154,26156,26158,26161,26164,26169,26172],[11,24427,24428,24429,24432,24433,24436,24437,919,24439,24442,24443,183],{},"This article is about exploring the new talk of the town, ",[59,24430,24431],{},"bun",", getting to know ",[59,24434,24435],{},"Outerbase",", and hanging out with old buddies ",[59,24438,24164],{},[59,24440,24441],{},"ChatGPT",". If you follow along, you'll get to know how the oven was heated, to bake fresh plugins for ",[47,24444,24435],{"href":24445,"rel":24446},"https:\u002F\u002Fouterbase.com\u002F",[51],[24,24448,22772],{"id":22771},[11,24450,24451,24452,24456,24457,24462],{},"When I learnt about the ",[47,24453,24435],{"href":24454,"rel":24455},"https:\u002F\u002Fbeta.outerbase.com\u002F",[51]," hackathon on ",[47,24458,24461],{"href":24459,"rel":24460},"https:\u002F\u002Fhashnode.com\u002F",[51],"Hashnode"," I was intrigued about it. I thought it was another database, \"base\" being the operative word. But I was only half right, or maybe half wrong, it depends on whom you're asking. Outerbase is an interface to your data (currently only residing in some relational databases), but it adds a lot of bells and whistles to make it interesting.",[11,24464,24465],{},"Some of the features include:",[123,24467,24468,24471,24474],{},[74,24469,24470],{},"Commands: These are functions in the cloud (Lambda functions?) that can talk to your database. You can create a chain of nodes to handle different steps of the process (AWS Step functions? Of course, it is not there yet but maybe soon...)",[74,24472,24473],{},"EZQL: It enables you to ask questions to your database in plain text. No more making your own SQL queries.",[74,24475,24476],{},"Plugins: Your data tables are much more than a spreadsheet, Outerbase plugins enable you to visualize that. You can add different plugins for different types of data, and interact with it in a way that feels native.",[11,24478,24479],{},"The last feature is what caught my fancy, and I decided to focus on that for this hackathon. Here is a short video demo of the plugins that I created.",[40,24481],{"url":24482},"https:\u002F\u002Fyoutu.be\u002FaErH1bOy73o",[24,24484,24486],{"id":24485},"preparing-the-base-with-bun","Preparing the base with Bun",[11,24488,24489],{},"Since I decided to focus on plugins, I wanted a way to quickly create the basic template on top of which I could build upon. One way was to just get the template from the Outerbase repo and copy-paste it to create more of it. But where is the fun in that? There is a saying, \"Automate it, silly.\" (Even if it takes you days to build that automation). So that is the path I took.",[32,24491,24493],{"id":24492},"creating-templates","Creating templates",[11,24495,24496,24497,24500,24501,24504],{},"You can create local templates (and maybe publish them later on) with ",[59,24498,24499],{},"Bun"," and then run a simple command to use that template. Your local templates should be present inside a ",[59,24502,24503],{},".bun-create"," folder in the following paths",[71,24506,24507,24513],{},[74,24508,24509,24512],{},[59,24510,24511],{},"$HOME\u002F.bun-create\u002F\u003Cname>",": global templates",[74,24514,24515,24518],{},[59,24516,24517],{},"\u003Cproject root>\u002F.bun-create\u002F\u003Cname>",": project-specific templates",[11,24520,24521,24524],{},[59,24522,24523],{},"\u003Cname>"," is the template\u002Ffolder name you want to use. Drop your template files into the template folder and run the following command to use that template",[3300,24526,24527,24529],{"dataNodeType":3302},[3300,24528,3785],{"dataNodeType":3305},[3300,24530,24531],{"dataNodeType":3309},"Using a local template will overwrite the destination folder, so make sure that it doesn't exist, or is empty.",[269,24533,24535],{"className":3335,"code":24534,"language":3337,"meta":274,"style":274},"# Notice the \".\u002F\" in the beginning. Without that it looks \n# for the template on Github (bug). \nbun create .\u002F\u003Ctemplate-name> \u003Cdestination>\n",[59,24536,24537,24542,24547],{"__ignoreMap":274},[278,24538,24539],{"class":280,"line":281},[278,24540,24541],{"class":284},"# Notice the \".\u002F\" in the beginning. Without that it looks \n",[278,24543,24544],{"class":280,"line":288},[278,24545,24546],{"class":284},"# for the template on Github (bug). \n",[278,24548,24549,24551,24554,24557,24559,24562,24564,24566,24569,24572,24575],{"class":280,"line":295},[278,24550,24431],{"class":333},[278,24552,24553],{"class":309}," create",[278,24555,24556],{"class":309}," .\u002F",[278,24558,1702],{"class":298},[278,24560,24561],{"class":309},"template-nam",[278,24563,6504],{"class":302},[278,24565,1074],{"class":298},[278,24567,24568],{"class":298}," \u003C",[278,24570,24571],{"class":309},"destinatio",[278,24573,24574],{"class":302},"n",[278,24576,372],{"class":298},[11,24578,24579],{},"There are two types of Outerbase plugins;",[71,24581,24582,24585],{},[74,24583,24584],{},"Table plugins: these work on the whole database table, and",[74,24586,24587],{},"Cell plugins: made for working with a cell but applied on a column so that they're available to all the cells of that column",[11,24589,24590,24591,24594],{},"An Outerbase plugin consists of at most three views; 1. The configuration view, 2. The data view, and 3. The data editor\u002Fdialog view. These views are created using ",[59,24592,24593],{},"Web Components"," (Custom HTML Elements). A basic Outerbase component can be represented as follows",[269,24596,24600],{"className":24597,"code":24598,"language":24599,"meta":274,"style":274},"language-javascript shiki shiki-themes github-light github-dark","const templateEditor = document.createElement(\"template\");\ntemplateEditor.innerHTML = `\n\u003Cstyle>\n  #container {\n    max-width: 320px;\n  }\n\u003C\u002Fstyle>\n\n\u003Cdiv id=\"container\">\u003C\u002Fdiv>\n`;\n\nclass OuterbasePluginCellEditor extends HTMLElement {\n  static get observedAttributes() {\n    return [...observed_attributes];\n  }\n\n  config = new OuterbasePluginConfig({});\n\n  constructor() {\n    super();\n\n    this.shadow = this.attachShadow({ mode: \"open\" });\n    this.shadow.appendChild(\n      templateEditor.content.cloneNode(true)\n    );\n  }\n\n  connectedCallback() {\n    this.config = new OuterbasePluginConfig(\n      decodeAttributeByName(this, \"configuration\")\n    );\n\n    this.config.cellValue = this.getAttribute(\"cellvalue\");\n    this.render();\n  }\n\n  render() {}\n}\n","javascript",[59,24601,24602,24607,24612,24617,24622,24627,24631,24636,24640,24645,24650,24654,24659,24664,24669,24673,24677,24682,24686,24691,24696,24700,24705,24710,24715,24719,24723,24727,24732,24737,24742,24746,24750,24755,24760,24764,24768,24773],{"__ignoreMap":274},[278,24603,24604],{"class":280,"line":281},[278,24605,24606],{},"const templateEditor = document.createElement(\"template\");\n",[278,24608,24609],{"class":280,"line":288},[278,24610,24611],{},"templateEditor.innerHTML = `\n",[278,24613,24614],{"class":280,"line":295},[278,24615,24616],{},"\u003Cstyle>\n",[278,24618,24619],{"class":280,"line":316},[278,24620,24621],{},"  #container {\n",[278,24623,24624],{"class":280,"line":322},[278,24625,24626],{},"    max-width: 320px;\n",[278,24628,24629],{"class":280,"line":327},[278,24630,1096],{},[278,24632,24633],{"class":280,"line":340},[278,24634,24635],{},"\u003C\u002Fstyle>\n",[278,24637,24638],{"class":280,"line":349},[278,24639,292],{"emptyLinePlaceholder":291},[278,24641,24642],{"class":280,"line":375},[278,24643,24644],{},"\u003Cdiv id=\"container\">\u003C\u002Fdiv>\n",[278,24646,24647],{"class":280,"line":386},[278,24648,24649],{},"`;\n",[278,24651,24652],{"class":280,"line":397},[278,24653,292],{"emptyLinePlaceholder":291},[278,24655,24656],{"class":280,"line":408},[278,24657,24658],{},"class OuterbasePluginCellEditor extends HTMLElement {\n",[278,24660,24661],{"class":280,"line":433},[278,24662,24663],{},"  static get observedAttributes() {\n",[278,24665,24666],{"class":280,"line":454},[278,24667,24668],{},"    return [...observed_attributes];\n",[278,24670,24671],{"class":280,"line":475},[278,24672,1096],{},[278,24674,24675],{"class":280,"line":496},[278,24676,292],{"emptyLinePlaceholder":291},[278,24678,24679],{"class":280,"line":505},[278,24680,24681],{},"  config = new OuterbasePluginConfig({});\n",[278,24683,24684],{"class":280,"line":516},[278,24685,292],{"emptyLinePlaceholder":291},[278,24687,24688],{"class":280,"line":527},[278,24689,24690],{},"  constructor() {\n",[278,24692,24693],{"class":280,"line":533},[278,24694,24695],{},"    super();\n",[278,24697,24698],{"class":280,"line":539},[278,24699,292],{"emptyLinePlaceholder":291},[278,24701,24702],{"class":280,"line":545},[278,24703,24704],{},"    this.shadow = this.attachShadow({ mode: \"open\" });\n",[278,24706,24707],{"class":280,"line":551},[278,24708,24709],{},"    this.shadow.appendChild(\n",[278,24711,24712],{"class":280,"line":557},[278,24713,24714],{},"      templateEditor.content.cloneNode(true)\n",[278,24716,24717],{"class":280,"line":567},[278,24718,1898],{},[278,24720,24721],{"class":280,"line":577},[278,24722,1096],{},[278,24724,24725],{"class":280,"line":587},[278,24726,292],{"emptyLinePlaceholder":291},[278,24728,24729],{"class":280,"line":597},[278,24730,24731],{},"  connectedCallback() {\n",[278,24733,24734],{"class":280,"line":608},[278,24735,24736],{},"    this.config = new OuterbasePluginConfig(\n",[278,24738,24739],{"class":280,"line":614},[278,24740,24741],{},"      decodeAttributeByName(this, \"configuration\")\n",[278,24743,24744],{"class":280,"line":620},[278,24745,1898],{},[278,24747,24748],{"class":280,"line":625},[278,24749,292],{"emptyLinePlaceholder":291},[278,24751,24752],{"class":280,"line":640},[278,24753,24754],{},"    this.config.cellValue = this.getAttribute(\"cellvalue\");\n",[278,24756,24757],{"class":280,"line":663},[278,24758,24759],{},"    this.render();\n",[278,24761,24762],{"class":280,"line":669},[278,24763,1096],{},[278,24765,24766],{"class":280,"line":680},[278,24767,292],{"emptyLinePlaceholder":291},[278,24769,24770],{"class":280,"line":686},[278,24771,24772],{},"  render() {}\n",[278,24774,24775],{"class":280,"line":1334},[278,24776,617],{},[11,24778,24779],{},"Before we can use this component we need to register this component with the window",[269,24781,24783],{"className":24597,"code":24782,"language":24599,"meta":274,"style":274},"window.customElements.define(\n  \"outerbase-plugin-cell-editor\",\n  OuterbasePluginCellEditor\n);\n",[59,24784,24785,24790,24795,24800],{"__ignoreMap":274},[278,24786,24787],{"class":280,"line":281},[278,24788,24789],{},"window.customElements.define(\n",[278,24791,24792],{"class":280,"line":288},[278,24793,24794],{},"  \"outerbase-plugin-cell-editor\",\n",[278,24796,24797],{"class":280,"line":295},[278,24798,24799],{},"  OuterbasePluginCellEditor\n",[278,24801,24802],{"class":280,"line":316},[278,24803,1280],{},[24,24805,24807],{"id":24806},"the-first-plugin-audio-player","The first plugin: Audio Player",[11,24809,24810],{},"If your data consists of audio urls, wouldn't it be cool to play the audio from the database view itself? This Outerbase cell plugin allows you to do exactly that.",[11,24812,24813,24814,24817,24818,24821],{},"All the magic of this plugin resides in its ",[59,24815,24816],{},"Editor"," view. We simply attach an HTML audio element to the view. Set the audio src and load it. And our audio link is ready to play. Below is the updated ",[59,24819,24820],{},"render"," method shown earlier.",[269,24823,24825],{"className":24597,"code":24824,"language":24599,"meta":274,"style":274},"render() {\n  const srcUrl = this.getAttribute(\"cellvalue\");\n  if (srcUrl) {\n    this.shadow.getElementById(\n      \"container\"\n    ).innerHTML = `\u003Caudio id=\"audio-player\" controls \u002F>`;\n    const player = this.shadow.getElementById(\"audio-player\");\n    player.src = srcUrl;\n    player.load();\n  }\n}\n",[59,24826,24827,24832,24837,24842,24847,24852,24857,24862,24867,24872,24876],{"__ignoreMap":274},[278,24828,24829],{"class":280,"line":281},[278,24830,24831],{},"render() {\n",[278,24833,24834],{"class":280,"line":288},[278,24835,24836],{},"  const srcUrl = this.getAttribute(\"cellvalue\");\n",[278,24838,24839],{"class":280,"line":295},[278,24840,24841],{},"  if (srcUrl) {\n",[278,24843,24844],{"class":280,"line":316},[278,24845,24846],{},"    this.shadow.getElementById(\n",[278,24848,24849],{"class":280,"line":322},[278,24850,24851],{},"      \"container\"\n",[278,24853,24854],{"class":280,"line":327},[278,24855,24856],{},"    ).innerHTML = `\u003Caudio id=\"audio-player\" controls \u002F>`;\n",[278,24858,24859],{"class":280,"line":340},[278,24860,24861],{},"    const player = this.shadow.getElementById(\"audio-player\");\n",[278,24863,24864],{"class":280,"line":349},[278,24865,24866],{},"    player.src = srcUrl;\n",[278,24868,24869],{"class":280,"line":375},[278,24870,24871],{},"    player.load();\n",[278,24873,24874],{"class":280,"line":386},[278,24875,1096],{},[278,24877,24878],{"class":280,"line":397},[278,24879,617],{},[11,24881,24882],{},"The above code allows us to get this view (the audio player dialog)",[11,24884,24885],{},[3135,24886],{"alt":24887,"src":24888},"audio player plugin view","\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002Ff7eebf16-0ea5-4ecd-8f78-9f4cea51ff09-6dbb1da716.png",[24,24890,24892],{"id":24891},"the-second-plugin-video-player","The second plugin: Video Player",[11,24894,24895],{},"Since we have an audio player for our database now, it is only logical to do the same thing for videos. But this is not that simple. Most of the video links you encounter are YouTube (and maybe some Vimeo) video links, so I set out to make that work.",[11,24897,24898],{},"Now, the URLs that we see in the browser's address bar (watch URLs) for YouTube videos are different from when you want to embed the YouTube player in your website. Assuming the database will store the watch URLs, we need to create the embed URLs. The below function does exactly that using a regex.",[269,24900,24902],{"className":24597,"code":24901,"language":24599,"meta":274,"style":274},"getYouTubeEmbedUrl(url) {\n  const youtubeRegex =\n    \u002F^.*(youtu.be\\\u002F|v\\\u002F|u\\\u002F\\w\\\u002F|embed\\\u002F|watch\\?v=|\\&v=)([^#\\&\\?]*).*\u002F;\n  const match = url.match(youtubeRegex);\n  if (match && match[2].length == 11) {\n    return `https:\u002F\u002Fyoutube.com\u002Fembed\u002F${match[2]}`;\n  }\n}\n",[59,24903,24904,24909,24914,24919,24924,24929,24934,24938],{"__ignoreMap":274},[278,24905,24906],{"class":280,"line":281},[278,24907,24908],{},"getYouTubeEmbedUrl(url) {\n",[278,24910,24911],{"class":280,"line":288},[278,24912,24913],{},"  const youtubeRegex =\n",[278,24915,24916],{"class":280,"line":295},[278,24917,24918],{},"    \u002F^.*(youtu.be\\\u002F|v\\\u002F|u\\\u002F\\w\\\u002F|embed\\\u002F|watch\\?v=|\\&v=)([^#\\&\\?]*).*\u002F;\n",[278,24920,24921],{"class":280,"line":316},[278,24922,24923],{},"  const match = url.match(youtubeRegex);\n",[278,24925,24926],{"class":280,"line":322},[278,24927,24928],{},"  if (match && match[2].length == 11) {\n",[278,24930,24931],{"class":280,"line":327},[278,24932,24933],{},"    return `https:\u002F\u002Fyoutube.com\u002Fembed\u002F${match[2]}`;\n",[278,24935,24936],{"class":280,"line":340},[278,24937,1096],{},[278,24939,24940],{"class":280,"line":349},[278,24941,617],{},[11,24943,24944,24945,24948,24949,24952],{},"Now we can create an ",[59,24946,24947],{},"iframe",", set its ",[59,24950,24951],{},"src"," as this embed URL and our player should be ready. We do all this in the editor\u002Fdialog view of the plugin.",[269,24954,24956],{"className":24597,"code":24955,"language":24599,"meta":274,"style":274},"this.shadow.getElementById(\"container\").innerHTML = \n    `\u003Ciframe id=\"video-player\" type=\"text\u002Fhtml\" width=\"360\" height=\"240\" frameborder=\"0\" \u002F>`;\nconst player = this.shadow.getElementById(\"video-player\");\nplayer.src = embedUrl;\n",[59,24957,24958,24963,24968,24973],{"__ignoreMap":274},[278,24959,24960],{"class":280,"line":281},[278,24961,24962],{},"this.shadow.getElementById(\"container\").innerHTML = \n",[278,24964,24965],{"class":280,"line":288},[278,24966,24967],{},"    `\u003Ciframe id=\"video-player\" type=\"text\u002Fhtml\" width=\"360\" height=\"240\" frameborder=\"0\" \u002F>`;\n",[278,24969,24970],{"class":280,"line":295},[278,24971,24972],{},"const player = this.shadow.getElementById(\"video-player\");\n",[278,24974,24975],{"class":280,"line":316},[278,24976,24977],{},"player.src = embedUrl;\n",[11,24979,24980],{},"The above code works fine locally but when deployed to the Outerbase console it fails to load the player. The reason is Outerbase plugins run inside a sandboxed iframe to keep the data secure. The YouTube player needs to use the browser cache to store its assets but the configured sandbox permissions don't allow it, and the player fails to appear.",[11,24982,24983,24984,24991],{},"But the same technique can be utilized for other video hosting platforms, Vimeo being one of them. The problem with Vimeo is that it doesn't have well structured consistent URLs. So we use its ",[47,24985,24988],{"href":24986,"rel":24987},"https:\u002F\u002Fdeveloper.vimeo.com\u002Fapi\u002Foembed\u002Fvideos",[51],[59,24989,24990],{},"oEmbed APIs"," to fetch the correct URL.",[269,24993,24995],{"className":24597,"code":24994,"language":24599,"meta":274,"style":274},"async getVimeoEmbedUrl(url) {\n  const res = await fetch(`https:\u002F\u002Fvimeo.com\u002Fapi\u002Foembed.json?url=${url}`);\n\n  const data = await res.json();\n  return `https:\u002F\u002Fplayer.vimeo.com\u002Fvideo\u002F${data.video_id}?title=0&byline=0&dnt=1`;\n}\n",[59,24996,24997,25002,25007,25011,25016,25021],{"__ignoreMap":274},[278,24998,24999],{"class":280,"line":281},[278,25000,25001],{},"async getVimeoEmbedUrl(url) {\n",[278,25003,25004],{"class":280,"line":288},[278,25005,25006],{},"  const res = await fetch(`https:\u002F\u002Fvimeo.com\u002Fapi\u002Foembed.json?url=${url}`);\n",[278,25008,25009],{"class":280,"line":295},[278,25010,292],{"emptyLinePlaceholder":291},[278,25012,25013],{"class":280,"line":316},[278,25014,25015],{},"  const data = await res.json();\n",[278,25017,25018],{"class":280,"line":322},[278,25019,25020],{},"  return `https:\u002F\u002Fplayer.vimeo.com\u002Fvideo\u002F${data.video_id}?title=0&byline=0&dnt=1`;\n",[278,25022,25023],{"class":280,"line":327},[278,25024,617],{},[11,25026,25027],{},"And now we can use the same iframe player to play this video.",[3300,25029,25030,25032],{"dataNodeType":3302},[3300,25031,3785],{"dataNodeType":3305},[3300,25033,25034,25035,25038,25039,25042],{"dataNodeType":3309},"Notice the query parameter ",[59,25036,25037],{},"&dnt=1"," in the created URL. Without that Vimeo player will also follow the YouTube player's way. ",[59,25040,25041],{},"dnt"," is \"do not track\". If it is 0 (the default value), the Vimeo player will try to use browser cookies and will fail as we're sandboxed.",[11,25044,25045,25047],{},[59,25046,25041],{}," parameter saved the day for this plugin, else all that work would have come to nought. Here is the view we get from the player",[11,25049,25050],{},[3135,25051],{"alt":25052,"src":25053},"vimeo player view","\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002F24d8e698-fc93-4d7a-8ea6-a084ac47e6a6-cf465f47b3.png",[11,25055,25056,25057,25060],{},"There is still one error present in the browser console for the Vimeo player, and that is for missing the ",[59,25058,25059],{},"presentation"," permission. This can be alleviated by sandbox=\"allow-presentation\" on the parent iframe, or maybe there is some other Vimeo URL query param that can help with that. I haven't explored further as even with the error in the console, the video can be played (The first player load doesn't work, but no problem afterwards).",[24,25062,25064],{"id":25063},"the-third-plugin-markdown-editor","The third plugin: Markdown Editor",[11,25066,25067,25068,25071],{},"How cool it would be to create blog posts or write docs from the database view itself? This ",[59,25069,25070],{},"md-editor"," plugin is exactly what you need for all such cases.",[11,25073,25074],{},"Let's first take a peek at the plugins editor view",[11,25076,25077],{},[3135,25078],{"alt":25079,"src":25080},"md-editor view","\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002F555b7179-bf05-404f-875a-ee6666f8ab31-0472254c41.png",[11,25082,25083],{},"This plugin is different from the other plugins we've seen so far, as here we need to use third-party scripts and CSS to achieve the goal. How do we go about this, and which markdown editor to integrate? These are important questions to answer. Let's tackle these questions one by one",[123,25085,25086,25175],{},[74,25087,25088,25089],{},"How to include third-party scripts and CSS: Even though we're using web components, these are still part of the DOM, so maybe we can create script and link tags dynamically to load these. An example would be as follows",[269,25090,25092],{"className":24597,"code":25091,"language":24599,"meta":274,"style":274},"\u002F\u002F JavaScript\nfunction loadCSS(url) {\n  const link = document.createElement('link');\n  link.rel = 'stylesheet';\n  link.href = url;\n  document.head.appendChild(link);\n}\n\nfunction loadJS(url) {\n  const script = document.createElement('script');\n  script.src = url;\n  document.body.appendChild(script);\n}\n\n\u002F\u002F Example usage:\nloadCSS('https:\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fbootstrap\u002F4.6.0\u002Fcss\u002Fbootstrap.min.css');\nloadJS('https:\u002F\u002Fcode.jquery.com\u002Fjquery-3.6.0.min.js');\n",[59,25093,25094,25099,25104,25109,25114,25119,25124,25128,25132,25137,25142,25147,25152,25156,25160,25165,25170],{"__ignoreMap":274},[278,25095,25096],{"class":280,"line":281},[278,25097,25098],{},"\u002F\u002F JavaScript\n",[278,25100,25101],{"class":280,"line":288},[278,25102,25103],{},"function loadCSS(url) {\n",[278,25105,25106],{"class":280,"line":295},[278,25107,25108],{},"  const link = document.createElement('link');\n",[278,25110,25111],{"class":280,"line":316},[278,25112,25113],{},"  link.rel = 'stylesheet';\n",[278,25115,25116],{"class":280,"line":322},[278,25117,25118],{},"  link.href = url;\n",[278,25120,25121],{"class":280,"line":327},[278,25122,25123],{},"  document.head.appendChild(link);\n",[278,25125,25126],{"class":280,"line":340},[278,25127,617],{},[278,25129,25130],{"class":280,"line":349},[278,25131,292],{"emptyLinePlaceholder":291},[278,25133,25134],{"class":280,"line":375},[278,25135,25136],{},"function loadJS(url) {\n",[278,25138,25139],{"class":280,"line":386},[278,25140,25141],{},"  const script = document.createElement('script');\n",[278,25143,25144],{"class":280,"line":397},[278,25145,25146],{},"  script.src = url;\n",[278,25148,25149],{"class":280,"line":408},[278,25150,25151],{},"  document.body.appendChild(script);\n",[278,25153,25154],{"class":280,"line":433},[278,25155,617],{},[278,25157,25158],{"class":280,"line":454},[278,25159,292],{"emptyLinePlaceholder":291},[278,25161,25162],{"class":280,"line":475},[278,25163,25164],{},"\u002F\u002F Example usage:\n",[278,25166,25167],{"class":280,"line":496},[278,25168,25169],{},"loadCSS('https:\u002F\u002Fcdnjs.cloudflare.com\u002Fajax\u002Flibs\u002Fbootstrap\u002F4.6.0\u002Fcss\u002Fbootstrap.min.css');\n",[278,25171,25172],{"class":280,"line":505},[278,25173,25174],{},"loadJS('https:\u002F\u002Fcode.jquery.com\u002Fjquery-3.6.0.min.js');\n",[74,25176,25177],{},"Which editor to integrate: There are many markdown editors available, but we need something that can work with Web Components (in-browser). I also wanted to avoid any kind of bundling of the scripts with the plugin. This is because the Outerbase plugins can have only so much size, and markdown\u002FWYSIWYG editors are bulky. So we need one which is available from a CDN. I looked at and tried QuillJs (with QuillJs Markdown module), SimpleMDE and ToastUI Editor, and the latter one seemed relatively maintained and worked on the first try so that is the one we integrate.",[11,25179,25180],{},"Using the functions listed just above, if you try to load the scripts and CSS in an Outerbase plugin you'll get the gotcha moment of this plugin; you're not allowed to load third-party CSS because of the CSP (content security policy). What do we do now? Without the stylesheets, it is pointless to integrate the editor. Maybe we bundle the CSS with the plugin (bundling the script is still a NO, due to its size)?",[32,25182,25184],{"id":25183},"bun-as-a-package-manager-bundler","Bun as a package manager & bundler",[11,25186,25187],{},"Now we go back to reading about bun. After reading the docs it became clear that Bun doesn't support bundling the CSS at present. It simply copies the CSS files to the outdir and renames their references. There are two ways out of this:",[123,25189,25190,25204],{},[74,25191,25192,25193],{},"Create a bun plugin to handle the CSS file bundling.",[123,25194,25195,25198,25201],{},[74,25196,25197],{},"Read the CSS files from node_modules",[74,25199,25200],{},"Minify using some third-party CSS minifier (no native CSS loader) and,",[74,25202,25203],{},"Inject as text into the final bundle",[74,25205,25206],{},"Simply get the minified CSS files from the CDN, and inject them as text into the plugin code (no bundling needed as the plugin code is very small)",[11,25208,25209,25210,25213,25214,25216,25217,25220],{},"So we pick the easy way out here and pick the second option. Run the ",[59,25211,25212],{},"bun init"," command to quickly create a ",[59,25215,5686],{}," file (so that we can use bun APIs). Create a new file ",[59,25218,25219],{},"build.js"," for handling the tooling. This is the function which does what we need. Replacements is just an object where the keys are the identifiers we want to replace with the actual CSS styles.",[269,25222,25224],{"className":24597,"code":25223,"language":24599,"meta":274,"style":274},"const bundle = async (replacements) => {\n  if (replacements) {\n    const indexFile = Bun.file(\"index.js\");\n    let indexFileText = await indexFile.text();\n\n    for (const key in replacements) {\n      \u002F\u002F Fetch the CSS file from the CDN using the URL\n      const res = await fetch(replacements[key]);\n      const fText = await res.text();\n\n      indexFileText = indexFileText.replace(key, fText);\n    }\n\n    createOutput(\"out\", indexFileText);\n  } else {\n    fs.cpSync(\"index.js\", \"out\u002Findex.js\");\n  }\n};\n\nconst createOutput = (dir, fileText) => {\n  const filePath = `${dir}\u002Findex.js`;\n  const directoryPath = path.dirname(filePath);\n  if (!fs.existsSync(directoryPath)) {\n    fs.mkdirSync(directoryPath, { recursive: true });\n  }\n\n  fs.writeFileSync(filePath, fileText);\n};\n",[59,25225,25226,25231,25236,25241,25246,25250,25255,25260,25265,25270,25274,25279,25283,25287,25292,25296,25301,25305,25309,25313,25318,25323,25328,25333,25338,25342,25346,25351],{"__ignoreMap":274},[278,25227,25228],{"class":280,"line":281},[278,25229,25230],{},"const bundle = async (replacements) => {\n",[278,25232,25233],{"class":280,"line":288},[278,25234,25235],{},"  if (replacements) {\n",[278,25237,25238],{"class":280,"line":295},[278,25239,25240],{},"    const indexFile = Bun.file(\"index.js\");\n",[278,25242,25243],{"class":280,"line":316},[278,25244,25245],{},"    let indexFileText = await indexFile.text();\n",[278,25247,25248],{"class":280,"line":322},[278,25249,292],{"emptyLinePlaceholder":291},[278,25251,25252],{"class":280,"line":327},[278,25253,25254],{},"    for (const key in replacements) {\n",[278,25256,25257],{"class":280,"line":340},[278,25258,25259],{},"      \u002F\u002F Fetch the CSS file from the CDN using the URL\n",[278,25261,25262],{"class":280,"line":349},[278,25263,25264],{},"      const res = await fetch(replacements[key]);\n",[278,25266,25267],{"class":280,"line":375},[278,25268,25269],{},"      const fText = await res.text();\n",[278,25271,25272],{"class":280,"line":386},[278,25273,292],{"emptyLinePlaceholder":291},[278,25275,25276],{"class":280,"line":397},[278,25277,25278],{},"      indexFileText = indexFileText.replace(key, fText);\n",[278,25280,25281],{"class":280,"line":408},[278,25282,1285],{},[278,25284,25285],{"class":280,"line":433},[278,25286,292],{"emptyLinePlaceholder":291},[278,25288,25289],{"class":280,"line":454},[278,25290,25291],{},"    createOutput(\"out\", indexFileText);\n",[278,25293,25294],{"class":280,"line":475},[278,25295,8120],{},[278,25297,25298],{"class":280,"line":496},[278,25299,25300],{},"    fs.cpSync(\"index.js\", \"out\u002Findex.js\");\n",[278,25302,25303],{"class":280,"line":505},[278,25304,1096],{},[278,25306,25307],{"class":280,"line":516},[278,25308,2817],{},[278,25310,25311],{"class":280,"line":527},[278,25312,292],{"emptyLinePlaceholder":291},[278,25314,25315],{"class":280,"line":533},[278,25316,25317],{},"const createOutput = (dir, fileText) => {\n",[278,25319,25320],{"class":280,"line":539},[278,25321,25322],{},"  const filePath = `${dir}\u002Findex.js`;\n",[278,25324,25325],{"class":280,"line":545},[278,25326,25327],{},"  const directoryPath = path.dirname(filePath);\n",[278,25329,25330],{"class":280,"line":551},[278,25331,25332],{},"  if (!fs.existsSync(directoryPath)) {\n",[278,25334,25335],{"class":280,"line":557},[278,25336,25337],{},"    fs.mkdirSync(directoryPath, { recursive: true });\n",[278,25339,25340],{"class":280,"line":567},[278,25341,1096],{},[278,25343,25344],{"class":280,"line":577},[278,25345,292],{"emptyLinePlaceholder":291},[278,25347,25348],{"class":280,"line":587},[278,25349,25350],{},"  fs.writeFileSync(filePath, fileText);\n",[278,25352,25353],{"class":280,"line":597},[278,25354,2817],{},[11,25356,25357,25358,25361],{},"Since we're in the CLI realm now, added a little complexity to get the file names as CLI input (using ",[59,25359,25360],{},"commander","). You can check the Github repo for the code.",[32,25363,25365],{"id":25364},"coding-the-plugin","Coding the plugin",[11,25367,25368],{},"First of all, we load the script (remember the CSS is already injected into the plugin, in the style tag of the cell editor's shadow dom, to be precise).",[269,25370,25372],{"className":24597,"code":25371,"language":24599,"meta":274,"style":274},"loadToastUiEditor() {\n  const scriptSrc =\n    \"https:\u002F\u002Fuicdn.toast.com\u002Feditor\u002Flatest\u002Ftoastui-editor-all.min.js\";\n  \u002F\u002F Optimization to not load the script again \n  \u002F\u002F and again, as the editor is recreated every time\n  if (document.scripts) {\n    for (const script of document.scripts) {\n      if (script.src === scriptSrc) {\n        console.log(\"script already loaded, bail out\");\n        return;\n      }\n    }\n  }\n\n  const el = document.createElement(\"script\");\n  el.src = scriptSrc;\n\n  el.onload = () => {\n    this.render();\n  };\n\n  el.onerror = (event) => {\n    console.log(\"failed to load the script\", event);\n  };\n\n  \u002F\u002F We're adding the script to the document and not \n  \u002F\u002F the shadow dom, because the shadom dom is recreated\n  \u002F\u002F whenver the editor is opened\n  document.head.appendChild(el);\n}\n",[59,25373,25374,25379,25384,25389,25394,25399,25404,25409,25414,25419,25424,25428,25432,25436,25440,25445,25450,25454,25459,25463,25467,25471,25476,25481,25485,25489,25494,25499,25504,25509],{"__ignoreMap":274},[278,25375,25376],{"class":280,"line":281},[278,25377,25378],{},"loadToastUiEditor() {\n",[278,25380,25381],{"class":280,"line":288},[278,25382,25383],{},"  const scriptSrc =\n",[278,25385,25386],{"class":280,"line":295},[278,25387,25388],{},"    \"https:\u002F\u002Fuicdn.toast.com\u002Feditor\u002Flatest\u002Ftoastui-editor-all.min.js\";\n",[278,25390,25391],{"class":280,"line":316},[278,25392,25393],{},"  \u002F\u002F Optimization to not load the script again \n",[278,25395,25396],{"class":280,"line":322},[278,25397,25398],{},"  \u002F\u002F and again, as the editor is recreated every time\n",[278,25400,25401],{"class":280,"line":327},[278,25402,25403],{},"  if (document.scripts) {\n",[278,25405,25406],{"class":280,"line":340},[278,25407,25408],{},"    for (const script of document.scripts) {\n",[278,25410,25411],{"class":280,"line":349},[278,25412,25413],{},"      if (script.src === scriptSrc) {\n",[278,25415,25416],{"class":280,"line":375},[278,25417,25418],{},"        console.log(\"script already loaded, bail out\");\n",[278,25420,25421],{"class":280,"line":386},[278,25422,25423],{},"        return;\n",[278,25425,25426],{"class":280,"line":397},[278,25427,6234],{},[278,25429,25430],{"class":280,"line":408},[278,25431,1285],{},[278,25433,25434],{"class":280,"line":433},[278,25435,1096],{},[278,25437,25438],{"class":280,"line":454},[278,25439,292],{"emptyLinePlaceholder":291},[278,25441,25442],{"class":280,"line":475},[278,25443,25444],{},"  const el = document.createElement(\"script\");\n",[278,25446,25447],{"class":280,"line":496},[278,25448,25449],{},"  el.src = scriptSrc;\n",[278,25451,25452],{"class":280,"line":505},[278,25453,292],{"emptyLinePlaceholder":291},[278,25455,25456],{"class":280,"line":516},[278,25457,25458],{},"  el.onload = () => {\n",[278,25460,25461],{"class":280,"line":527},[278,25462,24759],{},[278,25464,25465],{"class":280,"line":533},[278,25466,901],{},[278,25468,25469],{"class":280,"line":539},[278,25470,292],{"emptyLinePlaceholder":291},[278,25472,25473],{"class":280,"line":545},[278,25474,25475],{},"  el.onerror = (event) => {\n",[278,25477,25478],{"class":280,"line":551},[278,25479,25480],{},"    console.log(\"failed to load the script\", event);\n",[278,25482,25483],{"class":280,"line":557},[278,25484,901],{},[278,25486,25487],{"class":280,"line":567},[278,25488,292],{"emptyLinePlaceholder":291},[278,25490,25491],{"class":280,"line":577},[278,25492,25493],{},"  \u002F\u002F We're adding the script to the document and not \n",[278,25495,25496],{"class":280,"line":587},[278,25497,25498],{},"  \u002F\u002F the shadow dom, because the shadom dom is recreated\n",[278,25500,25501],{"class":280,"line":597},[278,25502,25503],{},"  \u002F\u002F whenver the editor is opened\n",[278,25505,25506],{"class":280,"line":608},[278,25507,25508],{},"  document.head.appendChild(el);\n",[278,25510,25511],{"class":280,"line":614},[278,25512,617],{},[11,25514,25515],{},"As soon as the script is loaded we are ready to show our markdown editor",[269,25517,25519],{"className":24597,"code":25518,"language":24599,"meta":274,"style":274},"render() {\n  try {\n    const Editor = toastui.Editor;\n    this.editor = new Editor({\n      el: this.shadow.querySelector(\"#editor\"),\n      height: \"420px\",\n      initialEditType: \"markdown\",\n      initialValue: this.getAttribute(\"cellvalue\"),\n      previewStyle: \"vertical\",\n      usageStatistics: false,\n      theme: this.config.theme,\n      events: { keydown: this.handleKeyDown },\n    });\n\n    this.setEditorPosition();\n  } catch (error) {\n    console.log(\"render error\", error);\n  }\n}\n",[59,25520,25521,25525,25529,25534,25539,25544,25549,25554,25559,25564,25569,25574,25579,25583,25587,25592,25596,25601,25605],{"__ignoreMap":274},[278,25522,25523],{"class":280,"line":281},[278,25524,24831],{},[278,25526,25527],{"class":280,"line":288},[278,25528,7562],{},[278,25530,25531],{"class":280,"line":295},[278,25532,25533],{},"    const Editor = toastui.Editor;\n",[278,25535,25536],{"class":280,"line":316},[278,25537,25538],{},"    this.editor = new Editor({\n",[278,25540,25541],{"class":280,"line":322},[278,25542,25543],{},"      el: this.shadow.querySelector(\"#editor\"),\n",[278,25545,25546],{"class":280,"line":327},[278,25547,25548],{},"      height: \"420px\",\n",[278,25550,25551],{"class":280,"line":340},[278,25552,25553],{},"      initialEditType: \"markdown\",\n",[278,25555,25556],{"class":280,"line":349},[278,25557,25558],{},"      initialValue: this.getAttribute(\"cellvalue\"),\n",[278,25560,25561],{"class":280,"line":375},[278,25562,25563],{},"      previewStyle: \"vertical\",\n",[278,25565,25566],{"class":280,"line":386},[278,25567,25568],{},"      usageStatistics: false,\n",[278,25570,25571],{"class":280,"line":397},[278,25572,25573],{},"      theme: this.config.theme,\n",[278,25575,25576],{"class":280,"line":408},[278,25577,25578],{},"      events: { keydown: this.handleKeyDown },\n",[278,25580,25581],{"class":280,"line":433},[278,25582,1233],{},[278,25584,25585],{"class":280,"line":454},[278,25586,292],{"emptyLinePlaceholder":291},[278,25588,25589],{"class":280,"line":475},[278,25590,25591],{},"    this.setEditorPosition();\n",[278,25593,25594],{"class":280,"line":496},[278,25595,8602],{},[278,25597,25598],{"class":280,"line":505},[278,25599,25600],{},"    console.log(\"render error\", error);\n",[278,25602,25603],{"class":280,"line":516},[278,25604,1096],{},[278,25606,25607],{"class":280,"line":527},[278,25608,617],{},[11,25610,25611],{},"Many things are going on here most of which are self-explanatory, I'll briefly touch upon the important points",[123,25613,25614,25626,25709,25736],{},[74,25615,25616,25617],{},"We open the editor with whatever content the cell was holding",[269,25618,25620],{"className":24597,"code":25619,"language":24599,"meta":274,"style":274},"initialValue: this.getAttribute(\"cellvalue\")\n",[59,25621,25622],{"__ignoreMap":274},[278,25623,25624],{"class":280,"line":281},[278,25625,25619],{},[74,25627,25628,25629],{},"Because of the way cell plugins have been designed, they pop up near the cell to which they're attached. For our plugin, we need a centred dialog. We achieve that through plain old DOM manipulation",[269,25630,25632],{"className":24597,"code":25631,"language":24599,"meta":274,"style":274},"setEditorPosition() {\n  const agPopUpChild = document.querySelector(\".ag-popup-child\");\n  const container = this.shadow.getElementById(\"container\");\n\n  setTimeout(() => {\n    agPopUpChild.style.left = `${\n      (window.innerWidth - container.offsetWidth) \u002F 2\n    }px`;\n    \u002F\u002F agPopUpChild.style.top = `${\n    \u002F\u002F   (window.innerHeight - container.offsetHeight - 100) \u002F 2\n    \u002F\u002F }px`; \u002F\u002F -100 offset for outerbase top bars\n\n    agPopUpChild.style.top = \"0px\"; \u002F\u002F Just hardcode at 0px otherwise top border not visible\n  }, 10);\n}\n",[59,25633,25634,25639,25644,25649,25653,25658,25663,25668,25673,25678,25683,25691,25695,25700,25705],{"__ignoreMap":274},[278,25635,25636],{"class":280,"line":281},[278,25637,25638],{},"setEditorPosition() {\n",[278,25640,25641],{"class":280,"line":288},[278,25642,25643],{},"  const agPopUpChild = document.querySelector(\".ag-popup-child\");\n",[278,25645,25646],{"class":280,"line":295},[278,25647,25648],{},"  const container = this.shadow.getElementById(\"container\");\n",[278,25650,25651],{"class":280,"line":316},[278,25652,292],{"emptyLinePlaceholder":291},[278,25654,25655],{"class":280,"line":322},[278,25656,25657],{},"  setTimeout(() => {\n",[278,25659,25660],{"class":280,"line":327},[278,25661,25662],{},"    agPopUpChild.style.left = `${\n",[278,25664,25665],{"class":280,"line":340},[278,25666,25667],{},"      (window.innerWidth - container.offsetWidth) \u002F 2\n",[278,25669,25670],{"class":280,"line":349},[278,25671,25672],{},"    }px`;\n",[278,25674,25675],{"class":280,"line":375},[278,25676,25677],{},"    \u002F\u002F agPopUpChild.style.top = `${\n",[278,25679,25680],{"class":280,"line":386},[278,25681,25682],{},"    \u002F\u002F   (window.innerHeight - container.offsetHeight - 100) \u002F 2\n",[278,25684,25685,25688],{"class":280,"line":397},[278,25686,25687],{},"    \u002F\u002F }px`;",[278,25689,25690],{}," \u002F\u002F -100 offset for outerbase top bars\n",[278,25692,25693],{"class":280,"line":408},[278,25694,292],{"emptyLinePlaceholder":291},[278,25696,25697],{"class":280,"line":433},[278,25698,25699],{},"    agPopUpChild.style.top = \"0px\"; \u002F\u002F Just hardcode at 0px otherwise top border not visible\n",[278,25701,25702],{"class":280,"line":454},[278,25703,25704],{},"  }, 10);\n",[278,25706,25707],{"class":280,"line":475},[278,25708,617],{},[74,25710,25711,25712,25715,25716],{},"The theme is set to the editor using ",[59,25713,25714],{},"theme: this.config.theme"," doesn't work without some manipulation. At the moment we do not receive the theme metadata in cell plugins. The below code finds the theme from the DOM styles",[269,25717,25719],{"className":24597,"code":25718,"language":24599,"meta":274,"style":274},"const agPopUp = document.querySelector(\".ag-popup\");\nconst colorScheme = window.getComputedStyle(agPopUp)[\"color-scheme\"];\nthis.config.theme = colorScheme === \"normal\" ? \"light\" : \"dark\";\n",[59,25720,25721,25726,25731],{"__ignoreMap":274},[278,25722,25723],{"class":280,"line":281},[278,25724,25725],{},"const agPopUp = document.querySelector(\".ag-popup\");\n",[278,25727,25728],{"class":280,"line":288},[278,25729,25730],{},"const colorScheme = window.getComputedStyle(agPopUp)[\"color-scheme\"];\n",[278,25732,25733],{"class":280,"line":295},[278,25734,25735],{},"this.config.theme = colorScheme === \"normal\" ? \"light\" : \"dark\";\n",[74,25737,25738,25739,25742,25743,25746,25747],{},"If we press enter in the markdown editor (to add a new line), the cell plugin editor closes itself (maybe there is a ",[59,25740,25741],{},"keydown"," event listener somewhere listening for ",[59,25744,25745],{},"Enter"," key events). We skirt through it by stopping such event propagation",[269,25748,25750],{"className":24597,"code":25749,"language":24599,"meta":274,"style":274},"events: { keydown: this.handleKeyDown }, \u002F\u002FListen for keydown events from the md editor\n\nhandleKeyDown(_, event) {\n  if (event.key === \"Enter\") {\n    event.stopPropagation();\n  }\n}\n",[59,25751,25752,25757,25761,25766,25771,25776,25780],{"__ignoreMap":274},[278,25753,25754],{"class":280,"line":281},[278,25755,25756],{},"events: { keydown: this.handleKeyDown }, \u002F\u002FListen for keydown events from the md editor\n",[278,25758,25759],{"class":280,"line":288},[278,25760,292],{"emptyLinePlaceholder":291},[278,25762,25763],{"class":280,"line":295},[278,25764,25765],{},"handleKeyDown(_, event) {\n",[278,25767,25768],{"class":280,"line":316},[278,25769,25770],{},"  if (event.key === \"Enter\") {\n",[278,25772,25773],{"class":280,"line":322},[278,25774,25775],{},"    event.stopPropagation();\n",[278,25777,25778],{"class":280,"line":327},[278,25779,1096],{},[278,25781,25782],{"class":280,"line":340},[278,25783,617],{},[11,25785,25786],{},"Now we're ready to enjoy our writing with the md-editor. Once we're done, we can click the save button to close the editor and update the cell's content.",[269,25788,25790],{"className":24597,"code":25789,"language":24599,"meta":274,"style":274},"const saveBtn = this.shadow.getElementById(\"save-btn\");\nsaveBtn.addEventListener(\"click\", () => {\n  const finalContent = this.editor.getMarkdown();\n  triggerEvent_$PLUGIN_ID(this, {\n    action: OuterbaseColumnEvent_$PLUGIN_ID.onStopEdit,\n    value: finalContent,\n  });\n  triggerEvent_$PLUGIN_ID(this, {\n    action: OuterbaseColumnEvent_$PLUGIN_ID.updateCell,\n    value: finalContent,\n  });\n});\n",[59,25791,25792,25797,25802,25807,25812,25817,25822,25826,25830,25835,25839,25843],{"__ignoreMap":274},[278,25793,25794],{"class":280,"line":281},[278,25795,25796],{},"const saveBtn = this.shadow.getElementById(\"save-btn\");\n",[278,25798,25799],{"class":280,"line":288},[278,25800,25801],{},"saveBtn.addEventListener(\"click\", () => {\n",[278,25803,25804],{"class":280,"line":295},[278,25805,25806],{},"  const finalContent = this.editor.getMarkdown();\n",[278,25808,25809],{"class":280,"line":316},[278,25810,25811],{},"  triggerEvent_$PLUGIN_ID(this, {\n",[278,25813,25814],{"class":280,"line":322},[278,25815,25816],{},"    action: OuterbaseColumnEvent_$PLUGIN_ID.onStopEdit,\n",[278,25818,25819],{"class":280,"line":327},[278,25820,25821],{},"    value: finalContent,\n",[278,25823,25824],{"class":280,"line":340},[278,25825,2037],{},[278,25827,25828],{"class":280,"line":349},[278,25829,25811],{},[278,25831,25832],{"class":280,"line":375},[278,25833,25834],{},"    action: OuterbaseColumnEvent_$PLUGIN_ID.updateCell,\n",[278,25836,25837],{"class":280,"line":386},[278,25838,25821],{},[278,25840,25841],{"class":280,"line":397},[278,25842,2037],{},[278,25844,25845],{"class":280,"line":408},[278,25846,3693],{},[32,25848,25850],{"id":25849},"chatgpt-the-secret-sauce-of-the-plugin","ChatGPT: The secret sauce of the plugin",[11,25852,25853],{},"This plugin comes integrated with ChatGPT to help with your writing with some preconfigured actions. Using it you can change the tone of your writing, get suggestions for headlines, generate a summary for your content, and more.",[11,25855,25856],{},"The below method prepares the UI for showing the tone selections.",[269,25858,25860],{"className":24597,"code":25859,"language":24599,"meta":274,"style":274},"prepareTonesSelections() {\n  const toneSelect = this.shadow.getElementById(\"tone-select\");\n  this.tones.forEach((value) => {\n    this.addSelectOption(toneSelect, value);\n  });\n\n  const toneBtn = this.shadow.getElementById(\"tone-btn\");\n  toneBtn.addEventListener(\"click\", () => {\n    const selectedIndex = toneSelect.selectedIndex;\n    if (selectedIndex) {\n      const selectedTone = toneSelect.options[selectedIndex].value;\n      const prompt = `Make the following text better and rewrite it in a ${selectedTone.toLowerCase()} tone`;\n      this.handleSelectionAction(prompt);\n    }\n  });\n}\n",[59,25861,25862,25867,25872,25877,25882,25886,25890,25895,25900,25905,25910,25915,25920,25925,25929,25933],{"__ignoreMap":274},[278,25863,25864],{"class":280,"line":281},[278,25865,25866],{},"prepareTonesSelections() {\n",[278,25868,25869],{"class":280,"line":288},[278,25870,25871],{},"  const toneSelect = this.shadow.getElementById(\"tone-select\");\n",[278,25873,25874],{"class":280,"line":295},[278,25875,25876],{},"  this.tones.forEach((value) => {\n",[278,25878,25879],{"class":280,"line":316},[278,25880,25881],{},"    this.addSelectOption(toneSelect, value);\n",[278,25883,25884],{"class":280,"line":322},[278,25885,2037],{},[278,25887,25888],{"class":280,"line":327},[278,25889,292],{"emptyLinePlaceholder":291},[278,25891,25892],{"class":280,"line":340},[278,25893,25894],{},"  const toneBtn = this.shadow.getElementById(\"tone-btn\");\n",[278,25896,25897],{"class":280,"line":349},[278,25898,25899],{},"  toneBtn.addEventListener(\"click\", () => {\n",[278,25901,25902],{"class":280,"line":375},[278,25903,25904],{},"    const selectedIndex = toneSelect.selectedIndex;\n",[278,25906,25907],{"class":280,"line":386},[278,25908,25909],{},"    if (selectedIndex) {\n",[278,25911,25912],{"class":280,"line":397},[278,25913,25914],{},"      const selectedTone = toneSelect.options[selectedIndex].value;\n",[278,25916,25917],{"class":280,"line":408},[278,25918,25919],{},"      const prompt = `Make the following text better and rewrite it in a ${selectedTone.toLowerCase()} tone`;\n",[278,25921,25922],{"class":280,"line":433},[278,25923,25924],{},"      this.handleSelectionAction(prompt);\n",[278,25926,25927],{"class":280,"line":454},[278,25928,1285],{},[278,25930,25931],{"class":280,"line":475},[278,25932,2037],{},[278,25934,25935],{"class":280,"line":496},[278,25936,617],{},[11,25938,25939],{},"The action is handled in the below method where we go ahead only if some text is selected in the editor. We also show a \"Thinking...\" text as a loader and finally replace that with the result received from the API call.",[269,25941,25943],{"className":24597,"code":25942,"language":24599,"meta":274,"style":274},"async handleSelectionAction(prompt) {\n  const [start, end] = this.editor.getSelection();\n  const selectedText = this.editor.getSelectedText(start, end);\n  if (selectedText) {\n    this.editor.insertText(`${selectedText}\\n\\nThinking...`);\n    const currLinePos = end[0] + 2;\n    this.editor.setSelection(\n      [currLinePos, 1],\n      [currLinePos, \"Thinking...\".length + 1]\n    );\n\n    const generatedText = await this.talkToChatGPT(prompt, selectedText);\n    if (generatedText) {\n      this.editor.insertText(generatedText);\n    }\n  }\n}\n",[59,25944,25945,25950,25955,25960,25965,25970,25975,25980,25985,25990,25994,25998,26003,26008,26013,26017,26021],{"__ignoreMap":274},[278,25946,25947],{"class":280,"line":281},[278,25948,25949],{},"async handleSelectionAction(prompt) {\n",[278,25951,25952],{"class":280,"line":288},[278,25953,25954],{},"  const [start, end] = this.editor.getSelection();\n",[278,25956,25957],{"class":280,"line":295},[278,25958,25959],{},"  const selectedText = this.editor.getSelectedText(start, end);\n",[278,25961,25962],{"class":280,"line":316},[278,25963,25964],{},"  if (selectedText) {\n",[278,25966,25967],{"class":280,"line":322},[278,25968,25969],{},"    this.editor.insertText(`${selectedText}\\n\\nThinking...`);\n",[278,25971,25972],{"class":280,"line":327},[278,25973,25974],{},"    const currLinePos = end[0] + 2;\n",[278,25976,25977],{"class":280,"line":340},[278,25978,25979],{},"    this.editor.setSelection(\n",[278,25981,25982],{"class":280,"line":349},[278,25983,25984],{},"      [currLinePos, 1],\n",[278,25986,25987],{"class":280,"line":375},[278,25988,25989],{},"      [currLinePos, \"Thinking...\".length + 1]\n",[278,25991,25992],{"class":280,"line":386},[278,25993,1898],{},[278,25995,25996],{"class":280,"line":397},[278,25997,292],{"emptyLinePlaceholder":291},[278,25999,26000],{"class":280,"line":408},[278,26001,26002],{},"    const generatedText = await this.talkToChatGPT(prompt, selectedText);\n",[278,26004,26005],{"class":280,"line":433},[278,26006,26007],{},"    if (generatedText) {\n",[278,26009,26010],{"class":280,"line":454},[278,26011,26012],{},"      this.editor.insertText(generatedText);\n",[278,26014,26015],{"class":280,"line":475},[278,26016,1285],{},[278,26018,26019],{"class":280,"line":496},[278,26020,1096],{},[278,26022,26023],{"class":280,"line":505},[278,26024,617],{},[11,26026,26027],{},"Below is the method which makes the API call to ChatGPT",[269,26029,26031],{"className":24597,"code":26030,"language":24599,"meta":274,"style":274},"async talkToChatGPT(instruction, text) {\n  const res = await fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fcompletions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application\u002Fjson\",\n      Authorization: `Bearer ${this.config.apiKey}`,\n    },\n    body: JSON.stringify({\n      model: \"gpt-3.5-turbo-instruct\",\n      prompt: `${instruction}: ${text}`,\n      max_tokens: 2048,\n      temperature: 0.3,\n      n: 1,\n    }),\n  });\n\n  const data = await res.json();\n  return data.choices[0].text.trim();\n}\n",[59,26032,26033,26038,26043,26048,26053,26058,26063,26067,26072,26077,26082,26087,26092,26097,26102,26106,26110,26114,26119],{"__ignoreMap":274},[278,26034,26035],{"class":280,"line":281},[278,26036,26037],{},"async talkToChatGPT(instruction, text) {\n",[278,26039,26040],{"class":280,"line":288},[278,26041,26042],{},"  const res = await fetch(\"https:\u002F\u002Fapi.openai.com\u002Fv1\u002Fcompletions\", {\n",[278,26044,26045],{"class":280,"line":295},[278,26046,26047],{},"    method: \"POST\",\n",[278,26049,26050],{"class":280,"line":316},[278,26051,26052],{},"    headers: {\n",[278,26054,26055],{"class":280,"line":322},[278,26056,26057],{},"      \"Content-Type\": \"application\u002Fjson\",\n",[278,26059,26060],{"class":280,"line":327},[278,26061,26062],{},"      Authorization: `Bearer ${this.config.apiKey}`,\n",[278,26064,26065],{"class":280,"line":340},[278,26066,2243],{},[278,26068,26069],{"class":280,"line":349},[278,26070,26071],{},"    body: JSON.stringify({\n",[278,26073,26074],{"class":280,"line":375},[278,26075,26076],{},"      model: \"gpt-3.5-turbo-instruct\",\n",[278,26078,26079],{"class":280,"line":386},[278,26080,26081],{},"      prompt: `${instruction}: ${text}`,\n",[278,26083,26084],{"class":280,"line":397},[278,26085,26086],{},"      max_tokens: 2048,\n",[278,26088,26089],{"class":280,"line":408},[278,26090,26091],{},"      temperature: 0.3,\n",[278,26093,26094],{"class":280,"line":433},[278,26095,26096],{},"      n: 1,\n",[278,26098,26099],{"class":280,"line":454},[278,26100,26101],{},"    }),\n",[278,26103,26104],{"class":280,"line":475},[278,26105,2037],{},[278,26107,26108],{"class":280,"line":496},[278,26109,292],{"emptyLinePlaceholder":291},[278,26111,26112],{"class":280,"line":505},[278,26113,25015],{},[278,26115,26116],{"class":280,"line":516},[278,26117,26118],{},"  return data.choices[0].text.trim();\n",[278,26120,26121],{"class":280,"line":527},[278,26122,617],{},[11,26124,26125],{},"Similarly, we handle the other commands using other appropriate prompts. You can check the GitHub repo for the complete source code of the plugin.",[24,26127,26129],{"id":26128},"current-limitations","Current Limitations",[123,26131,26132,26135,26138],{},[74,26133,26134],{},"The cell plugin editors are closed automatically as soon as users click somewhere outside. It would be great to have an option to close the editor when users explicitly click on a button",[74,26136,26137],{},"The changed data does not persist even though correct events are sent to the parent DOM. One workaround is to use the Outerbase commands to make a call to the database, but due to lack of time I haven't explored that",[74,26139,26140],{},"The toolbar items that open a pop-up\u002Fdialog in the markdown editor (Heading \u002F Link \u002F Image etc.) do not work. The dialogs get closed as soon as you click on them. This is an open issue with the ToastUI editor where this functionality doesn't work in Shadow Dom. There is a workaround to replace these buttons with a similar button that doesn't open a dialog. Again due to lack of time, this has not been explored.",[24,26142,26144],{"id":26143},"resources","Resources",[11,26146,26147],{},"The complete source code of the plugins and the templates can be found here",[40,26149],{"url":26150},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fouterbase-adventures",[11,26152,26153],{},"The demo video showing the plugins in action",[40,26155],{"url":24482},[24,26157,10634],{"id":10633},[11,26159,26160],{},"When you explore a new thing many roadblocks will come. Some roadblocks will have a workaround, and some will be dead ends. Now it is up to you whether you quit on the first roadblock, or push ahead and try to find a way. This Outerbase hackathon was one such adventure for me, and I thoroughly enjoyed it.",[11,26162,26163],{},"Hope you liked reading the article. Do let me know your thoughts in the comments section.",[11,26165,26166],{},[3061,26167,26168],{},"Remember to keep adding the bits, soon you'll have more bytes than you'll ever need :-)",[11,26170,26171],{},"Until text time! Adios.",[3065,26173,26174],{},"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 .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}",{"title":274,"searchDepth":288,"depth":288,"links":26176},[26177,26178,26181,26182,26183,26188,26189,26190],{"id":22771,"depth":288,"text":22772},{"id":24485,"depth":288,"text":24486,"children":26179},[26180],{"id":24492,"depth":295,"text":24493},{"id":24806,"depth":288,"text":24807},{"id":24891,"depth":288,"text":24892},{"id":25063,"depth":288,"text":25064,"children":26184},[26185,26186,26187],{"id":25183,"depth":295,"text":25184},{"id":25364,"depth":295,"text":25365},{"id":25849,"depth":295,"text":25850},{"id":26128,"depth":288,"text":26129},{"id":26143,"depth":288,"text":26144},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt\u002F7a3213c083ad21700d87e2fbb2bb66b5-dbcb9b2815.jpeg","2023-10-02T03:28:27.792Z","This article is about exploring the new talk of the town, `bun`, getting to know `Outerbase`, and hanging out with old buddies `markdown` and `ChatGPT`. If you follow along, you...","cln8bzn2o000209moajbn32z5",{},"\u002Fto-outerbase-with-bun-toastui-editor-and-chatgpt",{"title":24423,"description":26193},"to-outerbase-with-bun-toastui-editor-and-chatgpt",[24164,24431,24418,26200,26201],"outerbasehackathon","outerbase","Fp2Po8jlQlfdXQ1gZyj9BeR39rwBwy54gTSc4IgzYTQ",{"id":26204,"title":26205,"body":26206,"cover":31997,"date":31998,"description":31999,"draft":3086,"extension":3087,"hashnodeId":32000,"meta":32001,"navigation":291,"path":32002,"seo":32003,"slug":32004,"stem":32004,"tags":32005,"__hash__":32009},"posts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite.md","Creating 1Password plugin and use it to build an app with Nuxt3, Passage & Appwrite",{"type":8,"value":26207,"toc":31978},[26208,26222,26227,26229,26238,26247,26251,26263,26268,26274,26278,26302,26306,26313,26316,26422,26432,26437,26442,26449,26519,26526,26531,26543,26550,26553,26556,26600,26617,26620,26624,26635,26640,26758,26765,26807,26812,26815,26922,26927,26930,27059,27065,27159,27165,27342,27352,27357,27360,27365,27369,27372,27395,27406,27415,27426,27435,27444,27448,27457,27510,27517,27531,27534,27562,27574,27606,27629,27634,27637,27640,27658,27665,27763,27766,27783,27787,27790,27809,27812,28109,28116,28383,28389,28787,28792,28795,28798,29007,29013,30077,30080,30291,30294,30342,30346,30349,30360,30364,30367,30370,30539,30544,30549,30554,30559,30564,30569,30574,30579,30584,30589,30594,30599,30604,30611,30616,30619,30624,30629,30634,30637,30642,30647,30652,30657,30662,30667,30671,30674,31754,31757,31784,31869,31872,31896,31900,31903,31920,31923,31934,31938,31941,31944,31952,31959,31961,31968,31971,31975],[11,26209,26210,26211,26216,26217,26221],{},"Last year I built an app for my son (and myself). The app idea was very simple, a digital piggy bank tracker integrated with optional auto credit of pocket money. We're actively using ",[47,26212,26215],{"href":26213,"rel":26214},"https:\u002F\u002Fmypiggyjar.com",[51],"this app"," even now (if interested, you can read the associated article ",[47,26218,3286],{"href":26219,"rel":26220},"https:\u002F\u002Frajeev.dev\u002Fnew-years-promise-to-my-son",[51],"). But this app had some shortcomings, the biggest one being not able to invite your family members to the app. This app rewrite is intended to fix that shortcoming.",[11,26223,26224],{},[3061,26225,26226],{},"Tl;dr this article covers a complete rewrite of an existing app using a different technology stack. The article also covers the creation of a 1Password CLI Shell Plugin.",[24,26228,22772],{"id":22771},[11,26230,26231,26232,26237],{},"To do the rewrite I wanted to use Nuxt3. The reasoning was simple, I've used Nuxt2 in the past, but haven't had the chance to look at Nuxt3 (and Vue3 for that matter). For the database and to process the important database events I decided to use Appwrite. Appwrite has inbuilt authentication but I wanted to try out ",[47,26233,26236],{"href":26234,"rel":26235},"https:\u002F\u002Fpassage.1password.com\u002F",[51],"Passage by 1Password",", so this app uses that.",[11,26239,26240,26241,26246],{},"Appwrite provides a CLI, and ",[47,26242,26245],{"href":26243,"rel":26244},"https:\u002F\u002F1password.com\u002Fdevelopers?utm_source=hashnode&utm_medium=landing-page&utm_campaign=hashnode-hackathon",[51],"1Password"," also has a CLI but both are not integrated. So it seemed logical to first integrate the two and create the Appwrite shell plugin for the 1Password CLI.",[24,26248,26250],{"id":26249},"part-1-creating-a-1password-shell-plugin","Part 1: Creating a 1Password shell plugin",[11,26252,26253,26254,26259,26260,183],{},"If you go over to the ",[47,26255,26258],{"href":26256,"rel":26257},"https:\u002F\u002Fdeveloper.1password.com\u002Fdocs\u002Fcli\u002Fshell-plugins\u002Fcontribute",[51],"1Password docs"," page which mentions how you can contribute and create your own shell plugin, you'll learn that the whole thing is implemented using ",[59,26261,26262],{},"Go",[11,26264,26265],{},[3135,26266],{"alt":274,"src":26267},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002F3oEjHWzZQaCrZW2aWs\u002Fgiphy.gif",[11,26269,26270,26271,26273],{},"I haven't tried going anywhere with ",[59,26272,26262],{},"😉. But the documentation looked straightforward forward so what is the harm in trying?",[32,26275,26277],{"id":26276},"setup-the-environment","Setup the environment",[11,26279,26280,26281,26286,26287,26290,26291,26293,26294,26297,26298,26301],{},"I'm not going to rewrite what is already covered in detail in the docs link shared above. Just follow the link and install the needed dependencies. I had installed GNU Make using ",[47,26282,26285],{"href":26283,"rel":26284},"https:\u002F\u002Fformulae.brew.sh\u002Fformula\u002Fmake",[51],"homebrew"," which installs it as ",[59,26288,26289],{},"gmake",". So we'll need to replace make commands with ",[59,26292,26289],{}," (unless you add a ",[59,26295,26296],{},"gnubin"," directory to the ",[59,26299,26300],{},"PATH"," as mentioned in the link).",[32,26303,26305],{"id":26304},"choosing-a-provisioner","Choosing a Provisioner",[11,26307,26308,26309,26312],{},"The first decision point comes when you need to choose a ",[59,26310,26311],{},"provisioner",". As the docs page says, \"Provisioners are in essence hooks that get executed before the executable is run by 1Password CLI, and after the executable exits in case any cleanup is needed.\". So how a third-party CLI authenticates you needs to be configured here. This is not a one size fits all scenario. You need to go and read the said third-party CLI documentation to figure it out. But I had used the appwrite CLI, it needs Email & Password so this reading documentation advice is not for me.",[11,26314,26315],{},"1Password CLI supports the following provisioners",[269,26317,26321],{"className":26318,"code":26319,"language":26320,"meta":274,"style":274},"language-go shiki shiki-themes github-light github-dark","const (\n    APIClientCredentials = sdk.CredentialName(\"API Client Credentials\")\n    APIKey               = sdk.CredentialName(\"API Key\")\n    APIToken             = sdk.CredentialName(\"API Token\")\n    AccessKey            = sdk.CredentialName(\"Access Key\")\n    AccessToken          = sdk.CredentialName(\"Access Token\")\n    AppPassword          = sdk.CredentialName(\"App Password\")\n    AppToken             = sdk.CredentialName(\"App Token\")\n    AuthToken            = sdk.CredentialName(\"Auth Token\")\n    CLIToken             = sdk.CredentialName(\"CLI Token\")\n    Credential           = sdk.CredentialName(\"Credential\")\n    Credentials          = sdk.CredentialName(\"Credentials\")\n    DatabaseCredentials  = sdk.CredentialName(\"Database Credentials\")\n    LoginDetails         = sdk.CredentialName(\"Login Details\")\n    PersonalAPIToken     = sdk.CredentialName(\"Personal API Token\")\n    PersonalAccessToken  = sdk.CredentialName(\"Personal Access Token\")\n    RegistryCredentials  = sdk.CredentialName(\"Registry Credentials\")\n    SecretKey            = sdk.CredentialName(\"Secret Key\")\n    UserLogin            = sdk.CredentialName(\"User Login\")\n)\n","go",[59,26322,26323,26328,26333,26338,26343,26348,26353,26358,26363,26368,26373,26378,26383,26388,26393,26398,26403,26408,26413,26418],{"__ignoreMap":274},[278,26324,26325],{"class":280,"line":281},[278,26326,26327],{},"const (\n",[278,26329,26330],{"class":280,"line":288},[278,26331,26332],{},"    APIClientCredentials = sdk.CredentialName(\"API Client Credentials\")\n",[278,26334,26335],{"class":280,"line":295},[278,26336,26337],{},"    APIKey               = sdk.CredentialName(\"API Key\")\n",[278,26339,26340],{"class":280,"line":316},[278,26341,26342],{},"    APIToken             = sdk.CredentialName(\"API Token\")\n",[278,26344,26345],{"class":280,"line":322},[278,26346,26347],{},"    AccessKey            = sdk.CredentialName(\"Access Key\")\n",[278,26349,26350],{"class":280,"line":327},[278,26351,26352],{},"    AccessToken          = sdk.CredentialName(\"Access Token\")\n",[278,26354,26355],{"class":280,"line":340},[278,26356,26357],{},"    AppPassword          = sdk.CredentialName(\"App Password\")\n",[278,26359,26360],{"class":280,"line":349},[278,26361,26362],{},"    AppToken             = sdk.CredentialName(\"App Token\")\n",[278,26364,26365],{"class":280,"line":375},[278,26366,26367],{},"    AuthToken            = sdk.CredentialName(\"Auth Token\")\n",[278,26369,26370],{"class":280,"line":386},[278,26371,26372],{},"    CLIToken             = sdk.CredentialName(\"CLI Token\")\n",[278,26374,26375],{"class":280,"line":397},[278,26376,26377],{},"    Credential           = sdk.CredentialName(\"Credential\")\n",[278,26379,26380],{"class":280,"line":408},[278,26381,26382],{},"    Credentials          = sdk.CredentialName(\"Credentials\")\n",[278,26384,26385],{"class":280,"line":433},[278,26386,26387],{},"    DatabaseCredentials  = sdk.CredentialName(\"Database Credentials\")\n",[278,26389,26390],{"class":280,"line":454},[278,26391,26392],{},"    LoginDetails         = sdk.CredentialName(\"Login Details\")\n",[278,26394,26395],{"class":280,"line":475},[278,26396,26397],{},"    PersonalAPIToken     = sdk.CredentialName(\"Personal API Token\")\n",[278,26399,26400],{"class":280,"line":496},[278,26401,26402],{},"    PersonalAccessToken  = sdk.CredentialName(\"Personal Access Token\")\n",[278,26404,26405],{"class":280,"line":505},[278,26406,26407],{},"    RegistryCredentials  = sdk.CredentialName(\"Registry Credentials\")\n",[278,26409,26410],{"class":280,"line":516},[278,26411,26412],{},"    SecretKey            = sdk.CredentialName(\"Secret Key\")\n",[278,26414,26415],{"class":280,"line":527},[278,26416,26417],{},"    UserLogin            = sdk.CredentialName(\"User Login\")\n",[278,26419,26420],{"class":280,"line":533},[278,26421,4590],{},[11,26423,26424,26425,919,26428,26431],{},"Appwrite CLI uses email\u002Fpassword for authentication so, ",[59,26426,26427],{},"LoginDetails",[59,26429,26430],{},"UserLogin"," look like the two promising choices. So I picked one of them. The next step asks for an example credential which was confusing as it didn't say whether this example is for the login or the password. Anyway, after putting some random string there we get the template code generated. And we've got a new problem",[11,26433,26434],{},[3135,26435],{"alt":274,"src":26436},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F455ad862-10ef-45b9-959b-e7530f934982-4e7e4b8c6d.png",[11,26438,26439],{},[3135,26440],{"alt":274,"src":26441},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F525ce48a-86fc-4c15-a10b-45cff91c0a41-7117285f7c.png",[11,26443,26444,26445,26448],{},"Both of the above choices generate code with errors. These field names are not defined. And also only one ",[59,26446,26447],{},"fieldname"," is created.",[269,26450,26452],{"className":26318,"code":26451,"language":26320,"meta":274,"style":274},"Fields: []schema.CredentialField{\n  {\n    Name:                fieldname.Login,\n    MarkdownDescription: \"Login used to authenticate to Appwrite.\",\n    Secret:              true,\n    Composition: &schema.ValueComposition{\n      Length: 21,\n      Charset: schema.Charset{\n        Lowercase: true,\n        Digits:    true,\n      },\n    },\n  },\n},\n",[59,26453,26454,26459,26463,26468,26473,26478,26483,26488,26493,26498,26503,26507,26511,26515],{"__ignoreMap":274},[278,26455,26456],{"class":280,"line":281},[278,26457,26458],{},"Fields: []schema.CredentialField{\n",[278,26460,26461],{"class":280,"line":288},[278,26462,11470],{},[278,26464,26465],{"class":280,"line":295},[278,26466,26467],{},"    Name:                fieldname.Login,\n",[278,26469,26470],{"class":280,"line":316},[278,26471,26472],{},"    MarkdownDescription: \"Login used to authenticate to Appwrite.\",\n",[278,26474,26475],{"class":280,"line":322},[278,26476,26477],{},"    Secret:              true,\n",[278,26479,26480],{"class":280,"line":327},[278,26481,26482],{},"    Composition: &schema.ValueComposition{\n",[278,26484,26485],{"class":280,"line":340},[278,26486,26487],{},"      Length: 21,\n",[278,26489,26490],{"class":280,"line":349},[278,26491,26492],{},"      Charset: schema.Charset{\n",[278,26494,26495],{"class":280,"line":375},[278,26496,26497],{},"        Lowercase: true,\n",[278,26499,26500],{"class":280,"line":386},[278,26501,26502],{},"        Digits:    true,\n",[278,26504,26505],{"class":280,"line":397},[278,26506,1165],{},[278,26508,26509],{"class":280,"line":408},[278,26510,2243],{},[278,26512,26513],{"class":280,"line":433},[278,26514,683],{},[278,26516,26517],{"class":280,"line":454},[278,26518,11040],{},[11,26520,26521,26522,26525],{},"After defining the missing fieldsname, and adding a new field (password) in the array, tried validating, building and initing the plugin (using ",[59,26523,26524],{},"op plugin init appwrite","). It asked for the login and password and created an entry in the 1Password app. The moment of truth is here",[11,26527,26528],{},[3135,26529],{"alt":274,"src":26530},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FKJBgowpC3j0U8MNxkQ\u002Fgiphy.gif",[11,26532,26533,26534,26537,26538,183],{},"Run ",[59,26535,26536],{},"appwrite login",". Wait for the email and password to be supplied by the 1Password App, ",[3061,26539,26540],{},[94,26541,26542],{},"and it never happens",[11,26544,26545,26546,26549],{},"This is the moment I realized something is not right. Tried looking for a config file in the system and found one at ",[59,26547,26548],{},"~\u002F.appwrite\u002Fprefs.json",". It doesn't store a password just a cookie. Essentially what the CLI does is, take the login credentials from you interactively, and then make an auth call and store the auth token in that file for future use. We could store the cookie in 1Password but then who takes care of refreshing the token?",[11,26551,26552],{},"Before giving up, this is where I got in touch with the 1Password team. Learnt that interactive login is not supported, and the following verbatim response \"If your file supports provisioning the credentials in a file, you can use the SDK’s file provisioner when building the plugin.\".",[11,26554,26555],{},"Interesting! File provisioner is mentioned in the docs but mostly you're going to miss reading it. Well, time to eat my words and go to appwrite CLI docs, and read it, of course. Found that there is a CI mode that works using the API Key. The only limitation is, it works with a single project at a time. And also the supported CLI commands depends on the permissions granted to the API Key.",[269,26557,26559],{"className":3335,"code":26558,"language":3337,"meta":274,"style":274},"appwrite client \\\n    --endpoint https:\u002F\u002Fcloud.appwrite.io\u002Fv1 \\\n    --projectId [YOUR_PROJECT_ID] \\\n    --key YOUR_API_KEY\n",[59,26560,26561,26572,26582,26592],{"__ignoreMap":274},[278,26562,26563,26566,26569],{"class":280,"line":281},[278,26564,26565],{"class":333},"appwrite",[278,26567,26568],{"class":309}," client",[278,26570,26571],{"class":650}," \\\n",[278,26573,26574,26577,26580],{"class":280,"line":288},[278,26575,26576],{"class":650},"    --endpoint",[278,26578,26579],{"class":309}," https:\u002F\u002Fcloud.appwrite.io\u002Fv1",[278,26581,26571],{"class":650},[278,26583,26584,26587,26590],{"class":280,"line":295},[278,26585,26586],{"class":650},"    --projectId",[278,26588,26589],{"class":302}," [YOUR_PROJECT_ID] ",[278,26591,11233],{"class":650},[278,26593,26594,26597],{"class":280,"line":316},[278,26595,26596],{"class":333},"    --key",[278,26598,26599],{"class":309}," YOUR_API_KEY\n",[11,26601,26602,26603,26606,26607,26610,26611,26613,26614,26616],{},"On running this command from the home dir of my system, it added the endpoint and the API key to the ",[59,26604,26605],{},"prefs.json"," mentioned earlier, but it also created a new ",[59,26608,26609],{},"appwrite.json"," file mentioning the project ID in the home dir (from where the command was executed). So the ",[59,26612,26605],{}," doesn't contain the project id. If you've used appwrite then you'll know that the project folder also contains an ",[59,26615,26609],{}," file mentioning the project id. This hints that we only need to store the endpoint and the API key in 1Password, and then run the appwrite commands from the project directory itself (1Password supports tying up credentials to a particular dir).",[11,26618,26619],{},"After all this detective work, we are finally ready to implement the plugin.",[32,26621,26623],{"id":26622},"implementing-the-plugin","Implementing the plugin",[11,26625,26626,26627,26630,26631,26634],{},"After deleting and running the ",[59,26628,26629],{},"gmake new-plugin"," command again, this time I selected API Key as the credential type. Modified the generate ",[59,26632,26633],{},"api_key.go"," file as shown below",[11,26636,26637],{},[94,26638,26639],{},"Required Fields, Default Provisioner & Importer",[269,26641,26643],{"className":26318,"code":26642,"language":26320,"meta":274,"style":274},"Fields: []schema.CredentialField{\n    {\n        Name:                fieldname.APIKey,\n        MarkdownDescription: \"API Key used to authenticate to Appwrite.\",\n        Secret:              true,\n        Composition: &schema.ValueComposition{\n            Length: 256,\n            Charset: schema.Charset{\n                Lowercase: true,\n                Digits:    true,\n            },\n        },\n    },\n    {\n        Name:                fieldname.Endpoint,\n        MarkdownDescription: \"Appwrite server endpoint.\",\n        Secret:              false,\n        Optional:            false,\n    },\n},\nDefaultProvisioner: provision.TempFile(appwriteConfig, provision.AtFixedPath(ConfigPath())),\nImporter: importer.TryAll(\n    TryAppwriteConfigFile(),\n)}\n",[59,26644,26645,26649,26653,26658,26663,26668,26673,26678,26683,26688,26693,26698,26702,26706,26710,26715,26720,26725,26730,26734,26738,26743,26748,26753],{"__ignoreMap":274},[278,26646,26647],{"class":280,"line":281},[278,26648,26458],{},[278,26650,26651],{"class":280,"line":288},[278,26652,2209],{},[278,26654,26655],{"class":280,"line":295},[278,26656,26657],{},"        Name:                fieldname.APIKey,\n",[278,26659,26660],{"class":280,"line":316},[278,26661,26662],{},"        MarkdownDescription: \"API Key used to authenticate to Appwrite.\",\n",[278,26664,26665],{"class":280,"line":322},[278,26666,26667],{},"        Secret:              true,\n",[278,26669,26670],{"class":280,"line":327},[278,26671,26672],{},"        Composition: &schema.ValueComposition{\n",[278,26674,26675],{"class":280,"line":340},[278,26676,26677],{},"            Length: 256,\n",[278,26679,26680],{"class":280,"line":349},[278,26681,26682],{},"            Charset: schema.Charset{\n",[278,26684,26685],{"class":280,"line":375},[278,26686,26687],{},"                Lowercase: true,\n",[278,26689,26690],{"class":280,"line":386},[278,26691,26692],{},"                Digits:    true,\n",[278,26694,26695],{"class":280,"line":397},[278,26696,26697],{},"            },\n",[278,26699,26700],{"class":280,"line":408},[278,26701,2606],{},[278,26703,26704],{"class":280,"line":433},[278,26705,2243],{},[278,26707,26708],{"class":280,"line":454},[278,26709,2209],{},[278,26711,26712],{"class":280,"line":475},[278,26713,26714],{},"        Name:                fieldname.Endpoint,\n",[278,26716,26717],{"class":280,"line":496},[278,26718,26719],{},"        MarkdownDescription: \"Appwrite server endpoint.\",\n",[278,26721,26722],{"class":280,"line":505},[278,26723,26724],{},"        Secret:              false,\n",[278,26726,26727],{"class":280,"line":516},[278,26728,26729],{},"        Optional:            false,\n",[278,26731,26732],{"class":280,"line":527},[278,26733,2243],{},[278,26735,26736],{"class":280,"line":533},[278,26737,11040],{},[278,26739,26740],{"class":280,"line":539},[278,26741,26742],{},"DefaultProvisioner: provision.TempFile(appwriteConfig, provision.AtFixedPath(ConfigPath())),\n",[278,26744,26745],{"class":280,"line":545},[278,26746,26747],{},"Importer: importer.TryAll(\n",[278,26749,26750],{"class":280,"line":551},[278,26751,26752],{},"    TryAppwriteConfigFile(),\n",[278,26754,26755],{"class":280,"line":557},[278,26756,26757],{},")}\n",[11,26759,26760,26761,26764],{},"Note that we're creating the temp file at a fixed path ",[59,26762,26763],{},"provision.AtFixedPath(ConfigPath()))",". Where ConfigPath is as shown below",[269,26766,26768],{"className":26318,"code":26767,"language":26320,"meta":274,"style":274},"func ConfigPath() string {\n    configDir, err := os.UserHomeDir()\n    if err != nil {\n        return \"~\u002F.appwrite\u002Fprefs.json\"\n    }\n\n    return configDir + \"\u002F.appwrite\u002Fprefs.json\"\n}\n",[59,26769,26770,26775,26780,26785,26790,26794,26798,26803],{"__ignoreMap":274},[278,26771,26772],{"class":280,"line":281},[278,26773,26774],{},"func ConfigPath() string {\n",[278,26776,26777],{"class":280,"line":288},[278,26778,26779],{},"    configDir, err := os.UserHomeDir()\n",[278,26781,26782],{"class":280,"line":295},[278,26783,26784],{},"    if err != nil {\n",[278,26786,26787],{"class":280,"line":316},[278,26788,26789],{},"        return \"~\u002F.appwrite\u002Fprefs.json\"\n",[278,26791,26792],{"class":280,"line":322},[278,26793,1285],{},[278,26795,26796],{"class":280,"line":327},[278,26797,292],{"emptyLinePlaceholder":291},[278,26799,26800],{"class":280,"line":340},[278,26801,26802],{},"    return configDir + \"\u002F.appwrite\u002Fprefs.json\"\n",[278,26804,26805],{"class":280,"line":349},[278,26806,617],{},[11,26808,26809],{},[94,26810,26811],{},"appwriteConfig function",[11,26813,26814],{},"This function takes the credentials from 1Password and converts that into a JSON object so that a temp config file can be created for the appwrite CLI",[269,26816,26818],{"className":26318,"code":26817,"language":26320,"meta":274,"style":274},"func appwriteConfig(in sdk.ProvisionInput) ([]byte, error) {\n    \u002F\u002F Create config object from the incoming fields\n    config := Config{\n        APIKey:   in.ItemFields[fieldname.APIKey],\n        Endpoint: in.ItemFields[fieldname.Endpoint],\n    }\n\n    \u002F\u002F Covert the content to a JSON string\n    contents, err := json.MarshalIndent(&config, \"\", \"  \")\n    if err != nil {\n        return nil, err\n    }\n\n    \u002F\u002F Convert the string to bytes, to be written to the temp file\n    return []byte(contents), nil\n}\n\n\u002F\u002F The config struct (notice the differnt json key names)\ntype Config struct {\n    APIKey   string `json:\"key\"`\n    Endpoint string `json:\"endpoint\"`\n}\n",[59,26819,26820,26825,26830,26835,26840,26845,26849,26853,26858,26863,26867,26872,26876,26880,26885,26890,26894,26898,26903,26908,26913,26918],{"__ignoreMap":274},[278,26821,26822],{"class":280,"line":281},[278,26823,26824],{},"func appwriteConfig(in sdk.ProvisionInput) ([]byte, error) {\n",[278,26826,26827],{"class":280,"line":288},[278,26828,26829],{},"    \u002F\u002F Create config object from the incoming fields\n",[278,26831,26832],{"class":280,"line":295},[278,26833,26834],{},"    config := Config{\n",[278,26836,26837],{"class":280,"line":316},[278,26838,26839],{},"        APIKey:   in.ItemFields[fieldname.APIKey],\n",[278,26841,26842],{"class":280,"line":322},[278,26843,26844],{},"        Endpoint: in.ItemFields[fieldname.Endpoint],\n",[278,26846,26847],{"class":280,"line":327},[278,26848,1285],{},[278,26850,26851],{"class":280,"line":340},[278,26852,292],{"emptyLinePlaceholder":291},[278,26854,26855],{"class":280,"line":349},[278,26856,26857],{},"    \u002F\u002F Covert the content to a JSON string\n",[278,26859,26860],{"class":280,"line":375},[278,26861,26862],{},"    contents, err := json.MarshalIndent(&config, \"\", \"  \")\n",[278,26864,26865],{"class":280,"line":386},[278,26866,26784],{},[278,26868,26869],{"class":280,"line":397},[278,26870,26871],{},"        return nil, err\n",[278,26873,26874],{"class":280,"line":408},[278,26875,1285],{},[278,26877,26878],{"class":280,"line":433},[278,26879,292],{"emptyLinePlaceholder":291},[278,26881,26882],{"class":280,"line":454},[278,26883,26884],{},"    \u002F\u002F Convert the string to bytes, to be written to the temp file\n",[278,26886,26887],{"class":280,"line":475},[278,26888,26889],{},"    return []byte(contents), nil\n",[278,26891,26892],{"class":280,"line":496},[278,26893,617],{},[278,26895,26896],{"class":280,"line":505},[278,26897,292],{"emptyLinePlaceholder":291},[278,26899,26900],{"class":280,"line":516},[278,26901,26902],{},"\u002F\u002F The config struct (notice the differnt json key names)\n",[278,26904,26905],{"class":280,"line":527},[278,26906,26907],{},"type Config struct {\n",[278,26909,26910],{"class":280,"line":533},[278,26911,26912],{},"    APIKey   string `json:\"key\"`\n",[278,26914,26915],{"class":280,"line":539},[278,26916,26917],{},"    Endpoint string `json:\"endpoint\"`\n",[278,26919,26920],{"class":280,"line":545},[278,26921,617],{},[11,26923,26924],{},[94,26925,26926],{},"TryAppwriteConfigFile function",[11,26928,26929],{},"This function reads an existing prefs.json file at the config path and shows a prompt to the user that these credentials can be imported into 1Password when they run any of the appwrite commands requiring authentication.",[269,26931,26933],{"className":26318,"code":26932,"language":26320,"meta":274,"style":274},"func TryAppwriteConfigFile() sdk.Importer {\n    return importer.TryFile(ConfigPath(), func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) {\n        var config Config\n        if err := contents.ToJSON(&config); err != nil {\n            out.AddError(err)\n            return\n        }\n\n        fmt.Println(\"Printing the imported file\")\n        fmt.Println(config)\n\n        if config.APIKey == \"\" {\n            return\n        }\n\n        if config.Endpoint == \"\" {\n            return\n        }\n\n        out.AddCandidate(sdk.ImportCandidate{\n            Fields: map[sdk.FieldName]string{\n                fieldname.APIKey:   config.APIKey,\n                fieldname.Endpoint: config.Endpoint,\n            },\n        })\n    })\n}\n",[59,26934,26935,26940,26945,26950,26955,26960,26965,26969,26973,26978,26983,26987,26992,26996,27000,27004,27009,27013,27017,27021,27026,27031,27036,27041,27045,27050,27055],{"__ignoreMap":274},[278,26936,26937],{"class":280,"line":281},[278,26938,26939],{},"func TryAppwriteConfigFile() sdk.Importer {\n",[278,26941,26942],{"class":280,"line":288},[278,26943,26944],{},"    return importer.TryFile(ConfigPath(), func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) {\n",[278,26946,26947],{"class":280,"line":295},[278,26948,26949],{},"        var config Config\n",[278,26951,26952],{"class":280,"line":316},[278,26953,26954],{},"        if err := contents.ToJSON(&config); err != nil {\n",[278,26956,26957],{"class":280,"line":322},[278,26958,26959],{},"            out.AddError(err)\n",[278,26961,26962],{"class":280,"line":327},[278,26963,26964],{},"            return\n",[278,26966,26967],{"class":280,"line":340},[278,26968,6954],{},[278,26970,26971],{"class":280,"line":349},[278,26972,292],{"emptyLinePlaceholder":291},[278,26974,26975],{"class":280,"line":375},[278,26976,26977],{},"        fmt.Println(\"Printing the imported file\")\n",[278,26979,26980],{"class":280,"line":386},[278,26981,26982],{},"        fmt.Println(config)\n",[278,26984,26985],{"class":280,"line":397},[278,26986,292],{"emptyLinePlaceholder":291},[278,26988,26989],{"class":280,"line":408},[278,26990,26991],{},"        if config.APIKey == \"\" {\n",[278,26993,26994],{"class":280,"line":433},[278,26995,26964],{},[278,26997,26998],{"class":280,"line":454},[278,26999,6954],{},[278,27001,27002],{"class":280,"line":475},[278,27003,292],{"emptyLinePlaceholder":291},[278,27005,27006],{"class":280,"line":496},[278,27007,27008],{},"        if config.Endpoint == \"\" {\n",[278,27010,27011],{"class":280,"line":505},[278,27012,26964],{},[278,27014,27015],{"class":280,"line":516},[278,27016,6954],{},[278,27018,27019],{"class":280,"line":527},[278,27020,292],{"emptyLinePlaceholder":291},[278,27022,27023],{"class":280,"line":533},[278,27024,27025],{},"        out.AddCandidate(sdk.ImportCandidate{\n",[278,27027,27028],{"class":280,"line":539},[278,27029,27030],{},"            Fields: map[sdk.FieldName]string{\n",[278,27032,27033],{"class":280,"line":545},[278,27034,27035],{},"                fieldname.APIKey:   config.APIKey,\n",[278,27037,27038],{"class":280,"line":551},[278,27039,27040],{},"                fieldname.Endpoint: config.Endpoint,\n",[278,27042,27043],{"class":280,"line":557},[278,27044,26697],{},[278,27046,27047],{"class":280,"line":567},[278,27048,27049],{},"        })\n",[278,27051,27052],{"class":280,"line":577},[278,27053,27054],{},"    })\n",[278,27056,27057],{"class":280,"line":587},[278,27058,617],{},[11,27060,27061,27062,26634],{},"That's it. We're done. The only thing remaining is writing some tests, and configuring which appwrite commands don't require auth. The latter can be done in ",[59,27063,27064],{},"appwrite.go",[269,27066,27068],{"className":26318,"code":27067,"language":26320,"meta":274,"style":274},"NeedsAuth: needsauth.IfAll(\n    needsauth.NotForHelpOrVersion(),\n    needsauth.NotWithoutArgs(),\n    needsauth.NotWhenContainsArgs(\"client\"),\n    needsauth.NotWhenContainsArgs(\"login\"),\n    needsauth.NotWhenContainsArgs(\"logout\"),\n    needsauth.NotForExactArgs(\"deploy\"),\n    needsauth.NotForExactArgs(\"projects\"),\n    needsauth.NotForExactArgs(\"storage\"),\n    needsauth.NotForExactArgs(\"teams\"),\n    needsauth.NotForExactArgs(\"users\"),\n    needsauth.NotForExactArgs(\"account\"),\n    needsauth.NotForExactArgs(\"avatars\"),\n    needsauth.NotForExactArgs(\"functions\"),\n    needsauth.NotForExactArgs(\"databases\"),\n    needsauth.NotForExactArgs(\"health\"),\n    needsauth.NotForExactArgs(\"locale\"),\n),\n",[59,27069,27070,27075,27080,27085,27090,27095,27100,27105,27110,27115,27120,27125,27130,27135,27140,27145,27150,27155],{"__ignoreMap":274},[278,27071,27072],{"class":280,"line":281},[278,27073,27074],{},"NeedsAuth: needsauth.IfAll(\n",[278,27076,27077],{"class":280,"line":288},[278,27078,27079],{},"    needsauth.NotForHelpOrVersion(),\n",[278,27081,27082],{"class":280,"line":295},[278,27083,27084],{},"    needsauth.NotWithoutArgs(),\n",[278,27086,27087],{"class":280,"line":316},[278,27088,27089],{},"    needsauth.NotWhenContainsArgs(\"client\"),\n",[278,27091,27092],{"class":280,"line":322},[278,27093,27094],{},"    needsauth.NotWhenContainsArgs(\"login\"),\n",[278,27096,27097],{"class":280,"line":327},[278,27098,27099],{},"    needsauth.NotWhenContainsArgs(\"logout\"),\n",[278,27101,27102],{"class":280,"line":340},[278,27103,27104],{},"    needsauth.NotForExactArgs(\"deploy\"),\n",[278,27106,27107],{"class":280,"line":349},[278,27108,27109],{},"    needsauth.NotForExactArgs(\"projects\"),\n",[278,27111,27112],{"class":280,"line":375},[278,27113,27114],{},"    needsauth.NotForExactArgs(\"storage\"),\n",[278,27116,27117],{"class":280,"line":386},[278,27118,27119],{},"    needsauth.NotForExactArgs(\"teams\"),\n",[278,27121,27122],{"class":280,"line":397},[278,27123,27124],{},"    needsauth.NotForExactArgs(\"users\"),\n",[278,27126,27127],{"class":280,"line":408},[278,27128,27129],{},"    needsauth.NotForExactArgs(\"account\"),\n",[278,27131,27132],{"class":280,"line":433},[278,27133,27134],{},"    needsauth.NotForExactArgs(\"avatars\"),\n",[278,27136,27137],{"class":280,"line":454},[278,27138,27139],{},"    needsauth.NotForExactArgs(\"functions\"),\n",[278,27141,27142],{"class":280,"line":475},[278,27143,27144],{},"    needsauth.NotForExactArgs(\"databases\"),\n",[278,27146,27147],{"class":280,"line":496},[278,27148,27149],{},"    needsauth.NotForExactArgs(\"health\"),\n",[278,27151,27152],{"class":280,"line":505},[278,27153,27154],{},"    needsauth.NotForExactArgs(\"locale\"),\n",[278,27156,27157],{"class":280,"line":516},[278,27158,4704],{},[11,27160,27161,27162,10991],{},"The plugin tests can be modified in ",[59,27163,27164],{},"api_key_test.go",[269,27166,27168],{"className":26318,"code":27167,"language":26320,"meta":274,"style":274},"\u002F\u002F Test whether our file provision is working\nfunc TestAPIKeyProvisioner(t *testing.T) {\n    plugintest.TestProvisioner(t, APIKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{\n        \"temp file\": {\n            ItemFields: map[sdk.FieldName]string{\n                fieldname.APIKey:   \"zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample\",\n                fieldname.Endpoint: \"http:\u002F\u002Flocalhost\u002Fv1\",\n            },\n            ExpectedOutput: sdk.ProvisionOutput{\n                Files: map[string]sdk.OutputFile{\n                    ConfigPath(): {\n                        Contents: []byte(plugintest.LoadFixture(t, \"import_prefs.json\")),\n                    },\n                },\n            },\n        },\n    })\n}\n\n\u002F\u002F Test the importer\nfunc TestAPIKeyImporter(t *testing.T) {\n    plugintest.TestImporter(t, APIKey().Importer, map[string]plugintest.ImportCase{\n        \"Appwrite prefs file\": {\n            Files: map[string]string{\n                ConfigPath(): plugintest.LoadFixture(t, \"import_prefs.json\"),\n            },\n            ExpectedCandidates: []sdk.ImportCandidate{\n                {\n                    Fields: map[sdk.FieldName]string{\n                        fieldname.APIKey:   \"zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample\",\n                        fieldname.Endpoint: \"http:\u002F\u002Flocalhost\u002Fv1\",\n                    },\n                },\n            },\n        },\n    })\n}\n",[59,27169,27170,27175,27180,27185,27190,27195,27200,27205,27209,27214,27219,27224,27229,27234,27239,27243,27247,27251,27255,27259,27264,27269,27274,27279,27284,27289,27293,27298,27303,27308,27313,27318,27322,27326,27330,27334,27338],{"__ignoreMap":274},[278,27171,27172],{"class":280,"line":281},[278,27173,27174],{},"\u002F\u002F Test whether our file provision is working\n",[278,27176,27177],{"class":280,"line":288},[278,27178,27179],{},"func TestAPIKeyProvisioner(t *testing.T) {\n",[278,27181,27182],{"class":280,"line":295},[278,27183,27184],{},"    plugintest.TestProvisioner(t, APIKey().DefaultProvisioner, map[string]plugintest.ProvisionCase{\n",[278,27186,27187],{"class":280,"line":316},[278,27188,27189],{},"        \"temp file\": {\n",[278,27191,27192],{"class":280,"line":322},[278,27193,27194],{},"            ItemFields: map[sdk.FieldName]string{\n",[278,27196,27197],{"class":280,"line":327},[278,27198,27199],{},"                fieldname.APIKey:   \"zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample\",\n",[278,27201,27202],{"class":280,"line":340},[278,27203,27204],{},"                fieldname.Endpoint: \"http:\u002F\u002Flocalhost\u002Fv1\",\n",[278,27206,27207],{"class":280,"line":349},[278,27208,26697],{},[278,27210,27211],{"class":280,"line":375},[278,27212,27213],{},"            ExpectedOutput: sdk.ProvisionOutput{\n",[278,27215,27216],{"class":280,"line":386},[278,27217,27218],{},"                Files: map[string]sdk.OutputFile{\n",[278,27220,27221],{"class":280,"line":397},[278,27222,27223],{},"                    ConfigPath(): {\n",[278,27225,27226],{"class":280,"line":408},[278,27227,27228],{},"                        Contents: []byte(plugintest.LoadFixture(t, \"import_prefs.json\")),\n",[278,27230,27231],{"class":280,"line":433},[278,27232,27233],{},"                    },\n",[278,27235,27236],{"class":280,"line":454},[278,27237,27238],{},"                },\n",[278,27240,27241],{"class":280,"line":475},[278,27242,26697],{},[278,27244,27245],{"class":280,"line":496},[278,27246,2606],{},[278,27248,27249],{"class":280,"line":505},[278,27250,27054],{},[278,27252,27253],{"class":280,"line":516},[278,27254,617],{},[278,27256,27257],{"class":280,"line":527},[278,27258,292],{"emptyLinePlaceholder":291},[278,27260,27261],{"class":280,"line":533},[278,27262,27263],{},"\u002F\u002F Test the importer\n",[278,27265,27266],{"class":280,"line":539},[278,27267,27268],{},"func TestAPIKeyImporter(t *testing.T) {\n",[278,27270,27271],{"class":280,"line":545},[278,27272,27273],{},"    plugintest.TestImporter(t, APIKey().Importer, map[string]plugintest.ImportCase{\n",[278,27275,27276],{"class":280,"line":551},[278,27277,27278],{},"        \"Appwrite prefs file\": {\n",[278,27280,27281],{"class":280,"line":557},[278,27282,27283],{},"            Files: map[string]string{\n",[278,27285,27286],{"class":280,"line":567},[278,27287,27288],{},"                ConfigPath(): plugintest.LoadFixture(t, \"import_prefs.json\"),\n",[278,27290,27291],{"class":280,"line":577},[278,27292,26697],{},[278,27294,27295],{"class":280,"line":587},[278,27296,27297],{},"            ExpectedCandidates: []sdk.ImportCandidate{\n",[278,27299,27300],{"class":280,"line":597},[278,27301,27302],{},"                {\n",[278,27304,27305],{"class":280,"line":608},[278,27306,27307],{},"                    Fields: map[sdk.FieldName]string{\n",[278,27309,27310],{"class":280,"line":614},[278,27311,27312],{},"                        fieldname.APIKey:   \"zsaugacpwq6k54nnbdbmh1cys98u2a32qqkacma2ioxn1e2j6eyrk9urom0vzcvm6qbbm8s6l4xbm86n37foauiqba9tlcvohuoz87j7nwvpob5wr71k58i105fn39a10vj7ob84opwf1vrfat3m8konch7xxy2z2dh1ykohdbef7xgmvtn82lebe4mzmfzoylqy4jslrok11zbjtmd6xs84ukd7b1k9ofyuanvinmlhkgua32p5x0gqbexample\",\n",[278,27314,27315],{"class":280,"line":620},[278,27316,27317],{},"                        fieldname.Endpoint: \"http:\u002F\u002Flocalhost\u002Fv1\",\n",[278,27319,27320],{"class":280,"line":625},[278,27321,27233],{},[278,27323,27324],{"class":280,"line":640},[278,27325,27238],{},[278,27327,27328],{"class":280,"line":663},[278,27329,26697],{},[278,27331,27332],{"class":280,"line":669},[278,27333,2606],{},[278,27335,27336],{"class":280,"line":680},[278,27337,27054],{},[278,27339,27340],{"class":280,"line":686},[278,27341,617],{},[11,27343,27344,27345,179,27348,27351],{},"To run the tests we can run ",[59,27346,27347],{},"gmake test",[59,27349,27350],{},"make test"," command.",[11,27353,27354],{},[3135,27355],{"alt":274,"src":27356},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F263bfdc5-70cd-4580-8d52-5c2045295ddb-762413a54b.png",[11,27358,27359],{},"And this gives us the green light we needed.",[11,27361,27362],{},[3135,27363],{"alt":274,"src":27364},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FeIG0HfouRQJQr1wBzz\u002Fgiphy.gif",[24,27366,27368],{"id":27367},"part-2-creating-the-app","Part 2: Creating the app",[11,27370,27371],{},"We start with the basics here. Scaffold the app using the below command",[269,27373,27375],{"className":3335,"code":27374,"language":3337,"meta":274,"style":274},"npx nuxi@latest init \u003Cproject-name>\n",[59,27376,27377],{"__ignoreMap":274},[278,27378,27379,27381,27384,27386,27388,27391,27393],{"class":280,"line":281},[278,27380,3349],{"class":333},[278,27382,27383],{"class":309}," nuxi@latest",[278,27385,3355],{"class":309},[278,27387,24568],{"class":298},[278,27389,27390],{"class":309},"project-nam",[278,27392,6504],{"class":302},[278,27394,372],{"class":298},[11,27396,27397,27398,27401,27402,27405],{},"Since we're using ",[59,27399,27400],{},"Passage"," for auth, install ",[59,27403,27404],{},"@passageidentity\u002Fpassage-elements",". Now here is a catch, to make use of the appwrite client SDK we need to create a session using email\u002Fpassword or through supported OAuth providers. Both of these are ruled out as passage auth doesn't need a password, and is also not a supported OAuth provider. I still installed the appwrite SDK as I used its locale services in my app flow.",[269,27407,27409],{"className":5690,"code":27408,"language":1310,"meta":274,"style":274},"yarn add @passageidentity\u002Fpassage-elements appwrite\n",[59,27410,27411],{"__ignoreMap":274},[278,27412,27413],{"class":280,"line":281},[278,27414,27408],{"class":302},[11,27416,27417,27418,27421,27422,27425],{},"We definitely need the server SDKs of both of the above. ",[59,27419,27420],{},"@passageidentity\u002Fpassage-node"," for verifying the authenticity of incoming client requests, and ",[59,27423,27424],{},"node-appwrite"," for interacting with the appwrite database.",[269,27427,27429],{"className":5690,"code":27428,"language":1310,"meta":274,"style":274},"yarn add node-appwrite @passageidentity\u002Fpassage-node\n",[59,27430,27431],{"__ignoreMap":274},[278,27432,27433],{"class":280,"line":281},[278,27434,27428],{"class":302},[11,27436,27437,27438,27443],{},"For styling and readymade components, I decided to use ",[47,27439,27442],{"href":27440,"rel":27441},"https:\u002F\u002Fui.nuxtlabs.com\u002F",[51],"NuxtLabs UI",". Now we're all set to start writing the app.",[32,27445,27447],{"id":27446},"passage-auth","Passage Auth",[11,27449,27450,27451,27454,27455,10991],{},"Passage provides readymade custom web components which handle the end-to-end auth for you. To use it in a Nuxt (or Vue) app, we need to register these components as custom elements in the Nuxt config file. If we do not do that then we'll get warnings in our browser console. Add the following inside the ",[59,27452,27453],{},"defineNuxtConfig"," input object in your ",[59,27456,3490],{},[269,27458,27460],{"className":271,"code":27459,"language":273,"meta":274,"style":274},"vue: {\n  compilerOptions: {\n    isCustomElement: (tag) => tag.startsWith('passage-'),\n  },\n},\n",[59,27461,27462,27469,27476,27502,27506],{"__ignoreMap":274},[278,27463,27464,27467],{"class":280,"line":281},[278,27465,27466],{"class":333},"vue",[278,27468,5706],{"class":302},[278,27470,27471,27474],{"class":280,"line":288},[278,27472,27473],{"class":333},"  compilerOptions",[278,27475,5706],{"class":302},[278,27477,27478,27481,27483,27486,27488,27490,27493,27495,27497,27500],{"class":280,"line":295},[278,27479,27480],{"class":333},"    isCustomElement",[278,27482,5525],{"class":302},[278,27484,27485],{"class":501},"tag",[278,27487,1845],{"class":302},[278,27489,1848],{"class":298},[278,27491,27492],{"class":302}," tag.",[278,27494,15615],{"class":333},[278,27496,1126],{"class":302},[278,27498,27499],{"class":309},"'passage-'",[278,27501,4704],{"class":302},[278,27503,27504],{"class":280,"line":316},[278,27505,683],{"class":302},[278,27507,27508],{"class":280,"line":322},[278,27509,11040],{"class":302},[11,27511,27512,27513,27516],{},"Now I tried to use the ",[59,27514,27515],{},"\u003Cpassage-auth>"," component directly in a page template after adding the necessary import",[269,27518,27520],{"className":271,"code":27519,"language":273,"meta":274,"style":274},"import '@passageidentity\u002Fpassage-elements\u002Fpassage-auth';\n",[59,27521,27522],{"__ignoreMap":274},[278,27523,27524,27526,27529],{"class":280,"line":281},[278,27525,299],{"class":298},[278,27527,27528],{"class":309}," '@passageidentity\u002Fpassage-elements\u002Fpassage-auth'",[278,27530,313],{"class":302},[11,27532,27533],{},"But it doesn't work, you'll get the below error",[269,27535,27537],{"className":3335,"code":27536,"language":3337,"meta":274,"style":274},"Cannot use import statement outside a module\n",[59,27538,27539],{"__ignoreMap":274},[278,27540,27541,27544,27547,27550,27553,27556,27559],{"class":280,"line":281},[278,27542,27543],{"class":333},"Cannot",[278,27545,27546],{"class":309}," use",[278,27548,27549],{"class":309}," import",[278,27551,27552],{"class":309}," statement",[278,27554,27555],{"class":309}," outside",[278,27557,27558],{"class":309}," a",[278,27560,27561],{"class":309}," module\n",[11,27563,27564,27565,27570,27571,27573],{},"This happens because Nuxt is running in SSR mode and web components are not available server side. You can get more information on this passage documentation for ",[47,27566,27569],{"href":27567,"rel":27568},"https:\u002F\u002Fdocs.passage.id\u002Ffrontend\u002Fexamples-by-framework\u002Fnext.js#:~:text=Calling%20the%20import,error%20being%20thrown.",[51],"NextJs here",". I tried following the same approach outlined in the link, load the component client side in the ",[59,27572,22215],{}," hook",[269,27575,27577],{"className":271,"code":27576,"language":273,"meta":274,"style":274},"onMounted(()=>{\n  require('@passageidentity\u002Fpassage-elements\u002Fpassage-auth');\n});\n",[59,27578,27579,27590,27602],{"__ignoreMap":274},[278,27580,27581,27583,27586,27588],{"class":280,"line":281},[278,27582,22215],{"class":333},[278,27584,27585],{"class":302},"(()",[278,27587,1848],{"class":298},[278,27589,524],{"class":302},[278,27591,27592,27595,27597,27600],{"class":280,"line":288},[278,27593,27594],{"class":333},"  require",[278,27596,1126],{"class":302},[278,27598,27599],{"class":309},"'@passageidentity\u002Fpassage-elements\u002Fpassage-auth'",[278,27601,1280],{"class":302},[278,27603,27604],{"class":280,"line":295},[278,27605,3693],{"class":302},[11,27607,27608,27609,27612,27613,27616,27617,27619,27620,27622,27623,27625,27626,183],{},"But sadly this also is a no-go. Nuxt3 ships with ",[59,27610,27611],{},"Vite"," bundler by default, and using ",[59,27614,27615],{},"require"," is not supported in ",[59,27618,27611],{},". No issues, simply replace ",[59,27621,27615],{}," with ",[59,27624,299],{}," there you might say, but we get typescript error ",[59,27627,27628],{},"\"An import declaration can only be used at the top level of a namespace or module\"",[11,27630,27631],{},[3135,27632],{"alt":274,"src":27633},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F4529834c-8f69-4acc-b186-06affe2b9394-3ce3d48348.png",[11,27635,27636],{},"So what is the solution? To resolve this I created separate components which only contain passage elements, and load this new component only on the client side using the \u003CClientOnly> option provided by Nuxt.",[11,27638,27639],{},"So a SignUp component might look like this",[269,27641,27643],{"className":7132,"code":27642,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003Cpassage-register :app-id=\"passageAppId\" \u002F>\n\u003C\u002Ftemplate>\n",[59,27644,27645,27649,27654],{"__ignoreMap":274},[278,27646,27647],{"class":280,"line":281},[278,27648,7146],{},[278,27650,27651],{"class":280,"line":288},[278,27652,27653],{},"  \u003Cpassage-register :app-id=\"passageAppId\" \u002F>\n",[278,27655,27656],{"class":280,"line":295},[278,27657,7422],{},[11,27659,27660,27661,27664],{},"And then you use this component on a ",[59,27662,27663],{},"sign-up"," page as shown below",[269,27666,27668],{"className":7132,"code":27667,"language":7134,"meta":274,"style":274},"\u003Ctemplate>\n  \u003CClientOnly>\n    \u003CUContainer>\n      \u003CUCard class=\"max-w-md mx-auto mt-8 text-center\">\n        \u003Ch1 class=\"text-3xl font-medium\">Sign up\u003C\u002Fh1>\n\n        \u003Csign-up \u002F>\n\n        \u003Cdiv\n          class=\"text-sm font-medium text-gray-500 dark:text-gray-300 text-center\"\n        >\n          Already have an account?\n          \u003CUButton variant=\"link\" :padded=\"false\" to=\"\u002Fsign-in\">\n            Sign in\n          \u003C\u002FUButton>\n        \u003C\u002Fdiv>\n      \u003C\u002FUCard>\n    \u003C\u002FUContainer>\n  \u003C\u002FClientOnly>\n\u003C\u002Ftemplate>\n",[59,27669,27670,27674,27678,27683,27688,27693,27697,27702,27706,27711,27716,27721,27726,27731,27736,27741,27746,27750,27755,27759],{"__ignoreMap":274},[278,27671,27672],{"class":280,"line":281},[278,27673,7146],{},[278,27675,27676],{"class":280,"line":288},[278,27677,22494],{},[278,27679,27680],{"class":280,"line":295},[278,27681,27682],{},"    \u003CUContainer>\n",[278,27684,27685],{"class":280,"line":316},[278,27686,27687],{},"      \u003CUCard class=\"max-w-md mx-auto mt-8 text-center\">\n",[278,27689,27690],{"class":280,"line":322},[278,27691,27692],{},"        \u003Ch1 class=\"text-3xl font-medium\">Sign up\u003C\u002Fh1>\n",[278,27694,27695],{"class":280,"line":327},[278,27696,292],{"emptyLinePlaceholder":291},[278,27698,27699],{"class":280,"line":340},[278,27700,27701],{},"        \u003Csign-up \u002F>\n",[278,27703,27704],{"class":280,"line":349},[278,27705,292],{"emptyLinePlaceholder":291},[278,27707,27708],{"class":280,"line":375},[278,27709,27710],{},"        \u003Cdiv\n",[278,27712,27713],{"class":280,"line":386},[278,27714,27715],{},"          class=\"text-sm font-medium text-gray-500 dark:text-gray-300 text-center\"\n",[278,27717,27718],{"class":280,"line":397},[278,27719,27720],{},"        >\n",[278,27722,27723],{"class":280,"line":408},[278,27724,27725],{},"          Already have an account?\n",[278,27727,27728],{"class":280,"line":433},[278,27729,27730],{},"          \u003CUButton variant=\"link\" :padded=\"false\" to=\"\u002Fsign-in\">\n",[278,27732,27733],{"class":280,"line":454},[278,27734,27735],{},"            Sign in\n",[278,27737,27738],{"class":280,"line":475},[278,27739,27740],{},"          \u003C\u002FUButton>\n",[278,27742,27743],{"class":280,"line":496},[278,27744,27745],{},"        \u003C\u002Fdiv>\n",[278,27747,27748],{"class":280,"line":505},[278,27749,7279],{},[278,27751,27752],{"class":280,"line":516},[278,27753,27754],{},"    \u003C\u002FUContainer>\n",[278,27756,27757],{"class":280,"line":527},[278,27758,22551],{},[278,27760,27761],{"class":280,"line":533},[278,27762,7422],{},[11,27764,27765],{},"After making the above adjustments it works all right.",[3300,27767,27768,27770],{"dataNodeType":3302},[3300,27769,3785],{"dataNodeType":3305},[3300,27771,27772,27773,919,27776,27779,27780,27782],{"dataNodeType":3309},"Please note that I'm using ",[59,27774,27775],{},"\u003Cpassage-register>",[59,27777,27778],{},"\u003Cpassage-login>"," components instead of the ",[59,27781,27515],{}," because I've some custom registration fields.",[32,27784,27786],{"id":27785},"creating-user-family-accounts","Creating user & family accounts",[11,27788,27789],{},"Since we want to be able to invite family members (or create accounts for them ourselves) to the app, we need to roll out our own database schema to connect their accounts. But appwrite supports the creation of Teams and linking user accounts to them. To use this feature we will need to create appwrite users and then link those users to a team. How do we correlate the Passage users to the appwrite users?",[11,27791,27792,27793,27796,27797,27800,27801,27804,27805,183],{},"Passage allows us to listen to new account creations or a fresh login, by attaching a callback (",[59,27794,27795],{},"onSuccess",") to passage elements. But there is a catch, we can't simply bind the callback to the pasasge element using the ",[59,27798,27799],{},"\"@\""," syntax of vue. I needed to use the ",[59,27802,27803],{},"\".\""," syntax to make it work. You can read more about this ",[47,27806,3286],{"href":27807,"rel":27808},"https:\u002F\u002Fvuejs.org\u002Fguide\u002Fextras\u002Fweb-components.html#passing-dom-properties",[51],[11,27810,27811],{},"This is my final SignUp component which uses the passage callback to make an API call to a Nuxt serverless API",[269,27813,27815],{"className":271,"code":27814,"language":273,"meta":274,"style":274},"\u003Cscript setup lang=\"ts\">\nimport '@passageidentity\u002Fpassage-elements\u002Fpassage-register';\nimport { authResult } from '@passageidentity\u002Fpassage-elements';\nconst { getUser } = usePassageUser();\n\nconst {\n  public: { passageAppId },\n} = useRuntimeConfig();\n\nconst onRegistrationDone = async (authResult: authResult) => {\n  try {\n    const res = await $fetch('\u002Fapi\u002Fusers', {\n      method: 'post',\n    });\n\n    console.log('got response from user create', res);\n    await getUser(authResult.auth_token);\n    navigateTo('\u002Fonboarding');\n  } catch (error) {\n    console.log('failed to create user in appwrite');\n  }\n};\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cpassage-register :app-id=\"passageAppId\" .onSuccess=\"onRegistrationDone\" \u002F>\n\u003C\u002Ftemplate>\n",[59,27816,27817,27831,27840,27854,27872,27876,27882,27895,27906,27910,27937,27943,27962,27971,27975,27979,27992,28002,28014,28022,28035,28039,28043,28052,28056,28065,28101],{"__ignoreMap":274},[278,27818,27819,27821,27824,27826,27829],{"class":280,"line":281},[278,27820,1702],{"class":298},[278,27822,27823],{"class":302},"script setup lang",[278,27825,358],{"class":298},[278,27827,27828],{"class":309},"\"ts\"",[278,27830,372],{"class":298},[278,27832,27833,27835,27838],{"class":280,"line":288},[278,27834,299],{"class":298},[278,27836,27837],{"class":309}," '@passageidentity\u002Fpassage-elements\u002Fpassage-register'",[278,27839,313],{"class":302},[278,27841,27842,27844,27847,27849,27852],{"class":280,"line":295},[278,27843,299],{"class":298},[278,27845,27846],{"class":302}," { authResult } ",[278,27848,306],{"class":298},[278,27850,27851],{"class":309}," '@passageidentity\u002Fpassage-elements'",[278,27853,313],{"class":302},[278,27855,27856,27858,27860,27863,27865,27867,27870],{"class":280,"line":316},[278,27857,5416],{"class":298},[278,27859,1009],{"class":302},[278,27861,27862],{"class":650},"getUser",[278,27864,1029],{"class":302},[278,27866,358],{"class":298},[278,27868,27869],{"class":333}," usePassageUser",[278,27871,1313],{"class":302},[278,27873,27874],{"class":280,"line":322},[278,27875,292],{"emptyLinePlaceholder":291},[278,27877,27878,27880],{"class":280,"line":327},[278,27879,5416],{"class":298},[278,27881,876],{"class":302},[278,27883,27884,27887,27890,27893],{"class":280,"line":340},[278,27885,27886],{"class":501},"  public",[278,27888,27889],{"class":302},": { ",[278,27891,27892],{"class":650},"passageAppId",[278,27894,3547],{"class":302},[278,27896,27897,27899,27901,27904],{"class":280,"line":349},[278,27898,12323],{"class":302},[278,27900,358],{"class":298},[278,27902,27903],{"class":333}," useRuntimeConfig",[278,27905,1313],{"class":302},[278,27907,27908],{"class":280,"line":375},[278,27909,292],{"emptyLinePlaceholder":291},[278,27911,27912,27914,27917,27919,27921,27923,27926,27928,27931,27933,27935],{"class":280,"line":386},[278,27913,5416],{"class":298},[278,27915,27916],{"class":333}," onRegistrationDone",[278,27918,764],{"class":298},[278,27920,2325],{"class":298},[278,27922,1245],{"class":302},[278,27924,27925],{"class":501},"authResult",[278,27927,960],{"class":298},[278,27929,27930],{"class":333}," authResult",[278,27932,1845],{"class":302},[278,27934,1848],{"class":298},[278,27936,876],{"class":302},[278,27938,27939,27941],{"class":280,"line":397},[278,27940,1105],{"class":298},[278,27942,876],{"class":302},[278,27944,27945,27947,27949,27951,27953,27955,27957,27960],{"class":280,"line":408},[278,27946,1112],{"class":298},[278,27948,17866],{"class":650},[278,27950,764],{"class":298},[278,27952,1120],{"class":298},[278,27954,15262],{"class":333},[278,27956,1126],{"class":302},[278,27958,27959],{"class":309},"'\u002Fapi\u002Fusers'",[278,27961,1132],{"class":302},[278,27963,27964,27966,27969],{"class":280,"line":433},[278,27965,1137],{"class":302},[278,27967,27968],{"class":309},"'post'",[278,27970,660],{"class":302},[278,27972,27973],{"class":280,"line":454},[278,27974,1233],{"class":302},[278,27976,27977],{"class":280,"line":475},[278,27978,292],{"emptyLinePlaceholder":291},[278,27980,27981,27983,27985,27987,27990],{"class":280,"line":496},[278,27982,1409],{"class":302},[278,27984,14851],{"class":333},[278,27986,1126],{"class":302},[278,27988,27989],{"class":309},"'got response from user create'",[278,27991,17894],{"class":302},[278,27993,27994,27996,27999],{"class":280,"line":505},[278,27995,5077],{"class":298},[278,27997,27998],{"class":333}," getUser",[278,28000,28001],{"class":302},"(authResult.auth_token);\n",[278,28003,28004,28007,28009,28012],{"class":280,"line":516},[278,28005,28006],{"class":333},"    navigateTo",[278,28008,1126],{"class":302},[278,28010,28011],{"class":309},"'\u002Fonboarding'",[278,28013,1280],{"class":302},[278,28015,28016,28018,28020],{"class":280,"line":527},[278,28017,1397],{"class":302},[278,28019,1400],{"class":298},[278,28021,1403],{"class":302},[278,28023,28024,28026,28028,28030,28033],{"class":280,"line":533},[278,28025,1409],{"class":302},[278,28027,14851],{"class":333},[278,28029,1126],{"class":302},[278,28031,28032],{"class":309},"'failed to create user in appwrite'",[278,28034,1280],{"class":302},[278,28036,28037],{"class":280,"line":539},[278,28038,1096],{"class":302},[278,28040,28041],{"class":280,"line":545},[278,28042,2817],{"class":302},[278,28044,28045,28047,28050],{"class":280,"line":551},[278,28046,16738],{"class":298},[278,28048,28049],{"class":302},"script",[278,28051,372],{"class":298},[278,28053,28054],{"class":280,"line":557},[278,28055,292],{"emptyLinePlaceholder":291},[278,28057,28058,28060,28063],{"class":280,"line":567},[278,28059,1702],{"class":302},[278,28061,28062],{"class":333},"template",[278,28064,372],{"class":302},[278,28066,28067,28069,28072,28074,28077,28080,28082,28085,28087,28090,28093,28095,28098],{"class":280,"line":577},[278,28068,16537],{"class":298},[278,28070,28071],{"class":302},"passage",[278,28073,16522],{"class":298},[278,28075,28076],{"class":333},"register",[278,28078,28079],{"class":302}," :app",[278,28081,16522],{"class":298},[278,28083,28084],{"class":302},"id",[278,28086,358],{"class":298},[278,28088,28089],{"class":309},"\"passageAppId\"",[278,28091,28092],{"class":302}," .onSuccess",[278,28094,358],{"class":298},[278,28096,28097],{"class":309},"\"onRegistrationDone\"",[278,28099,28100],{"class":298}," \u002F>\n",[278,28102,28103,28105,28107],{"class":280,"line":587},[278,28104,16738],{"class":298},[278,28106,28062],{"class":302},[278,28108,372],{"class":298},[11,28110,28111,28112,28115],{},"And this is the ",[59,28113,28114],{},"\u002Fapi\u002Fusers"," code. We receive the request from the client, verify its authenticity using the passage's node SDK, and then create a new user in appwrite using their node SDK.",[269,28117,28119],{"className":271,"code":28118,"language":273,"meta":274,"style":274},"import { UserObject } from '@passageidentity\u002Fpassage-node';\nimport { protectRoute } from '..\u002FusePassage';\nimport { useAppwrite } from '..\u002FuseAppwrite';\n\nexport default defineEventHandler(async (event) => {\n  console.log('incoming post event for api\u002Fusers\u002F', event);\n\n  await protectRoute(event);\n\n  const user = event.context.auth.user as UserObject;\n  console.log(`got some auth user:`, user);\n\n  const { $users } = useAppwrite();\n\n  \u002F\u002F create an appwrite user with the same ID returned by Passage\n  const res = await $users.create(\n    user.id,\n    user.email,\n    undefined,\n    undefined,\n    user.user_metadata?.name as string\n  );\n\n  console.log('res of user create', res);\n\n  return {\n    status: 'ok',\n  };\n});\n",[59,28120,28121,28135,28149,28163,28167,28189,28203,28207,28217,28221,28240,28254,28258,28276,28280,28285,28302,28307,28312,28319,28325,28334,28338,28342,28355,28359,28365,28375,28379],{"__ignoreMap":274},[278,28122,28123,28125,28128,28130,28133],{"class":280,"line":281},[278,28124,299],{"class":298},[278,28126,28127],{"class":302}," { UserObject } ",[278,28129,306],{"class":298},[278,28131,28132],{"class":309}," '@passageidentity\u002Fpassage-node'",[278,28134,313],{"class":302},[278,28136,28137,28139,28142,28144,28147],{"class":280,"line":288},[278,28138,299],{"class":298},[278,28140,28141],{"class":302}," { protectRoute } ",[278,28143,306],{"class":298},[278,28145,28146],{"class":309}," '..\u002FusePassage'",[278,28148,313],{"class":302},[278,28150,28151,28153,28156,28158,28161],{"class":280,"line":295},[278,28152,299],{"class":298},[278,28154,28155],{"class":302}," { useAppwrite } ",[278,28157,306],{"class":298},[278,28159,28160],{"class":309}," '..\u002FuseAppwrite'",[278,28162,313],{"class":302},[278,28164,28165],{"class":280,"line":316},[278,28166,292],{"emptyLinePlaceholder":291},[278,28168,28169,28171,28173,28175,28177,28179,28181,28183,28185,28187],{"class":280,"line":322},[278,28170,628],{"class":298},[278,28172,631],{"class":298},[278,28174,3878],{"class":333},[278,28176,1126],{"class":302},[278,28178,1050],{"class":298},[278,28180,1245],{"class":302},[278,28182,3887],{"class":501},[278,28184,1845],{"class":302},[278,28186,1848],{"class":298},[278,28188,876],{"class":302},[278,28190,28191,28193,28195,28197,28200],{"class":280,"line":327},[278,28192,17975],{"class":302},[278,28194,14851],{"class":333},[278,28196,1126],{"class":302},[278,28198,28199],{"class":309},"'incoming post event for api\u002Fusers\u002F'",[278,28201,28202],{"class":302},", event);\n",[278,28204,28205],{"class":280,"line":340},[278,28206,292],{"emptyLinePlaceholder":291},[278,28208,28209,28212,28215],{"class":280,"line":349},[278,28210,28211],{"class":298},"  await",[278,28213,28214],{"class":333}," protectRoute",[278,28216,3910],{"class":302},[278,28218,28219],{"class":280,"line":375},[278,28220,292],{"emptyLinePlaceholder":291},[278,28222,28223,28225,28228,28230,28233,28235,28238],{"class":280,"line":386},[278,28224,758],{"class":298},[278,28226,28227],{"class":650}," user",[278,28229,764],{"class":298},[278,28231,28232],{"class":302}," event.context.auth.user ",[278,28234,2937],{"class":298},[278,28236,28237],{"class":333}," UserObject",[278,28239,313],{"class":302},[278,28241,28242,28244,28246,28248,28251],{"class":280,"line":397},[278,28243,17975],{"class":302},[278,28245,14851],{"class":333},[278,28247,1126],{"class":302},[278,28249,28250],{"class":309},"`got some auth user:`",[278,28252,28253],{"class":302},", user);\n",[278,28255,28256],{"class":280,"line":408},[278,28257,292],{"emptyLinePlaceholder":291},[278,28259,28260,28262,28264,28267,28269,28271,28274],{"class":280,"line":433},[278,28261,758],{"class":298},[278,28263,1009],{"class":302},[278,28265,28266],{"class":650},"$users",[278,28268,1029],{"class":302},[278,28270,358],{"class":298},[278,28272,28273],{"class":333}," useAppwrite",[278,28275,1313],{"class":302},[278,28277,28278],{"class":280,"line":454},[278,28279,292],{"emptyLinePlaceholder":291},[278,28281,28282],{"class":280,"line":475},[278,28283,28284],{"class":284},"  \u002F\u002F create an appwrite user with the same ID returned by Passage\n",[278,28286,28287,28289,28291,28293,28295,28298,28300],{"class":280,"line":496},[278,28288,758],{"class":298},[278,28290,17866],{"class":650},[278,28292,764],{"class":298},[278,28294,1120],{"class":298},[278,28296,28297],{"class":302}," $users.",[278,28299,11913],{"class":333},[278,28301,770],{"class":302},[278,28303,28304],{"class":280,"line":505},[278,28305,28306],{"class":302},"    user.id,\n",[278,28308,28309],{"class":280,"line":516},[278,28310,28311],{"class":302},"    user.email,\n",[278,28313,28314,28317],{"class":280,"line":527},[278,28315,28316],{"class":650},"    undefined",[278,28318,660],{"class":302},[278,28320,28321,28323],{"class":280,"line":533},[278,28322,28316],{"class":650},[278,28324,660],{"class":302},[278,28326,28327,28330,28332],{"class":280,"line":539},[278,28328,28329],{"class":302},"    user.user_metadata?.name ",[278,28331,2937],{"class":298},[278,28333,17191],{"class":650},[278,28335,28336],{"class":280,"line":545},[278,28337,611],{"class":302},[278,28339,28340],{"class":280,"line":551},[278,28341,292],{"emptyLinePlaceholder":291},[278,28343,28344,28346,28348,28350,28353],{"class":280,"line":557},[278,28345,17975],{"class":302},[278,28347,14851],{"class":333},[278,28349,1126],{"class":302},[278,28351,28352],{"class":309},"'res of user create'",[278,28354,17894],{"class":302},[278,28356,28357],{"class":280,"line":567},[278,28358,292],{"emptyLinePlaceholder":291},[278,28360,28361,28363],{"class":280,"line":577},[278,28362,343],{"class":298},[278,28364,876],{"class":302},[278,28366,28367,28370,28373],{"class":280,"line":587},[278,28368,28369],{"class":302},"    status: ",[278,28371,28372],{"class":309},"'ok'",[278,28374,660],{"class":302},[278,28376,28377],{"class":280,"line":597},[278,28378,901],{"class":302},[278,28380,28381],{"class":280,"line":608},[278,28382,3693],{"class":302},[11,28384,28111,28385,28388],{},[59,28386,28387],{},"protectRoute"," function to verify the authenticity of the request",[269,28390,28392],{"className":271,"code":28391,"language":273,"meta":274,"style":274},"import { H3Event } from 'h3';\nimport Passage, { Metadata } from '@passageidentity\u002Fpassage-node';\n\nlet _passage: Passage | null = null;\n\nconst getPassage = () => {\n  if (!_passage) {\n    const {\n      passageApiKey,\n      public: { passageAppId },\n    } = useRuntimeConfig();\n\n    const passageConfig = {\n      appID: passageAppId,\n      apiKey: passageApiKey,\n    };\n\n    _passage = new Passage(passageConfig);\n  }\n\n  return _passage;\n};\n\nexport const protectRoute = async (event: H3Event) => {\n  const passage = getPassage();\n\n  try {\n    const userId = await passage.authenticateRequest(event.node.req);\n\n    if (userId) {\n      console.log('request authenticated', userId);\n\n      const user = await passage.user.get(userId);\n      event.context.auth = { user };\n \n      return;\n    }\n  } catch (error) {\n    console.log('failed to authenticate request', error);\n  }\n\n  throw createError({\n    statusCode: 401,\n    message: 'Unauthorized',\n  });\n};\n",[59,28393,28394,28408,28421,28425,28447,28451,28466,28477,28483,28490,28501,28511,28515,28526,28531,28536,28540,28544,28558,28562,28566,28573,28577,28581,28607,28620,28624,28630,28650,28654,28661,28675,28679,28697,28707,28712,28718,28722,28730,28743,28747,28751,28759,28769,28779,28783],{"__ignoreMap":274},[278,28395,28396,28398,28401,28403,28406],{"class":280,"line":281},[278,28397,299],{"class":298},[278,28399,28400],{"class":302}," { H3Event } ",[278,28402,306],{"class":298},[278,28404,28405],{"class":309}," 'h3'",[278,28407,313],{"class":302},[278,28409,28410,28412,28415,28417,28419],{"class":280,"line":288},[278,28411,299],{"class":298},[278,28413,28414],{"class":302}," Passage, { Metadata } ",[278,28416,306],{"class":298},[278,28418,28132],{"class":309},[278,28420,313],{"class":302},[278,28422,28423],{"class":280,"line":295},[278,28424,292],{"emptyLinePlaceholder":291},[278,28426,28427,28429,28432,28434,28437,28439,28441,28443,28445],{"class":280,"line":316},[278,28428,1001],{"class":298},[278,28430,28431],{"class":302}," _passage",[278,28433,960],{"class":298},[278,28435,28436],{"class":333}," Passage",[278,28438,1621],{"class":298},[278,28440,1035],{"class":650},[278,28442,764],{"class":298},[278,28444,1035],{"class":650},[278,28446,313],{"class":302},[278,28448,28449],{"class":280,"line":322},[278,28450,292],{"emptyLinePlaceholder":291},[278,28452,28453,28455,28458,28460,28462,28464],{"class":280,"line":327},[278,28454,5416],{"class":298},[278,28456,28457],{"class":333}," getPassage",[278,28459,764],{"class":298},[278,28461,5860],{"class":302},[278,28463,1848],{"class":298},[278,28465,876],{"class":302},[278,28467,28468,28470,28472,28474],{"class":280,"line":340},[278,28469,1062],{"class":298},[278,28471,1245],{"class":302},[278,28473,1209],{"class":298},[278,28475,28476],{"class":302},"_passage) {\n",[278,28478,28479,28481],{"class":280,"line":349},[278,28480,1112],{"class":298},[278,28482,876],{"class":302},[278,28484,28485,28488],{"class":280,"line":375},[278,28486,28487],{"class":650},"      passageApiKey",[278,28489,660],{"class":302},[278,28491,28492,28495,28497,28499],{"class":280,"line":386},[278,28493,28494],{"class":501},"      public",[278,28496,27889],{"class":302},[278,28498,27892],{"class":650},[278,28500,3547],{"class":302},[278,28502,28503,28505,28507,28509],{"class":280,"line":397},[278,28504,6636],{"class":302},[278,28506,358],{"class":298},[278,28508,27903],{"class":333},[278,28510,1313],{"class":302},[278,28512,28513],{"class":280,"line":408},[278,28514,292],{"emptyLinePlaceholder":291},[278,28516,28517,28519,28522,28524],{"class":280,"line":433},[278,28518,1112],{"class":298},[278,28520,28521],{"class":650}," passageConfig",[278,28523,764],{"class":298},[278,28525,876],{"class":302},[278,28527,28528],{"class":280,"line":454},[278,28529,28530],{"class":302},"      appID: passageAppId,\n",[278,28532,28533],{"class":280,"line":475},[278,28534,28535],{"class":302},"      apiKey: passageApiKey,\n",[278,28537,28538],{"class":280,"line":496},[278,28539,1378],{"class":302},[278,28541,28542],{"class":280,"line":505},[278,28543,292],{"emptyLinePlaceholder":291},[278,28545,28546,28549,28551,28553,28555],{"class":280,"line":516},[278,28547,28548],{"class":302},"    _passage ",[278,28550,358],{"class":298},[278,28552,1258],{"class":298},[278,28554,28436],{"class":333},[278,28556,28557],{"class":302},"(passageConfig);\n",[278,28559,28560],{"class":280,"line":527},[278,28561,1096],{"class":302},[278,28563,28564],{"class":280,"line":533},[278,28565,292],{"emptyLinePlaceholder":291},[278,28567,28568,28570],{"class":280,"line":539},[278,28569,343],{"class":298},[278,28571,28572],{"class":302}," _passage;\n",[278,28574,28575],{"class":280,"line":545},[278,28576,2817],{"class":302},[278,28578,28579],{"class":280,"line":551},[278,28580,292],{"emptyLinePlaceholder":291},[278,28582,28583,28585,28587,28589,28591,28593,28595,28597,28599,28601,28603,28605],{"class":280,"line":557},[278,28584,628],{"class":298},[278,28586,4559],{"class":298},[278,28588,28214],{"class":333},[278,28590,764],{"class":298},[278,28592,2325],{"class":298},[278,28594,1245],{"class":302},[278,28596,3887],{"class":501},[278,28598,960],{"class":298},[278,28600,11852],{"class":333},[278,28602,1845],{"class":302},[278,28604,1848],{"class":298},[278,28606,876],{"class":302},[278,28608,28609,28611,28614,28616,28618],{"class":280,"line":567},[278,28610,758],{"class":298},[278,28612,28613],{"class":650}," passage",[278,28615,764],{"class":298},[278,28617,28457],{"class":333},[278,28619,1313],{"class":302},[278,28621,28622],{"class":280,"line":577},[278,28623,292],{"emptyLinePlaceholder":291},[278,28625,28626,28628],{"class":280,"line":587},[278,28627,1105],{"class":298},[278,28629,876],{"class":302},[278,28631,28632,28634,28637,28639,28641,28644,28647],{"class":280,"line":597},[278,28633,1112],{"class":298},[278,28635,28636],{"class":650}," userId",[278,28638,764],{"class":298},[278,28640,1120],{"class":298},[278,28642,28643],{"class":302}," passage.",[278,28645,28646],{"class":333},"authenticateRequest",[278,28648,28649],{"class":302},"(event.node.req);\n",[278,28651,28652],{"class":280,"line":608},[278,28653,292],{"emptyLinePlaceholder":291},[278,28655,28656,28658],{"class":280,"line":614},[278,28657,1242],{"class":298},[278,28659,28660],{"class":302}," (userId) {\n",[278,28662,28663,28665,28667,28669,28672],{"class":280,"line":620},[278,28664,1919],{"class":302},[278,28666,14851],{"class":333},[278,28668,1126],{"class":302},[278,28670,28671],{"class":309},"'request authenticated'",[278,28673,28674],{"class":302},", userId);\n",[278,28676,28677],{"class":280,"line":625},[278,28678,292],{"emptyLinePlaceholder":291},[278,28680,28681,28683,28685,28687,28689,28692,28694],{"class":280,"line":640},[278,28682,2461],{"class":298},[278,28684,28227],{"class":650},[278,28686,764],{"class":298},[278,28688,1120],{"class":298},[278,28690,28691],{"class":302}," passage.user.",[278,28693,3925],{"class":333},[278,28695,28696],{"class":302},"(userId);\n",[278,28698,28699,28702,28704],{"class":280,"line":663},[278,28700,28701],{"class":302},"      event.context.auth ",[278,28703,358],{"class":298},[278,28705,28706],{"class":302}," { user };\n",[278,28708,28709],{"class":280,"line":669},[278,28710,28711],{"class":302}," \n",[278,28713,28714,28716],{"class":280,"line":680},[278,28715,1942],{"class":298},[278,28717,313],{"class":302},[278,28719,28720],{"class":280,"line":686},[278,28721,1285],{"class":302},[278,28723,28724,28726,28728],{"class":280,"line":1334},[278,28725,1397],{"class":302},[278,28727,1400],{"class":298},[278,28729,1403],{"class":302},[278,28731,28732,28734,28736,28738,28741],{"class":280,"line":1375},[278,28733,1409],{"class":302},[278,28735,14851],{"class":333},[278,28737,1126],{"class":302},[278,28739,28740],{"class":309},"'failed to authenticate request'",[278,28742,1420],{"class":302},[278,28744,28745],{"class":280,"line":1381},[278,28746,1096],{"class":302},[278,28748,28749],{"class":280,"line":1386},[278,28750,292],{"emptyLinePlaceholder":291},[278,28752,28753,28755,28757],{"class":280,"line":1394},[278,28754,23492],{"class":298},[278,28756,3957],{"class":333},[278,28758,637],{"class":302},[278,28760,28761,28764,28767],{"class":280,"line":1406},[278,28762,28763],{"class":302},"    statusCode: ",[278,28765,28766],{"class":650},"401",[278,28768,660],{"class":302},[278,28770,28771,28774,28777],{"class":280,"line":1423},[278,28772,28773],{"class":302},"    message: ",[278,28775,28776],{"class":309},"'Unauthorized'",[278,28778,660],{"class":302},[278,28780,28781],{"class":280,"line":1432},[278,28782,2037],{"class":302},[278,28784,28785],{"class":280,"line":1437},[278,28786,2817],{"class":302},[11,28788,28789],{},[94,28790,28791],{},"Handle adding family members",[11,28793,28794],{},"During the onboarding of a new user, the app asks them to create a family account and add family members to it. But to invite the family members they should have a passage account, right? That is the approach we've followed above, the appwrite user id is provided by Passage. To handle this, we first create Passage users using the passage-node SDK, get the user ids, create corresponding appwrite users, and then finally link these users to the created team\u002Ffamily.",[11,28796,28797],{},"The frontend function which adds the family members",[269,28799,28801],{"className":271,"code":28800,"language":273,"meta":274,"style":274},"const addMembers = async () => {\n  loading.value = true;\n  try {\n    const res: any = await $fetch('\u002Fapi\u002Ffamilies', {\n      method: 'post',\n      body: {\n        type: 'ADD_MEMBERS',\n        familyId: metadata?.family_id,\n        members: members.value,\n        onboardStep: metadata?.onboard_step,\n      },\n    });\n\n    console.log('response of add members', res);\n    if (res.user && user.value) {\n      user.value.user_metadata = res.user.user_metadata;\n    }\n\n    emit('membersAdded');\n  } catch (error) {\n    console.log('error is adding members', error);\n  }\n\n  loading.value = false;\n};\n",[59,28802,28803,28820,28831,28837,28860,28868,28873,28882,28887,28892,28897,28901,28905,28909,28922,28934,28944,28948,28952,28964,28972,28985,28989,28993,29003],{"__ignoreMap":274},[278,28804,28805,28807,28810,28812,28814,28816,28818],{"class":280,"line":281},[278,28806,5416],{"class":298},[278,28808,28809],{"class":333}," addMembers",[278,28811,764],{"class":298},[278,28813,2325],{"class":298},[278,28815,5860],{"class":302},[278,28817,1848],{"class":298},[278,28819,876],{"class":302},[278,28821,28822,28825,28827,28829],{"class":280,"line":288},[278,28823,28824],{"class":302},"  loading.value ",[278,28826,358],{"class":298},[278,28828,6575],{"class":650},[278,28830,313],{"class":302},[278,28832,28833,28835],{"class":280,"line":295},[278,28834,1105],{"class":298},[278,28836,876],{"class":302},[278,28838,28839,28841,28843,28845,28847,28849,28851,28853,28855,28858],{"class":280,"line":316},[278,28840,1112],{"class":298},[278,28842,17866],{"class":650},[278,28844,960],{"class":298},[278,28846,1842],{"class":650},[278,28848,764],{"class":298},[278,28850,1120],{"class":298},[278,28852,15262],{"class":333},[278,28854,1126],{"class":302},[278,28856,28857],{"class":309},"'\u002Fapi\u002Ffamilies'",[278,28859,1132],{"class":302},[278,28861,28862,28864,28866],{"class":280,"line":322},[278,28863,1137],{"class":302},[278,28865,27968],{"class":309},[278,28867,660],{"class":302},[278,28869,28870],{"class":280,"line":327},[278,28871,28872],{"class":302},"      body: {\n",[278,28874,28875,28877,28880],{"class":280,"line":340},[278,28876,11517],{"class":302},[278,28878,28879],{"class":309},"'ADD_MEMBERS'",[278,28881,660],{"class":302},[278,28883,28884],{"class":280,"line":349},[278,28885,28886],{"class":302},"        familyId: metadata?.family_id,\n",[278,28888,28889],{"class":280,"line":375},[278,28890,28891],{"class":302},"        members: members.value,\n",[278,28893,28894],{"class":280,"line":386},[278,28895,28896],{"class":302},"        onboardStep: metadata?.onboard_step,\n",[278,28898,28899],{"class":280,"line":397},[278,28900,1165],{"class":302},[278,28902,28903],{"class":280,"line":408},[278,28904,1233],{"class":302},[278,28906,28907],{"class":280,"line":433},[278,28908,292],{"emptyLinePlaceholder":291},[278,28910,28911,28913,28915,28917,28920],{"class":280,"line":454},[278,28912,1409],{"class":302},[278,28914,14851],{"class":333},[278,28916,1126],{"class":302},[278,28918,28919],{"class":309},"'response of add members'",[278,28921,17894],{"class":302},[278,28923,28924,28926,28929,28931],{"class":280,"line":475},[278,28925,1242],{"class":298},[278,28927,28928],{"class":302}," (res.user ",[278,28930,1068],{"class":298},[278,28932,28933],{"class":302}," user.value) {\n",[278,28935,28936,28939,28941],{"class":280,"line":496},[278,28937,28938],{"class":302},"      user.value.user_metadata ",[278,28940,358],{"class":298},[278,28942,28943],{"class":302}," res.user.user_metadata;\n",[278,28945,28946],{"class":280,"line":505},[278,28947,1285],{"class":302},[278,28949,28950],{"class":280,"line":516},[278,28951,292],{"emptyLinePlaceholder":291},[278,28953,28954,28957,28959,28962],{"class":280,"line":527},[278,28955,28956],{"class":333},"    emit",[278,28958,1126],{"class":302},[278,28960,28961],{"class":309},"'membersAdded'",[278,28963,1280],{"class":302},[278,28965,28966,28968,28970],{"class":280,"line":533},[278,28967,1397],{"class":302},[278,28969,1400],{"class":298},[278,28971,1403],{"class":302},[278,28973,28974,28976,28978,28980,28983],{"class":280,"line":539},[278,28975,1409],{"class":302},[278,28977,14851],{"class":333},[278,28979,1126],{"class":302},[278,28981,28982],{"class":309},"'error is adding members'",[278,28984,1420],{"class":302},[278,28986,28987],{"class":280,"line":545},[278,28988,1096],{"class":302},[278,28990,28991],{"class":280,"line":551},[278,28992,292],{"emptyLinePlaceholder":291},[278,28994,28995,28997,28999,29001],{"class":280,"line":557},[278,28996,28824],{"class":302},[278,28998,358],{"class":298},[278,29000,6872],{"class":650},[278,29002,313],{"class":302},[278,29004,29005],{"class":280,"line":567},[278,29006,2817],{"class":302},[11,29008,4796,29009,29012],{},[59,29010,29011],{},"\u002Fapi\u002Ffamilies"," code",[269,29014,29016],{"className":271,"code":29015,"language":273,"meta":274,"style":274},"import { UserObject } from '@passageidentity\u002Fpassage-node';\nimport { protectRoute, createUser, updateUser } from '..\u002FusePassage';\nimport { useAppwrite } from '..\u002FuseAppwrite';\n\nconst addFamilyMembers = async (\n  userId: string,\n  urlOrigin: string,\n  data: any\n) => {\n  if (!data.familyId || !data.members || !data.members.length) {\n    throw createError({\n      statusCode: 400,\n      message: 'Missing familyId or members to add',\n    });\n  }\n\n  const { $users, $teams } = useAppwrite();\n  try {\n    const userPromises = [];\n    for (const member of data.members) {\n      \u002F\u002F Create Passage Users\n      userPromises.push(\n        createUser(member.email, {\n          name: member.name,\n          family_id: data.familyId,\n          roles: member.role,\n          onboard_step: 'done',\n        })\n      );\n    }\n\n    const users = await Promise.all(userPromises);\n    console.log('created new users in passage: ', users);\n\n    const appwriteUserPromises = [];\n    for (const user of users) {\n      appwriteUserPromises.push(\n        $users.create(\n          user.id,\n          user.email,\n          undefined,\n          undefined,\n          user.user_metadata?.name as string\n        )\n      );\n    }\n\n    const appwriteUsers = await Promise.all(appwriteUserPromises);\n\n    console.log('created new users in appwrite: ', appwriteUsers);\n\n    const memberPromises = [];\n    for (const user of users) {\n      memberPromises.push(\n        $teams.createMembership(\n          data.familyId,\n          [user.user_metadata?.roles as string],\n          `${urlOrigin}\u002Fjoin-team`,\n          user.email,\n          user.id,\n          user.phone,\n          user.user_metadata?.name as string\n        )\n      );\n    }\n\n    const memberships = await Promise.all(memberPromises);\n\n    console.log('created memberships in appwrite', memberships);\n\n    let updatedUser;\n    if (data.onboardStep === 'family') {\n      \u002F\u002F updat passage user metadata\n      updatedUser = await updateUser(userId, {\n        onboard_step: 'jar',\n      });\n    }\n\n    return {\n      user: updatedUser,\n    };\n  } catch (error) {\n    console.log('failed to add members', error);\n    throw createError({\n      statusCode: 500,\n      message: 'Failed to add family members',\n    });\n  }\n};\n\nexport default defineEventHandler(async (event) => {\n  console.log('incoming post event for api\u002Ffamilies\u002F');\n\n  await protectRoute(event);\n\n  const body = await readBody(event);\n  console.log('body', body);\n\n  if (!body.type || !['CREATE', 'ADD_MEMBERS'].includes(body.type)) {\n    throw createError({\n      statusCode: 400,\n      message: 'Missing or unsupported event type',\n    });\n  }\n\n  const user = event.context.auth.user as UserObject;\n  console.log(`got some auth user: ${user}`);\n\n  const origin = getHeader(event, 'origin');\n\n  let data;\n  if (body.type === 'CREATE') {\n    data = await createFamily(user.id, origin || '', body);\n  } else {\n    data = await addFamilyMembers(user.id, origin || '', body);\n  }\n\n  return {\n    status: 'ok',\n    ...data,\n  };\n});\n",[59,29017,29018,29030,29043,29055,29059,29072,29083,29094,29104,29112,29141,29149,29157,29166,29170,29174,29178,29199,29205,29216,29232,29237,29246,29254,29259,29264,29269,29279,29283,29287,29291,29295,29315,29329,29333,29344,29359,29368,29377,29382,29387,29394,29400,29409,29414,29418,29422,29426,29446,29450,29464,29468,29479,29493,29502,29512,29517,29528,29541,29545,29549,29554,29562,29566,29570,29574,29578,29598,29602,29616,29620,29627,29641,29646,29661,29671,29675,29679,29683,29689,29694,29698,29706,29719,29727,29735,29744,29748,29752,29756,29760,29782,29795,29799,29807,29811,29825,29839,29843,29875,29883,29891,29900,29904,29908,29912,29928,29945,29949,29968,29972,29979,29993,30014,30022,30040,30044,30048,30054,30062,30069,30073],{"__ignoreMap":274},[278,29019,29020,29022,29024,29026,29028],{"class":280,"line":281},[278,29021,299],{"class":298},[278,29023,28127],{"class":302},[278,29025,306],{"class":298},[278,29027,28132],{"class":309},[278,29029,313],{"class":302},[278,29031,29032,29034,29037,29039,29041],{"class":280,"line":288},[278,29033,299],{"class":298},[278,29035,29036],{"class":302}," { protectRoute, createUser, updateUser } ",[278,29038,306],{"class":298},[278,29040,28146],{"class":309},[278,29042,313],{"class":302},[278,29044,29045,29047,29049,29051,29053],{"class":280,"line":295},[278,29046,299],{"class":298},[278,29048,28155],{"class":302},[278,29050,306],{"class":298},[278,29052,28160],{"class":309},[278,29054,313],{"class":302},[278,29056,29057],{"class":280,"line":316},[278,29058,292],{"emptyLinePlaceholder":291},[278,29060,29061,29063,29066,29068,29070],{"class":280,"line":322},[278,29062,5416],{"class":298},[278,29064,29065],{"class":333}," addFamilyMembers",[278,29067,764],{"class":298},[278,29069,2325],{"class":298},[278,29071,346],{"class":302},[278,29073,29074,29077,29079,29081],{"class":280,"line":327},[278,29075,29076],{"class":501},"  userId",[278,29078,960],{"class":298},[278,29080,963],{"class":650},[278,29082,660],{"class":302},[278,29084,29085,29088,29090,29092],{"class":280,"line":340},[278,29086,29087],{"class":501},"  urlOrigin",[278,29089,960],{"class":298},[278,29091,963],{"class":650},[278,29093,660],{"class":302},[278,29095,29096,29099,29101],{"class":280,"line":349},[278,29097,29098],{"class":501},"  data",[278,29100,960],{"class":298},[278,29102,29103],{"class":650}," any\n",[278,29105,29106,29108,29110],{"class":280,"line":375},[278,29107,1845],{"class":302},[278,29109,1848],{"class":298},[278,29111,876],{"class":302},[278,29113,29114,29116,29118,29120,29123,29125,29127,29130,29132,29134,29137,29139],{"class":280,"line":386},[278,29115,1062],{"class":298},[278,29117,1245],{"class":302},[278,29119,1209],{"class":298},[278,29121,29122],{"class":302},"data.familyId ",[278,29124,5954],{"class":298},[278,29126,6192],{"class":298},[278,29128,29129],{"class":302},"data.members ",[278,29131,5954],{"class":298},[278,29133,6192],{"class":298},[278,29135,29136],{"class":302},"data.members.",[278,29138,15645],{"class":650},[278,29140,1718],{"class":302},[278,29142,29143,29145,29147],{"class":280,"line":397},[278,29144,1426],{"class":298},[278,29146,3957],{"class":333},[278,29148,637],{"class":302},[278,29150,29151,29153,29155],{"class":280,"line":408},[278,29152,3964],{"class":302},[278,29154,3967],{"class":650},[278,29156,660],{"class":302},[278,29158,29159,29161,29164],{"class":280,"line":433},[278,29160,3974],{"class":302},[278,29162,29163],{"class":309},"'Missing familyId or members to add'",[278,29165,660],{"class":302},[278,29167,29168],{"class":280,"line":454},[278,29169,1233],{"class":302},[278,29171,29172],{"class":280,"line":475},[278,29173,1096],{"class":302},[278,29175,29176],{"class":280,"line":496},[278,29177,292],{"emptyLinePlaceholder":291},[278,29179,29180,29182,29184,29186,29188,29191,29193,29195,29197],{"class":280,"line":505},[278,29181,758],{"class":298},[278,29183,1009],{"class":302},[278,29185,28266],{"class":650},[278,29187,1708],{"class":302},[278,29189,29190],{"class":650},"$teams",[278,29192,1029],{"class":302},[278,29194,358],{"class":298},[278,29196,28273],{"class":333},[278,29198,1313],{"class":302},[278,29200,29201,29203],{"class":280,"line":516},[278,29202,1105],{"class":298},[278,29204,876],{"class":302},[278,29206,29207,29209,29212,29214],{"class":280,"line":527},[278,29208,1112],{"class":298},[278,29210,29211],{"class":650}," userPromises",[278,29213,764],{"class":298},[278,29215,6483],{"class":302},[278,29217,29218,29220,29222,29224,29227,29229],{"class":280,"line":533},[278,29219,12012],{"class":298},[278,29221,1245],{"class":302},[278,29223,5416],{"class":298},[278,29225,29226],{"class":650}," member",[278,29228,12022],{"class":298},[278,29230,29231],{"class":302}," data.members) {\n",[278,29233,29234],{"class":280,"line":539},[278,29235,29236],{"class":284},"      \u002F\u002F Create Passage Users\n",[278,29238,29239,29242,29244],{"class":280,"line":545},[278,29240,29241],{"class":302},"      userPromises.",[278,29243,6524],{"class":333},[278,29245,770],{"class":302},[278,29247,29248,29251],{"class":280,"line":551},[278,29249,29250],{"class":333},"        createUser",[278,29252,29253],{"class":302},"(member.email, {\n",[278,29255,29256],{"class":280,"line":557},[278,29257,29258],{"class":302},"          name: member.name,\n",[278,29260,29261],{"class":280,"line":567},[278,29262,29263],{"class":302},"          family_id: data.familyId,\n",[278,29265,29266],{"class":280,"line":577},[278,29267,29268],{"class":302},"          roles: member.role,\n",[278,29270,29271,29274,29277],{"class":280,"line":587},[278,29272,29273],{"class":302},"          onboard_step: ",[278,29275,29276],{"class":309},"'done'",[278,29278,660],{"class":302},[278,29280,29281],{"class":280,"line":597},[278,29282,27049],{"class":302},[278,29284,29285],{"class":280,"line":608},[278,29286,2616],{"class":302},[278,29288,29289],{"class":280,"line":614},[278,29290,1285],{"class":302},[278,29292,29293],{"class":280,"line":620},[278,29294,292],{"emptyLinePlaceholder":291},[278,29296,29297,29299,29302,29304,29306,29308,29310,29312],{"class":280,"line":625},[278,29298,1112],{"class":298},[278,29300,29301],{"class":650}," users",[278,29303,764],{"class":298},[278,29305,1120],{"class":298},[278,29307,2057],{"class":650},[278,29309,183],{"class":302},[278,29311,2062],{"class":333},[278,29313,29314],{"class":302},"(userPromises);\n",[278,29316,29317,29319,29321,29323,29326],{"class":280,"line":640},[278,29318,1409],{"class":302},[278,29320,14851],{"class":333},[278,29322,1126],{"class":302},[278,29324,29325],{"class":309},"'created new users in passage: '",[278,29327,29328],{"class":302},", users);\n",[278,29330,29331],{"class":280,"line":663},[278,29332,292],{"emptyLinePlaceholder":291},[278,29334,29335,29337,29340,29342],{"class":280,"line":669},[278,29336,1112],{"class":298},[278,29338,29339],{"class":650}," appwriteUserPromises",[278,29341,764],{"class":298},[278,29343,6483],{"class":302},[278,29345,29346,29348,29350,29352,29354,29356],{"class":280,"line":680},[278,29347,12012],{"class":298},[278,29349,1245],{"class":302},[278,29351,5416],{"class":298},[278,29353,28227],{"class":650},[278,29355,12022],{"class":298},[278,29357,29358],{"class":302}," users) {\n",[278,29360,29361,29364,29366],{"class":280,"line":686},[278,29362,29363],{"class":302},"      appwriteUserPromises.",[278,29365,6524],{"class":333},[278,29367,770],{"class":302},[278,29369,29370,29373,29375],{"class":280,"line":1334},[278,29371,29372],{"class":302},"        $users.",[278,29374,11913],{"class":333},[278,29376,770],{"class":302},[278,29378,29379],{"class":280,"line":1375},[278,29380,29381],{"class":302},"          user.id,\n",[278,29383,29384],{"class":280,"line":1381},[278,29385,29386],{"class":302},"          user.email,\n",[278,29388,29389,29392],{"class":280,"line":1386},[278,29390,29391],{"class":650},"          undefined",[278,29393,660],{"class":302},[278,29395,29396,29398],{"class":280,"line":1394},[278,29397,29391],{"class":650},[278,29399,660],{"class":302},[278,29401,29402,29405,29407],{"class":280,"line":1406},[278,29403,29404],{"class":302},"          user.user_metadata?.name ",[278,29406,2937],{"class":298},[278,29408,17191],{"class":650},[278,29410,29411],{"class":280,"line":1423},[278,29412,29413],{"class":302},"        )\n",[278,29415,29416],{"class":280,"line":1432},[278,29417,2616],{"class":302},[278,29419,29420],{"class":280,"line":1437},[278,29421,1285],{"class":302},[278,29423,29424],{"class":280,"line":1916},[278,29425,292],{"emptyLinePlaceholder":291},[278,29427,29428,29430,29433,29435,29437,29439,29441,29443],{"class":280,"line":1939},[278,29429,1112],{"class":298},[278,29431,29432],{"class":650}," appwriteUsers",[278,29434,764],{"class":298},[278,29436,1120],{"class":298},[278,29438,2057],{"class":650},[278,29440,183],{"class":302},[278,29442,2062],{"class":333},[278,29444,29445],{"class":302},"(appwriteUserPromises);\n",[278,29447,29448],{"class":280,"line":1949},[278,29449,292],{"emptyLinePlaceholder":291},[278,29451,29452,29454,29456,29458,29461],{"class":280,"line":1954},[278,29453,1409],{"class":302},[278,29455,14851],{"class":333},[278,29457,1126],{"class":302},[278,29459,29460],{"class":309},"'created new users in appwrite: '",[278,29462,29463],{"class":302},", appwriteUsers);\n",[278,29465,29466],{"class":280,"line":1959},[278,29467,292],{"emptyLinePlaceholder":291},[278,29469,29470,29472,29475,29477],{"class":280,"line":1985},[278,29471,1112],{"class":298},[278,29473,29474],{"class":650}," memberPromises",[278,29476,764],{"class":298},[278,29478,6483],{"class":302},[278,29480,29481,29483,29485,29487,29489,29491],{"class":280,"line":1990},[278,29482,12012],{"class":298},[278,29484,1245],{"class":302},[278,29486,5416],{"class":298},[278,29488,28227],{"class":650},[278,29490,12022],{"class":298},[278,29492,29358],{"class":302},[278,29494,29495,29498,29500],{"class":280,"line":1997},[278,29496,29497],{"class":302},"      memberPromises.",[278,29499,6524],{"class":333},[278,29501,770],{"class":302},[278,29503,29504,29507,29510],{"class":280,"line":2006},[278,29505,29506],{"class":302},"        $teams.",[278,29508,29509],{"class":333},"createMembership",[278,29511,770],{"class":302},[278,29513,29514],{"class":280,"line":2018},[278,29515,29516],{"class":302},"          data.familyId,\n",[278,29518,29519,29522,29524,29526],{"class":280,"line":2029},[278,29520,29521],{"class":302},"          [user.user_metadata?.roles ",[278,29523,2937],{"class":298},[278,29525,963],{"class":650},[278,29527,3533],{"class":302},[278,29529,29530,29533,29536,29539],{"class":280,"line":2034},[278,29531,29532],{"class":309},"          `${",[278,29534,29535],{"class":302},"urlOrigin",[278,29537,29538],{"class":309},"}\u002Fjoin-team`",[278,29540,660],{"class":302},[278,29542,29543],{"class":280,"line":2040},[278,29544,29386],{"class":302},[278,29546,29547],{"class":280,"line":2045},[278,29548,29381],{"class":302},[278,29550,29551],{"class":280,"line":2068},[278,29552,29553],{"class":302},"          user.phone,\n",[278,29555,29556,29558,29560],{"class":280,"line":2099},[278,29557,29404],{"class":302},[278,29559,2937],{"class":298},[278,29561,17191],{"class":650},[278,29563,29564],{"class":280,"line":6428},[278,29565,29413],{"class":302},[278,29567,29568],{"class":280,"line":6439},[278,29569,2616],{"class":302},[278,29571,29572],{"class":280,"line":6450},[278,29573,1285],{"class":302},[278,29575,29576],{"class":280,"line":6455},[278,29577,292],{"emptyLinePlaceholder":291},[278,29579,29580,29582,29585,29587,29589,29591,29593,29595],{"class":280,"line":6460},[278,29581,1112],{"class":298},[278,29583,29584],{"class":650}," memberships",[278,29586,764],{"class":298},[278,29588,1120],{"class":298},[278,29590,2057],{"class":650},[278,29592,183],{"class":302},[278,29594,2062],{"class":333},[278,29596,29597],{"class":302},"(memberPromises);\n",[278,29599,29600],{"class":280,"line":6475},[278,29601,292],{"emptyLinePlaceholder":291},[278,29603,29604,29606,29608,29610,29613],{"class":280,"line":6486},[278,29605,1409],{"class":302},[278,29607,14851],{"class":333},[278,29609,1126],{"class":302},[278,29611,29612],{"class":309},"'created memberships in appwrite'",[278,29614,29615],{"class":302},", memberships);\n",[278,29617,29618],{"class":280,"line":6491},[278,29619,292],{"emptyLinePlaceholder":291},[278,29621,29622,29624],{"class":280,"line":6518},[278,29623,20815],{"class":298},[278,29625,29626],{"class":302}," updatedUser;\n",[278,29628,29629,29631,29634,29636,29639],{"class":280,"line":6530},[278,29630,1242],{"class":298},[278,29632,29633],{"class":302}," (data.onboardStep ",[278,29635,2451],{"class":298},[278,29637,29638],{"class":309}," 'family'",[278,29640,1718],{"class":302},[278,29642,29643],{"class":280,"line":6542},[278,29644,29645],{"class":284},"      \u002F\u002F updat passage user metadata\n",[278,29647,29648,29651,29653,29655,29658],{"class":280,"line":6547},[278,29649,29650],{"class":302},"      updatedUser ",[278,29652,358],{"class":298},[278,29654,1120],{"class":298},[278,29656,29657],{"class":333}," updateUser",[278,29659,29660],{"class":302},"(userId, {\n",[278,29662,29663,29666,29669],{"class":280,"line":6552},[278,29664,29665],{"class":302},"        onboard_step: ",[278,29667,29668],{"class":309},"'jar'",[278,29670,660],{"class":302},[278,29672,29673],{"class":280,"line":6567},[278,29674,5148],{"class":302},[278,29676,29677],{"class":280,"line":6580},[278,29678,1285],{"class":302},[278,29680,29681],{"class":280,"line":6593},[278,29682,292],{"emptyLinePlaceholder":291},[278,29684,29685,29687],{"class":280,"line":6605},[278,29686,1088],{"class":298},[278,29688,876],{"class":302},[278,29690,29691],{"class":280,"line":6620},[278,29692,29693],{"class":302},"      user: updatedUser,\n",[278,29695,29696],{"class":280,"line":6625},[278,29697,1378],{"class":302},[278,29699,29700,29702,29704],{"class":280,"line":6633},[278,29701,1397],{"class":302},[278,29703,1400],{"class":298},[278,29705,1403],{"class":302},[278,29707,29708,29710,29712,29714,29717],{"class":280,"line":6643},[278,29709,1409],{"class":302},[278,29711,14851],{"class":333},[278,29713,1126],{"class":302},[278,29715,29716],{"class":309},"'failed to add members'",[278,29718,1420],{"class":302},[278,29720,29721,29723,29725],{"class":280,"line":6657},[278,29722,1426],{"class":298},[278,29724,3957],{"class":333},[278,29726,637],{"class":302},[278,29728,29729,29731,29733],{"class":280,"line":6665},[278,29730,3964],{"class":302},[278,29732,2779],{"class":650},[278,29734,660],{"class":302},[278,29736,29737,29739,29742],{"class":280,"line":6670},[278,29738,3974],{"class":302},[278,29740,29741],{"class":309},"'Failed to add family members'",[278,29743,660],{"class":302},[278,29745,29746],{"class":280,"line":6675},[278,29747,1233],{"class":302},[278,29749,29750],{"class":280,"line":6680},[278,29751,1096],{"class":302},[278,29753,29754],{"class":280,"line":6698},[278,29755,2817],{"class":302},[278,29757,29758],{"class":280,"line":6725},[278,29759,292],{"emptyLinePlaceholder":291},[278,29761,29762,29764,29766,29768,29770,29772,29774,29776,29778,29780],{"class":280,"line":6738},[278,29763,628],{"class":298},[278,29765,631],{"class":298},[278,29767,3878],{"class":333},[278,29769,1126],{"class":302},[278,29771,1050],{"class":298},[278,29773,1245],{"class":302},[278,29775,3887],{"class":501},[278,29777,1845],{"class":302},[278,29779,1848],{"class":298},[278,29781,876],{"class":302},[278,29783,29784,29786,29788,29790,29793],{"class":280,"line":6752},[278,29785,17975],{"class":302},[278,29787,14851],{"class":333},[278,29789,1126],{"class":302},[278,29791,29792],{"class":309},"'incoming post event for api\u002Ffamilies\u002F'",[278,29794,1280],{"class":302},[278,29796,29797],{"class":280,"line":6769},[278,29798,292],{"emptyLinePlaceholder":291},[278,29800,29801,29803,29805],{"class":280,"line":6786},[278,29802,28211],{"class":298},[278,29804,28214],{"class":333},[278,29806,3910],{"class":302},[278,29808,29809],{"class":280,"line":6798},[278,29810,292],{"emptyLinePlaceholder":291},[278,29812,29813,29815,29817,29819,29821,29823],{"class":280,"line":6803},[278,29814,758],{"class":298},[278,29816,2517],{"class":650},[278,29818,764],{"class":298},[278,29820,1120],{"class":298},[278,29822,16302],{"class":333},[278,29824,3910],{"class":302},[278,29826,29827,29829,29831,29833,29836],{"class":280,"line":6815},[278,29828,17975],{"class":302},[278,29830,14851],{"class":333},[278,29832,1126],{"class":302},[278,29834,29835],{"class":309},"'body'",[278,29837,29838],{"class":302},", body);\n",[278,29840,29841],{"class":280,"line":6827},[278,29842,292],{"emptyLinePlaceholder":291},[278,29844,29845,29847,29849,29851,29854,29856,29858,29861,29864,29866,29868,29870,29872],{"class":280,"line":6839},[278,29846,1062],{"class":298},[278,29848,1245],{"class":302},[278,29850,1209],{"class":298},[278,29852,29853],{"class":302},"body.type ",[278,29855,5954],{"class":298},[278,29857,6192],{"class":298},[278,29859,29860],{"class":302},"[",[278,29862,29863],{"class":309},"'CREATE'",[278,29865,1708],{"class":302},[278,29867,28879],{"class":309},[278,29869,17447],{"class":302},[278,29871,13297],{"class":333},[278,29873,29874],{"class":302},"(body.type)) {\n",[278,29876,29877,29879,29881],{"class":280,"line":6844},[278,29878,1426],{"class":298},[278,29880,3957],{"class":333},[278,29882,637],{"class":302},[278,29884,29885,29887,29889],{"class":280,"line":6853},[278,29886,3964],{"class":302},[278,29888,3967],{"class":650},[278,29890,660],{"class":302},[278,29892,29893,29895,29898],{"class":280,"line":6859},[278,29894,3974],{"class":302},[278,29896,29897],{"class":309},"'Missing or unsupported event type'",[278,29899,660],{"class":302},[278,29901,29902],{"class":280,"line":6864},[278,29903,1233],{"class":302},[278,29905,29906],{"class":280,"line":6877},[278,29907,1096],{"class":302},[278,29909,29910],{"class":280,"line":6887},[278,29911,292],{"emptyLinePlaceholder":291},[278,29913,29914,29916,29918,29920,29922,29924,29926],{"class":280,"line":6918},[278,29915,758],{"class":298},[278,29917,28227],{"class":650},[278,29919,764],{"class":298},[278,29921,28232],{"class":302},[278,29923,2937],{"class":298},[278,29925,28237],{"class":333},[278,29927,313],{"class":302},[278,29929,29930,29932,29934,29936,29939,29941,29943],{"class":280,"line":6923},[278,29931,17975],{"class":302},[278,29933,14851],{"class":333},[278,29935,1126],{"class":302},[278,29937,29938],{"class":309},"`got some auth user: ${",[278,29940,5022],{"class":302},[278,29942,1277],{"class":309},[278,29944,1280],{"class":302},[278,29946,29947],{"class":280,"line":6931},[278,29948,292],{"emptyLinePlaceholder":291},[278,29950,29951,29953,29956,29958,29961,29963,29966],{"class":280,"line":6939},[278,29952,758],{"class":298},[278,29954,29955],{"class":650}," origin",[278,29957,764],{"class":298},[278,29959,29960],{"class":333}," getHeader",[278,29962,5162],{"class":302},[278,29964,29965],{"class":309},"'origin'",[278,29967,1280],{"class":302},[278,29969,29970],{"class":280,"line":6951},[278,29971,292],{"emptyLinePlaceholder":291},[278,29973,29974,29976],{"class":280,"line":6957},[278,29975,6050],{"class":298},[278,29977,29978],{"class":302}," data;\n",[278,29980,29981,29983,29986,29988,29991],{"class":280,"line":6962},[278,29982,1062],{"class":298},[278,29984,29985],{"class":302}," (body.type ",[278,29987,2451],{"class":298},[278,29989,29990],{"class":309}," 'CREATE'",[278,29992,1718],{"class":302},[278,29994,29995,29998,30000,30002,30005,30008,30010,30012],{"class":280,"line":6973},[278,29996,29997],{"class":302},"    data ",[278,29999,358],{"class":298},[278,30001,1120],{"class":298},[278,30003,30004],{"class":333}," createFamily",[278,30006,30007],{"class":302},"(user.id, origin ",[278,30009,5954],{"class":298},[278,30011,13973],{"class":309},[278,30013,29838],{"class":302},[278,30015,30016,30018,30020],{"class":280,"line":6985},[278,30017,1397],{"class":302},[278,30019,15659],{"class":298},[278,30021,876],{"class":302},[278,30023,30024,30026,30028,30030,30032,30034,30036,30038],{"class":280,"line":6990},[278,30025,29997],{"class":302},[278,30027,358],{"class":298},[278,30029,1120],{"class":298},[278,30031,29065],{"class":333},[278,30033,30007],{"class":302},[278,30035,5954],{"class":298},[278,30037,13973],{"class":309},[278,30039,29838],{"class":302},[278,30041,30042],{"class":280,"line":6995},[278,30043,1096],{"class":302},[278,30045,30046],{"class":280,"line":7000},[278,30047,292],{"emptyLinePlaceholder":291},[278,30049,30050,30052],{"class":280,"line":7005},[278,30051,343],{"class":298},[278,30053,876],{"class":302},[278,30055,30056,30058,30060],{"class":280,"line":7017},[278,30057,28369],{"class":302},[278,30059,28372],{"class":309},[278,30061,660],{"class":302},[278,30063,30064,30066],{"class":280,"line":7025},[278,30065,16395],{"class":298},[278,30067,30068],{"class":302},"data,\n",[278,30070,30071],{"class":280,"line":7030},[278,30072,901],{"class":302},[278,30074,30075],{"class":280,"line":7035},[278,30076,3693],{"class":302},[11,30078,30079],{},"These are the passage functions to create and update a user.",[269,30081,30083],{"className":271,"code":30082,"language":273,"meta":274,"style":274},"import Passage, { Metadata } from '@passageidentity\u002Fpassage-node';\n\nexport const createUser = async (email: string, metadata: Metadata) => {\n  const passage = getPassage();\n\n  let userData = await passage.user.create({ email, user_metadata: metadata });\n  console.log(userData);\n\n  return userData;\n};\n\nexport const updateUser = async (userId: string, data: Metadata) => {\n  const passage = getPassage();\n\n  let userData = await passage.user.update(userId, { user_metadata: data });\n  console.log(userData);\n\n  return userData;\n};\n",[59,30084,30085,30097,30101,30139,30151,30155,30173,30182,30186,30193,30197,30201,30236,30248,30252,30269,30277,30281,30287],{"__ignoreMap":274},[278,30086,30087,30089,30091,30093,30095],{"class":280,"line":281},[278,30088,299],{"class":298},[278,30090,28414],{"class":302},[278,30092,306],{"class":298},[278,30094,28132],{"class":309},[278,30096,313],{"class":302},[278,30098,30099],{"class":280,"line":288},[278,30100,292],{"emptyLinePlaceholder":291},[278,30102,30103,30105,30107,30110,30112,30114,30116,30119,30121,30123,30125,30128,30130,30133,30135,30137],{"class":280,"line":295},[278,30104,628],{"class":298},[278,30106,4559],{"class":298},[278,30108,30109],{"class":333}," createUser",[278,30111,764],{"class":298},[278,30113,2325],{"class":298},[278,30115,1245],{"class":302},[278,30117,30118],{"class":501},"email",[278,30120,960],{"class":298},[278,30122,963],{"class":650},[278,30124,1708],{"class":302},[278,30126,30127],{"class":501},"metadata",[278,30129,960],{"class":298},[278,30131,30132],{"class":333}," Metadata",[278,30134,1845],{"class":302},[278,30136,1848],{"class":298},[278,30138,876],{"class":302},[278,30140,30141,30143,30145,30147,30149],{"class":280,"line":316},[278,30142,758],{"class":298},[278,30144,28613],{"class":650},[278,30146,764],{"class":298},[278,30148,28457],{"class":333},[278,30150,1313],{"class":302},[278,30152,30153],{"class":280,"line":322},[278,30154,292],{"emptyLinePlaceholder":291},[278,30156,30157,30159,30162,30164,30166,30168,30170],{"class":280,"line":327},[278,30158,6050],{"class":298},[278,30160,30161],{"class":302}," userData ",[278,30163,358],{"class":298},[278,30165,1120],{"class":298},[278,30167,28691],{"class":302},[278,30169,11913],{"class":333},[278,30171,30172],{"class":302},"({ email, user_metadata: metadata });\n",[278,30174,30175,30177,30179],{"class":280,"line":340},[278,30176,17975],{"class":302},[278,30178,14851],{"class":333},[278,30180,30181],{"class":302},"(userData);\n",[278,30183,30184],{"class":280,"line":349},[278,30185,292],{"emptyLinePlaceholder":291},[278,30187,30188,30190],{"class":280,"line":375},[278,30189,343],{"class":298},[278,30191,30192],{"class":302}," userData;\n",[278,30194,30195],{"class":280,"line":386},[278,30196,2817],{"class":302},[278,30198,30199],{"class":280,"line":397},[278,30200,292],{"emptyLinePlaceholder":291},[278,30202,30203,30205,30207,30209,30211,30213,30215,30218,30220,30222,30224,30226,30228,30230,30232,30234],{"class":280,"line":408},[278,30204,628],{"class":298},[278,30206,4559],{"class":298},[278,30208,29657],{"class":333},[278,30210,764],{"class":298},[278,30212,2325],{"class":298},[278,30214,1245],{"class":302},[278,30216,30217],{"class":501},"userId",[278,30219,960],{"class":298},[278,30221,963],{"class":650},[278,30223,1708],{"class":302},[278,30225,15996],{"class":501},[278,30227,960],{"class":298},[278,30229,30132],{"class":333},[278,30231,1845],{"class":302},[278,30233,1848],{"class":298},[278,30235,876],{"class":302},[278,30237,30238,30240,30242,30244,30246],{"class":280,"line":433},[278,30239,758],{"class":298},[278,30241,28613],{"class":650},[278,30243,764],{"class":298},[278,30245,28457],{"class":333},[278,30247,1313],{"class":302},[278,30249,30250],{"class":280,"line":454},[278,30251,292],{"emptyLinePlaceholder":291},[278,30253,30254,30256,30258,30260,30262,30264,30266],{"class":280,"line":475},[278,30255,6050],{"class":298},[278,30257,30161],{"class":302},[278,30259,358],{"class":298},[278,30261,1120],{"class":298},[278,30263,28691],{"class":302},[278,30265,2541],{"class":333},[278,30267,30268],{"class":302},"(userId, { user_metadata: data });\n",[278,30270,30271,30273,30275],{"class":280,"line":496},[278,30272,17975],{"class":302},[278,30274,14851],{"class":333},[278,30276,30181],{"class":302},[278,30278,30279],{"class":280,"line":505},[278,30280,292],{"emptyLinePlaceholder":291},[278,30282,30283,30285],{"class":280,"line":516},[278,30284,343],{"class":298},[278,30286,30192],{"class":302},[278,30288,30289],{"class":280,"line":527},[278,30290,2817],{"class":302},[11,30292,30293],{},"As you can see, we're storing important user metadata inside the passage user object itself. Barring the name, none of the other fields are visible to the user. We use the familyId field from this metadata to add members to the same family as the calling user.",[269,30295,30297],{"className":271,"code":30296,"language":273,"meta":274,"style":274},"{\n  name: member.name,\n  family_id: data.familyId,\n  roles: member.role,\n  onboard_step: 'done',\n}\n",[59,30298,30299,30303,30311,30319,30327,30338],{"__ignoreMap":274},[278,30300,30301],{"class":280,"line":281},[278,30302,524],{"class":302},[278,30304,30305,30308],{"class":280,"line":288},[278,30306,30307],{"class":333},"  name",[278,30309,30310],{"class":302},": member.name,\n",[278,30312,30313,30316],{"class":280,"line":295},[278,30314,30315],{"class":333},"  family_id",[278,30317,30318],{"class":302},": data.familyId,\n",[278,30320,30321,30324],{"class":280,"line":316},[278,30322,30323],{"class":333},"  roles",[278,30325,30326],{"class":302},": member.role,\n",[278,30328,30329,30332,30334,30336],{"class":280,"line":322},[278,30330,30331],{"class":333},"  onboard_step",[278,30333,1155],{"class":302},[278,30335,29276],{"class":309},[278,30337,660],{"class":302},[278,30339,30340],{"class":280,"line":327},[278,30341,617],{"class":302},[32,30343,30345],{"id":30344},"appwrite-functions","Appwrite functions",[11,30347,30348],{},"The app also uses some appwrite functions to handle important database events. The shell plugin that we created in the first part gets verified for creating\u002Fdeploying these functions. The database events that the app handles currently include",[123,30350,30351,30354,30357],{},[74,30352,30353],{},"Jar created event: Whenever a new jar is created and it has auto credit enabled, then we update the jar object and set its nextMoneyAt field. This is the field that is queried by the cron job for auto-crediting the configured amount to the jar.",[74,30355,30356],{},"Transaction event: On every transaction (create or update), we update the corresponding jar balance. If the transaction was done by a child, it will be pending and the jar won't be updated. Once the said transaction is approved (transaction update event) by a family member (role: member), then only the jar gets updated.",[74,30358,30359],{},"Cron job: This job runs every day at midnight and auto credits the configured auto credit amount to the eligible jars.",[32,30361,30363],{"id":30362},"app-screenshots","App Screenshots",[11,30365,30366],{},"The rest of the app follows the same principle. All app data is fetched from Nuxt serverless APIs where every API call is authenticated by the passage-node SDK. Here are some of the screenshots from the app.",[11,30368,30369],{},"For styling the passage elements, I had to set the following CSS variables",[269,30371,30373],{"className":3724,"code":30372,"language":3726,"meta":274,"style":274},"passage-register,\npassage-login {\n  --passage-container-background-color: transparent;\n  --passage-body-text-color: #ffffff;\n  --passage-primary-color: #fbbf24;\n  --passage-onprimary-color: #0f172a;\n  --passage-hover-color: #f59e0b;\n  --passage-button-font-weight: 500;\n  --passage-container-max-width: 100%;\n  --passage-container-padding: 30px 0 10px;\n  --passage-button-width: 100%;\n  --passage-control-border-color: #6b6b6b;\n  --passage-otp-input-background-color: #434343;\n}\n",[59,30374,30375,30383,30390,30402,30414,30426,30438,30450,30461,30476,30498,30511,30523,30535],{"__ignoreMap":274},[278,30376,30377,30381],{"class":280,"line":281},[278,30378,30380],{"class":30379},"s9eBZ","passage-register",[278,30382,660],{"class":302},[278,30384,30385,30388],{"class":280,"line":288},[278,30386,30387],{"class":30379},"passage-login",[278,30389,876],{"class":302},[278,30391,30392,30395,30397,30400],{"class":280,"line":295},[278,30393,30394],{"class":501},"  --passage-container-background-color",[278,30396,1155],{"class":302},[278,30398,30399],{"class":650},"transparent",[278,30401,313],{"class":302},[278,30403,30404,30407,30409,30412],{"class":280,"line":316},[278,30405,30406],{"class":501},"  --passage-body-text-color",[278,30408,1155],{"class":302},[278,30410,30411],{"class":650},"#ffffff",[278,30413,313],{"class":302},[278,30415,30416,30419,30421,30424],{"class":280,"line":322},[278,30417,30418],{"class":501},"  --passage-primary-color",[278,30420,1155],{"class":302},[278,30422,30423],{"class":650},"#fbbf24",[278,30425,313],{"class":302},[278,30427,30428,30431,30433,30436],{"class":280,"line":327},[278,30429,30430],{"class":501},"  --passage-onprimary-color",[278,30432,1155],{"class":302},[278,30434,30435],{"class":650},"#0f172a",[278,30437,313],{"class":302},[278,30439,30440,30443,30445,30448],{"class":280,"line":340},[278,30441,30442],{"class":501},"  --passage-hover-color",[278,30444,1155],{"class":302},[278,30446,30447],{"class":650},"#f59e0b",[278,30449,313],{"class":302},[278,30451,30452,30455,30457,30459],{"class":280,"line":349},[278,30453,30454],{"class":501},"  --passage-button-font-weight",[278,30456,1155],{"class":302},[278,30458,2779],{"class":650},[278,30460,313],{"class":302},[278,30462,30463,30466,30468,30471,30474],{"class":280,"line":375},[278,30464,30465],{"class":501},"  --passage-container-max-width",[278,30467,1155],{"class":302},[278,30469,30470],{"class":650},"100",[278,30472,30473],{"class":298},"%",[278,30475,313],{"class":302},[278,30477,30478,30481,30483,30486,30489,30491,30494,30496],{"class":280,"line":386},[278,30479,30480],{"class":501},"  --passage-container-padding",[278,30482,1155],{"class":302},[278,30484,30485],{"class":650},"30",[278,30487,30488],{"class":298},"px",[278,30490,6588],{"class":650},[278,30492,30493],{"class":650}," 10",[278,30495,30488],{"class":298},[278,30497,313],{"class":302},[278,30499,30500,30503,30505,30507,30509],{"class":280,"line":397},[278,30501,30502],{"class":501},"  --passage-button-width",[278,30504,1155],{"class":302},[278,30506,30470],{"class":650},[278,30508,30473],{"class":298},[278,30510,313],{"class":302},[278,30512,30513,30516,30518,30521],{"class":280,"line":408},[278,30514,30515],{"class":501},"  --passage-control-border-color",[278,30517,1155],{"class":302},[278,30519,30520],{"class":650},"#6b6b6b",[278,30522,313],{"class":302},[278,30524,30525,30528,30530,30533],{"class":280,"line":433},[278,30526,30527],{"class":501},"  --passage-otp-input-background-color",[278,30529,1155],{"class":302},[278,30531,30532],{"class":650},"#434343",[278,30534,313],{"class":302},[278,30536,30537],{"class":280,"line":454},[278,30538,617],{"class":302},[11,30540,30541],{},[94,30542,30543],{},"Sign up screen",[11,30545,30546],{},[3135,30547],{"alt":274,"src":30548},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F76c11c70-5027-4877-a728-c513995b70e1-217ff6f9d7.png",[11,30550,30551],{},[94,30552,30553],{},"Sign in screen",[11,30555,30556],{},[3135,30557],{"alt":274,"src":30558},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F080ed958-8923-42e3-b1c5-bc03a0b9ac58-e8754762a3.png",[11,30560,30561],{},[94,30562,30563],{},"Sign in code",[11,30565,30566],{},[3135,30567],{"alt":274,"src":30568},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F840f6a6e-aadc-4cca-9cc7-ba4ca7230358-aa9c709935.png",[11,30570,30571],{},[94,30572,30573],{},"App dashboard",[11,30575,30576],{},[3135,30577],{"alt":274,"src":30578},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002Fe8c785ff-8264-4106-9621-21eb1fab5ebd-c64013df21.png",[11,30580,30581],{},[94,30582,30583],{},"Family screen",[11,30585,30586],{},[3135,30587],{"alt":274,"src":30588},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F5f08fe85-092c-4cf7-bcf0-896afcd1d63f-e0b2b2b2dd.png",[11,30590,30591],{},[94,30592,30593],{},"Transactions screen",[11,30595,30596],{},[3135,30597],{"alt":274,"src":30598},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F0fbc61de-fa7d-43e3-b18c-174d0655636f-9eb3315a2d.png",[11,30600,30601],{},[94,30602,30603],{},"User profile screen",[11,30605,30606,30607,30610],{},"This screen uses the ",[59,30608,30609],{},"\u003Cpassage-profile>"," component provided by the passage's client SDK. We can change our profile update operation from here.",[11,30612,30613],{},[3135,30614],{"alt":274,"src":30615},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F0d6b79ca-bf7e-4b25-aecc-aac2af48d7a8-b6d533fd98.png",[11,30617,30618],{},"But it seems there is a bug in profile updation which the Passage team needs to look at. This app uses 3 other hidden metadata fields, and when you try to update the name it throws an error as shown below",[11,30620,30621],{},[3135,30622],{"alt":274,"src":30623},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002Fe9a25f5b-8511-463c-b6cb-8461791e0b32-711d59e2ad.png",[11,30625,30626],{},[3135,30627],{"alt":274,"src":30628},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F180dd719-7d5b-4ac4-9f19-403bb2506ba1-81c404632e.png",[11,30630,30631],{},[3135,30632],{"alt":274,"src":30633},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002Fed3c8ec4-168d-4c52-b77a-61f9d6dcda88-a716576372.png",[11,30635,30636],{},"The error message is clear enough, but the app doesn't have any control on this component. This can be fixed by the Passage team only.",[11,30638,30639],{},[94,30640,30641],{},"Create jar",[11,30643,30644],{},[3135,30645],{"alt":274,"src":30646},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002Fa45d7db4-dc6f-49e8-8015-15d0327f3491-86581615cc.png",[11,30648,30649],{},[94,30650,30651],{},"Add family members",[11,30653,30654],{},[3135,30655],{"alt":274,"src":30656},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F13e01a97-8906-4927-a921-6c1187ef0a21-3a772cbc96.png",[11,30658,30659],{},[94,30660,30661],{},"Make transaction",[11,30663,30664],{},[3135,30665],{"alt":274,"src":30666},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002F7d3955f1-8cb8-45e9-b7ee-c63cee1016c2-01372e5400.png",[24,30668,30670],{"id":30669},"part-3-automating-the-env-file","Part 3: Automating the env file",[11,30672,30673],{},"As this app utilizes 1Password in a big way, I decided to make use of the 1Password CLI to handle the app env file as well. Below is a simple script that reads an env file and creates a 1Password item in the specified vault and replaces field values with the respective 1Password secrets.",[269,30675,30677],{"className":3335,"code":30676,"language":3337,"meta":274,"style":274},"#!\u002Fbin\u002Fsh\n\n# Parse command-line arguments\nwhile [ $# -gt 0 ]; do\n  key=\"$1\"\n\n  case $key in\n    --vault)\n      vault=\"$2\"\n      shift # past argument\n      shift # past value\n      ;;\n    --item-name)\n      item_name=\"$2\"\n      shift # past argument\n      shift # past value\n      ;;\n    --env-file)\n      env_file=\"$2\"\n      shift # past argument\n      shift # past value\n      ;;\n    *)\n      # unknown option\n      shift\n      ;;\n  esac\ndone\n\n# Check if the --vault, --item-name, and --env-file parameters were provided\nif [ -z \"$vault\" ] || [ -z \"$item_name\" ] || [ -z \"$env_file\" ]; then\n  echo \"Please provide --vault, --item-name, and --env-file parameters.\"\n  exit 1\nfi\n\n# Check if the specified .env file exists\nif [ ! -f \"$env_file\" ]; then\n  echo \"The specified .env file does not exist.\"\n  exit 1\nfi\n\n# Check if the 1Password CLI is installed\nif ! command -v op > \u002Fdev\u002Fnull 2>&1; then\n  echo \"1Password CLI is not installed. Please install it before running this script.\"\n  exit 1\nfi\n\n# Check if the item already exists in the vault\nif op item get \"$item_name\" --vault \"$vault\" > \u002Fdev\u002Fnull 2>&1; then\n  echo \"Item '$item_name' already exists in the '$vault' vault. Skipping creation.\"\n  exit 0\nfi\n\n# Variable to store the fields string\nfields=()\nskipped_fields=()\n\n# Read the .env file line by line\nwhile IFS= read -r line || [ -n \"$line\" ]; do\n  # Skip empty lines and comments\n  if [ -z \"$line\" ] || [ \"${line#\"#\"}\" != \"$line\" ]; then\n    continue\n  fi\n\n  # Split the line into key and value\n  key=\"${line%%=*}\"\n  value=\"${line#*=}\"\n\n  # Check if the value is already a reference to a 1Password secret\n  if [ \"${value#op:\u002F\u002F}\" != \"$value\" ]; then\n    echo \"Skipping creation of '$key' field as it is already a reference to a 1Password secret.\"\n    skipped_fields+=($line)\n    continue\n  fi\n\n  # Add the key-value pair to the fields array\n  fields+=(\"$key=$value\")\ndone \u003C \"$env_file\"\n\n# Create the item in 1Password using the op create command\nop item create \\\n  --category Server \\\n  --title \"$item_name\" \\\n  --vault \"$vault\" \\\n  \"${fields[@]}\" \\\n  --tags \"$item_name,env\"\n\n# Update the .env file with the secret references\nfor ((i = 0; i \u003C ${#fields[@]}; i++)); do\n  # Split the field into key and value\n  field=\"${fields[i]}\"\n  IFS=\"=\" read -r key value \u003C\u003C\u003C \"$field\"\n\n  fields[i]=\"$key=op:\u002F\u002F$vault\u002F$item_name\u002F$key\"\ndone\n\nfinal_fields=(\"${fields[@]}\" \"${skipped_fields[@]}\")\n\n# Overwrite the .env file with the updated key-value pairs\nprintf \"%s\\n\" \"${final_fields[@]}\" > \"$env_file\"\n\necho \"Item '$item_name' created successfully in the '$vault' vault.\"\necho \"Updated the '$env_file' file with the secret references.\"\n",[59,30678,30679,30684,30688,30693,30714,30730,30734,30745,30752,30766,30774,30781,30786,30793,30806,30812,30818,30822,30829,30842,30848,30854,30858,30863,30868,30873,30877,30882,30887,30891,30896,30949,30957,30965,30970,30974,30979,31000,31007,31013,31017,31021,31026,31054,31061,31067,31071,31075,31080,31117,31134,31141,31145,31149,31154,31163,31172,31176,31181,31216,31221,31265,31270,31275,31279,31284,31304,31321,31325,31330,31364,31378,31388,31392,31396,31400,31405,31426,31438,31442,31447,31457,31467,31480,31493,31510,31522,31526,31531,31569,31574,31588,31616,31620,31646,31650,31654,31686,31690,31695,31721,31725,31742],{"__ignoreMap":274},[278,30680,30681],{"class":280,"line":281},[278,30682,30683],{"class":284},"#!\u002Fbin\u002Fsh\n",[278,30685,30686],{"class":280,"line":288},[278,30687,292],{"emptyLinePlaceholder":291},[278,30689,30690],{"class":280,"line":295},[278,30691,30692],{"class":284},"# Parse command-line arguments\n",[278,30694,30695,30697,30700,30703,30706,30708,30711],{"class":280,"line":316},[278,30696,20616],{"class":298},[278,30698,30699],{"class":302}," [ ",[278,30701,30702],{"class":650},"$#",[278,30704,30705],{"class":298}," -gt",[278,30707,6588],{"class":650},[278,30709,30710],{"class":302}," ]; ",[278,30712,30713],{"class":298},"do\n",[278,30715,30716,30719,30721,30724,30727],{"class":280,"line":322},[278,30717,30718],{"class":302},"  key",[278,30720,358],{"class":298},[278,30722,30723],{"class":309},"\"",[278,30725,30726],{"class":650},"$1",[278,30728,30729],{"class":309},"\"\n",[278,30731,30732],{"class":280,"line":327},[278,30733,292],{"emptyLinePlaceholder":291},[278,30735,30736,30739,30742],{"class":280,"line":340},[278,30737,30738],{"class":298},"  case",[278,30740,30741],{"class":302}," $key ",[278,30743,30744],{"class":298},"in\n",[278,30746,30747,30750],{"class":280,"line":349},[278,30748,30749],{"class":17404},"    --vault",[278,30751,4590],{"class":298},[278,30753,30754,30757,30759,30761,30764],{"class":280,"line":375},[278,30755,30756],{"class":302},"      vault",[278,30758,358],{"class":298},[278,30760,30723],{"class":309},[278,30762,30763],{"class":650},"$2",[278,30765,30729],{"class":309},[278,30767,30768,30771],{"class":280,"line":386},[278,30769,30770],{"class":650},"      shift",[278,30772,30773],{"class":284}," # past argument\n",[278,30775,30776,30778],{"class":280,"line":397},[278,30777,30770],{"class":650},[278,30779,30780],{"class":284}," # past value\n",[278,30782,30783],{"class":280,"line":408},[278,30784,30785],{"class":302},"      ;;\n",[278,30787,30788,30791],{"class":280,"line":433},[278,30789,30790],{"class":17404},"    --item-name",[278,30792,4590],{"class":298},[278,30794,30795,30798,30800,30802,30804],{"class":280,"line":454},[278,30796,30797],{"class":302},"      item_name",[278,30799,358],{"class":298},[278,30801,30723],{"class":309},[278,30803,30763],{"class":650},[278,30805,30729],{"class":309},[278,30807,30808,30810],{"class":280,"line":475},[278,30809,30770],{"class":650},[278,30811,30773],{"class":284},[278,30813,30814,30816],{"class":280,"line":496},[278,30815,30770],{"class":650},[278,30817,30780],{"class":284},[278,30819,30820],{"class":280,"line":505},[278,30821,30785],{"class":302},[278,30823,30824,30827],{"class":280,"line":516},[278,30825,30826],{"class":17404},"    --env-file",[278,30828,4590],{"class":298},[278,30830,30831,30834,30836,30838,30840],{"class":280,"line":527},[278,30832,30833],{"class":302},"      env_file",[278,30835,358],{"class":298},[278,30837,30723],{"class":309},[278,30839,30763],{"class":650},[278,30841,30729],{"class":309},[278,30843,30844,30846],{"class":280,"line":533},[278,30845,30770],{"class":650},[278,30847,30773],{"class":284},[278,30849,30850,30852],{"class":280,"line":539},[278,30851,30770],{"class":650},[278,30853,30780],{"class":284},[278,30855,30856],{"class":280,"line":545},[278,30857,30785],{"class":302},[278,30859,30860],{"class":280,"line":551},[278,30861,30862],{"class":298},"    *)\n",[278,30864,30865],{"class":280,"line":557},[278,30866,30867],{"class":284},"      # unknown option\n",[278,30869,30870],{"class":280,"line":567},[278,30871,30872],{"class":650},"      shift\n",[278,30874,30875],{"class":280,"line":577},[278,30876,30785],{"class":302},[278,30878,30879],{"class":280,"line":587},[278,30880,30881],{"class":298},"  esac\n",[278,30883,30884],{"class":280,"line":597},[278,30885,30886],{"class":298},"done\n",[278,30888,30889],{"class":280,"line":608},[278,30890,292],{"emptyLinePlaceholder":291},[278,30892,30893],{"class":280,"line":614},[278,30894,30895],{"class":284},"# Check if the --vault, --item-name, and --env-file parameters were provided\n",[278,30897,30898,30900,30902,30905,30908,30911,30913,30916,30918,30920,30922,30924,30927,30929,30931,30933,30935,30937,30939,30942,30944,30946],{"class":280,"line":620},[278,30899,12721],{"class":298},[278,30901,30699],{"class":302},[278,30903,30904],{"class":298},"-z",[278,30906,30907],{"class":309}," \"",[278,30909,30910],{"class":302},"$vault",[278,30912,30723],{"class":309},[278,30914,30915],{"class":302}," ] ",[278,30917,5954],{"class":298},[278,30919,30699],{"class":302},[278,30921,30904],{"class":298},[278,30923,30907],{"class":309},[278,30925,30926],{"class":302},"$item_name",[278,30928,30723],{"class":309},[278,30930,30915],{"class":302},[278,30932,5954],{"class":298},[278,30934,30699],{"class":302},[278,30936,30904],{"class":298},[278,30938,30907],{"class":309},[278,30940,30941],{"class":302},"$env_file",[278,30943,30723],{"class":309},[278,30945,30710],{"class":302},[278,30947,30948],{"class":298},"then\n",[278,30950,30951,30954],{"class":280,"line":625},[278,30952,30953],{"class":650},"  echo",[278,30955,30956],{"class":309}," \"Please provide --vault, --item-name, and --env-file parameters.\"\n",[278,30958,30959,30962],{"class":280,"line":640},[278,30960,30961],{"class":650},"  exit",[278,30963,30964],{"class":650}," 1\n",[278,30966,30967],{"class":280,"line":663},[278,30968,30969],{"class":298},"fi\n",[278,30971,30972],{"class":280,"line":669},[278,30973,292],{"emptyLinePlaceholder":291},[278,30975,30976],{"class":280,"line":680},[278,30977,30978],{"class":284},"# Check if the specified .env file exists\n",[278,30980,30981,30983,30985,30987,30990,30992,30994,30996,30998],{"class":280,"line":686},[278,30982,12721],{"class":298},[278,30984,30699],{"class":302},[278,30986,1209],{"class":298},[278,30988,30989],{"class":298}," -f",[278,30991,30907],{"class":309},[278,30993,30941],{"class":302},[278,30995,30723],{"class":309},[278,30997,30710],{"class":302},[278,30999,30948],{"class":298},[278,31001,31002,31004],{"class":280,"line":1334},[278,31003,30953],{"class":650},[278,31005,31006],{"class":309}," \"The specified .env file does not exist.\"\n",[278,31008,31009,31011],{"class":280,"line":1375},[278,31010,30961],{"class":650},[278,31012,30964],{"class":650},[278,31014,31015],{"class":280,"line":1381},[278,31016,30969],{"class":298},[278,31018,31019],{"class":280,"line":1386},[278,31020,292],{"emptyLinePlaceholder":291},[278,31022,31023],{"class":280,"line":1394},[278,31024,31025],{"class":284},"# Check if the 1Password CLI is installed\n",[278,31027,31028,31030,31032,31035,31038,31041,31044,31047,31050,31052],{"class":280,"line":1406},[278,31029,12721],{"class":298},[278,31031,6192],{"class":298},[278,31033,31034],{"class":650}," command",[278,31036,31037],{"class":650}," -v",[278,31039,31040],{"class":309}," op",[278,31042,31043],{"class":298}," >",[278,31045,31046],{"class":309}," \u002Fdev\u002Fnull",[278,31048,31049],{"class":298}," 2>&1",[278,31051,1019],{"class":302},[278,31053,30948],{"class":298},[278,31055,31056,31058],{"class":280,"line":1423},[278,31057,30953],{"class":650},[278,31059,31060],{"class":309}," \"1Password CLI is not installed. Please install it before running this script.\"\n",[278,31062,31063,31065],{"class":280,"line":1432},[278,31064,30961],{"class":650},[278,31066,30964],{"class":650},[278,31068,31069],{"class":280,"line":1437},[278,31070,30969],{"class":298},[278,31072,31073],{"class":280,"line":1916},[278,31074,292],{"emptyLinePlaceholder":291},[278,31076,31077],{"class":280,"line":1939},[278,31078,31079],{"class":284},"# Check if the item already exists in the vault\n",[278,31081,31082,31084,31086,31089,31092,31094,31096,31098,31101,31103,31105,31107,31109,31111,31113,31115],{"class":280,"line":1949},[278,31083,12721],{"class":298},[278,31085,31040],{"class":333},[278,31087,31088],{"class":309}," item",[278,31090,31091],{"class":309}," get",[278,31093,30907],{"class":309},[278,31095,30926],{"class":302},[278,31097,30723],{"class":309},[278,31099,31100],{"class":650}," --vault",[278,31102,30907],{"class":309},[278,31104,30910],{"class":302},[278,31106,30723],{"class":309},[278,31108,31043],{"class":298},[278,31110,31046],{"class":309},[278,31112,31049],{"class":298},[278,31114,1019],{"class":302},[278,31116,30948],{"class":298},[278,31118,31119,31121,31124,31126,31129,31131],{"class":280,"line":1954},[278,31120,30953],{"class":650},[278,31122,31123],{"class":309}," \"Item '",[278,31125,30926],{"class":302},[278,31127,31128],{"class":309},"' already exists in the '",[278,31130,30910],{"class":302},[278,31132,31133],{"class":309},"' vault. Skipping creation.\"\n",[278,31135,31136,31138],{"class":280,"line":1959},[278,31137,30961],{"class":650},[278,31139,31140],{"class":650}," 0\n",[278,31142,31143],{"class":280,"line":1985},[278,31144,30969],{"class":298},[278,31146,31147],{"class":280,"line":1990},[278,31148,292],{"emptyLinePlaceholder":291},[278,31150,31151],{"class":280,"line":1997},[278,31152,31153],{"class":284},"# Variable to store the fields string\n",[278,31155,31156,31159,31161],{"class":280,"line":2006},[278,31157,31158],{"class":302},"fields",[278,31160,358],{"class":298},[278,31162,4601],{"class":302},[278,31164,31165,31168,31170],{"class":280,"line":2018},[278,31166,31167],{"class":302},"skipped_fields",[278,31169,358],{"class":298},[278,31171,4601],{"class":302},[278,31173,31174],{"class":280,"line":2029},[278,31175,292],{"emptyLinePlaceholder":291},[278,31177,31178],{"class":280,"line":2034},[278,31179,31180],{"class":284},"# Read the .env file line by line\n",[278,31182,31183,31185,31188,31190,31193,31196,31198,31200,31202,31205,31207,31210,31212,31214],{"class":280,"line":2040},[278,31184,20616],{"class":298},[278,31186,31187],{"class":302}," IFS",[278,31189,358],{"class":298},[278,31191,31192],{"class":650}," read",[278,31194,31195],{"class":650}," -r",[278,31197,15599],{"class":309},[278,31199,20111],{"class":298},[278,31201,30699],{"class":302},[278,31203,31204],{"class":298},"-n",[278,31206,30907],{"class":309},[278,31208,31209],{"class":302},"$line",[278,31211,30723],{"class":309},[278,31213,30710],{"class":302},[278,31215,30713],{"class":298},[278,31217,31218],{"class":280,"line":2045},[278,31219,31220],{"class":284},"  # Skip empty lines and comments\n",[278,31222,31223,31225,31227,31229,31231,31233,31235,31237,31239,31241,31244,31246,31249,31252,31255,31257,31259,31261,31263],{"class":280,"line":2068},[278,31224,1062],{"class":298},[278,31226,30699],{"class":302},[278,31228,30904],{"class":298},[278,31230,30907],{"class":309},[278,31232,31209],{"class":302},[278,31234,30723],{"class":309},[278,31236,30915],{"class":302},[278,31238,5954],{"class":298},[278,31240,30699],{"class":302},[278,31242,31243],{"class":309},"\"${",[278,31245,280],{"class":302},[278,31247,31248],{"class":298},"#",[278,31250,31251],{"class":309},"\"#\"}\"",[278,31253,31254],{"class":298}," !=",[278,31256,30907],{"class":309},[278,31258,31209],{"class":302},[278,31260,30723],{"class":309},[278,31262,30710],{"class":302},[278,31264,30948],{"class":298},[278,31266,31267],{"class":280,"line":2099},[278,31268,31269],{"class":298},"    continue\n",[278,31271,31272],{"class":280,"line":6428},[278,31273,31274],{"class":298},"  fi\n",[278,31276,31277],{"class":280,"line":6439},[278,31278,292],{"emptyLinePlaceholder":291},[278,31280,31281],{"class":280,"line":6450},[278,31282,31283],{"class":284},"  # Split the line into key and value\n",[278,31285,31286,31288,31290,31292,31294,31297,31299,31301],{"class":280,"line":6455},[278,31287,30718],{"class":302},[278,31289,358],{"class":298},[278,31291,31243],{"class":309},[278,31293,280],{"class":302},[278,31295,31296],{"class":298},"%%",[278,31298,358],{"class":309},[278,31300,1351],{"class":298},[278,31302,31303],{"class":309},"}\"\n",[278,31305,31306,31309,31311,31313,31315,31318],{"class":280,"line":6460},[278,31307,31308],{"class":302},"  value",[278,31310,358],{"class":298},[278,31312,31243],{"class":309},[278,31314,280],{"class":302},[278,31316,31317],{"class":298},"#*",[278,31319,31320],{"class":309},"=}\"\n",[278,31322,31323],{"class":280,"line":6475},[278,31324,292],{"emptyLinePlaceholder":291},[278,31326,31327],{"class":280,"line":6486},[278,31328,31329],{"class":284},"  # Check if the value is already a reference to a 1Password secret\n",[278,31331,31332,31334,31336,31338,31340,31342,31345,31348,31351,31353,31355,31358,31360,31362],{"class":280,"line":6491},[278,31333,1062],{"class":298},[278,31335,30699],{"class":302},[278,31337,31243],{"class":309},[278,31339,14768],{"class":302},[278,31341,31248],{"class":298},[278,31343,31344],{"class":302},"op",[278,31346,31347],{"class":298},":\u002F\u002F",[278,31349,31350],{"class":309},"}\"",[278,31352,31254],{"class":298},[278,31354,30907],{"class":309},[278,31356,31357],{"class":302},"$value",[278,31359,30723],{"class":309},[278,31361,30710],{"class":302},[278,31363,30948],{"class":298},[278,31365,31366,31369,31372,31375],{"class":280,"line":6518},[278,31367,31368],{"class":650},"    echo",[278,31370,31371],{"class":309}," \"Skipping creation of '",[278,31373,31374],{"class":302},"$key",[278,31376,31377],{"class":309},"' field as it is already a reference to a 1Password secret.\"\n",[278,31379,31380,31383,31385],{"class":280,"line":6530},[278,31381,31382],{"class":302},"    skipped_fields",[278,31384,6271],{"class":298},[278,31386,31387],{"class":302},"($line)\n",[278,31389,31390],{"class":280,"line":6542},[278,31391,31269],{"class":298},[278,31393,31394],{"class":280,"line":6547},[278,31395,31274],{"class":298},[278,31397,31398],{"class":280,"line":6552},[278,31399,292],{"emptyLinePlaceholder":291},[278,31401,31402],{"class":280,"line":6567},[278,31403,31404],{"class":284},"  # Add the key-value pair to the fields array\n",[278,31406,31407,31410,31412,31414,31416,31418,31420,31422,31424],{"class":280,"line":6580},[278,31408,31409],{"class":302},"  fields",[278,31411,6271],{"class":298},[278,31413,1126],{"class":302},[278,31415,30723],{"class":309},[278,31417,31374],{"class":302},[278,31419,358],{"class":309},[278,31421,31357],{"class":302},[278,31423,30723],{"class":309},[278,31425,4590],{"class":302},[278,31427,31428,31430,31432,31434,31436],{"class":280,"line":6593},[278,31429,15383],{"class":298},[278,31431,24568],{"class":298},[278,31433,30907],{"class":309},[278,31435,30941],{"class":302},[278,31437,30729],{"class":309},[278,31439,31440],{"class":280,"line":6605},[278,31441,292],{"emptyLinePlaceholder":291},[278,31443,31444],{"class":280,"line":6620},[278,31445,31446],{"class":284},"# Create the item in 1Password using the op create command\n",[278,31448,31449,31451,31453,31455],{"class":280,"line":6625},[278,31450,31344],{"class":333},[278,31452,31088],{"class":309},[278,31454,24553],{"class":309},[278,31456,26571],{"class":650},[278,31458,31459,31462,31465],{"class":280,"line":6633},[278,31460,31461],{"class":650},"  --category",[278,31463,31464],{"class":309}," Server",[278,31466,26571],{"class":650},[278,31468,31469,31472,31474,31476,31478],{"class":280,"line":6643},[278,31470,31471],{"class":650},"  --title",[278,31473,30907],{"class":309},[278,31475,30926],{"class":302},[278,31477,30723],{"class":309},[278,31479,26571],{"class":650},[278,31481,31482,31485,31487,31489,31491],{"class":280,"line":6657},[278,31483,31484],{"class":650},"  --vault",[278,31486,30907],{"class":309},[278,31488,30910],{"class":302},[278,31490,30723],{"class":309},[278,31492,26571],{"class":650},[278,31494,31495,31498,31500,31502,31505,31508],{"class":280,"line":6665},[278,31496,31497],{"class":309},"  \"${",[278,31499,31158],{"class":302},[278,31501,29860],{"class":309},[278,31503,31504],{"class":298},"@",[278,31506,31507],{"class":309},"]}\"",[278,31509,26571],{"class":650},[278,31511,31512,31515,31517,31519],{"class":280,"line":6670},[278,31513,31514],{"class":650},"  --tags",[278,31516,30907],{"class":309},[278,31518,30926],{"class":302},[278,31520,31521],{"class":309},",env\"\n",[278,31523,31524],{"class":280,"line":6675},[278,31525,292],{"emptyLinePlaceholder":291},[278,31527,31528],{"class":280,"line":6680},[278,31529,31530],{"class":284},"# Update the .env file with the secret references\n",[278,31532,31533,31536,31539,31541,31543,31546,31548,31551,31553,31556,31558,31561,31564,31567],{"class":280,"line":6698},[278,31534,31535],{"class":298},"for",[278,31537,31538],{"class":302}," ((i ",[278,31540,358],{"class":298},[278,31542,6588],{"class":650},[278,31544,31545],{"class":302},"; i ",[278,31547,1702],{"class":298},[278,31549,31550],{"class":302}," ${",[278,31552,31248],{"class":298},[278,31554,31555],{"class":302},"fields[",[278,31557,31504],{"class":298},[278,31559,31560],{"class":302},"]}; i",[278,31562,31563],{"class":298},"++",[278,31565,31566],{"class":302},")); ",[278,31568,30713],{"class":298},[278,31570,31571],{"class":280,"line":6725},[278,31572,31573],{"class":284},"  # Split the field into key and value\n",[278,31575,31576,31579,31581,31583,31585],{"class":280,"line":6738},[278,31577,31578],{"class":302},"  field",[278,31580,358],{"class":298},[278,31582,31243],{"class":309},[278,31584,31158],{"class":302},[278,31586,31587],{"class":309},"[i]}\"\n",[278,31589,31590,31593,31595,31598,31600,31602,31604,31606,31609,31611,31614],{"class":280,"line":6752},[278,31591,31592],{"class":302},"  IFS",[278,31594,358],{"class":298},[278,31596,31597],{"class":309},"\"=\"",[278,31599,31192],{"class":650},[278,31601,31195],{"class":650},[278,31603,22002],{"class":309},[278,31605,14706],{"class":309},[278,31607,31608],{"class":298}," \u003C\u003C\u003C",[278,31610,30907],{"class":309},[278,31612,31613],{"class":302},"$field",[278,31615,30729],{"class":309},[278,31617,31618],{"class":280,"line":6769},[278,31619,292],{"emptyLinePlaceholder":291},[278,31621,31622,31625,31627,31629,31631,31634,31636,31638,31640,31642,31644],{"class":280,"line":6786},[278,31623,31624],{"class":302},"  fields[i]",[278,31626,358],{"class":298},[278,31628,30723],{"class":309},[278,31630,31374],{"class":302},[278,31632,31633],{"class":309},"=op:\u002F\u002F",[278,31635,30910],{"class":302},[278,31637,13413],{"class":309},[278,31639,30926],{"class":302},[278,31641,13413],{"class":309},[278,31643,31374],{"class":302},[278,31645,30729],{"class":309},[278,31647,31648],{"class":280,"line":6798},[278,31649,30886],{"class":298},[278,31651,31652],{"class":280,"line":6803},[278,31653,292],{"emptyLinePlaceholder":291},[278,31655,31656,31659,31661,31663,31665,31667,31669,31671,31673,31676,31678,31680,31682,31684],{"class":280,"line":6815},[278,31657,31658],{"class":302},"final_fields",[278,31660,358],{"class":298},[278,31662,1126],{"class":302},[278,31664,31243],{"class":309},[278,31666,31158],{"class":302},[278,31668,29860],{"class":309},[278,31670,31504],{"class":298},[278,31672,31507],{"class":309},[278,31674,31675],{"class":309}," \"${",[278,31677,31167],{"class":302},[278,31679,29860],{"class":309},[278,31681,31504],{"class":298},[278,31683,31507],{"class":309},[278,31685,4590],{"class":302},[278,31687,31688],{"class":280,"line":6827},[278,31689,292],{"emptyLinePlaceholder":291},[278,31691,31692],{"class":280,"line":6839},[278,31693,31694],{"class":284},"# Overwrite the .env file with the updated key-value pairs\n",[278,31696,31697,31700,31703,31705,31707,31709,31711,31713,31715,31717,31719],{"class":280,"line":6844},[278,31698,31699],{"class":650},"printf",[278,31701,31702],{"class":309}," \"%s\\n\"",[278,31704,31675],{"class":309},[278,31706,31658],{"class":302},[278,31708,29860],{"class":309},[278,31710,31504],{"class":298},[278,31712,31507],{"class":309},[278,31714,31043],{"class":298},[278,31716,30907],{"class":309},[278,31718,30941],{"class":302},[278,31720,30729],{"class":309},[278,31722,31723],{"class":280,"line":6853},[278,31724,292],{"emptyLinePlaceholder":291},[278,31726,31727,31730,31732,31734,31737,31739],{"class":280,"line":6859},[278,31728,31729],{"class":650},"echo",[278,31731,31123],{"class":309},[278,31733,30926],{"class":302},[278,31735,31736],{"class":309},"' created successfully in the '",[278,31738,30910],{"class":302},[278,31740,31741],{"class":309},"' vault.\"\n",[278,31743,31744,31746,31749,31751],{"class":280,"line":6864},[278,31745,31729],{"class":650},[278,31747,31748],{"class":309}," \"Updated the '",[278,31750,30941],{"class":302},[278,31752,31753],{"class":309},"' file with the secret references.\"\n",[11,31755,31756],{},"We need to pass the vault name, the item entry name and the env file path while executing the script. Now this env file can be uploaded to the repo along with the rest of the code.",[269,31758,31760],{"className":3335,"code":31759,"language":3337,"meta":274,"style":274},".\u002Fsave-env.sh --vault AppVaults --item-name FamPro --env-file .\u002Fclient\u002F.env\n",[59,31761,31762],{"__ignoreMap":274},[278,31763,31764,31767,31769,31772,31775,31778,31781],{"class":280,"line":281},[278,31765,31766],{"class":333},".\u002Fsave-env.sh",[278,31768,31100],{"class":650},[278,31770,31771],{"class":309}," AppVaults",[278,31773,31774],{"class":650}," --item-name",[278,31776,31777],{"class":309}," FamPro",[278,31779,31780],{"class":650}," --env-file",[278,31782,31783],{"class":309}," .\u002Fclient\u002F.env\n",[269,31785,31787],{"className":3335,"code":31786,"language":3337,"meta":274,"style":274},"APPWRITE_ENDPOINT=op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_ENDPOINT\nAPPWRITE_PROJECT_ID=op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_PROJECT_ID\nAPPWRITE_DATABASE_ID=op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_DATABASE_ID\nAPPWRITE_JAR_COLLECTION_ID=op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_JAR_COLLECTION_ID\nAPPWRITE_TRANSACTION_COLLECTION_ID=op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_TRANSACTION_COLLECTION_ID\nAPPWRITE_API_KEY=op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_API_KEY\nPASSAGE_APP_ID=op:\u002F\u002FAppVaults\u002FFamPro\u002FPASSAGE_APP_ID\nPASSAGE_API_KEY=op:\u002F\u002FAppVaults\u002FFamPro\u002FPASSAGE_API_KEY\n",[59,31788,31789,31799,31809,31819,31829,31839,31849,31859],{"__ignoreMap":274},[278,31790,31791,31794,31796],{"class":280,"line":281},[278,31792,31793],{"class":302},"APPWRITE_ENDPOINT",[278,31795,358],{"class":298},[278,31797,31798],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_ENDPOINT\n",[278,31800,31801,31804,31806],{"class":280,"line":288},[278,31802,31803],{"class":302},"APPWRITE_PROJECT_ID",[278,31805,358],{"class":298},[278,31807,31808],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_PROJECT_ID\n",[278,31810,31811,31814,31816],{"class":280,"line":295},[278,31812,31813],{"class":302},"APPWRITE_DATABASE_ID",[278,31815,358],{"class":298},[278,31817,31818],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_DATABASE_ID\n",[278,31820,31821,31824,31826],{"class":280,"line":316},[278,31822,31823],{"class":302},"APPWRITE_JAR_COLLECTION_ID",[278,31825,358],{"class":298},[278,31827,31828],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_JAR_COLLECTION_ID\n",[278,31830,31831,31834,31836],{"class":280,"line":322},[278,31832,31833],{"class":302},"APPWRITE_TRANSACTION_COLLECTION_ID",[278,31835,358],{"class":298},[278,31837,31838],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_TRANSACTION_COLLECTION_ID\n",[278,31840,31841,31844,31846],{"class":280,"line":327},[278,31842,31843],{"class":302},"APPWRITE_API_KEY",[278,31845,358],{"class":298},[278,31847,31848],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FAPPWRITE_API_KEY\n",[278,31850,31851,31854,31856],{"class":280,"line":340},[278,31852,31853],{"class":302},"PASSAGE_APP_ID",[278,31855,358],{"class":298},[278,31857,31858],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FPASSAGE_APP_ID\n",[278,31860,31861,31864,31866],{"class":280,"line":349},[278,31862,31863],{"class":302},"PASSAGE_API_KEY",[278,31865,358],{"class":298},[278,31867,31868],{"class":309},"op:\u002F\u002FAppVaults\u002FFamPro\u002FPASSAGE_API_KEY\n",[11,31870,31871],{},"To load the secrets back into the env file we can use the below command (with the correct file paths)",[269,31873,31875],{"className":3335,"code":31874,"language":3337,"meta":274,"style":274},"op inject -i .env_template -o .env\n",[59,31876,31877],{"__ignoreMap":274},[278,31878,31879,31881,31884,31887,31890,31893],{"class":280,"line":281},[278,31880,31344],{"class":333},[278,31882,31883],{"class":309}," inject",[278,31885,31886],{"class":650}," -i",[278,31888,31889],{"class":309}," .env_template",[278,31891,31892],{"class":650}," -o",[278,31894,31895],{"class":309}," .env\n",[24,31897,31899],{"id":31898},"supported-features","Supported features",[11,31901,31902],{},"The app supports the following features at the moment",[123,31904,31905,31908,31911,31914,31917],{},[74,31906,31907],{},"Creating a family account.",[74,31909,31910],{},"Adding members to the family with a member or child role. Only a user with a member role can add other members.",[74,31912,31913],{},"Creating multiple jars per user (member or child). Again, only users with a member role can create jars.",[74,31915,31916],{},"Making ad-hoc transactions against a jar. A user with a member role can make transactions in any of the jars in the family. A child can only view and do transactions in their jars.",[74,31918,31919],{},"Transactions done by a user with a child role remain pending. These transactions need to be approved by a user with a member role before the jar balance can be updated.",[24,31921,31922],{"id":10535},"Further enhancements",[123,31924,31925,31928,31931],{},[74,31926,31927],{},"Improve the dashboard experience",[74,31929,31930],{},"Allow deletion of member accounts",[74,31932,31933],{},"Allow modification and deletion of transactions",[24,31935,31937],{"id":31936},"app-source-code-and-link","App source code and link",[11,31939,31940],{},"The full source code of the app along with the 1Password CLI script can be found here",[40,31942],{"url":31943},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002FFamPro",[11,31945,31946,31947,183],{},"The code for the appwrite shell plugin can be checked in ",[47,31948,31951],{"href":31949,"rel":31950},"https:\u002F\u002Fgithub.com\u002F1Password\u002Fshell-plugins\u002Fpull\u002F336",[51],"this PR",[11,31953,31954,31955],{},"You can play around with the app here: ",[47,31956,31957],{"href":31957,"rel":31958},"https:\u002F\u002Ffam-pro.vercel.app\u002F",[51],[24,31960,10634],{"id":10633},[11,31962,31963,31964,31967],{},"Overall it was a great experience building with 1Password, Appwrite and Nuxt3. I did face some challenges in the process outlined above, but we could overcome those. 1Password offers a seamless auth experience and it would be interesting to see what features they add on top of the existing functionality. I also thank the ",[47,31965,24461],{"href":24459,"rel":31966},[51]," team for organizing this hackathon.",[11,31969,31970],{},"I hope you enjoyed reading the article. It would mean a lot to me if you can show appreciation by sharing your feedback. If you found any mistake then please let me know in the comments.",[11,31972,31973],{},[3061,31974,24390],{},[3065,31976,31977],{},"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 .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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}",{"title":274,"searchDepth":288,"depth":288,"links":31979},[31980,31981,31986,31992,31993,31994,31995,31996],{"id":22771,"depth":288,"text":22772},{"id":26249,"depth":288,"text":26250,"children":31982},[31983,31984,31985],{"id":26276,"depth":295,"text":26277},{"id":26304,"depth":295,"text":26305},{"id":26622,"depth":295,"text":26623},{"id":27367,"depth":288,"text":27368,"children":31987},[31988,31989,31990,31991],{"id":27446,"depth":295,"text":27447},{"id":27785,"depth":295,"text":27786},{"id":30344,"depth":295,"text":30345},{"id":30362,"depth":295,"text":30363},{"id":30669,"depth":288,"text":30670},{"id":31898,"depth":288,"text":31899},{"id":10535,"depth":288,"text":31922},{"id":31936,"depth":288,"text":31937},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite\u002Fea13967cd552231b6545aab22352a803-8a1198ee04.jpeg","2023-07-01T06:35:17.622Z","Last year I built an app for my son (and myself). The app idea was very simple, a digital piggy bank tracker integrated with optional auto credit of pocket money. We're actively...","cljjmpomt000n09jvf4097h23",{},"\u002Fcreating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite",{"title":26205,"description":31999},"creating-1password-plugin-and-use-it-to-build-an-app-with-nuxt3-passage-appwrite",[10710,26565,32006,32007,32008],"1password","buildwith1password","1password-hackathon","k5FrEXdBirHUtwjEpqql0JvyhV50JnRRrPaxR-vJUP0",{"id":32011,"title":32012,"body":32013,"cover":34823,"date":34824,"description":34825,"draft":3086,"extension":3087,"hashnodeId":34826,"meta":34827,"navigation":291,"path":34828,"seo":34829,"slug":34830,"stem":34830,"tags":34831,"__hash__":34833},"posts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension.md","Creating an OpenAI powered Writing Assistant for VS Code",{"type":8,"value":32014,"toc":34811},[32015,32023,32025,32028,32031,32037,32040,32044,32064,32074,32080,32086,32093,32099,32103,32112,32136,32142,32157,32161,32177,32234,32237,32243,32247,32256,32303,32310,32319,32615,32637,32646,32652,33003,33006,33012,33015,33019,33025,33033,33167,33173,33201,33220,33253,33259,33466,33472,33475,33479,33482,33485,33499,33504,33523,33528,34460,34463,34469,34472,34476,34492,34635,34642,34762,34765,34767,34770,34787,34794,34801,34804,34808],[11,32016,32017,32018,32022],{},"Rabbit holes are many, and as a developer, you keep falling into one or the other during your journey. This is not necessarily a bad thing—granted that it may frustrate you—that is how you learn something extra (the 0.01 of 1.01",[32019,32020,32021],"sup",{},"365","). This is one such story where my urge to create something new pushed me into writing a brand new VS Code extension.",[24,32024,22772],{"id":22771},[11,32026,32027],{},"I've always wondered how a VS Code extension works and what it takes to implement one. Recently this urge got the best of me and I decided to finally create a VS Code extension. But what to build? I like using Hashnode's AI editor for rephrasing texts and other writing assistance, so I thought why not implement a similar functionality for VS Code? Since writing assistance is most useful for a markdown file (which can also work as a blog post), I created a basic writing assistant for markdown (and text) files.",[11,32029,32030],{},"Here is a simple GIF showcasing how the extension works.",[11,32032,32033],{},[3135,32034],{"alt":32035,"src":32036},"Write Assist AI working gif","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002Fd8a0ae80-fa3e-4b76-ba45-89146a35ef6b-270734e4f0.gif",[11,32038,32039],{},"Ready to dive into how I implemented it? Let's get started.",[24,32041,32043],{"id":32042},"getting-started","Getting Started",[11,32045,32046,32047,919,32052,32057,32058,32063],{},"Before we can start building the extension, we need to gather and prepare the necessary tools. In this case, the needed tools are node, git, ",[47,32048,32051],{"href":32049,"rel":32050},"https:\u002F\u002Fyeoman.io\u002F",[51],"yeoman",[47,32053,32056],{"href":32054,"rel":32055},"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fgenerator-code",[51],"generator-code",". For a newcomer like myself, ",[47,32059,32062],{"href":32060,"rel":32061},"https:\u002F\u002Fcode.visualstudio.com\u002Fapi\u002Fget-started\u002Fyour-first-extension",[51],"this basic tutorial"," is perfect. I recommend going through it to learn the fundamentals.",[11,32065,26533,32066,32069,32070,32073],{},[59,32067,32068],{},"yo code"," command and pick ",[59,32071,32072],{},"New Extension (js\u002Fts)"," from the choices as shown below",[11,32075,32076],{},[3135,32077],{"alt":32078,"src":32079},"yo code command choices","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002F103a52d9-c518-4e23-8694-fae49d972fce-1e72801190.png",[11,32081,32082],{},[3135,32083],{"alt":32084,"src":32085},"All steps of the yo code command","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002F569c6d97-1a09-4c45-8fe9-9e0e9ddc73a8-c9e17c625f.png",[11,32087,32088,32089,32092],{},"I haven't selected the ",[59,32090,32091],{},"webpack"," option yet. We can always do so later on. This is my current directory structure.",[11,32094,32095],{},[3135,32096],{"alt":32097,"src":32098},"Basic directory structure for a VS Code extension","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002Fefee7119-e8fc-4bdd-9640-9507fc7197ed-d2d2ac73cb.png",[24,32100,32102],{"id":32101},"implementing-the-extension","Implementing the extension",[11,32104,32105,32106,919,32108,32111],{},"We're mostly interested in the ",[59,32107,5686],{},[59,32109,32110],{},"extension.ts"," files while building the extension. The important fields in the package.json file are",[123,32113,32114,32120,32130],{},[74,32115,32116,32119],{},[94,32117,32118],{},"activationEvents",": When should our extension be activated",[74,32121,32122,32125,32126,32129],{},[94,32123,32124],{},"main",": The entry point of the extension code (the entry is in the ",[59,32127,32128],{},"out"," folder which gets generated when you debug or run the extension)",[74,32131,32132,32135],{},[94,32133,32134],{},"contributes",": What does this extension contributes to the VS Code commands and settings",[11,32137,32138],{},[3135,32139],{"alt":32140,"src":32141},"package.json fields","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002F5d4626a4-1d24-4809-9080-03883ff31b23-0a34819095.png",[11,32143,32144,32145,32150,32151,32156],{},"For my extension I wanted an experience similar to the Hashnode AI Editor, so adding commands to the VS Code command palette was not what I was after. What helped me here was the sample extensions directory on ",[47,32146,32149],{"href":32147,"rel":32148},"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode-extension-samples\u002F",[51],"GitHub",". Their ",[47,32152,32155],{"href":32153,"rel":32154},"https:\u002F\u002Fgithub.com\u002Fmicrosoft\u002Fvscode-extension-samples\u002Ftree\u002Fmain\u002Fcode-actions-sample",[51],"code-actions"," sample was exactly what I had in mind (and it targets only the markdown files).",[32,32158,32160],{"id":32159},"target-markdown-and-text-files","Target Markdown and Text files",[11,32162,32163,32164,32166,32167,919,32170,32173,32174,32176],{},"To target specific types of files you need to change the ",[59,32165,32118],{},". Since we want to work only with ",[59,32168,32169],{},"Markdown",[59,32171,32172],{},"Text"," files at the moment, this is what my ",[59,32175,5686],{}," says",[269,32178,32180],{"className":5690,"code":32179,"language":1310,"meta":274,"style":274},"\u002F\u002F ...\n\"activationEvents\": [\n  \"onLanguage:markdown\",\n  \"onLanguage:plaintext\"\n],\n\"main\": \".\u002Fout\u002Fextension.js\",\n\"contributes\": {},\n\u002F\u002F ...\n",[59,32181,32182,32186,32194,32201,32206,32210,32222,32230],{"__ignoreMap":274},[278,32183,32184],{"class":280,"line":281},[278,32185,319],{"class":284},[278,32187,32188,32191],{"class":280,"line":288},[278,32189,32190],{"class":309},"\"activationEvents\"",[278,32192,32193],{"class":302},": [\n",[278,32195,32196,32199],{"class":280,"line":295},[278,32197,32198],{"class":309},"  \"onLanguage:markdown\"",[278,32200,660],{"class":302},[278,32202,32203],{"class":280,"line":316},[278,32204,32205],{"class":309},"  \"onLanguage:plaintext\"\n",[278,32207,32208],{"class":280,"line":322},[278,32209,3533],{"class":302},[278,32211,32212,32215,32217,32220],{"class":280,"line":327},[278,32213,32214],{"class":309},"\"main\"",[278,32216,1155],{"class":302},[278,32218,32219],{"class":309},"\".\u002Fout\u002Fextension.js\"",[278,32221,660],{"class":302},[278,32223,32224,32227],{"class":280,"line":340},[278,32225,32226],{"class":309},"\"contributes\"",[278,32228,32229],{"class":302},": {},\n",[278,32231,32232],{"class":280,"line":349},[278,32233,319],{"class":284},[11,32235,32236],{},"This extension will only get activated for the languages mentioned above.",[11,32238,32239,32240,32242],{},"Also, since we don't want any command palette commands, we can remove everything under the ",[59,32241,32134],{}," key.",[32,32244,32246],{"id":32245},"showing-the-actions-in-the-light-bulb-menu","Showing the actions in the light bulb menu",[11,32248,32249,32250,3855,32253,32255],{},"There is one function called ",[59,32251,32252],{},"activate",[59,32254,32110],{}," file which is of interest. This is where you need to write your code. Notice that I've removed all the unnecessary boilerplate code",[269,32257,32259],{"className":271,"code":32258,"language":273,"meta":274,"style":274},"\u002F\u002F This method is called when your extension is activated\n\u002F\u002F Your extension is activated the very first time the \n\u002F\u002F command is executed\nexport function activate(context: vscode.ExtensionContext) {}\n",[59,32260,32261,32266,32271,32276],{"__ignoreMap":274},[278,32262,32263],{"class":280,"line":281},[278,32264,32265],{"class":284},"\u002F\u002F This method is called when your extension is activated\n",[278,32267,32268],{"class":280,"line":288},[278,32269,32270],{"class":284},"\u002F\u002F Your extension is activated the very first time the \n",[278,32272,32273],{"class":280,"line":295},[278,32274,32275],{"class":284},"\u002F\u002F command is executed\n",[278,32277,32278,32280,32282,32285,32287,32290,32292,32295,32297,32300],{"class":280,"line":316},[278,32279,628],{"class":298},[278,32281,748],{"class":298},[278,32283,32284],{"class":333}," activate",[278,32286,1126],{"class":302},[278,32288,32289],{"class":501},"context",[278,32291,960],{"class":298},[278,32293,32294],{"class":333}," vscode",[278,32296,183],{"class":302},[278,32298,32299],{"class":333},"ExtensionContext",[278,32301,32302],{"class":302},") {}\n",[11,32304,32305,32306,32309],{},"There is another complementary function called ",[59,32307,32308],{},"deactivate"," which can be used if you need to do any kind of resource cleaning before the extension is deactivated.",[11,32311,32312,32313,32315,32316,32318],{},"If you run\u002Fdebug the extension now, and create a new markdown file in the new VS Code window which pops up, you won't notice any difference as there is no palette command, and also there is no code in the ",[59,32314,32252],{}," function. Let's change that and add the following in the ",[59,32317,32110],{}," file",[269,32320,32322],{"className":271,"code":32321,"language":273,"meta":274,"style":274},"class MyCodeActionProvider implements vscode.CodeActionProvider {\n  \n  provideCodeActions(\n    document: vscode.TextDocument,\n    range: vscode.Range | vscode.Selection,\n    context: vscode.CodeActionContext,\n    token: vscode.CancellationToken\n  ): vscode.ProviderResult\u003C(vscode.CodeAction | vscode.Command)[]> {\n    console.log('inside the provideCodeActions method');\n    throw new Error('Method not implemented.');\n  }\n}\n\nexport function activate(context: vscode.ExtensionContext) {\n  const actionProvider = vscode.languages.registerCodeActionsProvider(\n    ['markdown', 'plaintext'],\n    new MyCodeActionProvider(),\n    {\n      providedCodeActionKinds: [\n        vscode.CodeActionKind.RefactorRewrite,\n        vscode.CodeActionKind.QuickFix,\n      ],\n    }\n  );\n\n  context.subscriptions.push(actionProvider);\n}\n",[59,32323,32324,32343,32348,32355,32371,32396,32412,32426,32462,32475,32490,32494,32498,32502,32524,32541,32556,32565,32569,32574,32579,32584,32589,32593,32597,32601,32611],{"__ignoreMap":274},[278,32325,32326,32329,32332,32334,32336,32338,32341],{"class":280,"line":281},[278,32327,32328],{"class":298},"class",[278,32330,32331],{"class":333}," MyCodeActionProvider",[278,32333,23651],{"class":298},[278,32335,32294],{"class":333},[278,32337,183],{"class":302},[278,32339,32340],{"class":333},"CodeActionProvider",[278,32342,876],{"class":302},[278,32344,32345],{"class":280,"line":288},[278,32346,32347],{"class":302},"  \n",[278,32349,32350,32353],{"class":280,"line":295},[278,32351,32352],{"class":333},"  provideCodeActions",[278,32354,770],{"class":302},[278,32356,32357,32360,32362,32364,32366,32369],{"class":280,"line":316},[278,32358,32359],{"class":501},"    document",[278,32361,960],{"class":298},[278,32363,32294],{"class":333},[278,32365,183],{"class":302},[278,32367,32368],{"class":333},"TextDocument",[278,32370,660],{"class":302},[278,32372,32373,32376,32378,32380,32382,32385,32387,32389,32391,32394],{"class":280,"line":322},[278,32374,32375],{"class":501},"    range",[278,32377,960],{"class":298},[278,32379,32294],{"class":333},[278,32381,183],{"class":302},[278,32383,32384],{"class":333},"Range",[278,32386,1621],{"class":298},[278,32388,32294],{"class":333},[278,32390,183],{"class":302},[278,32392,32393],{"class":333},"Selection",[278,32395,660],{"class":302},[278,32397,32398,32401,32403,32405,32407,32410],{"class":280,"line":327},[278,32399,32400],{"class":501},"    context",[278,32402,960],{"class":298},[278,32404,32294],{"class":333},[278,32406,183],{"class":302},[278,32408,32409],{"class":333},"CodeActionContext",[278,32411,660],{"class":302},[278,32413,32414,32417,32419,32421,32423],{"class":280,"line":340},[278,32415,32416],{"class":501},"    token",[278,32418,960],{"class":298},[278,32420,32294],{"class":333},[278,32422,183],{"class":302},[278,32424,32425],{"class":333},"CancellationToken\n",[278,32427,32428,32430,32432,32434,32436,32439,32442,32445,32447,32450,32452,32454,32456,32459],{"class":280,"line":349},[278,32429,23763],{"class":302},[278,32431,960],{"class":298},[278,32433,32294],{"class":333},[278,32435,183],{"class":302},[278,32437,32438],{"class":333},"ProviderResult",[278,32440,32441],{"class":302},"\u003C(",[278,32443,32444],{"class":333},"vscode",[278,32446,183],{"class":302},[278,32448,32449],{"class":333},"CodeAction",[278,32451,1621],{"class":298},[278,32453,32294],{"class":333},[278,32455,183],{"class":302},[278,32457,32458],{"class":333},"Command",[278,32460,32461],{"class":302},")[]> {\n",[278,32463,32464,32466,32468,32470,32473],{"class":280,"line":375},[278,32465,1409],{"class":302},[278,32467,14851],{"class":333},[278,32469,1126],{"class":302},[278,32471,32472],{"class":309},"'inside the provideCodeActions method'",[278,32474,1280],{"class":302},[278,32476,32477,32479,32481,32483,32485,32488],{"class":280,"line":386},[278,32478,1426],{"class":298},[278,32480,1258],{"class":298},[278,32482,1261],{"class":333},[278,32484,1126],{"class":302},[278,32486,32487],{"class":309},"'Method not implemented.'",[278,32489,1280],{"class":302},[278,32491,32492],{"class":280,"line":397},[278,32493,1096],{"class":302},[278,32495,32496],{"class":280,"line":408},[278,32497,617],{"class":302},[278,32499,32500],{"class":280,"line":433},[278,32501,292],{"emptyLinePlaceholder":291},[278,32503,32504,32506,32508,32510,32512,32514,32516,32518,32520,32522],{"class":280,"line":454},[278,32505,628],{"class":298},[278,32507,748],{"class":298},[278,32509,32284],{"class":333},[278,32511,1126],{"class":302},[278,32513,32289],{"class":501},[278,32515,960],{"class":298},[278,32517,32294],{"class":333},[278,32519,183],{"class":302},[278,32521,32299],{"class":333},[278,32523,1718],{"class":302},[278,32525,32526,32528,32531,32533,32536,32539],{"class":280,"line":475},[278,32527,758],{"class":298},[278,32529,32530],{"class":650}," actionProvider",[278,32532,764],{"class":298},[278,32534,32535],{"class":302}," vscode.languages.",[278,32537,32538],{"class":333},"registerCodeActionsProvider",[278,32540,770],{"class":302},[278,32542,32543,32546,32549,32551,32554],{"class":280,"line":496},[278,32544,32545],{"class":302},"    [",[278,32547,32548],{"class":309},"'markdown'",[278,32550,1708],{"class":302},[278,32552,32553],{"class":309},"'plaintext'",[278,32555,3533],{"class":302},[278,32557,32558,32561,32563],{"class":280,"line":505},[278,32559,32560],{"class":298},"    new",[278,32562,32331],{"class":333},[278,32564,4664],{"class":302},[278,32566,32567],{"class":280,"line":516},[278,32568,2209],{"class":302},[278,32570,32571],{"class":280,"line":527},[278,32572,32573],{"class":302},"      providedCodeActionKinds: [\n",[278,32575,32576],{"class":280,"line":533},[278,32577,32578],{"class":302},"        vscode.CodeActionKind.RefactorRewrite,\n",[278,32580,32581],{"class":280,"line":539},[278,32582,32583],{"class":302},"        vscode.CodeActionKind.QuickFix,\n",[278,32585,32586],{"class":280,"line":545},[278,32587,32588],{"class":302},"      ],\n",[278,32590,32591],{"class":280,"line":551},[278,32592,1285],{"class":302},[278,32594,32595],{"class":280,"line":557},[278,32596,611],{"class":302},[278,32598,32599],{"class":280,"line":567},[278,32600,292],{"emptyLinePlaceholder":291},[278,32602,32603,32606,32608],{"class":280,"line":577},[278,32604,32605],{"class":302},"  context.subscriptions.",[278,32607,6524],{"class":333},[278,32609,32610],{"class":302},"(actionProvider);\n",[278,32612,32613],{"class":280,"line":587},[278,32614,617],{"class":302},[11,32616,32617,32618,19634,32621,32624,32625,32628,32629,32632,32633,32636],{},"What we're doing here is informing VS Code about the kind of code actions we're providing which include ",[59,32619,32620],{},"RefactorRewrite",[59,32622,32623],{},"QuickFix",". The actual ",[59,32626,32627],{},"\"commands\u002Fcode actions\""," need to be provided by the ",[59,32630,32631],{},"provideCodeActions"," method of the ",[59,32634,32635],{},"MyCodeActionProvider"," class. If you debug the extension now you should see the below console log in your original project window's debug console.",[269,32638,32640],{"className":20708,"code":32639,"language":20710,"meta":274,"style":274},"inside the provideCodeActions method\n",[59,32641,32642],{"__ignoreMap":274},[278,32643,32644],{"class":280,"line":281},[278,32645,32639],{},[11,32647,32648,32649,32651],{},"We're making progress. Replace the ",[59,32650,32631],{}," method's code with the following",[269,32653,32655],{"className":271,"code":32654,"language":273,"meta":274,"style":274},"provideCodeActions(\n  document: vscode.TextDocument,\n  range: vscode.Range | vscode.Selection,\n  context: vscode.CodeActionContext,\n  token: vscode.CancellationToken\n): vscode.ProviderResult\u003C(vscode.CodeAction | vscode.Command)[]> {\n  \u002F\u002F If there is nothing selected, we won't provide any action\n  if (range.isEmpty) {\n    return;\n  }\n\n  \u002F\u002F supported actions and their kinds\n  const actions = [\n    {\n      id: 'rephrase',\n      title: 'Rephrase selected text',\n      kind: vscode.CodeActionKind.QuickFix,\n    },\n    {\n      id: 'headlines',\n      title: 'Suggest headlines',\n      kind: vscode.CodeActionKind.QuickFix,\n    },\n    {\n      id: 'professional',\n      title: 'Rewrite in professional tone',\n      kind: vscode.CodeActionKind.RefactorRewrite,\n    },\n    {\n      id: 'casual',\n      title: 'Rewrite in casual tone',\n      kind: vscode.CodeActionKind.RefactorRewrite,\n    },\n  ];\n\n  const cActions = [];\n  \u002F\u002F prepare the code actions for the above actions\n  for (const action of actions) {\n    const cAction = new vscode.CodeAction(action.title, action.kind);\n    cAction.command = {\n      command: `my-shiny-extension.${action.id}`,\n      title: action.title,\n      arguments: [action.id],\n    };\n\n    cActions.push(cAction);\n  }\n\n  return cActions;\n}\n",[59,32656,32657,32663,32668,32678,32683,32688,32707,32712,32719,32725,32729,32733,32738,32747,32751,32761,32771,32776,32780,32784,32793,32802,32806,32810,32814,32823,32832,32837,32841,32845,32854,32863,32867,32871,32875,32879,32888,32893,32911,32930,32939,32958,32963,32968,32972,32976,32986,32990,32994,32999],{"__ignoreMap":274},[278,32658,32659,32661],{"class":280,"line":281},[278,32660,32631],{"class":333},[278,32662,770],{"class":302},[278,32664,32665],{"class":280,"line":288},[278,32666,32667],{"class":302},"  document: vscode.TextDocument,\n",[278,32669,32670,32673,32675],{"class":280,"line":295},[278,32671,32672],{"class":302},"  range: vscode.Range ",[278,32674,1032],{"class":298},[278,32676,32677],{"class":302}," vscode.Selection,\n",[278,32679,32680],{"class":280,"line":316},[278,32681,32682],{"class":302},"  context: vscode.CodeActionContext,\n",[278,32684,32685],{"class":280,"line":322},[278,32686,32687],{"class":302},"  token: vscode.CancellationToken\n",[278,32689,32690,32693,32695,32698,32700,32703,32705],{"class":280,"line":327},[278,32691,32692],{"class":302},"): vscode.ProviderResult",[278,32694,1702],{"class":298},[278,32696,32697],{"class":302},"(vscode.CodeAction ",[278,32699,1032],{"class":298},[278,32701,32702],{"class":302}," vscode.Command)[]",[278,32704,1074],{"class":298},[278,32706,876],{"class":302},[278,32708,32709],{"class":280,"line":340},[278,32710,32711],{"class":284},"  \u002F\u002F If there is nothing selected, we won't provide any action\n",[278,32713,32714,32716],{"class":280,"line":349},[278,32715,1062],{"class":333},[278,32717,32718],{"class":302}," (range.isEmpty) {\n",[278,32720,32721,32723],{"class":280,"line":375},[278,32722,1088],{"class":298},[278,32724,313],{"class":302},[278,32726,32727],{"class":280,"line":386},[278,32728,1096],{"class":302},[278,32730,32731],{"class":280,"line":397},[278,32732,292],{"emptyLinePlaceholder":291},[278,32734,32735],{"class":280,"line":408},[278,32736,32737],{"class":284},"  \u002F\u002F supported actions and their kinds\n",[278,32739,32740,32743,32745],{"class":280,"line":433},[278,32741,32742],{"class":302},"  const actions ",[278,32744,358],{"class":298},[278,32746,5876],{"class":302},[278,32748,32749],{"class":280,"line":454},[278,32750,2209],{"class":302},[278,32752,32753,32756,32759],{"class":280,"line":475},[278,32754,32755],{"class":302},"      id: ",[278,32757,32758],{"class":309},"'rephrase'",[278,32760,660],{"class":302},[278,32762,32763,32766,32769],{"class":280,"line":496},[278,32764,32765],{"class":302},"      title: ",[278,32767,32768],{"class":309},"'Rephrase selected text'",[278,32770,660],{"class":302},[278,32772,32773],{"class":280,"line":505},[278,32774,32775],{"class":302},"      kind: vscode.CodeActionKind.QuickFix,\n",[278,32777,32778],{"class":280,"line":516},[278,32779,2243],{"class":302},[278,32781,32782],{"class":280,"line":527},[278,32783,2209],{"class":302},[278,32785,32786,32788,32791],{"class":280,"line":533},[278,32787,32755],{"class":302},[278,32789,32790],{"class":309},"'headlines'",[278,32792,660],{"class":302},[278,32794,32795,32797,32800],{"class":280,"line":539},[278,32796,32765],{"class":302},[278,32798,32799],{"class":309},"'Suggest headlines'",[278,32801,660],{"class":302},[278,32803,32804],{"class":280,"line":545},[278,32805,32775],{"class":302},[278,32807,32808],{"class":280,"line":551},[278,32809,2243],{"class":302},[278,32811,32812],{"class":280,"line":557},[278,32813,2209],{"class":302},[278,32815,32816,32818,32821],{"class":280,"line":567},[278,32817,32755],{"class":302},[278,32819,32820],{"class":309},"'professional'",[278,32822,660],{"class":302},[278,32824,32825,32827,32830],{"class":280,"line":577},[278,32826,32765],{"class":302},[278,32828,32829],{"class":309},"'Rewrite in professional tone'",[278,32831,660],{"class":302},[278,32833,32834],{"class":280,"line":587},[278,32835,32836],{"class":302},"      kind: vscode.CodeActionKind.RefactorRewrite,\n",[278,32838,32839],{"class":280,"line":597},[278,32840,2243],{"class":302},[278,32842,32843],{"class":280,"line":608},[278,32844,2209],{"class":302},[278,32846,32847,32849,32852],{"class":280,"line":614},[278,32848,32755],{"class":302},[278,32850,32851],{"class":309},"'casual'",[278,32853,660],{"class":302},[278,32855,32856,32858,32861],{"class":280,"line":620},[278,32857,32765],{"class":302},[278,32859,32860],{"class":309},"'Rewrite in casual tone'",[278,32862,660],{"class":302},[278,32864,32865],{"class":280,"line":625},[278,32866,32836],{"class":302},[278,32868,32869],{"class":280,"line":640},[278,32870,2243],{"class":302},[278,32872,32873],{"class":280,"line":663},[278,32874,5916],{"class":302},[278,32876,32877],{"class":280,"line":669},[278,32878,292],{"emptyLinePlaceholder":291},[278,32880,32881,32884,32886],{"class":280,"line":680},[278,32882,32883],{"class":302},"  const cActions ",[278,32885,358],{"class":298},[278,32887,6483],{"class":302},[278,32889,32890],{"class":280,"line":686},[278,32891,32892],{"class":284},"  \u002F\u002F prepare the code actions for the above actions\n",[278,32894,32895,32897,32899,32901,32904,32906,32909],{"class":280,"line":1334},[278,32896,12738],{"class":333},[278,32898,1245],{"class":302},[278,32900,5416],{"class":501},[278,32902,32903],{"class":501}," action",[278,32905,12022],{"class":501},[278,32907,32908],{"class":501}," actions",[278,32910,1718],{"class":302},[278,32912,32913,32915,32918,32920,32922,32925,32927],{"class":280,"line":1375},[278,32914,1112],{"class":298},[278,32916,32917],{"class":650}," cAction",[278,32919,764],{"class":298},[278,32921,1258],{"class":298},[278,32923,32924],{"class":302}," vscode.",[278,32926,32449],{"class":333},[278,32928,32929],{"class":302},"(action.title, action.kind);\n",[278,32931,32932,32935,32937],{"class":280,"line":1381},[278,32933,32934],{"class":302},"    cAction.command ",[278,32936,358],{"class":298},[278,32938,876],{"class":302},[278,32940,32941,32944,32947,32950,32952,32954,32956],{"class":280,"line":1386},[278,32942,32943],{"class":302},"      command: ",[278,32945,32946],{"class":309},"`my-shiny-extension.${",[278,32948,32949],{"class":302},"action",[278,32951,183],{"class":309},[278,32953,28084],{"class":302},[278,32955,1277],{"class":309},[278,32957,660],{"class":302},[278,32959,32960],{"class":280,"line":1394},[278,32961,32962],{"class":302},"      title: action.title,\n",[278,32964,32965],{"class":280,"line":1406},[278,32966,32967],{"class":302},"      arguments: [action.id],\n",[278,32969,32970],{"class":280,"line":1423},[278,32971,1378],{"class":302},[278,32973,32974],{"class":280,"line":1432},[278,32975,292],{"emptyLinePlaceholder":291},[278,32977,32978,32981,32983],{"class":280,"line":1437},[278,32979,32980],{"class":302},"    cActions.",[278,32982,6524],{"class":333},[278,32984,32985],{"class":302},"(cAction);\n",[278,32987,32988],{"class":280,"line":1916},[278,32989,1096],{"class":302},[278,32991,32992],{"class":280,"line":1939},[278,32993,292],{"emptyLinePlaceholder":291},[278,32995,32996],{"class":280,"line":1949},[278,32997,32998],{"class":302},"  return cActions;\n",[278,33000,33001],{"class":280,"line":1954},[278,33002,617],{"class":302},[11,33004,33005],{},"Debug\u002Frun the extension now and you should see the above actions in the bulb tooltip when you select some text in a markdown\u002Ftext file.",[11,33007,33008],{},[3135,33009],{"alt":33010,"src":33011},"code action window intermediate view","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002F5b8b0741-8035-46e9-a57d-39e97d9d7af1-90f809ff51.png",[11,33013,33014],{},"Of course, nothing will happen if you click on any of these actions. This is because we've only created the actions, but haven't written any code to handle them.",[32,33016,33018],{"id":33017},"handling-the-code-actions","Handling the Code Actions",[11,33020,33021,33022,33024],{},"To handle the actions we need to register these commands with the VS Code extension context. This can be done inside the ",[59,33023,32252],{}," function. Let's do a little bit of refactoring.",[11,33026,33027,33028,33030,33031],{},"Move the actions out of the ",[59,33029,32631],{}," method and make it a class property of ",[59,33032,32635],{},[269,33034,33036],{"className":271,"code":33035,"language":273,"meta":274,"style":274},"public static readonly actions = [\n  {\n    id: 'rephrase',\n    title: 'Rephrase selected text',\n    kind: vscode.CodeActionKind.QuickFix,\n  },\n  {\n    id: 'headlines',\n    title: 'Suggest headlines',\n    kind: vscode.CodeActionKind.QuickFix,\n  },\n  {\n    id: 'professional',\n    title: 'Rewrite in professional tone',\n    kind: vscode.CodeActionKind.RefactorRewrite,\n  },\n  {\n    id: 'casual',\n    title: 'Rewrite in casual tone',\n    kind: vscode.CodeActionKind.RefactorRewrite,\n  },\n];\n",[59,33037,33038,33047,33051,33060,33069,33074,33078,33082,33090,33098,33102,33106,33110,33118,33126,33131,33135,33139,33147,33155,33159,33163],{"__ignoreMap":274},[278,33039,33040,33043,33045],{"class":280,"line":281},[278,33041,33042],{"class":302},"public static readonly actions ",[278,33044,358],{"class":298},[278,33046,5876],{"class":302},[278,33048,33049],{"class":280,"line":288},[278,33050,11470],{"class":302},[278,33052,33053,33056,33058],{"class":280,"line":295},[278,33054,33055],{"class":302},"    id: ",[278,33057,32758],{"class":309},[278,33059,660],{"class":302},[278,33061,33062,33065,33067],{"class":280,"line":316},[278,33063,33064],{"class":302},"    title: ",[278,33066,32768],{"class":309},[278,33068,660],{"class":302},[278,33070,33071],{"class":280,"line":322},[278,33072,33073],{"class":302},"    kind: vscode.CodeActionKind.QuickFix,\n",[278,33075,33076],{"class":280,"line":327},[278,33077,683],{"class":302},[278,33079,33080],{"class":280,"line":340},[278,33081,11470],{"class":302},[278,33083,33084,33086,33088],{"class":280,"line":349},[278,33085,33055],{"class":302},[278,33087,32790],{"class":309},[278,33089,660],{"class":302},[278,33091,33092,33094,33096],{"class":280,"line":375},[278,33093,33064],{"class":302},[278,33095,32799],{"class":309},[278,33097,660],{"class":302},[278,33099,33100],{"class":280,"line":386},[278,33101,33073],{"class":302},[278,33103,33104],{"class":280,"line":397},[278,33105,683],{"class":302},[278,33107,33108],{"class":280,"line":408},[278,33109,11470],{"class":302},[278,33111,33112,33114,33116],{"class":280,"line":433},[278,33113,33055],{"class":302},[278,33115,32820],{"class":309},[278,33117,660],{"class":302},[278,33119,33120,33122,33124],{"class":280,"line":454},[278,33121,33064],{"class":302},[278,33123,32829],{"class":309},[278,33125,660],{"class":302},[278,33127,33128],{"class":280,"line":475},[278,33129,33130],{"class":302},"    kind: vscode.CodeActionKind.RefactorRewrite,\n",[278,33132,33133],{"class":280,"line":496},[278,33134,683],{"class":302},[278,33136,33137],{"class":280,"line":505},[278,33138,11470],{"class":302},[278,33140,33141,33143,33145],{"class":280,"line":516},[278,33142,33055],{"class":302},[278,33144,32851],{"class":309},[278,33146,660],{"class":302},[278,33148,33149,33151,33153],{"class":280,"line":527},[278,33150,33064],{"class":302},[278,33152,32860],{"class":309},[278,33154,660],{"class":302},[278,33156,33157],{"class":280,"line":533},[278,33158,33130],{"class":302},[278,33160,33161],{"class":280,"line":539},[278,33162,683],{"class":302},[278,33164,33165],{"class":280,"line":545},[278,33166,11714],{"class":302},[11,33168,33169,33170,33172],{},"Change its reference inside the ",[59,33171,32631],{}," method to",[269,33174,33176],{"className":271,"code":33175,"language":273,"meta":274,"style":274},"for (const action of MyCodeActionProvider.actions) {\n  \u002F\u002F ...\n}\n",[59,33177,33178,33193,33197],{"__ignoreMap":274},[278,33179,33180,33182,33184,33186,33188,33190],{"class":280,"line":281},[278,33181,31535],{"class":298},[278,33183,1245],{"class":302},[278,33185,5416],{"class":298},[278,33187,32903],{"class":650},[278,33189,12022],{"class":298},[278,33191,33192],{"class":302}," MyCodeActionProvider.actions) {\n",[278,33194,33195],{"class":280,"line":288},[278,33196,12729],{"class":284},[278,33198,33199],{"class":280,"line":295},[278,33200,617],{"class":302},[11,33202,33203,33204,33207,33208,33211,33212,33215,33216,33219],{},"Add a new method ",[59,33205,33206],{},"handleAction"," which will handle the actions when a user clicks on them. The ",[59,33209,33210],{},"actionId"," argument will be passed by the caller. Remember we had passed ",[59,33213,33214],{},"arguments: [action.id]"," while returning the code actions from the ",[59,33217,33218],{},"providecodeActions"," method?",[269,33221,33223],{"className":271,"code":33222,"language":273,"meta":274,"style":274},"handleAction(actionId: string) {\n  console.log(`handleAction for ${actionId}`);\n}\n",[59,33224,33225,33232,33249],{"__ignoreMap":274},[278,33226,33227,33229],{"class":280,"line":281},[278,33228,33206],{"class":333},[278,33230,33231],{"class":302},"(actionId: string) {\n",[278,33233,33234,33236,33238,33240,33243,33245,33247],{"class":280,"line":288},[278,33235,17975],{"class":302},[278,33237,14851],{"class":333},[278,33239,1126],{"class":302},[278,33241,33242],{"class":309},"`handleAction for ${",[278,33244,33210],{"class":302},[278,33246,1277],{"class":309},[278,33248,1280],{"class":302},[278,33250,33251],{"class":280,"line":295},[278,33252,617],{"class":302},[11,33254,33255,33256,33258],{},"Now change the ",[59,33257,32252],{}," function as shown below",[269,33260,33262],{"className":271,"code":33261,"language":273,"meta":274,"style":274},"export function activate(context: vscode.ExtensionContext) {\n  const myActionProvider = new MyCodeActionProvider();\n  const actionProvider = vscode.languages.registerCodeActionsProvider(\n    ['markdown', 'plaintext'],\n    myActionProvider,\n    {\n      providedCodeActionKinds: [\n        vscode.CodeActionKind.RefactorRewrite,\n        vscode.CodeActionKind.QuickFix,\n      ],\n    }\n  );\n\n  context.subscriptions.push(actionProvider);\n  for (const action of MyCodeActionProvider.actions) {\n    context.subscriptions.push(\n      \u002F\u002F use the same id which we used in the command field \n      \u002F\u002F of the code actions\n      vscode.commands.registerCommand(\n        `my-shiny-extension.${action.id}`,\n        (args) => myActionProvider.handleAction(args)\n      )\n    );\n  }\n}\n",[59,33263,33264,33286,33301,33315,33327,33332,33336,33340,33344,33348,33352,33356,33360,33364,33372,33386,33395,33400,33405,33415,33430,33450,33454,33458,33462],{"__ignoreMap":274},[278,33265,33266,33268,33270,33272,33274,33276,33278,33280,33282,33284],{"class":280,"line":281},[278,33267,628],{"class":298},[278,33269,748],{"class":298},[278,33271,32284],{"class":333},[278,33273,1126],{"class":302},[278,33275,32289],{"class":501},[278,33277,960],{"class":298},[278,33279,32294],{"class":333},[278,33281,183],{"class":302},[278,33283,32299],{"class":333},[278,33285,1718],{"class":302},[278,33287,33288,33290,33293,33295,33297,33299],{"class":280,"line":288},[278,33289,758],{"class":298},[278,33291,33292],{"class":650}," myActionProvider",[278,33294,764],{"class":298},[278,33296,1258],{"class":298},[278,33298,32331],{"class":333},[278,33300,1313],{"class":302},[278,33302,33303,33305,33307,33309,33311,33313],{"class":280,"line":295},[278,33304,758],{"class":298},[278,33306,32530],{"class":650},[278,33308,764],{"class":298},[278,33310,32535],{"class":302},[278,33312,32538],{"class":333},[278,33314,770],{"class":302},[278,33316,33317,33319,33321,33323,33325],{"class":280,"line":316},[278,33318,32545],{"class":302},[278,33320,32548],{"class":309},[278,33322,1708],{"class":302},[278,33324,32553],{"class":309},[278,33326,3533],{"class":302},[278,33328,33329],{"class":280,"line":322},[278,33330,33331],{"class":302},"    myActionProvider,\n",[278,33333,33334],{"class":280,"line":327},[278,33335,2209],{"class":302},[278,33337,33338],{"class":280,"line":340},[278,33339,32573],{"class":302},[278,33341,33342],{"class":280,"line":349},[278,33343,32578],{"class":302},[278,33345,33346],{"class":280,"line":375},[278,33347,32583],{"class":302},[278,33349,33350],{"class":280,"line":386},[278,33351,32588],{"class":302},[278,33353,33354],{"class":280,"line":397},[278,33355,1285],{"class":302},[278,33357,33358],{"class":280,"line":408},[278,33359,611],{"class":302},[278,33361,33362],{"class":280,"line":433},[278,33363,292],{"emptyLinePlaceholder":291},[278,33365,33366,33368,33370],{"class":280,"line":454},[278,33367,32605],{"class":302},[278,33369,6524],{"class":333},[278,33371,32610],{"class":302},[278,33373,33374,33376,33378,33380,33382,33384],{"class":280,"line":475},[278,33375,12738],{"class":298},[278,33377,1245],{"class":302},[278,33379,5416],{"class":298},[278,33381,32903],{"class":650},[278,33383,12022],{"class":298},[278,33385,33192],{"class":302},[278,33387,33388,33391,33393],{"class":280,"line":496},[278,33389,33390],{"class":302},"    context.subscriptions.",[278,33392,6524],{"class":333},[278,33394,770],{"class":302},[278,33396,33397],{"class":280,"line":505},[278,33398,33399],{"class":284},"      \u002F\u002F use the same id which we used in the command field \n",[278,33401,33402],{"class":280,"line":516},[278,33403,33404],{"class":284},"      \u002F\u002F of the code actions\n",[278,33406,33407,33410,33413],{"class":280,"line":527},[278,33408,33409],{"class":302},"      vscode.commands.",[278,33411,33412],{"class":333},"registerCommand",[278,33414,770],{"class":302},[278,33416,33417,33420,33422,33424,33426,33428],{"class":280,"line":533},[278,33418,33419],{"class":309},"        `my-shiny-extension.${",[278,33421,32949],{"class":302},[278,33423,183],{"class":309},[278,33425,28084],{"class":302},[278,33427,1277],{"class":309},[278,33429,660],{"class":302},[278,33431,33432,33435,33438,33440,33442,33445,33447],{"class":280,"line":539},[278,33433,33434],{"class":302},"        (",[278,33436,33437],{"class":501},"args",[278,33439,1845],{"class":302},[278,33441,1848],{"class":298},[278,33443,33444],{"class":302}," myActionProvider.",[278,33446,33206],{"class":333},[278,33448,33449],{"class":302},"(args)\n",[278,33451,33452],{"class":280,"line":545},[278,33453,17693],{"class":302},[278,33455,33456],{"class":280,"line":551},[278,33457,1898],{"class":302},[278,33459,33460],{"class":280,"line":557},[278,33461,1096],{"class":302},[278,33463,33464],{"class":280,"line":567},[278,33465,617],{"class":302},[11,33467,33468,33469,33471],{},"For all the actions which we support, we're registering a corresponding command with the VS Code extension context. The command id that we use here must match the command id which we returned from the ",[59,33470,32631],{}," method.",[11,33473,33474],{},"If we run\u002Fdebug the extension now and click any of our actions from the light bulb menu we should see the corresponding console log in the debug console.",[32,33476,33478],{"id":33477},"integrating-with-the-openai-apis","Integrating with the OpenAI APIs",[11,33480,33481],{},"Now the only thing remaining is: using the OpenAI APIs to make changes to any written text. Let's get it over with.",[11,33483,33484],{},"Add the OpenAI library to the codebase",[269,33486,33488],{"className":3335,"code":33487,"language":3337,"meta":274,"style":274},"yarn add openai\n",[59,33489,33490],{"__ignoreMap":274},[278,33491,33492,33495,33497],{"class":280,"line":281},[278,33493,33494],{"class":333},"yarn",[278,33496,3418],{"class":309},[278,33498,10971],{"class":309},[11,33500,33501,33502,32318],{},"Import it into the ",[59,33503,32110],{},[269,33505,33507],{"className":271,"code":33506,"language":273,"meta":274,"style":274},"import { OpenAIApi, Configuration } from 'openai';\n",[59,33508,33509],{"__ignoreMap":274},[278,33510,33511,33513,33516,33518,33521],{"class":280,"line":281},[278,33512,299],{"class":298},[278,33514,33515],{"class":302}," { OpenAIApi, Configuration } ",[278,33517,306],{"class":298},[278,33519,33520],{"class":309}," 'openai'",[278,33522,313],{"class":302},[11,33524,33525,33526,32651],{},"And replace the ",[59,33527,33206],{},[269,33529,33531],{"className":271,"code":33530,"language":273,"meta":274,"style":274},"async handleAction(actionId: string) {\n  const editor = vscode.window.activeTextEditor;\n  if (\n    !editor ||\n    editor.selection.isEmpty ||\n    !['rephrase', 'headlines', 'professional', 'casual'].includes(actionId)\n  ) {\n    \u002F\u002F return if no active editor, or no active selection \n    \u002F\u002F or if unsupported actionId passed\n    return;\n  }\n\n  \u002F\u002F Create the OpenAI Service\n  const openAiSvc = new OpenAIApi(\n    new Configuration({\n      apiKey: '\u003Cyour_open_ai_api_key>',\n    })\n  );\n\n  \u002F\u002F Get the currently selected text\n  const text = editor.document.getText(editor.selection);\n  \u002F\u002F current selection range\n  let currRange = editor.selection;\n\n  try {\n    \u002F\u002F Adding a filler\u002Floading text before making the API call\n    const fillerText = '\\n\\nThinking...';\n    editor\n      .edit((editBuilder) => {\n        \u002F\u002F insert the filler text after the current selection end\n        editBuilder.insert(currRange.end, fillerText);\n      })\n      .then((success) => {\n        if (success) {\n          \u002F\u002F Select the filler text now\n          editor.selection = new vscode.Selection(\n            editor.selection.end.line,\n            0,\n            editor.selection.end.line,\n            editor.selection.end.character\n          );\n\n          \u002F\u002F store this new selection range\n          currRange = editor.selection;\n        }\n      });\n\n    \u002F\u002F Create the prompt prefix based on the action id\n    let promptPrefix = '';\n    switch (actionId) {\n      case 'rephrase':\n        promptPrefix =\n          'Rephrase the following text and make the sentences more clear and readable';\n        break;\n      case 'headlines':\n        promptPrefix = 'Suggest some short headlines for the following text';\n        break;\n      case 'professional':\n        promptPrefix =\n          'Make the following text better and rewrite it in a professional tone';\n        break;\n      case 'casual':\n        promptPrefix =\n          'Make the following text better and rewrite it in a casual tone';\n        break;\n    }\n\n    \u002F\u002F Make the OpenAI API Call using the desired model and configs\n    \u002F* eslint-disable @typescript-eslint\u002Fnaming-convention *\u002F\n    const response = await openAiSvc.createCompletion({\n      model: 'text-davinci-003',\n      prompt: `${promptPrefix}:\\n\\n${text}\\n\\n`,\n      temperature: 0.3,\n      max_tokens: 500,\n      frequency_penalty: 0.0,\n      presence_penalty: 0.0,\n      n: 1,\n    });\n    \u002F* eslint-enable @typescript-eslint\u002Fnaming-convention *\u002F\n\n    \u002F\u002F We'd reuqested for only one result, use that\n    let result = response.data.choices[0].text;\n    editor\n      .edit((editBuilder) => {\n        if (result) {\n          \u002F\u002F replace the filler text with the actual result\n          editBuilder.replace(\n            new vscode.Range(currRange.start, currRange.end),\n            result.trim()\n          );\n        }\n      })\n      .then((success) => {\n        if (success) {\n          \u002F\u002F Select the resulting text (the text can be longer\n          \u002F\u002F and span over multiple lines, so we treat it \n          \u002F\u002F appropriately to make a complete selection)\n          editor.selection = new vscode.Selection(\n            currRange.start.line,\n            currRange.start.character,\n            currRange.end.line,\n            editor.document.lineAt(currRange.end.line).text.length\n          );\n\n          return;\n        }\n      });\n  } catch (error) {\n    console.error(error);\n  }\n\n  \u002F\u002F In case of API error, show an error text instead\n  editor.edit((editBuilder) => {\n    editor.selection = new vscode.Selection(currRange.start, currRange.end);\n    editBuilder.replace(editor.selection, 'Failed to process...');\n  }):\n}\n",[59,33532,33533,33542,33554,33560,33570,33577,33604,33608,33613,33618,33624,33628,33632,33637,33653,33662,33672,33676,33680,33684,33689,33707,33712,33724,33728,33734,33739,33758,33763,33781,33786,33796,33801,33819,33826,33831,33846,33851,33858,33862,33867,33871,33875,33880,33889,33893,33897,33901,33906,33919,33927,33938,33946,33953,33960,33969,33980,33986,33995,34001,34008,34014,34023,34029,34036,34042,34046,34050,34055,34060,34078,34088,34117,34126,34135,34145,34154,34163,34167,34172,34176,34181,34198,34202,34218,34225,34230,34239,34251,34260,34264,34268,34272,34288,34294,34299,34304,34309,34323,34328,34333,34338,34352,34356,34360,34366,34370,34374,34382,34390,34394,34398,34403,34420,34436,34451,34456],{"__ignoreMap":274},[278,33534,33535,33538,33540],{"class":280,"line":281},[278,33536,33537],{"class":302},"async ",[278,33539,33206],{"class":333},[278,33541,33231],{"class":302},[278,33543,33544,33546,33549,33551],{"class":280,"line":288},[278,33545,758],{"class":298},[278,33547,33548],{"class":650}," editor",[278,33550,764],{"class":298},[278,33552,33553],{"class":302}," vscode.window.activeTextEditor;\n",[278,33555,33556,33558],{"class":280,"line":295},[278,33557,1062],{"class":298},[278,33559,346],{"class":302},[278,33561,33562,33565,33568],{"class":280,"line":316},[278,33563,33564],{"class":298},"    !",[278,33566,33567],{"class":302},"editor ",[278,33569,17245],{"class":298},[278,33571,33572,33575],{"class":280,"line":322},[278,33573,33574],{"class":302},"    editor.selection.isEmpty ",[278,33576,17245],{"class":298},[278,33578,33579,33581,33583,33585,33587,33589,33591,33593,33595,33597,33599,33601],{"class":280,"line":327},[278,33580,33564],{"class":298},[278,33582,29860],{"class":302},[278,33584,32758],{"class":309},[278,33586,1708],{"class":302},[278,33588,32790],{"class":309},[278,33590,1708],{"class":302},[278,33592,32820],{"class":309},[278,33594,1708],{"class":302},[278,33596,32851],{"class":309},[278,33598,17447],{"class":302},[278,33600,13297],{"class":333},[278,33602,33603],{"class":302},"(actionId)\n",[278,33605,33606],{"class":280,"line":340},[278,33607,17292],{"class":302},[278,33609,33610],{"class":280,"line":349},[278,33611,33612],{"class":284},"    \u002F\u002F return if no active editor, or no active selection \n",[278,33614,33615],{"class":280,"line":375},[278,33616,33617],{"class":284},"    \u002F\u002F or if unsupported actionId passed\n",[278,33619,33620,33622],{"class":280,"line":386},[278,33621,1088],{"class":298},[278,33623,313],{"class":302},[278,33625,33626],{"class":280,"line":397},[278,33627,1096],{"class":302},[278,33629,33630],{"class":280,"line":408},[278,33631,292],{"emptyLinePlaceholder":291},[278,33633,33634],{"class":280,"line":433},[278,33635,33636],{"class":284},"  \u002F\u002F Create the OpenAI Service\n",[278,33638,33639,33641,33644,33646,33648,33651],{"class":280,"line":454},[278,33640,758],{"class":298},[278,33642,33643],{"class":650}," openAiSvc",[278,33645,764],{"class":298},[278,33647,1258],{"class":298},[278,33649,33650],{"class":333}," OpenAIApi",[278,33652,770],{"class":302},[278,33654,33655,33657,33660],{"class":280,"line":475},[278,33656,32560],{"class":298},[278,33658,33659],{"class":333}," Configuration",[278,33661,637],{"class":302},[278,33663,33664,33667,33670],{"class":280,"line":496},[278,33665,33666],{"class":302},"      apiKey: ",[278,33668,33669],{"class":309},"'\u003Cyour_open_ai_api_key>'",[278,33671,660],{"class":302},[278,33673,33674],{"class":280,"line":505},[278,33675,27054],{"class":302},[278,33677,33678],{"class":280,"line":516},[278,33679,611],{"class":302},[278,33681,33682],{"class":280,"line":527},[278,33683,292],{"emptyLinePlaceholder":291},[278,33685,33686],{"class":280,"line":533},[278,33687,33688],{"class":284},"  \u002F\u002F Get the currently selected text\n",[278,33690,33691,33693,33696,33698,33701,33704],{"class":280,"line":539},[278,33692,758],{"class":298},[278,33694,33695],{"class":650}," text",[278,33697,764],{"class":298},[278,33699,33700],{"class":302}," editor.document.",[278,33702,33703],{"class":333},"getText",[278,33705,33706],{"class":302},"(editor.selection);\n",[278,33708,33709],{"class":280,"line":545},[278,33710,33711],{"class":284},"  \u002F\u002F current selection range\n",[278,33713,33714,33716,33719,33721],{"class":280,"line":551},[278,33715,6050],{"class":298},[278,33717,33718],{"class":302}," currRange ",[278,33720,358],{"class":298},[278,33722,33723],{"class":302}," editor.selection;\n",[278,33725,33726],{"class":280,"line":557},[278,33727,292],{"emptyLinePlaceholder":291},[278,33729,33730,33732],{"class":280,"line":567},[278,33731,1105],{"class":298},[278,33733,876],{"class":302},[278,33735,33736],{"class":280,"line":577},[278,33737,33738],{"class":284},"    \u002F\u002F Adding a filler\u002Floading text before making the API call\n",[278,33740,33741,33743,33746,33748,33751,33753,33756],{"class":280,"line":587},[278,33742,1112],{"class":298},[278,33744,33745],{"class":650}," fillerText",[278,33747,764],{"class":298},[278,33749,33750],{"class":309}," '",[278,33752,14777],{"class":650},[278,33754,33755],{"class":309},"Thinking...'",[278,33757,313],{"class":302},[278,33759,33760],{"class":280,"line":597},[278,33761,33762],{"class":302},"    editor\n",[278,33764,33765,33767,33770,33772,33775,33777,33779],{"class":280,"line":608},[278,33766,5086],{"class":302},[278,33768,33769],{"class":333},"edit",[278,33771,2079],{"class":302},[278,33773,33774],{"class":501},"editBuilder",[278,33776,1845],{"class":302},[278,33778,1848],{"class":298},[278,33780,876],{"class":302},[278,33782,33783],{"class":280,"line":614},[278,33784,33785],{"class":284},"        \u002F\u002F insert the filler text after the current selection end\n",[278,33787,33788,33791,33793],{"class":280,"line":620},[278,33789,33790],{"class":302},"        editBuilder.",[278,33792,5089],{"class":333},[278,33794,33795],{"class":302},"(currRange.end, fillerText);\n",[278,33797,33798],{"class":280,"line":625},[278,33799,33800],{"class":302},"      })\n",[278,33802,33803,33805,33808,33810,33813,33815,33817],{"class":280,"line":640},[278,33804,5086],{"class":302},[278,33806,33807],{"class":333},"then",[278,33809,2079],{"class":302},[278,33811,33812],{"class":501},"success",[278,33814,1845],{"class":302},[278,33816,1848],{"class":298},[278,33818,876],{"class":302},[278,33820,33821,33823],{"class":280,"line":663},[278,33822,6926],{"class":298},[278,33824,33825],{"class":302}," (success) {\n",[278,33827,33828],{"class":280,"line":669},[278,33829,33830],{"class":284},"          \u002F\u002F Select the filler text now\n",[278,33832,33833,33836,33838,33840,33842,33844],{"class":280,"line":680},[278,33834,33835],{"class":302},"          editor.selection ",[278,33837,358],{"class":298},[278,33839,1258],{"class":298},[278,33841,32924],{"class":302},[278,33843,32393],{"class":333},[278,33845,770],{"class":302},[278,33847,33848],{"class":280,"line":686},[278,33849,33850],{"class":302},"            editor.selection.end.line,\n",[278,33852,33853,33856],{"class":280,"line":1334},[278,33854,33855],{"class":650},"            0",[278,33857,660],{"class":302},[278,33859,33860],{"class":280,"line":1375},[278,33861,33850],{"class":302},[278,33863,33864],{"class":280,"line":1381},[278,33865,33866],{"class":302},"            editor.selection.end.character\n",[278,33868,33869],{"class":280,"line":1386},[278,33870,14787],{"class":302},[278,33872,33873],{"class":280,"line":1394},[278,33874,292],{"emptyLinePlaceholder":291},[278,33876,33877],{"class":280,"line":1406},[278,33878,33879],{"class":284},"          \u002F\u002F store this new selection range\n",[278,33881,33882,33885,33887],{"class":280,"line":1423},[278,33883,33884],{"class":302},"          currRange ",[278,33886,358],{"class":298},[278,33888,33723],{"class":302},[278,33890,33891],{"class":280,"line":1432},[278,33892,6954],{"class":302},[278,33894,33895],{"class":280,"line":1437},[278,33896,5148],{"class":302},[278,33898,33899],{"class":280,"line":1916},[278,33900,292],{"emptyLinePlaceholder":291},[278,33902,33903],{"class":280,"line":1939},[278,33904,33905],{"class":284},"    \u002F\u002F Create the prompt prefix based on the action id\n",[278,33907,33908,33910,33913,33915,33917],{"class":280,"line":1949},[278,33909,20815],{"class":298},[278,33911,33912],{"class":302}," promptPrefix ",[278,33914,358],{"class":298},[278,33916,13973],{"class":309},[278,33918,313],{"class":302},[278,33920,33921,33924],{"class":280,"line":1954},[278,33922,33923],{"class":298},"    switch",[278,33925,33926],{"class":302}," (actionId) {\n",[278,33928,33929,33932,33935],{"class":280,"line":1959},[278,33930,33931],{"class":298},"      case",[278,33933,33934],{"class":309}," 'rephrase'",[278,33936,33937],{"class":302},":\n",[278,33939,33940,33943],{"class":280,"line":1985},[278,33941,33942],{"class":302},"        promptPrefix ",[278,33944,33945],{"class":298},"=\n",[278,33947,33948,33951],{"class":280,"line":1990},[278,33949,33950],{"class":309},"          'Rephrase the following text and make the sentences more clear and readable'",[278,33952,313],{"class":302},[278,33954,33955,33958],{"class":280,"line":1997},[278,33956,33957],{"class":298},"        break",[278,33959,313],{"class":302},[278,33961,33962,33964,33967],{"class":280,"line":2006},[278,33963,33931],{"class":298},[278,33965,33966],{"class":309}," 'headlines'",[278,33968,33937],{"class":302},[278,33970,33971,33973,33975,33978],{"class":280,"line":2018},[278,33972,33942],{"class":302},[278,33974,358],{"class":298},[278,33976,33977],{"class":309}," 'Suggest some short headlines for the following text'",[278,33979,313],{"class":302},[278,33981,33982,33984],{"class":280,"line":2029},[278,33983,33957],{"class":298},[278,33985,313],{"class":302},[278,33987,33988,33990,33993],{"class":280,"line":2034},[278,33989,33931],{"class":298},[278,33991,33992],{"class":309}," 'professional'",[278,33994,33937],{"class":302},[278,33996,33997,33999],{"class":280,"line":2040},[278,33998,33942],{"class":302},[278,34000,33945],{"class":298},[278,34002,34003,34006],{"class":280,"line":2045},[278,34004,34005],{"class":309},"          'Make the following text better and rewrite it in a professional tone'",[278,34007,313],{"class":302},[278,34009,34010,34012],{"class":280,"line":2068},[278,34011,33957],{"class":298},[278,34013,313],{"class":302},[278,34015,34016,34018,34021],{"class":280,"line":2099},[278,34017,33931],{"class":298},[278,34019,34020],{"class":309}," 'casual'",[278,34022,33937],{"class":302},[278,34024,34025,34027],{"class":280,"line":6428},[278,34026,33942],{"class":302},[278,34028,33945],{"class":298},[278,34030,34031,34034],{"class":280,"line":6439},[278,34032,34033],{"class":309},"          'Make the following text better and rewrite it in a casual tone'",[278,34035,313],{"class":302},[278,34037,34038,34040],{"class":280,"line":6450},[278,34039,33957],{"class":298},[278,34041,313],{"class":302},[278,34043,34044],{"class":280,"line":6455},[278,34045,1285],{"class":302},[278,34047,34048],{"class":280,"line":6460},[278,34049,292],{"emptyLinePlaceholder":291},[278,34051,34052],{"class":280,"line":6475},[278,34053,34054],{"class":284},"    \u002F\u002F Make the OpenAI API Call using the desired model and configs\n",[278,34056,34057],{"class":280,"line":6486},[278,34058,34059],{"class":284},"    \u002F* eslint-disable @typescript-eslint\u002Fnaming-convention *\u002F\n",[278,34061,34062,34064,34066,34068,34070,34073,34076],{"class":280,"line":6491},[278,34063,1112],{"class":298},[278,34065,1115],{"class":650},[278,34067,764],{"class":298},[278,34069,1120],{"class":298},[278,34071,34072],{"class":302}," openAiSvc.",[278,34074,34075],{"class":333},"createCompletion",[278,34077,637],{"class":302},[278,34079,34080,34083,34086],{"class":280,"line":6518},[278,34081,34082],{"class":302},"      model: ",[278,34084,34085],{"class":309},"'text-davinci-003'",[278,34087,660],{"class":302},[278,34089,34090,34093,34096,34099,34102,34104,34107,34109,34111,34113,34115],{"class":280,"line":6530},[278,34091,34092],{"class":302},"      prompt: ",[278,34094,34095],{"class":309},"`${",[278,34097,34098],{"class":302},"promptPrefix",[278,34100,34101],{"class":309},"}:",[278,34103,14777],{"class":650},[278,34105,34106],{"class":309},"${",[278,34108,4582],{"class":302},[278,34110,14774],{"class":309},[278,34112,14777],{"class":650},[278,34114,14780],{"class":309},[278,34116,660],{"class":302},[278,34118,34119,34122,34124],{"class":280,"line":6542},[278,34120,34121],{"class":302},"      temperature: ",[278,34123,24077],{"class":650},[278,34125,660],{"class":302},[278,34127,34128,34131,34133],{"class":280,"line":6547},[278,34129,34130],{"class":302},"      max_tokens: ",[278,34132,2779],{"class":650},[278,34134,660],{"class":302},[278,34136,34137,34140,34143],{"class":280,"line":6552},[278,34138,34139],{"class":302},"      frequency_penalty: ",[278,34141,34142],{"class":650},"0.0",[278,34144,660],{"class":302},[278,34146,34147,34150,34152],{"class":280,"line":6567},[278,34148,34149],{"class":302},"      presence_penalty: ",[278,34151,34142],{"class":650},[278,34153,660],{"class":302},[278,34155,34156,34159,34161],{"class":280,"line":6580},[278,34157,34158],{"class":302},"      n: ",[278,34160,17444],{"class":650},[278,34162,660],{"class":302},[278,34164,34165],{"class":280,"line":6593},[278,34166,1233],{"class":302},[278,34168,34169],{"class":280,"line":6605},[278,34170,34171],{"class":284},"    \u002F* eslint-enable @typescript-eslint\u002Fnaming-convention *\u002F\n",[278,34173,34174],{"class":280,"line":6620},[278,34175,292],{"emptyLinePlaceholder":291},[278,34177,34178],{"class":280,"line":6625},[278,34179,34180],{"class":284},"    \u002F\u002F We'd reuqested for only one result, use that\n",[278,34182,34183,34185,34188,34190,34193,34195],{"class":280,"line":6633},[278,34184,20815],{"class":298},[278,34186,34187],{"class":302}," result ",[278,34189,358],{"class":298},[278,34191,34192],{"class":302}," response.data.choices[",[278,34194,2012],{"class":650},[278,34196,34197],{"class":302},"].text;\n",[278,34199,34200],{"class":280,"line":6643},[278,34201,33762],{"class":302},[278,34203,34204,34206,34208,34210,34212,34214,34216],{"class":280,"line":6657},[278,34205,5086],{"class":302},[278,34207,33769],{"class":333},[278,34209,2079],{"class":302},[278,34211,33774],{"class":501},[278,34213,1845],{"class":302},[278,34215,1848],{"class":298},[278,34217,876],{"class":302},[278,34219,34220,34222],{"class":280,"line":6665},[278,34221,6926],{"class":298},[278,34223,34224],{"class":302}," (result) {\n",[278,34226,34227],{"class":280,"line":6670},[278,34228,34229],{"class":284},"          \u002F\u002F replace the filler text with the actual result\n",[278,34231,34232,34235,34237],{"class":280,"line":6675},[278,34233,34234],{"class":302},"          editBuilder.",[278,34236,13408],{"class":333},[278,34238,770],{"class":302},[278,34240,34241,34244,34246,34248],{"class":280,"line":6680},[278,34242,34243],{"class":298},"            new",[278,34245,32924],{"class":302},[278,34247,32384],{"class":333},[278,34249,34250],{"class":302},"(currRange.start, currRange.end),\n",[278,34252,34253,34256,34258],{"class":280,"line":6698},[278,34254,34255],{"class":302},"            result.",[278,34257,13245],{"class":333},[278,34259,4601],{"class":302},[278,34261,34262],{"class":280,"line":6725},[278,34263,14787],{"class":302},[278,34265,34266],{"class":280,"line":6738},[278,34267,6954],{"class":302},[278,34269,34270],{"class":280,"line":6752},[278,34271,33800],{"class":302},[278,34273,34274,34276,34278,34280,34282,34284,34286],{"class":280,"line":6769},[278,34275,5086],{"class":302},[278,34277,33807],{"class":333},[278,34279,2079],{"class":302},[278,34281,33812],{"class":501},[278,34283,1845],{"class":302},[278,34285,1848],{"class":298},[278,34287,876],{"class":302},[278,34289,34290,34292],{"class":280,"line":6786},[278,34291,6926],{"class":298},[278,34293,33825],{"class":302},[278,34295,34296],{"class":280,"line":6798},[278,34297,34298],{"class":284},"          \u002F\u002F Select the resulting text (the text can be longer\n",[278,34300,34301],{"class":280,"line":6803},[278,34302,34303],{"class":284},"          \u002F\u002F and span over multiple lines, so we treat it \n",[278,34305,34306],{"class":280,"line":6815},[278,34307,34308],{"class":284},"          \u002F\u002F appropriately to make a complete selection)\n",[278,34310,34311,34313,34315,34317,34319,34321],{"class":280,"line":6827},[278,34312,33835],{"class":302},[278,34314,358],{"class":298},[278,34316,1258],{"class":298},[278,34318,32924],{"class":302},[278,34320,32393],{"class":333},[278,34322,770],{"class":302},[278,34324,34325],{"class":280,"line":6839},[278,34326,34327],{"class":302},"            currRange.start.line,\n",[278,34329,34330],{"class":280,"line":6844},[278,34331,34332],{"class":302},"            currRange.start.character,\n",[278,34334,34335],{"class":280,"line":6853},[278,34336,34337],{"class":302},"            currRange.end.line,\n",[278,34339,34340,34343,34346,34349],{"class":280,"line":6859},[278,34341,34342],{"class":302},"            editor.document.",[278,34344,34345],{"class":333},"lineAt",[278,34347,34348],{"class":302},"(currRange.end.line).text.",[278,34350,34351],{"class":650},"length\n",[278,34353,34354],{"class":280,"line":6864},[278,34355,14787],{"class":302},[278,34357,34358],{"class":280,"line":6877},[278,34359,292],{"emptyLinePlaceholder":291},[278,34361,34362,34364],{"class":280,"line":6887},[278,34363,15447],{"class":298},[278,34365,313],{"class":302},[278,34367,34368],{"class":280,"line":6918},[278,34369,6954],{"class":302},[278,34371,34372],{"class":280,"line":6923},[278,34373,5148],{"class":302},[278,34375,34376,34378,34380],{"class":280,"line":6931},[278,34377,1397],{"class":302},[278,34379,1400],{"class":298},[278,34381,1403],{"class":302},[278,34383,34384,34386,34388],{"class":280,"line":6939},[278,34385,1409],{"class":302},[278,34387,1412],{"class":333},[278,34389,12653],{"class":302},[278,34391,34392],{"class":280,"line":6951},[278,34393,1096],{"class":302},[278,34395,34396],{"class":280,"line":6957},[278,34397,292],{"emptyLinePlaceholder":291},[278,34399,34400],{"class":280,"line":6962},[278,34401,34402],{"class":284},"  \u002F\u002F In case of API error, show an error text instead\n",[278,34404,34405,34408,34410,34412,34414,34416,34418],{"class":280,"line":6973},[278,34406,34407],{"class":302},"  editor.",[278,34409,33769],{"class":333},[278,34411,2079],{"class":302},[278,34413,33774],{"class":501},[278,34415,1845],{"class":302},[278,34417,1848],{"class":298},[278,34419,876],{"class":302},[278,34421,34422,34425,34427,34429,34431,34433],{"class":280,"line":6985},[278,34423,34424],{"class":302},"    editor.selection ",[278,34426,358],{"class":298},[278,34428,1258],{"class":298},[278,34430,32924],{"class":302},[278,34432,32393],{"class":333},[278,34434,34435],{"class":302},"(currRange.start, currRange.end);\n",[278,34437,34438,34441,34443,34446,34449],{"class":280,"line":6990},[278,34439,34440],{"class":302},"    editBuilder.",[278,34442,13408],{"class":333},[278,34444,34445],{"class":302},"(editor.selection, ",[278,34447,34448],{"class":309},"'Failed to process...'",[278,34450,1280],{"class":302},[278,34452,34453],{"class":280,"line":6995},[278,34454,34455],{"class":302},"  }):\n",[278,34457,34458],{"class":280,"line":7000},[278,34459,617],{"class":302},[11,34461,34462],{},"And we're done with the code. If you run\u002Fdebug the extension you should see appropriate text replacements for your text. Running it on a couple of my sentences gives me the following results",[11,34464,34465],{},[3135,34466],{"alt":34467,"src":34468},"Result of running the extension","\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002F63f92d9c-f86d-45bb-9069-514c6128c113-3f94006919.png",[11,34470,34471],{},"As always, you can play around with the prompts and get better consistent output from the OpenAI API.",[32,34473,34475],{"id":34474},"adding-extension-settings","Adding Extension Settings",[11,34477,34478,34479,34481,34482,34484,34485,34488,34489,34491],{},"You may have noticed that we've added the OpenAI API key directly in the code. We should move it to VS Code settings under our extension name. To do that we need to change the ",[59,34480,32134],{}," key in the ",[59,34483,5686],{}," file. While we're doing that we can also move the ",[59,34486,34487],{},"maxTokens"," property instead of hardcoding it to ",[59,34490,2779],{}," in the code.",[269,34493,34495],{"className":5690,"code":34494,"language":1310,"meta":274,"style":274},"\u002F\u002F ..\n\"contributes\": {\n  \"configuration\": {\n    \"title\": \"My Shiny Extension\",\n    \"properties\": {\n      \"myShinyExtension.openAiApiKey\": {\n        \"type\": \"string\",\n        \"default\": \"\",\n        \"description\": \"Enter you OpenAI API Key here\"\n      },\n      \"myShinyExtension.maxTokens\": {\n        \"type\": \"number\",\n        \"default\": 1200,\n        \"description\": \"Enter the maximum number of tokens to use for each OpenAI API call\"\n      }\n    }\n  }\n},\n\u002F\u002F ..\n",[59,34496,34497,34501,34507,34514,34526,34533,34540,34552,34563,34573,34577,34584,34595,34606,34615,34619,34623,34627,34631],{"__ignoreMap":274},[278,34498,34499],{"class":280,"line":281},[278,34500,5698],{"class":284},[278,34502,34503,34505],{"class":280,"line":288},[278,34504,32226],{"class":309},[278,34506,5706],{"class":302},[278,34508,34509,34512],{"class":280,"line":295},[278,34510,34511],{"class":650},"  \"configuration\"",[278,34513,5706],{"class":302},[278,34515,34516,34519,34521,34524],{"class":280,"line":316},[278,34517,34518],{"class":650},"    \"title\"",[278,34520,1155],{"class":302},[278,34522,34523],{"class":309},"\"My Shiny Extension\"",[278,34525,660],{"class":302},[278,34527,34528,34531],{"class":280,"line":322},[278,34529,34530],{"class":650},"    \"properties\"",[278,34532,5706],{"class":302},[278,34534,34535,34538],{"class":280,"line":327},[278,34536,34537],{"class":650},"      \"myShinyExtension.openAiApiKey\"",[278,34539,5706],{"class":302},[278,34541,34542,34545,34547,34550],{"class":280,"line":340},[278,34543,34544],{"class":650},"        \"type\"",[278,34546,1155],{"class":302},[278,34548,34549],{"class":309},"\"string\"",[278,34551,660],{"class":302},[278,34553,34554,34557,34559,34561],{"class":280,"line":349},[278,34555,34556],{"class":650},"        \"default\"",[278,34558,1155],{"class":302},[278,34560,23424],{"class":309},[278,34562,660],{"class":302},[278,34564,34565,34568,34570],{"class":280,"line":375},[278,34566,34567],{"class":650},"        \"description\"",[278,34569,1155],{"class":302},[278,34571,34572],{"class":309},"\"Enter you OpenAI API Key here\"\n",[278,34574,34575],{"class":280,"line":386},[278,34576,1165],{"class":302},[278,34578,34579,34582],{"class":280,"line":397},[278,34580,34581],{"class":650},"      \"myShinyExtension.maxTokens\"",[278,34583,5706],{"class":302},[278,34585,34586,34588,34590,34593],{"class":280,"line":408},[278,34587,34544],{"class":650},[278,34589,1155],{"class":302},[278,34591,34592],{"class":309},"\"number\"",[278,34594,660],{"class":302},[278,34596,34597,34599,34601,34604],{"class":280,"line":433},[278,34598,34556],{"class":650},[278,34600,1155],{"class":302},[278,34602,34603],{"class":650},"1200",[278,34605,660],{"class":302},[278,34607,34608,34610,34612],{"class":280,"line":454},[278,34609,34567],{"class":650},[278,34611,1155],{"class":302},[278,34613,34614],{"class":309},"\"Enter the maximum number of tokens to use for each OpenAI API call\"\n",[278,34616,34617],{"class":280,"line":475},[278,34618,6234],{"class":302},[278,34620,34621],{"class":280,"line":496},[278,34622,1285],{"class":302},[278,34624,34625],{"class":280,"line":505},[278,34626,1096],{"class":302},[278,34628,34629],{"class":280,"line":516},[278,34630,11040],{"class":302},[278,34632,34633],{"class":280,"line":527},[278,34634,5698],{"class":284},[11,34636,34637,34638,34641],{},"Now these two entries will appear under \"",[59,34639,34640],{},"My Shiny Extension","\" in the VS Code settings. To read the values of these entries we can use the below code (put it just above the OpenAI service creation).",[269,34643,34645],{"className":271,"code":34644,"language":273,"meta":274,"style":274},"const configs = vscode.workspace.getConfiguration('myShinyExtension');\nconst openAIApiKey = configs.get\u003Cstring>('openAiApiKey');\nconst maxTokens = configs.get\u003Cnumber>('maxTokens');\nif (!openAIApiKey) {\n  vscode.window.showErrorMessage(\n    'Missing OpenAI API Key. Please add your key in VSCode settings to use this extension.'\n  );\n\n  return;\n}\n",[59,34646,34647,34668,34693,34718,34729,34739,34744,34748,34752,34758],{"__ignoreMap":274},[278,34648,34649,34651,34653,34655,34658,34661,34663,34666],{"class":280,"line":281},[278,34650,5416],{"class":298},[278,34652,1964],{"class":650},[278,34654,764],{"class":298},[278,34656,34657],{"class":302}," vscode.workspace.",[278,34659,34660],{"class":333},"getConfiguration",[278,34662,1126],{"class":302},[278,34664,34665],{"class":309},"'myShinyExtension'",[278,34667,1280],{"class":302},[278,34669,34670,34672,34675,34677,34680,34682,34684,34686,34688,34691],{"class":280,"line":288},[278,34671,5416],{"class":298},[278,34673,34674],{"class":650}," openAIApiKey",[278,34676,764],{"class":298},[278,34678,34679],{"class":302}," configs.",[278,34681,3925],{"class":333},[278,34683,1702],{"class":302},[278,34685,1705],{"class":650},[278,34687,20521],{"class":302},[278,34689,34690],{"class":309},"'openAiApiKey'",[278,34692,1280],{"class":302},[278,34694,34695,34697,34700,34702,34704,34706,34708,34711,34713,34716],{"class":280,"line":295},[278,34696,5416],{"class":298},[278,34698,34699],{"class":650}," maxTokens",[278,34701,764],{"class":298},[278,34703,34679],{"class":302},[278,34705,3925],{"class":333},[278,34707,1702],{"class":302},[278,34709,34710],{"class":650},"number",[278,34712,20521],{"class":302},[278,34714,34715],{"class":309},"'maxTokens'",[278,34717,1280],{"class":302},[278,34719,34720,34722,34724,34726],{"class":280,"line":316},[278,34721,12721],{"class":298},[278,34723,1245],{"class":302},[278,34725,1209],{"class":298},[278,34727,34728],{"class":302},"openAIApiKey) {\n",[278,34730,34731,34734,34737],{"class":280,"line":322},[278,34732,34733],{"class":302},"  vscode.window.",[278,34735,34736],{"class":333},"showErrorMessage",[278,34738,770],{"class":302},[278,34740,34741],{"class":280,"line":327},[278,34742,34743],{"class":309},"    'Missing OpenAI API Key. Please add your key in VSCode settings to use this extension.'\n",[278,34745,34746],{"class":280,"line":340},[278,34747,611],{"class":302},[278,34749,34750],{"class":280,"line":349},[278,34751,292],{"emptyLinePlaceholder":291},[278,34753,34754,34756],{"class":280,"line":375},[278,34755,343],{"class":298},[278,34757,313],{"class":302},[278,34759,34760],{"class":280,"line":386},[278,34761,617],{"class":302},[11,34763,34764],{},"Do remember that we're creating the OpenAI service instance on each API call. This is not optimal and you should move it to the constructor and handle the error scenarios appropriately.",[24,34766,10634],{"id":10633},[11,34768,34769],{},"As is evident from the article, developing a VS Code extension can be easy and fun. If you're observant you can recreate an existing thing (Hashnode AI editor in this case) in your way someplace else, and learn a ton along the way.",[11,34771,34772,34773,34775,34776,34781,34782],{},"To publish an extension we need to complete a few more steps and optionally bundle it using ",[59,34774,32091],{}," or a suitable bundler. To learn more about the process you can visit these links: 1. ",[47,34777,34780],{"href":34778,"rel":34779},"https:\u002F\u002Fcode.visualstudio.com\u002Fapi\u002Fworking-with-extensions\u002Fpublishing-extension",[51],"publishing an extension",", 2. ",[47,34783,34786],{"href":34784,"rel":34785},"https:\u002F\u002Fcode.visualstudio.com\u002Fapi\u002Fworking-with-extensions\u002Fbundling-extension",[51],"Bundling an extension",[11,34788,34789,34790,183],{},"This was just a sneak peek of the extension I created. If interested, you can look at the complete source code of the extension ",[47,34791,3286],{"href":34792,"rel":34793},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fwrite-assist-ai",[51],[11,34795,34796,34797,183],{},"If you want to try out the extension (Write Assist AI), you can get it from ",[47,34798,3286],{"href":34799,"rel":34800},"https:\u002F\u002Fmarketplace.visualstudio.com\u002Fitems?itemName=ra-jeev.write-assist-ai",[51],[11,34802,34803],{},"I hope you liked reading the article. If you found any mistakes in the article, please let me know in the comments. Your suggestions and feedback are most welcome.",[11,34805,34806],{},[3061,34807,24390],{},[3065,34809,34810],{},"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 .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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}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}",{"title":274,"searchDepth":288,"depth":288,"links":34812},[34813,34814,34815,34822],{"id":22771,"depth":288,"text":22772},{"id":32042,"depth":288,"text":32043},{"id":32101,"depth":288,"text":32102,"children":34816},[34817,34818,34819,34820,34821],{"id":32159,"depth":295,"text":32160},{"id":32245,"depth":295,"text":32246},{"id":33017,"depth":295,"text":33018},{"id":33477,"depth":295,"text":33478},{"id":34474,"depth":295,"text":34475},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension\u002Fb2749874-26ba-49da-9a07-ef174c979231-e1d533e1e6.png","2023-06-19T20:19:25.949Z","Rabbit holes are many, and as a developer, you keep falling into one or the other during your journey. This is not necessarily a bad thing—granted that it may frustrate you—that...","clj3avb7h000j09l4fc3j2uk8",{},"\u002Fcreating-an-openai-powered-writing-assistant-vs-code-extension",{"title":32012,"description":34825},"creating-an-openai-powered-writing-assistant-vs-code-extension",[24599,273,18316,34832,24418],"vscode-extensions","emfBtr3BexBcV8eyfdyjbewmTAi7iz2765pWgXjEV98",{"id":34835,"title":34836,"body":34837,"cover":37426,"date":37427,"description":22772,"draft":3086,"extension":3087,"hashnodeId":37428,"meta":37429,"navigation":291,"path":37430,"seo":37431,"slug":37432,"stem":37432,"tags":37433,"__hash__":37437},"posts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb.md","How to Make a Gmail Bot with a persona using OpenAI GPT and MindsDB",{"type":8,"value":34838,"toc":37407},[34839,34841,34844,34847,34857,34861,34883,34892,34897,34901,34904,34910,34913,34968,34971,34976,34983,34988,34991,34997,35002,35016,35020,35035,35041,35045,35048,35051,35059,35063,35070,35192,35196,35199,35540,35543,35572,35576,35589,35596,35977,35989,36090,36105,36398,36407,36726,36735,36765,36769,36780,37052,37072,37092,37096,37099,37103,37106,37112,37116,37123,37183,37186,37211,37214,37220,37223,37229,37233,37236,37289,37292,37314,37317,37323,37326,37348,37351,37357,37360,37364,37367,37392,37394,37397,37400,37405],[24,34840,22772],{"id":22771},[11,34842,34843],{},"The AI hype refuses to die, more so after the release of Chat GPT and more recently the GPT4. I've been missing the AI action so far, so when MindsDB & Hashnode announced this hackathon and I saw the Twitter Bot implementation using the OpenAI APIs I knew that I wanted to build a bot for the hackathon.",[11,34845,34846],{},"But then the question arose, a bot for what purpose and for which application? Twitter was already done and dusted. I wanted my bot to be a wise and witty companion who is always available, so that helped me zero down on a Gmail bot. But the Gmail Integration was not yet implemented, and that was the second reason for choosing to build it. Practicing my Python skills, and the idea of contributing to a good project were the other important reasons.",[11,34848,34849,11160,34854],{},[3061,34850,34851],{},[94,34852,34853],{},"Tl;dr",[3061,34855,34856],{},"this article is about creating a new application (Gmail) handler for MindsDB and then using that handler to create an email bot that replies to incoming emails interestingly and poetically.",[24,34858,34860],{"id":34859},"setting-up-the-environment","Setting up the environment",[11,34862,34863,34864,34869,34870,34875,34876,34879,34880],{},"Since the first task is to develop the Gmail handler, we must set up the environment for development. But before that, I needed to know ",[47,34865,34868],{"href":34866,"rel":34867},"https:\u002F\u002Fdocs.mindsdb.com\u002Fcontribute",[51],"how to contribute to this project",", and ",[47,34871,34874],{"href":34872,"rel":34873},"https:\u002F\u002Fdocs.mindsdb.com\u002Fcontribute\u002Finstall",[51],"how to install MindsDb for development",". During the installation I faced only one issue related to ",[59,34877,34878],{},"libmagic"," which was not installed on my Mac by default, so had to install it using ",[59,34881,34882],{},"brew install libmagic.",[11,34884,34885,34886,34891],{},"The next step was to learn the ",[47,34887,34890],{"href":34888,"rel":34889},"https:\u002F\u002Fdocs.mindsdb.com\u002Fcontribute\u002Fapp-handlers",[51],"basics of creating an app handler",". This gave me a good overview of what I'm supposed to do for creating the Gmail handler.",[11,34893,34894],{},[3061,34895,34896],{},"Going through the relevant docs and following the mentioned steps is crucial if you want to contribute to any existing project.",[24,34898,34900],{"id":34899},"running-the-existing-installation","Running the existing installation",[11,34902,34903],{},"Now it was time to get my hands dirty, and the first step in that direction was to use an existing app handler. But before that, I followed the \"Predict Home Rental Prices\" tutorial from the \"Learning Hub\" in the local MindsDB web console, and everything worked fine.",[11,34905,34906],{},[3135,34907],{"alt":34908,"src":34909},"Predict Home Rental Prices Tutorial","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002Fb86a2975-1673-45e2-b000-df73cc21b00d-97c33a90f7.png",[11,34911,34912],{},"Next was the turn of the Twitter handler. I tried creating a tweets database using the below command in the local MindsDB browser console.",[269,34914,34917],{"className":34915,"code":34916,"language":4698,"meta":274,"style":274},"language-sql shiki shiki-themes github-light github-dark","CREATE DATABASE my_twitter \nWITH \n    ENGINE = 'twitter',\n    PARAMETERS = {\n      \"bearer_token\": \"twitter bearer token\",\n      \"consumer_key\": \"twitter consumer key\",\n      \"consumer_secret\": \"twitter consumer key secret\",\n      \"access_token\": \"twitter access token\",\n      \"access_token_secret\": \"twitter access token secret\"\n    };\n",[59,34918,34919,34924,34929,34934,34939,34944,34949,34954,34959,34964],{"__ignoreMap":274},[278,34920,34921],{"class":280,"line":281},[278,34922,34923],{},"CREATE DATABASE my_twitter \n",[278,34925,34926],{"class":280,"line":288},[278,34927,34928],{},"WITH \n",[278,34930,34931],{"class":280,"line":295},[278,34932,34933],{},"    ENGINE = 'twitter',\n",[278,34935,34936],{"class":280,"line":316},[278,34937,34938],{},"    PARAMETERS = {\n",[278,34940,34941],{"class":280,"line":322},[278,34942,34943],{},"      \"bearer_token\": \"twitter bearer token\",\n",[278,34945,34946],{"class":280,"line":327},[278,34947,34948],{},"      \"consumer_key\": \"twitter consumer key\",\n",[278,34950,34951],{"class":280,"line":340},[278,34952,34953],{},"      \"consumer_secret\": \"twitter consumer key secret\",\n",[278,34955,34956],{"class":280,"line":349},[278,34957,34958],{},"      \"access_token\": \"twitter access token\",\n",[278,34960,34961],{"class":280,"line":375},[278,34962,34963],{},"      \"access_token_secret\": \"twitter access token secret\"\n",[278,34965,34966],{"class":280,"line":386},[278,34967,1378],{},[11,34969,34970],{},"At least it should error out saying invalid credentials, but instead, I got the below error",[11,34972,34973],{},[59,34974,34975],{},"Can't connect to db: Handler 'twitter' can not be used",[11,34977,34978,34979,34982],{},"Well, that's a bummer. Why it is not working? This is where you start debugging and find out what is happening in the codebase. And how do we do that? I simply searched for the error string ",[59,34980,34981],{},"\"Can't connect to db\""," in the codebase and found the issue to be related to the handler not getting imported. After some more investigation saw that the zsh console has this info message",[11,34984,34985],{},[59,34986,34987],{},"Dependencies for the handler 'twitter' are not installed by default. If you want to use \"twitter\" please install \"['tweepy']\"",[11,34989,34990],{},"And there we have it. This gives us a clue that if we want to use any of the other handlers (except the basic ones) we need to install their dependencies manually.",[11,34992,34993,34996],{},[59,34994,34995],{},"pip install tweepy"," and restarting MindsDB was enough to get the error I was hoping for in the first place",[11,34998,34999],{},[59,35000,35001],{},"Can't connect to db: Error connecting to Twitter api: 401 Unauthorized Unauthorized. Check bearer_token",[11,35003,35004,35005,919,35008,35011,35012,35015],{},"Now we're all set for development. As the docs said to study the Twitter handler, I simply created a copy of the twitter_handler folder and renamed it to gmail_handler. Then replaced \"Twitter\" with \"Gmail\" in ",[59,35006,35007],{},"__init__.py",[59,35009,35010],{},"__about__.py"," files along with related method name changes in the ",[59,35013,35014],{},"gmail_handler"," file. Verified it by executing the same Twitter create database command but replacing the engine with \"gmail\", and it seemed to call our gmail_handler.",[24,35017,35019],{"id":35018},"implementing-the-gmail-handler","Implementing the Gmail Handler",[11,35021,35022,35023,35027,35028,35030,35031,35034],{},"Going by the steps mentioned in ",[47,35024,35026],{"href":34888,"rel":35025},[51],"how to create an application handler"," we need to modify the below methods. Before we can read\u002Fwrite emails we need to authenticate the user, so the first targets were the ",[59,35029,6407],{}," and the ",[59,35032,35033],{},"check_connection"," methods.",[11,35036,35037],{},[3135,35038],{"alt":35039,"src":35040},"Handler methods to implement","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002F29a859f0-b86d-439f-bea4-a341053208a1-903c3f4252.png",[32,35042,35044],{"id":35043},"setting-up-a-google-project-for-gmail-apis","Setting up a Google project for Gmail APIs",[11,35046,35047],{},"To use the Gmail APIs we need to set up a Google Cloud Project and a Google Account with Gmail enabled. We will also need to enable the Gmail API from the Google Cloud Console.",[11,35049,35050],{},"Then we need to create OAuth Client Ids for authenticating users, and possibly an Auth Consent Screen (if this is the first time we're setting up OAuth)",[11,35052,35053,35054,183],{},"Setting up OAuth Client Id will give us a credentials file which we will need in our gmail_handler for connection. You can find more information on ",[47,35055,35058],{"href":35056,"rel":35057},"https:\u002F\u002Fdevelopers.google.com\u002Fgmail\u002Fquickstart\u002Fpython",[51],"how to set up a Google project for the Gmail APIs here",[32,35060,35062],{"id":35061},"initing-the-gmailhandler-class","Initing the GmailHandler class",[11,35064,35065,35066,35069],{},"We take the connection arguments (which are passed with the CREATE DATABASE command) and store them for future use. We also register an ",[59,35067,35068],{},"\"emails\""," table where we will store our data.",[269,35071,35075],{"className":35072,"code":35073,"language":35074,"meta":274,"style":274},"language-python shiki shiki-themes github-light github-dark","class GmailHandler(APIHandler):\n    \"\"\"A class for handling connections and interactions with the Gmail API.\n\n    Attributes:\n        credentials_file (str): The path to the Google Auth Credentials file for authentication\n        and interacting with the Gmail API on behalf of the uesr.\n\n        scopes (List[str], Optional): The scopes to use when authenticating with the Gmail API.\n    \"\"\"\n\n    def __init__(self, name=None, **kwargs):\n        super().__init__(name)\n\n        self.connection_args = kwargs.get('connection_data', {})\n        self.credentials_file = self.connection_args['credentials_file']\n        self.scopes = self.connection_args.get('scopes', DEFAULT_SCOPES)\n        self.token_file = None\n        self.max_page_size = 500\n        self.max_batch_size = 100\n        self.service = None\n        self.is_connected = False\n\n        emails = EmailsTable(self)\n        self._register_table('emails', emails)\n","python",[59,35076,35077,35082,35087,35091,35096,35101,35106,35110,35115,35120,35124,35129,35134,35138,35143,35148,35153,35158,35163,35168,35173,35178,35182,35187],{"__ignoreMap":274},[278,35078,35079],{"class":280,"line":281},[278,35080,35081],{},"class GmailHandler(APIHandler):\n",[278,35083,35084],{"class":280,"line":288},[278,35085,35086],{},"    \"\"\"A class for handling connections and interactions with the Gmail API.\n",[278,35088,35089],{"class":280,"line":295},[278,35090,292],{"emptyLinePlaceholder":291},[278,35092,35093],{"class":280,"line":316},[278,35094,35095],{},"    Attributes:\n",[278,35097,35098],{"class":280,"line":322},[278,35099,35100],{},"        credentials_file (str): The path to the Google Auth Credentials file for authentication\n",[278,35102,35103],{"class":280,"line":327},[278,35104,35105],{},"        and interacting with the Gmail API on behalf of the uesr.\n",[278,35107,35108],{"class":280,"line":340},[278,35109,292],{"emptyLinePlaceholder":291},[278,35111,35112],{"class":280,"line":349},[278,35113,35114],{},"        scopes (List[str], Optional): The scopes to use when authenticating with the Gmail API.\n",[278,35116,35117],{"class":280,"line":375},[278,35118,35119],{},"    \"\"\"\n",[278,35121,35122],{"class":280,"line":386},[278,35123,292],{"emptyLinePlaceholder":291},[278,35125,35126],{"class":280,"line":397},[278,35127,35128],{},"    def __init__(self, name=None, **kwargs):\n",[278,35130,35131],{"class":280,"line":408},[278,35132,35133],{},"        super().__init__(name)\n",[278,35135,35136],{"class":280,"line":433},[278,35137,292],{"emptyLinePlaceholder":291},[278,35139,35140],{"class":280,"line":454},[278,35141,35142],{},"        self.connection_args = kwargs.get('connection_data', {})\n",[278,35144,35145],{"class":280,"line":475},[278,35146,35147],{},"        self.credentials_file = self.connection_args['credentials_file']\n",[278,35149,35150],{"class":280,"line":496},[278,35151,35152],{},"        self.scopes = self.connection_args.get('scopes', DEFAULT_SCOPES)\n",[278,35154,35155],{"class":280,"line":505},[278,35156,35157],{},"        self.token_file = None\n",[278,35159,35160],{"class":280,"line":516},[278,35161,35162],{},"        self.max_page_size = 500\n",[278,35164,35165],{"class":280,"line":527},[278,35166,35167],{},"        self.max_batch_size = 100\n",[278,35169,35170],{"class":280,"line":533},[278,35171,35172],{},"        self.service = None\n",[278,35174,35175],{"class":280,"line":539},[278,35176,35177],{},"        self.is_connected = False\n",[278,35179,35180],{"class":280,"line":545},[278,35181,292],{"emptyLinePlaceholder":291},[278,35183,35184],{"class":280,"line":551},[278,35185,35186],{},"        emails = EmailsTable(self)\n",[278,35188,35189],{"class":280,"line":557},[278,35190,35191],{},"        self._register_table('emails', emails)\n",[32,35193,35195],{"id":35194},"handling-google-authentication","Handling Google Authentication",[11,35197,35198],{},"Following the link in the previous section, and the MindsDB code requirements, we need to do the following",[123,35200,35201,35224,35410],{},[74,35202,35203,35204],{},"Replace the content of requirements.txt inside the gmail_handler folder with the following. Do remember to install these modules using the pip install command",[269,35205,35207],{"className":24597,"code":35206,"language":24599,"meta":274,"style":274},"google-api-python-client\ngoogle-auth-httplib2\ngoogle-auth-oauthlib\n",[59,35208,35209,35214,35219],{"__ignoreMap":274},[278,35210,35211],{"class":280,"line":281},[278,35212,35213],{},"google-api-python-client\n",[278,35215,35216],{"class":280,"line":288},[278,35217,35218],{},"google-auth-httplib2\n",[278,35220,35221],{"class":280,"line":295},[278,35222,35223],{},"google-auth-oauthlib\n",[74,35225,4796,35226,35228,35229],{},[59,35227,6407],{}," method. Here we use the credentials files created in the previous section for authenticating the user.",[269,35230,35232],{"className":35072,"code":35231,"language":35074,"meta":274,"style":274},"def connect(self):\n    \"\"\"Authenticate with the Gmail API using the credentials file.\n\n    Returns\n    -------\n    service: object\n        The authenticated Gmail API service object.\n    \"\"\"\n    if self.is_connected is True:\n        return self.service\n\n    self.service = self.create_connection()\n\n    self.is_connected = True\n    return self.service\n\ndef create_connection(self):\n    creds = None\n    token_file = os.path.join(os.path.dirname(self.credentials_file), 'token.json')\n\n    if os.path.isfile(token_file):\n        creds = Credentials.from_authorized_user_file(token_file, self.scopes)\n\n    if not creds or not creds.valid:\n        if creds and creds.expired and creds.refresh_token:\n            creds.refresh(Request())\n        elif not os.path.isfile(self.credentials_file):\n            raise Exception('Credentials must be a file path')\n        else:\n            flow = InstalledAppFlow.from_client_secrets_file(self.credentials_file, self.scopes)\n            creds = flow.run_local_server(port=0, timeout_seconds=120)\n\n    # Save the credentials for the next run\n    with open(token_file, 'w') as token:\n        token.write(creds.to_json())\n\n    return build('gmail', 'v1', credentials=creds)\n",[59,35233,35234,35239,35244,35248,35253,35258,35263,35268,35272,35277,35282,35286,35291,35295,35300,35305,35309,35314,35319,35324,35328,35333,35338,35342,35347,35352,35357,35362,35367,35372,35377,35382,35386,35391,35396,35401,35405],{"__ignoreMap":274},[278,35235,35236],{"class":280,"line":281},[278,35237,35238],{},"def connect(self):\n",[278,35240,35241],{"class":280,"line":288},[278,35242,35243],{},"    \"\"\"Authenticate with the Gmail API using the credentials file.\n",[278,35245,35246],{"class":280,"line":295},[278,35247,292],{"emptyLinePlaceholder":291},[278,35249,35250],{"class":280,"line":316},[278,35251,35252],{},"    Returns\n",[278,35254,35255],{"class":280,"line":322},[278,35256,35257],{},"    -------\n",[278,35259,35260],{"class":280,"line":327},[278,35261,35262],{},"    service: object\n",[278,35264,35265],{"class":280,"line":340},[278,35266,35267],{},"        The authenticated Gmail API service object.\n",[278,35269,35270],{"class":280,"line":349},[278,35271,35119],{},[278,35273,35274],{"class":280,"line":375},[278,35275,35276],{},"    if self.is_connected is True:\n",[278,35278,35279],{"class":280,"line":386},[278,35280,35281],{},"        return self.service\n",[278,35283,35284],{"class":280,"line":397},[278,35285,292],{"emptyLinePlaceholder":291},[278,35287,35288],{"class":280,"line":408},[278,35289,35290],{},"    self.service = self.create_connection()\n",[278,35292,35293],{"class":280,"line":433},[278,35294,292],{"emptyLinePlaceholder":291},[278,35296,35297],{"class":280,"line":454},[278,35298,35299],{},"    self.is_connected = True\n",[278,35301,35302],{"class":280,"line":475},[278,35303,35304],{},"    return self.service\n",[278,35306,35307],{"class":280,"line":496},[278,35308,292],{"emptyLinePlaceholder":291},[278,35310,35311],{"class":280,"line":505},[278,35312,35313],{},"def create_connection(self):\n",[278,35315,35316],{"class":280,"line":516},[278,35317,35318],{},"    creds = None\n",[278,35320,35321],{"class":280,"line":527},[278,35322,35323],{},"    token_file = os.path.join(os.path.dirname(self.credentials_file), 'token.json')\n",[278,35325,35326],{"class":280,"line":533},[278,35327,292],{"emptyLinePlaceholder":291},[278,35329,35330],{"class":280,"line":539},[278,35331,35332],{},"    if os.path.isfile(token_file):\n",[278,35334,35335],{"class":280,"line":545},[278,35336,35337],{},"        creds = Credentials.from_authorized_user_file(token_file, self.scopes)\n",[278,35339,35340],{"class":280,"line":551},[278,35341,292],{"emptyLinePlaceholder":291},[278,35343,35344],{"class":280,"line":557},[278,35345,35346],{},"    if not creds or not creds.valid:\n",[278,35348,35349],{"class":280,"line":567},[278,35350,35351],{},"        if creds and creds.expired and creds.refresh_token:\n",[278,35353,35354],{"class":280,"line":577},[278,35355,35356],{},"            creds.refresh(Request())\n",[278,35358,35359],{"class":280,"line":587},[278,35360,35361],{},"        elif not os.path.isfile(self.credentials_file):\n",[278,35363,35364],{"class":280,"line":597},[278,35365,35366],{},"            raise Exception('Credentials must be a file path')\n",[278,35368,35369],{"class":280,"line":608},[278,35370,35371],{},"        else:\n",[278,35373,35374],{"class":280,"line":614},[278,35375,35376],{},"            flow = InstalledAppFlow.from_client_secrets_file(self.credentials_file, self.scopes)\n",[278,35378,35379],{"class":280,"line":620},[278,35380,35381],{},"            creds = flow.run_local_server(port=0, timeout_seconds=120)\n",[278,35383,35384],{"class":280,"line":625},[278,35385,292],{"emptyLinePlaceholder":291},[278,35387,35388],{"class":280,"line":640},[278,35389,35390],{},"    # Save the credentials for the next run\n",[278,35392,35393],{"class":280,"line":663},[278,35394,35395],{},"    with open(token_file, 'w') as token:\n",[278,35397,35398],{"class":280,"line":669},[278,35399,35400],{},"        token.write(creds.to_json())\n",[278,35402,35403],{"class":280,"line":680},[278,35404,292],{"emptyLinePlaceholder":291},[278,35406,35407],{"class":280,"line":686},[278,35408,35409],{},"    return build('gmail', 'v1', credentials=creds)\n",[74,35411,4796,35412,35414,35415],{},[59,35413,35033],{}," method",[269,35416,35418],{"className":35072,"code":35417,"language":35074,"meta":274,"style":274},"def check_connection(self) -> StatusResponse:\n    \"\"\"Check connection to the handler.\n\n    Returns\n    -------\n    StatusResponse\n        Status confirmation\n    \"\"\"\n    response = StatusResponse(False)\n\n    try:\n        # Call the Gmail API\n        service = self.connect()\n\n        result = service.users().getProfile(userId='me').execute()\n\n        if result and result.get('emailAddress', None) is not None:\n            response.success = True\n    except HttpError as error:\n        response.error_message = f'Error connecting to Gmail api: {error}.'\n        log.logger.error(response.error_message)\n\n    if response.success is False and self.is_connected is True:\n        self.is_connected = False\n\n    return response\n",[59,35419,35420,35425,35430,35434,35438,35442,35447,35452,35456,35461,35465,35470,35475,35480,35484,35489,35493,35498,35503,35508,35513,35518,35522,35527,35531,35535],{"__ignoreMap":274},[278,35421,35422],{"class":280,"line":281},[278,35423,35424],{},"def check_connection(self) -> StatusResponse:\n",[278,35426,35427],{"class":280,"line":288},[278,35428,35429],{},"    \"\"\"Check connection to the handler.\n",[278,35431,35432],{"class":280,"line":295},[278,35433,292],{"emptyLinePlaceholder":291},[278,35435,35436],{"class":280,"line":316},[278,35437,35252],{},[278,35439,35440],{"class":280,"line":322},[278,35441,35257],{},[278,35443,35444],{"class":280,"line":327},[278,35445,35446],{},"    StatusResponse\n",[278,35448,35449],{"class":280,"line":340},[278,35450,35451],{},"        Status confirmation\n",[278,35453,35454],{"class":280,"line":349},[278,35455,35119],{},[278,35457,35458],{"class":280,"line":375},[278,35459,35460],{},"    response = StatusResponse(False)\n",[278,35462,35463],{"class":280,"line":386},[278,35464,292],{"emptyLinePlaceholder":291},[278,35466,35467],{"class":280,"line":397},[278,35468,35469],{},"    try:\n",[278,35471,35472],{"class":280,"line":408},[278,35473,35474],{},"        # Call the Gmail API\n",[278,35476,35477],{"class":280,"line":433},[278,35478,35479],{},"        service = self.connect()\n",[278,35481,35482],{"class":280,"line":454},[278,35483,292],{"emptyLinePlaceholder":291},[278,35485,35486],{"class":280,"line":475},[278,35487,35488],{},"        result = service.users().getProfile(userId='me').execute()\n",[278,35490,35491],{"class":280,"line":496},[278,35492,292],{"emptyLinePlaceholder":291},[278,35494,35495],{"class":280,"line":505},[278,35496,35497],{},"        if result and result.get('emailAddress', None) is not None:\n",[278,35499,35500],{"class":280,"line":516},[278,35501,35502],{},"            response.success = True\n",[278,35504,35505],{"class":280,"line":527},[278,35506,35507],{},"    except HttpError as error:\n",[278,35509,35510],{"class":280,"line":533},[278,35511,35512],{},"        response.error_message = f'Error connecting to Gmail api: {error}.'\n",[278,35514,35515],{"class":280,"line":539},[278,35516,35517],{},"        log.logger.error(response.error_message)\n",[278,35519,35520],{"class":280,"line":545},[278,35521,292],{"emptyLinePlaceholder":291},[278,35523,35524],{"class":280,"line":551},[278,35525,35526],{},"    if response.success is False and self.is_connected is True:\n",[278,35528,35529],{"class":280,"line":557},[278,35530,35177],{},[278,35532,35533],{"class":280,"line":567},[278,35534,292],{"emptyLinePlaceholder":291},[278,35536,35537],{"class":280,"line":577},[278,35538,35539],{},"    return response\n",[11,35541,35542],{},"Now we're ready to run the create database command and authenticate the user (of course we've not decided on the columns of our table but we will come to that). Simply run",[269,35544,35546],{"className":34915,"code":35545,"language":4698,"meta":274,"style":274},"CREATE DATABASE mindsdb_gmail\nWITH ENGINE = 'gmail',\nPARAMETERS = {\n  \"credentials_file\": \"mindsdb\u002Fintegrations\u002Fhandlers\u002Fgmail_handler\u002Fcredentials.json\"\n};\n",[59,35547,35548,35553,35558,35563,35568],{"__ignoreMap":274},[278,35549,35550],{"class":280,"line":281},[278,35551,35552],{},"CREATE DATABASE mindsdb_gmail\n",[278,35554,35555],{"class":280,"line":288},[278,35556,35557],{},"WITH ENGINE = 'gmail',\n",[278,35559,35560],{"class":280,"line":295},[278,35561,35562],{},"PARAMETERS = {\n",[278,35564,35565],{"class":280,"line":316},[278,35566,35567],{},"  \"credentials_file\": \"mindsdb\u002Fintegrations\u002Fhandlers\u002Fgmail_handler\u002Fcredentials.json\"\n",[278,35569,35570],{"class":280,"line":322},[278,35571,2817],{},[32,35573,35575],{"id":35574},"fetching-emails-from-the-gmail-api","Fetching Emails from the Gmail API",[11,35577,35578,35579,35582,35583,32632,35585,35588],{},"The flow for fetching emails using MindsDB is like this: you execute an ",[59,35580,35581],{},"SQL SELECT"," query, and the ",[59,35584,5291],{},[59,35586,35587],{},"APITable"," class gets called. There you parse the query params and finally call the Gmail API accordingly.",[71,35590,35591],{},[74,35592,4796,35593,35595],{},[59,35594,5291],{}," method of the EmailsTable",[269,35597,35599],{"className":35072,"code":35598,"language":35074,"meta":274,"style":274},"class EmailsTable(APITable):\n    \"\"\"Implementation for the emails table for Gmail\"\"\"\n\n    def select(self, query: ast.Select) -> Response:\n        \"\"\"Pulls emails from Gmail \"users.messages.list\" API\n\n        Parameters\n        ----------\n        query : ast.Select\n           Given SQL SELECT query\n\n        Returns\n        -------\n        pd.DataFrame\n            Email matching the query\n\n        Raises\n        ------\n        NotImplementedError\n            If the query contains an unsupported operation or condition\n        \"\"\"\n\n        conditions = extract_comparison_conditions(query.where)\n\n        params = {}\n        for op, arg1, arg2 in conditions:\n\n            if op == 'or':\n                raise NotImplementedError(f'OR is not supported')\n\n            if arg1 in ['query', 'label_ids', 'include_spam_trash']:\n                if op == '=':\n                    if arg1 == 'query':\n                        params['q'] = arg2\n                    elif arg1 == 'label_ids':\n                        params['labelIds'] = arg2.split(',')\n                    else:\n                        params['includeSpamTrash'] = arg2\n                else:\n                    raise NotImplementedError(f'Unknown op: {op}')\n\n            else:\n                raise NotImplementedError(f'Unknown clause: {arg1}')\n\n        if query.limit is not None:\n            params['maxResults'] = query.limit.value\n\n        result = self.handler.call_gmail_api(\n            method_name='list_messages',\n            params=params\n        )\n\n        # filter targets\n        columns = []\n        for target in query.targets:\n            if isinstance(target, ast.Star):\n                columns = []\n                break\n            elif isinstance(target, ast.Identifier):\n                columns.append(target.parts[-1])\n            else:\n                raise NotImplementedError(f\"Unknown query target {type(target)}\")\n\n        if len(columns) == 0:\n            columns = self.get_columns()\n\n        # columns to lower case\n        columns = [name.lower() for name in columns]\n\n        if len(result) == 0:\n            result = pd.DataFrame([], columns=columns)\n        else:\n            # add absent columns\n            for col in set(columns) & set(result.columns) ^ set(columns):\n                result[col] = None\n\n            # filter by columns\n            result = result[columns]\n        return result\n",[59,35600,35601,35606,35611,35615,35620,35625,35629,35634,35639,35644,35649,35653,35658,35663,35668,35673,35677,35682,35687,35692,35697,35702,35706,35711,35715,35720,35725,35729,35734,35739,35743,35748,35753,35758,35763,35768,35773,35778,35783,35788,35793,35797,35802,35807,35811,35816,35821,35825,35830,35835,35840,35844,35848,35853,35858,35863,35868,35873,35878,35883,35888,35892,35897,35901,35906,35911,35915,35920,35925,35929,35934,35939,35943,35948,35953,35958,35962,35967,35972],{"__ignoreMap":274},[278,35602,35603],{"class":280,"line":281},[278,35604,35605],{},"class EmailsTable(APITable):\n",[278,35607,35608],{"class":280,"line":288},[278,35609,35610],{},"    \"\"\"Implementation for the emails table for Gmail\"\"\"\n",[278,35612,35613],{"class":280,"line":295},[278,35614,292],{"emptyLinePlaceholder":291},[278,35616,35617],{"class":280,"line":316},[278,35618,35619],{},"    def select(self, query: ast.Select) -> Response:\n",[278,35621,35622],{"class":280,"line":322},[278,35623,35624],{},"        \"\"\"Pulls emails from Gmail \"users.messages.list\" API\n",[278,35626,35627],{"class":280,"line":327},[278,35628,292],{"emptyLinePlaceholder":291},[278,35630,35631],{"class":280,"line":340},[278,35632,35633],{},"        Parameters\n",[278,35635,35636],{"class":280,"line":349},[278,35637,35638],{},"        ----------\n",[278,35640,35641],{"class":280,"line":375},[278,35642,35643],{},"        query : ast.Select\n",[278,35645,35646],{"class":280,"line":386},[278,35647,35648],{},"           Given SQL SELECT query\n",[278,35650,35651],{"class":280,"line":397},[278,35652,292],{"emptyLinePlaceholder":291},[278,35654,35655],{"class":280,"line":408},[278,35656,35657],{},"        Returns\n",[278,35659,35660],{"class":280,"line":433},[278,35661,35662],{},"        -------\n",[278,35664,35665],{"class":280,"line":454},[278,35666,35667],{},"        pd.DataFrame\n",[278,35669,35670],{"class":280,"line":475},[278,35671,35672],{},"            Email matching the query\n",[278,35674,35675],{"class":280,"line":496},[278,35676,292],{"emptyLinePlaceholder":291},[278,35678,35679],{"class":280,"line":505},[278,35680,35681],{},"        Raises\n",[278,35683,35684],{"class":280,"line":516},[278,35685,35686],{},"        ------\n",[278,35688,35689],{"class":280,"line":527},[278,35690,35691],{},"        NotImplementedError\n",[278,35693,35694],{"class":280,"line":533},[278,35695,35696],{},"            If the query contains an unsupported operation or condition\n",[278,35698,35699],{"class":280,"line":539},[278,35700,35701],{},"        \"\"\"\n",[278,35703,35704],{"class":280,"line":545},[278,35705,292],{"emptyLinePlaceholder":291},[278,35707,35708],{"class":280,"line":551},[278,35709,35710],{},"        conditions = extract_comparison_conditions(query.where)\n",[278,35712,35713],{"class":280,"line":557},[278,35714,292],{"emptyLinePlaceholder":291},[278,35716,35717],{"class":280,"line":567},[278,35718,35719],{},"        params = {}\n",[278,35721,35722],{"class":280,"line":577},[278,35723,35724],{},"        for op, arg1, arg2 in conditions:\n",[278,35726,35727],{"class":280,"line":587},[278,35728,292],{"emptyLinePlaceholder":291},[278,35730,35731],{"class":280,"line":597},[278,35732,35733],{},"            if op == 'or':\n",[278,35735,35736],{"class":280,"line":608},[278,35737,35738],{},"                raise NotImplementedError(f'OR is not supported')\n",[278,35740,35741],{"class":280,"line":614},[278,35742,292],{"emptyLinePlaceholder":291},[278,35744,35745],{"class":280,"line":620},[278,35746,35747],{},"            if arg1 in ['query', 'label_ids', 'include_spam_trash']:\n",[278,35749,35750],{"class":280,"line":625},[278,35751,35752],{},"                if op == '=':\n",[278,35754,35755],{"class":280,"line":640},[278,35756,35757],{},"                    if arg1 == 'query':\n",[278,35759,35760],{"class":280,"line":663},[278,35761,35762],{},"                        params['q'] = arg2\n",[278,35764,35765],{"class":280,"line":669},[278,35766,35767],{},"                    elif arg1 == 'label_ids':\n",[278,35769,35770],{"class":280,"line":680},[278,35771,35772],{},"                        params['labelIds'] = arg2.split(',')\n",[278,35774,35775],{"class":280,"line":686},[278,35776,35777],{},"                    else:\n",[278,35779,35780],{"class":280,"line":1334},[278,35781,35782],{},"                        params['includeSpamTrash'] = arg2\n",[278,35784,35785],{"class":280,"line":1375},[278,35786,35787],{},"                else:\n",[278,35789,35790],{"class":280,"line":1381},[278,35791,35792],{},"                    raise NotImplementedError(f'Unknown op: {op}')\n",[278,35794,35795],{"class":280,"line":1386},[278,35796,292],{"emptyLinePlaceholder":291},[278,35798,35799],{"class":280,"line":1394},[278,35800,35801],{},"            else:\n",[278,35803,35804],{"class":280,"line":1406},[278,35805,35806],{},"                raise NotImplementedError(f'Unknown clause: {arg1}')\n",[278,35808,35809],{"class":280,"line":1423},[278,35810,292],{"emptyLinePlaceholder":291},[278,35812,35813],{"class":280,"line":1432},[278,35814,35815],{},"        if query.limit is not None:\n",[278,35817,35818],{"class":280,"line":1437},[278,35819,35820],{},"            params['maxResults'] = query.limit.value\n",[278,35822,35823],{"class":280,"line":1916},[278,35824,292],{"emptyLinePlaceholder":291},[278,35826,35827],{"class":280,"line":1939},[278,35828,35829],{},"        result = self.handler.call_gmail_api(\n",[278,35831,35832],{"class":280,"line":1949},[278,35833,35834],{},"            method_name='list_messages',\n",[278,35836,35837],{"class":280,"line":1954},[278,35838,35839],{},"            params=params\n",[278,35841,35842],{"class":280,"line":1959},[278,35843,29413],{},[278,35845,35846],{"class":280,"line":1985},[278,35847,292],{"emptyLinePlaceholder":291},[278,35849,35850],{"class":280,"line":1990},[278,35851,35852],{},"        # filter targets\n",[278,35854,35855],{"class":280,"line":1997},[278,35856,35857],{},"        columns = []\n",[278,35859,35860],{"class":280,"line":2006},[278,35861,35862],{},"        for target in query.targets:\n",[278,35864,35865],{"class":280,"line":2018},[278,35866,35867],{},"            if isinstance(target, ast.Star):\n",[278,35869,35870],{"class":280,"line":2029},[278,35871,35872],{},"                columns = []\n",[278,35874,35875],{"class":280,"line":2034},[278,35876,35877],{},"                break\n",[278,35879,35880],{"class":280,"line":2040},[278,35881,35882],{},"            elif isinstance(target, ast.Identifier):\n",[278,35884,35885],{"class":280,"line":2045},[278,35886,35887],{},"                columns.append(target.parts[-1])\n",[278,35889,35890],{"class":280,"line":2068},[278,35891,35801],{},[278,35893,35894],{"class":280,"line":2099},[278,35895,35896],{},"                raise NotImplementedError(f\"Unknown query target {type(target)}\")\n",[278,35898,35899],{"class":280,"line":6428},[278,35900,292],{"emptyLinePlaceholder":291},[278,35902,35903],{"class":280,"line":6439},[278,35904,35905],{},"        if len(columns) == 0:\n",[278,35907,35908],{"class":280,"line":6450},[278,35909,35910],{},"            columns = self.get_columns()\n",[278,35912,35913],{"class":280,"line":6455},[278,35914,292],{"emptyLinePlaceholder":291},[278,35916,35917],{"class":280,"line":6460},[278,35918,35919],{},"        # columns to lower case\n",[278,35921,35922],{"class":280,"line":6475},[278,35923,35924],{},"        columns = [name.lower() for name in columns]\n",[278,35926,35927],{"class":280,"line":6486},[278,35928,292],{"emptyLinePlaceholder":291},[278,35930,35931],{"class":280,"line":6491},[278,35932,35933],{},"        if len(result) == 0:\n",[278,35935,35936],{"class":280,"line":6518},[278,35937,35938],{},"            result = pd.DataFrame([], columns=columns)\n",[278,35940,35941],{"class":280,"line":6530},[278,35942,35371],{},[278,35944,35945],{"class":280,"line":6542},[278,35946,35947],{},"            # add absent columns\n",[278,35949,35950],{"class":280,"line":6547},[278,35951,35952],{},"            for col in set(columns) & set(result.columns) ^ set(columns):\n",[278,35954,35955],{"class":280,"line":6552},[278,35956,35957],{},"                result[col] = None\n",[278,35959,35960],{"class":280,"line":6567},[278,35961,292],{"emptyLinePlaceholder":291},[278,35963,35964],{"class":280,"line":6580},[278,35965,35966],{},"            # filter by columns\n",[278,35968,35969],{"class":280,"line":6593},[278,35970,35971],{},"            result = result[columns]\n",[278,35973,35974],{"class":280,"line":6605},[278,35975,35976],{},"        return result\n",[71,35978,35979],{},[74,35980,4796,35981,35984,35985,35988],{},[59,35982,35983],{},"get_columns"," method. These are the columns that our ",[59,35986,35987],{},"\"EmailsTable\""," will have.",[269,35990,35992],{"className":35072,"code":35991,"language":35074,"meta":274,"style":274},"def get_columns(self):\n    \"\"\"Gets all columns to be returned in pandas DataFrame responses\n\n    Returns\n    -------\n    List[str]\n        List of columns\n    \"\"\"\n    return [\n        'id',\n        'message_id',\n        'thread_id',\n        'label_ids',\n        'from',\n        'to',\n        'date',\n        'subject',\n        'snippet',\n        'body',\n    ]\n",[59,35993,35994,35999,36004,36008,36012,36016,36021,36026,36030,36035,36040,36045,36050,36055,36060,36065,36070,36075,36080,36085],{"__ignoreMap":274},[278,35995,35996],{"class":280,"line":281},[278,35997,35998],{},"def get_columns(self):\n",[278,36000,36001],{"class":280,"line":288},[278,36002,36003],{},"    \"\"\"Gets all columns to be returned in pandas DataFrame responses\n",[278,36005,36006],{"class":280,"line":295},[278,36007,292],{"emptyLinePlaceholder":291},[278,36009,36010],{"class":280,"line":316},[278,36011,35252],{},[278,36013,36014],{"class":280,"line":322},[278,36015,35257],{},[278,36017,36018],{"class":280,"line":327},[278,36019,36020],{},"    List[str]\n",[278,36022,36023],{"class":280,"line":340},[278,36024,36025],{},"        List of columns\n",[278,36027,36028],{"class":280,"line":349},[278,36029,35119],{},[278,36031,36032],{"class":280,"line":375},[278,36033,36034],{},"    return [\n",[278,36036,36037],{"class":280,"line":386},[278,36038,36039],{},"        'id',\n",[278,36041,36042],{"class":280,"line":397},[278,36043,36044],{},"        'message_id',\n",[278,36046,36047],{"class":280,"line":408},[278,36048,36049],{},"        'thread_id',\n",[278,36051,36052],{"class":280,"line":433},[278,36053,36054],{},"        'label_ids',\n",[278,36056,36057],{"class":280,"line":454},[278,36058,36059],{},"        'from',\n",[278,36061,36062],{"class":280,"line":475},[278,36063,36064],{},"        'to',\n",[278,36066,36067],{"class":280,"line":496},[278,36068,36069],{},"        'date',\n",[278,36071,36072],{"class":280,"line":505},[278,36073,36074],{},"        'subject',\n",[278,36076,36077],{"class":280,"line":516},[278,36078,36079],{},"        'snippet',\n",[278,36081,36082],{"class":280,"line":527},[278,36083,36084],{},"        'body',\n",[278,36086,36087],{"class":280,"line":533},[278,36088,36089],{},"    ]\n",[71,36091,36092],{},[74,36093,4796,36094,32632,36097,36100,36101,36104],{},[59,36095,36096],{},"call_gmail_api",[59,36098,36099],{},"GmailHandler"," class. The way the Gmail messages API works is, first it returns a list of the messages that match your query criteria. These messages contain just the threadId & the messageId etc and not the full email. Then using the ",[59,36102,36103],{},"\"messageIds\""," you fetch the full messages separately.",[269,36106,36108],{"className":35072,"code":36107,"language":35074,"meta":274,"style":274},"def call_gmail_api(self, method_name: str = None, params: dict = None):\n    \"\"\"Call Gmail API and map the data to pandas DataFrame\n    Args:\n        method_name (str): method name\n        params (dict): query parameters\n    Returns:\n        DataFrame\n    \"\"\"\n    service = self.connect()\n    if method_name == 'list_messages':\n        method = service.users().messages().list\n    elif method_name == 'send_message':\n        method = service.users().messages().send\n    else:\n        raise NotImplementedError(f'Unknown method_name: {method_name}')\n\n    left = None\n    count_results = None\n    if 'maxResults' in params:\n        count_results = params['maxResults']\n\n    params['userId'] = 'me'\n\n    data = []\n    limit_exec_time = time.time() + 60\n\n    while True:\n        if time.time() > limit_exec_time:\n            raise RuntimeError('Handler request timeout error')\n\n        if count_results is not None:\n            left = count_results - len(data)\n            if left == 0:\n                break\n            elif left \u003C 0:\n                # got more results that we need\n                data = data[:left]\n                break\n\n            if left > self.max_page_size:\n                params['maxResults'] = self.max_page_size\n            else:\n                params['maxResults'] = left\n\n        log.logger.debug(f'Calling Gmail API: {method_name} with params ({params})')\n\n        resp = method(**params).execute()\n\n        if 'messages' in resp:\n            self._handle_list_messages_response(data, resp['messages'])\n        elif isinstance(resp, dict):\n            data.append(resp)\n\n        if count_results is not None and 'nextPageToken' in resp:\n            params['pageToken'] = resp['nextPageToken']\n        else:\n            break\n\n    df = pd.DataFrame(data)\n\n    return df\n",[59,36109,36110,36115,36120,36125,36130,36135,36140,36145,36149,36154,36159,36164,36169,36174,36179,36184,36188,36193,36198,36203,36208,36212,36217,36221,36226,36231,36235,36240,36245,36250,36254,36259,36264,36269,36273,36278,36283,36288,36292,36296,36301,36306,36310,36315,36319,36324,36328,36333,36337,36342,36347,36352,36357,36361,36366,36371,36375,36380,36384,36389,36393],{"__ignoreMap":274},[278,36111,36112],{"class":280,"line":281},[278,36113,36114],{},"def call_gmail_api(self, method_name: str = None, params: dict = None):\n",[278,36116,36117],{"class":280,"line":288},[278,36118,36119],{},"    \"\"\"Call Gmail API and map the data to pandas DataFrame\n",[278,36121,36122],{"class":280,"line":295},[278,36123,36124],{},"    Args:\n",[278,36126,36127],{"class":280,"line":316},[278,36128,36129],{},"        method_name (str): method name\n",[278,36131,36132],{"class":280,"line":322},[278,36133,36134],{},"        params (dict): query parameters\n",[278,36136,36137],{"class":280,"line":327},[278,36138,36139],{},"    Returns:\n",[278,36141,36142],{"class":280,"line":340},[278,36143,36144],{},"        DataFrame\n",[278,36146,36147],{"class":280,"line":349},[278,36148,35119],{},[278,36150,36151],{"class":280,"line":375},[278,36152,36153],{},"    service = self.connect()\n",[278,36155,36156],{"class":280,"line":386},[278,36157,36158],{},"    if method_name == 'list_messages':\n",[278,36160,36161],{"class":280,"line":397},[278,36162,36163],{},"        method = service.users().messages().list\n",[278,36165,36166],{"class":280,"line":408},[278,36167,36168],{},"    elif method_name == 'send_message':\n",[278,36170,36171],{"class":280,"line":433},[278,36172,36173],{},"        method = service.users().messages().send\n",[278,36175,36176],{"class":280,"line":454},[278,36177,36178],{},"    else:\n",[278,36180,36181],{"class":280,"line":475},[278,36182,36183],{},"        raise NotImplementedError(f'Unknown method_name: {method_name}')\n",[278,36185,36186],{"class":280,"line":496},[278,36187,292],{"emptyLinePlaceholder":291},[278,36189,36190],{"class":280,"line":505},[278,36191,36192],{},"    left = None\n",[278,36194,36195],{"class":280,"line":516},[278,36196,36197],{},"    count_results = None\n",[278,36199,36200],{"class":280,"line":527},[278,36201,36202],{},"    if 'maxResults' in params:\n",[278,36204,36205],{"class":280,"line":533},[278,36206,36207],{},"        count_results = params['maxResults']\n",[278,36209,36210],{"class":280,"line":539},[278,36211,292],{"emptyLinePlaceholder":291},[278,36213,36214],{"class":280,"line":545},[278,36215,36216],{},"    params['userId'] = 'me'\n",[278,36218,36219],{"class":280,"line":551},[278,36220,292],{"emptyLinePlaceholder":291},[278,36222,36223],{"class":280,"line":557},[278,36224,36225],{},"    data = []\n",[278,36227,36228],{"class":280,"line":567},[278,36229,36230],{},"    limit_exec_time = time.time() + 60\n",[278,36232,36233],{"class":280,"line":577},[278,36234,292],{"emptyLinePlaceholder":291},[278,36236,36237],{"class":280,"line":587},[278,36238,36239],{},"    while True:\n",[278,36241,36242],{"class":280,"line":597},[278,36243,36244],{},"        if time.time() > limit_exec_time:\n",[278,36246,36247],{"class":280,"line":608},[278,36248,36249],{},"            raise RuntimeError('Handler request timeout error')\n",[278,36251,36252],{"class":280,"line":614},[278,36253,292],{"emptyLinePlaceholder":291},[278,36255,36256],{"class":280,"line":620},[278,36257,36258],{},"        if count_results is not None:\n",[278,36260,36261],{"class":280,"line":625},[278,36262,36263],{},"            left = count_results - len(data)\n",[278,36265,36266],{"class":280,"line":640},[278,36267,36268],{},"            if left == 0:\n",[278,36270,36271],{"class":280,"line":663},[278,36272,35877],{},[278,36274,36275],{"class":280,"line":669},[278,36276,36277],{},"            elif left \u003C 0:\n",[278,36279,36280],{"class":280,"line":680},[278,36281,36282],{},"                # got more results that we need\n",[278,36284,36285],{"class":280,"line":686},[278,36286,36287],{},"                data = data[:left]\n",[278,36289,36290],{"class":280,"line":1334},[278,36291,35877],{},[278,36293,36294],{"class":280,"line":1375},[278,36295,292],{"emptyLinePlaceholder":291},[278,36297,36298],{"class":280,"line":1381},[278,36299,36300],{},"            if left > self.max_page_size:\n",[278,36302,36303],{"class":280,"line":1386},[278,36304,36305],{},"                params['maxResults'] = self.max_page_size\n",[278,36307,36308],{"class":280,"line":1394},[278,36309,35801],{},[278,36311,36312],{"class":280,"line":1406},[278,36313,36314],{},"                params['maxResults'] = left\n",[278,36316,36317],{"class":280,"line":1423},[278,36318,292],{"emptyLinePlaceholder":291},[278,36320,36321],{"class":280,"line":1432},[278,36322,36323],{},"        log.logger.debug(f'Calling Gmail API: {method_name} with params ({params})')\n",[278,36325,36326],{"class":280,"line":1437},[278,36327,292],{"emptyLinePlaceholder":291},[278,36329,36330],{"class":280,"line":1916},[278,36331,36332],{},"        resp = method(**params).execute()\n",[278,36334,36335],{"class":280,"line":1939},[278,36336,292],{"emptyLinePlaceholder":291},[278,36338,36339],{"class":280,"line":1949},[278,36340,36341],{},"        if 'messages' in resp:\n",[278,36343,36344],{"class":280,"line":1954},[278,36345,36346],{},"            self._handle_list_messages_response(data, resp['messages'])\n",[278,36348,36349],{"class":280,"line":1959},[278,36350,36351],{},"        elif isinstance(resp, dict):\n",[278,36353,36354],{"class":280,"line":1985},[278,36355,36356],{},"            data.append(resp)\n",[278,36358,36359],{"class":280,"line":1990},[278,36360,292],{"emptyLinePlaceholder":291},[278,36362,36363],{"class":280,"line":1997},[278,36364,36365],{},"        if count_results is not None and 'nextPageToken' in resp:\n",[278,36367,36368],{"class":280,"line":2006},[278,36369,36370],{},"            params['pageToken'] = resp['nextPageToken']\n",[278,36372,36373],{"class":280,"line":2018},[278,36374,35371],{},[278,36376,36377],{"class":280,"line":2029},[278,36378,36379],{},"            break\n",[278,36381,36382],{"class":280,"line":2034},[278,36383,292],{"emptyLinePlaceholder":291},[278,36385,36386],{"class":280,"line":2040},[278,36387,36388],{},"    df = pd.DataFrame(data)\n",[278,36390,36391],{"class":280,"line":2045},[278,36392,292],{"emptyLinePlaceholder":291},[278,36394,36395],{"class":280,"line":2068},[278,36396,36397],{},"    return df\n",[71,36399,36400],{},[74,36401,36402,36403,36406],{},"Inner method ",[59,36404,36405],{},"_handle_list_messages_response"," and other related methods",[269,36408,36410],{"className":35072,"code":36409,"language":35074,"meta":274,"style":274},"# Handle the API response by downloading the full messages\n# using a Batch Request.\ndef _handle_list_messages_response(self, data, messages):\n    total_pages = len(messages) \u002F\u002F self.max_batch_size\n    for page in range(total_pages):\n        self._get_messages(data, messages[page * self.max_batch_size:(page + 1) * self.max_batch_size])\n\n    # Get the remaining messsages, if any\n    if len(messages) % self.max_batch_size > 0:\n        self._get_messages(data, messages[total_pages * self.max_batch_size:])\n\ndef _get_messages(self, data, messages):\n    batch_req = self.service.new_batch_http_request(lambda id, response, exception: self._parse_message(data, response, exception))\n    for message in messages:\n        batch_req.add(self.service.users().messages().get(userId='me', id=message['id']))\n\n    batch_req.execute()\n\n# This method shows how to parse the full email returned \n# by the Gmail API\ndef _parse_message(self, data, message, exception):\n    if exception:\n        log.logger.error(f'Exception in getting full email: {exception}')\n        return\n\n    payload = message['payload']\n    headers = payload.get(\"headers\")\n    parts = payload.get(\"parts\")\n\n    row = {\n        'id': message['id'],\n        'thread_id': message['threadId'],\n        'label_ids': message.get('labelIds', []),\n        'snippet': message.get('snippet', ''),\n    }\n\n    if headers:\n        for header in headers:\n            key = header['name'].lower()\n            value = header['value']\n\n            if key in ['from', 'to', 'subject', 'date']:\n                row[key] = value\n            elif key == 'message-id':\n                row['message_id'] = value\n\n    row['body'] = self._parse_parts(parts)\n\n    data.append(row)\n\ndef _parse_parts(self, parts):\n    if not parts:\n        return\n\n    body = ''\n    for part in parts:\n        if part['mimeType'] == 'text\u002Fplain':\n            part_body = part.get('body', {}).get('data', '')\n            body += urlsafe_b64decode(part_body).decode('utf-8')\n        elif part['mimeType'] == 'multipart\u002Falternative' or 'parts' in part:\n            # Recursively iterate over nested parts to find the plain text body\n            body += self._parse_parts(part['parts'])\n        else:\n            log.logger.debug(f\"Unhandled mimeType: {part['mimeType']}\")\n\n    return body\n",[59,36411,36412,36417,36422,36427,36432,36437,36442,36446,36451,36456,36461,36465,36470,36475,36480,36485,36489,36494,36498,36503,36508,36513,36518,36523,36528,36532,36537,36542,36547,36551,36556,36561,36566,36571,36576,36580,36584,36589,36594,36599,36604,36608,36613,36618,36623,36628,36632,36637,36641,36646,36650,36655,36660,36664,36668,36673,36678,36683,36688,36693,36698,36703,36708,36712,36717,36721],{"__ignoreMap":274},[278,36413,36414],{"class":280,"line":281},[278,36415,36416],{},"# Handle the API response by downloading the full messages\n",[278,36418,36419],{"class":280,"line":288},[278,36420,36421],{},"# using a Batch Request.\n",[278,36423,36424],{"class":280,"line":295},[278,36425,36426],{},"def _handle_list_messages_response(self, data, messages):\n",[278,36428,36429],{"class":280,"line":316},[278,36430,36431],{},"    total_pages = len(messages) \u002F\u002F self.max_batch_size\n",[278,36433,36434],{"class":280,"line":322},[278,36435,36436],{},"    for page in range(total_pages):\n",[278,36438,36439],{"class":280,"line":327},[278,36440,36441],{},"        self._get_messages(data, messages[page * self.max_batch_size:(page + 1) * self.max_batch_size])\n",[278,36443,36444],{"class":280,"line":340},[278,36445,292],{"emptyLinePlaceholder":291},[278,36447,36448],{"class":280,"line":349},[278,36449,36450],{},"    # Get the remaining messsages, if any\n",[278,36452,36453],{"class":280,"line":375},[278,36454,36455],{},"    if len(messages) % self.max_batch_size > 0:\n",[278,36457,36458],{"class":280,"line":386},[278,36459,36460],{},"        self._get_messages(data, messages[total_pages * self.max_batch_size:])\n",[278,36462,36463],{"class":280,"line":397},[278,36464,292],{"emptyLinePlaceholder":291},[278,36466,36467],{"class":280,"line":408},[278,36468,36469],{},"def _get_messages(self, data, messages):\n",[278,36471,36472],{"class":280,"line":433},[278,36473,36474],{},"    batch_req = self.service.new_batch_http_request(lambda id, response, exception: self._parse_message(data, response, exception))\n",[278,36476,36477],{"class":280,"line":454},[278,36478,36479],{},"    for message in messages:\n",[278,36481,36482],{"class":280,"line":475},[278,36483,36484],{},"        batch_req.add(self.service.users().messages().get(userId='me', id=message['id']))\n",[278,36486,36487],{"class":280,"line":496},[278,36488,292],{"emptyLinePlaceholder":291},[278,36490,36491],{"class":280,"line":505},[278,36492,36493],{},"    batch_req.execute()\n",[278,36495,36496],{"class":280,"line":516},[278,36497,292],{"emptyLinePlaceholder":291},[278,36499,36500],{"class":280,"line":527},[278,36501,36502],{},"# This method shows how to parse the full email returned \n",[278,36504,36505],{"class":280,"line":533},[278,36506,36507],{},"# by the Gmail API\n",[278,36509,36510],{"class":280,"line":539},[278,36511,36512],{},"def _parse_message(self, data, message, exception):\n",[278,36514,36515],{"class":280,"line":545},[278,36516,36517],{},"    if exception:\n",[278,36519,36520],{"class":280,"line":551},[278,36521,36522],{},"        log.logger.error(f'Exception in getting full email: {exception}')\n",[278,36524,36525],{"class":280,"line":557},[278,36526,36527],{},"        return\n",[278,36529,36530],{"class":280,"line":567},[278,36531,292],{"emptyLinePlaceholder":291},[278,36533,36534],{"class":280,"line":577},[278,36535,36536],{},"    payload = message['payload']\n",[278,36538,36539],{"class":280,"line":587},[278,36540,36541],{},"    headers = payload.get(\"headers\")\n",[278,36543,36544],{"class":280,"line":597},[278,36545,36546],{},"    parts = payload.get(\"parts\")\n",[278,36548,36549],{"class":280,"line":608},[278,36550,292],{"emptyLinePlaceholder":291},[278,36552,36553],{"class":280,"line":614},[278,36554,36555],{},"    row = {\n",[278,36557,36558],{"class":280,"line":620},[278,36559,36560],{},"        'id': message['id'],\n",[278,36562,36563],{"class":280,"line":625},[278,36564,36565],{},"        'thread_id': message['threadId'],\n",[278,36567,36568],{"class":280,"line":640},[278,36569,36570],{},"        'label_ids': message.get('labelIds', []),\n",[278,36572,36573],{"class":280,"line":663},[278,36574,36575],{},"        'snippet': message.get('snippet', ''),\n",[278,36577,36578],{"class":280,"line":669},[278,36579,1285],{},[278,36581,36582],{"class":280,"line":680},[278,36583,292],{"emptyLinePlaceholder":291},[278,36585,36586],{"class":280,"line":686},[278,36587,36588],{},"    if headers:\n",[278,36590,36591],{"class":280,"line":1334},[278,36592,36593],{},"        for header in headers:\n",[278,36595,36596],{"class":280,"line":1375},[278,36597,36598],{},"            key = header['name'].lower()\n",[278,36600,36601],{"class":280,"line":1381},[278,36602,36603],{},"            value = header['value']\n",[278,36605,36606],{"class":280,"line":1386},[278,36607,292],{"emptyLinePlaceholder":291},[278,36609,36610],{"class":280,"line":1394},[278,36611,36612],{},"            if key in ['from', 'to', 'subject', 'date']:\n",[278,36614,36615],{"class":280,"line":1406},[278,36616,36617],{},"                row[key] = value\n",[278,36619,36620],{"class":280,"line":1423},[278,36621,36622],{},"            elif key == 'message-id':\n",[278,36624,36625],{"class":280,"line":1432},[278,36626,36627],{},"                row['message_id'] = value\n",[278,36629,36630],{"class":280,"line":1437},[278,36631,292],{"emptyLinePlaceholder":291},[278,36633,36634],{"class":280,"line":1916},[278,36635,36636],{},"    row['body'] = self._parse_parts(parts)\n",[278,36638,36639],{"class":280,"line":1939},[278,36640,292],{"emptyLinePlaceholder":291},[278,36642,36643],{"class":280,"line":1949},[278,36644,36645],{},"    data.append(row)\n",[278,36647,36648],{"class":280,"line":1954},[278,36649,292],{"emptyLinePlaceholder":291},[278,36651,36652],{"class":280,"line":1959},[278,36653,36654],{},"def _parse_parts(self, parts):\n",[278,36656,36657],{"class":280,"line":1985},[278,36658,36659],{},"    if not parts:\n",[278,36661,36662],{"class":280,"line":1990},[278,36663,36527],{},[278,36665,36666],{"class":280,"line":1997},[278,36667,292],{"emptyLinePlaceholder":291},[278,36669,36670],{"class":280,"line":2006},[278,36671,36672],{},"    body = ''\n",[278,36674,36675],{"class":280,"line":2018},[278,36676,36677],{},"    for part in parts:\n",[278,36679,36680],{"class":280,"line":2029},[278,36681,36682],{},"        if part['mimeType'] == 'text\u002Fplain':\n",[278,36684,36685],{"class":280,"line":2034},[278,36686,36687],{},"            part_body = part.get('body', {}).get('data', '')\n",[278,36689,36690],{"class":280,"line":2040},[278,36691,36692],{},"            body += urlsafe_b64decode(part_body).decode('utf-8')\n",[278,36694,36695],{"class":280,"line":2045},[278,36696,36697],{},"        elif part['mimeType'] == 'multipart\u002Falternative' or 'parts' in part:\n",[278,36699,36700],{"class":280,"line":2068},[278,36701,36702],{},"            # Recursively iterate over nested parts to find the plain text body\n",[278,36704,36705],{"class":280,"line":2099},[278,36706,36707],{},"            body += self._parse_parts(part['parts'])\n",[278,36709,36710],{"class":280,"line":6428},[278,36711,35371],{},[278,36713,36714],{"class":280,"line":6439},[278,36715,36716],{},"            log.logger.debug(f\"Unhandled mimeType: {part['mimeType']}\")\n",[278,36718,36719],{"class":280,"line":6450},[278,36720,292],{"emptyLinePlaceholder":291},[278,36722,36723],{"class":280,"line":6455},[278,36724,36725],{},"    return body\n",[11,36727,36728,36729,36734],{},"The above is sufficient to fetch and store the emails of the authenticated user in the database. We can run an SQSL SELECT query like below to fetch the emails. The query parameter supports all the ",[47,36730,36733],{"href":36731,"rel":36732},"https:\u002F\u002Fsupport.google.com\u002Fmail\u002Fanswer\u002F7190",[51],"filter options"," that are available in the Gmail API",[269,36736,36738],{"className":34915,"code":36737,"language":4698,"meta":274,"style":274},"SELECT *\nFROM mindsdb_gmail.emails\nWHERE query = 'from:test@example.com OR search_text OR from:test@example1.com'\nAND label_ids = \"INBOX,UNREAD\" \nLIMIT 20;\n",[59,36739,36740,36745,36750,36755,36760],{"__ignoreMap":274},[278,36741,36742],{"class":280,"line":281},[278,36743,36744],{},"SELECT *\n",[278,36746,36747],{"class":280,"line":288},[278,36748,36749],{},"FROM mindsdb_gmail.emails\n",[278,36751,36752],{"class":280,"line":295},[278,36753,36754],{},"WHERE query = 'from:test@example.com OR search_text OR from:test@example1.com'\n",[278,36756,36757],{"class":280,"line":316},[278,36758,36759],{},"AND label_ids = \"INBOX,UNREAD\" \n",[278,36761,36762],{"class":280,"line":322},[278,36763,36764],{},"LIMIT 20;\n",[32,36766,36768],{"id":36767},"sending-emails-using-the-gmail-api","Sending Emails using the Gmail API",[11,36770,36771,36772,36775,36776,36779],{},"For sending emails through the Gmail API and MindsDB we need to use the ",[59,36773,36774],{},"SQL INSERT"," query. This in turn calls the insert method of the ",[59,36777,36778],{},"EmailsTable"," class, we created earlier.",[269,36781,36783],{"className":35072,"code":36782,"language":35074,"meta":274,"style":274},"def insert(self, query: ast.Insert):\n    \"\"\"Sends emails using the Gmail \"users.messages.send\" API\n\n    Parameters\n    ----------\n    query : ast.Insert\n        Given SQL INSERT query\n\n    Raises\n    ------\n    ValueError\n        If the query contains an unsupported condition\n    \"\"\"\n    columns = [col.name for col in query.columns]\n\n    if self.handler.connection_args.get('credentials_file', None) is None:\n        raise ValueError(\n            \"Need the Google Auth Credentials file in order to write an email\"\n        )\n\n    supported_columns = {\"message_id\", \"thread_id\", \"to_email\", \"subject\", \"body\"}\n    if not set(columns).issubset(supported_columns):\n        unsupported_columns = set(columns).difference(supported_columns)\n        raise ValueError(\n            \"Unsupported columns for create email: \"\n            + \", \".join(unsupported_columns)\n        )\n\n    for row in query.values:\n        params = dict(zip(columns, row))\n\n        if not 'to_email' in params:\n            raise ValueError('\"to_email\" parameter is required to send an email')\n\n        message = EmailMessage()\n        message['To'] = params['to_email']\n        message['Subject'] = params['subject'] if 'subject' in params else ''\n\n        content = params['body'] if 'body' in params else ''\n        message.set_content(content)\n\n        # If threadId is present then add References and In-Reply-To headers\n        # so that proper threading can happen\n        if 'thread_id' in params and 'message_id' in params:\n            message['In-Reply-To'] = params['message_id']\n            message['References'] = params['message_id']\n\n        encoded_message = urlsafe_b64encode(message.as_bytes()).decode()\n\n        message = {\n            'raw': encoded_message\n        }\n\n        if 'thread_id' in params:\n            message['threadId'] = params['thread_id']\n\n        self.handler.call_gmail_api('send_message', {'body': message})\n",[59,36784,36785,36790,36795,36799,36804,36809,36814,36819,36823,36828,36833,36838,36843,36847,36852,36856,36861,36866,36871,36875,36879,36884,36889,36894,36898,36903,36908,36912,36916,36921,36926,36930,36935,36940,36944,36949,36954,36959,36963,36968,36973,36977,36982,36987,36992,36997,37002,37006,37011,37015,37020,37025,37029,37033,37038,37043,37047],{"__ignoreMap":274},[278,36786,36787],{"class":280,"line":281},[278,36788,36789],{},"def insert(self, query: ast.Insert):\n",[278,36791,36792],{"class":280,"line":288},[278,36793,36794],{},"    \"\"\"Sends emails using the Gmail \"users.messages.send\" API\n",[278,36796,36797],{"class":280,"line":295},[278,36798,292],{"emptyLinePlaceholder":291},[278,36800,36801],{"class":280,"line":316},[278,36802,36803],{},"    Parameters\n",[278,36805,36806],{"class":280,"line":322},[278,36807,36808],{},"    ----------\n",[278,36810,36811],{"class":280,"line":327},[278,36812,36813],{},"    query : ast.Insert\n",[278,36815,36816],{"class":280,"line":340},[278,36817,36818],{},"        Given SQL INSERT query\n",[278,36820,36821],{"class":280,"line":349},[278,36822,292],{"emptyLinePlaceholder":291},[278,36824,36825],{"class":280,"line":375},[278,36826,36827],{},"    Raises\n",[278,36829,36830],{"class":280,"line":386},[278,36831,36832],{},"    ------\n",[278,36834,36835],{"class":280,"line":397},[278,36836,36837],{},"    ValueError\n",[278,36839,36840],{"class":280,"line":408},[278,36841,36842],{},"        If the query contains an unsupported condition\n",[278,36844,36845],{"class":280,"line":433},[278,36846,35119],{},[278,36848,36849],{"class":280,"line":454},[278,36850,36851],{},"    columns = [col.name for col in query.columns]\n",[278,36853,36854],{"class":280,"line":475},[278,36855,292],{"emptyLinePlaceholder":291},[278,36857,36858],{"class":280,"line":496},[278,36859,36860],{},"    if self.handler.connection_args.get('credentials_file', None) is None:\n",[278,36862,36863],{"class":280,"line":505},[278,36864,36865],{},"        raise ValueError(\n",[278,36867,36868],{"class":280,"line":516},[278,36869,36870],{},"            \"Need the Google Auth Credentials file in order to write an email\"\n",[278,36872,36873],{"class":280,"line":527},[278,36874,29413],{},[278,36876,36877],{"class":280,"line":533},[278,36878,292],{"emptyLinePlaceholder":291},[278,36880,36881],{"class":280,"line":539},[278,36882,36883],{},"    supported_columns = {\"message_id\", \"thread_id\", \"to_email\", \"subject\", \"body\"}\n",[278,36885,36886],{"class":280,"line":545},[278,36887,36888],{},"    if not set(columns).issubset(supported_columns):\n",[278,36890,36891],{"class":280,"line":551},[278,36892,36893],{},"        unsupported_columns = set(columns).difference(supported_columns)\n",[278,36895,36896],{"class":280,"line":557},[278,36897,36865],{},[278,36899,36900],{"class":280,"line":567},[278,36901,36902],{},"            \"Unsupported columns for create email: \"\n",[278,36904,36905],{"class":280,"line":577},[278,36906,36907],{},"            + \", \".join(unsupported_columns)\n",[278,36909,36910],{"class":280,"line":587},[278,36911,29413],{},[278,36913,36914],{"class":280,"line":597},[278,36915,292],{"emptyLinePlaceholder":291},[278,36917,36918],{"class":280,"line":608},[278,36919,36920],{},"    for row in query.values:\n",[278,36922,36923],{"class":280,"line":614},[278,36924,36925],{},"        params = dict(zip(columns, row))\n",[278,36927,36928],{"class":280,"line":620},[278,36929,292],{"emptyLinePlaceholder":291},[278,36931,36932],{"class":280,"line":625},[278,36933,36934],{},"        if not 'to_email' in params:\n",[278,36936,36937],{"class":280,"line":640},[278,36938,36939],{},"            raise ValueError('\"to_email\" parameter is required to send an email')\n",[278,36941,36942],{"class":280,"line":663},[278,36943,292],{"emptyLinePlaceholder":291},[278,36945,36946],{"class":280,"line":669},[278,36947,36948],{},"        message = EmailMessage()\n",[278,36950,36951],{"class":280,"line":680},[278,36952,36953],{},"        message['To'] = params['to_email']\n",[278,36955,36956],{"class":280,"line":686},[278,36957,36958],{},"        message['Subject'] = params['subject'] if 'subject' in params else ''\n",[278,36960,36961],{"class":280,"line":1334},[278,36962,292],{"emptyLinePlaceholder":291},[278,36964,36965],{"class":280,"line":1375},[278,36966,36967],{},"        content = params['body'] if 'body' in params else ''\n",[278,36969,36970],{"class":280,"line":1381},[278,36971,36972],{},"        message.set_content(content)\n",[278,36974,36975],{"class":280,"line":1386},[278,36976,292],{"emptyLinePlaceholder":291},[278,36978,36979],{"class":280,"line":1394},[278,36980,36981],{},"        # If threadId is present then add References and In-Reply-To headers\n",[278,36983,36984],{"class":280,"line":1406},[278,36985,36986],{},"        # so that proper threading can happen\n",[278,36988,36989],{"class":280,"line":1423},[278,36990,36991],{},"        if 'thread_id' in params and 'message_id' in params:\n",[278,36993,36994],{"class":280,"line":1432},[278,36995,36996],{},"            message['In-Reply-To'] = params['message_id']\n",[278,36998,36999],{"class":280,"line":1437},[278,37000,37001],{},"            message['References'] = params['message_id']\n",[278,37003,37004],{"class":280,"line":1916},[278,37005,292],{"emptyLinePlaceholder":291},[278,37007,37008],{"class":280,"line":1939},[278,37009,37010],{},"        encoded_message = urlsafe_b64encode(message.as_bytes()).decode()\n",[278,37012,37013],{"class":280,"line":1949},[278,37014,292],{"emptyLinePlaceholder":291},[278,37016,37017],{"class":280,"line":1954},[278,37018,37019],{},"        message = {\n",[278,37021,37022],{"class":280,"line":1959},[278,37023,37024],{},"            'raw': encoded_message\n",[278,37026,37027],{"class":280,"line":1985},[278,37028,6954],{},[278,37030,37031],{"class":280,"line":1990},[278,37032,292],{"emptyLinePlaceholder":291},[278,37034,37035],{"class":280,"line":1997},[278,37036,37037],{},"        if 'thread_id' in params:\n",[278,37039,37040],{"class":280,"line":2006},[278,37041,37042],{},"            message['threadId'] = params['thread_id']\n",[278,37044,37045],{"class":280,"line":2018},[278,37046,292],{"emptyLinePlaceholder":291},[278,37048,37049],{"class":280,"line":2029},[278,37050,37051],{},"        self.handler.call_gmail_api('send_message', {'body': message})\n",[11,37053,37054,37055,37057,37058,37060,37061,919,37064,37067,37068,37071],{},"This method calls the same ",[59,37056,36096],{}," we saw earlier to send an email. We can use an ",[59,37059,36774],{}," query to send an email. The ",[59,37062,37063],{},"thread_id",[59,37065,37066],{},"message_id"," parameter values are only required if we're replying to an incoming email and want that our reply should form a thread with the original email. (The ",[59,37069,37070],{},"\"subject\""," should exactly match the original subject line for it to work)",[269,37073,37075],{"className":34915,"code":37074,"language":4698,"meta":274,"style":274},"INSERT INTO mindsdb_gmail.emails (thread_id, message_id, to_email, subject, body)\nVALUES ('187cbdd861350934d', '8e54ccfd-abd0-756b-a12e-f7bc95ebc75b@Spark', 'test@example2.com', 'Trying out MindsDB',\n        'This seems awesome. You must try it out whenever you can.')\n",[59,37076,37077,37082,37087],{"__ignoreMap":274},[278,37078,37079],{"class":280,"line":281},[278,37080,37081],{},"INSERT INTO mindsdb_gmail.emails (thread_id, message_id, to_email, subject, body)\n",[278,37083,37084],{"class":280,"line":288},[278,37085,37086],{},"VALUES ('187cbdd861350934d', '8e54ccfd-abd0-756b-a12e-f7bc95ebc75b@Spark', 'test@example2.com', 'Trying out MindsDB',\n",[278,37088,37089],{"class":280,"line":295},[278,37090,37091],{},"        'This seems awesome. You must try it out whenever you can.')\n",[24,37093,37095],{"id":37094},"creating-the-gmail-bot","Creating the Gmail Bot",[11,37097,37098],{},"Now that we're unblocked and can fetch\u002Fsend emails easily using our shiny new GmailHandler, we're ready to work on our Gmail bot.",[32,37100,37102],{"id":37101},"obtaining-an-openai-api-key","Obtaining an OpenAI API key",[11,37104,37105],{},"Since we're developing this locally we do not have the luxury of using the inbuilt API key provided by MindsDB Cloud. We need to create an account on OpenAI and create an API key.",[11,37107,37108],{},[3135,37109],{"alt":37110,"src":37111},"Creating an API Key for OpenAI API","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002Fb83632b7-fc9a-42f4-a746-5f41eb5214c4-585b63482d.png",[32,37113,37115],{"id":37114},"training-the-model-using-mindsdb","Training the Model using MindsDB",[11,37117,37118,37119,37122],{},"I do not have access to GPT4 APIs, so I'm using the ",[59,37120,37121],{},"gpt-3.5-turbo"," model itself. We're just telling GPT to respond to the email with a proper salutation and signature and to keep the email tone casual. This is done using the prompt_template parameter.",[269,37124,37126],{"className":34915,"code":37125,"language":4698,"meta":274,"style":274},"CREATE MODEL mindsdb.gpt_model\nPREDICT response\nUSING\nengine = 'openai',\nmax_tokens = 500,\napi_key = '\u003Cyour_api_key>', \nmodel_name = 'gpt-3.5-turbo',\nprompt_template = 'From input message: {{input_text}}\\\nby from_user: {{from_email}}\\\nIn less than 500 characters, write an email response to {{from_email}} in the following format:\\\nStart with proper salutation and respond with a short message in a casual tone, and sign the email with my name mindsdb';\n",[59,37127,37128,37133,37138,37143,37148,37153,37158,37163,37168,37173,37178],{"__ignoreMap":274},[278,37129,37130],{"class":280,"line":281},[278,37131,37132],{},"CREATE MODEL mindsdb.gpt_model\n",[278,37134,37135],{"class":280,"line":288},[278,37136,37137],{},"PREDICT response\n",[278,37139,37140],{"class":280,"line":295},[278,37141,37142],{},"USING\n",[278,37144,37145],{"class":280,"line":316},[278,37146,37147],{},"engine = 'openai',\n",[278,37149,37150],{"class":280,"line":322},[278,37151,37152],{},"max_tokens = 500,\n",[278,37154,37155],{"class":280,"line":327},[278,37156,37157],{},"api_key = '\u003Cyour_api_key>', \n",[278,37159,37160],{"class":280,"line":340},[278,37161,37162],{},"model_name = 'gpt-3.5-turbo',\n",[278,37164,37165],{"class":280,"line":349},[278,37166,37167],{},"prompt_template = 'From input message: {{input_text}}\\\n",[278,37169,37170],{"class":280,"line":375},[278,37171,37172],{},"by from_user: {{from_email}}\\\n",[278,37174,37175],{"class":280,"line":386},[278,37176,37177],{},"In less than 500 characters, write an email response to {{from_email}} in the following format:\\\n",[278,37179,37180],{"class":280,"line":397},[278,37181,37182],{},"Start with proper salutation and respond with a short message in a casual tone, and sign the email with my name mindsdb';\n",[11,37184,37185],{},"Once the training is complete we're ready to see our bot in action. Run the following command",[269,37187,37189],{"className":34915,"code":37188,"language":4698,"meta":274,"style":274},"SELECT response\nFROM mindsdb.gpt_model_email\nWHERE from_email = \"alice@example.com\" \nAND input_text = \"Hi there, I'm bored. Give me a puzzle to solve\";\n",[59,37190,37191,37196,37201,37206],{"__ignoreMap":274},[278,37192,37193],{"class":280,"line":281},[278,37194,37195],{},"SELECT response\n",[278,37197,37198],{"class":280,"line":288},[278,37199,37200],{},"FROM mindsdb.gpt_model_email\n",[278,37202,37203],{"class":280,"line":295},[278,37204,37205],{},"WHERE from_email = \"alice@example.com\" \n",[278,37207,37208],{"class":280,"line":316},[278,37209,37210],{},"AND input_text = \"Hi there, I'm bored. Give me a puzzle to solve\";\n",[11,37212,37213],{},"And we get the following response, seems quite all right, isn't it?",[11,37215,37216],{},[3135,37217],{"alt":37218,"src":37219},"model email response in casual tone","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002F6f2a16bf-5bb5-4a5b-99fa-5e7aba3db750-68c4aac1a1.png",[11,37221,37222],{},"On asking for a new puzzle it says the following",[11,37224,37225],{},[3135,37226],{"alt":37227,"src":37228},"casual second response by GPT","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002F7a64f4f9-9b02-446a-946c-c049362b0ae3-218a96bb25.png",[32,37230,37232],{"id":37231},"giving-the-bot-a-persona","Giving the bot a persona",[11,37234,37235],{},"Since our initial experiments seem to work fine, we can get a bit adventurous now. Let's give our bot a combined persona of Master Yoda from the Star Wars movies, and Edgar Allan Poe, the famous poet. We do this by changing the prompt_template in our earlier command",[269,37237,37239],{"className":34915,"code":37238,"language":4698,"meta":274,"style":274},"CREATE MODEL mindsdb.gpt_model_yodapoe\nPREDICT response\nUSING\nengine = 'openai',\nmax_tokens = 800,\napi_key = '\u003Cyour_api_key>', \nmodel_name = 'gpt-3.5-turbo', -- you can also use 'text-davinci-003' or 'gpt-3.5-turbo'\nprompt_template = 'From input message: {{input_text}}\\\nby from_user: {{from_email}}\\\nIn less than 500 characters, write an email response to {{from_email}} in the following format:\\\n\u003Crespond with a 4 line poem as if you were Edgar Allan Poe but you are also a wise elder like Master Yoda from the Star Wars movies. The wordings should be like Master Yoda but the format should be like Poe. Do not mention that you are Master Yoda or Edgar Allan Poe. Sign it with a made up quote similar to what Voltaire, Nietzsche etc would say. Do not explain or say anything else about the quote.>';\n",[59,37240,37241,37246,37250,37254,37258,37263,37267,37272,37276,37280,37284],{"__ignoreMap":274},[278,37242,37243],{"class":280,"line":281},[278,37244,37245],{},"CREATE MODEL mindsdb.gpt_model_yodapoe\n",[278,37247,37248],{"class":280,"line":288},[278,37249,37137],{},[278,37251,37252],{"class":280,"line":295},[278,37253,37142],{},[278,37255,37256],{"class":280,"line":316},[278,37257,37147],{},[278,37259,37260],{"class":280,"line":322},[278,37261,37262],{},"max_tokens = 800,\n",[278,37264,37265],{"class":280,"line":327},[278,37266,37157],{},[278,37268,37269],{"class":280,"line":340},[278,37270,37271],{},"model_name = 'gpt-3.5-turbo', -- you can also use 'text-davinci-003' or 'gpt-3.5-turbo'\n",[278,37273,37274],{"class":280,"line":349},[278,37275,37167],{},[278,37277,37278],{"class":280,"line":375},[278,37279,37172],{},[278,37281,37282],{"class":280,"line":386},[278,37283,37177],{},[278,37285,37286],{"class":280,"line":397},[278,37287,37288],{},"\u003Crespond with a 4 line poem as if you were Edgar Allan Poe but you are also a wise elder like Master Yoda from the Star Wars movies. The wordings should be like Master Yoda but the format should be like Poe. Do not mention that you are Master Yoda or Edgar Allan Poe. Sign it with a made up quote similar to what Voltaire, Nietzsche etc would say. Do not explain or say anything else about the quote.>';\n",[11,37290,37291],{},"Train the model, and then run the same SELECT queries by changing the model name",[269,37293,37295],{"className":34915,"code":37294,"language":4698,"meta":274,"style":274},"SELECT response\nFROM mindsdb.gpt_model_yodapoe\nWHERE from_email = \"alice@example.com\" \nAND input_text = \"Hi there, I'm bored. Give me a puzzle to solve\";\n",[59,37296,37297,37301,37306,37310],{"__ignoreMap":274},[278,37298,37299],{"class":280,"line":281},[278,37300,37195],{},[278,37302,37303],{"class":280,"line":288},[278,37304,37305],{},"FROM mindsdb.gpt_model_yodapoe\n",[278,37307,37308],{"class":280,"line":295},[278,37309,37205],{},[278,37311,37312],{"class":280,"line":316},[278,37313,37210],{},[11,37315,37316],{},"This is what I get",[11,37318,37319],{},[3135,37320],{"alt":37321,"src":37322},"persona response for a puzzle ","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002F9e09c975-adb7-4081-a610-50f1ca4c2ca3-92ffb310b7.png",[11,37324,37325],{},"On changing the input text to the following",[269,37327,37329],{"className":34915,"code":37328,"language":4698,"meta":274,"style":274},"SELECT response\nFROM mindsdb.gpt_model_yodapoe\nWHERE from_email = \"alice@example.com\" \nAND input_text = \"Hi there, What's in a hackathon?\";\n",[59,37330,37331,37335,37339,37343],{"__ignoreMap":274},[278,37332,37333],{"class":280,"line":281},[278,37334,37195],{},[278,37336,37337],{"class":280,"line":288},[278,37338,37305],{},[278,37340,37341],{"class":280,"line":295},[278,37342,37205],{},[278,37344,37345],{"class":280,"line":316},[278,37346,37347],{},"AND input_text = \"Hi there, What's in a hackathon?\";\n",[11,37349,37350],{},"we get the following result",[11,37352,37353],{},[3135,37354],{"alt":37355,"src":37356},"what is in a hackathon response","\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002Fb78bcd53-3d16-4aab-8b70-7e2b7cfe2c37-e7fd240543.png",[11,37358,37359],{},"Overall this seems to be working fine. We can always fine-tune the persona based on our taste. Now we can connect this response generation with the actual email sending and we may as well create a scheduled job to read emails at regular intervals and reply to them based on some predefined criteria.",[24,37361,37363],{"id":37362},"outcomes","Outcomes",[11,37365,37366],{},"When I started working on this feature, I had the following goals and their respective outcomes at the end",[123,37368,37369,37372,37381,37389],{},[74,37370,37371],{},"Create the Gmail handler: This in my opinion is done. There may be some bugs that I'll need to solve in due course",[74,37373,37374,37375,37380],{},"Contribute to the MindsDB project: I've already ",[47,37376,37379],{"href":37377,"rel":37378},"https:\u002F\u002Fgithub.com\u002Fmindsdb\u002Fmindsdb\u002Fpull\u002F5889",[51],"opened a PR"," for my changes and now I'm hoping that it gets merged into the codebase.",[74,37382,37383,37384,183],{},"Create a Gmail Bot: We've all the ingredients in place, we just need to deploy it somewhere so that it is always available. I did try deploying the source on a droplet but I'm stuck with an error for which I've raised a ",[47,37385,37388],{"href":37386,"rel":37387},"https:\u002F\u002Fgithub.com\u002Fmindsdb\u002Fmindsdb\u002Fissues\u002F5892",[51],"GitHub issue",[74,37390,37391],{},"Practice my Python skills: I think I've made good progress on this while working on the feature. Python is not my area of expertise, so I'm quite pumped that I was able to create a working integration in a short span of 7-8 days.",[24,37393,10634],{"id":10633},[11,37395,37396],{},"Overall it was quite fun and interesting to create a Gmail Bot and the needed Gmail Handler for MindsDB. During the process, I got to see the inner workings of the MindsDB codebase. I learnt to use the Gmail APIs and how emails are structured behind the scenes and how to parse it. It also allowed me to use the MindsDB integration with OpenAI, and now I too can say AI is eating the world :-).",[11,37398,37399],{},"Hope you liked reading the article. If you've noticed any error or issue anywhere please do let me know in the comments.",[11,37401,37402],{},[3061,37403,37404],{},"-- Keep adding the bits, soon you'll have more bytes than you may need.",[3065,37406,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":37408},[37409,37410,37411,37412,37419,37424,37425],{"id":22771,"depth":288,"text":22772},{"id":34859,"depth":288,"text":34860},{"id":34899,"depth":288,"text":34900},{"id":35018,"depth":288,"text":35019,"children":37413},[37414,37415,37416,37417,37418],{"id":35043,"depth":295,"text":35044},{"id":35061,"depth":295,"text":35062},{"id":35194,"depth":295,"text":35195},{"id":35574,"depth":295,"text":35575},{"id":36767,"depth":295,"text":36768},{"id":37094,"depth":288,"text":37095,"children":37420},[37421,37422,37423],{"id":37101,"depth":295,"text":37102},{"id":37114,"depth":295,"text":37115},{"id":37231,"depth":295,"text":37232},{"id":37362,"depth":288,"text":37363},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb\u002Fa4045aa3-6c49-4e0d-ab2b-89effb24538d-7a7fced9c1.jpeg","2023-04-30T11:37:56.088Z","clh3c82go000h09mm7drj7ykn",{},"\u002Fhow-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb",{"title":34836,"description":22772},"how-to-make-a-gmail-bot-using-openai-gpt-and-mindsdb",[35074,37434,18316,37435,37436],"gmail","mindsdb","mindsdbhackathon","u6I_cbkVmIYQ9qM2SrN_wIMpGu3re1Iw7-2g-bcUL0U",{"id":37439,"title":37440,"body":37441,"cover":38586,"date":38587,"description":38588,"draft":3086,"extension":3087,"hashnodeId":38589,"meta":38590,"navigation":291,"path":38591,"seo":38592,"slug":38593,"stem":38593,"tags":38594,"__hash__":38598},"posts\u002Fmastering-keyboard-navigation-with-roving-tabindex-in-grids.md","Mastering Keyboard Navigation with Roving tabindex in Grids",{"type":8,"value":37442,"toc":38571},[37443,37450,37452,37461,37475,37485,37489,37506,37512,37527,37530,37534,37537,37560,37564,37567,37571,37588,37591,37595,37601,37604,37608,37618,37745,37754,37764,37827,37831,37834,37900,37908,37911,38059,38063,38066,38071,38086,38096,38105,38111,38266,38273,38535,38547,38550,38552,38563,38566,38569],[11,37444,37445,37446,37449],{},"Did you ever try to navigate a webpage using just your keyboard? Do you know how it works under the hood? Do you know what is ",[59,37447,37448],{},"\"tabindex\","," and what is its role in keyboard navigation? In this article, we will cover all these burning questions and also learn to implement custom keyboard navigation for a grid using the roving tabindex technique.",[24,37451,22772],{"id":22771},[11,37453,37454,37455,37460],{},"Recently I was looking to implement keyboard navigation for my daily puzzle game ",[47,37456,37459],{"href":37457,"rel":37458},"https:\u002F\u002Fplaygoldroad.com",[51],"GoldRoad",". I had a vague idea about adding key event listeners and using these key presses but I wanted to learn if there is something more to it. And I wasn't disappointed. This article tells the story of the rabbit hole I fell into.",[11,37462,37463,37464,37467,37468,37471,37472,37474],{},"To navigate a website using the keyboard, we typically use the ",[59,37465,37466],{},"Tab"," key (If you're on the Safari browser, and haven't changed any settings then you need to use the ",[59,37469,37470],{},"option + Tab"," keys). We keep pressing the ",[59,37473,37466],{}," key and we're taken to different elements of the webpage.",[11,37476,37477,37478,37480,37481,37484],{},"If you're on a laptop or desktop right now, and if you haven't tried it already, you can experience it yourself by pressing the ",[59,37479,37466],{}," key a couple of times. The elements that get focussed and in what order are decided by their ",[59,37482,37483],{},"\"tabindex\""," attribute.",[32,37486,37488],{"id":37487},"what-is-tabindex","What is tabindex?",[11,37490,37491,37492,37495,37496,37498,37499,37501,37502,37505],{},"As the name suggests, ",[59,37493,37494],{},"tabindex"," is the index of tabbing and it is a global HTML attribute. It decides which elements on a page get focussed and in what order when keyboard navigation is done using the ",[59,37497,37466],{}," key. It can take integer values: a lower positive value implies that the element will be reached (and focussed) ahead of others (including elements having ",[59,37500,37494],{}," of 0). Any negative value (usually ",[59,37503,37504],{},"-1"," is used) means that the element can't be reached by pressing the tab key alone.",[11,37507,37508,37509,37511],{},"Some interactive HTML elements like buttons, inputs, selects, anchor tags etc get a default ",[59,37510,37494],{}," value of 0, and these are the elements that usually get focussed when we do keyboard navigation.",[11,37513,37514,37515,37517,37518,37520,37521,37523,37524,37526],{},"Run the below CodeSandBox to see it in action. The ",[59,37516,24],{}," element has a ",[59,37519,37494],{}," of ",[59,37522,2012],{},", and since it is present before the grid of buttons, it gets focused first when you press the ",[59,37525,37466],{}," key, and then the buttons get focused one after another in the order they were added to the DOM, and so on. Notice that the button with tabindex -1 doesn't get focussed.",[40,37528],{"url":37529},"https:\u002F\u002Fcodesandbox.io\u002Fembed\u002Fbasic-tab-index-demo-1s3ec9?fontsize=14&hidenavigation=1&theme=dark&view=preview?runonclick=1",[32,37531,37533],{"id":37532},"shortcomings-of-the-tab-key","Shortcomings of the Tab key",[11,37535,37536],{},"If you tried navigating the previous grid, you'll notice the below problems",[123,37538,37539,37548,37554],{},[74,37540,37541,37542,37544,37545,37547],{},"To get to the 9th button, 10 key presses are needed (one for the ",[59,37543,24],{}," element, and then 9 more for the 9 buttons). And how do you go back and forth between these buttons? We can keep pressing the ",[59,37546,37466],{}," key but that is not an optimal experience.",[74,37549,37550,37551,37553],{},"What if we wanted to start from the middle of the grid? We could give the middle button a positive ",[59,37552,37494],{}," value, but even though positive integers are valid values we should avoid using values greater than 0 (this is because it messes up the keyboard navigation for people using assistive technologies).",[74,37555,37556,37557,37559],{},"Similar to issue 1, if we've other focusable elements below the grid (e.g. the bottom ",[59,37558,11],{}," tag), then it will take a lot of key presses to reach there. How do we solve this?",[24,37561,37563],{"id":37562},"grid-navigation-using-the-arrow-keys","Grid Navigation using the Arrow Keys",[11,37565,37566],{},"We can improve our grid navigation by using the arrow keys. This will solve the 1st issue, and maybe the 3rd issue partially, but to solve it effectively we need to consider the grid as a single entity in terms of the tab stops. So the first tab key takes us to the h2 element, the second one takes us to the grid, and the third tab press takes us to the paragraph element. And to navigate inside the grid we use the arrow keys with the help of the roving tabindex technique.",[32,37568,37570],{"id":37569},"what-is-roving-tabindex","What is Roving tabindex?",[11,37572,37573,37574,37576,37577,37580,37581,37584,37585,37587],{},"To consider the grid as a single entity we need to assign a ",[59,37575,37494],{}," of -1 to all the inner buttons and give a ",[59,37578,37579],{},"tabindex=\"0\""," to that one button where the focus should land. Then we use ",[59,37582,37583],{},"EventListeners"," for tracking the key presses, and we keep roving this ",[59,37586,37579],{}," and the corresponding focus to the appropriate button. At a time there will be only one button having a 0 tabindex.",[11,37589,37590],{},"This technique of managing focus inside a component using the tabindex attribute is called the roving tabindex technique. This also allows us to come back to the same element of the grid from where we tab away from it (this is a requirement for better accessibility).",[24,37592,37594],{"id":37593},"roving-tabindex-implementation","Roving tabindex implementation",[11,37596,37597,37598,37600],{},"Enough talk. Let's implement the \"roving tabindex\" technique for the below grid. You can try navigating this grid with your arrow keys after pressing the ",[59,37599,37466],{}," key once.",[40,37602],{"url":37603},"https:\u002F\u002Fcodesandbox.io\u002Fembed\u002Fnavigate-a-grid-using-keyboard-with-roving-tabindex-technique-v5ucrj?fontsize=14&hidenavigation=1&theme=dark&view=preview&runonclick=1",[32,37605,37607],{"id":37606},"create-a-grid-of-buttons","Create a Grid of Buttons",[11,37609,37610,37611,37614,37615,37617],{},"Create a new react project with the create-react-app command or use whichever CRA alternative you prefer. Then create a new file called ",[59,37612,37613],{},"GridNavigator.js"," inside your ",[59,37616,24951],{}," folder. Add the following code to the file.",[269,37619,37621],{"className":24597,"code":37620,"language":24599,"meta":274,"style":274},"import { useRef } from \"react\";\n\nexport const GridNavigator = ({ rows, cols, start }) => {\n  const currentIdx = useRef(start);\n\n  return (\n    \u003Cdiv>\n      {Array.from(Array(rows), (_, row) => (\n        \u003Cdiv key={`row-${row}`}>\n          {Array.from(Array(cols), (_, col) => {\n            const idx = `${row}${col}`;\n\n            return (\n              \u003Cbutton\n                key={`col-${idx}`}\n                tabIndex={currentIdx.current === idx ? \"0\" : \"-1\"}\n              >\n                {idx}\n              \u003C\u002Fbutton>\n            );\n          })}\n        \u003C\u002Fdiv>\n      ))}\n    \u003C\u002Fdiv>\n  );\n};\n",[59,37622,37623,37628,37632,37637,37642,37646,37651,37656,37661,37666,37671,37676,37680,37685,37690,37695,37700,37705,37710,37715,37719,37724,37728,37733,37737,37741],{"__ignoreMap":274},[278,37624,37625],{"class":280,"line":281},[278,37626,37627],{},"import { useRef } from \"react\";\n",[278,37629,37630],{"class":280,"line":288},[278,37631,292],{"emptyLinePlaceholder":291},[278,37633,37634],{"class":280,"line":295},[278,37635,37636],{},"export const GridNavigator = ({ rows, cols, start }) => {\n",[278,37638,37639],{"class":280,"line":316},[278,37640,37641],{},"  const currentIdx = useRef(start);\n",[278,37643,37644],{"class":280,"line":322},[278,37645,292],{"emptyLinePlaceholder":291},[278,37647,37648],{"class":280,"line":327},[278,37649,37650],{},"  return (\n",[278,37652,37653],{"class":280,"line":340},[278,37654,37655],{},"    \u003Cdiv>\n",[278,37657,37658],{"class":280,"line":349},[278,37659,37660],{},"      {Array.from(Array(rows), (_, row) => (\n",[278,37662,37663],{"class":280,"line":375},[278,37664,37665],{},"        \u003Cdiv key={`row-${row}`}>\n",[278,37667,37668],{"class":280,"line":386},[278,37669,37670],{},"          {Array.from(Array(cols), (_, col) => {\n",[278,37672,37673],{"class":280,"line":397},[278,37674,37675],{},"            const idx = `${row}${col}`;\n",[278,37677,37678],{"class":280,"line":408},[278,37679,292],{"emptyLinePlaceholder":291},[278,37681,37682],{"class":280,"line":433},[278,37683,37684],{},"            return (\n",[278,37686,37687],{"class":280,"line":454},[278,37688,37689],{},"              \u003Cbutton\n",[278,37691,37692],{"class":280,"line":475},[278,37693,37694],{},"                key={`col-${idx}`}\n",[278,37696,37697],{"class":280,"line":496},[278,37698,37699],{},"                tabIndex={currentIdx.current === idx ? \"0\" : \"-1\"}\n",[278,37701,37702],{"class":280,"line":505},[278,37703,37704],{},"              >\n",[278,37706,37707],{"class":280,"line":516},[278,37708,37709],{},"                {idx}\n",[278,37711,37712],{"class":280,"line":527},[278,37713,37714],{},"              \u003C\u002Fbutton>\n",[278,37716,37717],{"class":280,"line":533},[278,37718,14244],{},[278,37720,37721],{"class":280,"line":539},[278,37722,37723],{},"          })}\n",[278,37725,37726],{"class":280,"line":545},[278,37727,27745],{},[278,37729,37730],{"class":280,"line":551},[278,37731,37732],{},"      ))}\n",[278,37734,37735],{"class":280,"line":557},[278,37736,7950],{},[278,37738,37739],{"class":280,"line":567},[278,37740,611],{},[278,37742,37743],{"class":280,"line":577},[278,37744,2817],{},[11,37746,37747,37748,37750,37751,4633],{},"This component takes the number of rows & columns from its parent and creates a corresponding grid of buttons. It also sets the ",[59,37749,37494],{}," of each of the buttons to -1 (except the button having its ",[59,37752,37753],{},"idx === start",[11,37755,37756,37757,37760,37761,26634],{},"To test this ",[59,37758,37759],{},"GridNavigator"," you can call it inside your ",[59,37762,37763],{},"App.js",[269,37765,37767],{"className":24597,"code":37766,"language":24599,"meta":274,"style":274},"import { GridNavigator } from '.\u002FGridNavigator';\n\nimport '.\u002FApp.css';\n\nfunction App() {\n  return (\n    \u003Cdiv className=\"App\">\n      \u003CGridNavigator rows={5} cols={5} start='24' \u002F>\n    \u003C\u002Fdiv>\n  );\n}\n\nexport default App;\n",[59,37768,37769,37774,37778,37783,37787,37792,37796,37801,37806,37810,37814,37818,37822],{"__ignoreMap":274},[278,37770,37771],{"class":280,"line":281},[278,37772,37773],{},"import { GridNavigator } from '.\u002FGridNavigator';\n",[278,37775,37776],{"class":280,"line":288},[278,37777,292],{"emptyLinePlaceholder":291},[278,37779,37780],{"class":280,"line":295},[278,37781,37782],{},"import '.\u002FApp.css';\n",[278,37784,37785],{"class":280,"line":316},[278,37786,292],{"emptyLinePlaceholder":291},[278,37788,37789],{"class":280,"line":322},[278,37790,37791],{},"function App() {\n",[278,37793,37794],{"class":280,"line":327},[278,37795,37650],{},[278,37797,37798],{"class":280,"line":340},[278,37799,37800],{},"    \u003Cdiv className=\"App\">\n",[278,37802,37803],{"class":280,"line":349},[278,37804,37805],{},"      \u003CGridNavigator rows={5} cols={5} start='24' \u002F>\n",[278,37807,37808],{"class":280,"line":375},[278,37809,7950],{},[278,37811,37812],{"class":280,"line":386},[278,37813,611],{},[278,37815,37816],{"class":280,"line":397},[278,37817,617],{},[278,37819,37820],{"class":280,"line":408},[278,37821,292],{"emptyLinePlaceholder":291},[278,37823,37824],{"class":280,"line":433},[278,37825,37826],{},"export default App;\n",[32,37828,37830],{"id":37829},"setting-the-accessibility-attributes","Setting the Accessibility Attributes",[11,37832,37833],{},"It is a good practice to assign proper roles to each grid item for better accessibility. And you should also assign proper ARIA attributes to describe the grid to the users who take the help of assistive technologies. Below are some of the attributes we'll be using",[71,37835,37836,37866,37872,37887],{},[74,37837,37838,37841,37842,37845,37846,37849,37850,37853,37854,37857,37858,37861,37862,37865],{},[59,37839,37840],{},"role",": Since our grid is made up of interactive elements we'll be using ",[59,37843,37844],{},"role=\"grid\""," for the outermost div, ",[59,37847,37848],{},"role=\"row\""," for each row, and ",[59,37851,37852],{},"role=\"gridcell\""," for the buttons. Other possible replacements for the ",[59,37855,37856],{},"grid"," role is the ",[59,37859,37860],{},"table"," or the ",[59,37863,37864],{},"treegrid"," roles.",[74,37867,37868,37871],{},[59,37869,37870],{},"aria-label",": This can be used to give the grid a proper caption.",[74,37873,37874,19634,37877,37880,37881,37883,37884],{},[59,37875,37876],{},"aria-rowcount",[59,37878,37879],{},"aria-colcount",": For mentioning the rows & columns count of the grid. These attributes need to be used on the outermost div having the ",[59,37882,37840],{},"=\"",[59,37885,37886],{},"grid\".",[74,37888,37889,19634,37892,37895,37896,37899],{},[59,37890,37891],{},"aria-rowindex",[59,37893,37894],{},"aria-colindex",": On each button having the role of ",[59,37897,37898],{},"gridcell",", we should mention its row & column index.",[11,37901,37902,37903,183],{},"For more details about various aria attributes related to the grid role, you can ",[47,37904,37907],{"href":37905,"rel":37906},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAccessibility\u002FARIA\u002FRoles\u002Fgrid_role",[51],"visit this MDN link",[11,37909,37910],{},"Make minor adjustments to the code from the last section as shown below",[269,37912,37914],{"className":24597,"code":37913,"language":24599,"meta":274,"style":274},"\u002F\u002F... rest of the code\nreturn (\n  \u003Cdiv \n    role=\"grid\"\n    aria-label='A grid of buttons'\n    aria-rowcount={rows} \n    aria-colcount={cols}\n  >\n    {Array.from(Array(rows), (_, row) => (\n      \u003Cdiv key={`row-${row}`} role=\"row\">\n        {Array.from(Array(cols), (_, col) => {\n          const idx = `${row}${col}`;\n\n          return (\n            \u003Cbutton\n              key={`col-${idx}`}\n              tabIndex={currentIdx.current === idx ? \"0\" : \"-1\"}\n              role=\"gridcell\"\n              aria-rowindex={row}\n              aria-colindex={col}\n            >\n              {idx}\n            \u003C\u002Fbutton>\n          );\n        })}\n      \u003C\u002Fdiv>\n    ))}\n  \u003C\u002Fdiv>\n);\n\u002F\u002F... rest of the code\n",[59,37915,37916,37921,37926,37931,37936,37941,37946,37951,37955,37960,37965,37970,37975,37979,37984,37989,37994,37999,38004,38009,38014,38019,38024,38029,38033,38038,38042,38047,38051,38055],{"__ignoreMap":274},[278,37917,37918],{"class":280,"line":281},[278,37919,37920],{},"\u002F\u002F... rest of the code\n",[278,37922,37923],{"class":280,"line":288},[278,37924,37925],{},"return (\n",[278,37927,37928],{"class":280,"line":295},[278,37929,37930],{},"  \u003Cdiv \n",[278,37932,37933],{"class":280,"line":316},[278,37934,37935],{},"    role=\"grid\"\n",[278,37937,37938],{"class":280,"line":322},[278,37939,37940],{},"    aria-label='A grid of buttons'\n",[278,37942,37943],{"class":280,"line":327},[278,37944,37945],{},"    aria-rowcount={rows} \n",[278,37947,37948],{"class":280,"line":340},[278,37949,37950],{},"    aria-colcount={cols}\n",[278,37952,37953],{"class":280,"line":349},[278,37954,7200],{},[278,37956,37957],{"class":280,"line":375},[278,37958,37959],{},"    {Array.from(Array(rows), (_, row) => (\n",[278,37961,37962],{"class":280,"line":386},[278,37963,37964],{},"      \u003Cdiv key={`row-${row}`} role=\"row\">\n",[278,37966,37967],{"class":280,"line":397},[278,37968,37969],{},"        {Array.from(Array(cols), (_, col) => {\n",[278,37971,37972],{"class":280,"line":408},[278,37973,37974],{},"          const idx = `${row}${col}`;\n",[278,37976,37977],{"class":280,"line":433},[278,37978,292],{"emptyLinePlaceholder":291},[278,37980,37981],{"class":280,"line":454},[278,37982,37983],{},"          return (\n",[278,37985,37986],{"class":280,"line":475},[278,37987,37988],{},"            \u003Cbutton\n",[278,37990,37991],{"class":280,"line":496},[278,37992,37993],{},"              key={`col-${idx}`}\n",[278,37995,37996],{"class":280,"line":505},[278,37997,37998],{},"              tabIndex={currentIdx.current === idx ? \"0\" : \"-1\"}\n",[278,38000,38001],{"class":280,"line":516},[278,38002,38003],{},"              role=\"gridcell\"\n",[278,38005,38006],{"class":280,"line":527},[278,38007,38008],{},"              aria-rowindex={row}\n",[278,38010,38011],{"class":280,"line":533},[278,38012,38013],{},"              aria-colindex={col}\n",[278,38015,38016],{"class":280,"line":539},[278,38017,38018],{},"            >\n",[278,38020,38021],{"class":280,"line":545},[278,38022,38023],{},"              {idx}\n",[278,38025,38026],{"class":280,"line":551},[278,38027,38028],{},"            \u003C\u002Fbutton>\n",[278,38030,38031],{"class":280,"line":557},[278,38032,14787],{},[278,38034,38035],{"class":280,"line":567},[278,38036,38037],{},"        })}\n",[278,38039,38040],{"class":280,"line":577},[278,38041,7873],{},[278,38043,38044],{"class":280,"line":587},[278,38045,38046],{},"    ))}\n",[278,38048,38049],{"class":280,"line":597},[278,38050,19082],{},[278,38052,38053],{"class":280,"line":608},[278,38054,1280],{},[278,38056,38057],{"class":280,"line":614},[278,38058,37920],{},[32,38060,38062],{"id":38061},"adding-the-key-event-listener","Adding the key event listener",[11,38064,38065],{},"Add a key-down event listener on the outermost div. Apart from the event listener, we will give every button a ref so that we can use it to focus the appropriate button on arrow key presses.",[11,38067,38068,38069,9801],{},"Make the following changes to the ",[59,38070,37759],{},[269,38072,38074],{"className":24597,"code":38073,"language":24599,"meta":274,"style":274},"\u002F\u002F import createRef\nimport { useRef, createRef } from 'react';\n",[59,38075,38076,38081],{"__ignoreMap":274},[278,38077,38078],{"class":280,"line":281},[278,38079,38080],{},"\u002F\u002F import createRef\n",[278,38082,38083],{"class":280,"line":288},[278,38084,38085],{},"import { useRef, createRef } from 'react';\n",[11,38087,38088,38089,38091,38092,38095],{},"Inside the ",[59,38090,37759],{}," function, create a ",[59,38093,38094],{},"ref"," to store the references of all the grid buttons",[269,38097,38099],{"className":24597,"code":38098,"language":24599,"meta":274,"style":274},"const btnRefs = useRef({});\n",[59,38100,38101],{"__ignoreMap":274},[278,38102,38103],{"class":280,"line":281},[278,38104,38098],{},[11,38106,38107,38108,38110],{},"Add the key event listener on the div with ",[59,38109,37844],{},". Also, create and add a ref to all the buttons",[269,38112,38114],{"className":24597,"code":38113,"language":24599,"meta":274,"style":274},"return (\n    \u003Cdiv\n      role='grid'\n      aria-label='A grid of buttons'\n      aria-rowcount={rows}\n      aria-colcount={cols}\n      onKeyDown={onKeyDown}\n    >\n      {Array.from(Array(rows), (_, row) => (\n        \u003Cdiv key={`row-${row}`} role='row'>\n          {Array.from(Array(cols), (_, col) => {\n            const idx = `${row}${col}`;\n            \u002F\u002F If we don't have a ref for this button, create it\n            if (!btnRefs.current[idx]) {\n              btnRefs.current[idx] = createRef();\n            }\n\n            return (\n              \u003Cbutton\n                key={`col-${idx}`}\n                ref={btnRefs.current[idx]}\n                tabIndex={currentIdx.current === idx ? '0' : '-1'}\n                role='gridcell'\n                aria-rowindex={row}\n                aria-colindex={col}\n              >\n                {idx}\n              \u003C\u002Fbutton>\n            );\n          })}\n        \u003C\u002Fdiv>\n      ))}\n    \u003C\u002Fdiv>\n  );\n",[59,38115,38116,38120,38124,38129,38134,38139,38144,38149,38153,38157,38162,38166,38170,38175,38180,38185,38189,38193,38197,38201,38205,38210,38215,38220,38225,38230,38234,38238,38242,38246,38250,38254,38258,38262],{"__ignoreMap":274},[278,38117,38118],{"class":280,"line":281},[278,38119,37925],{},[278,38121,38122],{"class":280,"line":288},[278,38123,7920],{},[278,38125,38126],{"class":280,"line":295},[278,38127,38128],{},"      role='grid'\n",[278,38130,38131],{"class":280,"line":316},[278,38132,38133],{},"      aria-label='A grid of buttons'\n",[278,38135,38136],{"class":280,"line":322},[278,38137,38138],{},"      aria-rowcount={rows}\n",[278,38140,38141],{"class":280,"line":327},[278,38142,38143],{},"      aria-colcount={cols}\n",[278,38145,38146],{"class":280,"line":340},[278,38147,38148],{},"      onKeyDown={onKeyDown}\n",[278,38150,38151],{"class":280,"line":349},[278,38152,7935],{},[278,38154,38155],{"class":280,"line":375},[278,38156,37660],{},[278,38158,38159],{"class":280,"line":386},[278,38160,38161],{},"        \u003Cdiv key={`row-${row}`} role='row'>\n",[278,38163,38164],{"class":280,"line":397},[278,38165,37670],{},[278,38167,38168],{"class":280,"line":408},[278,38169,37675],{},[278,38171,38172],{"class":280,"line":433},[278,38173,38174],{},"            \u002F\u002F If we don't have a ref for this button, create it\n",[278,38176,38177],{"class":280,"line":454},[278,38178,38179],{},"            if (!btnRefs.current[idx]) {\n",[278,38181,38182],{"class":280,"line":475},[278,38183,38184],{},"              btnRefs.current[idx] = createRef();\n",[278,38186,38187],{"class":280,"line":496},[278,38188,15703],{},[278,38190,38191],{"class":280,"line":505},[278,38192,292],{"emptyLinePlaceholder":291},[278,38194,38195],{"class":280,"line":516},[278,38196,37684],{},[278,38198,38199],{"class":280,"line":527},[278,38200,37689],{},[278,38202,38203],{"class":280,"line":533},[278,38204,37694],{},[278,38206,38207],{"class":280,"line":539},[278,38208,38209],{},"                ref={btnRefs.current[idx]}\n",[278,38211,38212],{"class":280,"line":545},[278,38213,38214],{},"                tabIndex={currentIdx.current === idx ? '0' : '-1'}\n",[278,38216,38217],{"class":280,"line":551},[278,38218,38219],{},"                role='gridcell'\n",[278,38221,38222],{"class":280,"line":557},[278,38223,38224],{},"                aria-rowindex={row}\n",[278,38226,38227],{"class":280,"line":567},[278,38228,38229],{},"                aria-colindex={col}\n",[278,38231,38232],{"class":280,"line":577},[278,38233,37704],{},[278,38235,38236],{"class":280,"line":587},[278,38237,37709],{},[278,38239,38240],{"class":280,"line":597},[278,38241,37714],{},[278,38243,38244],{"class":280,"line":608},[278,38245,14244],{},[278,38247,38248],{"class":280,"line":614},[278,38249,37723],{},[278,38251,38252],{"class":280,"line":620},[278,38253,27745],{},[278,38255,38256],{"class":280,"line":625},[278,38257,37732],{},[278,38259,38260],{"class":280,"line":640},[278,38261,7950],{},[278,38263,38264],{"class":280,"line":663},[278,38265,611],{},[11,38267,38268,38269,38272],{},"Add the ",[59,38270,38271],{},"onKeyDown"," function along with the helper functions",[269,38274,38276],{"className":24597,"code":38275,"language":24599,"meta":274,"style":274},"\u002F\u002F Parses the row & col value of the currently selected button\nconst parseRowCol = () => {\n  const row = parseInt(currentIdx.current[0], 10);\n  const col = parseInt(currentIdx.current[1], 10);\n  return { row, col };\n};\n\n\u002F\u002F Moves the focus & saves the id of the newly focused button\nconst handleFocus = (row, col) => {\n  currentIdx.current = `${row}${col}`;\n  const btnRef = btnRefs.current[currentIdx.current];\n  btnRef.current.focus();\n};\n\n\u002F\u002F Handles keyboard events\nconst onKeyDown = (event) => {\n  const { row, col } = parseRowCol();\n\n  switch (event.key) {\n    case 'ArrowUp':\n      if (row > 0) {\n        handleFocus(row - 1, col);\n      }\n\n      break;\n    case 'ArrowDown':\n      if (row \u003C rows - 1) {\n        handleFocus(row + 1, col);\n      }\n\n      break;\n    case 'ArrowLeft':\n      \u002F\u002F If we're on the leftmost col then move to the extreme right\n      \u002F\u002F col of the previous row, provided it's not the first row\n      if (col > 0) {\n        handleFocus(row, col - 1);\n      } else if (row > 0) {\n        handleFocus(row - 1, cols - 1);\n      }\n\n      break;\n    case 'ArrowRight':\n      \u002F\u002F If we're on the rightmost col then move to the first \n      \u002F\u002F col of the next row, provided it's not the last row\n      if (col \u003C cols - 1) {\n        handleFocus(row, col + 1);\n      } else if (row \u003C rows - 1) {\n        handleFocus(row + 1, 0);\n      }\n\n      break;\n    default:\n      return;\n  }\n};\n",[59,38277,38278,38283,38288,38293,38298,38303,38307,38311,38316,38321,38326,38331,38336,38340,38344,38349,38354,38359,38363,38368,38373,38378,38383,38387,38391,38396,38401,38406,38411,38415,38419,38423,38428,38433,38438,38443,38448,38453,38458,38462,38466,38470,38475,38480,38485,38490,38495,38500,38505,38509,38513,38517,38522,38527,38531],{"__ignoreMap":274},[278,38279,38280],{"class":280,"line":281},[278,38281,38282],{},"\u002F\u002F Parses the row & col value of the currently selected button\n",[278,38284,38285],{"class":280,"line":288},[278,38286,38287],{},"const parseRowCol = () => {\n",[278,38289,38290],{"class":280,"line":295},[278,38291,38292],{},"  const row = parseInt(currentIdx.current[0], 10);\n",[278,38294,38295],{"class":280,"line":316},[278,38296,38297],{},"  const col = parseInt(currentIdx.current[1], 10);\n",[278,38299,38300],{"class":280,"line":322},[278,38301,38302],{},"  return { row, col };\n",[278,38304,38305],{"class":280,"line":327},[278,38306,2817],{},[278,38308,38309],{"class":280,"line":340},[278,38310,292],{"emptyLinePlaceholder":291},[278,38312,38313],{"class":280,"line":349},[278,38314,38315],{},"\u002F\u002F Moves the focus & saves the id of the newly focused button\n",[278,38317,38318],{"class":280,"line":375},[278,38319,38320],{},"const handleFocus = (row, col) => {\n",[278,38322,38323],{"class":280,"line":386},[278,38324,38325],{},"  currentIdx.current = `${row}${col}`;\n",[278,38327,38328],{"class":280,"line":397},[278,38329,38330],{},"  const btnRef = btnRefs.current[currentIdx.current];\n",[278,38332,38333],{"class":280,"line":408},[278,38334,38335],{},"  btnRef.current.focus();\n",[278,38337,38338],{"class":280,"line":433},[278,38339,2817],{},[278,38341,38342],{"class":280,"line":454},[278,38343,292],{"emptyLinePlaceholder":291},[278,38345,38346],{"class":280,"line":475},[278,38347,38348],{},"\u002F\u002F Handles keyboard events\n",[278,38350,38351],{"class":280,"line":496},[278,38352,38353],{},"const onKeyDown = (event) => {\n",[278,38355,38356],{"class":280,"line":505},[278,38357,38358],{},"  const { row, col } = parseRowCol();\n",[278,38360,38361],{"class":280,"line":516},[278,38362,292],{"emptyLinePlaceholder":291},[278,38364,38365],{"class":280,"line":527},[278,38366,38367],{},"  switch (event.key) {\n",[278,38369,38370],{"class":280,"line":533},[278,38371,38372],{},"    case 'ArrowUp':\n",[278,38374,38375],{"class":280,"line":539},[278,38376,38377],{},"      if (row > 0) {\n",[278,38379,38380],{"class":280,"line":545},[278,38381,38382],{},"        handleFocus(row - 1, col);\n",[278,38384,38385],{"class":280,"line":551},[278,38386,6234],{},[278,38388,38389],{"class":280,"line":557},[278,38390,292],{"emptyLinePlaceholder":291},[278,38392,38393],{"class":280,"line":567},[278,38394,38395],{},"      break;\n",[278,38397,38398],{"class":280,"line":577},[278,38399,38400],{},"    case 'ArrowDown':\n",[278,38402,38403],{"class":280,"line":587},[278,38404,38405],{},"      if (row \u003C rows - 1) {\n",[278,38407,38408],{"class":280,"line":597},[278,38409,38410],{},"        handleFocus(row + 1, col);\n",[278,38412,38413],{"class":280,"line":608},[278,38414,6234],{},[278,38416,38417],{"class":280,"line":614},[278,38418,292],{"emptyLinePlaceholder":291},[278,38420,38421],{"class":280,"line":620},[278,38422,38395],{},[278,38424,38425],{"class":280,"line":625},[278,38426,38427],{},"    case 'ArrowLeft':\n",[278,38429,38430],{"class":280,"line":640},[278,38431,38432],{},"      \u002F\u002F If we're on the leftmost col then move to the extreme right\n",[278,38434,38435],{"class":280,"line":663},[278,38436,38437],{},"      \u002F\u002F col of the previous row, provided it's not the first row\n",[278,38439,38440],{"class":280,"line":669},[278,38441,38442],{},"      if (col > 0) {\n",[278,38444,38445],{"class":280,"line":680},[278,38446,38447],{},"        handleFocus(row, col - 1);\n",[278,38449,38450],{"class":280,"line":686},[278,38451,38452],{},"      } else if (row > 0) {\n",[278,38454,38455],{"class":280,"line":1334},[278,38456,38457],{},"        handleFocus(row - 1, cols - 1);\n",[278,38459,38460],{"class":280,"line":1375},[278,38461,6234],{},[278,38463,38464],{"class":280,"line":1381},[278,38465,292],{"emptyLinePlaceholder":291},[278,38467,38468],{"class":280,"line":1386},[278,38469,38395],{},[278,38471,38472],{"class":280,"line":1394},[278,38473,38474],{},"    case 'ArrowRight':\n",[278,38476,38477],{"class":280,"line":1406},[278,38478,38479],{},"      \u002F\u002F If we're on the rightmost col then move to the first \n",[278,38481,38482],{"class":280,"line":1423},[278,38483,38484],{},"      \u002F\u002F col of the next row, provided it's not the last row\n",[278,38486,38487],{"class":280,"line":1432},[278,38488,38489],{},"      if (col \u003C cols - 1) {\n",[278,38491,38492],{"class":280,"line":1437},[278,38493,38494],{},"        handleFocus(row, col + 1);\n",[278,38496,38497],{"class":280,"line":1916},[278,38498,38499],{},"      } else if (row \u003C rows - 1) {\n",[278,38501,38502],{"class":280,"line":1939},[278,38503,38504],{},"        handleFocus(row + 1, 0);\n",[278,38506,38507],{"class":280,"line":1949},[278,38508,6234],{},[278,38510,38511],{"class":280,"line":1954},[278,38512,292],{"emptyLinePlaceholder":291},[278,38514,38515],{"class":280,"line":1959},[278,38516,38395],{},[278,38518,38519],{"class":280,"line":1985},[278,38520,38521],{},"    default:\n",[278,38523,38524],{"class":280,"line":1990},[278,38525,38526],{},"      return;\n",[278,38528,38529],{"class":280,"line":1997},[278,38530,1096],{},[278,38532,38533],{"class":280,"line":2006},[278,38534,2817],{},[11,38536,38537,38538,38540,38541,38543,38544,38546],{},"Now try navigating the grid using your keyboard. To start the navigation you need to press the ",[59,38539,37466],{}," key which will put the focus on the button having ",[59,38542,37753],{},". Afterwards, you can use your arrow keys to navigate the grid. To come out of the grid press the ",[59,38545,37466],{}," key once more. If you try to focus the grid once more, your focus should land on the exact button where you left it.",[11,38548,38549],{},"This implementation solves all the problems we set out to solve. But some of the end users may not know that they can use the arrow keys to navigate the grid, so we mustn't rely on keyboard navigation alone.",[24,38551,10634],{"id":10633},[71,38553,38554,38557,38560],{},[74,38555,38556],{},"Keyboard navigation is crucial for web accessibility and improves the user experience for those who depend on it.",[74,38558,38559],{},"The roving tabindex method can enhance keyboard navigation for complicated interactive elements such as grids.",[74,38561,38562],{},"Combining tabindex with correct accessibility attributes such as roles and ARIA attributes ensures that web applications are accessible to a broader audience, including those with disabilities.",[11,38564,38565],{},"Hope you enjoyed reading the article. If you found any mistake in the article please let me know in the comments so that I can fix it.",[11,38567,38568],{},"Cheers :-)",[3065,38570,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":38572},[38573,38577,38580,38585],{"id":22771,"depth":288,"text":22772,"children":38574},[38575,38576],{"id":37487,"depth":295,"text":37488},{"id":37532,"depth":295,"text":37533},{"id":37562,"depth":288,"text":37563,"children":38578},[38579],{"id":37569,"depth":295,"text":37570},{"id":37593,"depth":288,"text":37594,"children":38581},[38582,38583,38584],{"id":37606,"depth":295,"text":37607},{"id":37829,"depth":295,"text":37830},{"id":38061,"depth":295,"text":38062},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fmastering-keyboard-navigation-with-roving-tabindex-in-grids\u002F394038a94a9e8ba3eeb96ef292f9aced-acbd5a9583.jpeg","2023-04-06T18:04:03.633Z","Did you ever try to navigate a webpage using just your keyboard? Do you know how it works under the hood? Do you know what is `\"tabindex\",` and what is its role in keyboard navi...","clg5fg6nl000809mhhm9n4dcd",{},"\u002Fmastering-keyboard-navigation-with-roving-tabindex-in-grids",{"title":37440,"description":38588},"mastering-keyboard-navigation-with-roving-tabindex-in-grids",[38595,24599,38596,3095,38597],"tutorial","accessibility","html5","kZX-CV1LieQFQPu5fueFrQ_mGsPnpQ8O2PHHkaZgX3M",{"id":38600,"title":38601,"body":38602,"cover":40289,"date":40290,"description":40291,"draft":3086,"extension":3087,"hashnodeId":40292,"meta":40293,"navigation":291,"path":40294,"seo":40295,"slug":40296,"stem":40296,"tags":40297,"__hash__":40301},"posts\u002Fguide-to-api-caching-with-firebase-cdn.md","Boost Your API Performance with Firebase CDN: A Guide to API Caching",{"type":8,"value":38603,"toc":40272},[38604,38614,38616,38619,38623,38629,38633,38642,38645,38650,38659,38664,38683,38686,38689,38693,38701,38704,38708,38711,38724,38727,38733,38743,38799,38818,38897,38900,38918,38928,38944,39501,39504,39510,39513,39519,39523,39530,39628,39631,39709,39721,39755,39762,39772,39778,39793,39796,39801,39807,39812,39818,39821,39866,39870,39881,39890,39896,39899,39929,39932,39935,39941,39948,39960,39966,39983,39989,40005,40008,40019,40023,40026,40029,40038,40042,40045,40101,40103,40111,40117,40121,40128,40131,40224,40227,40229,40232,40235,40238,40240,40244,40252,40255,40262,40269],[11,38605,38606,38607,179,38610,38613],{},"If you're using or thinking of using ",[59,38608,38609],{},"Firebase Cloud Functions",[59,38611,38612],{},"Cloud Run"," for your website\u002Fapp backend, I highly recommend looking into caching your API responses. Used appropriately it can improve the speed and performance of your website\u002Fapp. This article will give you a simple overview of what CDN and caching are, and how you can easily configure them in Firebase.",[24,38615,22772],{"id":22771},[11,38617,38618],{},"Before going further, let's review the two terms, Caching and CDN, which are essential for a basic understanding of the topic.",[32,38620,38622],{"id":38621},"what-is-caching","What is Caching?",[11,38624,38625,38628],{},[94,38626,38627],{},"Caching"," is the process of storing copies of some of your data in temporary storage locations for faster retrieval. These temporary storage locations can be anywhere in between and including the original content storage (e.g. a database) and the client (e.g. your browser).",[32,38630,38632],{"id":38631},"what-is-a-cdn","What is a CDN?",[11,38634,3275,38635,179,38638,38641],{},[94,38636,38637],{},"CDN",[94,38639,38640],{},"Content Delivery Network"," is a network of geographically distributed servers that are used to serve content\u002Fdata to end users faster.",[11,38643,38644],{},"These servers sit between us, the client, and the origin server, and can keep a cache of our content for the configured time. Also, since they are distributed across the world, the data is served from the edge that is closest to us. And because of this proximity, the round trip delay is much lower compared to if the request had to be served from the origin server (or simply the origin).",[11,38646,38647],{},[94,38648,38649],{},"But how does the CDN get the data in the first place?",[11,38651,38652,38653,38658],{},"Initially, the CDN cache doesn't have any data, we call it a ",[3061,38654,38655],{},[94,38656,38657],{},"cold cache",". When a request is made, it is routed to the edge closest to you. The edge location checks its cache, which is empty, gets the data from the origin and returns it to you, simultaneously storing it in its cache (this is called warming the cache) for future requests.",[11,38660,38661],{},[94,38662,38663],{},"So one request is all it takes for the CDN to get the data?",[11,38665,38666,38667,38672,38673,179,38678,183],{},"Yes and no! The edge location closest to you gets the data when you make the first request. Now, any subsequent \"",[3061,38668,38669],{},[94,38670,38671],{},"similar","\" request to the same edge, either by you or someone else, is served from the cache. Since this cache has the relevant data for the request, it is called a ",[3061,38674,38675],{},[94,38676,38677],{},"warm",[3061,38679,38680],{},[94,38681,38682],{},"hot cache",[11,38684,38685],{},"If a request is made to some other edge location by someone else then that server might not have any data stored yet, so the same process of data fetching and storing gets repeated there.",[11,38687,38688],{},"As you might have guessed, in this article we will be looking at storing our API responses at the CDN for faster retrieval.",[24,38690,38692],{"id":38691},"configuring-api-caching-with-firebase-cdn","Configuring API Caching with Firebase CDN",[11,38694,38695,38696,183],{},"So how do we configure the firebase CDN? If you look at the firebase documentation, you won't find any separate subsection called CDN. Firebase CDN is covered under firebase hosting, you can read about it ",[47,38697,38700],{"href":38698,"rel":38699},"https:\u002F\u002Ffirebase.google.com\u002Fdocs\u002Fhosting#:~:text=deploy%20web%20apps%20and%20serve%20both%20static%20and%20dynamic%20content%20to%20a%20global%20CDN%20(content%20delivery%20network)",[51],"in the introduction here",[11,38702,38703],{},"To use the CDN for API caching we need to connect our firebase function(s)\u002Fcloud run to firebase hosting. But does that mean we need to host our website on firebase hosting? Not necessarily.",[32,38705,38707],{"id":38706},"setup-a-firebase-project","Setup a Firebase Project",[11,38709,38710],{},"If you don't have a firebase project already, create one using either the command line or the firebase console.",[11,38712,38713,38714,38717,38718,919,38721,183],{},"To show the difference between a direct firebase function call and one through the CDN we'll be creating a simple project. I simply executed the ",[59,38715,38716],{},"firebase init"," command inside an empty directory and followed the prompts (selected the default choice for each). In this project, I've enabled only ",[59,38719,38720],{},"hosting",[59,38722,38723],{},"functions",[11,38725,38726],{},"This is my current project directory. Everything has been generated by the init command",[11,38728,38729],{},[3135,38730],{"alt":38731,"src":38732},"current project directory structure","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002Fa9f96ac7-3f02-4cd0-9d38-1f4413114335-44fe93a869.png",[11,38734,38735,38736,38739,38740,38742],{},"Next, head to the ",[59,38737,38738],{},"index.js"," file within the ",[59,38741,38723],{}," folder. I've created two simple functions inside it as shown below. We'll be using one of the functions directly and the other one through the CDN. On function calls, we just respond with preconfigured messages",[269,38744,38746],{"className":24597,"code":38745,"language":24599,"meta":274,"style":274},"const functions = require('firebase-functions');\n\nexports.helloWorld = functions.https.onRequest((req, res) => {\n  functions.logger.info(`helloWorld! Hostname: ${req.hostname}`);\n  res.send({ message: 'Hello World!' });\n});\n\nexports.wonderfulWorld = functions.https.onRequest((req, res) => {\n  functions.logger.info(`wonderfulWorld! Hostname: ${req.hostname}`);\n  res.send({ message: 'What a Wonderful World!' });\n});\n",[59,38747,38748,38753,38757,38762,38767,38772,38776,38780,38785,38790,38795],{"__ignoreMap":274},[278,38749,38750],{"class":280,"line":281},[278,38751,38752],{},"const functions = require('firebase-functions');\n",[278,38754,38755],{"class":280,"line":288},[278,38756,292],{"emptyLinePlaceholder":291},[278,38758,38759],{"class":280,"line":295},[278,38760,38761],{},"exports.helloWorld = functions.https.onRequest((req, res) => {\n",[278,38763,38764],{"class":280,"line":316},[278,38765,38766],{},"  functions.logger.info(`helloWorld! Hostname: ${req.hostname}`);\n",[278,38768,38769],{"class":280,"line":322},[278,38770,38771],{},"  res.send({ message: 'Hello World!' });\n",[278,38773,38774],{"class":280,"line":327},[278,38775,3693],{},[278,38777,38778],{"class":280,"line":340},[278,38779,292],{"emptyLinePlaceholder":291},[278,38781,38782],{"class":280,"line":349},[278,38783,38784],{},"exports.wonderfulWorld = functions.https.onRequest((req, res) => {\n",[278,38786,38787],{"class":280,"line":375},[278,38788,38789],{},"  functions.logger.info(`wonderfulWorld! Hostname: ${req.hostname}`);\n",[278,38791,38792],{"class":280,"line":386},[278,38793,38794],{},"  res.send({ message: 'What a Wonderful World!' });\n",[278,38796,38797],{"class":280,"line":397},[278,38798,3693],{},[11,38800,38801,38802,38805,38806,38809,38810,38813,38814,38817],{},"If we try to call these functions from our ",[59,38803,38804],{},"index.html"," file we'll get ",[59,38807,38808],{},"CORS"," errors. So before deploying the functions let's install the ",[59,38811,38812],{},"cors"," package using the ",[59,38815,38816],{},"\"npm i cors\""," command and wrap the two functions' bodies inside it.",[269,38819,38821],{"className":24597,"code":38820,"language":24599,"meta":274,"style":274},"const functions = require('firebase-functions');\n\u002F\u002F Require cors. For testing we're allowing it for all origins\nconst cors = require('cors')({origin: true})\n\nexports.helloWorld = functions.https.onRequest((req, res) => {\n  functions.logger.info(`helloWorld! Hostname: ${req.hostname}`);\n  cors(req, res, () => {\n    res.send({ message: 'Hello World!' });\n  })\n});\n\nexports.wonderfulWorld = functions.https.onRequest((req, res) => {\n  functions.logger.info(`wonderfulWorld! Hostname: ${req.hostname}`);\n  cors(req, res, () => {\n    res.send({ message: 'What a Wonderful World!' });\n  })\n});\n",[59,38822,38823,38827,38832,38837,38841,38845,38849,38854,38859,38864,38868,38872,38876,38880,38884,38889,38893],{"__ignoreMap":274},[278,38824,38825],{"class":280,"line":281},[278,38826,38752],{},[278,38828,38829],{"class":280,"line":288},[278,38830,38831],{},"\u002F\u002F Require cors. For testing we're allowing it for all origins\n",[278,38833,38834],{"class":280,"line":295},[278,38835,38836],{},"const cors = require('cors')({origin: true})\n",[278,38838,38839],{"class":280,"line":316},[278,38840,292],{"emptyLinePlaceholder":291},[278,38842,38843],{"class":280,"line":322},[278,38844,38761],{},[278,38846,38847],{"class":280,"line":327},[278,38848,38766],{},[278,38850,38851],{"class":280,"line":340},[278,38852,38853],{},"  cors(req, res, () => {\n",[278,38855,38856],{"class":280,"line":349},[278,38857,38858],{},"    res.send({ message: 'Hello World!' });\n",[278,38860,38861],{"class":280,"line":375},[278,38862,38863],{},"  })\n",[278,38865,38866],{"class":280,"line":386},[278,38867,3693],{},[278,38869,38870],{"class":280,"line":397},[278,38871,292],{"emptyLinePlaceholder":291},[278,38873,38874],{"class":280,"line":408},[278,38875,38784],{},[278,38877,38878],{"class":280,"line":433},[278,38879,38789],{},[278,38881,38882],{"class":280,"line":454},[278,38883,38853],{},[278,38885,38886],{"class":280,"line":475},[278,38887,38888],{},"    res.send({ message: 'What a Wonderful World!' });\n",[278,38890,38891],{"class":280,"line":496},[278,38892,38863],{},[278,38894,38895],{"class":280,"line":505},[278,38896,3693],{},[11,38898,38899],{},"Now we're ready to test these functions. Deploy the functions using the firebase deploy command",[269,38901,38903],{"className":3335,"code":38902,"language":3337,"meta":274,"style":274},"firebase deploy --only functions\n",[59,38904,38905],{"__ignoreMap":274},[278,38906,38907,38909,38912,38915],{"class":280,"line":281},[278,38908,24417],{"class":333},[278,38910,38911],{"class":309}," deploy",[278,38913,38914],{"class":650}," --only",[278,38916,38917],{"class":309}," functions\n",[11,38919,38920,38921,38923,38924,38927],{},"Now head over to the ",[59,38922,38804],{}," file inside the ",[59,38925,38926],{},"public"," folder and replace its content with the below code. I've just modified the file generated by firebase and added the two function calls in it.",[11,38929,38930,11160,38933,11160,38936,11160,38939],{},[3061,38931,38932],{},"Don't forget to replace",[59,38934,38935],{},"\u003Cproject_id>",[3061,38937,38938],{},"with your actual project id. You may also need to change the function region if you deployed to a region other than",[3061,38940,38941],{},[94,38942,38943],{},"us-central1",[269,38945,38947],{"className":7132,"code":38946,"language":7134,"meta":274,"style":274},"\u003C!DOCTYPE html>\n\u003Chtml>\n  \u003Chead>\n    \u003Cmeta charset=\"utf-8\" \u002F>\n    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \u002F>\n    \u003Ctitle>Welcome to Firebase Hosting\u003C\u002Ftitle>\n\n    \u003Cstyle media=\"screen\">\n      body {\n        background: #eceff1;\n        color: rgba(0, 0, 0, 0.87);\n        font-family: Roboto, Helvetica, Arial, sans-serif;\n        margin: 0;\n        padding: 0;\n      }\n      #message {\n        background: white;\n        max-width: 360px;\n        margin: 100px auto 16px;\n        padding: 32px 24px;\n        border-radius: 3px;\n      }\n      #message h1 {\n        font-size: 32px;\n        color: #ffa100;\n        font-weight: bold;\n        margin: 0 0 16px;\n      }\n      #message p {\n        line-height: 140%;\n        font-size: 14px;\n      }\n      #message a {\n        display: block;\n        text-align: center;\n        background: #039be5;\n        text-transform: uppercase;\n        text-decoration: none;\n        color: white;\n        padding: 16px;\n        border-radius: 4px;\n        cursor: pointer;\n      }\n      #message a:hover {\n        background: #028bd5;\n      }\n      #message,\n      #message a {\n        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);\n      }\n      #cdn,\n      #direct {\n        background: #e1e1e1;\n        padding: 4px 8px;\n        border-radius: 4px;\n        color: black;\n      }\n      @media (max-width: 600px) {\n        body,\n        #message {\n          margin-top: 0;\n          background: white;\n          box-shadow: none;\n        }\n        body {\n          border-top: 16px solid #ffa100;\n        }\n      }\n    \u003C\u002Fstyle>\n  \u003C\u002Fhead>\n  \u003Cbody>\n    \u003Cdiv id=\"message\">\n      \u003Ch1>Welcome\u003C\u002Fh1>\n      \u003Cp>Click on the button below to make the API Calls...\u003C\u002Fp>\n      \u003Ca onclick=\"makeCalls()\">Test API Calls\u003C\u002Fa>\n      \u003Cp>Response through CDN\u003C\u002Fp>\n      \u003Cpre id=\"cdn\">Waiting for the click&hellip;\u003C\u002Fpre>\n      \u003Cp>Response through function\u003C\u002Fp>\n      \u003Cpre id=\"direct\">Waiting for the click&hellip;\u003C\u002Fpre>\n    \u003C\u002Fdiv>\n\n    \u003Cscript>\n      const directEl = document.getElementById('direct');\n      const cdnEl = document.getElementById('cdn');\n\n      async function makeCalls() {\n        directEl.textContent = 'Loading...';\n        cdnEl.textContent = 'Loading...';\n\n        const promises = [\n          fetch(\n            'https:\u002F\u002Fus-central1-\u003Cproject_id>.cloudfunctions.net\u002FhelloWorld'\n          ),\n          fetch(\n            'https:\u002F\u002Fus-central1-\u003Cproject_id>.cloudfunctions.net\u002FwonderfulWorld'\n          ),\n        ];\n\n        const [directCall, cdnCall] = await Promise.allSettled(promises);\n        if (directCall.status === 'fulfilled' && directCall.value.ok) {\n          const directCallData = await directCall.value.json();\n          console.log('directCallData', directCallData);\n\n          directEl.textContent = JSON.stringify(directCallData, null, 2);\n        }\n\n        if (cdnCall.status === 'fulfilled' && cdnCall.value.ok) {\n          const cdnCallData = await cdnCall.value.json();\n          console.log('cdnCallData', cdnCallData);\n\n          cdnEl.textContent = JSON.stringify(cdnCallData, null, 2);\n        }\n      }\n    \u003C\u002Fscript>\n  \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[59,38948,38949,38954,38959,38964,38969,38974,38979,38983,38988,38993,38998,39003,39008,39013,39018,39022,39027,39032,39037,39042,39047,39052,39056,39061,39066,39071,39076,39081,39085,39090,39095,39100,39104,39109,39114,39119,39124,39129,39134,39139,39144,39149,39154,39158,39163,39168,39172,39177,39181,39186,39190,39195,39200,39205,39210,39214,39219,39223,39228,39232,39237,39242,39247,39252,39256,39261,39266,39270,39274,39279,39284,39289,39294,39299,39304,39309,39314,39319,39324,39329,39333,39337,39342,39347,39352,39356,39361,39366,39371,39375,39380,39385,39390,39395,39399,39404,39408,39413,39417,39422,39427,39432,39437,39441,39446,39450,39454,39459,39464,39469,39473,39478,39482,39486,39491,39496],{"__ignoreMap":274},[278,38950,38951],{"class":280,"line":281},[278,38952,38953],{},"\u003C!DOCTYPE html>\n",[278,38955,38956],{"class":280,"line":288},[278,38957,38958],{},"\u003Chtml>\n",[278,38960,38961],{"class":280,"line":295},[278,38962,38963],{},"  \u003Chead>\n",[278,38965,38966],{"class":280,"line":316},[278,38967,38968],{},"    \u003Cmeta charset=\"utf-8\" \u002F>\n",[278,38970,38971],{"class":280,"line":322},[278,38972,38973],{},"    \u003Cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \u002F>\n",[278,38975,38976],{"class":280,"line":327},[278,38977,38978],{},"    \u003Ctitle>Welcome to Firebase Hosting\u003C\u002Ftitle>\n",[278,38980,38981],{"class":280,"line":340},[278,38982,292],{"emptyLinePlaceholder":291},[278,38984,38985],{"class":280,"line":349},[278,38986,38987],{},"    \u003Cstyle media=\"screen\">\n",[278,38989,38990],{"class":280,"line":375},[278,38991,38992],{},"      body {\n",[278,38994,38995],{"class":280,"line":386},[278,38996,38997],{},"        background: #eceff1;\n",[278,38999,39000],{"class":280,"line":397},[278,39001,39002],{},"        color: rgba(0, 0, 0, 0.87);\n",[278,39004,39005],{"class":280,"line":408},[278,39006,39007],{},"        font-family: Roboto, Helvetica, Arial, sans-serif;\n",[278,39009,39010],{"class":280,"line":433},[278,39011,39012],{},"        margin: 0;\n",[278,39014,39015],{"class":280,"line":454},[278,39016,39017],{},"        padding: 0;\n",[278,39019,39020],{"class":280,"line":475},[278,39021,6234],{},[278,39023,39024],{"class":280,"line":496},[278,39025,39026],{},"      #message {\n",[278,39028,39029],{"class":280,"line":505},[278,39030,39031],{},"        background: white;\n",[278,39033,39034],{"class":280,"line":516},[278,39035,39036],{},"        max-width: 360px;\n",[278,39038,39039],{"class":280,"line":527},[278,39040,39041],{},"        margin: 100px auto 16px;\n",[278,39043,39044],{"class":280,"line":533},[278,39045,39046],{},"        padding: 32px 24px;\n",[278,39048,39049],{"class":280,"line":539},[278,39050,39051],{},"        border-radius: 3px;\n",[278,39053,39054],{"class":280,"line":545},[278,39055,6234],{},[278,39057,39058],{"class":280,"line":551},[278,39059,39060],{},"      #message h1 {\n",[278,39062,39063],{"class":280,"line":557},[278,39064,39065],{},"        font-size: 32px;\n",[278,39067,39068],{"class":280,"line":567},[278,39069,39070],{},"        color: #ffa100;\n",[278,39072,39073],{"class":280,"line":577},[278,39074,39075],{},"        font-weight: bold;\n",[278,39077,39078],{"class":280,"line":587},[278,39079,39080],{},"        margin: 0 0 16px;\n",[278,39082,39083],{"class":280,"line":597},[278,39084,6234],{},[278,39086,39087],{"class":280,"line":608},[278,39088,39089],{},"      #message p {\n",[278,39091,39092],{"class":280,"line":614},[278,39093,39094],{},"        line-height: 140%;\n",[278,39096,39097],{"class":280,"line":620},[278,39098,39099],{},"        font-size: 14px;\n",[278,39101,39102],{"class":280,"line":625},[278,39103,6234],{},[278,39105,39106],{"class":280,"line":640},[278,39107,39108],{},"      #message a {\n",[278,39110,39111],{"class":280,"line":663},[278,39112,39113],{},"        display: block;\n",[278,39115,39116],{"class":280,"line":669},[278,39117,39118],{},"        text-align: center;\n",[278,39120,39121],{"class":280,"line":680},[278,39122,39123],{},"        background: #039be5;\n",[278,39125,39126],{"class":280,"line":686},[278,39127,39128],{},"        text-transform: uppercase;\n",[278,39130,39131],{"class":280,"line":1334},[278,39132,39133],{},"        text-decoration: none;\n",[278,39135,39136],{"class":280,"line":1375},[278,39137,39138],{},"        color: white;\n",[278,39140,39141],{"class":280,"line":1381},[278,39142,39143],{},"        padding: 16px;\n",[278,39145,39146],{"class":280,"line":1386},[278,39147,39148],{},"        border-radius: 4px;\n",[278,39150,39151],{"class":280,"line":1394},[278,39152,39153],{},"        cursor: pointer;\n",[278,39155,39156],{"class":280,"line":1406},[278,39157,6234],{},[278,39159,39160],{"class":280,"line":1423},[278,39161,39162],{},"      #message a:hover {\n",[278,39164,39165],{"class":280,"line":1432},[278,39166,39167],{},"        background: #028bd5;\n",[278,39169,39170],{"class":280,"line":1437},[278,39171,6234],{},[278,39173,39174],{"class":280,"line":1916},[278,39175,39176],{},"      #message,\n",[278,39178,39179],{"class":280,"line":1939},[278,39180,39108],{},[278,39182,39183],{"class":280,"line":1949},[278,39184,39185],{},"        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);\n",[278,39187,39188],{"class":280,"line":1954},[278,39189,6234],{},[278,39191,39192],{"class":280,"line":1959},[278,39193,39194],{},"      #cdn,\n",[278,39196,39197],{"class":280,"line":1985},[278,39198,39199],{},"      #direct {\n",[278,39201,39202],{"class":280,"line":1990},[278,39203,39204],{},"        background: #e1e1e1;\n",[278,39206,39207],{"class":280,"line":1997},[278,39208,39209],{},"        padding: 4px 8px;\n",[278,39211,39212],{"class":280,"line":2006},[278,39213,39148],{},[278,39215,39216],{"class":280,"line":2018},[278,39217,39218],{},"        color: black;\n",[278,39220,39221],{"class":280,"line":2029},[278,39222,6234],{},[278,39224,39225],{"class":280,"line":2034},[278,39226,39227],{},"      @media (max-width: 600px) {\n",[278,39229,39230],{"class":280,"line":2040},[278,39231,15280],{},[278,39233,39234],{"class":280,"line":2045},[278,39235,39236],{},"        #message {\n",[278,39238,39239],{"class":280,"line":2068},[278,39240,39241],{},"          margin-top: 0;\n",[278,39243,39244],{"class":280,"line":2099},[278,39245,39246],{},"          background: white;\n",[278,39248,39249],{"class":280,"line":6428},[278,39250,39251],{},"          box-shadow: none;\n",[278,39253,39254],{"class":280,"line":6439},[278,39255,6954],{},[278,39257,39258],{"class":280,"line":6450},[278,39259,39260],{},"        body {\n",[278,39262,39263],{"class":280,"line":6455},[278,39264,39265],{},"          border-top: 16px solid #ffa100;\n",[278,39267,39268],{"class":280,"line":6460},[278,39269,6954],{},[278,39271,39272],{"class":280,"line":6475},[278,39273,6234],{},[278,39275,39276],{"class":280,"line":6486},[278,39277,39278],{},"    \u003C\u002Fstyle>\n",[278,39280,39281],{"class":280,"line":6491},[278,39282,39283],{},"  \u003C\u002Fhead>\n",[278,39285,39286],{"class":280,"line":6518},[278,39287,39288],{},"  \u003Cbody>\n",[278,39290,39291],{"class":280,"line":6530},[278,39292,39293],{},"    \u003Cdiv id=\"message\">\n",[278,39295,39296],{"class":280,"line":6542},[278,39297,39298],{},"      \u003Ch1>Welcome\u003C\u002Fh1>\n",[278,39300,39301],{"class":280,"line":6547},[278,39302,39303],{},"      \u003Cp>Click on the button below to make the API Calls...\u003C\u002Fp>\n",[278,39305,39306],{"class":280,"line":6552},[278,39307,39308],{},"      \u003Ca onclick=\"makeCalls()\">Test API Calls\u003C\u002Fa>\n",[278,39310,39311],{"class":280,"line":6567},[278,39312,39313],{},"      \u003Cp>Response through CDN\u003C\u002Fp>\n",[278,39315,39316],{"class":280,"line":6580},[278,39317,39318],{},"      \u003Cpre id=\"cdn\">Waiting for the click&hellip;\u003C\u002Fpre>\n",[278,39320,39321],{"class":280,"line":6593},[278,39322,39323],{},"      \u003Cp>Response through function\u003C\u002Fp>\n",[278,39325,39326],{"class":280,"line":6605},[278,39327,39328],{},"      \u003Cpre id=\"direct\">Waiting for the click&hellip;\u003C\u002Fpre>\n",[278,39330,39331],{"class":280,"line":6620},[278,39332,7950],{},[278,39334,39335],{"class":280,"line":6625},[278,39336,292],{"emptyLinePlaceholder":291},[278,39338,39339],{"class":280,"line":6633},[278,39340,39341],{},"    \u003Cscript>\n",[278,39343,39344],{"class":280,"line":6643},[278,39345,39346],{},"      const directEl = document.getElementById('direct');\n",[278,39348,39349],{"class":280,"line":6657},[278,39350,39351],{},"      const cdnEl = document.getElementById('cdn');\n",[278,39353,39354],{"class":280,"line":6665},[278,39355,292],{"emptyLinePlaceholder":291},[278,39357,39358],{"class":280,"line":6670},[278,39359,39360],{},"      async function makeCalls() {\n",[278,39362,39363],{"class":280,"line":6675},[278,39364,39365],{},"        directEl.textContent = 'Loading...';\n",[278,39367,39368],{"class":280,"line":6680},[278,39369,39370],{},"        cdnEl.textContent = 'Loading...';\n",[278,39372,39373],{"class":280,"line":6698},[278,39374,292],{"emptyLinePlaceholder":291},[278,39376,39377],{"class":280,"line":6725},[278,39378,39379],{},"        const promises = [\n",[278,39381,39382],{"class":280,"line":6738},[278,39383,39384],{},"          fetch(\n",[278,39386,39387],{"class":280,"line":6752},[278,39388,39389],{},"            'https:\u002F\u002Fus-central1-\u003Cproject_id>.cloudfunctions.net\u002FhelloWorld'\n",[278,39391,39392],{"class":280,"line":6769},[278,39393,39394],{},"          ),\n",[278,39396,39397],{"class":280,"line":6786},[278,39398,39384],{},[278,39400,39401],{"class":280,"line":6798},[278,39402,39403],{},"            'https:\u002F\u002Fus-central1-\u003Cproject_id>.cloudfunctions.net\u002FwonderfulWorld'\n",[278,39405,39406],{"class":280,"line":6803},[278,39407,39394],{},[278,39409,39410],{"class":280,"line":6815},[278,39411,39412],{},"        ];\n",[278,39414,39415],{"class":280,"line":6827},[278,39416,292],{"emptyLinePlaceholder":291},[278,39418,39419],{"class":280,"line":6839},[278,39420,39421],{},"        const [directCall, cdnCall] = await Promise.allSettled(promises);\n",[278,39423,39424],{"class":280,"line":6844},[278,39425,39426],{},"        if (directCall.status === 'fulfilled' && directCall.value.ok) {\n",[278,39428,39429],{"class":280,"line":6853},[278,39430,39431],{},"          const directCallData = await directCall.value.json();\n",[278,39433,39434],{"class":280,"line":6859},[278,39435,39436],{},"          console.log('directCallData', directCallData);\n",[278,39438,39439],{"class":280,"line":6864},[278,39440,292],{"emptyLinePlaceholder":291},[278,39442,39443],{"class":280,"line":6877},[278,39444,39445],{},"          directEl.textContent = JSON.stringify(directCallData, null, 2);\n",[278,39447,39448],{"class":280,"line":6887},[278,39449,6954],{},[278,39451,39452],{"class":280,"line":6918},[278,39453,292],{"emptyLinePlaceholder":291},[278,39455,39456],{"class":280,"line":6923},[278,39457,39458],{},"        if (cdnCall.status === 'fulfilled' && cdnCall.value.ok) {\n",[278,39460,39461],{"class":280,"line":6931},[278,39462,39463],{},"          const cdnCallData = await cdnCall.value.json();\n",[278,39465,39466],{"class":280,"line":6939},[278,39467,39468],{},"          console.log('cdnCallData', cdnCallData);\n",[278,39470,39471],{"class":280,"line":6951},[278,39472,292],{"emptyLinePlaceholder":291},[278,39474,39475],{"class":280,"line":6957},[278,39476,39477],{},"          cdnEl.textContent = JSON.stringify(cdnCallData, null, 2);\n",[278,39479,39480],{"class":280,"line":6962},[278,39481,6954],{},[278,39483,39484],{"class":280,"line":6973},[278,39485,6234],{},[278,39487,39488],{"class":280,"line":6985},[278,39489,39490],{},"    \u003C\u002Fscript>\n",[278,39492,39493],{"class":280,"line":6990},[278,39494,39495],{},"  \u003C\u002Fbody>\n",[278,39497,39498],{"class":280,"line":6995},[278,39499,39500],{},"\u003C\u002Fhtml>\n",[11,39502,39503],{},"If you load this html file in your browser and click the button you should be able to see the configured messages. But the first output label is misleading, as we haven't configured any CDN yet. Let's change that in the next section.",[11,39505,39506],{},[3135,39507],{"alt":39508,"src":39509},"api call output","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F733b743d-7c70-4be1-9c95-1fdb7a0238ab-ef3f895698.png",[11,39511,39512],{},"Below is a screenshot of my Chrome dev console's network tab. As you can see both requests are taking similar times at this point",[11,39514,39515],{},[3135,39516],{"alt":39517,"src":39518},"api network calls in the browser dev console","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F45f97f97-ad10-444d-92aa-a499ef7cfe64-81e487e580.png",[32,39520,39522],{"id":39521},"connecting-firebase-function-cdn","Connecting Firebase function & CDN",[11,39524,39525,39526,39529],{},"To connect one of the functions to the CDN, let's head over to the ",[59,39527,39528],{},"\"firebase.json\""," file at the root of the project, and make the below changes",[269,39531,39533],{"className":5690,"code":39532,"language":1310,"meta":274,"style":274},"\u002F\u002F Modify the hosting attribute of your \"firebase.json\"\n\"hosting\": {\n  \u002F\u002F...other hosting settings\n\n  \u002F\u002F Add the \"rewrites\" attribute within \"hosting\"\n  \"rewrites\": [\n    {\n      \"source\": \"\u002Fapi\u002Fwonderful\", \u002F\u002F Your api route\n      \"function\": \"wonderfulWorld\", \u002F\u002F Your function name\n      \"region\": \"us-central1\" \u002F\u002F The region where the function is deployed\n    }\n  ]\n}\n",[59,39534,39535,39540,39547,39552,39556,39561,39568,39572,39587,39602,39615,39619,39624],{"__ignoreMap":274},[278,39536,39537],{"class":280,"line":281},[278,39538,39539],{"class":284},"\u002F\u002F Modify the hosting attribute of your \"firebase.json\"\n",[278,39541,39542,39545],{"class":280,"line":288},[278,39543,39544],{"class":309},"\"hosting\"",[278,39546,5706],{"class":302},[278,39548,39549],{"class":280,"line":295},[278,39550,39551],{"class":284},"  \u002F\u002F...other hosting settings\n",[278,39553,39554],{"class":280,"line":316},[278,39555,292],{"emptyLinePlaceholder":291},[278,39557,39558],{"class":280,"line":322},[278,39559,39560],{"class":284},"  \u002F\u002F Add the \"rewrites\" attribute within \"hosting\"\n",[278,39562,39563,39566],{"class":280,"line":327},[278,39564,39565],{"class":650},"  \"rewrites\"",[278,39567,32193],{"class":302},[278,39569,39570],{"class":280,"line":340},[278,39571,2209],{"class":302},[278,39573,39574,39577,39579,39582,39584],{"class":280,"line":349},[278,39575,39576],{"class":650},"      \"source\"",[278,39578,1155],{"class":302},[278,39580,39581],{"class":309},"\"\u002Fapi\u002Fwonderful\"",[278,39583,1708],{"class":302},[278,39585,39586],{"class":284},"\u002F\u002F Your api route\n",[278,39588,39589,39592,39594,39597,39599],{"class":280,"line":375},[278,39590,39591],{"class":650},"      \"function\"",[278,39593,1155],{"class":302},[278,39595,39596],{"class":309},"\"wonderfulWorld\"",[278,39598,1708],{"class":302},[278,39600,39601],{"class":284},"\u002F\u002F Your function name\n",[278,39603,39604,39607,39609,39612],{"class":280,"line":386},[278,39605,39606],{"class":650},"      \"region\"",[278,39608,1155],{"class":302},[278,39610,39611],{"class":309},"\"us-central1\"",[278,39613,39614],{"class":284}," \u002F\u002F The region where the function is deployed\n",[278,39616,39617],{"class":280,"line":397},[278,39618,1285],{"class":302},[278,39620,39621],{"class":280,"line":408},[278,39622,39623],{"class":302},"  ]\n",[278,39625,39626],{"class":280,"line":433},[278,39627,617],{"class":302},[11,39629,39630],{},"If you want to use Google Cloud Run instead of firebase cloud functions, you can connect the two using the below rewrite",[269,39632,39634],{"className":5690,"code":39633,"language":1310,"meta":274,"style":274},"\"hosting\": {\n  \"rewrites\": [\n    {\n      \"source\": \"\u002Fapi\u002Fwonderful\",\n      \"run\": {\n        \"serviceId\": \"\u003Ccloud_run_service_id>\",\n        \"region\": \"us-central1\" \u002F\u002F The region where cloud run is deployed\n      }\n    }\n  ]\n}\n",[59,39635,39636,39642,39648,39652,39662,39669,39681,39693,39697,39701,39705],{"__ignoreMap":274},[278,39637,39638,39640],{"class":280,"line":281},[278,39639,39544],{"class":309},[278,39641,5706],{"class":302},[278,39643,39644,39646],{"class":280,"line":288},[278,39645,39565],{"class":650},[278,39647,32193],{"class":302},[278,39649,39650],{"class":280,"line":295},[278,39651,2209],{"class":302},[278,39653,39654,39656,39658,39660],{"class":280,"line":316},[278,39655,39576],{"class":650},[278,39657,1155],{"class":302},[278,39659,39581],{"class":309},[278,39661,660],{"class":302},[278,39663,39664,39667],{"class":280,"line":322},[278,39665,39666],{"class":650},"      \"run\"",[278,39668,5706],{"class":302},[278,39670,39671,39674,39676,39679],{"class":280,"line":327},[278,39672,39673],{"class":650},"        \"serviceId\"",[278,39675,1155],{"class":302},[278,39677,39678],{"class":309},"\"\u003Ccloud_run_service_id>\"",[278,39680,660],{"class":302},[278,39682,39683,39686,39688,39690],{"class":280,"line":340},[278,39684,39685],{"class":650},"        \"region\"",[278,39687,1155],{"class":302},[278,39689,39611],{"class":309},[278,39691,39692],{"class":284}," \u002F\u002F The region where cloud run is deployed\n",[278,39694,39695],{"class":280,"line":349},[278,39696,6234],{"class":302},[278,39698,39699],{"class":280,"line":375},[278,39700,1285],{"class":302},[278,39702,39703],{"class":280,"line":386},[278,39704,39623],{"class":302},[278,39706,39707],{"class":280,"line":397},[278,39708,617],{"class":302},[11,39710,39711,39712,39714,39715,39717,39718,39720],{},"Next, head over to the ",[59,39713,38804],{}," file and replace the ",[59,39716,39596],{}," function URL with the below URL. Replace ",[59,39719,38935],{}," with your actual project id.",[269,39722,39724],{"className":24597,"code":39723,"language":24599,"meta":274,"style":274},"const promises = [\n  fetch(\n    'https:\u002F\u002Fus-central1-\u003Cproject_id>.cloudfunctions.net\u002FhelloWorld'\n  ),\n  fetch('https:\u002F\u002F\u003Cproject_id>.web.app\u002Fapi\u002Fwonderful'),\n];\n",[59,39725,39726,39731,39736,39741,39746,39751],{"__ignoreMap":274},[278,39727,39728],{"class":280,"line":281},[278,39729,39730],{},"const promises = [\n",[278,39732,39733],{"class":280,"line":288},[278,39734,39735],{},"  fetch(\n",[278,39737,39738],{"class":280,"line":295},[278,39739,39740],{},"    'https:\u002F\u002Fus-central1-\u003Cproject_id>.cloudfunctions.net\u002FhelloWorld'\n",[278,39742,39743],{"class":280,"line":316},[278,39744,39745],{},"  ),\n",[278,39747,39748],{"class":280,"line":322},[278,39749,39750],{},"  fetch('https:\u002F\u002F\u003Cproject_id>.web.app\u002Fapi\u002Fwonderful'),\n",[278,39752,39753],{"class":280,"line":327},[278,39754,11714],{},[11,39756,39757,39758,39761],{},"After making this change we need to deploy our changes to firebase hosting. We can simply execute the ",[59,39759,39760],{},"\"firebase deploy\" or \"firebase deploy --only hosting\""," command to do it.",[11,39763,39764,39765,39767,39768,39771],{},"Now, if we reload our local ",[59,39766,38804],{}," file, or, head over to the URL ",[59,39769,39770],{},"https:\u002F\u002F\u003Cproject_id>.web.app"," and click on the button a couple of times, we should see results similar to as shown below.",[11,39773,39774],{},[3135,39775],{"alt":39776,"src":39777},"API calls, direct and through the CDN","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F30d41b52-2850-4bae-ba3b-37d649d6775c-d953ea16a0.png",[11,39779,39780,39781,39784,39785,39788,39789,39792],{},"Do note that the response size for the ",[59,39782,39783],{},"wonderful API call"," has increased from the earlier value of ",[59,39786,39787],{},"13 Bytes",". Also, as you can see, the first calls for both functions take significantly more time which is because of the cold start of the cloud function. Afterwards, the direct function call is consistently faster compared to the ",[59,39790,39791],{},"wonderful"," call routed through the CDN. This is because we've added one extra step to the trip and haven't added any cache yet.",[11,39794,39795],{},"You can click on these calls to see their details (specifically the Response headers)",[11,39797,39798],{},[94,39799,39800],{},"The helloWorld call",[11,39802,39803],{},[3135,39804],{"alt":39805,"src":39806},"helloWorld function call","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002Fd9ff0a32-ab48-4570-b6dc-a354d899f704-976e6ada88.png",[11,39808,39809],{},[94,39810,39811],{},"The wonderful API call",[11,39813,39814],{},[3135,39815],{"alt":39816,"src":39817},"wonderful api call","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F766cf201-f347-4300-b93d-87b84538db71-f7a394d263.png",[11,39819,39820],{},"As you can see, there are many extra headers present in this call compared to the previous one. Some of the interesting headers we can notice",[71,39822,39823,39829,39835,39845,39853],{},[74,39824,39825,39828],{},[59,39826,39827],{},"x-served-by",": it mentions a cache (of course it is empty at this point)",[74,39830,39831,39834],{},[59,39832,39833],{},"vary",": This contains more header names as compared to the direct function call",[74,39836,39837,39840,39841,39844],{},[59,39838,39839],{},"x-cache",": With value ",[59,39842,39843],{},"MISS",", which is correct because there is nothing in the cache so it missed serving from the cache",[74,39846,39847,39840,39850,39852],{},[59,39848,39849],{},"x-cache-hits",[59,39851,2012],{},". If it serves from the cache then this value would be a positive integer",[74,39854,39855,39858,39859,39862,39863,39865],{},[59,39856,39857],{},"cache-control",": With a value ",[59,39860,39861],{},"private"," for both calls. This is the header we'll be modifying for enabling cache. ",[59,39864,39861],{}," means that the data is private and can only be stored in a private cache, say your browser.",[32,39867,39869],{"id":39868},"configuring-cache-control-header","Configuring cache-control header",[11,39871,39872,39873,38739,39875,39877,39878,15633],{},"Head over to the ",[59,39874,38738],{},[59,39876,38723],{}," folder, and add the following line to the two functions just before the ",[59,39879,39880],{},"\"res.send\"",[269,39882,39884],{"className":24597,"code":39883,"language":24599,"meta":274,"style":274},"res.setHeader('cache-control', 'public, max-age=30, s-maxage=90');\n",[59,39885,39886],{"__ignoreMap":274},[278,39887,39888],{"class":280,"line":281},[278,39889,39883],{},[11,39891,39892,39893],{},"Deploy your functions changes by executing ",[59,39894,39895],{},"\"firebase deploy --only functions\".",[11,39897,39898],{},"What we're doing here is:",[71,39900,39901,39910,39920],{},[74,39902,39903,39904,39906,39907,39909],{},"Setting the ",[59,39905,39857],{}," header to ",[59,39908,38926],{},". This allows the CDN to cache the data",[74,39911,39912,39913,39916,39917,39919],{},"Setting ",[59,39914,39915],{},"max-age"," to ",[59,39918,30485],{},". This means that the browser can store this response, and it will remain fresh and valid for 30 seconds from the time it was generated.",[74,39921,39912,39922,39916,39925,39928],{},[59,39923,39924],{},"s-maxage",[59,39926,39927],{},"90",". This directive is similar to the max-age directive but applies only to the shared caches (the CDNs and proxies in between the origin and the client). So we're storing the response for a longer duration at the CDN. This may or may not be the case always and it can be removed if not needed.",[11,39930,39931],{},"So essentially what we've done is allow caching at the client as well as the intermediate steps in the journey. And also configured the time for which we can go on without asking the origin for fresh data.",[11,39933,39934],{},"Below is what I see in the network tab of my browser's dev console after making some requests",[11,39936,39937],{},[3135,39938],{"alt":39939,"src":39940},"API calls with cache control header","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002Fbe32d6dc-fff3-452a-9dbb-986076e7408a-e00bc3a0f3.png",[11,39942,39943,39944,39947],{},"As you can see, the first requests for both functions are taking their usual timings. But the second requests which were made after ~10 seconds take only ",[59,39945,39946],{},"4ms",". And we also see that it has been served from the disk cache. That means the responses were cached locally and no new network request was made.",[11,39949,39950,39951,39953,39954,39956,39957,39959],{},"Below are the response headers for the first two ",[59,39952,39791],{}," calls. Notice the ",[59,39955,39839],{}," header with a value of ",[59,39958,39843],{},". If you check your dev console, you'll also see that for the second call, no request headers are present. This is because no network request was made for it.",[11,39961,39962],{},[3135,39963],{"alt":39964,"src":39965},"wonderful api call 1","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F82e111b3-8913-42c0-bb3f-cee6e633fe3e-91fd9c7bee.png",[11,39967,39968,39969,39972,39973,39975,39976,39979,39980,39982],{},"What is interesting is the third batch of requests. The helloWorld function call takes its usual ",[59,39970,39971],{},"~400-500ms"," range (because a fresh request was made to the firebase function) but the ",[59,39974,39791],{}," call takes only ",[59,39977,39978],{},"69ms",". Further clicking on the ",[59,39981,39791],{}," request gives me the below details",[11,39984,39985],{},[3135,39986],{"alt":39987,"src":39988},"wonderful api call 3","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F2209a84f-f39a-4ce7-b2b2-d7d3e57ab543-ebf01da988.png",[11,39990,39991,39992,39994,39995,39998,39999,40001,40002,40004],{},"We see that the ",[59,39993,39839],{}," has a value of ",[59,39996,39997],{},"HIT"," now and ",[59,40000,39849],{}," is ",[59,40003,17444],{},". This request was served from the cache, and our firebase function was not called. We can also verify this by checking the function logs in the Google Cloud Console.",[11,40006,40007],{},"What we've achieved here is:",[123,40009,40010,40013,40016],{},[74,40011,40012],{},"significantly lesser response time",[74,40014,40015],{},"a lesser number of firebase functions invocations",[74,40017,40018],{},"potentially lesser number of database calls (generally you would get the data from a database and not just return a static value which we're doing currently)",[24,40020,40022],{"id":40021},"when-and-where-to-cache","When and where to cache",[11,40024,40025],{},"When deciding to use a cache, the first question you should ask yourself is, \"When and where (locally, CDN etc.) to do it\"? There is no silver bullet here, and every case needs to be analyzed based on its pros and cons. A wrong decision may return stale and irrelevant data to your users.",[11,40027,40028],{},"In general, if the same data is needed for a lot of users then it is a good candidate to be cached at the CDN. For example, weather data, some meta information, a blog post etc. The time for which to store the data depends on the case at hand.",[11,40030,40031,40032,40034,40035,40037],{},"User profile data doesn't change that frequently, but it is useful for only one person so it can be stored locally (using ",[59,40033,39861],{}," for ",[59,40036,39857],{},") for a short duration. And so on.",[24,40039,40041],{"id":40040},"things-to-keep-in-mind-with-firebase-cdn","Things to Keep in Mind with Firebase CDN",[11,40043,40044],{},"If you're convinced about using a cache for your API, below are some things that you should keep in mind",[71,40046,40047,40050,40053,40085,40091],{},[74,40048,40049],{},"In general, a firebase function can be configured for up to 9 minutes of request timeout. But when you connect your functions to firebase hosting, the request timeout value for these functions gets capped at 60 seconds.",[74,40051,40052],{},"Only GET & HEAD requests can be cached at the CDN",[74,40054,40055,40056],{},"For Firebase the Cache keys' generation depends on the following factors. If any of these factors are different a different key gets generated and hence cache hit or miss happens accordingly",[71,40057,40058,40065,40071,40074],{},[74,40059,40060,40061,40064],{},"The hostname (",[59,40062,40063],{},"\u003Cproject_id>.web.app"," in the example project of this article)",[74,40066,40067,40068,17418],{},"The path (",[59,40069,40070],{},"\u002Fapi\u002Fwonderful",[74,40072,40073],{},"The query string (we didn't use any query string)",[74,40075,40076,40077,40080,40081,40084],{},"The content of the request headers specified in the ",[59,40078,40079],{},"Vary"," header (by default Firebase CDN uses Origin, cookie, need-authorization, x-fh-requested-host and accept-encoding as can be seen from the screenshots above). Only the ",[94,40082,40083],{},"__session"," cookie (if present) is made part of the cache key.",[74,40086,40087,40088,17418],{},"Sometimes you need to remove the API data cached at the CDN. This can be done by redeploying to firebase hosting (using ",[59,40089,40090],{},"firebase deploy --only hosting",[74,40092,40093,40094,40096,40097,40100],{},"Instead of setting the ",[59,40095,39857],{}," header individually within each function, we can add it to ",[59,40098,40099],{},"firebase.json"," itself. Though for more control over individual requests caching, adding the header within the function is better.",[24,40102,23832],{"id":23831},[11,40104,40105,40106,183],{},"While Firebase CDN is good for general use cases, if you want more from your CDN then you should look for a proper CDN service like Cloudflare, Google and so on. Firebase CDN doesn't provide DDoS protection, Rate Limiting etc out of the box. Also, data transfer out of hosting is free till 360MB\u002Fday, beyond that you get charged ",[47,40107,40110],{"href":40108,"rel":40109},"https:\u002F\u002Ffirebase.google.com\u002Fpricing",[51],"$0.15\u002FGB",[11,40112,40113],{},[3135,40114],{"alt":40115,"src":40116},"firebase hosting pricing","\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002F7e81492b-30fb-4de2-810d-69ec7822154a-bf00769150.png",[24,40118,40120],{"id":40119},"real-live-caching-example","Real Live Caching Example",[11,40122,40123,40124,40127],{},"For one of my recent projects, I applied the Firebase CDN cache for one of the endpoints. The project is a daily puzzle game based on React SPA, called ",[47,40125,37459],{"href":37457,"rel":40126},[51]," where every player gets the same puzzle which gets refreshed at midnight GMT.",[11,40129,40130],{},"This is how I've configured the cache. The request gets stored at the CDN for the maximum time possible (including a buffer of 5 mins).",[269,40132,40134],{"className":24597,"code":40133,"language":24599,"meta":274,"style":274},"let cacheTime = 300; \u002F\u002F 5 mins local cache time\nlet serverCacheTime;\n\n\u002F\u002F 1. game is the current puzzle object\n\u002F\u002F 2. nextGameAt is the dateTime when the new puzzle will be available\nconst nextGameAtInMs = new Date(game.nextGameAt).getTime();\n\n\u002F\u002F Server Cache time, minus the local cache time (some buffer)\nserverCacheTime = parseInt((nextGameAtInMs - Date.now()) \u002F 1000) - cacheTime;\nif (serverCacheTime \u003C 0) {\n  serverCacheTime = 0;\n}\n\nif (serverCacheTime \u003C cacheTime) {\n  cacheTime = serverCacheTime;\n}\n\nresponse.set('Cache-Control', `public, max-age=${cacheTime}, s-maxage=${serverCacheTime}`\n);\n",[59,40135,40136,40141,40146,40150,40155,40160,40165,40169,40174,40179,40184,40189,40193,40197,40202,40207,40211,40215,40220],{"__ignoreMap":274},[278,40137,40138],{"class":280,"line":281},[278,40139,40140],{},"let cacheTime = 300; \u002F\u002F 5 mins local cache time\n",[278,40142,40143],{"class":280,"line":288},[278,40144,40145],{},"let serverCacheTime;\n",[278,40147,40148],{"class":280,"line":295},[278,40149,292],{"emptyLinePlaceholder":291},[278,40151,40152],{"class":280,"line":316},[278,40153,40154],{},"\u002F\u002F 1. game is the current puzzle object\n",[278,40156,40157],{"class":280,"line":322},[278,40158,40159],{},"\u002F\u002F 2. nextGameAt is the dateTime when the new puzzle will be available\n",[278,40161,40162],{"class":280,"line":327},[278,40163,40164],{},"const nextGameAtInMs = new Date(game.nextGameAt).getTime();\n",[278,40166,40167],{"class":280,"line":340},[278,40168,292],{"emptyLinePlaceholder":291},[278,40170,40171],{"class":280,"line":349},[278,40172,40173],{},"\u002F\u002F Server Cache time, minus the local cache time (some buffer)\n",[278,40175,40176],{"class":280,"line":375},[278,40177,40178],{},"serverCacheTime = parseInt((nextGameAtInMs - Date.now()) \u002F 1000) - cacheTime;\n",[278,40180,40181],{"class":280,"line":386},[278,40182,40183],{},"if (serverCacheTime \u003C 0) {\n",[278,40185,40186],{"class":280,"line":397},[278,40187,40188],{},"  serverCacheTime = 0;\n",[278,40190,40191],{"class":280,"line":408},[278,40192,617],{},[278,40194,40195],{"class":280,"line":433},[278,40196,292],{"emptyLinePlaceholder":291},[278,40198,40199],{"class":280,"line":454},[278,40200,40201],{},"if (serverCacheTime \u003C cacheTime) {\n",[278,40203,40204],{"class":280,"line":475},[278,40205,40206],{},"  cacheTime = serverCacheTime;\n",[278,40208,40209],{"class":280,"line":496},[278,40210,617],{},[278,40212,40213],{"class":280,"line":505},[278,40214,292],{"emptyLinePlaceholder":291},[278,40216,40217],{"class":280,"line":516},[278,40218,40219],{},"response.set('Cache-Control', `public, max-age=${cacheTime}, s-maxage=${serverCacheTime}`\n",[278,40221,40222],{"class":280,"line":527},[278,40223,1280],{},[11,40225,40226],{},"Many other directives can be used in the cache-control header. You should read more about these directives for advanced use cases.",[24,40228,10634],{"id":10633},[11,40230,40231],{},"In conclusion, using the firebase CDN can greatly enhance your API performance by caching frequently accessed data closer to the end user, thus reducing latency and improving response times. It also reduces firebase functions invocations, as well as database calls, thus reducing cost.",[11,40233,40234],{},"But before embarking on this journey, do analyze the pros and cons of adding a cache for your use case.",[11,40236,40237],{},"Hope you enjoyed reading the article. If you found any mistake in the article please let me know in the comments.",[11,40239,38568],{},[24,40241,40243],{"id":40242},"further-reading","Further reading",[11,40245,40246,40247],{},"We've only looked at a couple of directives for the cache-control header, for a ",[47,40248,40251],{"href":40249,"rel":40250},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FHeaders\u002FCache-Control",[51],"complete overview please visit the MDN site",[11,40253,40254],{},"For understanding caching in detail you can visit the following links",[11,40256,40257],{},[47,40258,40261],{"href":40259,"rel":40260},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FCaching",[51],"HTTP caching on MDN",[11,40263,40264],{},[47,40265,40268],{"href":40266,"rel":40267},"https:\u002F\u002Fweb.dev\u002Fhttp-cache\u002F",[51],"HTTP Cache on web.dev",[3065,40270,40271],{},"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 .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 .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":274,"searchDepth":288,"depth":288,"links":40273},[40274,40278,40283,40284,40285,40286,40287,40288],{"id":22771,"depth":288,"text":22772,"children":40275},[40276,40277],{"id":38621,"depth":295,"text":38622},{"id":38631,"depth":295,"text":38632},{"id":38691,"depth":288,"text":38692,"children":40279},[40280,40281,40282],{"id":38706,"depth":295,"text":38707},{"id":39521,"depth":295,"text":39522},{"id":39868,"depth":295,"text":39869},{"id":40021,"depth":288,"text":40022},{"id":40040,"depth":288,"text":40041},{"id":23831,"depth":288,"text":23832},{"id":40119,"depth":288,"text":40120},{"id":10633,"depth":288,"text":10634},{"id":40242,"depth":288,"text":40243},"\u002Fimages\u002Fposts\u002Fguide-to-api-caching-with-firebase-cdn\u002Fbba787bc-b4c2-4af2-a49c-b6fdb2f187ba-a35946029a.png","2023-03-17T05:10:52.714Z","If you're using or thinking of using `Firebase Cloud Functions` or `Cloud Run` for your website\u002Fapp backend, I highly recommend looking into caching your API responses. Used app...","clfc30tux000709msbmll0bcd",{},"\u002Fguide-to-api-caching-with-firebase-cdn",{"title":38601,"description":40291},"guide-to-api-caching-with-firebase-cdn",[24417,40298,40299,10999,40300],"web-development","apis","2articles1week","K-YkikowhDhW7XApX7z0s2Px_ua1duvaqyHVWLi63p8",{"id":40303,"title":40304,"body":40305,"cover":40878,"date":40879,"description":22772,"draft":3086,"extension":3087,"hashnodeId":40880,"meta":40881,"navigation":291,"path":40882,"seo":40883,"slug":40884,"stem":40884,"tags":40885,"__hash__":40888},"posts\u002Fdebug-or-not-there-is-no-middle-ground.md","Debug or not, there is no middle ground",{"type":8,"value":40306,"toc":40867},[40307,40309,40312,40315,40319,40322,40325,40328,40332,40335,40338,40352,40355,40358,40361,40364,40368,40371,40374,40377,40381,40384,40388,40395,40458,40467,40473,40492,40498,40501,40504,40532,40536,40549,40617,40624,40843,40846,40854,40856,40859,40862,40865],[24,40308,22772],{"id":22771},[11,40310,40311],{},"\"What is debugging?\" is a typical question to start an article on the topic, but this is not that article. Instead what I want you to do here is, take a look at the cover image, and process it. Now, try to answer, why the person in the image is doing whatever he is doing. There can be multiple correct answers to this question, but if you answered along the lines of \"to keep the crops healthy for a better yield\" then you're not far from the zen of software development.",[11,40313,40314],{},"Debugging is not a chore, even though it feels like one at times, but is an important pillar on which any healthy and yielding software is built. You cannot love programming and not love (okay, \"love\" may be a strong word here, any value on the positive axis should do) debugging at the same time. This is akin to \"Heisenberg's Certainty Principle\" (is there one?) for Software development. The sooner you start loving the debugging process, the closer you're to achieving zen.",[24,40316,40318],{"id":40317},"why-and-what-to-debug","Why, and what, to debug?",[11,40320,40321],{},"Before we get to the \"how\", we need to understand the \"why\", and the \"what\". I'm sure by now you've some inklings on why we need to debug. Because we want our software to be healthy and useful for the end user. And we achieve that by making it error-free while giving the best experience.",[11,40323,40324],{},"The \"what\" is the next step of the \"why\". Whatever hinders our ability to give the best experience to the end user needs to be debugged and corrected. Some of these hindrances can be because of faulty code, some because of the UI\u002FUX, some others may be because of the network, and so on.",[11,40326,40327],{},"The \"what\" may have left you confused. There is no clear answer in the previous paragraph. And you're right because it depends on the situation you're in. In my previous life, I used to work on Video Telephony (think Zoom calls for phones, but long back and with different standards and protocols). Now there was a phone already in the market which used to work fine with other similar phones. Then there was this phone in the testing phase which we were working on. And when you make a video call between these two, no video used to come on either of the devices. Of course, the video call between the two of our devices was fine as well. Now, whose fault is it? The only truth here is that the call should work, and we should debug every entity in between and including the two devices but starting with ours.",[24,40329,40331],{"id":40330},"how-to-debug","How to debug?",[11,40333,40334],{},"There are many techniques and approaches to debugging. What you need to understand is that the what, and the how, go hand in hand. Depending on the issue you're facing, one way of debugging might be better suited for the job at hand. And sometimes you may need to apply more than one technique to find out the root cause. I'm not going to go into the definitions and detailed explanations of these techniques, you can read about them with a simple Google Search (or maybe ask ChatGPT?).",[11,40336,40337],{},"The starting point of debugging is not using a debugger or adding some debug messages. Rather it is having at least a basic understanding of how something works. If you have that understanding, you can use any appropriate technique to debug. Let's go back to the video call example. On a high level, the way it works is:",[123,40339,40340,40343,40346,40349],{},[74,40341,40342],{},"The calling device sends signals\u002Fmessages to the server that it wants to do a video call with the other device",[74,40344,40345],{},"The server informs the other device about the incoming call",[74,40347,40348],{},"The two devices do further signalling to arrive at a common medium of communication (the audio & the video codecs)",[74,40350,40351],{},"Finally, the media flows between the devices (with or without the server in the loop)",[11,40353,40354],{},"Now we can start eliminating the suspects based on the information we've at hand. Since the call gets established we can rule out the signalling issues. Next, we check whether the other device's transmitted video packets reach our device and whether our video packets leave the device or not. After verifying all these using network packet sniffers, the conclusion was that there is something wrong with the video packets themselves.",[11,40356,40357],{},"So after talking to my senior, I dumped these incoming\u002Foutgoing packets into separate files (like the debug messages), and tried playing these with a video player. Finally, the issue was traced to the other device not sending (and expecting) initial video config header bytes (if I remember correctly, mere 12-16 bytes). And this I could figure out only because I had the required knowledge of the config header standard. The final fix was us adding a check; if the call is with that device, don't send or expect the config bytes and hardcode it. Even though we were doing everything correctly we had to add the fix, because the other device was already live in the market and could not be changed.",[11,40359,40360],{},"So the point I'm trying to make here is that unless you have the needed knowledge it can be very challenging to fix an issue. That doesn't mean that you cannot gain this knowledge in parallel while fixing the issue, in most cases that is how you do it. So try to gain knowledge about how exactly your software works. This may require you to venture beyond your assigned work, but that is how you improve.",[11,40362,40363],{},"Also, many times it is beneficial to discuss the issue with your peers and seniors, as they may provide a clue based on their experiences. And the other point is, sometimes you need to add the fix at your end even if everything is correct there :-). So don't resent it.",[24,40365,40367],{"id":40366},"what-to-take-home-from-debugging","What to take home from debugging?",[11,40369,40370],{},"The first thing you should do after any debugging session is, reflect on it. Think about why the issue happened, and how it was fixed. Now that you have that extra knowledge what you could have done better if you had this knowledge before the issue? In my case, I could have checked for the config header bytes in the packet sniffer logs instead of dumping them into files and then finding out.",[11,40372,40373],{},"The second thing is, actually apply whatever you learnt in your previous experiences. Later on, with a different device and codec a similar issue occurred where the initial video was blurry. From the packets' analysis itself, I could figure out that the issue was with the config header.",[11,40375,40376],{},"Every debugging session should make you a smarter and better developer.",[24,40378,40380],{"id":40379},"dont-rule-out-anything","Don't rule out anything",[11,40382,40383],{},"During one of my recent debugging sessions, I relearned the fact that one should not rule out anything while fixing an issue.",[32,40385,40387],{"id":40386},"the-recent-issue","The recent issue",[11,40389,40390,40391,40394],{},"The issue was the \"copy to clipboard\" not working on the DuckDuckGo Android browser. The issue was reported by one of the players of my puzzle game ",[47,40392,37459],{"href":37457,"rel":40393},[51],". I use the Web Share API for sharing the user's daily stats, and as a fallback for sharing Clipboard API is used for copying the result to the clipboard.",[269,40396,40398],{"className":24597,"code":40397,"language":24599,"meta":274,"style":274},"const shareStats = async () => {\n  const text = 'The text to share';\n\n  if (window.navigator.share) {\n    try {\n      await window.navigator.share({\n        text,\n      });\n    } catch (error) {}\n  } else {\n    await window.navigator.clipboard.writeText(text);\n  }\n};\n",[59,40399,40400,40405,40410,40414,40419,40423,40428,40432,40436,40441,40445,40450,40454],{"__ignoreMap":274},[278,40401,40402],{"class":280,"line":281},[278,40403,40404],{},"const shareStats = async () => {\n",[278,40406,40407],{"class":280,"line":288},[278,40408,40409],{},"  const text = 'The text to share';\n",[278,40411,40412],{"class":280,"line":295},[278,40413,292],{"emptyLinePlaceholder":291},[278,40415,40416],{"class":280,"line":316},[278,40417,40418],{},"  if (window.navigator.share) {\n",[278,40420,40421],{"class":280,"line":322},[278,40422,8279],{},[278,40424,40425],{"class":280,"line":327},[278,40426,40427],{},"      await window.navigator.share({\n",[278,40429,40430],{"class":280,"line":340},[278,40431,5106],{},[278,40433,40434],{"class":280,"line":349},[278,40435,5148],{},[278,40437,40438],{"class":280,"line":375},[278,40439,40440],{},"    } catch (error) {}\n",[278,40442,40443],{"class":280,"line":386},[278,40444,8120],{},[278,40446,40447],{"class":280,"line":397},[278,40448,40449],{},"    await window.navigator.clipboard.writeText(text);\n",[278,40451,40452],{"class":280,"line":408},[278,40453,1096],{},[278,40455,40456],{"class":280,"line":433},[278,40457,2817],{},[11,40459,40460,40461,40466],{},"If you look at the compatibility chart for the ",[47,40462,40465],{"href":40463,"rel":40464},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FClipboard_API",[51],"Clipboard API"," you'll see that it has wide availability.",[11,40468,40469],{},[3135,40470],{"alt":40471,"src":40472},"Clipboard compatibility","\u002Fimages\u002Fposts\u002Fdebug-or-not-there-is-no-middle-ground\u002Fa54fbf29-f60c-4958-8aac-10f3d7bbf348-a2904b7ac0.png",[11,40474,40475,40476,40479,40480,40487,40488,40491],{},"Also, the ",[59,40477,40478],{},"\"clipboard-write\""," permission of the ",[47,40481,40484],{"href":40482,"rel":40483},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FPermissions_API",[51],[94,40485,40486],{},"Permissions API"," is granted automatically to pages when they are in the active tab. So that should allow me to use the ",[59,40489,40490],{},"writeText"," method of the Clipboard API on any browser.",[11,40493,40494],{},[3135,40495],{"alt":40496,"src":40497},"writeText compatibility","\u002Fimages\u002Fposts\u002Fdebug-or-not-there-is-no-middle-ground\u002F28396ea9-008b-4f7f-93a7-d8f0902754cc-de47cd825d.png",[11,40499,40500],{},"But it didn't work despite such reassurances. Now how do you debug the issue considering it needs to be done on Android (the DuckDuckGo Mac client works fine)? Since console logs were of no use, I used the normal p \u002F div tags to print the debugging messages in the browser window itself.",[11,40502,40503],{},"The debugging revealed the following",[123,40505,40506,40519],{},[74,40507,40508,40511,40512,40515,40516],{},[59,40509,40510],{},"\"Navigator.clipboard.writeText\""," throws ",[59,40513,40514],{},"NotAllowedError"," with a message saying ",[59,40517,40518],{},"Write permission denied",[74,40520,40521,40522,40527,40528,40531],{},"Since write permission was denied, so maybe somehow we could query and ask for the needed permission using the ",[47,40523,40525],{"href":40482,"rel":40524},[51],[94,40526,40486],{},"? Alas! ",[59,40529,40530],{},"\"Navigator.permissions\""," is not available on DuckDuckGo",[32,40533,40535],{"id":40534},"the-fix","The Fix",[11,40537,40538,40539,40542,40543,40548],{},"The workaround was to use the ",[59,40540,40541],{},"document.execCommand",". After some searching on Google, ",[47,40544,40547],{"href":40545,"rel":40546},"https:\u002F\u002Fweb.dev\u002Fasync-clipboard\u002F#:~:text=be%20triggered%20using-,document.execCommand(%27copy%27),-and%20document.execCommand",[51],"found this link"," with a code snippet",[269,40550,40552],{"className":24597,"code":40551,"language":24599,"meta":274,"style":274},"button.addEventListener('click', (e) => {\n  const input = document.createElement('input');\n  input.style.display = 'none';\n  document.body.appendChild(input);\n  input.value = text;\n  input.focus();\n  input.select();\n  const result = document.execCommand('copy');\n  if (result === 'unsuccessful') {\n    console.error('Failed to copy text.');\n  }\n  input.remove();\n});\n",[59,40553,40554,40559,40564,40569,40574,40579,40584,40589,40594,40599,40604,40608,40613],{"__ignoreMap":274},[278,40555,40556],{"class":280,"line":281},[278,40557,40558],{},"button.addEventListener('click', (e) => {\n",[278,40560,40561],{"class":280,"line":288},[278,40562,40563],{},"  const input = document.createElement('input');\n",[278,40565,40566],{"class":280,"line":295},[278,40567,40568],{},"  input.style.display = 'none';\n",[278,40570,40571],{"class":280,"line":316},[278,40572,40573],{},"  document.body.appendChild(input);\n",[278,40575,40576],{"class":280,"line":322},[278,40577,40578],{},"  input.value = text;\n",[278,40580,40581],{"class":280,"line":327},[278,40582,40583],{},"  input.focus();\n",[278,40585,40586],{"class":280,"line":340},[278,40587,40588],{},"  input.select();\n",[278,40590,40591],{"class":280,"line":349},[278,40592,40593],{},"  const result = document.execCommand('copy');\n",[278,40595,40596],{"class":280,"line":375},[278,40597,40598],{},"  if (result === 'unsuccessful') {\n",[278,40600,40601],{"class":280,"line":386},[278,40602,40603],{},"    console.error('Failed to copy text.');\n",[278,40605,40606],{"class":280,"line":397},[278,40607,1096],{},[278,40609,40610],{"class":280,"line":408},[278,40611,40612],{},"  input.remove();\n",[278,40614,40615],{"class":280,"line":433},[278,40616,3693],{},[11,40618,40619,40620,40623],{},"But this also didn't work in my case. Further trials and errors showed that we can't do ",[59,40621,40622],{},"input.style.display = 'none';"," Because if the element is not visible then it won't work for DuckDuckGo. So the final working code is as below",[269,40625,40627],{"className":24597,"code":40626,"language":24599,"meta":274,"style":274},"const shareStats = async () => {\n  const text = `The text to share\\nWith multiple lines`;\n\n  if (window.navigator.share) {\n    try {\n      await window.navigator.share({\n        text,\n      });\n    } catch (error) {}\n\n    return;\n  }\n\n  await copyToClipboard(text);\n};\n\nconst copyToClipboard = async (text) => {\n  if (window.navigator.clipboard) {\n    try {\n      await window.navigator.clipboard.writeText(text);\n      return;\n    } catch (error) {}\n  }\n\n  const textarea = document.createElement('textarea');\n  textarea.style.position = 'fixed';\n  textarea.style.width = '1px';\n  textarea.style.height = '1px';\n  textarea.style.padding = 0;\n  textarea.style.border = 'none';\n  textarea.style.outline = 'none';\n  textarea.style.boxShadow = 'none';\n  textarea.style.background = 'transparent';\n\n  document.body.appendChild(textarea);\n\n  textarea.textContent = text;\n  textarea.focus();\n  textarea.select();\n\n  const result = document.execCommand('copy');\n  textarea.remove();\n  if (!result) {\n    \u002F\u002F Show some error message to the user\n  } else {\n    \u002F\u002F Show a success message to the user mentioning the text is copied to their clipboard\n  }\n};\n",[59,40628,40629,40633,40638,40642,40646,40650,40654,40658,40662,40666,40670,40674,40678,40682,40687,40691,40695,40700,40705,40709,40714,40718,40722,40726,40730,40735,40740,40745,40750,40755,40760,40765,40770,40775,40779,40784,40788,40793,40798,40803,40807,40811,40816,40821,40826,40830,40835,40839],{"__ignoreMap":274},[278,40630,40631],{"class":280,"line":281},[278,40632,40404],{},[278,40634,40635],{"class":280,"line":288},[278,40636,40637],{},"  const text = `The text to share\\nWith multiple lines`;\n",[278,40639,40640],{"class":280,"line":295},[278,40641,292],{"emptyLinePlaceholder":291},[278,40643,40644],{"class":280,"line":316},[278,40645,40418],{},[278,40647,40648],{"class":280,"line":322},[278,40649,8279],{},[278,40651,40652],{"class":280,"line":327},[278,40653,40427],{},[278,40655,40656],{"class":280,"line":340},[278,40657,5106],{},[278,40659,40660],{"class":280,"line":349},[278,40661,5148],{},[278,40663,40664],{"class":280,"line":375},[278,40665,40440],{},[278,40667,40668],{"class":280,"line":386},[278,40669,292],{"emptyLinePlaceholder":291},[278,40671,40672],{"class":280,"line":397},[278,40673,8881],{},[278,40675,40676],{"class":280,"line":408},[278,40677,1096],{},[278,40679,40680],{"class":280,"line":433},[278,40681,292],{"emptyLinePlaceholder":291},[278,40683,40684],{"class":280,"line":454},[278,40685,40686],{},"  await copyToClipboard(text);\n",[278,40688,40689],{"class":280,"line":475},[278,40690,2817],{},[278,40692,40693],{"class":280,"line":496},[278,40694,292],{"emptyLinePlaceholder":291},[278,40696,40697],{"class":280,"line":505},[278,40698,40699],{},"const copyToClipboard = async (text) => {\n",[278,40701,40702],{"class":280,"line":516},[278,40703,40704],{},"  if (window.navigator.clipboard) {\n",[278,40706,40707],{"class":280,"line":527},[278,40708,8279],{},[278,40710,40711],{"class":280,"line":533},[278,40712,40713],{},"      await window.navigator.clipboard.writeText(text);\n",[278,40715,40716],{"class":280,"line":539},[278,40717,38526],{},[278,40719,40720],{"class":280,"line":545},[278,40721,40440],{},[278,40723,40724],{"class":280,"line":551},[278,40725,1096],{},[278,40727,40728],{"class":280,"line":557},[278,40729,292],{"emptyLinePlaceholder":291},[278,40731,40732],{"class":280,"line":567},[278,40733,40734],{},"  const textarea = document.createElement('textarea');\n",[278,40736,40737],{"class":280,"line":577},[278,40738,40739],{},"  textarea.style.position = 'fixed';\n",[278,40741,40742],{"class":280,"line":587},[278,40743,40744],{},"  textarea.style.width = '1px';\n",[278,40746,40747],{"class":280,"line":597},[278,40748,40749],{},"  textarea.style.height = '1px';\n",[278,40751,40752],{"class":280,"line":608},[278,40753,40754],{},"  textarea.style.padding = 0;\n",[278,40756,40757],{"class":280,"line":614},[278,40758,40759],{},"  textarea.style.border = 'none';\n",[278,40761,40762],{"class":280,"line":620},[278,40763,40764],{},"  textarea.style.outline = 'none';\n",[278,40766,40767],{"class":280,"line":625},[278,40768,40769],{},"  textarea.style.boxShadow = 'none';\n",[278,40771,40772],{"class":280,"line":640},[278,40773,40774],{},"  textarea.style.background = 'transparent';\n",[278,40776,40777],{"class":280,"line":663},[278,40778,292],{"emptyLinePlaceholder":291},[278,40780,40781],{"class":280,"line":669},[278,40782,40783],{},"  document.body.appendChild(textarea);\n",[278,40785,40786],{"class":280,"line":680},[278,40787,292],{"emptyLinePlaceholder":291},[278,40789,40790],{"class":280,"line":686},[278,40791,40792],{},"  textarea.textContent = text;\n",[278,40794,40795],{"class":280,"line":1334},[278,40796,40797],{},"  textarea.focus();\n",[278,40799,40800],{"class":280,"line":1375},[278,40801,40802],{},"  textarea.select();\n",[278,40804,40805],{"class":280,"line":1381},[278,40806,292],{"emptyLinePlaceholder":291},[278,40808,40809],{"class":280,"line":1386},[278,40810,40593],{},[278,40812,40813],{"class":280,"line":1394},[278,40814,40815],{},"  textarea.remove();\n",[278,40817,40818],{"class":280,"line":1406},[278,40819,40820],{},"  if (!result) {\n",[278,40822,40823],{"class":280,"line":1423},[278,40824,40825],{},"    \u002F\u002F Show some error message to the user\n",[278,40827,40828],{"class":280,"line":1432},[278,40829,8120],{},[278,40831,40832],{"class":280,"line":1437},[278,40833,40834],{},"    \u002F\u002F Show a success message to the user mentioning the text is copied to their clipboard\n",[278,40836,40837],{"class":280,"line":1916},[278,40838,1096],{},[278,40840,40841],{"class":280,"line":1939},[278,40842,2817],{},[11,40844,40845],{},"Learnings?",[123,40847,40848,40851],{},[74,40849,40850],{},"Don't rule out anything, even if everything says that it will work",[74,40852,40853],{},"Code copied from the internet may not work in its entirety and all situations",[24,40855,10634],{"id":10633},[11,40857,40858],{},"To summarize, debugging plays a crucial role in software development and requires a good grasp of the software's know-how. There are diverse methods and strategies for debugging, and we must remain open-minded while resolving any issue. Each debugging session presents an opportunity for us to enhance our skills and knowledge, so we should welcome it with open arms.",[11,40860,40861],{},"If you liked reading the article, do drop a 👋 in the comments section.",[11,40863,40864],{},"Keep adding the bits, only they make a BYTE. :-)",[3065,40866,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":40868},[40869,40870,40871,40872,40873,40877],{"id":22771,"depth":288,"text":22772},{"id":40317,"depth":288,"text":40318},{"id":40330,"depth":288,"text":40331},{"id":40366,"depth":288,"text":40367},{"id":40379,"depth":288,"text":40380,"children":40874},[40875,40876],{"id":40386,"depth":295,"text":40387},{"id":40534,"depth":295,"text":40535},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fdebug-or-not-there-is-no-middle-ground\u002Ff6e1c8d5-4d27-4223-945f-102475efcf07-507db8e76a.jpeg","2023-03-11T23:17:34.003Z","clf4l77hv000109jq8dm86o4n",{},"\u002Fdebug-or-not-there-is-no-middle-ground",{"title":40304,"description":22772},"debug-or-not-there-is-no-middle-ground",[40886,40300,40887],"software-development","debuggingfeb","Zrz6YJY49St_DJRHqJROow7qi3m9ccP0Jlxm8cP2Ce8",{"id":40890,"title":40891,"body":40892,"cover":44731,"date":44732,"description":22772,"draft":3086,"extension":3087,"hashnodeId":44733,"meta":44734,"navigation":291,"path":44735,"seo":44736,"slug":44737,"stem":44737,"tags":44738,"__hash__":44741},"posts\u002Freact-app-with-mongodb-atlas-app-services.md","Create a personal expense tracker using MongoDB Atlas App Services & Triggers",{"type":8,"value":40893,"toc":44707},[40894,40896,40899,40903,40906,40909,40920,40923,40927,40930,40944,40947,40951,40957,41026,41029,41115,41119,41133,41274,41278,41297,41376,41380,41387,41829,41832,41838,41842,41845,42455,42458,42464,42468,42477,42630,42637,42643,42647,42660,42664,42671,42677,42683,42690,42696,42700,42714,42720,42727,42733,42737,42740,42746,42755,42761,42765,42771,42781,42787,42813,42819,42829,42928,42935,42939,42951,42954,43001,43012,43053,43058,43292,43298,43388,43394,43416,43419,43424,43430,43435,43441,43455,43461,43468,43474,43477,43481,43486,43589,43592,43601,43607,43616,43634,43640,43654,43688,43694,43716,43719,43723,43730,43758,43780,43787,43969,43972,43976,43979,44025,44028,44034,44037,44043,44053,44189,44194,44328,44331,44337,44340,44343,44411,44422,44428,44432,44443,44475,44481,44488,44667,44670,44672,44675,44691,44694,44702,44704],[24,40895,22772],{"id":22771},[11,40897,40898],{},"Did you know that Atlas App Services is a fully managed cloud service offering from MongoDB that we can use as a backend for our apps? This article provides a step-by-step guide on how to use MongoDB Atlas App Services and its various triggers for creating an app. The app is a basic personal expense tracker, and we will be using the React library to code it from scratch.",[24,40900,40902],{"id":40901},"what-is-a-trigger","What is a trigger?",[11,40904,40905],{},"Before moving ahead, let's answer this question first. What is a trigger? I'm sure all of us are familiar with the most common usage of this word, but it is much more than that. \"A trigger is a cause or an event that precedes or starts an action\". It is like \"if-this-then-that\" but at a different layer and with a broader scope. Triggers play a crucial role in the smooth functioning of any serverless application development, we must have a good grasp of them.",[11,40907,40908],{},"So which triggers and actions we're talking about here considering application development in general and our app in particular? Well, some of the examples can be",[123,40910,40911,40914,40917],{},[74,40912,40913],{},"If someone signs up for our app, then we can send them a welcome email, and\u002For create a user entry in our database.",[74,40915,40916],{},"If a user does something inside the app, say creates a new transaction then we can update their balance for quick retrieval",[74,40918,40919],{},"If there is an action that happens on a regular schedule, then maybe we can automate it instead of updating it manually, and so on...",[11,40921,40922],{},"There can be many more examples, but these 3 are sufficient to understand the triggers which App services offer. The first one is called the \"Auth Trigger\" as we're doing something because of the auth event. The second one is a database trigger, as when a new entry is added to the database we do something else asynchronously. And the last one is a scheduled trigger because it happens on a regular schedule. Now let's dive into the app we're going to build.",[24,40924,40926],{"id":40925},"the-frontend","The Frontend",[11,40928,40929],{},"As we're building a personal expense tracker, at the bare minimum it needs to have the following features",[123,40931,40932,40935,40938,40941],{},[74,40933,40934],{},"Ability to create an account so that we can associate the transactions with a particular user. To keep it simple we'll be using anonymous login to achieve it",[74,40936,40937],{},"Ability to add manual entries for any credit or debit",[74,40939,40940],{},"A basic dashboard where we can get a holistic view of our finances for the month, and also see the individual transactions",[74,40942,40943],{},"On change of month reset the credits\u002Fdebits and possibly do other related chores",[11,40945,40946],{},"Now that the scope of the work is defined, let's start building it",[32,40948,40950],{"id":40949},"setting-up-the-react-app","Setting up the React App",[11,40952,40953,40954],{},"Let's quickly set up a React project using the following set of commands in your terminal window. ",[3061,40955,40956],{},"I'm using \"yarn\" as my package manager, you can use commands specific to your preferred package manager.",[269,40958,40960],{"className":3335,"code":40959,"language":3337,"meta":274,"style":274},"# Create the project dir & client subdir and immediately cd into it.\n# \"mkdir -p\" creates the non existant parent dir.\nmkdir -p my-expenses-tracker\u002Fclient && cd $_\n\n# Create a React app in the current dir (client)\nyarn create react-app .\n\n# Run the app and start the dev server\nyarn start\n",[59,40961,40962,40967,40972,40989,40993,40998,41010,41014,41019],{"__ignoreMap":274},[278,40963,40964],{"class":280,"line":281},[278,40965,40966],{"class":284},"# Create the project dir & client subdir and immediately cd into it.\n",[278,40968,40969],{"class":280,"line":288},[278,40970,40971],{"class":284},"# \"mkdir -p\" creates the non existant parent dir.\n",[278,40973,40974,40977,40980,40983,40985,40987],{"class":280,"line":295},[278,40975,40976],{"class":333},"mkdir",[278,40978,40979],{"class":650}," -p",[278,40981,40982],{"class":309}," my-expenses-tracker\u002Fclient",[278,40984,3361],{"class":302},[278,40986,3364],{"class":650},[278,40988,3367],{"class":650},[278,40990,40991],{"class":280,"line":316},[278,40992,292],{"emptyLinePlaceholder":291},[278,40994,40995],{"class":280,"line":322},[278,40996,40997],{"class":284},"# Create a React app in the current dir (client)\n",[278,40999,41000,41002,41004,41007],{"class":280,"line":327},[278,41001,33494],{"class":333},[278,41003,24553],{"class":309},[278,41005,41006],{"class":309}," react-app",[278,41008,41009],{"class":309}," .\n",[278,41011,41012],{"class":280,"line":340},[278,41013,292],{"emptyLinePlaceholder":291},[278,41015,41016],{"class":280,"line":349},[278,41017,41018],{"class":284},"# Run the app and start the dev server\n",[278,41020,41021,41023],{"class":280,"line":375},[278,41022,33494],{"class":333},[278,41024,41025],{"class":309}," start\n",[11,41027,41028],{},"Open another terminal window, navigate to the client folder and run the following commands.",[269,41030,41032],{"className":3335,"code":41031,"language":3337,"meta":274,"style":274},"# Add react-router-dom and react-icons to the project\nyarn add react-router-dom react-icons\n\n# Create routes & components folders inside src dir\nmkdir src\u002Froutes src\u002Fcomponents\n\n# Create the initial pages & components\ntouch src\u002Froutes\u002FDashboard.js src\u002Froutes\u002FAddExpense.js src\u002Fcomponents\u002FNavbar.js\n\n# Open the project in VS Code\ncd .. && code .\n",[59,41033,41034,41039,41051,41055,41060,41070,41074,41079,41093,41097,41102],{"__ignoreMap":274},[278,41035,41036],{"class":280,"line":281},[278,41037,41038],{"class":284},"# Add react-router-dom and react-icons to the project\n",[278,41040,41041,41043,41045,41048],{"class":280,"line":288},[278,41042,33494],{"class":333},[278,41044,3418],{"class":309},[278,41046,41047],{"class":309}," react-router-dom",[278,41049,41050],{"class":309}," react-icons\n",[278,41052,41053],{"class":280,"line":295},[278,41054,292],{"emptyLinePlaceholder":291},[278,41056,41057],{"class":280,"line":316},[278,41058,41059],{"class":284},"# Create routes & components folders inside src dir\n",[278,41061,41062,41064,41067],{"class":280,"line":322},[278,41063,40976],{"class":333},[278,41065,41066],{"class":309}," src\u002Froutes",[278,41068,41069],{"class":309}," src\u002Fcomponents\n",[278,41071,41072],{"class":280,"line":327},[278,41073,292],{"emptyLinePlaceholder":291},[278,41075,41076],{"class":280,"line":340},[278,41077,41078],{"class":284},"# Create the initial pages & components\n",[278,41080,41081,41084,41087,41090],{"class":280,"line":349},[278,41082,41083],{"class":333},"touch",[278,41085,41086],{"class":309}," src\u002Froutes\u002FDashboard.js",[278,41088,41089],{"class":309}," src\u002Froutes\u002FAddExpense.js",[278,41091,41092],{"class":309}," src\u002Fcomponents\u002FNavbar.js\n",[278,41094,41095],{"class":280,"line":375},[278,41096,292],{"emptyLinePlaceholder":291},[278,41098,41099],{"class":280,"line":386},[278,41100,41101],{"class":284},"# Open the project in VS Code\n",[278,41103,41104,41106,41109,41111,41113],{"class":280,"line":397},[278,41105,3364],{"class":650},[278,41107,41108],{"class":309}," ..",[278,41110,3361],{"class":302},[278,41112,59],{"class":333},[278,41114,41009],{"class":309},[32,41116,41118],{"id":41117},"creating-the-routes","Creating the routes",[11,41120,41121,41122,41125,41126,41129,41130,41132],{},"We'll be adding two routes to the app: ",[94,41123,41124],{},"1."," the dashboard page, and ",[94,41127,41128],{},"2."," the new transaction page. Replace the content of the ",[59,41131,37763],{}," file with the following:",[269,41134,41136],{"className":24597,"code":41135,"language":24599,"meta":274,"style":274},"import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';\n\nimport { Dashboard } from '.\u002Froutes\u002FDashboard';\nimport { NewTransaction } from '.\u002Froutes\u002FNewTransaction';\nimport { Navbar } from '.\u002Fcomponents\u002FNavbar';\nimport '.\u002FApp.css';\n\nconst Layout = () => {\n  return (\n    \u003Cdiv className='app'>\n      \u003CNavbar \u002F>\n      \u003COutlet \u002F>\n    \u003C\u002Fdiv>\n  );\n};\n\nfunction App() {\n  return (\n    \u003CBrowserRouter>\n      \u003CRoutes>\n        \u003CRoute path='\u002F' element={\u003CLayout \u002F>}>\n          \u003CRoute index element={\u003CDashboard \u002F>} \u002F>\n          \u003CRoute path='\u002Fnew' element={\u003CNewTransaction \u002F>} \u002F>\n        \u003C\u002FRoute>\n      \u003C\u002FRoutes>\n    \u003C\u002FBrowserRouter>\n  );\n}\n\nexport default App;\n",[59,41137,41138,41143,41147,41152,41157,41162,41166,41170,41175,41179,41184,41189,41194,41198,41202,41206,41210,41214,41218,41223,41228,41233,41238,41243,41248,41253,41258,41262,41266,41270],{"__ignoreMap":274},[278,41139,41140],{"class":280,"line":281},[278,41141,41142],{},"import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';\n",[278,41144,41145],{"class":280,"line":288},[278,41146,292],{"emptyLinePlaceholder":291},[278,41148,41149],{"class":280,"line":295},[278,41150,41151],{},"import { Dashboard } from '.\u002Froutes\u002FDashboard';\n",[278,41153,41154],{"class":280,"line":316},[278,41155,41156],{},"import { NewTransaction } from '.\u002Froutes\u002FNewTransaction';\n",[278,41158,41159],{"class":280,"line":322},[278,41160,41161],{},"import { Navbar } from '.\u002Fcomponents\u002FNavbar';\n",[278,41163,41164],{"class":280,"line":327},[278,41165,37782],{},[278,41167,41168],{"class":280,"line":340},[278,41169,292],{"emptyLinePlaceholder":291},[278,41171,41172],{"class":280,"line":349},[278,41173,41174],{},"const Layout = () => {\n",[278,41176,41177],{"class":280,"line":375},[278,41178,37650],{},[278,41180,41181],{"class":280,"line":386},[278,41182,41183],{},"    \u003Cdiv className='app'>\n",[278,41185,41186],{"class":280,"line":397},[278,41187,41188],{},"      \u003CNavbar \u002F>\n",[278,41190,41191],{"class":280,"line":408},[278,41192,41193],{},"      \u003COutlet \u002F>\n",[278,41195,41196],{"class":280,"line":433},[278,41197,7950],{},[278,41199,41200],{"class":280,"line":454},[278,41201,611],{},[278,41203,41204],{"class":280,"line":475},[278,41205,2817],{},[278,41207,41208],{"class":280,"line":496},[278,41209,292],{"emptyLinePlaceholder":291},[278,41211,41212],{"class":280,"line":505},[278,41213,37791],{},[278,41215,41216],{"class":280,"line":516},[278,41217,37650],{},[278,41219,41220],{"class":280,"line":527},[278,41221,41222],{},"    \u003CBrowserRouter>\n",[278,41224,41225],{"class":280,"line":533},[278,41226,41227],{},"      \u003CRoutes>\n",[278,41229,41230],{"class":280,"line":539},[278,41231,41232],{},"        \u003CRoute path='\u002F' element={\u003CLayout \u002F>}>\n",[278,41234,41235],{"class":280,"line":545},[278,41236,41237],{},"          \u003CRoute index element={\u003CDashboard \u002F>} \u002F>\n",[278,41239,41240],{"class":280,"line":551},[278,41241,41242],{},"          \u003CRoute path='\u002Fnew' element={\u003CNewTransaction \u002F>} \u002F>\n",[278,41244,41245],{"class":280,"line":557},[278,41246,41247],{},"        \u003C\u002FRoute>\n",[278,41249,41250],{"class":280,"line":567},[278,41251,41252],{},"      \u003C\u002FRoutes>\n",[278,41254,41255],{"class":280,"line":577},[278,41256,41257],{},"    \u003C\u002FBrowserRouter>\n",[278,41259,41260],{"class":280,"line":587},[278,41261,611],{},[278,41263,41264],{"class":280,"line":597},[278,41265,617],{},[278,41267,41268],{"class":280,"line":608},[278,41269,292],{"emptyLinePlaceholder":291},[278,41271,41272],{"class":280,"line":614},[278,41273,37826],{},[32,41275,41277],{"id":41276},"the-navbar-component","The Navbar component",[11,41279,41280,41281,41284,41285,19634,41288,41291,41292,183],{},"Add the following code into the ",[59,41282,41283],{},"Navbar.js"," file that we created earlier. You can get the image assets, as well as the CSS files (",[59,41286,41287],{},"index.css",[59,41289,41290],{},"App.css",") from ",[47,41293,41296],{"href":41294,"rel":41295},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fexpense-buddy",[51],"the Github repo",[269,41298,41300],{"className":24597,"code":41299,"language":24599,"meta":274,"style":274},"import { NavLink } from 'react-router-dom';\nimport { FaPlusCircle } from 'react-icons\u002Ffa';\n\nexport const Navbar = () => {\n  return (\n    \u003Cnav className='navbar '>\n      \u003CNavLink className='logo nav-link' to='\u002F'>\n        Expense Buddy\n      \u003C\u002FNavLink>\n\n      \u003CNavLink className='nav-link' to='\u002Fnew'>\n        \u003CFaPlusCircle \u002F> Add New\n      \u003C\u002FNavLink>\n    \u003C\u002Fnav>\n  );\n};\n",[59,41301,41302,41307,41312,41316,41321,41325,41330,41335,41340,41345,41349,41354,41359,41363,41368,41372],{"__ignoreMap":274},[278,41303,41304],{"class":280,"line":281},[278,41305,41306],{},"import { NavLink } from 'react-router-dom';\n",[278,41308,41309],{"class":280,"line":288},[278,41310,41311],{},"import { FaPlusCircle } from 'react-icons\u002Ffa';\n",[278,41313,41314],{"class":280,"line":295},[278,41315,292],{"emptyLinePlaceholder":291},[278,41317,41318],{"class":280,"line":316},[278,41319,41320],{},"export const Navbar = () => {\n",[278,41322,41323],{"class":280,"line":322},[278,41324,37650],{},[278,41326,41327],{"class":280,"line":327},[278,41328,41329],{},"    \u003Cnav className='navbar '>\n",[278,41331,41332],{"class":280,"line":340},[278,41333,41334],{},"      \u003CNavLink className='logo nav-link' to='\u002F'>\n",[278,41336,41337],{"class":280,"line":349},[278,41338,41339],{},"        Expense Buddy\n",[278,41341,41342],{"class":280,"line":375},[278,41343,41344],{},"      \u003C\u002FNavLink>\n",[278,41346,41347],{"class":280,"line":386},[278,41348,292],{"emptyLinePlaceholder":291},[278,41350,41351],{"class":280,"line":397},[278,41352,41353],{},"      \u003CNavLink className='nav-link' to='\u002Fnew'>\n",[278,41355,41356],{"class":280,"line":408},[278,41357,41358],{},"        \u003CFaPlusCircle \u002F> Add New\n",[278,41360,41361],{"class":280,"line":433},[278,41362,41344],{},[278,41364,41365],{"class":280,"line":454},[278,41366,41367],{},"    \u003C\u002Fnav>\n",[278,41369,41370],{"class":280,"line":475},[278,41371,611],{},[278,41373,41374],{"class":280,"line":496},[278,41375,2817],{},[32,41377,41379],{"id":41378},"dashboard-page","Dashboard Page",[11,41381,41382,41383,41386],{},"Add the following code to the ",[59,41384,41385],{},"Dashboard.js"," file. We'll come back to this file and modify it to add the backend interaction later on.",[269,41388,41390],{"className":24597,"code":41389,"language":24599,"meta":274,"style":274},"import { useState, useEffect } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { FaPlusCircle } from 'react-icons\u002Ffa';\n\nimport { formatDateTime, formatCurrency } from '..\u002Futils';\nimport AddImage from '..\u002Fassets\u002Fimages\u002Fadd-notes.svg';\n\nexport const Dashboard = () => {\n  const [user, setUser] = useState();\n  const [transactions, setTransactions] = useState([]);\n  const [loading, setLoading] = useState(false);\n\n  const navigate = useNavigate();\n\n  return (\n    \u003Cdiv className='container'>\n      {loading ? (\n        \u003Cdiv className='loader'>Loading...\u003C\u002Fdiv>\n      ) : transactions.length ? (\n        \u003Cdiv className='dashboard'>\n          {user && (\n            \u003Cdiv className='card summary-card'>\n              \u003Ch2>This month\u003C\u002Fh2>\n\n              \u003Cdiv className='details'>\n                \u003Cdiv>Current Balance\u003C\u002Fdiv>\n                \u003Cdiv className='details-value'>\n                  {formatCurrency(user.balance)}\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n\n              \u003Cdiv className='card-row'>\n                \u003Cdiv className='details money-in'>\n                  \u003Cdiv className='details-label'>Total money in\u003C\u002Fdiv>\n                  \u003Cdiv className='details-value'>\n                    {formatCurrency(user.currMonth.in)}\n                  \u003C\u002Fdiv>\n                \u003C\u002Fdiv>\n                \u003Cdiv className='details money-out'>\n                  \u003Cdiv className='details-label'>Total money out\u003C\u002Fdiv>\n                  \u003Cdiv className='details-value'>\n                    {formatCurrency(user.currMonth.out)}\n                  \u003C\u002Fdiv>\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n          )}\n\n          \u003Ch3 className='transactions-title'>Transactions\u003C\u002Fh3>\n\n          {transactions.map((transaction) => {\n            return (\n              \u003Cdiv\n                key={transaction._id}\n                className={`card transaction-card ${\n                  transaction.type === 'IN'\n                    ? 'transaction-in'\n                    : 'transaction-out'\n                }`}\n              >\n                \u003Cdiv>\n                  \u003Cdiv>{transaction.comment}\u003C\u002Fdiv>\n                  \u003Cdiv className='transaction-date'>\n                    {formatDateTime(transaction.createdAt)}\n                  \u003C\u002Fdiv>\n                \u003C\u002Fdiv>\n                \u003Cdiv className='transaction-value'>\n                  {formatCurrency(transaction.amount)}\n                \u003C\u002Fdiv>\n              \u003C\u002Fdiv>\n            );\n          })}\n        \u003C\u002Fdiv>\n      ) : (\n        \u003Cdiv className='no-data'>\n          \u003Cimg\n            className='no-data-img'\n            src={AddImage}\n            alt='No transactions found, add one'\n          \u002F>\n          \u003Cdiv className='no-data-text'>No transactions found\u003C\u002Fdiv>\n          \u003Cbutton\n            type='button'\n            className='btn btn-primary'\n            onClick={() => navigate('\u002Fnew')}\n          >\n            \u003CFaPlusCircle \u002F> Add Transaction\n          \u003C\u002Fbutton>\n        \u003C\u002Fdiv>\n      )}\n    \u003C\u002Fdiv>\n  );\n};\n",[59,41391,41392,41397,41402,41406,41410,41415,41420,41424,41429,41434,41439,41444,41448,41453,41457,41461,41466,41471,41476,41481,41486,41491,41496,41501,41505,41510,41515,41520,41525,41530,41535,41539,41544,41549,41554,41559,41564,41569,41573,41578,41583,41587,41592,41596,41600,41604,41609,41614,41618,41623,41627,41632,41636,41641,41646,41651,41656,41661,41666,41671,41675,41680,41685,41690,41695,41699,41703,41708,41713,41717,41721,41725,41729,41733,41738,41743,41748,41753,41758,41763,41768,41773,41778,41783,41788,41793,41798,41803,41808,41812,41817,41821,41825],{"__ignoreMap":274},[278,41393,41394],{"class":280,"line":281},[278,41395,41396],{},"import { useState, useEffect } from 'react';\n",[278,41398,41399],{"class":280,"line":288},[278,41400,41401],{},"import { useNavigate } from 'react-router-dom';\n",[278,41403,41404],{"class":280,"line":295},[278,41405,41311],{},[278,41407,41408],{"class":280,"line":316},[278,41409,292],{"emptyLinePlaceholder":291},[278,41411,41412],{"class":280,"line":322},[278,41413,41414],{},"import { formatDateTime, formatCurrency } from '..\u002Futils';\n",[278,41416,41417],{"class":280,"line":327},[278,41418,41419],{},"import AddImage from '..\u002Fassets\u002Fimages\u002Fadd-notes.svg';\n",[278,41421,41422],{"class":280,"line":340},[278,41423,292],{"emptyLinePlaceholder":291},[278,41425,41426],{"class":280,"line":349},[278,41427,41428],{},"export const Dashboard = () => {\n",[278,41430,41431],{"class":280,"line":375},[278,41432,41433],{},"  const [user, setUser] = useState();\n",[278,41435,41436],{"class":280,"line":386},[278,41437,41438],{},"  const [transactions, setTransactions] = useState([]);\n",[278,41440,41441],{"class":280,"line":397},[278,41442,41443],{},"  const [loading, setLoading] = useState(false);\n",[278,41445,41446],{"class":280,"line":408},[278,41447,292],{"emptyLinePlaceholder":291},[278,41449,41450],{"class":280,"line":433},[278,41451,41452],{},"  const navigate = useNavigate();\n",[278,41454,41455],{"class":280,"line":454},[278,41456,292],{"emptyLinePlaceholder":291},[278,41458,41459],{"class":280,"line":475},[278,41460,37650],{},[278,41462,41463],{"class":280,"line":496},[278,41464,41465],{},"    \u003Cdiv className='container'>\n",[278,41467,41468],{"class":280,"line":505},[278,41469,41470],{},"      {loading ? (\n",[278,41472,41473],{"class":280,"line":516},[278,41474,41475],{},"        \u003Cdiv className='loader'>Loading...\u003C\u002Fdiv>\n",[278,41477,41478],{"class":280,"line":527},[278,41479,41480],{},"      ) : transactions.length ? (\n",[278,41482,41483],{"class":280,"line":533},[278,41484,41485],{},"        \u003Cdiv className='dashboard'>\n",[278,41487,41488],{"class":280,"line":539},[278,41489,41490],{},"          {user && (\n",[278,41492,41493],{"class":280,"line":545},[278,41494,41495],{},"            \u003Cdiv className='card summary-card'>\n",[278,41497,41498],{"class":280,"line":551},[278,41499,41500],{},"              \u003Ch2>This month\u003C\u002Fh2>\n",[278,41502,41503],{"class":280,"line":557},[278,41504,292],{"emptyLinePlaceholder":291},[278,41506,41507],{"class":280,"line":567},[278,41508,41509],{},"              \u003Cdiv className='details'>\n",[278,41511,41512],{"class":280,"line":577},[278,41513,41514],{},"                \u003Cdiv>Current Balance\u003C\u002Fdiv>\n",[278,41516,41517],{"class":280,"line":587},[278,41518,41519],{},"                \u003Cdiv className='details-value'>\n",[278,41521,41522],{"class":280,"line":597},[278,41523,41524],{},"                  {formatCurrency(user.balance)}\n",[278,41526,41527],{"class":280,"line":608},[278,41528,41529],{},"                \u003C\u002Fdiv>\n",[278,41531,41532],{"class":280,"line":614},[278,41533,41534],{},"              \u003C\u002Fdiv>\n",[278,41536,41537],{"class":280,"line":620},[278,41538,292],{"emptyLinePlaceholder":291},[278,41540,41541],{"class":280,"line":625},[278,41542,41543],{},"              \u003Cdiv className='card-row'>\n",[278,41545,41546],{"class":280,"line":640},[278,41547,41548],{},"                \u003Cdiv className='details money-in'>\n",[278,41550,41551],{"class":280,"line":663},[278,41552,41553],{},"                  \u003Cdiv className='details-label'>Total money in\u003C\u002Fdiv>\n",[278,41555,41556],{"class":280,"line":669},[278,41557,41558],{},"                  \u003Cdiv className='details-value'>\n",[278,41560,41561],{"class":280,"line":680},[278,41562,41563],{},"                    {formatCurrency(user.currMonth.in)}\n",[278,41565,41566],{"class":280,"line":686},[278,41567,41568],{},"                  \u003C\u002Fdiv>\n",[278,41570,41571],{"class":280,"line":1334},[278,41572,41529],{},[278,41574,41575],{"class":280,"line":1375},[278,41576,41577],{},"                \u003Cdiv className='details money-out'>\n",[278,41579,41580],{"class":280,"line":1381},[278,41581,41582],{},"                  \u003Cdiv className='details-label'>Total money out\u003C\u002Fdiv>\n",[278,41584,41585],{"class":280,"line":1386},[278,41586,41558],{},[278,41588,41589],{"class":280,"line":1394},[278,41590,41591],{},"                    {formatCurrency(user.currMonth.out)}\n",[278,41593,41594],{"class":280,"line":1406},[278,41595,41568],{},[278,41597,41598],{"class":280,"line":1423},[278,41599,41529],{},[278,41601,41602],{"class":280,"line":1432},[278,41603,41534],{},[278,41605,41606],{"class":280,"line":1437},[278,41607,41608],{},"            \u003C\u002Fdiv>\n",[278,41610,41611],{"class":280,"line":1916},[278,41612,41613],{},"          )}\n",[278,41615,41616],{"class":280,"line":1939},[278,41617,292],{"emptyLinePlaceholder":291},[278,41619,41620],{"class":280,"line":1949},[278,41621,41622],{},"          \u003Ch3 className='transactions-title'>Transactions\u003C\u002Fh3>\n",[278,41624,41625],{"class":280,"line":1954},[278,41626,292],{"emptyLinePlaceholder":291},[278,41628,41629],{"class":280,"line":1959},[278,41630,41631],{},"          {transactions.map((transaction) => {\n",[278,41633,41634],{"class":280,"line":1985},[278,41635,37684],{},[278,41637,41638],{"class":280,"line":1990},[278,41639,41640],{},"              \u003Cdiv\n",[278,41642,41643],{"class":280,"line":1997},[278,41644,41645],{},"                key={transaction._id}\n",[278,41647,41648],{"class":280,"line":2006},[278,41649,41650],{},"                className={`card transaction-card ${\n",[278,41652,41653],{"class":280,"line":2018},[278,41654,41655],{},"                  transaction.type === 'IN'\n",[278,41657,41658],{"class":280,"line":2029},[278,41659,41660],{},"                    ? 'transaction-in'\n",[278,41662,41663],{"class":280,"line":2034},[278,41664,41665],{},"                    : 'transaction-out'\n",[278,41667,41668],{"class":280,"line":2040},[278,41669,41670],{},"                }`}\n",[278,41672,41673],{"class":280,"line":2045},[278,41674,37704],{},[278,41676,41677],{"class":280,"line":2068},[278,41678,41679],{},"                \u003Cdiv>\n",[278,41681,41682],{"class":280,"line":2099},[278,41683,41684],{},"                  \u003Cdiv>{transaction.comment}\u003C\u002Fdiv>\n",[278,41686,41687],{"class":280,"line":6428},[278,41688,41689],{},"                  \u003Cdiv className='transaction-date'>\n",[278,41691,41692],{"class":280,"line":6439},[278,41693,41694],{},"                    {formatDateTime(transaction.createdAt)}\n",[278,41696,41697],{"class":280,"line":6450},[278,41698,41568],{},[278,41700,41701],{"class":280,"line":6455},[278,41702,41529],{},[278,41704,41705],{"class":280,"line":6460},[278,41706,41707],{},"                \u003Cdiv className='transaction-value'>\n",[278,41709,41710],{"class":280,"line":6475},[278,41711,41712],{},"                  {formatCurrency(transaction.amount)}\n",[278,41714,41715],{"class":280,"line":6486},[278,41716,41529],{},[278,41718,41719],{"class":280,"line":6491},[278,41720,41534],{},[278,41722,41723],{"class":280,"line":6518},[278,41724,14244],{},[278,41726,41727],{"class":280,"line":6530},[278,41728,37723],{},[278,41730,41731],{"class":280,"line":6542},[278,41732,27745],{},[278,41734,41735],{"class":280,"line":6547},[278,41736,41737],{},"      ) : (\n",[278,41739,41740],{"class":280,"line":6552},[278,41741,41742],{},"        \u003Cdiv className='no-data'>\n",[278,41744,41745],{"class":280,"line":6567},[278,41746,41747],{},"          \u003Cimg\n",[278,41749,41750],{"class":280,"line":6580},[278,41751,41752],{},"            className='no-data-img'\n",[278,41754,41755],{"class":280,"line":6593},[278,41756,41757],{},"            src={AddImage}\n",[278,41759,41760],{"class":280,"line":6605},[278,41761,41762],{},"            alt='No transactions found, add one'\n",[278,41764,41765],{"class":280,"line":6620},[278,41766,41767],{},"          \u002F>\n",[278,41769,41770],{"class":280,"line":6625},[278,41771,41772],{},"          \u003Cdiv className='no-data-text'>No transactions found\u003C\u002Fdiv>\n",[278,41774,41775],{"class":280,"line":6633},[278,41776,41777],{},"          \u003Cbutton\n",[278,41779,41780],{"class":280,"line":6643},[278,41781,41782],{},"            type='button'\n",[278,41784,41785],{"class":280,"line":6657},[278,41786,41787],{},"            className='btn btn-primary'\n",[278,41789,41790],{"class":280,"line":6665},[278,41791,41792],{},"            onClick={() => navigate('\u002Fnew')}\n",[278,41794,41795],{"class":280,"line":6670},[278,41796,41797],{},"          >\n",[278,41799,41800],{"class":280,"line":6675},[278,41801,41802],{},"            \u003CFaPlusCircle \u002F> Add Transaction\n",[278,41804,41805],{"class":280,"line":6680},[278,41806,41807],{},"          \u003C\u002Fbutton>\n",[278,41809,41810],{"class":280,"line":6698},[278,41811,27745],{},[278,41813,41814],{"class":280,"line":6725},[278,41815,41816],{},"      )}\n",[278,41818,41819],{"class":280,"line":6738},[278,41820,7950],{},[278,41822,41823],{"class":280,"line":6752},[278,41824,611],{},[278,41826,41827],{"class":280,"line":6769},[278,41828,2817],{},[11,41830,41831],{},"Without anything to show, the page looks like the below screenshot",[11,41833,41834],{},[3135,41835],{"alt":41836,"src":41837},"dashboard page without any data","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F2a4e7af6-2aa0-4a7d-bd2d-b2a0ec7318b0-aace308e8d.png",[32,41839,41841],{"id":41840},"newtransaction-page","NewTransaction Page",[11,41843,41844],{},"Here we create a form to submit a new transaction to the database. Right now it is just the barebones file, we'll add the backend interaction later on",[269,41846,41848],{"className":24597,"code":41847,"language":24599,"meta":274,"style":274},"import { useState } from 'react';\nimport { useNavigate } from 'react-router-dom';\n\nconst INITIAL_STATE = {\n  comment: '',\n  amount: '',\n  type: '',\n};\n\nconst TRANSACTION_TYPES = {\n  SELECT: 'Select a type',\n  IN: 'Add',\n  OUT: 'Deduct',\n};\n\nexport const NewTransaction = () => {\n  const [formState, setFormState] = useState(INITIAL_STATE);\n  const [loading, setLoading] = useState(false);\n  const [message, setMessage] = useState(null);\n\n  const navigate = useNavigate();\n\n  const setInput = (key, value) => {\n    setFormState({ ...formState, [key]: value });\n  };\n\n  useEffect(() => {\n    if (message) {\n      const timerId = setTimeout(() => {\n        if (message.type === 'success') {\n          navigate('\u002F', { replace: true });\n        }\n        setMessage(null);\n      }, 2000);\n\n      return () => clearTimeout(timerId);\n    }\n  }, [message, navigate]);\n\n  const onSubmit = async (e) => {\n    e.preventDefault();\n\n    const amount = parseFloat(formState.amount);\n    const comment = formState.comment.trim();\n\n    if (!amount || !comment || !formState.type) {\n      alert('Please fill in all fields');\n      return;\n    }\n\n    try {\n      console.log('final transaction data', {\n        ...formState,\n        createdAt: new Date(),\n      });\n    } catch (error) {\n      console.log('failed to save the transaction');\n    }\n  };\n\n  return (\n    \u003Cdiv className='container'>\n      \u003Cdiv className='card transaction-form'>\n        \u003Ch2>Add transaction details\u003C\u002Fh2>\n        \u003Cform onSubmit={onSubmit}>\n          \u003Cdiv className='form-group'>\n            \u003Clabel htmlFor='name'>Transaction amount\u003C\u002Flabel>\n            \u003Cinput\n              type='number'\n              name='amount'\n              id='amount'\n              placeholder='Enter the amount'\n              value={formState.amount}\n              onChange={(e) => setInput('amount', e.target.value)}\n            \u002F>\n          \u003C\u002Fdiv>\n          \u003Cdiv className='form-group'>\n            \u003Clabel htmlFor='comment'>Transaction comment\u003C\u002Flabel>\n            \u003Cinput\n              type='text'\n              name='comment'\n              id='comment'\n              placeholder='Transaction comment'\n              value={formState.comment}\n              onChange={(e) => setInput('comment', e.target.value)}\n            \u002F>\n          \u003C\u002Fdiv>\n          \u003Cdiv className='form-group'>\n            \u003Clabel htmlFor='transaction-type'>Transaction type\u003C\u002Flabel>\n            \u003Cselect\n              name='transaction-type'\n              id='transaction-type'\n              value={formState.type}\n              onChange={(e) => setInput('type', e.target.value)}\n            >\n              {Object.keys(TRANSACTION_TYPES).map((type) => {\n                return (\n                  \u003Coption key={`type-${type}`} value={type}>\n                    {TRANSACTION_TYPES[type]}\n                  \u003C\u002Foption>\n                );\n              })}\n            \u003C\u002Fselect>\n          \u003C\u002Fdiv>\n\n          {message && (\n            \u003Cdiv className={`message-${message.type}`}>{message.text}\u003C\u002Fdiv>\n          )}\n\n          \u003Cdiv className='card-row'>\n            \u003Cbutton\n              className='btn btn-outlined'\n              disabled={loading}\n              type='button'\n              onClick={() => setFormState(INITIAL_STATE)}\n            >\n              Cancel\n            \u003C\u002Fbutton>\n            \u003Cbutton \n              className='btn btn-primary'\n              disabled={loading}\n              type='submit'\n            >\n              Save\n            \u003C\u002Fbutton>\n          \u003C\u002Fdiv>\n        \u003C\u002Fform>\n      \u003C\u002Fdiv>\n    \u003C\u002Fdiv>\n  );\n};\n",[59,41849,41850,41855,41859,41863,41868,41873,41878,41883,41887,41891,41896,41901,41906,41911,41915,41919,41924,41929,41933,41938,41942,41946,41950,41955,41960,41964,41968,41973,41978,41983,41988,41993,41997,42002,42007,42011,42016,42020,42025,42029,42034,42039,42043,42048,42053,42057,42062,42067,42071,42075,42079,42083,42088,42093,42098,42102,42107,42112,42116,42120,42124,42128,42132,42137,42142,42147,42152,42157,42162,42167,42172,42177,42182,42187,42192,42196,42201,42205,42210,42214,42219,42224,42229,42234,42239,42244,42248,42252,42256,42261,42266,42271,42276,42281,42286,42290,42295,42300,42305,42310,42315,42320,42325,42330,42334,42338,42343,42348,42352,42356,42361,42365,42370,42375,42380,42385,42389,42394,42398,42403,42408,42412,42417,42421,42426,42430,42434,42439,42443,42447,42451],{"__ignoreMap":274},[278,41851,41852],{"class":280,"line":281},[278,41853,41854],{},"import { useState } from 'react';\n",[278,41856,41857],{"class":280,"line":288},[278,41858,41401],{},[278,41860,41861],{"class":280,"line":295},[278,41862,292],{"emptyLinePlaceholder":291},[278,41864,41865],{"class":280,"line":316},[278,41866,41867],{},"const INITIAL_STATE = {\n",[278,41869,41870],{"class":280,"line":322},[278,41871,41872],{},"  comment: '',\n",[278,41874,41875],{"class":280,"line":327},[278,41876,41877],{},"  amount: '',\n",[278,41879,41880],{"class":280,"line":340},[278,41881,41882],{},"  type: '',\n",[278,41884,41885],{"class":280,"line":349},[278,41886,2817],{},[278,41888,41889],{"class":280,"line":375},[278,41890,292],{"emptyLinePlaceholder":291},[278,41892,41893],{"class":280,"line":386},[278,41894,41895],{},"const TRANSACTION_TYPES = {\n",[278,41897,41898],{"class":280,"line":397},[278,41899,41900],{},"  SELECT: 'Select a type',\n",[278,41902,41903],{"class":280,"line":408},[278,41904,41905],{},"  IN: 'Add',\n",[278,41907,41908],{"class":280,"line":433},[278,41909,41910],{},"  OUT: 'Deduct',\n",[278,41912,41913],{"class":280,"line":454},[278,41914,2817],{},[278,41916,41917],{"class":280,"line":475},[278,41918,292],{"emptyLinePlaceholder":291},[278,41920,41921],{"class":280,"line":496},[278,41922,41923],{},"export const NewTransaction = () => {\n",[278,41925,41926],{"class":280,"line":505},[278,41927,41928],{},"  const [formState, setFormState] = useState(INITIAL_STATE);\n",[278,41930,41931],{"class":280,"line":516},[278,41932,41443],{},[278,41934,41935],{"class":280,"line":527},[278,41936,41937],{},"  const [message, setMessage] = useState(null);\n",[278,41939,41940],{"class":280,"line":533},[278,41941,292],{"emptyLinePlaceholder":291},[278,41943,41944],{"class":280,"line":539},[278,41945,41452],{},[278,41947,41948],{"class":280,"line":545},[278,41949,292],{"emptyLinePlaceholder":291},[278,41951,41952],{"class":280,"line":551},[278,41953,41954],{},"  const setInput = (key, value) => {\n",[278,41956,41957],{"class":280,"line":557},[278,41958,41959],{},"    setFormState({ ...formState, [key]: value });\n",[278,41961,41962],{"class":280,"line":567},[278,41963,901],{},[278,41965,41966],{"class":280,"line":577},[278,41967,292],{"emptyLinePlaceholder":291},[278,41969,41970],{"class":280,"line":587},[278,41971,41972],{},"  useEffect(() => {\n",[278,41974,41975],{"class":280,"line":597},[278,41976,41977],{},"    if (message) {\n",[278,41979,41980],{"class":280,"line":608},[278,41981,41982],{},"      const timerId = setTimeout(() => {\n",[278,41984,41985],{"class":280,"line":614},[278,41986,41987],{},"        if (message.type === 'success') {\n",[278,41989,41990],{"class":280,"line":620},[278,41991,41992],{},"          navigate('\u002F', { replace: true });\n",[278,41994,41995],{"class":280,"line":625},[278,41996,6954],{},[278,41998,41999],{"class":280,"line":640},[278,42000,42001],{},"        setMessage(null);\n",[278,42003,42004],{"class":280,"line":663},[278,42005,42006],{},"      }, 2000);\n",[278,42008,42009],{"class":280,"line":669},[278,42010,292],{"emptyLinePlaceholder":291},[278,42012,42013],{"class":280,"line":680},[278,42014,42015],{},"      return () => clearTimeout(timerId);\n",[278,42017,42018],{"class":280,"line":686},[278,42019,1285],{},[278,42021,42022],{"class":280,"line":1334},[278,42023,42024],{},"  }, [message, navigate]);\n",[278,42026,42027],{"class":280,"line":1375},[278,42028,292],{"emptyLinePlaceholder":291},[278,42030,42031],{"class":280,"line":1381},[278,42032,42033],{},"  const onSubmit = async (e) => {\n",[278,42035,42036],{"class":280,"line":1386},[278,42037,42038],{},"    e.preventDefault();\n",[278,42040,42041],{"class":280,"line":1394},[278,42042,292],{"emptyLinePlaceholder":291},[278,42044,42045],{"class":280,"line":1406},[278,42046,42047],{},"    const amount = parseFloat(formState.amount);\n",[278,42049,42050],{"class":280,"line":1423},[278,42051,42052],{},"    const comment = formState.comment.trim();\n",[278,42054,42055],{"class":280,"line":1432},[278,42056,292],{"emptyLinePlaceholder":291},[278,42058,42059],{"class":280,"line":1437},[278,42060,42061],{},"    if (!amount || !comment || !formState.type) {\n",[278,42063,42064],{"class":280,"line":1916},[278,42065,42066],{},"      alert('Please fill in all fields');\n",[278,42068,42069],{"class":280,"line":1939},[278,42070,38526],{},[278,42072,42073],{"class":280,"line":1949},[278,42074,1285],{},[278,42076,42077],{"class":280,"line":1954},[278,42078,292],{"emptyLinePlaceholder":291},[278,42080,42081],{"class":280,"line":1959},[278,42082,8279],{},[278,42084,42085],{"class":280,"line":1985},[278,42086,42087],{},"      console.log('final transaction data', {\n",[278,42089,42090],{"class":280,"line":1990},[278,42091,42092],{},"        ...formState,\n",[278,42094,42095],{"class":280,"line":1997},[278,42096,42097],{},"        createdAt: new Date(),\n",[278,42099,42100],{"class":280,"line":2006},[278,42101,5148],{},[278,42103,42104],{"class":280,"line":2018},[278,42105,42106],{},"    } catch (error) {\n",[278,42108,42109],{"class":280,"line":2029},[278,42110,42111],{},"      console.log('failed to save the transaction');\n",[278,42113,42114],{"class":280,"line":2034},[278,42115,1285],{},[278,42117,42118],{"class":280,"line":2040},[278,42119,901],{},[278,42121,42122],{"class":280,"line":2045},[278,42123,292],{"emptyLinePlaceholder":291},[278,42125,42126],{"class":280,"line":2068},[278,42127,37650],{},[278,42129,42130],{"class":280,"line":2099},[278,42131,41465],{},[278,42133,42134],{"class":280,"line":6428},[278,42135,42136],{},"      \u003Cdiv className='card transaction-form'>\n",[278,42138,42139],{"class":280,"line":6439},[278,42140,42141],{},"        \u003Ch2>Add transaction details\u003C\u002Fh2>\n",[278,42143,42144],{"class":280,"line":6450},[278,42145,42146],{},"        \u003Cform onSubmit={onSubmit}>\n",[278,42148,42149],{"class":280,"line":6455},[278,42150,42151],{},"          \u003Cdiv className='form-group'>\n",[278,42153,42154],{"class":280,"line":6460},[278,42155,42156],{},"            \u003Clabel htmlFor='name'>Transaction amount\u003C\u002Flabel>\n",[278,42158,42159],{"class":280,"line":6475},[278,42160,42161],{},"            \u003Cinput\n",[278,42163,42164],{"class":280,"line":6486},[278,42165,42166],{},"              type='number'\n",[278,42168,42169],{"class":280,"line":6491},[278,42170,42171],{},"              name='amount'\n",[278,42173,42174],{"class":280,"line":6518},[278,42175,42176],{},"              id='amount'\n",[278,42178,42179],{"class":280,"line":6530},[278,42180,42181],{},"              placeholder='Enter the amount'\n",[278,42183,42184],{"class":280,"line":6542},[278,42185,42186],{},"              value={formState.amount}\n",[278,42188,42189],{"class":280,"line":6547},[278,42190,42191],{},"              onChange={(e) => setInput('amount', e.target.value)}\n",[278,42193,42194],{"class":280,"line":6552},[278,42195,554],{},[278,42197,42198],{"class":280,"line":6567},[278,42199,42200],{},"          \u003C\u002Fdiv>\n",[278,42202,42203],{"class":280,"line":6580},[278,42204,42151],{},[278,42206,42207],{"class":280,"line":6593},[278,42208,42209],{},"            \u003Clabel htmlFor='comment'>Transaction comment\u003C\u002Flabel>\n",[278,42211,42212],{"class":280,"line":6605},[278,42213,42161],{},[278,42215,42216],{"class":280,"line":6620},[278,42217,42218],{},"              type='text'\n",[278,42220,42221],{"class":280,"line":6625},[278,42222,42223],{},"              name='comment'\n",[278,42225,42226],{"class":280,"line":6633},[278,42227,42228],{},"              id='comment'\n",[278,42230,42231],{"class":280,"line":6643},[278,42232,42233],{},"              placeholder='Transaction comment'\n",[278,42235,42236],{"class":280,"line":6657},[278,42237,42238],{},"              value={formState.comment}\n",[278,42240,42241],{"class":280,"line":6665},[278,42242,42243],{},"              onChange={(e) => setInput('comment', e.target.value)}\n",[278,42245,42246],{"class":280,"line":6670},[278,42247,554],{},[278,42249,42250],{"class":280,"line":6675},[278,42251,42200],{},[278,42253,42254],{"class":280,"line":6680},[278,42255,42151],{},[278,42257,42258],{"class":280,"line":6698},[278,42259,42260],{},"            \u003Clabel htmlFor='transaction-type'>Transaction type\u003C\u002Flabel>\n",[278,42262,42263],{"class":280,"line":6725},[278,42264,42265],{},"            \u003Cselect\n",[278,42267,42268],{"class":280,"line":6738},[278,42269,42270],{},"              name='transaction-type'\n",[278,42272,42273],{"class":280,"line":6752},[278,42274,42275],{},"              id='transaction-type'\n",[278,42277,42278],{"class":280,"line":6769},[278,42279,42280],{},"              value={formState.type}\n",[278,42282,42283],{"class":280,"line":6786},[278,42284,42285],{},"              onChange={(e) => setInput('type', e.target.value)}\n",[278,42287,42288],{"class":280,"line":6798},[278,42289,38018],{},[278,42291,42292],{"class":280,"line":6803},[278,42293,42294],{},"              {Object.keys(TRANSACTION_TYPES).map((type) => {\n",[278,42296,42297],{"class":280,"line":6815},[278,42298,42299],{},"                return (\n",[278,42301,42302],{"class":280,"line":6827},[278,42303,42304],{},"                  \u003Coption key={`type-${type}`} value={type}>\n",[278,42306,42307],{"class":280,"line":6839},[278,42308,42309],{},"                    {TRANSACTION_TYPES[type]}\n",[278,42311,42312],{"class":280,"line":6844},[278,42313,42314],{},"                  \u003C\u002Foption>\n",[278,42316,42317],{"class":280,"line":6853},[278,42318,42319],{},"                );\n",[278,42321,42322],{"class":280,"line":6859},[278,42323,42324],{},"              })}\n",[278,42326,42327],{"class":280,"line":6864},[278,42328,42329],{},"            \u003C\u002Fselect>\n",[278,42331,42332],{"class":280,"line":6877},[278,42333,42200],{},[278,42335,42336],{"class":280,"line":6887},[278,42337,292],{"emptyLinePlaceholder":291},[278,42339,42340],{"class":280,"line":6918},[278,42341,42342],{},"          {message && (\n",[278,42344,42345],{"class":280,"line":6923},[278,42346,42347],{},"            \u003Cdiv className={`message-${message.type}`}>{message.text}\u003C\u002Fdiv>\n",[278,42349,42350],{"class":280,"line":6931},[278,42351,41613],{},[278,42353,42354],{"class":280,"line":6939},[278,42355,292],{"emptyLinePlaceholder":291},[278,42357,42358],{"class":280,"line":6951},[278,42359,42360],{},"          \u003Cdiv className='card-row'>\n",[278,42362,42363],{"class":280,"line":6957},[278,42364,37988],{},[278,42366,42367],{"class":280,"line":6962},[278,42368,42369],{},"              className='btn btn-outlined'\n",[278,42371,42372],{"class":280,"line":6973},[278,42373,42374],{},"              disabled={loading}\n",[278,42376,42377],{"class":280,"line":6985},[278,42378,42379],{},"              type='button'\n",[278,42381,42382],{"class":280,"line":6990},[278,42383,42384],{},"              onClick={() => setFormState(INITIAL_STATE)}\n",[278,42386,42387],{"class":280,"line":6995},[278,42388,38018],{},[278,42390,42391],{"class":280,"line":7000},[278,42392,42393],{},"              Cancel\n",[278,42395,42396],{"class":280,"line":7005},[278,42397,38028],{},[278,42399,42400],{"class":280,"line":7017},[278,42401,42402],{},"            \u003Cbutton \n",[278,42404,42405],{"class":280,"line":7025},[278,42406,42407],{},"              className='btn btn-primary'\n",[278,42409,42410],{"class":280,"line":7030},[278,42411,42374],{},[278,42413,42414],{"class":280,"line":7035},[278,42415,42416],{},"              type='submit'\n",[278,42418,42419],{"class":280,"line":7042},[278,42420,38018],{},[278,42422,42423],{"class":280,"line":7054},[278,42424,42425],{},"              Save\n",[278,42427,42428],{"class":280,"line":7060},[278,42429,38028],{},[278,42431,42432],{"class":280,"line":7066},[278,42433,42200],{},[278,42435,42436],{"class":280,"line":7071},[278,42437,42438],{},"        \u003C\u002Fform>\n",[278,42440,42441],{"class":280,"line":8344},[278,42442,7873],{},[278,42444,42445],{"class":280,"line":8350},[278,42446,7950],{},[278,42448,42449],{"class":280,"line":8356},[278,42450,611],{},[278,42452,42453],{"class":280,"line":8362},[278,42454,2817],{},[11,42456,42457],{},"This is how the page looks",[11,42459,42460],{},[3135,42461],{"alt":42462,"src":42463},"new transaction page","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fba5aed05-55c2-4ff9-bd46-646b58efb566-b9a7a30090.png",[32,42465,42467],{"id":42466},"utility-functions","Utility functions",[11,42469,42470,42471,263,42474,42476],{},"Create a new folder called ",[59,42472,42473],{},"utils",[59,42475,24951],{}," directory, and add an index.js file inside it. Then add the following utility functions for formatting dates and currency in it. You can change the currency code to your desired currency, or even make it configurable per user.",[269,42478,42480],{"className":24597,"code":42479,"language":24599,"meta":274,"style":274},"let dateTimeFormatter;\nlet currencyFormatter;\n\nexport const formatDateTime = (dateString) => {\n  if (!dateString) {\n    return '';\n  }\n\n  if (!dateTimeFormatter) {\n    dateTimeFormatter = new Intl.DateTimeFormat('en-US', {\n      year: 'numeric',\n      month: 'short',\n      day: 'numeric',\n      hour: 'numeric',\n      minute: 'numeric',\n    });\n  }\n\n  return dateTimeFormatter.format(new Date(dateString));\n};\n\nexport const formatCurrency = (amount) => {\n  if (!currencyFormatter) {\n    currencyFormatter = new Intl.NumberFormat('en-US', {\n      style: 'currency',\n      currency: 'INR',\n      maximumFractionDigits: 2,\n    });\n  }\n\n  return currencyFormatter.format(amount);\n};\n",[59,42481,42482,42487,42492,42496,42501,42506,42511,42515,42519,42524,42529,42534,42539,42544,42549,42554,42558,42562,42566,42571,42575,42579,42584,42589,42594,42599,42604,42609,42613,42617,42621,42626],{"__ignoreMap":274},[278,42483,42484],{"class":280,"line":281},[278,42485,42486],{},"let dateTimeFormatter;\n",[278,42488,42489],{"class":280,"line":288},[278,42490,42491],{},"let currencyFormatter;\n",[278,42493,42494],{"class":280,"line":295},[278,42495,292],{"emptyLinePlaceholder":291},[278,42497,42498],{"class":280,"line":316},[278,42499,42500],{},"export const formatDateTime = (dateString) => {\n",[278,42502,42503],{"class":280,"line":322},[278,42504,42505],{},"  if (!dateString) {\n",[278,42507,42508],{"class":280,"line":327},[278,42509,42510],{},"    return '';\n",[278,42512,42513],{"class":280,"line":340},[278,42514,1096],{},[278,42516,42517],{"class":280,"line":349},[278,42518,292],{"emptyLinePlaceholder":291},[278,42520,42521],{"class":280,"line":375},[278,42522,42523],{},"  if (!dateTimeFormatter) {\n",[278,42525,42526],{"class":280,"line":386},[278,42527,42528],{},"    dateTimeFormatter = new Intl.DateTimeFormat('en-US', {\n",[278,42530,42531],{"class":280,"line":397},[278,42532,42533],{},"      year: 'numeric',\n",[278,42535,42536],{"class":280,"line":408},[278,42537,42538],{},"      month: 'short',\n",[278,42540,42541],{"class":280,"line":433},[278,42542,42543],{},"      day: 'numeric',\n",[278,42545,42546],{"class":280,"line":454},[278,42547,42548],{},"      hour: 'numeric',\n",[278,42550,42551],{"class":280,"line":475},[278,42552,42553],{},"      minute: 'numeric',\n",[278,42555,42556],{"class":280,"line":496},[278,42557,1233],{},[278,42559,42560],{"class":280,"line":505},[278,42561,1096],{},[278,42563,42564],{"class":280,"line":516},[278,42565,292],{"emptyLinePlaceholder":291},[278,42567,42568],{"class":280,"line":527},[278,42569,42570],{},"  return dateTimeFormatter.format(new Date(dateString));\n",[278,42572,42573],{"class":280,"line":533},[278,42574,2817],{},[278,42576,42577],{"class":280,"line":539},[278,42578,292],{"emptyLinePlaceholder":291},[278,42580,42581],{"class":280,"line":545},[278,42582,42583],{},"export const formatCurrency = (amount) => {\n",[278,42585,42586],{"class":280,"line":551},[278,42587,42588],{},"  if (!currencyFormatter) {\n",[278,42590,42591],{"class":280,"line":557},[278,42592,42593],{},"    currencyFormatter = new Intl.NumberFormat('en-US', {\n",[278,42595,42596],{"class":280,"line":567},[278,42597,42598],{},"      style: 'currency',\n",[278,42600,42601],{"class":280,"line":577},[278,42602,42603],{},"      currency: 'INR',\n",[278,42605,42606],{"class":280,"line":587},[278,42607,42608],{},"      maximumFractionDigits: 2,\n",[278,42610,42611],{"class":280,"line":597},[278,42612,1233],{},[278,42614,42615],{"class":280,"line":608},[278,42616,1096],{},[278,42618,42619],{"class":280,"line":614},[278,42620,292],{"emptyLinePlaceholder":291},[278,42622,42623],{"class":280,"line":620},[278,42624,42625],{},"  return currencyFormatter.format(amount);\n",[278,42627,42628],{"class":280,"line":625},[278,42629,2817],{},[11,42631,42632,42633,42636],{},"This is my current project folder structure after making the above changes. I've removed the ",[59,42634,42635],{},"logo.svg"," file from the project.",[11,42638,42639],{},[3135,42640],{"alt":42641,"src":42642},"current project folder structure","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F44a18f14-8630-41d8-b3a8-36fd0012eeeb-9c4d2470c9.png",[24,42644,42646],{"id":42645},"the-backend","The Backend",[11,42648,42649,42650,42655,42656,42659],{},"First of all, we'll create a new cluster on MongoDB Atlas. For our current purposes, an M0 FREE shared cluster is fine. If this is your first time interacting with MongoDB Atlas, you can use ",[47,42651,42654],{"href":42652,"rel":42653},"https:\u002F\u002Fwww.mongodb.com\u002Fdocs\u002Fguides\u002Fatlas\u002Faccount\u002F",[51],"this excellent guide"," to get started (you can follow till the ",[59,42657,42658],{},"\"Configure a Network Connection\""," section).",[32,42661,42663],{"id":42662},"database-and-collections","Database and collections",[11,42665,42666,42667,42670],{},"Now let's create a database, and add ",[59,42668,42669],{},"transactions"," collection to it.",[11,42672,42673],{},[3135,42674],{"alt":42675,"src":42676},"Create database page","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F7e169c3d-8934-4578-8cae-123fb1c2dd88-3a6c92e175.png",[11,42678,42679],{},[3135,42680],{"alt":42681,"src":42682},"create database and collection","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fb53585b0-9725-4a7f-9f6d-ab8dd775d498-7d73ffcea4.png",[11,42684,42685,42686,42689],{},"Add one more collection called ",[59,42687,42688],{},"\"users\""," to the database",[11,42691,42692],{},[3135,42693],{"alt":42694,"src":42695},"create users collection","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F609d9579-44df-45a1-8533-2b7bdcf0e33a-4003272ac3.png",[32,42697,42699],{"id":42698},"atlas-app-services","Atlas App Services",[11,42701,39872,42702,42705,42706,42709,42710,42713],{},[59,42703,42704],{},"App Services"," tab and create a new Atlas App Services application. Click next in the ",[59,42707,42708],{},"Start with an app template"," screen (while ",[59,42711,42712],{},"\"Build your own App\""," is selected, the default).",[11,42715,42716],{},[3135,42717],{"alt":42718,"src":42719},"Create Atlas App Services app","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fffc73221-92d5-42b7-b237-beeb76e7c798-1e0b4ce09e.png",[11,42721,42722,42723,42726],{},"Give a name to your application, and click on the ",[59,42724,42725],{},"\"Create App Service\""," button.",[11,42728,42729],{},[3135,42730],{"alt":42731,"src":42732},"configure Atlas App Services App","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F34a74faf-fe76-4945-b9a9-b9ffbbab55f9-a868d71881.png",[32,42734,42736],{"id":42735},"authentication","Authentication",[11,42738,42739],{},"Click on Authentication from the left sidebar and enable anonymous auth and save the draft.",[11,42741,42742],{},[3135,42743],{"alt":42744,"src":42745},"enable anonymous auth","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F15cc1f98-8c90-4b77-9dfb-c7b3919bde54-6cd3025afd.png",[11,42747,42748,42749,42752,42753],{},"After making any changes in the App Services application, we need to deploy the changes for it to take effect. Click on ",[59,42750,42751],{},"REVIEW DRAFT & DEPLOY"," button and deploy the change",[59,42754,183],{},[11,42756,42757],{},[3135,42758],{"alt":42759,"src":42760},"deploy changes","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F1e16a637-f948-4f87-b7bd-a710a034b83f-55f8313043.png",[32,42762,42764],{"id":42763},"creating-the-auth-trigger","Creating the Auth Trigger",[11,42766,42767,42768,42770],{},"Now we're ready to create our first trigger. Whenever a user signs up for our application (even if anonymously), we will create a corresponding document in the ",[59,42769,42688],{}," collection in the database.",[11,42772,42773,42774,42777,42778,42726],{},"Click on triggers from the left sidebar, select ",[59,42775,42776],{},"Authentication Triggers"," from the dropdown menu on the top left, and click on ",[59,42779,42780],{},"Add an Authentication Trigger",[11,42782,42783],{},[3135,42784],{"alt":42785,"src":42786},"create auth trigger","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F43444c56-9d30-4e5a-8821-bcafc8855869-b20d66db95.png",[11,42788,42789,42790,42793,42794,42797,42798,42801,42802,42805,42806,42808,42809,42812],{},"For ",[59,42791,42792],{},"Action Type,"," choose ",[59,42795,42796],{},"Create",", from ",[59,42799,42800],{},"Providers"," dropdown pick ",[59,42803,42804],{},"Anonymous",", and select ",[59,42807,330],{}," as the ",[59,42810,42811],{},"Event Type",". Doing this allows us to automatically trigger a function whenever a new user signs up for our application.",[11,42814,42815],{},[3135,42816],{"alt":42817,"src":42818},"Configure auth trigger","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F4c61e0ff-29c5-499c-a60f-8476a7665515-f40965d8ed.png",[11,42820,42821,42822,42825,42826,42828],{},"Add the following code in the code panel on the same screen. Don't forget to replace the ",[59,42823,42824],{},"\u003Cdb_name>"," with your database name. What we do here is, from the incoming auth event get the auth user id, and create an entry in the ",[59,42827,42688],{}," collection with some default values.",[269,42830,42832],{"className":24597,"code":42831,"language":24599,"meta":274,"style":274},"exports = async function(authEvent) {\n  const { user, time } = authEvent;\n\n  const mongoDb = context.services.get('mongodb-atlas').db('\u003Cdb_name>');\n  const usersCollection = mongoDb.collection('users');\n\n  const userData = {\n    _id: BSON.ObjectId(user.id),\n    balance: 0,\n    currMonth: {\n      in: 0,\n      out: 0,\n    },\n    createdAt: time, \n    updatedAt: time,\n  };\n\n  const res = await usersCollection.insertOne(userData);\n  console.log('result of user insert op: ', JSON.stringify(res));\n};\n",[59,42833,42834,42839,42844,42848,42853,42858,42862,42867,42872,42877,42882,42887,42892,42896,42901,42906,42910,42914,42919,42924],{"__ignoreMap":274},[278,42835,42836],{"class":280,"line":281},[278,42837,42838],{},"exports = async function(authEvent) {\n",[278,42840,42841],{"class":280,"line":288},[278,42842,42843],{},"  const { user, time } = authEvent;\n",[278,42845,42846],{"class":280,"line":295},[278,42847,292],{"emptyLinePlaceholder":291},[278,42849,42850],{"class":280,"line":316},[278,42851,42852],{},"  const mongoDb = context.services.get('mongodb-atlas').db('\u003Cdb_name>');\n",[278,42854,42855],{"class":280,"line":322},[278,42856,42857],{},"  const usersCollection = mongoDb.collection('users');\n",[278,42859,42860],{"class":280,"line":327},[278,42861,292],{"emptyLinePlaceholder":291},[278,42863,42864],{"class":280,"line":340},[278,42865,42866],{},"  const userData = {\n",[278,42868,42869],{"class":280,"line":349},[278,42870,42871],{},"    _id: BSON.ObjectId(user.id),\n",[278,42873,42874],{"class":280,"line":375},[278,42875,42876],{},"    balance: 0,\n",[278,42878,42879],{"class":280,"line":386},[278,42880,42881],{},"    currMonth: {\n",[278,42883,42884],{"class":280,"line":397},[278,42885,42886],{},"      in: 0,\n",[278,42888,42889],{"class":280,"line":408},[278,42890,42891],{},"      out: 0,\n",[278,42893,42894],{"class":280,"line":433},[278,42895,2243],{},[278,42897,42898],{"class":280,"line":454},[278,42899,42900],{},"    createdAt: time, \n",[278,42902,42903],{"class":280,"line":475},[278,42904,42905],{},"    updatedAt: time,\n",[278,42907,42908],{"class":280,"line":496},[278,42909,901],{},[278,42911,42912],{"class":280,"line":505},[278,42913,292],{"emptyLinePlaceholder":291},[278,42915,42916],{"class":280,"line":516},[278,42917,42918],{},"  const res = await usersCollection.insertOne(userData);\n",[278,42920,42921],{"class":280,"line":527},[278,42922,42923],{},"  console.log('result of user insert op: ', JSON.stringify(res));\n",[278,42925,42926],{"class":280,"line":533},[278,42927,2817],{},[11,42929,42930,42931,42934],{},"Afterwards, deploy your changes for them to take effect. Should you need to make any changes to the function code, you can do so by clicking the ",[59,42932,42933],{},"Functions"," menu item from the left sidebar and then click on your function name.",[32,42936,42938],{"id":42937},"testing-the-auth-trigger","Testing the Auth Trigger",[11,42940,42941,42942,42945,42946,183],{},"Now we're ready to test the Auth trigger we created in the last section. Before doing that we'll pull the App Services application to our local machine. It is not mandatory to do so, but having the functions' code on the local machine, makes it easier to make changes. Let's install the ",[59,42943,42944],{},"\"realm-cli\""," and configure it ",[47,42947,42950],{"href":42948,"rel":42949},"https:\u002F\u002Fwww.mongodb.com\u002Fdocs\u002Fatlas\u002Fapp-services\u002Fcli\u002F",[51],"using this guide",[11,42952,42953],{},"Now pull the application code by firing up a terminal, navigate to the project's root directory and run the following command.",[269,42955,42957],{"className":3335,"code":42956,"language":3337,"meta":274,"style":274},"# This will pull the application to the backend \n# folder (the folder will be created automatically)\n# Also, don't forget to use your app_id\nrealm-cli pull --local backend\u002F --remote \u003Capp_id>\n",[59,42958,42959,42964,42969,42974],{"__ignoreMap":274},[278,42960,42961],{"class":280,"line":281},[278,42962,42963],{"class":284},"# This will pull the application to the backend \n",[278,42965,42966],{"class":280,"line":288},[278,42967,42968],{"class":284},"# folder (the folder will be created automatically)\n",[278,42970,42971],{"class":280,"line":295},[278,42972,42973],{"class":284},"# Also, don't forget to use your app_id\n",[278,42975,42976,42979,42982,42985,42988,42991,42993,42996,42999],{"class":280,"line":316},[278,42977,42978],{"class":333},"realm-cli",[278,42980,42981],{"class":309}," pull",[278,42983,42984],{"class":650}," --local",[278,42986,42987],{"class":309}," backend\u002F",[278,42989,42990],{"class":650}," --remote",[278,42992,24568],{"class":298},[278,42994,42995],{"class":309},"app_i",[278,42997,42998],{"class":302},"d",[278,43000,372],{"class":298},[11,43002,43003,43004,43007,43008,43011],{},"Now go to the ",[59,43005,43006],{},"client"," folder, and install the ",[59,43009,43010],{},"realm-web"," SDK.",[269,43013,43015],{"className":3335,"code":43014,"language":3337,"meta":274,"style":274},"# From project root run the following\ncd client && yarn add realm-web\n\n# Make a new file for handling realm auth etc\ntouch src\u002FRealmApp.js\n",[59,43016,43017,43022,43037,43041,43046],{"__ignoreMap":274},[278,43018,43019],{"class":280,"line":281},[278,43020,43021],{"class":284},"# From project root run the following\n",[278,43023,43024,43026,43028,43030,43032,43034],{"class":280,"line":288},[278,43025,3364],{"class":650},[278,43027,26568],{"class":309},[278,43029,3361],{"class":302},[278,43031,33494],{"class":333},[278,43033,3418],{"class":309},[278,43035,43036],{"class":309}," realm-web\n",[278,43038,43039],{"class":280,"line":295},[278,43040,292],{"emptyLinePlaceholder":291},[278,43042,43043],{"class":280,"line":316},[278,43044,43045],{"class":284},"# Make a new file for handling realm auth etc\n",[278,43047,43048,43050],{"class":280,"line":322},[278,43049,41083],{"class":333},[278,43051,43052],{"class":309}," src\u002FRealmApp.js\n",[11,43054,41382,43055,32318],{},[59,43056,43057],{},"RealmApp.js",[269,43059,43061],{"className":24597,"code":43060,"language":24599,"meta":274,"style":274},"import { createContext, useContext, useState, useEffect } from 'react';\nimport * as Realm from 'realm-web';\n\nconst RealmContext = createContext(null);\n\nexport function RealmAppProvider({ appId, children }) {\n  const [realmApp, setRealmApp] = useState(null);\n  const [appDB, setAppDB] = useState(null);\n  const [realmUser, setRealmUser] = useState(null);\n\n  useEffect(() => {\n    setRealmApp(Realm.getApp(appId));\n  }, [appId]);\n\n  useEffect(() => {\n    const init = async () => {\n      if (!realmApp.currentUser) {\n        await realmApp.logIn(Realm.Credentials.anonymous());\n      }\n\n      setRealmUser(realmApp.currentUser);\n      setAppDB(\n        realmApp.currentUser\n          .mongoClient(process.env.REACT_APP_MONGO_SVC_NAME)\n          .db(process.env.REACT_APP_MONGO_DB_NAME)\n      );\n    };\n\n    if (realmApp) {\n      init();\n    }\n  }, [realmApp]);\n\n  return (\n    \u003CRealmContext.Provider value={{ realmUser, appDB }}>\n      {children}\n    \u003C\u002FRealmContext.Provider>\n  );\n}\n\nexport function useRealmApp() {\n  const app = useContext(RealmContext);\n  if (!app) {\n    throw new Error(\n      `No Realm App found. Did you call useRealmApp() inside of a \u003CRealmAppProvider \u002F>.`\n    );\n  }\n\n  return app;\n}\n",[59,43062,43063,43068,43073,43077,43082,43086,43091,43096,43101,43106,43110,43114,43119,43124,43128,43132,43137,43142,43147,43151,43155,43160,43165,43170,43175,43180,43184,43188,43192,43197,43202,43206,43211,43215,43219,43224,43229,43234,43238,43242,43246,43251,43256,43261,43266,43271,43275,43279,43283,43288],{"__ignoreMap":274},[278,43064,43065],{"class":280,"line":281},[278,43066,43067],{},"import { createContext, useContext, useState, useEffect } from 'react';\n",[278,43069,43070],{"class":280,"line":288},[278,43071,43072],{},"import * as Realm from 'realm-web';\n",[278,43074,43075],{"class":280,"line":295},[278,43076,292],{"emptyLinePlaceholder":291},[278,43078,43079],{"class":280,"line":316},[278,43080,43081],{},"const RealmContext = createContext(null);\n",[278,43083,43084],{"class":280,"line":322},[278,43085,292],{"emptyLinePlaceholder":291},[278,43087,43088],{"class":280,"line":327},[278,43089,43090],{},"export function RealmAppProvider({ appId, children }) {\n",[278,43092,43093],{"class":280,"line":340},[278,43094,43095],{},"  const [realmApp, setRealmApp] = useState(null);\n",[278,43097,43098],{"class":280,"line":349},[278,43099,43100],{},"  const [appDB, setAppDB] = useState(null);\n",[278,43102,43103],{"class":280,"line":375},[278,43104,43105],{},"  const [realmUser, setRealmUser] = useState(null);\n",[278,43107,43108],{"class":280,"line":386},[278,43109,292],{"emptyLinePlaceholder":291},[278,43111,43112],{"class":280,"line":397},[278,43113,41972],{},[278,43115,43116],{"class":280,"line":408},[278,43117,43118],{},"    setRealmApp(Realm.getApp(appId));\n",[278,43120,43121],{"class":280,"line":433},[278,43122,43123],{},"  }, [appId]);\n",[278,43125,43126],{"class":280,"line":454},[278,43127,292],{"emptyLinePlaceholder":291},[278,43129,43130],{"class":280,"line":475},[278,43131,41972],{},[278,43133,43134],{"class":280,"line":496},[278,43135,43136],{},"    const init = async () => {\n",[278,43138,43139],{"class":280,"line":505},[278,43140,43141],{},"      if (!realmApp.currentUser) {\n",[278,43143,43144],{"class":280,"line":516},[278,43145,43146],{},"        await realmApp.logIn(Realm.Credentials.anonymous());\n",[278,43148,43149],{"class":280,"line":527},[278,43150,6234],{},[278,43152,43153],{"class":280,"line":533},[278,43154,292],{"emptyLinePlaceholder":291},[278,43156,43157],{"class":280,"line":539},[278,43158,43159],{},"      setRealmUser(realmApp.currentUser);\n",[278,43161,43162],{"class":280,"line":545},[278,43163,43164],{},"      setAppDB(\n",[278,43166,43167],{"class":280,"line":551},[278,43168,43169],{},"        realmApp.currentUser\n",[278,43171,43172],{"class":280,"line":557},[278,43173,43174],{},"          .mongoClient(process.env.REACT_APP_MONGO_SVC_NAME)\n",[278,43176,43177],{"class":280,"line":567},[278,43178,43179],{},"          .db(process.env.REACT_APP_MONGO_DB_NAME)\n",[278,43181,43182],{"class":280,"line":577},[278,43183,2616],{},[278,43185,43186],{"class":280,"line":587},[278,43187,1378],{},[278,43189,43190],{"class":280,"line":597},[278,43191,292],{"emptyLinePlaceholder":291},[278,43193,43194],{"class":280,"line":608},[278,43195,43196],{},"    if (realmApp) {\n",[278,43198,43199],{"class":280,"line":614},[278,43200,43201],{},"      init();\n",[278,43203,43204],{"class":280,"line":620},[278,43205,1285],{},[278,43207,43208],{"class":280,"line":625},[278,43209,43210],{},"  }, [realmApp]);\n",[278,43212,43213],{"class":280,"line":640},[278,43214,292],{"emptyLinePlaceholder":291},[278,43216,43217],{"class":280,"line":663},[278,43218,37650],{},[278,43220,43221],{"class":280,"line":669},[278,43222,43223],{},"    \u003CRealmContext.Provider value={{ realmUser, appDB }}>\n",[278,43225,43226],{"class":280,"line":680},[278,43227,43228],{},"      {children}\n",[278,43230,43231],{"class":280,"line":686},[278,43232,43233],{},"    \u003C\u002FRealmContext.Provider>\n",[278,43235,43236],{"class":280,"line":1334},[278,43237,611],{},[278,43239,43240],{"class":280,"line":1375},[278,43241,617],{},[278,43243,43244],{"class":280,"line":1381},[278,43245,292],{"emptyLinePlaceholder":291},[278,43247,43248],{"class":280,"line":1386},[278,43249,43250],{},"export function useRealmApp() {\n",[278,43252,43253],{"class":280,"line":1394},[278,43254,43255],{},"  const app = useContext(RealmContext);\n",[278,43257,43258],{"class":280,"line":1406},[278,43259,43260],{},"  if (!app) {\n",[278,43262,43263],{"class":280,"line":1423},[278,43264,43265],{},"    throw new Error(\n",[278,43267,43268],{"class":280,"line":1432},[278,43269,43270],{},"      `No Realm App found. Did you call useRealmApp() inside of a \u003CRealmAppProvider \u002F>.`\n",[278,43272,43273],{"class":280,"line":1437},[278,43274,1898],{},[278,43276,43277],{"class":280,"line":1916},[278,43278,1096],{},[278,43280,43281],{"class":280,"line":1939},[278,43282,292],{"emptyLinePlaceholder":291},[278,43284,43285],{"class":280,"line":1949},[278,43286,43287],{},"  return app;\n",[278,43289,43290],{"class":280,"line":1954},[278,43291,617],{},[11,43293,43294,43295,43297],{},"Modify the ",[59,43296,37763],{}," file and add the following changes to it",[269,43299,43301],{"className":24597,"code":43300,"language":24599,"meta":274,"style":274},"\u002F\u002F Add the RealmApp import\nimport { RealmAppProvider } from '.\u002FRealmApp';\n\n\u002F\u002F Wrap the BrowserRouter inside the RealmAppProvider\nfunction App() {\n  return (\n    \u003CRealmAppProvider appId={process.env.REACT_APP_REALM_APP_ID}>\n      \u003CBrowserRouter>\n        \u003CRoutes>\n          \u003CRoute path='\u002F' element={\u003CLayout \u002F>}>\n            \u003CRoute index element={\u003CDashboard \u002F>} \u002F>\n            \u003CRoute path='\u002Fnew' element={\u003CNewTransaction \u002F>} \u002F>\n          \u003C\u002FRoute>\n        \u003C\u002FRoutes>\n      \u003C\u002FBrowserRouter>\n    \u003C\u002FRealmAppProvider>\n  );\n}\n",[59,43302,43303,43308,43313,43317,43322,43326,43330,43335,43340,43345,43350,43355,43360,43365,43370,43375,43380,43384],{"__ignoreMap":274},[278,43304,43305],{"class":280,"line":281},[278,43306,43307],{},"\u002F\u002F Add the RealmApp import\n",[278,43309,43310],{"class":280,"line":288},[278,43311,43312],{},"import { RealmAppProvider } from '.\u002FRealmApp';\n",[278,43314,43315],{"class":280,"line":295},[278,43316,292],{"emptyLinePlaceholder":291},[278,43318,43319],{"class":280,"line":316},[278,43320,43321],{},"\u002F\u002F Wrap the BrowserRouter inside the RealmAppProvider\n",[278,43323,43324],{"class":280,"line":322},[278,43325,37791],{},[278,43327,43328],{"class":280,"line":327},[278,43329,37650],{},[278,43331,43332],{"class":280,"line":340},[278,43333,43334],{},"    \u003CRealmAppProvider appId={process.env.REACT_APP_REALM_APP_ID}>\n",[278,43336,43337],{"class":280,"line":349},[278,43338,43339],{},"      \u003CBrowserRouter>\n",[278,43341,43342],{"class":280,"line":375},[278,43343,43344],{},"        \u003CRoutes>\n",[278,43346,43347],{"class":280,"line":386},[278,43348,43349],{},"          \u003CRoute path='\u002F' element={\u003CLayout \u002F>}>\n",[278,43351,43352],{"class":280,"line":397},[278,43353,43354],{},"            \u003CRoute index element={\u003CDashboard \u002F>} \u002F>\n",[278,43356,43357],{"class":280,"line":408},[278,43358,43359],{},"            \u003CRoute path='\u002Fnew' element={\u003CNewTransaction \u002F>} \u002F>\n",[278,43361,43362],{"class":280,"line":433},[278,43363,43364],{},"          \u003C\u002FRoute>\n",[278,43366,43367],{"class":280,"line":454},[278,43368,43369],{},"        \u003C\u002FRoutes>\n",[278,43371,43372],{"class":280,"line":475},[278,43373,43374],{},"      \u003C\u002FBrowserRouter>\n",[278,43376,43377],{"class":280,"line":496},[278,43378,43379],{},"    \u003C\u002FRealmAppProvider>\n",[278,43381,43382],{"class":280,"line":505},[278,43383,611],{},[278,43385,43386],{"class":280,"line":516},[278,43387,617],{},[11,43389,43390,43391,43393],{},"Create an env file named ",[59,43392,10990],{}," at the root of the react project and add the following keys and their respective values",[269,43395,43399],{"className":43396,"code":43397,"language":43398,"meta":274,"style":274},"language-ini shiki shiki-themes github-light github-dark","REACT_APP_REALM_APP_ID=\u003Crealm_app_id>\nREACT_APP_MONGO_SVC_NAME=mongodb-atlas\nREACT_APP_MONGO_DB_NAME=\u003Cdb_name>\n","ini",[59,43400,43401,43406,43411],{"__ignoreMap":274},[278,43402,43403],{"class":280,"line":281},[278,43404,43405],{},"REACT_APP_REALM_APP_ID=\u003Crealm_app_id>\n",[278,43407,43408],{"class":280,"line":288},[278,43409,43410],{},"REACT_APP_MONGO_SVC_NAME=mongodb-atlas\n",[278,43412,43413],{"class":280,"line":295},[278,43414,43415],{},"REACT_APP_MONGO_DB_NAME=\u003Cdb_name>\n",[11,43417,43418],{},"At this point, the project structure looks like the following",[11,43420,43421],{},[94,43422,43423],{},"The client folder",[11,43425,43426],{},[3135,43427],{"alt":43428,"src":43429},"client folder structure","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Ffb9ab78d-a253-46fe-8f9e-f33571dbcf7d-fccc4edb63.png",[11,43431,43432],{},[94,43433,43434],{},"The backend folder",[11,43436,43437],{},[3135,43438],{"alt":43439,"src":43440},"the backend folder structure","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F825fcf67-d0ab-40f8-b7ea-4b713a46bd28-b25118dc19.png",[11,43442,43443,43444,43446,43447,43450,43451,43454],{},"Restart the dev server for the env values to take effect. As soon as the application loads in your browser, an anonymous user should have been created in the realm app. You can check it by going to your ",[59,43445,42704],{}," dashboard, and clicking ",[59,43448,43449],{},"App Users"," from the left sidebar menu. To view the function logs, we can click on ",[59,43452,43453],{},"\"Logs\""," from the same sidebar menu.",[11,43456,43457],{},[3135,43458],{"alt":43459,"src":43460},"app services users","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F696996f4-f283-429b-8bee-d8f059241eff-f256a44435.png",[11,43462,43463,43464,43467],{},"Also, if you go the ",[59,43465,43466],{},"\"Data Services\""," tab, and browse your database's users collection, you should see an entry there. This was created by the Auth Trigger we had created earlier.",[11,43469,43470],{},[3135,43471],{"alt":43472,"src":43473},"user in the users collection of the database","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F45558c99-8c74-4a1b-b29f-faa1731f95f5-825fdbc29c.png",[11,43475,43476],{},"Congratulations are in order. You've made it this far, you were able to create a trigger, and make it work successfully :-).",[32,43478,43480],{"id":43479},"database-interactions-from-the-client","Database interactions from the client",[11,43482,43483,43484,10991],{},"For interacting with our database through the Realm SDK, we need to define the data access rules first. Without that we won't be able to read or write to the database. You can verify this by doing the following changes to our ",[59,43485,41385],{},[269,43487,43489],{"className":24597,"code":43488,"language":24599,"meta":274,"style":274},"\u002F\u002F Add the following import\nimport { useRealmApp } from '..\u002FRealmApp';\n\nexport const Dashboard = () => {\n  \u002F\u002F ...\n\n  \u002F\u002F Add the following before the return statement\n  const { appDB } = useRealmApp();\n\n  useEffect(() => {\n    const getUser = async () => {\n      const res = await appDB.collection('users').find({});\n      console.log('got some user', res);\n    };\n\n    if (appDB) {\n      getUser();\n    }\n  }, [appDB]);\n\n  \u002F\u002F ...\n}\n",[59,43490,43491,43496,43501,43505,43509,43513,43517,43522,43527,43531,43535,43540,43545,43550,43554,43558,43563,43568,43572,43577,43581,43585],{"__ignoreMap":274},[278,43492,43493],{"class":280,"line":281},[278,43494,43495],{},"\u002F\u002F Add the following import\n",[278,43497,43498],{"class":280,"line":288},[278,43499,43500],{},"import { useRealmApp } from '..\u002FRealmApp';\n",[278,43502,43503],{"class":280,"line":295},[278,43504,292],{"emptyLinePlaceholder":291},[278,43506,43507],{"class":280,"line":316},[278,43508,41428],{},[278,43510,43511],{"class":280,"line":322},[278,43512,12729],{},[278,43514,43515],{"class":280,"line":327},[278,43516,292],{"emptyLinePlaceholder":291},[278,43518,43519],{"class":280,"line":340},[278,43520,43521],{},"  \u002F\u002F Add the following before the return statement\n",[278,43523,43524],{"class":280,"line":349},[278,43525,43526],{},"  const { appDB } = useRealmApp();\n",[278,43528,43529],{"class":280,"line":375},[278,43530,292],{"emptyLinePlaceholder":291},[278,43532,43533],{"class":280,"line":386},[278,43534,41972],{},[278,43536,43537],{"class":280,"line":397},[278,43538,43539],{},"    const getUser = async () => {\n",[278,43541,43542],{"class":280,"line":408},[278,43543,43544],{},"      const res = await appDB.collection('users').find({});\n",[278,43546,43547],{"class":280,"line":433},[278,43548,43549],{},"      console.log('got some user', res);\n",[278,43551,43552],{"class":280,"line":454},[278,43553,1378],{},[278,43555,43556],{"class":280,"line":475},[278,43557,292],{"emptyLinePlaceholder":291},[278,43559,43560],{"class":280,"line":496},[278,43561,43562],{},"    if (appDB) {\n",[278,43564,43565],{"class":280,"line":505},[278,43566,43567],{},"      getUser();\n",[278,43569,43570],{"class":280,"line":516},[278,43571,1285],{},[278,43573,43574],{"class":280,"line":527},[278,43575,43576],{},"  }, [appDB]);\n",[278,43578,43579],{"class":280,"line":533},[278,43580,292],{"emptyLinePlaceholder":291},[278,43582,43583],{"class":280,"line":539},[278,43584,12729],{},[278,43586,43587],{"class":280,"line":545},[278,43588,617],{},[11,43590,43591],{},"After saving the code if you try to get the user from the database you'll get the following error message",[269,43593,43595],{"className":24597,"code":43594,"language":24599,"meta":274,"style":274},"no rule exists for namespace '\u003Cyour_db_name>.users'\n",[59,43596,43597],{"__ignoreMap":274},[278,43598,43599],{"class":280,"line":281},[278,43600,43594],{},[11,43602,43603,43604,43606],{},"Then how could the user entry creation in the database get through earlier, you might ask? Well, the backend triggers are run as the system, so it has the required privileges. You can verify whether the triggers run as ",[59,43605,361],{}," or not by adding the following console log to the auth trigger function code.",[269,43608,43610],{"className":24597,"code":43609,"language":24599,"meta":274,"style":274},"console.log('context.user.type:', context.user.type)\n",[59,43611,43612],{"__ignoreMap":274},[278,43613,43614],{"class":280,"line":281},[278,43615,43609],{},[11,43617,43618,43619,43622,43623,43626,43627,43629,43630,43633],{},"Also, the rules we're talking about here are for the Realm App (used by the React App) to access the database on our behalf. Click on ",[59,43620,43621],{},"Rules"," from the sidebar under ",[59,43624,43625],{},"DATA ACCESS"," menu section. Then click on the ",[59,43628,42688],{}," collection in the middle panel, and click on ",[59,43631,43632],{},"Skip (start from scratch)"," at the bottom of the rightmost panel.",[11,43635,43636],{},[3135,43637],{"alt":43638,"src":43639},"add data access rules","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F1ccec609-e70c-4cac-a283-5671df525beb-9811dfa60d.png",[11,43641,43642,43643,43646,43647,43649,43650,43653],{},"Give this rule a proper name, say ReadWriteOwn, and click on ",[59,43644,43645],{},"Advanced Document Filters",", and add the following ",[59,43648,2230],{}," expression to both the read and the write text boxes. Select ",[59,43651,43652],{},"Read and write all fields"," from the dropdown menu at the bottom, save the draft, and then deploy your changes.",[269,43655,43657],{"className":5690,"code":43656,"language":1310,"meta":274,"style":274},"{\n    \"_id\": {\n        \"%stringToOid\": \"%%user.id\"\n    }\n}\n",[59,43658,43659,43663,43670,43680,43684],{"__ignoreMap":274},[278,43660,43661],{"class":280,"line":281},[278,43662,524],{"class":302},[278,43664,43665,43668],{"class":280,"line":288},[278,43666,43667],{"class":650},"    \"_id\"",[278,43669,5706],{"class":302},[278,43671,43672,43675,43677],{"class":280,"line":295},[278,43673,43674],{"class":650},"        \"%stringToOid\"",[278,43676,1155],{"class":302},[278,43678,43679],{"class":309},"\"%%user.id\"\n",[278,43681,43682],{"class":280,"line":316},[278,43683,1285],{"class":302},[278,43685,43686],{"class":280,"line":322},[278,43687,617],{"class":302},[11,43689,43690],{},[3135,43691],{"alt":43692,"src":43693},"Add read \u002F write checks for authroization","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F1c72645a-6d5c-4377-a41c-2bb1b0bbaf86-689fda4ee5.png",[11,43695,43696,43697,43699,43700,43703,43704,43706,43707,43710,43711,43713,43714,4633],{},"What we're doing above is: matching the incoming userId (from the HTTP request that the Realm SDK makes) against the ",[59,43698,1883],{},"(which is the document owner's user id ) of the document. If both are the same, the user making the request will be authorized to read\u002Fwrite, else the access will be denied. This is necessary if we don't want any unauthorized access to our data, which is true in this case. Also, ",[59,43701,43702],{},"\"%stringToOid\": \"%%user.id\""," converts the incoming user id (which is a ",[59,43705,1705],{},") to the mongo ",[59,43708,43709],{},"ObjectId"," so that we can compare it against ",[59,43712,1883],{}," (which is an ",[59,43715,43709],{},[11,43717,43718],{},"Now if you reload the dashboard page, you can verify that the user data is getting returned successfully from the database through the console log we've added there.",[32,43720,43722],{"id":43721},"add-transactions","Add Transactions",[11,43724,43725,43726,43729],{},"We're ready to create transactions now. Let's make the same data access rules for the ",[59,43727,43728],{},"\"transactions\""," collection with two minor changes.",[123,43731,43732,43744],{},[74,43733,43734,43735,43737,43738,43741,43742,4633],{},"Users should be able to create new transactions on their own, so we need to allow inserting new documents into the collection (as opposed to the ",[59,43736,42688],{}," collection where the insert happens only once, and that too from the auth trigger). So we need to check\u002Fselect the ",[59,43739,43740],{},"\"insert\""," option (just above the ",[59,43743,43645],{},[74,43745,43746,43747,43750,43751,43753,43754,43757],{},"We'll save the transaction's owner id in a new field ",[59,43748,43749],{},"owner_id"," (which will be a ",[59,43752,1705],{},"), so we don't need to convert the incoming userId to an ObjectId. Use the following for ",[59,43755,43756],{},"\"Advance Document Filters\""," read & write text boxes.",[269,43759,43761],{"className":5690,"code":43760,"language":1310,"meta":274,"style":274},"{\n  \"owner_id\": \"%%user.id\"\n}\n",[59,43762,43763,43767,43776],{"__ignoreMap":274},[278,43764,43765],{"class":280,"line":281},[278,43766,524],{"class":302},[278,43768,43769,43772,43774],{"class":280,"line":288},[278,43770,43771],{"class":650},"  \"owner_id\"",[278,43773,1155],{"class":302},[278,43775,43679],{"class":309},[278,43777,43778],{"class":280,"line":295},[278,43779,617],{"class":302},[11,43781,43782,43783,43786],{},"Save the draft and deploy your changes. Head over to the ",[59,43784,43785],{},"NewTransaction.js"," file and add the following changes.",[269,43788,43790],{"className":24597,"code":43789,"language":24599,"meta":274,"style":274},"\u002F\u002F Add the following import statement\nimport { useRealmApp } from '..\u002FRealmApp';\n\n\u002F\u002F Call useRealmApp inside the function component\nconst { appDB, realmUser } = useRealmApp();\n\n\u002F\u002F Replace the onSubmit function with the following\nconst onSubmit = async (e) => {\n    e.preventDefault();\n\n    const amount = parseFloat(formState.amount);\n    const comment = formState.comment.trim();\n\n    if (!amount || !comment || !formState.type) {\n      alert('Please fill in all fields');\n      return;\n    }\n\n    try {\n      const finalData = {\n        amount,\n        comment,\n        type: formState.type,\n        owner_id: realmUser.id,\n        createdAt: new Date(),\n      };\n\n      setLoading(true);\n      const res = await appDB.collection('transactions').insertOne(finalData);\n      console.log('result of insert op', res);\n\n      setFormState(INITIAL_STATE);\n      setMessage({ type: 'success', text: 'Successfully saved the transaction.' });\n    } catch (error) {\n      console.log('failed to save the transaction');\n      setMessage({ type: 'error', text: 'Failed to save the transaction.' });\n    }\n\n    setLoading(false);\n  };\n",[59,43791,43792,43797,43801,43805,43810,43815,43819,43824,43829,43833,43837,43841,43845,43849,43853,43857,43861,43865,43869,43873,43878,43883,43888,43893,43898,43902,43906,43910,43915,43920,43925,43929,43934,43939,43943,43947,43952,43956,43960,43965],{"__ignoreMap":274},[278,43793,43794],{"class":280,"line":281},[278,43795,43796],{},"\u002F\u002F Add the following import statement\n",[278,43798,43799],{"class":280,"line":288},[278,43800,43500],{},[278,43802,43803],{"class":280,"line":295},[278,43804,292],{"emptyLinePlaceholder":291},[278,43806,43807],{"class":280,"line":316},[278,43808,43809],{},"\u002F\u002F Call useRealmApp inside the function component\n",[278,43811,43812],{"class":280,"line":322},[278,43813,43814],{},"const { appDB, realmUser } = useRealmApp();\n",[278,43816,43817],{"class":280,"line":327},[278,43818,292],{"emptyLinePlaceholder":291},[278,43820,43821],{"class":280,"line":340},[278,43822,43823],{},"\u002F\u002F Replace the onSubmit function with the following\n",[278,43825,43826],{"class":280,"line":349},[278,43827,43828],{},"const onSubmit = async (e) => {\n",[278,43830,43831],{"class":280,"line":375},[278,43832,42038],{},[278,43834,43835],{"class":280,"line":386},[278,43836,292],{"emptyLinePlaceholder":291},[278,43838,43839],{"class":280,"line":397},[278,43840,42047],{},[278,43842,43843],{"class":280,"line":408},[278,43844,42052],{},[278,43846,43847],{"class":280,"line":433},[278,43848,292],{"emptyLinePlaceholder":291},[278,43850,43851],{"class":280,"line":454},[278,43852,42061],{},[278,43854,43855],{"class":280,"line":475},[278,43856,42066],{},[278,43858,43859],{"class":280,"line":496},[278,43860,38526],{},[278,43862,43863],{"class":280,"line":505},[278,43864,1285],{},[278,43866,43867],{"class":280,"line":516},[278,43868,292],{"emptyLinePlaceholder":291},[278,43870,43871],{"class":280,"line":527},[278,43872,8279],{},[278,43874,43875],{"class":280,"line":533},[278,43876,43877],{},"      const finalData = {\n",[278,43879,43880],{"class":280,"line":539},[278,43881,43882],{},"        amount,\n",[278,43884,43885],{"class":280,"line":545},[278,43886,43887],{},"        comment,\n",[278,43889,43890],{"class":280,"line":551},[278,43891,43892],{},"        type: formState.type,\n",[278,43894,43895],{"class":280,"line":557},[278,43896,43897],{},"        owner_id: realmUser.id,\n",[278,43899,43900],{"class":280,"line":567},[278,43901,42097],{},[278,43903,43904],{"class":280,"line":577},[278,43905,1650],{},[278,43907,43908],{"class":280,"line":587},[278,43909,292],{"emptyLinePlaceholder":291},[278,43911,43912],{"class":280,"line":597},[278,43913,43914],{},"      setLoading(true);\n",[278,43916,43917],{"class":280,"line":608},[278,43918,43919],{},"      const res = await appDB.collection('transactions').insertOne(finalData);\n",[278,43921,43922],{"class":280,"line":614},[278,43923,43924],{},"      console.log('result of insert op', res);\n",[278,43926,43927],{"class":280,"line":620},[278,43928,292],{"emptyLinePlaceholder":291},[278,43930,43931],{"class":280,"line":625},[278,43932,43933],{},"      setFormState(INITIAL_STATE);\n",[278,43935,43936],{"class":280,"line":640},[278,43937,43938],{},"      setMessage({ type: 'success', text: 'Successfully saved the transaction.' });\n",[278,43940,43941],{"class":280,"line":663},[278,43942,42106],{},[278,43944,43945],{"class":280,"line":669},[278,43946,42111],{},[278,43948,43949],{"class":280,"line":680},[278,43950,43951],{},"      setMessage({ type: 'error', text: 'Failed to save the transaction.' });\n",[278,43953,43954],{"class":280,"line":686},[278,43955,1285],{},[278,43957,43958],{"class":280,"line":1334},[278,43959,292],{"emptyLinePlaceholder":291},[278,43961,43962],{"class":280,"line":1375},[278,43963,43964],{},"    setLoading(false);\n",[278,43966,43967],{"class":280,"line":1381},[278,43968,901],{},[11,43970,43971],{},"Now try creating a transaction, you should be able to see the created transaction in the database. But the main balance in the user document won't change as we haven't written any trigger for that. Let's remedy that and create our second trigger in the next section.",[32,43973,43975],{"id":43974},"creating-a-database-trigger","Creating a Database Trigger",[11,43977,43978],{},"What we want to do here is: whenever a new transaction is created by the user, we update the main balance as well as the in\u002Fout values for the current month. Remember the user document had the below structure",[269,43980,43982],{"className":24597,"code":43981,"language":24599,"meta":274,"style":274},"const userData = {\n    _id: BSON.ObjectId(user.id),\n    balance: 0,\n    currMonth: {\n      in: 0,\n      out: 0,\n    },\n    createdAt: time, \n    updatedAt: time,\n};\n",[59,43983,43984,43989,43993,43997,44001,44005,44009,44013,44017,44021],{"__ignoreMap":274},[278,43985,43986],{"class":280,"line":281},[278,43987,43988],{},"const userData = {\n",[278,43990,43991],{"class":280,"line":288},[278,43992,42871],{},[278,43994,43995],{"class":280,"line":295},[278,43996,42876],{},[278,43998,43999],{"class":280,"line":316},[278,44000,42881],{},[278,44002,44003],{"class":280,"line":322},[278,44004,42886],{},[278,44006,44007],{"class":280,"line":327},[278,44008,42891],{},[278,44010,44011],{"class":280,"line":340},[278,44012,2243],{},[278,44014,44015],{"class":280,"line":349},[278,44016,42900],{},[278,44018,44019],{"class":280,"line":375},[278,44020,42905],{},[278,44022,44023],{"class":280,"line":386},[278,44024,2817],{},[11,44026,44027],{},"Head over to the App Services Triggers section and create a new database trigger.",[11,44029,44030],{},[3135,44031],{"alt":44032,"src":44033},"creating a database trigger","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fa6adb894-366d-448a-b28b-c57b6e2ae744-e8323bfcdb.png",[11,44035,44036],{},"Select your cluster and database from the dropdowns, and choose transactions as the collection name. Again select function as the event type and create a new function by giving it an appropriate name.",[11,44038,44039],{},[3135,44040],{"alt":44041,"src":44042},"configuring database trigger","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fb2ee859d-9149-4e97-af37-d4ab4a2a08fd-de4c1dcaef.png",[11,44044,44045,44046,44048,44049,44052],{},"Add the following code to the code panel for the function. What the code essentially does is: get the inserted document, extract the ",[59,44047,43749],{}," from it, and then update the corresponding user document. We use the ",[59,44050,44051],{},"$inc"," pipeline of mongoDB to increment (or decrement in case of deduction by making the value negative) the respective fields.",[269,44054,44056],{"className":24597,"code":44055,"language":24599,"meta":274,"style":274},"exports = async function(changeEvent) {\n    const doc = changeEvent.fullDocument;\n    \n    console.log('incoming doc:', JSON.stringify(doc))\n    \n    const filter = { _id: BSON.ObjectId(doc.owner_id) };\n    const update = {\n      $set: { updatedAt: new Date() },\n      $inc: {},\n    };\n  \n    if (doc.type === 'IN') {\n      update.$inc.balance = doc.amount;\n      update.$inc['currMonth.in'] = doc.amount;\n    } else {\n      update.$inc.balance = -doc.amount;\n      update.$inc['currMonth.out'] = doc.amount;\n    }\n  \n    \u002F\u002F Replace the DB name with your db name\n    const usersCollection = context.services\n      .get('mongodb-atlas')\n      .db('\u003Cdb_name>')\n      .collection('users');\n    \n    const res = await usersCollection.updateOne(filter, update);\n    console.log('update op res:', JSON.stringify(res));\n};\n",[59,44057,44058,44063,44068,44072,44077,44081,44086,44091,44096,44101,44105,44109,44114,44119,44124,44128,44133,44138,44142,44146,44151,44156,44161,44166,44171,44175,44180,44185],{"__ignoreMap":274},[278,44059,44060],{"class":280,"line":281},[278,44061,44062],{},"exports = async function(changeEvent) {\n",[278,44064,44065],{"class":280,"line":288},[278,44066,44067],{},"    const doc = changeEvent.fullDocument;\n",[278,44069,44070],{"class":280,"line":295},[278,44071,11953],{},[278,44073,44074],{"class":280,"line":316},[278,44075,44076],{},"    console.log('incoming doc:', JSON.stringify(doc))\n",[278,44078,44079],{"class":280,"line":322},[278,44080,11953],{},[278,44082,44083],{"class":280,"line":327},[278,44084,44085],{},"    const filter = { _id: BSON.ObjectId(doc.owner_id) };\n",[278,44087,44088],{"class":280,"line":340},[278,44089,44090],{},"    const update = {\n",[278,44092,44093],{"class":280,"line":349},[278,44094,44095],{},"      $set: { updatedAt: new Date() },\n",[278,44097,44098],{"class":280,"line":375},[278,44099,44100],{},"      $inc: {},\n",[278,44102,44103],{"class":280,"line":386},[278,44104,1378],{},[278,44106,44107],{"class":280,"line":397},[278,44108,32347],{},[278,44110,44111],{"class":280,"line":408},[278,44112,44113],{},"    if (doc.type === 'IN') {\n",[278,44115,44116],{"class":280,"line":433},[278,44117,44118],{},"      update.$inc.balance = doc.amount;\n",[278,44120,44121],{"class":280,"line":454},[278,44122,44123],{},"      update.$inc['currMonth.in'] = doc.amount;\n",[278,44125,44126],{"class":280,"line":475},[278,44127,8971],{},[278,44129,44130],{"class":280,"line":496},[278,44131,44132],{},"      update.$inc.balance = -doc.amount;\n",[278,44134,44135],{"class":280,"line":505},[278,44136,44137],{},"      update.$inc['currMonth.out'] = doc.amount;\n",[278,44139,44140],{"class":280,"line":516},[278,44141,1285],{},[278,44143,44144],{"class":280,"line":527},[278,44145,32347],{},[278,44147,44148],{"class":280,"line":533},[278,44149,44150],{},"    \u002F\u002F Replace the DB name with your db name\n",[278,44152,44153],{"class":280,"line":539},[278,44154,44155],{},"    const usersCollection = context.services\n",[278,44157,44158],{"class":280,"line":545},[278,44159,44160],{},"      .get('mongodb-atlas')\n",[278,44162,44163],{"class":280,"line":551},[278,44164,44165],{},"      .db('\u003Cdb_name>')\n",[278,44167,44168],{"class":280,"line":557},[278,44169,44170],{},"      .collection('users');\n",[278,44172,44173],{"class":280,"line":567},[278,44174,11953],{},[278,44176,44177],{"class":280,"line":577},[278,44178,44179],{},"    const res = await usersCollection.updateOne(filter, update);\n",[278,44181,44182],{"class":280,"line":587},[278,44183,44184],{},"    console.log('update op res:', JSON.stringify(res));\n",[278,44186,44187],{"class":280,"line":597},[278,44188,2817],{},[11,44190,43003,44191,44193],{},[59,44192,41385],{}," file and make the following changes to the component code",[269,44195,44197],{"className":24597,"code":44196,"language":24599,"meta":274,"style":274},"\u002F\u002F import BSON from 'realm-web\nimport { BSON } from 'realm-web';\n\n\u002F\u002F Destructure realmUser also from useRealmApp\nconst { realmUser, appDB } = useRealmApp();\n\n\u002F\u002F Update the useEffect to the following\nuseEffect(() => {\n    const getUser = async () => {\n      const res = await appDB\n        .collection('users')\n        .findOne({ _id: new BSON.ObjectId(realmUser.id) });\n      console.log('got some user', res);\n      setUser(res);\n    };\n\n    const getTransactions = async () => {\n      const res = await appDB.collection('transactions').find({});\n      console.log('got transactions res', res);\n      setTransactions(res);\n      setLoading(false);\n    };\n\n    if (appDB) {\n      getUser();\n      getTransactions();\n    }\n}, [appDB, realmUser]);\n",[59,44198,44199,44204,44209,44213,44218,44223,44227,44232,44237,44241,44246,44251,44256,44260,44265,44269,44273,44278,44283,44288,44293,44298,44302,44306,44310,44314,44319,44323],{"__ignoreMap":274},[278,44200,44201],{"class":280,"line":281},[278,44202,44203],{},"\u002F\u002F import BSON from 'realm-web\n",[278,44205,44206],{"class":280,"line":288},[278,44207,44208],{},"import { BSON } from 'realm-web';\n",[278,44210,44211],{"class":280,"line":295},[278,44212,292],{"emptyLinePlaceholder":291},[278,44214,44215],{"class":280,"line":316},[278,44216,44217],{},"\u002F\u002F Destructure realmUser also from useRealmApp\n",[278,44219,44220],{"class":280,"line":322},[278,44221,44222],{},"const { realmUser, appDB } = useRealmApp();\n",[278,44224,44225],{"class":280,"line":327},[278,44226,292],{"emptyLinePlaceholder":291},[278,44228,44229],{"class":280,"line":340},[278,44230,44231],{},"\u002F\u002F Update the useEffect to the following\n",[278,44233,44234],{"class":280,"line":349},[278,44235,44236],{},"useEffect(() => {\n",[278,44238,44239],{"class":280,"line":375},[278,44240,43539],{},[278,44242,44243],{"class":280,"line":386},[278,44244,44245],{},"      const res = await appDB\n",[278,44247,44248],{"class":280,"line":397},[278,44249,44250],{},"        .collection('users')\n",[278,44252,44253],{"class":280,"line":408},[278,44254,44255],{},"        .findOne({ _id: new BSON.ObjectId(realmUser.id) });\n",[278,44257,44258],{"class":280,"line":433},[278,44259,43549],{},[278,44261,44262],{"class":280,"line":454},[278,44263,44264],{},"      setUser(res);\n",[278,44266,44267],{"class":280,"line":475},[278,44268,1378],{},[278,44270,44271],{"class":280,"line":496},[278,44272,292],{"emptyLinePlaceholder":291},[278,44274,44275],{"class":280,"line":505},[278,44276,44277],{},"    const getTransactions = async () => {\n",[278,44279,44280],{"class":280,"line":516},[278,44281,44282],{},"      const res = await appDB.collection('transactions').find({});\n",[278,44284,44285],{"class":280,"line":527},[278,44286,44287],{},"      console.log('got transactions res', res);\n",[278,44289,44290],{"class":280,"line":533},[278,44291,44292],{},"      setTransactions(res);\n",[278,44294,44295],{"class":280,"line":539},[278,44296,44297],{},"      setLoading(false);\n",[278,44299,44300],{"class":280,"line":545},[278,44301,1378],{},[278,44303,44304],{"class":280,"line":551},[278,44305,292],{"emptyLinePlaceholder":291},[278,44307,44308],{"class":280,"line":557},[278,44309,43562],{},[278,44311,44312],{"class":280,"line":567},[278,44313,43567],{},[278,44315,44316],{"class":280,"line":577},[278,44317,44318],{},"      getTransactions();\n",[278,44320,44321],{"class":280,"line":587},[278,44322,1285],{},[278,44324,44325],{"class":280,"line":597},[278,44326,44327],{},"}, [appDB, realmUser]);\n",[11,44329,44330],{},"We're using the realm user id to get the user data now. Also, we've added the code to fetch the user's transactions. After saving the code, make a couple of transactions to see if everything is working properly (it is better to delete the transactions before the database trigger was created for the Math to add up). You should get a screen like the below screenshot",[11,44332,44333],{},[3135,44334],{"alt":44335,"src":44336},"dashboard with transactions","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fe82a67a0-4ca2-45d3-bd88-4bc910c3cd9f-54e27c1a96.png",[11,44338,44339],{},"If you observe, you'll see that the transactions are in the order in which they were added to the database. Also, we only want to show transactions for the current month in the dashboard. You can verify this by adding an entry for the last month directly to the database, and then on dashboard refresh that entry also shows up (do note that this also changes the main balances as there is no date\u002Fmonth guard for the insert trigger).",[11,44341,44342],{},"To rectify the above issues, make the following changes to the realm call",[269,44344,44346],{"className":24597,"code":44345,"language":24599,"meta":274,"style":274},"const date = new Date();\ndate.setDate(1);\ndate.setHours(0, 0, 0, 0);\n\nconst res = await appDB.collection('transactions').find(\n    {\n        createdAt: { $gte: date, $lte: new Date() },\n    },\n    {\n        sort: {\n            createdAt: -1,\n        },\n    }\n);\n",[59,44347,44348,44353,44358,44363,44367,44372,44376,44381,44385,44389,44394,44399,44403,44407],{"__ignoreMap":274},[278,44349,44350],{"class":280,"line":281},[278,44351,44352],{},"const date = new Date();\n",[278,44354,44355],{"class":280,"line":288},[278,44356,44357],{},"date.setDate(1);\n",[278,44359,44360],{"class":280,"line":295},[278,44361,44362],{},"date.setHours(0, 0, 0, 0);\n",[278,44364,44365],{"class":280,"line":316},[278,44366,292],{"emptyLinePlaceholder":291},[278,44368,44369],{"class":280,"line":322},[278,44370,44371],{},"const res = await appDB.collection('transactions').find(\n",[278,44373,44374],{"class":280,"line":327},[278,44375,2209],{},[278,44377,44378],{"class":280,"line":340},[278,44379,44380],{},"        createdAt: { $gte: date, $lte: new Date() },\n",[278,44382,44383],{"class":280,"line":349},[278,44384,2243],{},[278,44386,44387],{"class":280,"line":375},[278,44388,2209],{},[278,44390,44391],{"class":280,"line":386},[278,44392,44393],{},"        sort: {\n",[278,44395,44396],{"class":280,"line":397},[278,44397,44398],{},"            createdAt: -1,\n",[278,44400,44401],{"class":280,"line":408},[278,44402,2606],{},[278,44404,44405],{"class":280,"line":433},[278,44406,1285],{},[278,44408,44409],{"class":280,"line":454},[278,44410,1280],{},[11,44412,44413,44414,44417,44418,44421],{},"We've added the descending sort order for the matched entries. Also, we're only asking for the documents added on or after day 1 of the current month using the ",[59,44415,44416],{},"$gte"," (greater than or equal to) pipeline. I've added the upper bound till the current time (",[59,44419,44420],{},"$lte"," pipeline, less than or equal to) also, though it is not needed. After making these changes you should get the transaction entries in the correct order, and only for the current month.",[11,44423,44424,44425],{},"Congratulations on creating and making the second type of trigger work. ",[94,44426,44427],{},"👏",[32,44429,44431],{"id":44430},"handling-month-changes","Handling Month Changes",[11,44433,44434,44435,44438,44439,44442],{},"Now the only thing remaining is: what happens when the month changes? Since we only want to show the inflows and outflows for the current month, we need to reset them to 0 on the month change, and this should happen automatically. The way to do this is a ",[59,44436,44437],{},"Scheduled Trigger"," (also known as a ",[59,44440,44441],{},"CRON"," job).",[11,44444,44445,44446,44449,44450,44453,44454,42808,44457,44460,44461,39916,44464,44467,44468,42808,44471,44474],{},"Let's go to the app services dashboard one final time, and click on ",[59,44447,44448],{},"Triggers"," in the left sidebar. Then click on \"",[59,44451,44452],{},"Add a Trigger\""," button, and select ",[59,44455,44456],{},"Scheduled",[59,44458,44459],{},"\"Trigger Type\"",". Change the ",[59,44462,44463],{},"\"Schedule Type\"",[59,44465,44466],{},"Advanced"," and use ",[59,44469,44470],{},"0 0 1 * *",[59,44472,44473],{},"CRON schedule",". You can see the dates with times when the next event will occur. Please note that these times are as per the UTC timezone. You can make appropriate changes in hours & minutes if you want to use other timezones.",[11,44476,44477],{},[3135,44478],{"alt":44479,"src":44480},"creating a scheduled trigger","\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002F04d0ea05-af88-42c7-9ccc-aa67f075ea6c-d73c06de65.png",[11,44482,44483,44484,44487],{},"Finally select ",[59,44485,44486],{},"Function"," as the event type, and add the following code in the code text box. We just fetch the users who've done any transaction during the last month (the in\u002Fout field(s) would be non-zero), and set them to 0.",[269,44489,44491],{"className":24597,"code":44490,"language":24599,"meta":274,"style":274},"exports = async function () {\n  const usersCollection = context.services\n    .get('mongodb-atlas')\n    .db('\u003Cdb_name>')\n    .collection('users');\n\n  \u002F\u002F Use the $or pipeline to fetch only those users \n  \u002F\u002F who've any transaction last month \n  const users = await usersCollection.find({\n    \"$or\": [\n      { \"currMonth.in\": { \"$gt\": 0 } },\n      { \"currMonth.out\": { \"$gt\": 0 } }\n    ]\n  }).toArray();\n\n  console.log(`find op users length: ${users.length}`);\n  const bulkOps = [];\n  for (const user of users) {\n    bulkOps.push({\n      updateOne: {\n        filter: { _id: user._id },\n        update: {\n          $set: {\n            updatedAt: new Date(),\n            'currMonth.in': 0,\n            'currMonth.out': 0,\n          },\n        },\n      },\n    });\n  }\n\n  if (bulkOps.length) {\n    await usersCollection.bulkWrite(bulkOps);\n    console.log('after the bulk write ops');\n  }\n};\n",[59,44492,44493,44498,44503,44508,44513,44518,44522,44527,44532,44537,44542,44547,44552,44556,44561,44565,44570,44575,44580,44585,44590,44595,44600,44605,44610,44615,44620,44624,44628,44632,44636,44640,44644,44649,44654,44659,44663],{"__ignoreMap":274},[278,44494,44495],{"class":280,"line":281},[278,44496,44497],{},"exports = async function () {\n",[278,44499,44500],{"class":280,"line":288},[278,44501,44502],{},"  const usersCollection = context.services\n",[278,44504,44505],{"class":280,"line":295},[278,44506,44507],{},"    .get('mongodb-atlas')\n",[278,44509,44510],{"class":280,"line":316},[278,44511,44512],{},"    .db('\u003Cdb_name>')\n",[278,44514,44515],{"class":280,"line":322},[278,44516,44517],{},"    .collection('users');\n",[278,44519,44520],{"class":280,"line":327},[278,44521,292],{"emptyLinePlaceholder":291},[278,44523,44524],{"class":280,"line":340},[278,44525,44526],{},"  \u002F\u002F Use the $or pipeline to fetch only those users \n",[278,44528,44529],{"class":280,"line":349},[278,44530,44531],{},"  \u002F\u002F who've any transaction last month \n",[278,44533,44534],{"class":280,"line":375},[278,44535,44536],{},"  const users = await usersCollection.find({\n",[278,44538,44539],{"class":280,"line":386},[278,44540,44541],{},"    \"$or\": [\n",[278,44543,44544],{"class":280,"line":397},[278,44545,44546],{},"      { \"currMonth.in\": { \"$gt\": 0 } },\n",[278,44548,44549],{"class":280,"line":408},[278,44550,44551],{},"      { \"currMonth.out\": { \"$gt\": 0 } }\n",[278,44553,44554],{"class":280,"line":433},[278,44555,36089],{},[278,44557,44558],{"class":280,"line":454},[278,44559,44560],{},"  }).toArray();\n",[278,44562,44563],{"class":280,"line":475},[278,44564,292],{"emptyLinePlaceholder":291},[278,44566,44567],{"class":280,"line":496},[278,44568,44569],{},"  console.log(`find op users length: ${users.length}`);\n",[278,44571,44572],{"class":280,"line":505},[278,44573,44574],{},"  const bulkOps = [];\n",[278,44576,44577],{"class":280,"line":516},[278,44578,44579],{},"  for (const user of users) {\n",[278,44581,44582],{"class":280,"line":527},[278,44583,44584],{},"    bulkOps.push({\n",[278,44586,44587],{"class":280,"line":533},[278,44588,44589],{},"      updateOne: {\n",[278,44591,44592],{"class":280,"line":539},[278,44593,44594],{},"        filter: { _id: user._id },\n",[278,44596,44597],{"class":280,"line":545},[278,44598,44599],{},"        update: {\n",[278,44601,44602],{"class":280,"line":551},[278,44603,44604],{},"          $set: {\n",[278,44606,44607],{"class":280,"line":557},[278,44608,44609],{},"            updatedAt: new Date(),\n",[278,44611,44612],{"class":280,"line":567},[278,44613,44614],{},"            'currMonth.in': 0,\n",[278,44616,44617],{"class":280,"line":577},[278,44618,44619],{},"            'currMonth.out': 0,\n",[278,44621,44622],{"class":280,"line":587},[278,44623,11557],{},[278,44625,44626],{"class":280,"line":597},[278,44627,2606],{},[278,44629,44630],{"class":280,"line":608},[278,44631,1165],{},[278,44633,44634],{"class":280,"line":614},[278,44635,1233],{},[278,44637,44638],{"class":280,"line":620},[278,44639,1096],{},[278,44641,44642],{"class":280,"line":625},[278,44643,292],{"emptyLinePlaceholder":291},[278,44645,44646],{"class":280,"line":640},[278,44647,44648],{},"  if (bulkOps.length) {\n",[278,44650,44651],{"class":280,"line":663},[278,44652,44653],{},"    await usersCollection.bulkWrite(bulkOps);\n",[278,44655,44656],{"class":280,"line":669},[278,44657,44658],{},"    console.log('after the bulk write ops');\n",[278,44660,44661],{"class":280,"line":680},[278,44662,1096],{},[278,44664,44665],{"class":280,"line":686},[278,44666,2817],{},[11,44668,44669],{},"And we're done. Every month on the first day at midnight UTC, we'll make the in & out for every user 0.",[24,44671,10634],{"id":10633},[11,44673,44674],{},"Congratulations on completing this basic tutorial on using MongoDB Atlas App Services and its triggers, and creating a simple react expense tracker app with it. But don't stop here as you can improve the app further. Below are some of the shortcomings of the app which we just built",[123,44676,44677,44685,44688],{},[74,44678,44679,44680,44684],{},"Our app is prone to the javascript floating point math precision issues. You can use the Decimal BSON type provided by MongoDB to handle it in a better way. See ",[47,44681,42654],{"href":44682,"rel":44683},"https:\u002F\u002Fwww.mongodb.com\u002Fdocs\u002Fmanual\u002Ftutorial\u002Fmodel-monetary-data\u002F",[51]," on this issue.",[74,44686,44687],{},"Our ScheduledJob looks for and updates all the users who've made any transaction in the last month. For smaller apps this is fine, but for apps with a large number of users we can't do this from one function. Atlas App functions have a runtime limitation of 180 seconds which may not be enough to do everything",[74,44689,44690],{},"Right now the scheduled trigger fires at midnight UTC, ideally it should fire in each of the user's timezone, etc.",[11,44692,44693],{},"I hope you work on solving some of these problems. If you've any questions, don't hesitate to leave a comment.",[11,44695,44696,44697,44701],{},"Thanks a lot for following along with this tutorial. I hope you found it useful and were able to gain something from it. Please check out the ",[47,44698,44700],{"href":41294,"rel":44699},[51],"final code on GitHub"," for your reference.",[11,44703,40864],{},[3065,44705,44706],{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":274,"searchDepth":288,"depth":288,"links":44708},[44709,44710,44711,44719,44730],{"id":22771,"depth":288,"text":22772},{"id":40901,"depth":288,"text":40902},{"id":40925,"depth":288,"text":40926,"children":44712},[44713,44714,44715,44716,44717,44718],{"id":40949,"depth":295,"text":40950},{"id":41117,"depth":295,"text":41118},{"id":41276,"depth":295,"text":41277},{"id":41378,"depth":295,"text":41379},{"id":41840,"depth":295,"text":41841},{"id":42466,"depth":295,"text":42467},{"id":42645,"depth":288,"text":42646,"children":44720},[44721,44722,44723,44724,44725,44726,44727,44728,44729],{"id":42662,"depth":295,"text":42663},{"id":42698,"depth":295,"text":42699},{"id":42735,"depth":295,"text":42736},{"id":42763,"depth":295,"text":42764},{"id":42937,"depth":295,"text":42938},{"id":43479,"depth":295,"text":43480},{"id":43721,"depth":295,"text":43722},{"id":43974,"depth":295,"text":43975},{"id":44430,"depth":295,"text":44431},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Freact-app-with-mongodb-atlas-app-services\u002Fa6593770b0659f9271a7324d22729ffe-895f261352.jpeg","2023-03-04T15:20:40.814Z","cleu42yr200080alaawb9axt7",{},"\u002Freact-app-with-mongodb-atlas-app-services",{"title":40891,"description":22772},"react-app-with-mongodb-atlas-app-services",[38595,44739,3095,44740,42698],"mongodb","beginners","6TpkimL54978lJGGj5l_oyt_ehQ4lXFUf-q4LGavjrg",{"id":44743,"title":44744,"body":44745,"cover":45210,"date":45211,"description":45212,"draft":3086,"extension":3087,"hashnodeId":45213,"meta":45214,"navigation":291,"path":45215,"seo":45216,"slug":45217,"stem":45217,"tags":45218,"__hash__":45224},"posts\u002Fhow-to-create-context-menu-actions-on-macbooks.md","How to create a simple context menu action on Macbooks",{"type":8,"value":44746,"toc":45191},[44747,44749,44752,44755,44758,44761,44765,44779,44785,44792,44800,44806,44812,44815,44825,44831,44848,44854,44857,44870,44876,44891,44897,44900,44904,44914,44920,44923,44929,44932,44938,44941,44947,44957,44963,44969,44972,44976,44991,44997,45003,45009,45024,45027,45030,45033,45036,45046,45052,45058,45076,45079,45117,45127,45133,45176,45180,45183,45186,45189],[24,44748,22772],{"id":22771},[11,44750,44751],{},"The backstory for this rabbit hole is not that deep. When you start your writing journey soon you get to know about some sort of text length restrictions. Sometimes it is for SEO because you want more eyeballs on your articles, organically. Or, maybe you're writing your Ads or Website copy. Or you're just the sort of person who needs to know the exact characters count at all times.",[11,44753,44754],{},"Whatever the case may be, this restriction mindset soon becomes a habit. You start selecting random texts and try to find out their length. Or, maybe not! This is just me, being my weird self.",[40,44756],{"url":44757},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FZtSGStwmFYQjMKRO2H\u002Fgiphy.gif",[11,44759,44760],{},"Until now I used to copy-paste the text into my VS Code, which is always open by the way, and find out its length by selecting it. But I was looking for a better & omnipresent solution (at least on my laptop). And that is how I learnt about the Automator App. You must believe me when I say this, before yesterday I hadn't heard of Automator App, even though I've had a MacBook for quite some time.",[24,44762,44764],{"id":44763},"the-automator-app","The Automator App",[11,44766,44767,44768,44771,44772,44775,44776,17418],{},"As the name implies, it allows you to create automated workflows and actions etc for your MacBook. When you launch it, using either the ",[59,44769,44770],{},"Spotlight Search"," or from ",[59,44773,44774],{},"Launchpad -> Other -> Automator",", you'll see something like the below (please note that my laptop is on ",[59,44777,44778],{},"macOS Monterey v12.6.2",[11,44780,44781],{},[3135,44782],{"alt":44783,"src":44784},"Automator app screen","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F37ede9cf-82b8-4a80-a97f-a13942171758-ff719492fe.png",[11,44786,44787,44788,44791],{},"We can create a whole lot of things from here. For now, I've only explored the ",[59,44789,44790],{},"Quick Action"," type as that is sufficient for creating a context menu action.",[24,44793,44795,44796,44799],{"id":44794},"creating-the-text-length-action","Creating the ",[59,44797,44798],{},"Text Length"," Action",[11,44801,44802,44803,44805],{},"After selecting the ",[59,44804,44790],{}," option you should get the below screen.",[11,44807,44808],{},[3135,44809],{"alt":44810,"src":44811},"Create quick action screen","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F97bb3037-d1c5-4b76-a022-44cacbd723da-f2ae041a53.png",[11,44813,44814],{},"The leftmost panel is the list of categories for which actions are available. The second panel lists the available actions, and the rightmost panel is where you create the action workflow.",[11,44816,44817,44818,919,44821,44824],{},"If you look at the top of the rightmost panel, it says ",[59,44819,44820],{},"\"Workflow receives current\"",[59,44822,44823],{},"\"Automatic (Text)\""," is already selected from the dropdown. This is exactly what we need to create our current action. If you want to create a different action then you can look at other available choices.",[32,44826,44828,32903],{"id":44827},"run-shell-script-action",[59,44829,44830],{},"Run Shell Script",[11,44832,44833,44834,19634,44837,44839,44840,44843,44844,44847],{},"As our workflow is already receiving the text selection, now we somehow need to calculate its length. If you go through the available actions, you'll some familiar names like ",[59,44835,44836],{},"Run JavaScript",[59,44838,44830],{}," (forgive me for not mentioning ",[59,44841,44842],{},"Run AppleScript"," as I didn't have the desire to explore ",[59,44845,44846],{},"AppleScript","). You can also type in the search box above the second panel (the middle panel)",[11,44849,44850],{},[3135,44851],{"alt":44852,"src":44853},"Different script options in Automator","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F12e2ac30-bff7-40a3-807a-0d7104a2ae27-a0d38d1b58.png",[11,44855,44856],{},"Drag and drop the Run Shell Script action to the right side panel. So this script is the second step of our workflow, the first being the automatic text selection input (notice the connection shown between these steps).",[11,44858,33255,44859,44862,44863,44866,44867],{},[59,44860,44861],{},"Pass Input"," option on the top right of the shell script to ",[59,44864,44865],{},"\"as arguments\""," instead of the current ",[59,44868,44869],{},"\"to stdin\"",[11,44871,44872],{},[3135,44873],{"alt":44874,"src":44875},"shell script input as arguments","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F2455d2f2-5510-4bdb-8d18-a6e7dd85595f-4fe7f653df.png",[11,44877,44878,44879,44882,44883,44886,44887,44890],{},"Now we're ready to work on the incoming arguments (provided by the previous step). Note that you can change the type of shell from the left top ",[59,44880,44881],{},"Shell"," option (mine is showing ",[59,44884,44885],{},"\u002Fbin\u002Fzsh","). Our script can receive multiple input arguments in some other workflows, and that's why you see the for loop which comes up automatically. For text inputs, we will receive only one input argument. So we can change the script to ",[59,44888,44889],{},"echo ${#1} characters",". Essentially we're taking the first argument and finding out its length using the # operator, and then adding the \"characters\" suffix.",[11,44892,44893],{},[3135,44894],{"alt":44895,"src":44896},"script for the shell script","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002Faf5afe05-5215-45bc-a523-d348fb7cae46-b0c24e96cc.png",[11,44898,44899],{},"And that is it. Our action needs only this much scripting.",[32,44901,44903],{"id":44902},"testing-the-action","Testing the action",[11,44905,44906,44907,44910,44911,44913],{},"Before finalizing and saving our newly created action we need to test it first. To test it while we're still in the Automator we can make use of ",[59,44908,44909],{},"Get Specified Text"," action. Search for \"text\" in the search box, and drag the ",[59,44912,44909],{}," action just above our shell script. Write something in the textarea (I've written \"This is me.\", how original).",[11,44915,44916],{},[3135,44917],{"alt":44918,"src":44919},"Adding the \"Get Specified Text\" action","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F34b13a20-35d0-4f89-be8c-5927c72ab586-d232107a16.png",[11,44921,44922],{},"We're ready to test our script now. Click on the run option from the top right part of the Automator, it shows that everything is a success in the logs at the bottom of the screen.",[11,44924,44925],{},[3135,44926],{"alt":44927,"src":44928},"running the test in the automator","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F176d7f18-a71b-4ed5-81ca-a436c2c35be7-4e204401c9.png",[11,44930,44931],{},"It's great that everything is green, but what was the length of \"This is me.\"? Well, we can see the output of our shell script by clicking on the results at the bottom of the script.",[11,44933,44934],{},[3135,44935],{"alt":44936,"src":44937},"result of the script","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002Fa28cbd5a-0213-46b2-8d9a-3ce44c194a37-eb0d7cb849.png",[11,44939,44940],{},"It seems to be giving the correct results. You can test it again by changing the input text to something else. For newlines, it adds one to the count for every newline character.",[32,44942,4796,44944,32903],{"id":44943},"the-speak-text-action",[59,44945,44946],{},"Speak Text",[11,44948,44949,44950,44953,44954],{},"But where will we see the output in real situations? We need one more action to get the output of the shell script delivered to us. There are a couple of ways to do this, one being a ",[59,44951,44952],{},"dialogbox"," giving us the output, but the one I liked and used is the ",[59,44955,44956],{},"\"Speak Text\".",[11,44958,44959,44960,44962],{},"When you searched for \"text\" in the search box, then you may have seen \"Speak Text\" also as one of the filtered results. Drag it to just below the Shell Script. So the output of the shell script will be the input to the ",[59,44961,44946],{}," action.",[11,44964,44965],{},[3135,44966],{"alt":44967,"src":44968},"Adding the speak text action","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F76b4f48c-e5e1-4375-b556-b7a61cf68250-e217deed47.png",[11,44970,44971],{},"Run the same test one more time, and this time you should hear \"11 characters\" (remember my dialogue is still \"This is me.\", maybe it is time to change my script writer) from the speaker of your MacBook, or your headphones if they are connected to it.",[32,44973,44975],{"id":44974},"saving-the-custom-action","Saving the custom action",[11,44977,44978,44979,44982,44983,44986,44987,44990],{},"We don't need the ",[59,44980,44981],{},"\"Get Specified Text\""," action anymore, delete it by clicking the ",[59,44984,44985],{},"\"x\""," button on its top right. Click on ",[59,44988,44989],{},"File -> Save"," from the top menu bar, give your action a name (I am using \"Text Length\") and save it.",[11,44992,44993],{},[3135,44994],{"alt":44995,"src":44996},"saving the action","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002Fcd51fd3f-f145-43fe-ad18-49a23c602fa0-4afde78172.png",[11,44998,44999],{},[3135,45000],{"alt":45001,"src":45002},"giving a name to the action","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F664371cd-8057-4934-8d0c-9d9d36087290-24fbe0fc07.png",[24,45004,45006,45008],{"id":45005},"text-length-in-action",[59,45007,44798],{}," in action",[11,45010,45011,45012,45015,45016,45019,45020,45023],{},"After saving the action it will be available from ",[59,45013,45014],{},"the mouse right-click -> Services -> Text Length."," In some applications where the right-click context menu is already customized by the application, say VS Code, you can get it from the ",[59,45017,45018],{},"mac menu bar -> \u003CApplication_Name> -> Services -> Text Length."," My created actions were saved in ",[59,45021,45022],{},"~\u002FLibrary\u002FServices,"," if you ever want to edit or delete them.",[11,45025,45026],{},"Below are some screen recordings of our custom action, in action. No prizes for figuring out the test subject of the first video :-).",[40,45028],{"url":45029},"https:\u002F\u002Fyoutu.be\u002FJ2xE_NetwUs",[40,45031],{"url":45032},"https:\u002F\u002Fyoutu.be\u002FqudohPicHo4",[40,45034],{"url":45035},"https:\u002F\u002Fyoutu.be\u002FDEs-pqtEYV8",[32,45037,45039,45040,45042,45043],{"id":45038},"using-run-javascript-action-in-place-of-shell-script","Using ",[59,45041,44836],{}," action in place of ",[59,45044,45045],{},"Shell Script",[11,45047,45048,45049,45051],{},"Tried to use Javascript instead of the shell script. Came to know that there are certain extra benefits provided to JavaScript (as well as AppleScript) actions. We don't need to use the ",[59,45050,44946],{}," action if we use these actions.",[11,45053,45054],{},[3135,45055],{"alt":45056,"src":45057},"using the run javascript action","\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002F3c0422ff-45f0-47ca-bb9a-99131de226ea-651775ffcd.png",[11,45059,45060,45061,19634,45063,45065,45066,45068,45069,45071,45072,45075],{},"As you can see, I've removed both, the ",[59,45062,44830],{},[59,45064,44946],{}," actions, and have instead added the ",[59,45067,44836],{}," action. Do remember to select ",[59,45070,4582],{}," from the dropdown menu for ",[59,45073,45074],{},"Workflow receives current"," setting at the top.",[11,45077,45078],{},"This is the code inside the code block",[269,45080,45082],{"className":24597,"code":45081,"language":24599,"meta":274,"style":274},"function run(input, parameters) {\n    const len = input[0].length\n    app = Application.currentApplication()\n    app.includeStandardAdditions = true\n    app.say(len + ' characters')\n    return;\n}\n",[59,45083,45084,45089,45094,45099,45104,45109,45113],{"__ignoreMap":274},[278,45085,45086],{"class":280,"line":281},[278,45087,45088],{},"function run(input, parameters) {\n",[278,45090,45091],{"class":280,"line":288},[278,45092,45093],{},"    const len = input[0].length\n",[278,45095,45096],{"class":280,"line":295},[278,45097,45098],{},"    app = Application.currentApplication()\n",[278,45100,45101],{"class":280,"line":316},[278,45102,45103],{},"    app.includeStandardAdditions = true\n",[278,45105,45106],{"class":280,"line":322},[278,45107,45108],{},"    app.say(len + ' characters')\n",[278,45110,45111],{"class":280,"line":327},[278,45112,8881],{},[278,45114,45115],{"class":280,"line":340},[278,45116,617],{},[11,45118,45119,45122,45123,45126],{},[59,45120,45121],{},"input"," is the input arguments (which is a list, as there can be multiple arguments). So we are grabbing the first item from the list and getting its length. Then we use those extra bells and whistles to speak the text in the next 3 lines. Actual speaking is done by ",[59,45124,45125],{},"app.say(len + ' characters')",", the first 2 lines are just enabling that.",[11,45128,40475,45129,45132],{},[59,45130,45131],{},"parameters"," above is not your typical function parameters, it is some sort of meta-information of the function. When I tried printing it, got the following result (with some different function code which is visible below)",[269,45134,45136],{"className":24597,"code":45135,"language":24599,"meta":274,"style":274},"{|temporary items path|:\"\u002Fvar\u002Ffolders\u002Fjs\u002F7l1q79ds78d8rc3sjs53_xp80000gn\u002FT\u002F6764B971-C5BA-484C-BCBA-1F88B3715B83\u002F1\u002Fcom.apple.Automator.RunJavaScript\", source:\"function run(input, parameters) {\n    \n    \u002F\u002F Your script goes here\n    console.log('input is:', input);\n    console.log(parameters);\n\n    return parameters;\n}\", ignoresInput:false}\n",[59,45137,45138,45143,45147,45152,45157,45162,45166,45171],{"__ignoreMap":274},[278,45139,45140],{"class":280,"line":281},[278,45141,45142],{},"{|temporary items path|:\"\u002Fvar\u002Ffolders\u002Fjs\u002F7l1q79ds78d8rc3sjs53_xp80000gn\u002FT\u002F6764B971-C5BA-484C-BCBA-1F88B3715B83\u002F1\u002Fcom.apple.Automator.RunJavaScript\", source:\"function run(input, parameters) {\n",[278,45144,45145],{"class":280,"line":288},[278,45146,11953],{},[278,45148,45149],{"class":280,"line":295},[278,45150,45151],{},"    \u002F\u002F Your script goes here\n",[278,45153,45154],{"class":280,"line":316},[278,45155,45156],{},"    console.log('input is:', input);\n",[278,45158,45159],{"class":280,"line":322},[278,45160,45161],{},"    console.log(parameters);\n",[278,45163,45164],{"class":280,"line":327},[278,45165,292],{"emptyLinePlaceholder":291},[278,45167,45168],{"class":280,"line":340},[278,45169,45170],{},"    return parameters;\n",[278,45172,45173],{"class":280,"line":349},[278,45174,45175],{},"}\", ignoresInput:false}\n",[24,45177,45179],{"id":45178},"summary","Summary",[11,45181,45182],{},"There are so many things around you waiting to be explored by you. You just need the drive, or the itch to go look for it. The above article is the perfect example of scratching your itch. Now that we've got the basic introduction to Automation on a MacBook, I'm sure we can create some good, time-saving workflows for ourselves.",[11,45184,45185],{},"Hope you enjoyed reading the article. Thanks for reading till the end. Do share your comments in the comment section.",[11,45187,45188],{},"Until next time. :-)",[3065,45190,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":45192},[45193,45194,45195,45204,45209],{"id":22771,"depth":288,"text":22772},{"id":44763,"depth":288,"text":44764},{"id":44794,"depth":288,"text":45196,"children":45197},"Creating the Text Length Action",[45198,45200,45201,45203],{"id":44827,"depth":295,"text":45199},"Run Shell Script action",{"id":44902,"depth":295,"text":44903},{"id":44943,"depth":295,"text":45202},"The Speak Text action",{"id":44974,"depth":295,"text":44975},{"id":45005,"depth":288,"text":45205,"children":45206},"Text Length in action",[45207],{"id":45038,"depth":295,"text":45208},"Using Run JavaScript action in place of Shell Script",{"id":45178,"depth":288,"text":45179},"\u002Fimages\u002Fposts\u002Fhow-to-create-context-menu-actions-on-macbooks\u002Fea73b3d2bb4ff61e8c5589cf846153b2-8ef6ee5da0.jpeg","2023-01-05T18:57:26.891Z","A step by step guide toon how to use the Automator App for creating a simple context menu action for MacBooks.","clcjgabmz000508l3fzojhr9w",{},"\u002Fhow-to-create-context-menu-actions-on-macbooks",{"title":44744,"description":45212},"how-to-create-context-menu-actions-on-macbooks",[45219,45220,45221,45222,45223],"programming","automation","shell","general-advice","macos","7JsXLfV9h10WWDRhp8CJO7DuNLxYjRy02VE2xsfvfCA",{"id":45226,"title":45227,"body":45228,"cover":45734,"date":45735,"description":45736,"draft":3086,"extension":3087,"hashnodeId":45737,"meta":45738,"navigation":291,"path":45739,"seo":45740,"slug":45741,"stem":45741,"tags":45742,"__hash__":45745},"posts\u002Fprogrammatic-copy-to-clipboard-duckduckgo-android.md","Handling programmatic \"Copy to Clipboard\" on DuckDuckGo Android browser",{"type":8,"value":45229,"toc":45725},[45230,45234,45248,45261,45264,45267,45270,45274,45330,45349,45355,45361,45365,45368,45404,45411,45418,45474,45487,45491,45494,45691,45706,45708,45717,45720,45723],[24,45231,45233],{"id":45232},"the-background","The background",[11,45235,45236,45237,45241,45242,45247],{},"This is going to be a short article. Recently I launched an in-browser daily puzzle game (",[47,45238,37459],{"href":45239,"rel":45240},"https:\u002F\u002Fgoldroad.web.app",[51],"). Later on, I added the ability to share your stats for the day on social media using the ",[47,45243,45246],{"href":45244,"rel":45245},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FWeb_Share_API",[51],"Web Share API",". This is how it looks on my Android phone",[37860,45249,45250],{},[45251,45252,45253],"thead",{},[45254,45255,45256,45259],"tr",{},[45257,45258],"th",{},[45257,45260],{},[11,45262,45263],{},"But sharing menu may not be available everywhere, considering this is a web app which can be opened on laptops as well. So I also added a fallback to copy the stats to the device's clipboard, feeling proud & clever at the same time having thought of fallbacks and all.",[40,45265],{"url":45266},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FK3vVkohWnJVO8rZ5Su\u002Fgiphy.gif",[11,45268,45269],{},"And then one of my friends who plays the game religiously informs me that this share button doesn't do anything for him. Knowing him, I asked which browser are you using, and he says DuckDuckGo on Android. And believe me, all my smugness is gone now :-)",[24,45271,45273],{"id":45272},"the-original-code","The original code",[269,45275,45276],{"className":24597,"code":40397,"language":24599,"meta":274,"style":274},[59,45277,45278,45282,45286,45290,45294,45298,45302,45306,45310,45314,45318,45322,45326],{"__ignoreMap":274},[278,45279,45280],{"class":280,"line":281},[278,45281,40404],{},[278,45283,45284],{"class":280,"line":288},[278,45285,40409],{},[278,45287,45288],{"class":280,"line":295},[278,45289,292],{"emptyLinePlaceholder":291},[278,45291,45292],{"class":280,"line":316},[278,45293,40418],{},[278,45295,45296],{"class":280,"line":322},[278,45297,8279],{},[278,45299,45300],{"class":280,"line":327},[278,45301,40427],{},[278,45303,45304],{"class":280,"line":340},[278,45305,5106],{},[278,45307,45308],{"class":280,"line":349},[278,45309,5148],{},[278,45311,45312],{"class":280,"line":375},[278,45313,40440],{},[278,45315,45316],{"class":280,"line":386},[278,45317,8120],{},[278,45319,45320],{"class":280,"line":397},[278,45321,40449],{},[278,45323,45324],{"class":280,"line":408},[278,45325,1096],{},[278,45327,45328],{"class":280,"line":433},[278,45329,2817],{},[11,45331,45332,45333,45336,45337,40479,45339,45342,45343,45348],{},"I had used the `writeText` method of the Clipboard API thinking it has wide availability (as can be seen ",[47,45334,3286],{"href":40463,"rel":45335},[51]," and in the below image) so it should work in almost any browser, and also because \"The ",[59,45338,40478],{},[47,45340,40486],{"href":40482,"rel":45341},[51]," is granted automatically to pages when they are in the active tab\". There was ",[47,45344,45347],{"href":45345,"rel":45346},"https:\u002F\u002Fweb.dev\u002Fasync-clipboard\u002F#:~:text=the%20Clipboard%20API%20is%20only%20supported%20for%20pages%20served%20over%20HTTPS",[51],"one more caveat"," I found, \"the Clipboard API is only supported for pages served over HTTPS\" (which is true anyway in my case).",[11,45350,45351],{},[3135,45352],{"alt":45353,"src":45354},"clipboard api support on major browsers","\u002Fimages\u002Fposts\u002Fprogrammatic-copy-to-clipboard-duckduckgo-android\u002Fc3ab6836-ba2d-4eb6-8b4b-d407befc18c0-249c314cf7.png",[11,45356,45357],{},[3135,45358],{"alt":45359,"src":45360},"writeText support on major browsers","\u002Fimages\u002Fposts\u002Fprogrammatic-copy-to-clipboard-duckduckgo-android\u002F8ac8f490-a99b-495f-a947-e3e98cb94188-da5cbeffcb.png",[24,45362,45364],{"id":45363},"the-issues-with-duckduckgo-ddg","The issues with DuckDuckGo (DDG)",[11,45366,45367],{},"After doing some debugging my investigations revealed the following",[123,45369,45370,45376,45385,45393],{},[74,45371,45372,45375],{},[59,45373,45374],{},"Navigator.share"," is not available on DDG",[74,45377,45378,40511,45381,40515,45383],{},[59,45379,45380],{},"Navigator.clipboard.writeText",[59,45382,40514],{},[59,45384,40518],{},[74,45386,40521,45387,40527,45390,45375],{},[47,45388,40486],{"href":40482,"rel":45389},[51],[59,45391,45392],{},"Navigator.permissions",[74,45394,45395,45396,45403],{},"Tried adding the clipboardWrite permission to the ",[47,45397,45400],{"href":45398,"rel":45399},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FMozilla\u002FAdd-ons\u002FWebExtensions\u002Fmanifest.json\u002Fpermissions",[51],[59,45401,45402],{},"manifest.json"," file (though that is applicable for a web extension only), of course, it didn't work",[24,45405,45407,45408],{"id":45406},"workaround-with-execcommand","Workaround with ",[59,45409,45410],{},"execCommand",[11,45412,45413,45414,45417],{},"Since all my trials with valid methods failed to yield results, I had to fall back to the ",[47,45415,40541],{"href":40545,"rel":45416},[51],". As shown in the linked article, you could use the below code to copy text to the device clipboard.",[269,45419,45420],{"className":24597,"code":40551,"language":24599,"meta":274,"style":274},[59,45421,45422,45426,45430,45434,45438,45442,45446,45450,45454,45458,45462,45466,45470],{"__ignoreMap":274},[278,45423,45424],{"class":280,"line":281},[278,45425,40558],{},[278,45427,45428],{"class":280,"line":288},[278,45429,40563],{},[278,45431,45432],{"class":280,"line":295},[278,45433,40568],{},[278,45435,45436],{"class":280,"line":316},[278,45437,40573],{},[278,45439,45440],{"class":280,"line":322},[278,45441,40578],{},[278,45443,45444],{"class":280,"line":327},[278,45445,40583],{},[278,45447,45448],{"class":280,"line":340},[278,45449,40588],{},[278,45451,45452],{"class":280,"line":349},[278,45453,40593],{},[278,45455,45456],{"class":280,"line":375},[278,45457,40598],{},[278,45459,45460],{"class":280,"line":386},[278,45461,40603],{},[278,45463,45464],{"class":280,"line":397},[278,45465,1096],{},[278,45467,45468],{"class":280,"line":408},[278,45469,40612],{},[278,45471,45472],{"class":280,"line":433},[278,45473,3693],{},[11,45475,45476,45477,45479,45480,39916,45483,45486],{},"But there is a twist here as well, if you set the ",[59,45478,45121],{}," element's ",[59,45481,45482],{},"display",[59,45484,45485],{},"none"," as shown above, then execCommand returns true but doesn't copy anything to the clipboard in the case of DDG (haven't tried it with other browsers, as for everyone else Clipboard API is working fine, at least for the latest versions where I tested)",[24,45488,45490],{"id":45489},"final-code","Final Code",[11,45492,45493],{},"So without further ado, here is my final code which is working fine on DDG (sometimes I do see the phone's keyboard popping up for a split second, but that is fine I think).",[269,45495,45497],{"className":24597,"code":45496,"language":24599,"meta":274,"style":274},"const shareStats = async () => {\n  const text = `The text to share\\nWith multiple lines`;\n\n  if (window.navigator.share) {\n    try {\n      await window.navigator.share({\n        text,\n      });\n    } catch (error) {}\n\n    return;\n  }\n\n  await copyToClipboard(text);\n};\n\nconst copyToClipboard = async (text) => {\n  if (window.navigator.clipboard) {\n    try {\n      await window.navigator.clipboard.writeText(text);\n      return;\n    } catch (error) {}\n  }\n\n  const textarea = document.createElement('textarea');\n  textarea.style.position = 'fixed';\n  textarea.style.width = '1px';\n  textarea.style.height = '1px';\n  textarea.style.padding = 0;\n  textarea.style.border = 'none';\n  textarea.style.outline = 'none';\n  textarea.style.boxShadow = 'none';\n  textarea.style.background = 'transparent';\n\n  document.body.appendChild(textarea);\n  \n  textarea.textContent = text;\n  textarea.focus();\n  textarea.select();\n\n  const result = document.execCommand('copy');\n  textarea.remove();\n  if (!result) {\n    \u002F\u002F Show some error message to the user\n  } else {\n    \u002F\u002F Show a success message to the user mentioning the text is copied to their clipboard\n  }\n};\n",[59,45498,45499,45503,45507,45511,45515,45519,45523,45527,45531,45535,45539,45543,45547,45551,45555,45559,45563,45567,45571,45575,45579,45583,45587,45591,45595,45599,45603,45607,45611,45615,45619,45623,45627,45631,45635,45639,45643,45647,45651,45655,45659,45663,45667,45671,45675,45679,45683,45687],{"__ignoreMap":274},[278,45500,45501],{"class":280,"line":281},[278,45502,40404],{},[278,45504,45505],{"class":280,"line":288},[278,45506,40637],{},[278,45508,45509],{"class":280,"line":295},[278,45510,292],{"emptyLinePlaceholder":291},[278,45512,45513],{"class":280,"line":316},[278,45514,40418],{},[278,45516,45517],{"class":280,"line":322},[278,45518,8279],{},[278,45520,45521],{"class":280,"line":327},[278,45522,40427],{},[278,45524,45525],{"class":280,"line":340},[278,45526,5106],{},[278,45528,45529],{"class":280,"line":349},[278,45530,5148],{},[278,45532,45533],{"class":280,"line":375},[278,45534,40440],{},[278,45536,45537],{"class":280,"line":386},[278,45538,292],{"emptyLinePlaceholder":291},[278,45540,45541],{"class":280,"line":397},[278,45542,8881],{},[278,45544,45545],{"class":280,"line":408},[278,45546,1096],{},[278,45548,45549],{"class":280,"line":433},[278,45550,292],{"emptyLinePlaceholder":291},[278,45552,45553],{"class":280,"line":454},[278,45554,40686],{},[278,45556,45557],{"class":280,"line":475},[278,45558,2817],{},[278,45560,45561],{"class":280,"line":496},[278,45562,292],{"emptyLinePlaceholder":291},[278,45564,45565],{"class":280,"line":505},[278,45566,40699],{},[278,45568,45569],{"class":280,"line":516},[278,45570,40704],{},[278,45572,45573],{"class":280,"line":527},[278,45574,8279],{},[278,45576,45577],{"class":280,"line":533},[278,45578,40713],{},[278,45580,45581],{"class":280,"line":539},[278,45582,38526],{},[278,45584,45585],{"class":280,"line":545},[278,45586,40440],{},[278,45588,45589],{"class":280,"line":551},[278,45590,1096],{},[278,45592,45593],{"class":280,"line":557},[278,45594,292],{"emptyLinePlaceholder":291},[278,45596,45597],{"class":280,"line":567},[278,45598,40734],{},[278,45600,45601],{"class":280,"line":577},[278,45602,40739],{},[278,45604,45605],{"class":280,"line":587},[278,45606,40744],{},[278,45608,45609],{"class":280,"line":597},[278,45610,40749],{},[278,45612,45613],{"class":280,"line":608},[278,45614,40754],{},[278,45616,45617],{"class":280,"line":614},[278,45618,40759],{},[278,45620,45621],{"class":280,"line":620},[278,45622,40764],{},[278,45624,45625],{"class":280,"line":625},[278,45626,40769],{},[278,45628,45629],{"class":280,"line":640},[278,45630,40774],{},[278,45632,45633],{"class":280,"line":663},[278,45634,292],{"emptyLinePlaceholder":291},[278,45636,45637],{"class":280,"line":669},[278,45638,40783],{},[278,45640,45641],{"class":280,"line":680},[278,45642,32347],{},[278,45644,45645],{"class":280,"line":686},[278,45646,40792],{},[278,45648,45649],{"class":280,"line":1334},[278,45650,40797],{},[278,45652,45653],{"class":280,"line":1375},[278,45654,40802],{},[278,45656,45657],{"class":280,"line":1381},[278,45658,292],{"emptyLinePlaceholder":291},[278,45660,45661],{"class":280,"line":1386},[278,45662,40593],{},[278,45664,45665],{"class":280,"line":1394},[278,45666,40815],{},[278,45668,45669],{"class":280,"line":1406},[278,45670,40820],{},[278,45672,45673],{"class":280,"line":1423},[278,45674,40825],{},[278,45676,45677],{"class":280,"line":1432},[278,45678,8120],{},[278,45680,45681],{"class":280,"line":1437},[278,45682,40834],{},[278,45684,45685],{"class":280,"line":1916},[278,45686,1096],{},[278,45688,45689],{"class":280,"line":1939},[278,45690,2817],{},[11,45692,45693,45694,45696,45697,45699,45700,45702,45703,45705],{},"The only thing to note above is the use of a ",[59,45695,7702],{}," instead of an ",[59,45698,45121],{},", because if our text is multiline then the ",[59,45701,45121],{}," will eat away all our newlines. Also, since we're not hiding the ",[59,45704,7702],{},", we are adding some styles to make it inconsequential.",[24,45707,10634],{"id":10633},[11,45709,45710,45712,45713,45716],{},[59,45711,45410],{}," seems to be working at this point, but it has been made obsolete and may go away in future versions of all browsers. Just to be on the safer side, it is ",[94,45714,45715],{},"better to surround the execCommand call with a try-catch block",". Hopefully in the future DDG will allow us to copy text using the Clipboard API itself.",[11,45718,45719],{},"Do let me know in the comments section if you spot an error, or if the explanation is wrong anywhere.",[11,45721,45722],{},"Thanks for reading :-)",[3065,45724,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":45726},[45727,45728,45729,45730,45732,45733],{"id":45232,"depth":288,"text":45233},{"id":45272,"depth":288,"text":45273},{"id":45363,"depth":288,"text":45364},{"id":45406,"depth":288,"text":45731},"Workaround with execCommand",{"id":45489,"depth":288,"text":45490},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fprogrammatic-copy-to-clipboard-duckduckgo-android\u002F96955551ec39f5fa3904edb052a0e688-aef10b1585.jpeg","2023-01-04T14:42:44.420Z","A workaround to the NotAllowedError you get when trying to use the write \u002F writeText methods of the clipboard API","clchrqwxw000h08jubj8rbaz4",{},"\u002Fprogrammatic-copy-to-clipboard-duckduckgo-android",{"title":45227,"description":45736},"programmatic-copy-to-clipboard-duckduckgo-android",[24599,40298,45743,45744],"duckduckgo","clipboard","yI9h_c8J5md76yzo1jq6TsRLgMm_pRx4ZwD9l1tE5p4",{"id":45747,"title":45748,"body":45749,"cover":49699,"date":49700,"description":49701,"draft":3086,"extension":3087,"hashnodeId":49702,"meta":49703,"navigation":291,"path":49704,"seo":49705,"slug":49706,"stem":49706,"tags":49707,"__hash__":49710},"posts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run.md","Creating a Daily Puzzle Game with React, MongoDB & Google Cloud Run",{"type":8,"value":45750,"toc":49676},[45751,45755,45763,45767,45782,45785,45789,45796,45800,45806,45810,45816,45820,45823,45827,45830,45834,45837,45843,45849,45852,45858,45864,45867,45873,45884,45890,45901,45907,45910,45916,45920,45927,45932,45935,45940,45943,45948,45951,45956,45963,45969,45972,45978,45982,45985,45993,45999,46008,46014,46020,46026,46033,46042,46048,46059,46065,46068,46115,46122,46206,46212,46219,46229,46235,46242,46245,46256,48884,48887,48890,48897,48900,48904,48907,48999,49003,49006,49009,49015,49019,49026,49032,49039,49233,49237,49240,49246,49252,49263,49272,49278,49285,49294,49297,49306,49313,49317,49320,49323,49410,49413,49417,49420,49423,49430,49512,49519,49606,49609,49618,49621,49630,49633,49635,49655,49658,49661,49663,49666,49673],[24,45752,45754],{"id":45753},"about-goldroad","About GoldRoad",[11,45756,45757,45758],{},"GoldRoad is a daily puzzle game in a browser. Your gold is to find the best possible path between two given coins. The best possible path is the one which allows the user to collect the maximum gold. I created this game as an entry for the MongoDb hackathon on ",[47,45759,45762],{"href":45760,"rel":45761},"https:\u002F\u002Fdev.to\u002Fra_jeeves\u002Fcreating-a-puzzle-game-with-reactjs-mongodb-atlas-gcp-cloud-run-1995",[51],"dev.to",[24,45764,45766],{"id":45765},"the-backstory","The backstory",[11,45768,45769,45770,45775,45776,45781],{},"Some months ago I had come across ",[47,45771,45774],{"href":45772,"rel":45773},"https:\u002F\u002Ftwitter.com\u002Fsumul\u002Fstatus\u002F1545430273113866240",[51],"one Twitter post"," announcing a new daily puzzle game. I gave it a try and liked the game so much that I implemented the same using Python and ",[47,45777,45780],{"href":45778,"rel":45779},"https:\u002F\u002Frajeev.dev\u002Fseries\u002Fpython-turtle-puzzle-game",[51],"published blogs"," documenting the process.",[11,45783,45784],{},"From then only, I wanted to create a similar, but different game. Recently I was watching a video explaining the greedy algorithm for finding the best path, and that instantly gelled with the Figure game in my mind. So here it is, after some back-and-forth with different approaches and customizations.",[24,45786,45788],{"id":45787},"app-link-screenshots","App Link & Screenshots",[11,45790,45791,45792],{},"You can play the ",[47,45793,45795],{"href":45239,"rel":45794},[51],"game here",[32,45797,45799],{"id":45798},"the-game-page","The Game Page",[11,45801,45802],{},[3135,45803],{"alt":45804,"src":45805},"The game page screenshot","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002Ftrf0sNxo1-05fa09aca0.png",[32,45807,45809],{"id":45808},"the-about-page","The About Page",[11,45811,45812],{},[3135,45813],{"alt":45814,"src":45815},"The about page screenshot","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002F0c9Q1_0eE-e37a8153e0.png",[24,45817,45819],{"id":45818},"why-use-mongodb-atlas-app-services","Why use MongoDB Atlas & App Services?",[11,45821,45822],{},"I came to know about the hackathon quite late. So it would have been easier to stick to my comfort zone, and use VueJs together with Firebase (of course Firestore\u002FRealtimeDb wasn't an option considering you're supposed to use MongoDB). But what is the point of doing that? The spirit of a hackathon is to use the given technology as much as possible (at least that is what I believe), and so that is what I did.",[24,45824,45826],{"id":45825},"the-implementation","The Implementation",[11,45828,45829],{},"I've divided this section into multiple subsections for ease of reading and following along.",[32,45831,45833],{"id":45832},"remote-setup-creating-mongodb-cluster","Remote Setup: Creating MongoDB Cluster",[11,45835,45836],{},"As soon as you create an account, it asks you to create a cluster, I selected the free shared cluster and used the below settings (You can pick the default options as well). In the additional settings, you can enable the \"Termination Protection\" option (Good to have it available with Free Shared Clusters also). You can give a recallable name to your cluster, or go ahead with the default Cluster0 name.",[11,45838,45839],{},[3135,45840],{"alt":45841,"src":45842},"Creating MongoDB cluster","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FZCq2EGyqY-e8d193421f.png",[11,45844,45845],{},[3135,45846],{"alt":45847,"src":45848},"Additional cluster settings","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FhOOGOHihm-2feda07ac8.png",[11,45850,45851],{},"As soon as you create a cluster it creates a project for you (Project 0) and asks you to create a database user and the location from where you'll be accessing the cluster. For network access, I selected my local IP address (there is a handy button available for this).",[11,45853,45854],{},[3135,45855],{"alt":45856,"src":45857},"Database user creation","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FfmEI7i0SG-8edb75e698.png",[11,45859,45860],{},[3135,45861],{"alt":45862,"src":45863},"Network access option for the cluster","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FKukJiAZ-7-8be27e6079.png",[11,45865,45866],{},"After completing the above steps you come to your cluster dashboard. From there you can browse your databases and collections. Of course, right now we don't have any of that. When you click on the `Collections` tab just below the cluster name, you can see options to load sample data or add your own data.",[11,45868,45869],{},[3135,45870],{"alt":45871,"src":45872},"Browse collection option","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FzzRPo1n17-70090ac7f9.png",[11,45874,45875,45876,45879,45880,45883],{},"Click on ",[59,45877,45878],{},"Add My Own Data",", it will ask you to create a database (pick a suitable name) and name your first collection. For my game, I named my first collection ",[59,45881,45882],{},"games"," (ironic, isn't it :-)). I left the rest of the two checkboxes unchecked (their default state).",[11,45885,45886],{},[3135,45887],{"alt":45888,"src":45889},"Create database and collection","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FRV_ZLTzO4-549fc5f366.png",[11,45891,45892,45893,45896,45897,45900],{},"Now there is only one step remaining, and that is to create an API Key for interacting with the newly created project. Click on the ",[59,45894,45895],{},"Access Manager"," menu at the top, and then click ",[59,45898,45899],{},"Project Access"," just above your project name (Project 0 in this case)",[11,45902,45903],{},[3135,45904],{"alt":45905,"src":45906},"Access manager settings","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FNa0q2ROWP-0544abd609.png",[11,45908,45909],{},"Click on the API Key tab, and create an API Key by giving the appropriate description and the project permissions. Do remember to note down the private key, as you can't retrieve it later on. Additionally, you can also restrict access to your local IP address.",[11,45911,45912],{},[3135,45913],{"alt":45914,"src":45915},"API Key creation setting","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FbwjJakDyZ-efa721e273.png",[32,45917,45919],{"id":45918},"the-local-setup","The Local Setup",[11,45921,45922,45923,45926],{},"To interact with the project from our local machine, we need to install ",[59,45924,45925],{},"realm-cli."," Run the following in your terminal",[11,45928,45929],{},[59,45930,45931],{},"npm install -g mongodb-realm-cli",[11,45933,45934],{},"Now we need to log in to the CLI using the API Key we created in the last step of the previous section.",[11,45936,45937],{},[59,45938,45939],{},"realm-cli login --api-key=\"\u003Cpublic_api_key>\" --private-api-key=\"\u003Cprivate_api_key>\"",[11,45941,45942],{},"Now we are ready with our setup and it is time to start coding. Create a new react project using",[11,45944,45945],{},[59,45946,45947],{},"yarn create react-app my-app",[11,45949,45950],{},"In my case, I moved all of the files to an inner folder called frontend. Then I created a backend folder inside the root directory and created a new MongoDB backend application using",[11,45952,45953],{},[59,45954,45955],{},"realm-cli app create --environment development --cluster \u003Cmy_cluster_name>",[11,45957,45958,45959,45962],{},"Now my project folder structure looks something like the below image (I used ",[59,45960,45961],{},"my-app-backend"," as my app name while creating the backend):",[11,45964,45965],{},[3135,45966],{"alt":45967,"src":45968},"Project folder structure","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FrSDzifBKx-60fb89a111.png",[11,45970,45971],{},"If we see our app services project in the MongoDB Atlas UI it will look something like this (we could have created the project from the UI itself, and then pulled it down using the CLI).",[11,45973,45974],{},[3135,45975],{"alt":45976,"src":45977},"App services project view","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FhRpAaf1qj-37b19aad9a.png",[32,45979,45981],{"id":45980},"creating-an-https-endpoint","Creating an HTTPS Endpoint",[11,45983,45984],{},"After the local project creation, the first task at hand was to be able to create new games and store them in the DB. I needed a backend worker to execute my already tested local game generation code. This backend worker needed to support 2 approaches",[123,45986,45987,45990],{},[74,45988,45989],{},"Ability to be called ad-hoc (so that I can readily create new games in case of emergencies)",[74,45991,45992],{},"Ability to be called automatically through some trigger (so that I need not call it ad-hoc :-))",[11,45994,45995,45996,45998],{},"HTTPS Endpoints backed by functions fit the bill perfectly. I couldn't find a way to create an endpoint interactively using the ",[59,45997,42978],{},", so created the same using the app services UI (Go inside your application in the App Services tab, and pick HTTPS Endpoints from the left sidebar menu).",[11,46000,46001,46002,46005,46006,183],{},"While creating an endpoint you can configure the Authentication for the function backing this endpoint (which is by default Application Auth). Since I will be calling this endpoint ad-hoc (using Postman and such), I had to change the authentication to ",[59,46003,46004],{},"System",". This is not possible to do while creating the endpoint. We can do it afterwards by going to the backing function settings, and changing the authentication to ",[59,46007,46004],{},[11,46009,46010],{},[3135,46011],{"alt":46012,"src":46013},"Creating an HTTPS endpoint - 1","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FxF5FdwntV-e78292f731.png",[11,46015,46016],{},[3135,46017],{"alt":46018,"src":46019},"Creating an HTTPS endpoint - 2","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FBPEjTwOMb-8acb72d408.png",[11,46021,46022],{},[3135,46023],{"alt":46024,"src":46025},"Creating an HTTPS endpoint - 3","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FDzY_13FES-5834428503.png",[11,46027,46028,46029,46032],{},"To give our endpoint a bit of protection, I changed the endpoint authorization to ",[59,46030,46031],{},"Verify Payload Signature",". By doing so, if we want to make a successful call to our endpoint we need to sign our payload with a secret (which we create in the app services UI itself). Since we didn't have any function till now, I also created a new function during the process and accepted the default generated code.",[11,46034,46035,46036,46038,46039,46041],{},"Before testing the function we need to change the function setting and allow it to run as ",[59,46037,46004],{},". We can also make the function ",[59,46040,39861],{}," if we don't want to call it from the client. For logging the function arguments etc., we can also enable the corresponding setting from the same page.",[11,46043,46044],{},[3135,46045],{"alt":46046,"src":46047},"Changing function setting to run as system","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FQ1Ph25V7g-d007e11fa6.png",[11,46049,46050,11160,46053,11160,46056],{},[94,46051,46052],{},"Don't forget to",[59,46054,46055],{},"\"Review & Deploy\"",[94,46057,46058],{},"your changes.",[11,46060,46061,46062,183],{},"Now we're ready to test this endpoint. Launch postman, configure the endpoint URL (You can get it from the HTTPS Endpoint setting we created earlier), select the correct method (POST in my case), add some dummy body, and BAM! ",[59,46063,46064],{},"Send",[11,46066,46067],{},"We should get an error",[269,46069,46071],{"className":5690,"code":46070,"language":1310,"meta":274,"style":274},"{\n    \"error\": \"expected to find Endpoint-Signature in header\",\n    \"error_code\": \"InvalidParameter\",\n    \"link\": \"https:\u002F\u002Frealm.mongodb.com\u002Fgroups\u002F6385ddf6b41c43346bccf9ff\u002F...\"\n}\n",[59,46072,46073,46077,46089,46101,46111],{"__ignoreMap":274},[278,46074,46075],{"class":280,"line":281},[278,46076,524],{"class":302},[278,46078,46079,46082,46084,46087],{"class":280,"line":288},[278,46080,46081],{"class":650},"    \"error\"",[278,46083,1155],{"class":302},[278,46085,46086],{"class":309},"\"expected to find Endpoint-Signature in header\"",[278,46088,660],{"class":302},[278,46090,46091,46094,46096,46099],{"class":280,"line":295},[278,46092,46093],{"class":650},"    \"error_code\"",[278,46095,1155],{"class":302},[278,46097,46098],{"class":309},"\"InvalidParameter\"",[278,46100,660],{"class":302},[278,46102,46103,46106,46108],{"class":280,"line":316},[278,46104,46105],{"class":650},"    \"link\"",[278,46107,1155],{"class":302},[278,46109,46110],{"class":309},"\"https:\u002F\u002Frealm.mongodb.com\u002Fgroups\u002F6385ddf6b41c43346bccf9ff\u002F...\"\n",[278,46112,46113],{"class":280,"line":322},[278,46114,617],{"class":302},[11,46116,46117,46118,46121],{},"This is expected because we had configured some auth mechanism earlier, so we need to sign our payload using the secret key we had created, and pass this in a header with the request. In postman, we can do so by adding the following code in the ",[59,46119,46120],{},"Pre-request Script"," tab",[269,46123,46127],{"className":46124,"code":46125,"language":46126,"meta":274,"style":274},"language-js shiki shiki-themes github-light github-dark","const signBytes = CryptoJS.HmacSHA256(pm.request.body.raw, '\u003Cyour_secret_code>');\nconst signHex = CryptoJS.enc.Hex.stringify(signBytes);\npm.request.headers.add({\n    key: \"Endpoint-Signature\",\n    value: \"sha256=\"+signHex\n});\n","js",[59,46128,46129,46152,46169,46179,46189,46202],{"__ignoreMap":274},[278,46130,46131,46133,46136,46138,46141,46144,46147,46150],{"class":280,"line":281},[278,46132,5416],{"class":298},[278,46134,46135],{"class":650}," signBytes",[278,46137,764],{"class":298},[278,46139,46140],{"class":302}," CryptoJS.",[278,46142,46143],{"class":333},"HmacSHA256",[278,46145,46146],{"class":302},"(pm.request.body.raw, ",[278,46148,46149],{"class":309},"'\u003Cyour_secret_code>'",[278,46151,1280],{"class":302},[278,46153,46154,46156,46159,46161,46164,46166],{"class":280,"line":288},[278,46155,5416],{"class":298},[278,46157,46158],{"class":650}," signHex",[278,46160,764],{"class":298},[278,46162,46163],{"class":302}," CryptoJS.enc.Hex.",[278,46165,2235],{"class":333},[278,46167,46168],{"class":302},"(signBytes);\n",[278,46170,46171,46174,46177],{"class":280,"line":295},[278,46172,46173],{"class":302},"pm.request.headers.",[278,46175,46176],{"class":333},"add",[278,46178,637],{"class":302},[278,46180,46181,46184,46187],{"class":280,"line":316},[278,46182,46183],{"class":302},"    key: ",[278,46185,46186],{"class":309},"\"Endpoint-Signature\"",[278,46188,660],{"class":302},[278,46190,46191,46194,46197,46199],{"class":280,"line":322},[278,46192,46193],{"class":302},"    value: ",[278,46195,46196],{"class":309},"\"sha256=\"",[278,46198,1345],{"class":298},[278,46200,46201],{"class":302},"signHex\n",[278,46203,46204],{"class":280,"line":327},[278,46205,3693],{"class":302},[11,46207,46208],{},[3135,46209],{"alt":46210,"src":46211},"Pre-request Script in postman","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FJ63jR8Ofa-a619e775a0.png",[11,46213,46214,46215,46218],{},"If we hit our backend again, we should get the correct response ",[59,46216,46217],{},"\"Hello World\""," (if you didn't change anything in the default function code).",[11,46220,46221,46222,46225,46226],{},"I struggled with this for some time, the reason being the App services UI itself. While creating the endpoint it shows how to call your endpoint using ",[59,46223,46224],{},"curl."," If you look closely at the image below, it shows the header name as ",[59,46227,46228],{},"\"X-Hook-Signature\".",[11,46230,46231],{},[3135,46232],{"alt":46233,"src":46234},"curl function call as shown in the UI","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FQWhPyPe3a-db6a00c8b1.png",[11,46236,46237,46238,46241],{},"After doing some Google searches, and also looking at the error messages in Postman (Why would I look there when the UI itself says ",[59,46239,46240],{},"X-Hook-Signature","), I was able to figure out the issue. I also needed some time to figure out the \"sha256=\" prefix for the header value. But all's well that ends well :-)",[11,46243,46244],{},"Since our endpoint is working, we can replace the default function code with the below code, which generates new games, find out their solution and saves them to the games collection in our database.",[11,46246,46247,46248,46251,46252,46255],{},"We can do the change either in the functions UI in the browser, or we can pull down the changes to our local project (go to ",[59,46249,46250],{},"backend -> my-app-backend"," folder in your terminal, and do ",[59,46253,46254],{},"realm-cli pull."," This will pull the remote changes to your local setup). Then go to the functions folder inside the backend project, and update the code of the appropriate function file.",[269,46257,46259],{"className":46124,"code":46258,"language":46126,"meta":274,"style":274},"const ROWS = 6;\nconst COLS = 6;\n\n\u002F\u002F Generate a random number between min (included) & max (excluded)\nconst randomInt = (min, max) => {\n  return Math.floor(Math.random() * (max - min)) + min;\n};\n\nconst getCoinsWithWalls = (start, end, count) => {\n  const coinColIndices = [];\n  while (coinColIndices.length \u003C count) {\n    const index = randomInt(start, end);\n    if (!coinColIndices.includes(index)) {\n      coinColIndices.push(index);\n    }\n  }\n\n  return coinColIndices;\n};\n\nconst addJob = (jobs, src, currJob) => {\n  jobs.push({\n    coins: JSON.parse(JSON.stringify(currJob.coins)),\n    src,\n    dst: currJob.dst,\n    pastMoves: JSON.parse(JSON.stringify(currJob.pastMoves)),\n    total: currJob.total,\n  });\n};\n\nconst handleJob = (jobs, job) => {\n  const row = job.src[0];\n  const col = job.src[1];\n  const srcNode = job.coins[row][col];\n\n  srcNode.finished = true;\n  if (row === job.dst[0] && col === job.dst[1]) {\n    job.total += srcNode.value;\n    job.pastMoves.push(`${job.dst[0]}${job.dst[1]}`);\n    return true;\n  }\n\n  const neighbors = {\n    prevNode: col > 0 ? job.coins[row][col - 1] : null,\n    nextNode: col \u003C COLS - 1 ? job.coins[row][col + 1] : null,\n    topNode: row > 0 ? job.coins[row - 1][col] : null,\n    bottomNode: row \u003C ROWS - 1 ? job.coins[row + 1][col] : null,\n  };\n\n  job.total += srcNode.value;\n  job.pastMoves.push(srcNode.id);\n\n  for (const key in neighbors) {\n    const neighbor = neighbors[key];\n    if (neighbor && !neighbor.finished) {\n      if (key === 'prevNode' && neighbor.wall !== 2 && srcNode.wall !== 4) {\n        addJob(jobs, [row, col - 1], job);\n      }\n\n      if (key === 'nextNode' && neighbor.wall !== 4 && srcNode.wall !== 2) {\n        addJob(jobs, [row, col + 1], job);\n      }\n\n      if (key === 'topNode' && neighbor.wall !== 3 && srcNode.wall !== 1) {\n        addJob(jobs, [row - 1, col], job);\n      }\n\n      if (key === 'bottomNode' && neighbor.wall !== 1 && srcNode.wall !== 3) {\n        addJob(jobs, [row + 1, col], job);\n      }\n    }\n  }\n\n  return false;\n};\n\nconst findBestRoute = (coins, start, end) => {\n  const src = [parseInt(start[0]), parseInt(start[1])];\n  const dst = [parseInt(end[0]), parseInt(end[1])];\n\n  const jobs = [{ coins, src, dst, pastMoves: [], total: 0 }];\n  const results = [];\n\n  while (jobs.length) {\n    const job = jobs.shift();\n    if (handleJob(jobs, job)) {\n      results.push({\n        total: job.total,\n        moves: job.pastMoves.length,\n        path: job.pastMoves,\n      });\n    }\n  }\n\n  if (results.length) {\n    results.sort((result1, result2) => {\n      return result2.total - result1.total;\n    });\n\n    return results[0];\n  } else {\n    console.log(`No valid path found`);\n  }\n};\n\nexports = async function (req) {\n  let reqBody = null;\n  if (req) {\n    if (req.body) {\n      console.log(`got a req body: ${req.body.text()}`);\n      reqBody = JSON.parse(req.body.text());\n    } else {\n      console.log(`got a req without req body: ${JSON.stringify(req)}`);\n    }\n  }\n\n  const coins = [];\n  for (let row = 0; row \u003C ROWS; row++) {\n    coins.push([]);\n\n    const blockages = getCoinsWithWalls(0, COLS, 2);\n    for (let col = 0; col \u003C COLS; col++) {\n      const coin = {\n        id: `${row}${col}`,\n        value: randomInt(1, 7),\n        wall: 0,\n      };\n\n      if (blockages.includes(col)) {\n        coin.wall = randomInt(1, 5);\n      }\n\n      coins[row].push(coin);\n    }\n  }\n\n  const start = `${randomInt(2, 4)}${randomInt(2, 4)}`;\n  let end = randomInt(1, 5);\n  if (end === 1) {\n    end = '00';\n  } else if (end === 2) {\n    end = `0${COLS - 1}`;\n  } else if (end === 3) {\n    end = `${ROWS - 1}0`;\n  } else {\n    end = `${ROWS - 1}${COLS - 1}`;\n  }\n\n  const date = new Date();\n  const gameEntry = {\n    coins,\n    start,\n    end,\n    active: false,\n    createdAt: date,\n    updatedAt: date,\n  };\n\n  const startTime = Date.now();\n  const bestMove = findBestRoute(JSON.parse(JSON.stringify(coins)), start, end);\n  console.log(\n    `Total time taken for finding bestRoute: ${Date.now() - startTime} ms`\n  );\n\n  if (bestMove) {\n    console.log(`best path: ${JSON.stringify(bestMove)}`);\n    gameEntry.maxScore = bestMove.total;\n    gameEntry.maxScoreMoves = bestMove.moves;\n    gameEntry.hints = bestMove.path;\n\n    const mongoDb = context.services.get('cluster-service-name').db('db-name');\n    const gamesCollection = mongoDb.collection('games');\n    const appCollection = mongoDb.collection('app');\n    const config = await appCollection.findOne({ type: 'config' });\n\n    console.log('fetch config data:', JSON.stringify(config));\n    console.log('lastPlayableGame:', config.lastPlayableGame);\n\n    if (config) {\n      if (config.lastPlayableGame) {\n        const lastPlayableDate = config.lastPlayableGame.playableAt;\n        lastPlayableDate.setUTCDate(lastPlayableDate.getDate() + 1);\n        gameEntry.playableAt = lastPlayableDate;\n        gameEntry.gameNo = config.lastPlayableGame.gameNo + 1;\n        if (reqBody) {\n          if (reqBody.active) {\n            gameEntry.active = true;\n          }\n\n          if (reqBody.current) {\n            gameEntry.current = true;\n          }\n        }\n      } else {\n        const playableDate = new Date();\n        playableDate.setUTCHours(0, 0, 0, 0);\n        gameEntry.playableAt = playableDate;\n        gameEntry.gameNo = 1;\n        gameEntry.current = true;\n        gameEntry.active = true;\n      }\n    }\n\n    let result = await gamesCollection.insertOne(gameEntry);\n    console.log(\n      `Successfully inserted game with _id: ${JSON.stringify(result)}`\n    );\n\n    result = await appCollection.updateOne(\n      { type: 'config' },\n      {\n        $set: {\n          lastPlayableGame: {\n            playableAt: gameEntry.playableAt,\n            gameNo: gameEntry.gameNo,\n            _id: result.insertedId,\n          },\n        },\n      }\n    );\n\n    console.log('result of update operation: ', JSON.stringify(result));\n  }\n\n  return gameEntry;\n};\n",[59,46260,46261,46275,46288,46292,46297,46320,46353,46357,46361,46390,46401,46416,46430,46446,46456,46460,46464,46468,46475,46479,46483,46512,46521,46543,46548,46553,46575,46580,46584,46588,46592,46616,46632,46647,46659,46663,46674,46703,46713,46757,46765,46769,46773,46784,46810,46839,46866,46895,46899,46903,46912,46922,46926,46942,46954,46968,47003,47018,47022,47026,47055,47067,47071,47075,47105,47119,47123,47127,47156,47168,47172,47176,47180,47184,47192,47196,47200,47228,47259,47287,47291,47308,47318,47322,47333,47350,47362,47371,47376,47385,47390,47394,47398,47402,47406,47417,47440,47452,47456,47460,47471,47479,47492,47496,47500,47504,47521,47534,47541,47548,47575,47595,47603,47630,47634,47638,47642,47653,47682,47692,47696,47723,47751,47762,47781,47800,47809,47813,47817,47829,47849,47853,47857,47867,47871,47875,47879,47920,47941,47954,47966,47982,48001,48017,48037,48045,48071,48075,48079,48095,48106,48111,48116,48121,48130,48135,48140,48144,48148,48163,48193,48201,48222,48226,48230,48237,48265,48275,48285,48295,48299,48330,48352,48372,48396,48400,48422,48436,48440,48447,48454,48466,48488,48498,48514,48521,48528,48539,48543,48548,48556,48568,48573,48578,48587,48603,48630,48640,48651,48663,48675,48680,48685,48690,48710,48719,48741,48746,48751,48768,48778,48783,48789,48795,48801,48807,48813,48818,48823,48828,48833,48838,48861,48866,48871,48879],{"__ignoreMap":274},[278,46262,46263,46265,46268,46270,46273],{"class":280,"line":281},[278,46264,5416],{"class":298},[278,46266,46267],{"class":650}," ROWS",[278,46269,764],{"class":298},[278,46271,46272],{"class":650}," 6",[278,46274,313],{"class":302},[278,46276,46277,46279,46282,46284,46286],{"class":280,"line":288},[278,46278,5416],{"class":298},[278,46280,46281],{"class":650}," COLS",[278,46283,764],{"class":298},[278,46285,46272],{"class":650},[278,46287,313],{"class":302},[278,46289,46290],{"class":280,"line":295},[278,46291,292],{"emptyLinePlaceholder":291},[278,46293,46294],{"class":280,"line":316},[278,46295,46296],{"class":284},"\u002F\u002F Generate a random number between min (included) & max (excluded)\n",[278,46298,46299,46301,46304,46306,46308,46310,46312,46314,46316,46318],{"class":280,"line":322},[278,46300,5416],{"class":298},[278,46302,46303],{"class":333}," randomInt",[278,46305,764],{"class":298},[278,46307,1245],{"class":302},[278,46309,5545],{"class":501},[278,46311,1708],{"class":302},[278,46313,5564],{"class":501},[278,46315,1845],{"class":302},[278,46317,1848],{"class":298},[278,46319,876],{"class":302},[278,46321,46322,46324,46327,46330,46333,46336,46338,46340,46343,46345,46348,46350],{"class":280,"line":327},[278,46323,343],{"class":298},[278,46325,46326],{"class":302}," Math.",[278,46328,46329],{"class":333},"floor",[278,46331,46332],{"class":302},"(Math.",[278,46334,46335],{"class":333},"random",[278,46337,1342],{"class":302},[278,46339,1351],{"class":298},[278,46341,46342],{"class":302}," (max ",[278,46344,16522],{"class":298},[278,46346,46347],{"class":302}," min)) ",[278,46349,1345],{"class":298},[278,46351,46352],{"class":302}," min;\n",[278,46354,46355],{"class":280,"line":340},[278,46356,2817],{"class":302},[278,46358,46359],{"class":280,"line":349},[278,46360,292],{"emptyLinePlaceholder":291},[278,46362,46363,46365,46368,46370,46372,46374,46376,46379,46381,46384,46386,46388],{"class":280,"line":375},[278,46364,5416],{"class":298},[278,46366,46367],{"class":333}," getCoinsWithWalls",[278,46369,764],{"class":298},[278,46371,1245],{"class":302},[278,46373,6610],{"class":501},[278,46375,1708],{"class":302},[278,46377,46378],{"class":501},"end",[278,46380,1708],{"class":302},[278,46382,46383],{"class":501},"count",[278,46385,1845],{"class":302},[278,46387,1848],{"class":298},[278,46389,876],{"class":302},[278,46391,46392,46394,46397,46399],{"class":280,"line":386},[278,46393,758],{"class":298},[278,46395,46396],{"class":650}," coinColIndices",[278,46398,764],{"class":298},[278,46400,6483],{"class":302},[278,46402,46403,46406,46409,46411,46413],{"class":280,"line":397},[278,46404,46405],{"class":298},"  while",[278,46407,46408],{"class":302}," (coinColIndices.",[278,46410,15645],{"class":650},[278,46412,24568],{"class":298},[278,46414,46415],{"class":302}," count) {\n",[278,46417,46418,46420,46423,46425,46427],{"class":280,"line":408},[278,46419,1112],{"class":298},[278,46421,46422],{"class":650}," index",[278,46424,764],{"class":298},[278,46426,46303],{"class":333},[278,46428,46429],{"class":302},"(start, end);\n",[278,46431,46432,46434,46436,46438,46441,46443],{"class":280,"line":433},[278,46433,1242],{"class":298},[278,46435,1245],{"class":302},[278,46437,1209],{"class":298},[278,46439,46440],{"class":302},"coinColIndices.",[278,46442,13297],{"class":333},[278,46444,46445],{"class":302},"(index)) {\n",[278,46447,46448,46451,46453],{"class":280,"line":454},[278,46449,46450],{"class":302},"      coinColIndices.",[278,46452,6524],{"class":333},[278,46454,46455],{"class":302},"(index);\n",[278,46457,46458],{"class":280,"line":475},[278,46459,1285],{"class":302},[278,46461,46462],{"class":280,"line":496},[278,46463,1096],{"class":302},[278,46465,46466],{"class":280,"line":505},[278,46467,292],{"emptyLinePlaceholder":291},[278,46469,46470,46472],{"class":280,"line":516},[278,46471,343],{"class":298},[278,46473,46474],{"class":302}," coinColIndices;\n",[278,46476,46477],{"class":280,"line":527},[278,46478,2817],{"class":302},[278,46480,46481],{"class":280,"line":533},[278,46482,292],{"emptyLinePlaceholder":291},[278,46484,46485,46487,46490,46492,46494,46497,46499,46501,46503,46506,46508,46510],{"class":280,"line":539},[278,46486,5416],{"class":298},[278,46488,46489],{"class":333}," addJob",[278,46491,764],{"class":298},[278,46493,1245],{"class":302},[278,46495,46496],{"class":501},"jobs",[278,46498,1708],{"class":302},[278,46500,24951],{"class":501},[278,46502,1708],{"class":302},[278,46504,46505],{"class":501},"currJob",[278,46507,1845],{"class":302},[278,46509,1848],{"class":298},[278,46511,876],{"class":302},[278,46513,46514,46517,46519],{"class":280,"line":545},[278,46515,46516],{"class":302},"  jobs.",[278,46518,6524],{"class":333},[278,46520,637],{"class":302},[278,46522,46523,46526,46528,46530,46532,46534,46536,46538,46540],{"class":280,"line":551},[278,46524,46525],{"class":302},"    coins: ",[278,46527,2230],{"class":650},[278,46529,183],{"class":302},[278,46531,12068],{"class":333},[278,46533,1126],{"class":302},[278,46535,2230],{"class":650},[278,46537,183],{"class":302},[278,46539,2235],{"class":333},[278,46541,46542],{"class":302},"(currJob.coins)),\n",[278,46544,46545],{"class":280,"line":557},[278,46546,46547],{"class":302},"    src,\n",[278,46549,46550],{"class":280,"line":567},[278,46551,46552],{"class":302},"    dst: currJob.dst,\n",[278,46554,46555,46558,46560,46562,46564,46566,46568,46570,46572],{"class":280,"line":577},[278,46556,46557],{"class":302},"    pastMoves: ",[278,46559,2230],{"class":650},[278,46561,183],{"class":302},[278,46563,12068],{"class":333},[278,46565,1126],{"class":302},[278,46567,2230],{"class":650},[278,46569,183],{"class":302},[278,46571,2235],{"class":333},[278,46573,46574],{"class":302},"(currJob.pastMoves)),\n",[278,46576,46577],{"class":280,"line":587},[278,46578,46579],{"class":302},"    total: currJob.total,\n",[278,46581,46582],{"class":280,"line":597},[278,46583,2037],{"class":302},[278,46585,46586],{"class":280,"line":608},[278,46587,2817],{"class":302},[278,46589,46590],{"class":280,"line":614},[278,46591,292],{"emptyLinePlaceholder":291},[278,46593,46594,46596,46599,46601,46603,46605,46607,46610,46612,46614],{"class":280,"line":620},[278,46595,5416],{"class":298},[278,46597,46598],{"class":333}," handleJob",[278,46600,764],{"class":298},[278,46602,1245],{"class":302},[278,46604,46496],{"class":501},[278,46606,1708],{"class":302},[278,46608,46609],{"class":501},"job",[278,46611,1845],{"class":302},[278,46613,1848],{"class":298},[278,46615,876],{"class":302},[278,46617,46618,46620,46623,46625,46628,46630],{"class":280,"line":625},[278,46619,758],{"class":298},[278,46621,46622],{"class":650}," row",[278,46624,764],{"class":298},[278,46626,46627],{"class":302}," job.src[",[278,46629,2012],{"class":650},[278,46631,11714],{"class":302},[278,46633,46634,46636,46639,46641,46643,46645],{"class":280,"line":640},[278,46635,758],{"class":298},[278,46637,46638],{"class":650}," col",[278,46640,764],{"class":298},[278,46642,46627],{"class":302},[278,46644,17444],{"class":650},[278,46646,11714],{"class":302},[278,46648,46649,46651,46654,46656],{"class":280,"line":663},[278,46650,758],{"class":298},[278,46652,46653],{"class":650}," srcNode",[278,46655,764],{"class":298},[278,46657,46658],{"class":302}," job.coins[row][col];\n",[278,46660,46661],{"class":280,"line":669},[278,46662,292],{"emptyLinePlaceholder":291},[278,46664,46665,46668,46670,46672],{"class":280,"line":680},[278,46666,46667],{"class":302},"  srcNode.finished ",[278,46669,358],{"class":298},[278,46671,6575],{"class":650},[278,46673,313],{"class":302},[278,46675,46676,46678,46681,46683,46686,46688,46690,46692,46695,46697,46699,46701],{"class":280,"line":686},[278,46677,1062],{"class":298},[278,46679,46680],{"class":302}," (row ",[278,46682,2451],{"class":298},[278,46684,46685],{"class":302}," job.dst[",[278,46687,2012],{"class":650},[278,46689,17763],{"class":302},[278,46691,1068],{"class":298},[278,46693,46694],{"class":302}," col ",[278,46696,2451],{"class":298},[278,46698,46685],{"class":302},[278,46700,17444],{"class":650},[278,46702,12520],{"class":302},[278,46704,46705,46708,46710],{"class":280,"line":1334},[278,46706,46707],{"class":302},"    job.total ",[278,46709,6271],{"class":298},[278,46711,46712],{"class":302}," srcNode.value;\n",[278,46714,46715,46718,46720,46722,46724,46726,46728,46731,46733,46735,46738,46741,46743,46745,46747,46749,46751,46753,46755],{"class":280,"line":1375},[278,46716,46717],{"class":302},"    job.pastMoves.",[278,46719,6524],{"class":333},[278,46721,1126],{"class":302},[278,46723,34095],{"class":309},[278,46725,46609],{"class":302},[278,46727,183],{"class":309},[278,46729,46730],{"class":302},"dst",[278,46732,29860],{"class":309},[278,46734,2012],{"class":650},[278,46736,46737],{"class":309},"]",[278,46739,46740],{"class":309},"}${",[278,46742,46609],{"class":302},[278,46744,183],{"class":309},[278,46746,46730],{"class":302},[278,46748,29860],{"class":309},[278,46750,17444],{"class":650},[278,46752,46737],{"class":309},[278,46754,1277],{"class":309},[278,46756,1280],{"class":302},[278,46758,46759,46761,46763],{"class":280,"line":1381},[278,46760,1088],{"class":298},[278,46762,6575],{"class":650},[278,46764,313],{"class":302},[278,46766,46767],{"class":280,"line":1386},[278,46768,1096],{"class":302},[278,46770,46771],{"class":280,"line":1394},[278,46772,292],{"emptyLinePlaceholder":291},[278,46774,46775,46777,46780,46782],{"class":280,"line":1406},[278,46776,758],{"class":298},[278,46778,46779],{"class":650}," neighbors",[278,46781,764],{"class":298},[278,46783,876],{"class":302},[278,46785,46786,46789,46791,46793,46795,46798,46800,46802,46804,46806,46808],{"class":280,"line":1423},[278,46787,46788],{"class":302},"    prevNode: col ",[278,46790,1074],{"class":298},[278,46792,6588],{"class":650},[278,46794,2752],{"class":298},[278,46796,46797],{"class":302}," job.coins[row][col ",[278,46799,16522],{"class":298},[278,46801,6274],{"class":650},[278,46803,17763],{"class":302},[278,46805,960],{"class":298},[278,46807,1035],{"class":650},[278,46809,660],{"class":302},[278,46811,46812,46815,46817,46819,46821,46823,46825,46827,46829,46831,46833,46835,46837],{"class":280,"line":1432},[278,46813,46814],{"class":302},"    nextNode: col ",[278,46816,1702],{"class":298},[278,46818,46281],{"class":650},[278,46820,1357],{"class":298},[278,46822,6274],{"class":650},[278,46824,2752],{"class":298},[278,46826,46797],{"class":302},[278,46828,1345],{"class":298},[278,46830,6274],{"class":650},[278,46832,17763],{"class":302},[278,46834,960],{"class":298},[278,46836,1035],{"class":650},[278,46838,660],{"class":302},[278,46840,46841,46844,46846,46848,46850,46853,46855,46857,46860,46862,46864],{"class":280,"line":1437},[278,46842,46843],{"class":302},"    topNode: row ",[278,46845,1074],{"class":298},[278,46847,6588],{"class":650},[278,46849,2752],{"class":298},[278,46851,46852],{"class":302}," job.coins[row ",[278,46854,16522],{"class":298},[278,46856,6274],{"class":650},[278,46858,46859],{"class":302},"][col] ",[278,46861,960],{"class":298},[278,46863,1035],{"class":650},[278,46865,660],{"class":302},[278,46867,46868,46871,46873,46875,46877,46879,46881,46883,46885,46887,46889,46891,46893],{"class":280,"line":1916},[278,46869,46870],{"class":302},"    bottomNode: row ",[278,46872,1702],{"class":298},[278,46874,46267],{"class":650},[278,46876,1357],{"class":298},[278,46878,6274],{"class":650},[278,46880,2752],{"class":298},[278,46882,46852],{"class":302},[278,46884,1345],{"class":298},[278,46886,6274],{"class":650},[278,46888,46859],{"class":302},[278,46890,960],{"class":298},[278,46892,1035],{"class":650},[278,46894,660],{"class":302},[278,46896,46897],{"class":280,"line":1939},[278,46898,901],{"class":302},[278,46900,46901],{"class":280,"line":1949},[278,46902,292],{"emptyLinePlaceholder":291},[278,46904,46905,46908,46910],{"class":280,"line":1954},[278,46906,46907],{"class":302},"  job.total ",[278,46909,6271],{"class":298},[278,46911,46712],{"class":302},[278,46913,46914,46917,46919],{"class":280,"line":1959},[278,46915,46916],{"class":302},"  job.pastMoves.",[278,46918,6524],{"class":333},[278,46920,46921],{"class":302},"(srcNode.id);\n",[278,46923,46924],{"class":280,"line":1985},[278,46925,292],{"emptyLinePlaceholder":291},[278,46927,46928,46930,46932,46934,46936,46939],{"class":280,"line":1990},[278,46929,12738],{"class":298},[278,46931,1245],{"class":302},[278,46933,5416],{"class":298},[278,46935,22002],{"class":650},[278,46937,46938],{"class":298}," in",[278,46940,46941],{"class":302}," neighbors) {\n",[278,46943,46944,46946,46949,46951],{"class":280,"line":1997},[278,46945,1112],{"class":298},[278,46947,46948],{"class":650}," neighbor",[278,46950,764],{"class":298},[278,46952,46953],{"class":302}," neighbors[key];\n",[278,46955,46956,46958,46961,46963,46965],{"class":280,"line":2006},[278,46957,1242],{"class":298},[278,46959,46960],{"class":302}," (neighbor ",[278,46962,1068],{"class":298},[278,46964,6192],{"class":298},[278,46966,46967],{"class":302},"neighbor.finished) {\n",[278,46969,46970,46972,46975,46977,46980,46983,46986,46988,46991,46993,46996,46998,47001],{"class":280,"line":2018},[278,46971,6207],{"class":298},[278,46973,46974],{"class":302}," (key ",[278,46976,2451],{"class":298},[278,46978,46979],{"class":309}," 'prevNode'",[278,46981,46982],{"class":298}," &&",[278,46984,46985],{"class":302}," neighbor.wall ",[278,46987,2092],{"class":298},[278,46989,46990],{"class":650}," 2",[278,46992,46982],{"class":298},[278,46994,46995],{"class":302}," srcNode.wall ",[278,46997,2092],{"class":298},[278,46999,47000],{"class":650}," 4",[278,47002,1718],{"class":302},[278,47004,47005,47008,47011,47013,47015],{"class":280,"line":2029},[278,47006,47007],{"class":333},"        addJob",[278,47009,47010],{"class":302},"(jobs, [row, col ",[278,47012,16522],{"class":298},[278,47014,6274],{"class":650},[278,47016,47017],{"class":302},"], job);\n",[278,47019,47020],{"class":280,"line":2034},[278,47021,6234],{"class":302},[278,47023,47024],{"class":280,"line":2040},[278,47025,292],{"emptyLinePlaceholder":291},[278,47027,47028,47030,47032,47034,47037,47039,47041,47043,47045,47047,47049,47051,47053],{"class":280,"line":2045},[278,47029,6207],{"class":298},[278,47031,46974],{"class":302},[278,47033,2451],{"class":298},[278,47035,47036],{"class":309}," 'nextNode'",[278,47038,46982],{"class":298},[278,47040,46985],{"class":302},[278,47042,2092],{"class":298},[278,47044,47000],{"class":650},[278,47046,46982],{"class":298},[278,47048,46995],{"class":302},[278,47050,2092],{"class":298},[278,47052,46990],{"class":650},[278,47054,1718],{"class":302},[278,47056,47057,47059,47061,47063,47065],{"class":280,"line":2068},[278,47058,47007],{"class":333},[278,47060,47010],{"class":302},[278,47062,1345],{"class":298},[278,47064,6274],{"class":650},[278,47066,47017],{"class":302},[278,47068,47069],{"class":280,"line":2099},[278,47070,6234],{"class":302},[278,47072,47073],{"class":280,"line":6428},[278,47074,292],{"emptyLinePlaceholder":291},[278,47076,47077,47079,47081,47083,47086,47088,47090,47092,47095,47097,47099,47101,47103],{"class":280,"line":6439},[278,47078,6207],{"class":298},[278,47080,46974],{"class":302},[278,47082,2451],{"class":298},[278,47084,47085],{"class":309}," 'topNode'",[278,47087,46982],{"class":298},[278,47089,46985],{"class":302},[278,47091,2092],{"class":298},[278,47093,47094],{"class":650}," 3",[278,47096,46982],{"class":298},[278,47098,46995],{"class":302},[278,47100,2092],{"class":298},[278,47102,6274],{"class":650},[278,47104,1718],{"class":302},[278,47106,47107,47109,47112,47114,47116],{"class":280,"line":6450},[278,47108,47007],{"class":333},[278,47110,47111],{"class":302},"(jobs, [row ",[278,47113,16522],{"class":298},[278,47115,6274],{"class":650},[278,47117,47118],{"class":302},", col], job);\n",[278,47120,47121],{"class":280,"line":6455},[278,47122,6234],{"class":302},[278,47124,47125],{"class":280,"line":6460},[278,47126,292],{"emptyLinePlaceholder":291},[278,47128,47129,47131,47133,47135,47138,47140,47142,47144,47146,47148,47150,47152,47154],{"class":280,"line":6475},[278,47130,6207],{"class":298},[278,47132,46974],{"class":302},[278,47134,2451],{"class":298},[278,47136,47137],{"class":309}," 'bottomNode'",[278,47139,46982],{"class":298},[278,47141,46985],{"class":302},[278,47143,2092],{"class":298},[278,47145,6274],{"class":650},[278,47147,46982],{"class":298},[278,47149,46995],{"class":302},[278,47151,2092],{"class":298},[278,47153,47094],{"class":650},[278,47155,1718],{"class":302},[278,47157,47158,47160,47162,47164,47166],{"class":280,"line":6486},[278,47159,47007],{"class":333},[278,47161,47111],{"class":302},[278,47163,1345],{"class":298},[278,47165,6274],{"class":650},[278,47167,47118],{"class":302},[278,47169,47170],{"class":280,"line":6491},[278,47171,6234],{"class":302},[278,47173,47174],{"class":280,"line":6518},[278,47175,1285],{"class":302},[278,47177,47178],{"class":280,"line":6530},[278,47179,1096],{"class":302},[278,47181,47182],{"class":280,"line":6542},[278,47183,292],{"emptyLinePlaceholder":291},[278,47185,47186,47188,47190],{"class":280,"line":6547},[278,47187,343],{"class":298},[278,47189,6872],{"class":650},[278,47191,313],{"class":302},[278,47193,47194],{"class":280,"line":6552},[278,47195,2817],{"class":302},[278,47197,47198],{"class":280,"line":6567},[278,47199,292],{"emptyLinePlaceholder":291},[278,47201,47202,47204,47207,47209,47211,47214,47216,47218,47220,47222,47224,47226],{"class":280,"line":6580},[278,47203,5416],{"class":298},[278,47205,47206],{"class":333}," findBestRoute",[278,47208,764],{"class":298},[278,47210,1245],{"class":302},[278,47212,47213],{"class":501},"coins",[278,47215,1708],{"class":302},[278,47217,6610],{"class":501},[278,47219,1708],{"class":302},[278,47221,46378],{"class":501},[278,47223,1845],{"class":302},[278,47225,1848],{"class":298},[278,47227,876],{"class":302},[278,47229,47230,47232,47235,47237,47239,47242,47245,47247,47250,47252,47254,47256],{"class":280,"line":6593},[278,47231,758],{"class":298},[278,47233,47234],{"class":650}," src",[278,47236,764],{"class":298},[278,47238,13367],{"class":302},[278,47240,47241],{"class":333},"parseInt",[278,47243,47244],{"class":302},"(start[",[278,47246,2012],{"class":650},[278,47248,47249],{"class":302},"]), ",[278,47251,47241],{"class":333},[278,47253,47244],{"class":302},[278,47255,17444],{"class":650},[278,47257,47258],{"class":302},"])];\n",[278,47260,47261,47263,47266,47268,47270,47272,47275,47277,47279,47281,47283,47285],{"class":280,"line":6605},[278,47262,758],{"class":298},[278,47264,47265],{"class":650}," dst",[278,47267,764],{"class":298},[278,47269,13367],{"class":302},[278,47271,47241],{"class":333},[278,47273,47274],{"class":302},"(end[",[278,47276,2012],{"class":650},[278,47278,47249],{"class":302},[278,47280,47241],{"class":333},[278,47282,47274],{"class":302},[278,47284,17444],{"class":650},[278,47286,47258],{"class":302},[278,47288,47289],{"class":280,"line":6620},[278,47290,292],{"emptyLinePlaceholder":291},[278,47292,47293,47295,47298,47300,47303,47305],{"class":280,"line":6625},[278,47294,758],{"class":298},[278,47296,47297],{"class":650}," jobs",[278,47299,764],{"class":298},[278,47301,47302],{"class":302}," [{ coins, src, dst, pastMoves: [], total: ",[278,47304,2012],{"class":650},[278,47306,47307],{"class":302}," }];\n",[278,47309,47310,47312,47314,47316],{"class":280,"line":6633},[278,47311,758],{"class":298},[278,47313,18103],{"class":650},[278,47315,764],{"class":298},[278,47317,6483],{"class":302},[278,47319,47320],{"class":280,"line":6643},[278,47321,292],{"emptyLinePlaceholder":291},[278,47323,47324,47326,47329,47331],{"class":280,"line":6657},[278,47325,46405],{"class":298},[278,47327,47328],{"class":302}," (jobs.",[278,47330,15645],{"class":650},[278,47332,1718],{"class":302},[278,47334,47335,47337,47340,47342,47345,47348],{"class":280,"line":6665},[278,47336,1112],{"class":298},[278,47338,47339],{"class":650}," job",[278,47341,764],{"class":298},[278,47343,47344],{"class":302}," jobs.",[278,47346,47347],{"class":333},"shift",[278,47349,1313],{"class":302},[278,47351,47352,47354,47356,47359],{"class":280,"line":6670},[278,47353,1242],{"class":298},[278,47355,1245],{"class":302},[278,47357,47358],{"class":333},"handleJob",[278,47360,47361],{"class":302},"(jobs, job)) {\n",[278,47363,47364,47367,47369],{"class":280,"line":6675},[278,47365,47366],{"class":302},"      results.",[278,47368,6524],{"class":333},[278,47370,637],{"class":302},[278,47372,47373],{"class":280,"line":6680},[278,47374,47375],{"class":302},"        total: job.total,\n",[278,47377,47378,47381,47383],{"class":280,"line":6698},[278,47379,47380],{"class":302},"        moves: job.pastMoves.",[278,47382,15645],{"class":650},[278,47384,660],{"class":302},[278,47386,47387],{"class":280,"line":6725},[278,47388,47389],{"class":302},"        path: job.pastMoves,\n",[278,47391,47392],{"class":280,"line":6738},[278,47393,5148],{"class":302},[278,47395,47396],{"class":280,"line":6752},[278,47397,1285],{"class":302},[278,47399,47400],{"class":280,"line":6769},[278,47401,1096],{"class":302},[278,47403,47404],{"class":280,"line":6786},[278,47405,292],{"emptyLinePlaceholder":291},[278,47407,47408,47410,47413,47415],{"class":280,"line":6798},[278,47409,1062],{"class":298},[278,47411,47412],{"class":302}," (results.",[278,47414,15645],{"class":650},[278,47416,1718],{"class":302},[278,47418,47419,47422,47424,47426,47429,47431,47434,47436,47438],{"class":280,"line":6803},[278,47420,47421],{"class":302},"    results.",[278,47423,13349],{"class":333},[278,47425,2079],{"class":302},[278,47427,47428],{"class":501},"result1",[278,47430,1708],{"class":302},[278,47432,47433],{"class":501},"result2",[278,47435,1845],{"class":302},[278,47437,1848],{"class":298},[278,47439,876],{"class":302},[278,47441,47442,47444,47447,47449],{"class":280,"line":6815},[278,47443,1942],{"class":298},[278,47445,47446],{"class":302}," result2.total ",[278,47448,16522],{"class":298},[278,47450,47451],{"class":302}," result1.total;\n",[278,47453,47454],{"class":280,"line":6827},[278,47455,1233],{"class":302},[278,47457,47458],{"class":280,"line":6839},[278,47459,292],{"emptyLinePlaceholder":291},[278,47461,47462,47464,47467,47469],{"class":280,"line":6844},[278,47463,1088],{"class":298},[278,47465,47466],{"class":302}," results[",[278,47468,2012],{"class":650},[278,47470,11714],{"class":302},[278,47472,47473,47475,47477],{"class":280,"line":6853},[278,47474,1397],{"class":302},[278,47476,15659],{"class":298},[278,47478,876],{"class":302},[278,47480,47481,47483,47485,47487,47490],{"class":280,"line":6859},[278,47482,1409],{"class":302},[278,47484,14851],{"class":333},[278,47486,1126],{"class":302},[278,47488,47489],{"class":309},"`No valid path found`",[278,47491,1280],{"class":302},[278,47493,47494],{"class":280,"line":6864},[278,47495,1096],{"class":302},[278,47497,47498],{"class":280,"line":6877},[278,47499,2817],{"class":302},[278,47501,47502],{"class":280,"line":6887},[278,47503,292],{"emptyLinePlaceholder":291},[278,47505,47506,47509,47511,47513,47515,47517,47519],{"class":280,"line":6918},[278,47507,47508],{"class":650},"exports",[278,47510,764],{"class":298},[278,47512,2325],{"class":298},[278,47514,748],{"class":298},[278,47516,1245],{"class":302},[278,47518,2330],{"class":501},[278,47520,1718],{"class":302},[278,47522,47523,47525,47528,47530,47532],{"class":280,"line":6923},[278,47524,6050],{"class":298},[278,47526,47527],{"class":302}," reqBody ",[278,47529,358],{"class":298},[278,47531,1035],{"class":650},[278,47533,313],{"class":302},[278,47535,47536,47538],{"class":280,"line":6931},[278,47537,1062],{"class":298},[278,47539,47540],{"class":302}," (req) {\n",[278,47542,47543,47545],{"class":280,"line":6939},[278,47544,1242],{"class":298},[278,47546,47547],{"class":302}," (req.body) {\n",[278,47549,47550,47552,47554,47556,47559,47561,47563,47565,47567,47569,47571,47573],{"class":280,"line":6951},[278,47551,1919],{"class":302},[278,47553,14851],{"class":333},[278,47555,1126],{"class":302},[278,47557,47558],{"class":309},"`got a req body: ${",[278,47560,2330],{"class":302},[278,47562,183],{"class":309},[278,47564,15197],{"class":302},[278,47566,183],{"class":309},[278,47568,4582],{"class":333},[278,47570,15224],{"class":309},[278,47572,1277],{"class":309},[278,47574,1280],{"class":302},[278,47576,47577,47580,47582,47584,47586,47588,47591,47593],{"class":280,"line":6957},[278,47578,47579],{"class":302},"      reqBody ",[278,47581,358],{"class":298},[278,47583,12063],{"class":650},[278,47585,183],{"class":302},[278,47587,12068],{"class":333},[278,47589,47590],{"class":302},"(req.body.",[278,47592,4582],{"class":333},[278,47594,6915],{"class":302},[278,47596,47597,47599,47601],{"class":280,"line":6962},[278,47598,6636],{"class":302},[278,47600,15659],{"class":298},[278,47602,876],{"class":302},[278,47604,47605,47607,47609,47611,47614,47616,47618,47620,47622,47624,47626,47628],{"class":280,"line":6973},[278,47606,1919],{"class":302},[278,47608,14851],{"class":333},[278,47610,1126],{"class":302},[278,47612,47613],{"class":309},"`got a req without req body: ${",[278,47615,2230],{"class":650},[278,47617,183],{"class":309},[278,47619,2235],{"class":333},[278,47621,1126],{"class":309},[278,47623,2330],{"class":302},[278,47625,17418],{"class":309},[278,47627,1277],{"class":309},[278,47629,1280],{"class":302},[278,47631,47632],{"class":280,"line":6985},[278,47633,1285],{"class":302},[278,47635,47636],{"class":280,"line":6990},[278,47637,1096],{"class":302},[278,47639,47640],{"class":280,"line":6995},[278,47641,292],{"emptyLinePlaceholder":291},[278,47643,47644,47646,47649,47651],{"class":280,"line":7000},[278,47645,758],{"class":298},[278,47647,47648],{"class":650}," coins",[278,47650,764],{"class":298},[278,47652,6483],{"class":302},[278,47654,47655,47657,47659,47661,47664,47666,47668,47671,47673,47675,47678,47680],{"class":280,"line":7005},[278,47656,12738],{"class":298},[278,47658,1245],{"class":302},[278,47660,1001],{"class":298},[278,47662,47663],{"class":302}," row ",[278,47665,358],{"class":298},[278,47667,6588],{"class":650},[278,47669,47670],{"class":302},"; row ",[278,47672,1702],{"class":298},[278,47674,46267],{"class":650},[278,47676,47677],{"class":302},"; row",[278,47679,31563],{"class":298},[278,47681,1718],{"class":302},[278,47683,47684,47687,47689],{"class":280,"line":7017},[278,47685,47686],{"class":302},"    coins.",[278,47688,6524],{"class":333},[278,47690,47691],{"class":302},"([]);\n",[278,47693,47694],{"class":280,"line":7025},[278,47695,292],{"emptyLinePlaceholder":291},[278,47697,47698,47700,47703,47705,47707,47709,47711,47713,47716,47718,47721],{"class":280,"line":7030},[278,47699,1112],{"class":298},[278,47701,47702],{"class":650}," blockages",[278,47704,764],{"class":298},[278,47706,46367],{"class":333},[278,47708,1126],{"class":302},[278,47710,2012],{"class":650},[278,47712,1708],{"class":302},[278,47714,47715],{"class":650},"COLS",[278,47717,1708],{"class":302},[278,47719,47720],{"class":650},"2",[278,47722,1280],{"class":302},[278,47724,47725,47727,47729,47731,47733,47735,47737,47740,47742,47744,47747,47749],{"class":280,"line":7035},[278,47726,12012],{"class":298},[278,47728,1245],{"class":302},[278,47730,1001],{"class":298},[278,47732,46694],{"class":302},[278,47734,358],{"class":298},[278,47736,6588],{"class":650},[278,47738,47739],{"class":302},"; col ",[278,47741,1702],{"class":298},[278,47743,46281],{"class":650},[278,47745,47746],{"class":302},"; col",[278,47748,31563],{"class":298},[278,47750,1718],{"class":302},[278,47752,47753,47755,47758,47760],{"class":280,"line":7042},[278,47754,2461],{"class":298},[278,47756,47757],{"class":650}," coin",[278,47759,764],{"class":298},[278,47761,876],{"class":302},[278,47763,47764,47767,47769,47772,47774,47777,47779],{"class":280,"line":7054},[278,47765,47766],{"class":302},"        id: ",[278,47768,34095],{"class":309},[278,47770,47771],{"class":302},"row",[278,47773,46740],{"class":309},[278,47775,47776],{"class":302},"col",[278,47778,1277],{"class":309},[278,47780,660],{"class":302},[278,47782,47783,47786,47789,47791,47793,47795,47798],{"class":280,"line":7060},[278,47784,47785],{"class":302},"        value: ",[278,47787,47788],{"class":333},"randomInt",[278,47790,1126],{"class":302},[278,47792,17444],{"class":650},[278,47794,1708],{"class":302},[278,47796,47797],{"class":650},"7",[278,47799,4704],{"class":302},[278,47801,47802,47805,47807],{"class":280,"line":7066},[278,47803,47804],{"class":302},"        wall: ",[278,47806,2012],{"class":650},[278,47808,660],{"class":302},[278,47810,47811],{"class":280,"line":7071},[278,47812,1650],{"class":302},[278,47814,47815],{"class":280,"line":8344},[278,47816,292],{"emptyLinePlaceholder":291},[278,47818,47819,47821,47824,47826],{"class":280,"line":8350},[278,47820,6207],{"class":298},[278,47822,47823],{"class":302}," (blockages.",[278,47825,13297],{"class":333},[278,47827,47828],{"class":302},"(col)) {\n",[278,47830,47831,47834,47836,47838,47840,47842,47844,47847],{"class":280,"line":8356},[278,47832,47833],{"class":302},"        coin.wall ",[278,47835,358],{"class":298},[278,47837,46303],{"class":333},[278,47839,1126],{"class":302},[278,47841,17444],{"class":650},[278,47843,1708],{"class":302},[278,47845,47846],{"class":650},"5",[278,47848,1280],{"class":302},[278,47850,47851],{"class":280,"line":8362},[278,47852,6234],{"class":302},[278,47854,47855],{"class":280,"line":8368},[278,47856,292],{"emptyLinePlaceholder":291},[278,47858,47859,47862,47864],{"class":280,"line":8373},[278,47860,47861],{"class":302},"      coins[row].",[278,47863,6524],{"class":333},[278,47865,47866],{"class":302},"(coin);\n",[278,47868,47869],{"class":280,"line":8378},[278,47870,1285],{"class":302},[278,47872,47873],{"class":280,"line":8383},[278,47874,1096],{"class":302},[278,47876,47877],{"class":280,"line":8388},[278,47878,292],{"emptyLinePlaceholder":291},[278,47880,47881,47883,47885,47887,47890,47892,47894,47896,47898,47900,47902,47904,47906,47908,47910,47912,47914,47916,47918],{"class":280,"line":8393},[278,47882,758],{"class":298},[278,47884,14680],{"class":650},[278,47886,764],{"class":298},[278,47888,47889],{"class":309}," `${",[278,47891,47788],{"class":333},[278,47893,1126],{"class":309},[278,47895,47720],{"class":650},[278,47897,1708],{"class":309},[278,47899,3591],{"class":650},[278,47901,17418],{"class":309},[278,47903,46740],{"class":309},[278,47905,47788],{"class":333},[278,47907,1126],{"class":309},[278,47909,47720],{"class":650},[278,47911,1708],{"class":309},[278,47913,3591],{"class":650},[278,47915,17418],{"class":309},[278,47917,1277],{"class":309},[278,47919,313],{"class":302},[278,47921,47922,47924,47927,47929,47931,47933,47935,47937,47939],{"class":280,"line":8399},[278,47923,6050],{"class":298},[278,47925,47926],{"class":302}," end ",[278,47928,358],{"class":298},[278,47930,46303],{"class":333},[278,47932,1126],{"class":302},[278,47934,17444],{"class":650},[278,47936,1708],{"class":302},[278,47938,47846],{"class":650},[278,47940,1280],{"class":302},[278,47942,47943,47945,47948,47950,47952],{"class":280,"line":8405},[278,47944,1062],{"class":298},[278,47946,47947],{"class":302}," (end ",[278,47949,2451],{"class":298},[278,47951,6274],{"class":650},[278,47953,1718],{"class":302},[278,47955,47956,47959,47961,47964],{"class":280,"line":8410},[278,47957,47958],{"class":302},"    end ",[278,47960,358],{"class":298},[278,47962,47963],{"class":309}," '00'",[278,47965,313],{"class":302},[278,47967,47968,47970,47972,47974,47976,47978,47980],{"class":280,"line":8416},[278,47969,1397],{"class":302},[278,47971,15659],{"class":298},[278,47973,15662],{"class":298},[278,47975,47947],{"class":302},[278,47977,2451],{"class":298},[278,47979,46990],{"class":650},[278,47981,1718],{"class":302},[278,47983,47984,47986,47988,47991,47993,47995,47997,47999],{"class":280,"line":8422},[278,47985,47958],{"class":302},[278,47987,358],{"class":298},[278,47989,47990],{"class":309}," `0${",[278,47992,47715],{"class":650},[278,47994,1357],{"class":298},[278,47996,6274],{"class":650},[278,47998,1277],{"class":309},[278,48000,313],{"class":302},[278,48002,48003,48005,48007,48009,48011,48013,48015],{"class":280,"line":8428},[278,48004,1397],{"class":302},[278,48006,15659],{"class":298},[278,48008,15662],{"class":298},[278,48010,47947],{"class":302},[278,48012,2451],{"class":298},[278,48014,47094],{"class":650},[278,48016,1718],{"class":302},[278,48018,48019,48021,48023,48025,48028,48030,48032,48035],{"class":280,"line":8433},[278,48020,47958],{"class":302},[278,48022,358],{"class":298},[278,48024,47889],{"class":309},[278,48026,48027],{"class":650},"ROWS",[278,48029,1357],{"class":298},[278,48031,6274],{"class":650},[278,48033,48034],{"class":309},"}0`",[278,48036,313],{"class":302},[278,48038,48039,48041,48043],{"class":280,"line":8439},[278,48040,1397],{"class":302},[278,48042,15659],{"class":298},[278,48044,876],{"class":302},[278,48046,48047,48049,48051,48053,48055,48057,48059,48061,48063,48065,48067,48069],{"class":280,"line":8444},[278,48048,47958],{"class":302},[278,48050,358],{"class":298},[278,48052,47889],{"class":309},[278,48054,48027],{"class":650},[278,48056,1357],{"class":298},[278,48058,6274],{"class":650},[278,48060,46740],{"class":309},[278,48062,47715],{"class":650},[278,48064,1357],{"class":298},[278,48066,6274],{"class":650},[278,48068,1277],{"class":309},[278,48070,313],{"class":302},[278,48072,48073],{"class":280,"line":8450},[278,48074,1096],{"class":302},[278,48076,48077],{"class":280,"line":8455},[278,48078,292],{"emptyLinePlaceholder":291},[278,48080,48081,48083,48086,48088,48090,48093],{"class":280,"line":8461},[278,48082,758],{"class":298},[278,48084,48085],{"class":650}," date",[278,48087,764],{"class":298},[278,48089,1258],{"class":298},[278,48091,48092],{"class":333}," Date",[278,48094,1313],{"class":302},[278,48096,48097,48099,48102,48104],{"class":280,"line":8467},[278,48098,758],{"class":298},[278,48100,48101],{"class":650}," gameEntry",[278,48103,764],{"class":298},[278,48105,876],{"class":302},[278,48107,48108],{"class":280,"line":8472},[278,48109,48110],{"class":302},"    coins,\n",[278,48112,48113],{"class":280,"line":8477},[278,48114,48115],{"class":302},"    start,\n",[278,48117,48118],{"class":280,"line":8482},[278,48119,48120],{"class":302},"    end,\n",[278,48122,48123,48126,48128],{"class":280,"line":8488},[278,48124,48125],{"class":302},"    active: ",[278,48127,2965],{"class":650},[278,48129,660],{"class":302},[278,48131,48132],{"class":280,"line":8494},[278,48133,48134],{"class":302},"    createdAt: date,\n",[278,48136,48137],{"class":280,"line":8499},[278,48138,48139],{"class":302},"    updatedAt: date,\n",[278,48141,48142],{"class":280,"line":8505},[278,48143,901],{"class":302},[278,48145,48146],{"class":280,"line":8511},[278,48147,292],{"emptyLinePlaceholder":291},[278,48149,48150,48152,48155,48157,48159,48161],{"class":280,"line":8517},[278,48151,758],{"class":298},[278,48153,48154],{"class":650}," startTime",[278,48156,764],{"class":298},[278,48158,1077],{"class":302},[278,48160,1080],{"class":333},[278,48162,1313],{"class":302},[278,48164,48165,48167,48170,48172,48174,48176,48178,48180,48182,48184,48186,48188,48190],{"class":280,"line":8523},[278,48166,758],{"class":298},[278,48168,48169],{"class":650}," bestMove",[278,48171,764],{"class":298},[278,48173,47206],{"class":333},[278,48175,1126],{"class":302},[278,48177,2230],{"class":650},[278,48179,183],{"class":302},[278,48181,12068],{"class":333},[278,48183,1126],{"class":302},[278,48185,2230],{"class":650},[278,48187,183],{"class":302},[278,48189,2235],{"class":333},[278,48191,48192],{"class":302},"(coins)), start, end);\n",[278,48194,48195,48197,48199],{"class":280,"line":8529},[278,48196,17975],{"class":302},[278,48198,14851],{"class":333},[278,48200,770],{"class":302},[278,48202,48203,48206,48209,48211,48213,48215,48217,48219],{"class":280,"line":8535},[278,48204,48205],{"class":309},"    `Total time taken for finding bestRoute: ${",[278,48207,48208],{"class":302},"Date",[278,48210,183],{"class":309},[278,48212,1080],{"class":333},[278,48214,1342],{"class":309},[278,48216,16522],{"class":298},[278,48218,48154],{"class":302},[278,48220,48221],{"class":309},"} ms`\n",[278,48223,48224],{"class":280,"line":8541},[278,48225,611],{"class":302},[278,48227,48228],{"class":280,"line":8546},[278,48229,292],{"emptyLinePlaceholder":291},[278,48231,48232,48234],{"class":280,"line":8551},[278,48233,1062],{"class":298},[278,48235,48236],{"class":302}," (bestMove) {\n",[278,48238,48239,48241,48243,48245,48248,48250,48252,48254,48256,48259,48261,48263],{"class":280,"line":8556},[278,48240,1409],{"class":302},[278,48242,14851],{"class":333},[278,48244,1126],{"class":302},[278,48246,48247],{"class":309},"`best path: ${",[278,48249,2230],{"class":650},[278,48251,183],{"class":309},[278,48253,2235],{"class":333},[278,48255,1126],{"class":309},[278,48257,48258],{"class":302},"bestMove",[278,48260,17418],{"class":309},[278,48262,1277],{"class":309},[278,48264,1280],{"class":302},[278,48266,48267,48270,48272],{"class":280,"line":8561},[278,48268,48269],{"class":302},"    gameEntry.maxScore ",[278,48271,358],{"class":298},[278,48273,48274],{"class":302}," bestMove.total;\n",[278,48276,48277,48280,48282],{"class":280,"line":8566},[278,48278,48279],{"class":302},"    gameEntry.maxScoreMoves ",[278,48281,358],{"class":298},[278,48283,48284],{"class":302}," bestMove.moves;\n",[278,48286,48287,48290,48292],{"class":280,"line":8572},[278,48288,48289],{"class":302},"    gameEntry.hints ",[278,48291,358],{"class":298},[278,48293,48294],{"class":302}," bestMove.path;\n",[278,48296,48297],{"class":280,"line":8578},[278,48298,292],{"emptyLinePlaceholder":291},[278,48300,48301,48303,48306,48308,48311,48313,48315,48318,48320,48323,48325,48328],{"class":280,"line":8583},[278,48302,1112],{"class":298},[278,48304,48305],{"class":650}," mongoDb",[278,48307,764],{"class":298},[278,48309,48310],{"class":302}," context.services.",[278,48312,3925],{"class":333},[278,48314,1126],{"class":302},[278,48316,48317],{"class":309},"'cluster-service-name'",[278,48319,4633],{"class":302},[278,48321,48322],{"class":333},"db",[278,48324,1126],{"class":302},[278,48326,48327],{"class":309},"'db-name'",[278,48329,1280],{"class":302},[278,48331,48332,48334,48337,48339,48342,48345,48347,48350],{"class":280,"line":8588},[278,48333,1112],{"class":298},[278,48335,48336],{"class":650}," gamesCollection",[278,48338,764],{"class":298},[278,48340,48341],{"class":302}," mongoDb.",[278,48343,48344],{"class":333},"collection",[278,48346,1126],{"class":302},[278,48348,48349],{"class":309},"'games'",[278,48351,1280],{"class":302},[278,48353,48354,48356,48359,48361,48363,48365,48367,48370],{"class":280,"line":8593},[278,48355,1112],{"class":298},[278,48357,48358],{"class":650}," appCollection",[278,48360,764],{"class":298},[278,48362,48341],{"class":302},[278,48364,48344],{"class":333},[278,48366,1126],{"class":302},[278,48368,48369],{"class":309},"'app'",[278,48371,1280],{"class":302},[278,48373,48374,48376,48378,48380,48382,48385,48388,48391,48394],{"class":280,"line":8599},[278,48375,1112],{"class":298},[278,48377,20161],{"class":650},[278,48379,764],{"class":298},[278,48381,1120],{"class":298},[278,48383,48384],{"class":302}," appCollection.",[278,48386,48387],{"class":333},"findOne",[278,48389,48390],{"class":302},"({ type: ",[278,48392,48393],{"class":309},"'config'",[278,48395,6346],{"class":302},[278,48397,48398],{"class":280,"line":8605},[278,48399,292],{"emptyLinePlaceholder":291},[278,48401,48402,48404,48406,48408,48411,48413,48415,48417,48419],{"class":280,"line":8611},[278,48403,1409],{"class":302},[278,48405,14851],{"class":333},[278,48407,1126],{"class":302},[278,48409,48410],{"class":309},"'fetch config data:'",[278,48412,1708],{"class":302},[278,48414,2230],{"class":650},[278,48416,183],{"class":302},[278,48418,2235],{"class":333},[278,48420,48421],{"class":302},"(config));\n",[278,48423,48424,48426,48428,48430,48433],{"class":280,"line":8616},[278,48425,1409],{"class":302},[278,48427,14851],{"class":333},[278,48429,1126],{"class":302},[278,48431,48432],{"class":309},"'lastPlayableGame:'",[278,48434,48435],{"class":302},", config.lastPlayableGame);\n",[278,48437,48438],{"class":280,"line":8621},[278,48439,292],{"emptyLinePlaceholder":291},[278,48441,48442,48444],{"class":280,"line":8626},[278,48443,1242],{"class":298},[278,48445,48446],{"class":302}," (config) {\n",[278,48448,48449,48451],{"class":280,"line":8632},[278,48450,6207],{"class":298},[278,48452,48453],{"class":302}," (config.lastPlayableGame) {\n",[278,48455,48456,48458,48461,48463],{"class":280,"line":8637},[278,48457,6741],{"class":298},[278,48459,48460],{"class":650}," lastPlayableDate",[278,48462,764],{"class":298},[278,48464,48465],{"class":302}," config.lastPlayableGame.playableAt;\n",[278,48467,48468,48471,48474,48477,48480,48482,48484,48486],{"class":280,"line":8643},[278,48469,48470],{"class":302},"        lastPlayableDate.",[278,48472,48473],{"class":333},"setUTCDate",[278,48475,48476],{"class":302},"(lastPlayableDate.",[278,48478,48479],{"class":333},"getDate",[278,48481,1342],{"class":302},[278,48483,1345],{"class":298},[278,48485,6274],{"class":650},[278,48487,1280],{"class":302},[278,48489,48490,48493,48495],{"class":280,"line":8648},[278,48491,48492],{"class":302},"        gameEntry.playableAt ",[278,48494,358],{"class":298},[278,48496,48497],{"class":302}," lastPlayableDate;\n",[278,48499,48500,48503,48505,48508,48510,48512],{"class":280,"line":8654},[278,48501,48502],{"class":302},"        gameEntry.gameNo ",[278,48504,358],{"class":298},[278,48506,48507],{"class":302}," config.lastPlayableGame.gameNo ",[278,48509,1345],{"class":298},[278,48511,6274],{"class":650},[278,48513,313],{"class":302},[278,48515,48516,48518],{"class":280,"line":8660},[278,48517,6926],{"class":298},[278,48519,48520],{"class":302}," (reqBody) {\n",[278,48522,48523,48525],{"class":280,"line":8666},[278,48524,13947],{"class":298},[278,48526,48527],{"class":302}," (reqBody.active) {\n",[278,48529,48530,48533,48535,48537],{"class":280,"line":8672},[278,48531,48532],{"class":302},"            gameEntry.active ",[278,48534,358],{"class":298},[278,48536,6575],{"class":650},[278,48538,313],{"class":302},[278,48540,48541],{"class":280,"line":8677},[278,48542,12122],{"class":302},[278,48544,48546],{"class":280,"line":48545},189,[278,48547,292],{"emptyLinePlaceholder":291},[278,48549,48551,48553],{"class":280,"line":48550},190,[278,48552,13947],{"class":298},[278,48554,48555],{"class":302}," (reqBody.current) {\n",[278,48557,48559,48562,48564,48566],{"class":280,"line":48558},191,[278,48560,48561],{"class":302},"            gameEntry.current ",[278,48563,358],{"class":298},[278,48565,6575],{"class":650},[278,48567,313],{"class":302},[278,48569,48571],{"class":280,"line":48570},192,[278,48572,12122],{"class":302},[278,48574,48576],{"class":280,"line":48575},193,[278,48577,6954],{"class":302},[278,48579,48581,48583,48585],{"class":280,"line":48580},194,[278,48582,14445],{"class":302},[278,48584,15659],{"class":298},[278,48586,876],{"class":302},[278,48588,48590,48592,48595,48597,48599,48601],{"class":280,"line":48589},195,[278,48591,6741],{"class":298},[278,48593,48594],{"class":650}," playableDate",[278,48596,764],{"class":298},[278,48598,1258],{"class":298},[278,48600,48092],{"class":333},[278,48602,1313],{"class":302},[278,48604,48606,48609,48612,48614,48616,48618,48620,48622,48624,48626,48628],{"class":280,"line":48605},196,[278,48607,48608],{"class":302},"        playableDate.",[278,48610,48611],{"class":333},"setUTCHours",[278,48613,1126],{"class":302},[278,48615,2012],{"class":650},[278,48617,1708],{"class":302},[278,48619,2012],{"class":650},[278,48621,1708],{"class":302},[278,48623,2012],{"class":650},[278,48625,1708],{"class":302},[278,48627,2012],{"class":650},[278,48629,1280],{"class":302},[278,48631,48633,48635,48637],{"class":280,"line":48632},197,[278,48634,48492],{"class":302},[278,48636,358],{"class":298},[278,48638,48639],{"class":302}," playableDate;\n",[278,48641,48643,48645,48647,48649],{"class":280,"line":48642},198,[278,48644,48502],{"class":302},[278,48646,358],{"class":298},[278,48648,6274],{"class":650},[278,48650,313],{"class":302},[278,48652,48654,48657,48659,48661],{"class":280,"line":48653},199,[278,48655,48656],{"class":302},"        gameEntry.current ",[278,48658,358],{"class":298},[278,48660,6575],{"class":650},[278,48662,313],{"class":302},[278,48664,48666,48669,48671,48673],{"class":280,"line":48665},200,[278,48667,48668],{"class":302},"        gameEntry.active ",[278,48670,358],{"class":298},[278,48672,6575],{"class":650},[278,48674,313],{"class":302},[278,48676,48678],{"class":280,"line":48677},201,[278,48679,6234],{"class":302},[278,48681,48683],{"class":280,"line":48682},202,[278,48684,1285],{"class":302},[278,48686,48688],{"class":280,"line":48687},203,[278,48689,292],{"emptyLinePlaceholder":291},[278,48691,48693,48695,48697,48699,48701,48704,48707],{"class":280,"line":48692},204,[278,48694,20815],{"class":298},[278,48696,34187],{"class":302},[278,48698,358],{"class":298},[278,48700,1120],{"class":298},[278,48702,48703],{"class":302}," gamesCollection.",[278,48705,48706],{"class":333},"insertOne",[278,48708,48709],{"class":302},"(gameEntry);\n",[278,48711,48713,48715,48717],{"class":280,"line":48712},205,[278,48714,1409],{"class":302},[278,48716,14851],{"class":333},[278,48718,770],{"class":302},[278,48720,48722,48725,48727,48729,48731,48733,48736,48738],{"class":280,"line":48721},206,[278,48723,48724],{"class":309},"      `Successfully inserted game with _id: ${",[278,48726,2230],{"class":650},[278,48728,183],{"class":309},[278,48730,2235],{"class":333},[278,48732,1126],{"class":309},[278,48734,48735],{"class":302},"result",[278,48737,17418],{"class":309},[278,48739,48740],{"class":309},"}`\n",[278,48742,48744],{"class":280,"line":48743},207,[278,48745,1898],{"class":302},[278,48747,48749],{"class":280,"line":48748},208,[278,48750,292],{"emptyLinePlaceholder":291},[278,48752,48754,48757,48759,48761,48763,48766],{"class":280,"line":48753},209,[278,48755,48756],{"class":302},"    result ",[278,48758,358],{"class":298},[278,48760,1120],{"class":298},[278,48762,48384],{"class":302},[278,48764,48765],{"class":333},"updateOne",[278,48767,770],{"class":302},[278,48769,48771,48774,48776],{"class":280,"line":48770},210,[278,48772,48773],{"class":302},"      { type: ",[278,48775,48393],{"class":309},[278,48777,3547],{"class":302},[278,48779,48781],{"class":280,"line":48780},211,[278,48782,2771],{"class":302},[278,48784,48786],{"class":280,"line":48785},212,[278,48787,48788],{"class":302},"        $set: {\n",[278,48790,48792],{"class":280,"line":48791},213,[278,48793,48794],{"class":302},"          lastPlayableGame: {\n",[278,48796,48798],{"class":280,"line":48797},214,[278,48799,48800],{"class":302},"            playableAt: gameEntry.playableAt,\n",[278,48802,48804],{"class":280,"line":48803},215,[278,48805,48806],{"class":302},"            gameNo: gameEntry.gameNo,\n",[278,48808,48810],{"class":280,"line":48809},216,[278,48811,48812],{"class":302},"            _id: result.insertedId,\n",[278,48814,48816],{"class":280,"line":48815},217,[278,48817,11557],{"class":302},[278,48819,48821],{"class":280,"line":48820},218,[278,48822,2606],{"class":302},[278,48824,48826],{"class":280,"line":48825},219,[278,48827,6234],{"class":302},[278,48829,48831],{"class":280,"line":48830},220,[278,48832,1898],{"class":302},[278,48834,48836],{"class":280,"line":48835},221,[278,48837,292],{"emptyLinePlaceholder":291},[278,48839,48841,48843,48845,48847,48850,48852,48854,48856,48858],{"class":280,"line":48840},222,[278,48842,1409],{"class":302},[278,48844,14851],{"class":333},[278,48846,1126],{"class":302},[278,48848,48849],{"class":309},"'result of update operation: '",[278,48851,1708],{"class":302},[278,48853,2230],{"class":650},[278,48855,183],{"class":302},[278,48857,2235],{"class":333},[278,48859,48860],{"class":302},"(result));\n",[278,48862,48864],{"class":280,"line":48863},223,[278,48865,1096],{"class":302},[278,48867,48869],{"class":280,"line":48868},224,[278,48870,292],{"emptyLinePlaceholder":291},[278,48872,48874,48876],{"class":280,"line":48873},225,[278,48875,343],{"class":298},[278,48877,48878],{"class":302}," gameEntry;\n",[278,48880,48882],{"class":280,"line":48881},226,[278,48883,2817],{"class":302},[11,48885,48886],{},"Behind the scenes, I also created another collection called \"app\" the purpose of which is to store the app-specific settings and information. Since the newly created game will be played sometime in future, and there will be some extra games stored in the games collection, I am utilizing the app collection to make note of the last created game.",[11,48888,48889],{},"Also notice that we are saving the new game only if it has a solution (there might be cases where the game has no solution, as everything is getting generated randomly).",[11,48891,48892,48893,48896],{},"If you did the above changes in your local setup, then you need to run ",[59,48894,48895],{},"realm-cli push"," in your terminal before you can call your endpoint.",[11,48898,48899],{},"Now, if you call your endpoint from postman, you should get back the game object as a reply. And also, there should be a new document present in your games collection of the database.",[32,48901,48903],{"id":48902},"some-gotchas-with-app-services-functions","Some gotchas with App Services Functions",[11,48905,48906],{},"In the above function also, I struggled for quite some time due to 2 issues.",[123,48908,48909,48931],{},[74,48910,48911,48912,48915,48916,48919,48920,48923,48924,48926,48927,48930],{},"Functions documentation says that it supports ",[59,48913,48914],{},"crypto"," module partially. I was using ",[59,48917,48918],{},"crypto.randomInt"," to do my random number generation. But the function was failing giving unhelpful error messages (",[59,48921,48922],{},"TypeError: Value is not an object: undefined","). After a lot of trial and error, ultimately I found out that the",[59,48925,47788],{}," method is unavailable in the app services crypto, so I created a new function using ",[59,48928,48929],{},"Math.random"," to generate random numbers. It would be great to have some meaningful error messages in such cases.",[74,48932,48933,48934,48937,48938,48941,48942,48945,48946,48948,48949,48951,48952,48954,48955,48958,48959,48961,48964,48965,48977,48979,48982,48983,48993,48995,48996],{},"I was using a ",[59,48935,48936],{},"Set"," to keep track of generated walls (",[59,48939,48940],{},"blockages","). But on checking ",[59,48943,48944],{},"if (blockages.has(col))"," I was getting ",[59,48947,2965],{}," for all columns except ",[59,48950,2012],{},". Again this needed some testing and figuring out. Finally replaced ",[59,48953,48936],{}," with a ",[59,48956,48957],{},"list"," (as is visible in the code above). Testing with a \"set\" in my local node setup works perfectly fine, so maybe it has something to do with how app services functions have been implemented behind the scenes, or altogether there is some other issue I can't say.",[131,48960],{},[94,48962,48963],{},"Update 1:"," So I couldn't stop thinking about the issue, and raised it in the official MongoDB Developer Community Forum. Heard back from them with the below response:",[18,48966,48967,48974],{},[11,48968,48969,48970,48973],{},"Thank you for raising this: we could reproduce the behaviour, and indeed it looks like ",[59,48971,48972],{},"Set.has(…)"," isn’t returning the expected results.",[11,48975,48976],{},"We’re opening an internal ticket about the matter, and will keep this post updated.",[131,48978],{},[94,48980,48981],{},"Update 2 (Final Update): 14 Dec 2022:"," Received a new reply from the mongo team that they've identified the bug, and it will be solved in due course (no timeframe). This concludes the mystery around the issue :-)",[18,48984,48985,48990],{},[11,48986,48987],{},[3061,48988,48989],{},"The Team responsible of the underlying function engine has confirmed that the error is due to mishandling of integer values. Set.has(…) should still work for other types, though.",[11,48991,48992],{},"(To clarify: the problem is that the addition of two integers is returning a float, that Set.has() doesn’t compare properly)",[131,48994],{},"Do notice that we're doing addition while generating a random number ",[59,48997,48998],{},"Math.floor(Math.random() * (max - min)) + min;",[32,49000,49002],{"id":49001},"data-access-rules","Data Access Rules",[11,49004,49005],{},"To get data using the realm Web SDK (which I added to the react app), we need to configure the data access rules for each of the collections we create. We can do so easily using the App Services UI (or the JSON files in the local setup, but that takes some time to get familiar with).",[11,49007,49008],{},"We can set these rules from App Services UI easily. There are some preset handy rules which we can use, or we can create our own rules from scratch.",[11,49010,49011],{},[3135,49012],{"alt":49013,"src":49014},"Data access rules","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FMdhc6quQ4-337ae83a53.png",[32,49016,49018],{"id":49017},"scheduled-triggers-and-backing-function","Scheduled Triggers and Backing Function",[11,49020,49021,49022,49025],{},"Since our puzzles should change every day, we need a trigger to do so automatically. App services provide many types of triggers, for my purpose I needed a simple CRON schedule which runs every day at 12:00 AM UTC. Used the advanced schedule type from the UI, and set the schedule to ",[59,49023,49024],{},"0 0 * * *"," and let it call another function to change the game.",[11,49027,49028],{},[3135,49029],{"alt":49030,"src":49031},"Scheduled trigger setting","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002Fvb70TVwf-631527e4ef.png",[11,49033,49034,49035,49038],{},"Below is the code of the function which makes the current game non-current, and makes the next game in line (recognized by the value of ",[59,49036,49037],{},"gameNo"," field of the document).",[269,49040,49042],{"className":24597,"code":49041,"language":24599,"meta":274,"style":274},"exports = async function () {\n  const mongoDb = context.services.get('cluster-service-nam').db('db-name');\n  const gamesCollection = mongoDb.collection('games');\n  const currGame = await gamesCollection.findOne({ current: true });\n  if (currGame) {\n    console.log('got the current game: ', JSON.stringify(currGame));\n    const date = new Date();\n    const nextGameDate = new Date();\n    nextGameDate.setUTCHours(0, 0, 0, 0);\n    nextGameDate.setUTCDate(nextGameDate.getDate() + 1);\n    await gamesCollection.bulkWrite(\n      [\n        {\n          updateOne: {\n            filter: { gameNo: currGame.gameNo + 1 },\n            update: {\n              $set: {\n                current: true,\n                active: true,\n                updatedAt: date,\n                playedAt: date,\n                nextGameAt: nextGameDate,\n              },\n            },\n          },\n        },\n        {\n          updateOne: {\n            filter: { _id: currGame._id },\n            update: { $set: { current: false, updatedAt: date } },\n          },\n        },\n      ],\n      { ordered: true }\n    );\n\n    console.log('after the bulkWrite Op');\n  } else {\n    console.log('Error! No current game found.');\n  }\n};\n",[59,49043,49044,49048,49053,49058,49063,49068,49073,49078,49083,49088,49093,49098,49103,49107,49112,49117,49122,49127,49132,49137,49142,49147,49152,49156,49160,49164,49168,49172,49176,49181,49186,49190,49194,49198,49203,49207,49211,49216,49220,49225,49229],{"__ignoreMap":274},[278,49045,49046],{"class":280,"line":281},[278,49047,44497],{},[278,49049,49050],{"class":280,"line":288},[278,49051,49052],{},"  const mongoDb = context.services.get('cluster-service-nam').db('db-name');\n",[278,49054,49055],{"class":280,"line":295},[278,49056,49057],{},"  const gamesCollection = mongoDb.collection('games');\n",[278,49059,49060],{"class":280,"line":316},[278,49061,49062],{},"  const currGame = await gamesCollection.findOne({ current: true });\n",[278,49064,49065],{"class":280,"line":322},[278,49066,49067],{},"  if (currGame) {\n",[278,49069,49070],{"class":280,"line":327},[278,49071,49072],{},"    console.log('got the current game: ', JSON.stringify(currGame));\n",[278,49074,49075],{"class":280,"line":340},[278,49076,49077],{},"    const date = new Date();\n",[278,49079,49080],{"class":280,"line":349},[278,49081,49082],{},"    const nextGameDate = new Date();\n",[278,49084,49085],{"class":280,"line":375},[278,49086,49087],{},"    nextGameDate.setUTCHours(0, 0, 0, 0);\n",[278,49089,49090],{"class":280,"line":386},[278,49091,49092],{},"    nextGameDate.setUTCDate(nextGameDate.getDate() + 1);\n",[278,49094,49095],{"class":280,"line":397},[278,49096,49097],{},"    await gamesCollection.bulkWrite(\n",[278,49099,49100],{"class":280,"line":408},[278,49101,49102],{},"      [\n",[278,49104,49105],{"class":280,"line":433},[278,49106,2581],{},[278,49108,49109],{"class":280,"line":454},[278,49110,49111],{},"          updateOne: {\n",[278,49113,49114],{"class":280,"line":475},[278,49115,49116],{},"            filter: { gameNo: currGame.gameNo + 1 },\n",[278,49118,49119],{"class":280,"line":496},[278,49120,49121],{},"            update: {\n",[278,49123,49124],{"class":280,"line":505},[278,49125,49126],{},"              $set: {\n",[278,49128,49129],{"class":280,"line":516},[278,49130,49131],{},"                current: true,\n",[278,49133,49134],{"class":280,"line":527},[278,49135,49136],{},"                active: true,\n",[278,49138,49139],{"class":280,"line":533},[278,49140,49141],{},"                updatedAt: date,\n",[278,49143,49144],{"class":280,"line":539},[278,49145,49146],{},"                playedAt: date,\n",[278,49148,49149],{"class":280,"line":545},[278,49150,49151],{},"                nextGameAt: nextGameDate,\n",[278,49153,49154],{"class":280,"line":551},[278,49155,14019],{},[278,49157,49158],{"class":280,"line":557},[278,49159,26697],{},[278,49161,49162],{"class":280,"line":567},[278,49163,11557],{},[278,49165,49166],{"class":280,"line":577},[278,49167,2606],{},[278,49169,49170],{"class":280,"line":587},[278,49171,2581],{},[278,49173,49174],{"class":280,"line":597},[278,49175,49111],{},[278,49177,49178],{"class":280,"line":608},[278,49179,49180],{},"            filter: { _id: currGame._id },\n",[278,49182,49183],{"class":280,"line":614},[278,49184,49185],{},"            update: { $set: { current: false, updatedAt: date } },\n",[278,49187,49188],{"class":280,"line":620},[278,49189,11557],{},[278,49191,49192],{"class":280,"line":625},[278,49193,2606],{},[278,49195,49196],{"class":280,"line":640},[278,49197,32588],{},[278,49199,49200],{"class":280,"line":663},[278,49201,49202],{},"      { ordered: true }\n",[278,49204,49205],{"class":280,"line":669},[278,49206,1898],{},[278,49208,49209],{"class":280,"line":680},[278,49210,292],{"emptyLinePlaceholder":291},[278,49212,49213],{"class":280,"line":686},[278,49214,49215],{},"    console.log('after the bulkWrite Op');\n",[278,49217,49218],{"class":280,"line":1334},[278,49219,8120],{},[278,49221,49222],{"class":280,"line":1375},[278,49223,49224],{},"    console.log('Error! No current game found.');\n",[278,49226,49227],{"class":280,"line":1381},[278,49228,1096],{},[278,49230,49231],{"class":280,"line":1386},[278,49232,2817],{},[32,49234,49236],{"id":49235},"database-trigger-and-backing-function","Database Trigger and Backing Function",[11,49238,49239],{},"Since we're consuming the stored game documents every day automatically (using the implementation above), we also need to find a way to generate games the same way. This can be done using database triggers. Whenever we mark one of the games as the current game, we can listen for the database trigger and generate a new game by calling our old function (which we created in the beginning).",[11,49241,49242],{},[3135,49243],{"alt":49244,"src":49245},"Creating database trigger 1","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002F8ian9-Sc-1668119a08.png",[11,49247,49248],{},[3135,49249],{"alt":49250,"src":49251},"Creating database trigger 2","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002Fa5yo3-zAF-edf7517e0d.png",[11,49253,49254,49255,49258,49259,49262],{},"Selected ",[59,49256,49257],{},"Operation Type as Update",". Since on game refresh, 2 documents are updated (one the current game, and the other the next game), so used a match expression (Advanced Optional setting) to only trigger the function for the next game document. We're updating the ",[59,49260,49261],{},"playedAt"," field as well on game refresh so I used that in the match expression.",[269,49264,49266],{"className":24597,"code":49265,"language":24599,"meta":274,"style":274},"{\"updateDescription.updatedFields.playedAt\":{\"$exists\":true}}\n",[59,49267,49268],{"__ignoreMap":274},[278,49269,49270],{"class":280,"line":281},[278,49271,49265],{},[11,49273,49274],{},[3135,49275],{"alt":49276,"src":49277},"Creating database trigger 3","\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002FdMu9n6eNu-6b1b7e5555.png",[11,49279,49280,49281,49284],{},"Somehow the match expression with a boolean field (",[59,49282,49283],{},"current",") was not working.",[269,49286,49288],{"className":24597,"code":49287,"language":24599,"meta":274,"style":274},"{\"updateDescription.updatedFields\":{\"current\":true}}\n",[59,49289,49290],{"__ignoreMap":274},[278,49291,49292],{"class":280,"line":281},[278,49293,49287],{},[11,49295,49296],{},"I didn't try with the dot notation, maybe that would've worked.",[269,49298,49300],{"className":24597,"code":49299,"language":24599,"meta":274,"style":274},"{\"updateDescription.updatedFields.current\": true}\n",[59,49301,49302],{"__ignoreMap":274},[278,49303,49304],{"class":280,"line":281},[278,49305,49299],{},[11,49307,49308,49309,49312],{},"We could have created a new game in the scheduled trigger itself (where we refresh our daily puzzle), but there is an upper limit of ",[59,49310,49311],{},"150 seconds"," in the app services on any function run time. So database trigger made more sense as it will provide some extra buffer time to my game generation function.",[32,49314,49316],{"id":49315},"anonymous-authentication-and-auth-trigger","Anonymous Authentication and Auth Trigger",[11,49318,49319],{},"We also want to store the users' play history. So enabled anonymous auth the same from the UI. Also added an auth trigger so that we can save the newly created users to the DB.",[11,49321,49322],{},"Auth Trigger function code which gets trigged on new user creation.",[269,49324,49326],{"className":24597,"code":49325,"language":24599,"meta":274,"style":274},"exports = async function (authEvent) {\n  const { user, time } = authEvent;\n\n  const mongoDb = context.services.get('cluster-service-name').db('db-name');\n  const usersCollection = mongoDb.collection('users');\n  const userData = { _id: user.id, ...user, createdAt: time, updatedAt: time };\n  userData.data = {\n    currStreak: 0,\n    longestStreak: 0,\n    isCurrLongestStreak: false,\n    solves: 0,\n    played: 0,\n  };\n\n  delete userData.id;\n  const res = await usersCollection.insertOne(userData);\n  console.log('result of user insert op: ', JSON.stringify(res));\n};\n",[59,49327,49328,49333,49337,49341,49346,49350,49355,49360,49365,49370,49375,49380,49385,49389,49393,49398,49402,49406],{"__ignoreMap":274},[278,49329,49330],{"class":280,"line":281},[278,49331,49332],{},"exports = async function (authEvent) {\n",[278,49334,49335],{"class":280,"line":288},[278,49336,42843],{},[278,49338,49339],{"class":280,"line":295},[278,49340,292],{"emptyLinePlaceholder":291},[278,49342,49343],{"class":280,"line":316},[278,49344,49345],{},"  const mongoDb = context.services.get('cluster-service-name').db('db-name');\n",[278,49347,49348],{"class":280,"line":322},[278,49349,42857],{},[278,49351,49352],{"class":280,"line":327},[278,49353,49354],{},"  const userData = { _id: user.id, ...user, createdAt: time, updatedAt: time };\n",[278,49356,49357],{"class":280,"line":340},[278,49358,49359],{},"  userData.data = {\n",[278,49361,49362],{"class":280,"line":349},[278,49363,49364],{},"    currStreak: 0,\n",[278,49366,49367],{"class":280,"line":375},[278,49368,49369],{},"    longestStreak: 0,\n",[278,49371,49372],{"class":280,"line":386},[278,49373,49374],{},"    isCurrLongestStreak: false,\n",[278,49376,49377],{"class":280,"line":397},[278,49378,49379],{},"    solves: 0,\n",[278,49381,49382],{"class":280,"line":408},[278,49383,49384],{},"    played: 0,\n",[278,49386,49387],{"class":280,"line":433},[278,49388,901],{},[278,49390,49391],{"class":280,"line":454},[278,49392,292],{"emptyLinePlaceholder":291},[278,49394,49395],{"class":280,"line":475},[278,49396,49397],{},"  delete userData.id;\n",[278,49399,49400],{"class":280,"line":496},[278,49401,42918],{},[278,49403,49404],{"class":280,"line":505},[278,49405,42923],{},[278,49407,49408],{"class":280,"line":516},[278,49409,2817],{},[11,49411,49412],{},"Didn't use any other type of auth provider as I don't want the players to worry about login at this point.",[32,49414,49416],{"id":49415},"frontend-hosting-with-google-cloud-run","Frontend Hosting with Google Cloud Run",[11,49418,49419],{},"The frontend is a basic React app which uses the Realm-SDK to interact with the backend which we just created. You can go through the code in the shared GitHub repo.",[11,49421,49422],{},"I wanted to try out App Services hosting also, but apparently, that is only available for paid accounts. Google Cloud to the rescue. Utilized Google Cloud Run to host the application frontend.",[11,49424,49425,49426,49429],{},"To do this we need to create a ",[59,49427,49428],{},"Dockerfile"," with the below content in the frontend root folder (wherever the frontend package.json is)",[269,49431,49433],{"className":24597,"code":49432,"language":24599,"meta":274,"style":274},"FROM node:lts-alpine as react-build\nWORKDIR \u002Fapp\nCOPY . .\u002F\nRUN yarn\nRUN yarn build\n\n# server environment\nFROM nginx:alpine\nCOPY nginx.conf \u002Fetc\u002Fnginx\u002Fconf.d\u002Fconfigfile.template\n\nCOPY --from=react-build \u002Fapp\u002Fbuild \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\n\nENV PORT 8080\nENV HOST 0.0.0.0\nEXPOSE 8080\nCMD sh -c \"envsubst '\\$PORT' \u003C \u002Fetc\u002Fnginx\u002Fconf.d\u002Fconfigfile.template > \u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf && nginx -g 'daemon off;'\"\n",[59,49434,49435,49440,49445,49450,49455,49460,49464,49469,49474,49479,49483,49488,49492,49497,49502,49507],{"__ignoreMap":274},[278,49436,49437],{"class":280,"line":281},[278,49438,49439],{},"FROM node:lts-alpine as react-build\n",[278,49441,49442],{"class":280,"line":288},[278,49443,49444],{},"WORKDIR \u002Fapp\n",[278,49446,49447],{"class":280,"line":295},[278,49448,49449],{},"COPY . .\u002F\n",[278,49451,49452],{"class":280,"line":316},[278,49453,49454],{},"RUN yarn\n",[278,49456,49457],{"class":280,"line":322},[278,49458,49459],{},"RUN yarn build\n",[278,49461,49462],{"class":280,"line":327},[278,49463,292],{"emptyLinePlaceholder":291},[278,49465,49466],{"class":280,"line":340},[278,49467,49468],{},"# server environment\n",[278,49470,49471],{"class":280,"line":349},[278,49472,49473],{},"FROM nginx:alpine\n",[278,49475,49476],{"class":280,"line":375},[278,49477,49478],{},"COPY nginx.conf \u002Fetc\u002Fnginx\u002Fconf.d\u002Fconfigfile.template\n",[278,49480,49481],{"class":280,"line":386},[278,49482,292],{"emptyLinePlaceholder":291},[278,49484,49485],{"class":280,"line":397},[278,49486,49487],{},"COPY --from=react-build \u002Fapp\u002Fbuild \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml\n",[278,49489,49490],{"class":280,"line":408},[278,49491,292],{"emptyLinePlaceholder":291},[278,49493,49494],{"class":280,"line":433},[278,49495,49496],{},"ENV PORT 8080\n",[278,49498,49499],{"class":280,"line":454},[278,49500,49501],{},"ENV HOST 0.0.0.0\n",[278,49503,49504],{"class":280,"line":475},[278,49505,49506],{},"EXPOSE 8080\n",[278,49508,49509],{"class":280,"line":496},[278,49510,49511],{},"CMD sh -c \"envsubst '\\$PORT' \u003C \u002Fetc\u002Fnginx\u002Fconf.d\u002Fconfigfile.template > \u002Fetc\u002Fnginx\u002Fconf.d\u002Fdefault.conf && nginx -g 'daemon off;'\"\n",[11,49513,49514,49515,49518],{},"We also need to create an ",[59,49516,49517],{},"nginx.conf"," file in the frontend root folder",[269,49520,49522],{"className":24597,"code":49521,"language":24599,"meta":274,"style":274},"server {\n     listen       $PORT;\n     server_name  localhost;\n\n     location \u002F {\n         root   \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml;\n         index  index.html index.htm;\n         try_files $uri \u002Findex.html;\n     }\n\n     gzip on;\n     gzip_vary on;\n     gzip_min_length 10240;\n     gzip_proxied expired no-cache no-store private auth;\n     gzip_types text\u002Fplain text\u002Fcss text\u002Fxml text\u002Fjavascript application\u002Fx-javascript application\u002Fxml;\n     gzip_disable \"MSIE [1-6]\\.\";\n}\n",[59,49523,49524,49529,49534,49539,49543,49548,49553,49558,49563,49568,49572,49577,49582,49587,49592,49597,49602],{"__ignoreMap":274},[278,49525,49526],{"class":280,"line":281},[278,49527,49528],{},"server {\n",[278,49530,49531],{"class":280,"line":288},[278,49532,49533],{},"     listen       $PORT;\n",[278,49535,49536],{"class":280,"line":295},[278,49537,49538],{},"     server_name  localhost;\n",[278,49540,49541],{"class":280,"line":316},[278,49542,292],{"emptyLinePlaceholder":291},[278,49544,49545],{"class":280,"line":322},[278,49546,49547],{},"     location \u002F {\n",[278,49549,49550],{"class":280,"line":327},[278,49551,49552],{},"         root   \u002Fusr\u002Fshare\u002Fnginx\u002Fhtml;\n",[278,49554,49555],{"class":280,"line":340},[278,49556,49557],{},"         index  index.html index.htm;\n",[278,49559,49560],{"class":280,"line":349},[278,49561,49562],{},"         try_files $uri \u002Findex.html;\n",[278,49564,49565],{"class":280,"line":375},[278,49566,49567],{},"     }\n",[278,49569,49570],{"class":280,"line":386},[278,49571,292],{"emptyLinePlaceholder":291},[278,49573,49574],{"class":280,"line":397},[278,49575,49576],{},"     gzip on;\n",[278,49578,49579],{"class":280,"line":408},[278,49580,49581],{},"     gzip_vary on;\n",[278,49583,49584],{"class":280,"line":433},[278,49585,49586],{},"     gzip_min_length 10240;\n",[278,49588,49589],{"class":280,"line":454},[278,49590,49591],{},"     gzip_proxied expired no-cache no-store private auth;\n",[278,49593,49594],{"class":280,"line":475},[278,49595,49596],{},"     gzip_types text\u002Fplain text\u002Fcss text\u002Fxml text\u002Fjavascript application\u002Fx-javascript application\u002Fxml;\n",[278,49598,49599],{"class":280,"line":496},[278,49600,49601],{},"     gzip_disable \"MSIE [1-6]\\.\";\n",[278,49603,49604],{"class":280,"line":505},[278,49605,617],{},[11,49607,49608],{},"Now we can build the project in a docker container using the below command (Do remember to create a Google Cloud project, and enable Cloud Run API, Google Container Registry API & Cloud Build API for that project)",[269,49610,49612],{"className":24597,"code":49611,"language":24599,"meta":274,"style":274},"gcloud builds submit --tag gcr.io\u002F\u003Cgoogle_project_id>\u002Fapp\n",[59,49613,49614],{"__ignoreMap":274},[278,49615,49616],{"class":280,"line":281},[278,49617,49611],{},[11,49619,49620],{},"Once the build is successful, we can deploy the application by running",[269,49622,49624],{"className":24597,"code":49623,"language":24599,"meta":274,"style":274},"gcloud run deploy --image gcr.io\u002F\u003Cproject_id>\u002Fapp --platform managed\n",[59,49625,49626],{"__ignoreMap":274},[278,49627,49628],{"class":280,"line":281},[278,49629,49623],{},[11,49631,49632],{},"And voila, we can visit our frontend by going to the service URL as mentioned in the console.",[24,49634,31922],{"id":10535},[123,49636,49637,49640,49643,49646,49649,49652],{},[74,49638,49639],{},"Frontend code for the gameplay needs some refactoring",[74,49641,49642],{},"Game stats and analytics needs to be added",[74,49644,49645],{},"The current user's game history is getting stored in DB (needs further testing) but it is not getting displayed anywhere. Need to create another app route for the same",[74,49647,49648],{},"Caching has not been used anywhere. Maybe we can utilize Firebase hosting for application caching, as well as for caching the current game data",[74,49650,49651],{},"For user gameplay-related interactions with the DB, need to use change streams\u002Fwatch for real-time updates.",[74,49653,49654],{},"Notify the user if a new game is available while they're using the app",[24,49656,10820],{"id":49657},"github-link",[40,49659],{"url":49660},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fgoldroad",[24,49662,10634],{"id":10633},[11,49664,49665],{},"Overall it was a very good experience building the game with MongoDB Atlas & App Services. We can have improved docs, but I am happy to have utilized this time and gotten familiar with Atlas & App Services.",[11,49667,49668,49669,183],{},"Hope you enjoyed reading the article and will also enjoy playing ",[47,49670,49672],{"href":45239,"rel":49671},[51],"the game",[3065,49674,49675],{},"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":274,"searchDepth":288,"depth":288,"links":49677},[49678,49679,49680,49684,49685,49696,49697,49698],{"id":45753,"depth":288,"text":45754},{"id":45765,"depth":288,"text":45766},{"id":45787,"depth":288,"text":45788,"children":49681},[49682,49683],{"id":45798,"depth":295,"text":45799},{"id":45808,"depth":295,"text":45809},{"id":45818,"depth":288,"text":45819},{"id":45825,"depth":288,"text":45826,"children":49686},[49687,49688,49689,49690,49691,49692,49693,49694,49695],{"id":45832,"depth":295,"text":45833},{"id":45918,"depth":295,"text":45919},{"id":45980,"depth":295,"text":45981},{"id":48902,"depth":295,"text":48903},{"id":49001,"depth":295,"text":49002},{"id":49017,"depth":295,"text":49018},{"id":49235,"depth":295,"text":49236},{"id":49315,"depth":295,"text":49316},{"id":49415,"depth":295,"text":49416},{"id":10535,"depth":288,"text":31922},{"id":49657,"depth":288,"text":10820},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run\u002F0ROzF2p1D-110886424e.png","2022-12-12T20:20:50.813Z","Step by step guide on using MongoDB Atlas & App Services to create a puzzle game, and how to use Google Cloud Run to host the react frontend.","clbl8p4ot000a08jver0rggb3",{},"\u002Fcreating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run",{"title":45748,"description":49701},"creating-daily-puzzle-game-with-react-mongodb-gcp-cloud-run",[44739,49708,3095,49709],"game-development","cloudrun","qA6P3ZZTf0pblHb3dx-qr-BLd1ZAPfk8Dxz1pkSHN2E",{"id":49712,"title":49713,"body":49714,"cover":50901,"date":50902,"description":50903,"draft":3086,"extension":3087,"hashnodeId":50904,"meta":50905,"navigation":291,"path":50906,"seo":50907,"slug":50908,"stem":50908,"tags":50909,"__hash__":50910},"posts\u002Fusing-python-multiprocessing-to-optimize-puzzle-solving.md","Using Python multiprocessing to optimize a problem",{"type":8,"value":49715,"toc":50886},[49716,49718,49733,49736,49740,49743,49749,49789,49792,49799,49802,49805,49808,49811,49822,49829,50250,50259,50262,50271,50281,50285,50295,50301,50304,50349,50352,50358,50364,50367,50378,50381,50387,50391,50402,50409,50429,50805,50808,50814,50817,50821,50828,50833,50838,50841,50847,50850,50853,50856,50858,50872,50875,50878,50881,50884],[24,49717,22772],{"id":22771},[11,49719,49720,49721,49726,49727,49732],{},"This is the final part of the Python puzzle game series. ",[47,49722,49725],{"href":49723,"rel":49724},"https:\u002F\u002Frajeev.dev\u002Fcreating-puzzle-game-using-python-turtle-1",[51],"In the first part"," we learnt to create a tiles puzzle game using Turtle module. And then ",[47,49728,49731],{"href":49729,"rel":49730},"https:\u002F\u002Frajeev.dev\u002Fsolve-puzzle-game-using-python",[51],"in the second part"," created an algorithm to find a solution of the puzzle. But the algorithm can be optimized further, and in this article we will look at some of the ways we can achieve that.",[11,49734,49735],{},"To make sense of this article, I suggest that first you go through the previous posts in this series (if you haven't already done so).",[24,49737,49739],{"id":49738},"the-need-for-further-optimization","The need for further optimization",[11,49741,49742],{},"Let's run the final code from the previous post on a different puzzle which requires more number of moves.",[11,49744,49745],{},[3135,49746],{"alt":49747,"src":49748},"A different puzzle","\u002Fimages\u002Fposts\u002Fusing-python-multiprocessing-to-optimize-puzzle-solving\u002FRMHO7pz6T-088b45b524.png",[269,49750,49752],{"className":35072,"code":49751,"language":35074,"meta":274,"style":274},"play([\n    ['hot pink', 'turquoise', 'yellow', 'white', 'turquoise'],\n    ['white', 'hot pink', 'turquoise', 'yellow', 'hot pink'],\n    ['white', 'yellow', 'hot pink', 'white', 'yellow'],\n    ['hot pink', 'yellow', 'white', 'hot pink', 'white'],\n    ['turquoise', 'turquoise', 'yellow', 'hot pink', 'yellow']\n])\n",[59,49753,49754,49759,49764,49769,49774,49779,49784],{"__ignoreMap":274},[278,49755,49756],{"class":280,"line":281},[278,49757,49758],{},"play([\n",[278,49760,49761],{"class":280,"line":288},[278,49762,49763],{},"    ['hot pink', 'turquoise', 'yellow', 'white', 'turquoise'],\n",[278,49765,49766],{"class":280,"line":295},[278,49767,49768],{},"    ['white', 'hot pink', 'turquoise', 'yellow', 'hot pink'],\n",[278,49770,49771],{"class":280,"line":316},[278,49772,49773],{},"    ['white', 'yellow', 'hot pink', 'white', 'yellow'],\n",[278,49775,49776],{"class":280,"line":322},[278,49777,49778],{},"    ['hot pink', 'yellow', 'white', 'hot pink', 'white'],\n",[278,49780,49781],{"class":280,"line":327},[278,49782,49783],{},"    ['turquoise', 'turquoise', 'yellow', 'hot pink', 'yellow']\n",[278,49785,49786],{"class":280,"line":340},[278,49787,49788],{},"])\n",[11,49790,49791],{},"These are the results",[269,49793,49797],{"className":49794,"code":49796,"language":4582},[49795],"language-text","start time: 0.113825565\nchanged min_moves to: 16, 4000001111223334, time: 0.12959095\nchanged min_moves to: 15, 400000114313122, time: 0.135476449\nchanged min_moves to: 14, 40000432310211, time: 0.483449261\nchanged min_moves to: 13, 4433100000122, time: 49.005072394\nNo more jobs: final count: 2012130, time: 324.622757167\nresult of operation: {'min_moves': '4433100000122', 'min_moves_len': 13, 'count': 2012130}\n",[59,49798,49796],{"__ignoreMap":274},[11,49800,49801],{},"So even with our previous optimization, we ran close to 2 million end to end sequences, and it took us around 5 minutes and 25 seconds to finish the job. Do remember that we're only looking for one valid solution, and not trying to find all possible sequences of the same length (and believe me, there will be many such sequences).",[11,49803,49804],{},"So, can we do something more?",[40,49806],{"url":49807},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fia3zQtyPYYhcpznFJ2\u002Fgiphy.gif",[11,49809,49810],{},"Well, what if we could run some of these jobs in parallel? That should save us some time, don't you think? Let's find out.",[32,49812,49814,49815,19634,49818,49821],{"id":49813},"modified-play-find_min_moves-functions","Modified ",[59,49816,49817],{},"play",[59,49819,49820],{},"find_min_moves"," functions",[11,49823,49824,49825,49828],{},"We will be using ",[59,49826,49827],{},"multiprocessing"," module to run multiple processes in parallel to see if it can make a difference. The reason for going with multiple processes instead of multiple threads is simple, our code is pure compute problem with no IO related tasks. Multiple threads make sense where there are some wait times involved (as in the case of IO tasks, like downloading something from internet etc).",[269,49830,49832],{"className":35072,"code":49831,"language":35074,"meta":274,"style":274},"from timeit import default_timer as timer\nimport copy\nimport multiprocessing as mp\nimport os\n\nimport AutoPlay\n\ndef find_min_moves(task):\n    # Create an internal job list for this process and add the incoming task to it\n    jobs = [task]\n    pid = os.getpid()\n    result = {'min_moves': None, 'min_moves_len': 0, 'count': 0, 'pid': pid}\n\n    print(\n        f'entered pid: {pid}: for moves: {task[\"curr_move\"]}, time: {timer()}')\n\n    while True:\n        # See if we've any jobs left. If there is no job, break the loop\n        job = jobs.pop() if len(jobs) > 0 else None\n        if job is None:\n            print(\n                f'No more jobs: pid: {pid}, final count: {result[\"count\"]}, time: {timer()}')\n            break\n\n        # Handle the current job. This will take of the combinations till its logical\n        # end (until the board is clear). Other encountered combinations will be added\n        # to the job list for processing in due course\n        final_moves_seq = AutoPlay.handle_job_recurse(\n            job, jobs, result['min_moves_len'])\n\n        result['count'] += 1\n\n        # If the one processed combination has minimum length, then that is the minimum\n        # numbers of moves needed to solve the puzzle\n        if result['min_moves_len'] == 0 or (final_moves_seq is not None\n                                            and len(final_moves_seq) \u003C result['min_moves_len']):\n            result['min_moves'] = final_moves_seq\n            result['min_moves_len'] = len(final_moves_seq)\n            print(\n                f'pid: {pid}, changed min_moves to: {result[\"min_moves_len\"]}, {final_moves_seq}, time: {timer()}')\n\n    return result\n\ndef play(colors):\n    # Single game object which holds the tiles in play,\n    # and the current connections groups and clickables\n    game = {\n        'tiles': [],\n        'clickables': [],\n        'connection_groups': []\n    }\n\n    # Set the board as per the input colors\n    for col in range(AutoPlay.MAX_COLS):\n        game['tiles'].append([])\n        for row in range(AutoPlay.MAX_ROWS):\n            tile = {'id': (row, col), \"connections\": [],\n                    \"clickable\": False, \"color\": colors[col][row]}\n            game['tiles'][col].append(tile)\n\n    # Go through the tiles and find out the connections\n    # between them, and also save the clickables\n    for col in range(AutoPlay.MAX_COLS):\n        AutoPlay.process_tile(game, 0, col)\n\n    start = timer()\n    print(f'start time: {start}')\n\n    # Create as many tasks as there are connection groups.\n    # We're using deepcopy to create a deeply cloned game\n    # object for each task. The current move is the first\n    # entry of every connection group (the lowest column\n    # index in the bottom row)\n    tasks = []\n    for connections in game['connection_groups']:\n        g = copy.deepcopy(game)\n        tasks.append(\n            {'game': g, 'curr_move': connections[0], 'past_moves': None})\n\n    # Get a managed pool from multiprocessing, and distribute the tasks to these\n    # pools. By default it will create processes equal to the number returned by\n    # os.cpu_count().\n    with mp.Pool() as pool:\n        results = pool.map(find_min_moves, tasks)\n        print('got results:', timer())\n        for result in results:\n            print('result:', result)\n",[59,49833,49834,49839,49844,49849,49854,49858,49863,49867,49872,49877,49882,49887,49892,49896,49901,49906,49910,49914,49919,49924,49929,49934,49939,49943,49947,49952,49957,49962,49967,49972,49976,49981,49985,49990,49995,50000,50005,50010,50015,50019,50024,50028,50033,50037,50042,50047,50052,50057,50062,50067,50072,50076,50080,50085,50090,50095,50100,50105,50110,50115,50119,50124,50129,50133,50138,50142,50147,50152,50156,50161,50166,50171,50176,50181,50186,50191,50196,50201,50206,50210,50215,50220,50225,50230,50235,50240,50245],{"__ignoreMap":274},[278,49835,49836],{"class":280,"line":281},[278,49837,49838],{},"from timeit import default_timer as timer\n",[278,49840,49841],{"class":280,"line":288},[278,49842,49843],{},"import copy\n",[278,49845,49846],{"class":280,"line":295},[278,49847,49848],{},"import multiprocessing as mp\n",[278,49850,49851],{"class":280,"line":316},[278,49852,49853],{},"import os\n",[278,49855,49856],{"class":280,"line":322},[278,49857,292],{"emptyLinePlaceholder":291},[278,49859,49860],{"class":280,"line":327},[278,49861,49862],{},"import AutoPlay\n",[278,49864,49865],{"class":280,"line":340},[278,49866,292],{"emptyLinePlaceholder":291},[278,49868,49869],{"class":280,"line":349},[278,49870,49871],{},"def find_min_moves(task):\n",[278,49873,49874],{"class":280,"line":375},[278,49875,49876],{},"    # Create an internal job list for this process and add the incoming task to it\n",[278,49878,49879],{"class":280,"line":386},[278,49880,49881],{},"    jobs = [task]\n",[278,49883,49884],{"class":280,"line":397},[278,49885,49886],{},"    pid = os.getpid()\n",[278,49888,49889],{"class":280,"line":408},[278,49890,49891],{},"    result = {'min_moves': None, 'min_moves_len': 0, 'count': 0, 'pid': pid}\n",[278,49893,49894],{"class":280,"line":433},[278,49895,292],{"emptyLinePlaceholder":291},[278,49897,49898],{"class":280,"line":454},[278,49899,49900],{},"    print(\n",[278,49902,49903],{"class":280,"line":475},[278,49904,49905],{},"        f'entered pid: {pid}: for moves: {task[\"curr_move\"]}, time: {timer()}')\n",[278,49907,49908],{"class":280,"line":496},[278,49909,292],{"emptyLinePlaceholder":291},[278,49911,49912],{"class":280,"line":505},[278,49913,36239],{},[278,49915,49916],{"class":280,"line":516},[278,49917,49918],{},"        # See if we've any jobs left. If there is no job, break the loop\n",[278,49920,49921],{"class":280,"line":527},[278,49922,49923],{},"        job = jobs.pop() if len(jobs) > 0 else None\n",[278,49925,49926],{"class":280,"line":533},[278,49927,49928],{},"        if job is None:\n",[278,49930,49931],{"class":280,"line":539},[278,49932,49933],{},"            print(\n",[278,49935,49936],{"class":280,"line":545},[278,49937,49938],{},"                f'No more jobs: pid: {pid}, final count: {result[\"count\"]}, time: {timer()}')\n",[278,49940,49941],{"class":280,"line":551},[278,49942,36379],{},[278,49944,49945],{"class":280,"line":557},[278,49946,292],{"emptyLinePlaceholder":291},[278,49948,49949],{"class":280,"line":567},[278,49950,49951],{},"        # Handle the current job. This will take of the combinations till its logical\n",[278,49953,49954],{"class":280,"line":577},[278,49955,49956],{},"        # end (until the board is clear). Other encountered combinations will be added\n",[278,49958,49959],{"class":280,"line":587},[278,49960,49961],{},"        # to the job list for processing in due course\n",[278,49963,49964],{"class":280,"line":597},[278,49965,49966],{},"        final_moves_seq = AutoPlay.handle_job_recurse(\n",[278,49968,49969],{"class":280,"line":608},[278,49970,49971],{},"            job, jobs, result['min_moves_len'])\n",[278,49973,49974],{"class":280,"line":614},[278,49975,292],{"emptyLinePlaceholder":291},[278,49977,49978],{"class":280,"line":620},[278,49979,49980],{},"        result['count'] += 1\n",[278,49982,49983],{"class":280,"line":625},[278,49984,292],{"emptyLinePlaceholder":291},[278,49986,49987],{"class":280,"line":640},[278,49988,49989],{},"        # If the one processed combination has minimum length, then that is the minimum\n",[278,49991,49992],{"class":280,"line":663},[278,49993,49994],{},"        # numbers of moves needed to solve the puzzle\n",[278,49996,49997],{"class":280,"line":669},[278,49998,49999],{},"        if result['min_moves_len'] == 0 or (final_moves_seq is not None\n",[278,50001,50002],{"class":280,"line":680},[278,50003,50004],{},"                                            and len(final_moves_seq) \u003C result['min_moves_len']):\n",[278,50006,50007],{"class":280,"line":686},[278,50008,50009],{},"            result['min_moves'] = final_moves_seq\n",[278,50011,50012],{"class":280,"line":1334},[278,50013,50014],{},"            result['min_moves_len'] = len(final_moves_seq)\n",[278,50016,50017],{"class":280,"line":1375},[278,50018,49933],{},[278,50020,50021],{"class":280,"line":1381},[278,50022,50023],{},"                f'pid: {pid}, changed min_moves to: {result[\"min_moves_len\"]}, {final_moves_seq}, time: {timer()}')\n",[278,50025,50026],{"class":280,"line":1386},[278,50027,292],{"emptyLinePlaceholder":291},[278,50029,50030],{"class":280,"line":1394},[278,50031,50032],{},"    return result\n",[278,50034,50035],{"class":280,"line":1406},[278,50036,292],{"emptyLinePlaceholder":291},[278,50038,50039],{"class":280,"line":1423},[278,50040,50041],{},"def play(colors):\n",[278,50043,50044],{"class":280,"line":1432},[278,50045,50046],{},"    # Single game object which holds the tiles in play,\n",[278,50048,50049],{"class":280,"line":1437},[278,50050,50051],{},"    # and the current connections groups and clickables\n",[278,50053,50054],{"class":280,"line":1916},[278,50055,50056],{},"    game = {\n",[278,50058,50059],{"class":280,"line":1939},[278,50060,50061],{},"        'tiles': [],\n",[278,50063,50064],{"class":280,"line":1949},[278,50065,50066],{},"        'clickables': [],\n",[278,50068,50069],{"class":280,"line":1954},[278,50070,50071],{},"        'connection_groups': []\n",[278,50073,50074],{"class":280,"line":1959},[278,50075,1285],{},[278,50077,50078],{"class":280,"line":1985},[278,50079,292],{"emptyLinePlaceholder":291},[278,50081,50082],{"class":280,"line":1990},[278,50083,50084],{},"    # Set the board as per the input colors\n",[278,50086,50087],{"class":280,"line":1997},[278,50088,50089],{},"    for col in range(AutoPlay.MAX_COLS):\n",[278,50091,50092],{"class":280,"line":2006},[278,50093,50094],{},"        game['tiles'].append([])\n",[278,50096,50097],{"class":280,"line":2018},[278,50098,50099],{},"        for row in range(AutoPlay.MAX_ROWS):\n",[278,50101,50102],{"class":280,"line":2029},[278,50103,50104],{},"            tile = {'id': (row, col), \"connections\": [],\n",[278,50106,50107],{"class":280,"line":2034},[278,50108,50109],{},"                    \"clickable\": False, \"color\": colors[col][row]}\n",[278,50111,50112],{"class":280,"line":2040},[278,50113,50114],{},"            game['tiles'][col].append(tile)\n",[278,50116,50117],{"class":280,"line":2045},[278,50118,292],{"emptyLinePlaceholder":291},[278,50120,50121],{"class":280,"line":2068},[278,50122,50123],{},"    # Go through the tiles and find out the connections\n",[278,50125,50126],{"class":280,"line":2099},[278,50127,50128],{},"    # between them, and also save the clickables\n",[278,50130,50131],{"class":280,"line":6428},[278,50132,50089],{},[278,50134,50135],{"class":280,"line":6439},[278,50136,50137],{},"        AutoPlay.process_tile(game, 0, col)\n",[278,50139,50140],{"class":280,"line":6450},[278,50141,292],{"emptyLinePlaceholder":291},[278,50143,50144],{"class":280,"line":6455},[278,50145,50146],{},"    start = timer()\n",[278,50148,50149],{"class":280,"line":6460},[278,50150,50151],{},"    print(f'start time: {start}')\n",[278,50153,50154],{"class":280,"line":6475},[278,50155,292],{"emptyLinePlaceholder":291},[278,50157,50158],{"class":280,"line":6486},[278,50159,50160],{},"    # Create as many tasks as there are connection groups.\n",[278,50162,50163],{"class":280,"line":6491},[278,50164,50165],{},"    # We're using deepcopy to create a deeply cloned game\n",[278,50167,50168],{"class":280,"line":6518},[278,50169,50170],{},"    # object for each task. The current move is the first\n",[278,50172,50173],{"class":280,"line":6530},[278,50174,50175],{},"    # entry of every connection group (the lowest column\n",[278,50177,50178],{"class":280,"line":6542},[278,50179,50180],{},"    # index in the bottom row)\n",[278,50182,50183],{"class":280,"line":6547},[278,50184,50185],{},"    tasks = []\n",[278,50187,50188],{"class":280,"line":6552},[278,50189,50190],{},"    for connections in game['connection_groups']:\n",[278,50192,50193],{"class":280,"line":6567},[278,50194,50195],{},"        g = copy.deepcopy(game)\n",[278,50197,50198],{"class":280,"line":6580},[278,50199,50200],{},"        tasks.append(\n",[278,50202,50203],{"class":280,"line":6593},[278,50204,50205],{},"            {'game': g, 'curr_move': connections[0], 'past_moves': None})\n",[278,50207,50208],{"class":280,"line":6605},[278,50209,292],{"emptyLinePlaceholder":291},[278,50211,50212],{"class":280,"line":6620},[278,50213,50214],{},"    # Get a managed pool from multiprocessing, and distribute the tasks to these\n",[278,50216,50217],{"class":280,"line":6625},[278,50218,50219],{},"    # pools. By default it will create processes equal to the number returned by\n",[278,50221,50222],{"class":280,"line":6633},[278,50223,50224],{},"    # os.cpu_count().\n",[278,50226,50227],{"class":280,"line":6643},[278,50228,50229],{},"    with mp.Pool() as pool:\n",[278,50231,50232],{"class":280,"line":6657},[278,50233,50234],{},"        results = pool.map(find_min_moves, tasks)\n",[278,50236,50237],{"class":280,"line":6665},[278,50238,50239],{},"        print('got results:', timer())\n",[278,50241,50242],{"class":280,"line":6670},[278,50243,50244],{},"        for result in results:\n",[278,50246,50247],{"class":280,"line":6675},[278,50248,50249],{},"            print('result:', result)\n",[269,50251,50253],{"className":35072,"code":50252,"language":35074,"meta":274,"style":274},"with mp.Pool() as pool:\n",[59,50254,50255],{"__ignoreMap":274},[278,50256,50257],{"class":280,"line":281},[278,50258,50252],{},[11,50260,50261],{},"gives us a managed pool which cleans after itself.",[269,50263,50265],{"className":35072,"code":50264,"language":35074,"meta":274,"style":274},"results = pool.map(find_min_moves, tasks)\n",[59,50266,50267],{"__ignoreMap":274},[278,50268,50269],{"class":280,"line":281},[278,50270,50264],{},[11,50272,50273,50274,50276,50277,50280],{},"is a blocking function which takes care of distributing the tasks to the processes from the pool. For every process, ",[59,50275,49820],{}," function is called with one task from the ",[59,50278,50279],{},"tasks"," list.",[32,50282,50284],{"id":50283},"the-result","The result",[11,50286,50287,50288,50290,50291,50294],{},"If we try to call our ",[59,50289,49817],{}," function now, we will get the below ",[59,50292,50293],{},"RuntimeError"," (on Windows and macOS).",[269,50296,50299],{"className":50297,"code":50298,"language":4582},[49795],"An attempt has been made to start a new process before the\ncurrent process has finished its bootstrapping phase.\n \nThis probably means that you are not using fork to start your\nchild processes and you have forgotten to use the proper idiom\nin the main module:\n \n    if __name__ == '__main__':\n        freeze_support()\n        ...\n \nThe \"freeze_support()\" line can be omitted if the program\nis not going to be frozen to produce an executable.\n",[59,50300,50298],{"__ignoreMap":274},[11,50302,50303],{},"As the error is saying, we need to protect the entry point of our program, else it will enter into an endless loop while spawning child processes. Let's modify the calling part",[269,50305,50307],{"className":35072,"code":50306,"language":35074,"meta":274,"style":274},"if __name__ == '__main__':\n    play([\n        ['hot pink', 'turquoise', 'yellow', 'white', 'turquoise'],\n        ['white', 'hot pink', 'turquoise', 'yellow', 'hot pink'],\n        ['white', 'yellow', 'hot pink', 'white', 'yellow'],\n        ['hot pink', 'yellow', 'white', 'hot pink', 'white'],\n        ['turquoise', 'turquoise', 'yellow', 'hot pink', 'yellow']\n    ])\n",[59,50308,50309,50314,50319,50324,50329,50334,50339,50344],{"__ignoreMap":274},[278,50310,50311],{"class":280,"line":281},[278,50312,50313],{},"if __name__ == '__main__':\n",[278,50315,50316],{"class":280,"line":288},[278,50317,50318],{},"    play([\n",[278,50320,50321],{"class":280,"line":295},[278,50322,50323],{},"        ['hot pink', 'turquoise', 'yellow', 'white', 'turquoise'],\n",[278,50325,50326],{"class":280,"line":316},[278,50327,50328],{},"        ['white', 'hot pink', 'turquoise', 'yellow', 'hot pink'],\n",[278,50330,50331],{"class":280,"line":322},[278,50332,50333],{},"        ['white', 'yellow', 'hot pink', 'white', 'yellow'],\n",[278,50335,50336],{"class":280,"line":327},[278,50337,50338],{},"        ['hot pink', 'yellow', 'white', 'hot pink', 'white'],\n",[278,50340,50341],{"class":280,"line":340},[278,50342,50343],{},"        ['turquoise', 'turquoise', 'yellow', 'hot pink', 'yellow']\n",[278,50345,50346],{"class":280,"line":349},[278,50347,50348],{},"    ])\n",[11,50350,50351],{},"And here are the results of the execution",[269,50353,50356],{"className":50354,"code":50355,"language":4582},[49795],"start time: 0.05643949\nentered pid: 39066: for moves: (0, 0), time: 0.120805656\npid: 39066, changed min_moves to: 18, 000001111223334444, time: 0.128779476\npid: 39066, changed min_moves to: 17, 00000111122344334, time: 0.130322022\npid: 39066, changed min_moves to: 16, 0000011112423334, time: 0.131969189\npid: 39066, changed min_moves to: 15, 000001144313122, time: 0.209503056\nentered pid: 39067: for moves: (0, 1), time: 0.193182385\npid: 39067, changed min_moves to: 16, 1000001223334444, time: 0.203432333\npid: 39067, changed min_moves to: 15, 100000122344334, time: 0.206294855\npid: 39067, changed min_moves to: 14, 10000012423334, time: 0.209911962\nentered pid: 39068: for moves: (0, 3), time: 0.2225178\nentered pid: 39069: for moves: (0, 4), time: 0.22732867\npid: 39068, changed min_moves to: 18, 300000111122334444, time: 0.254337267\npid: 39068, changed min_moves to: 17, 30000011112244334, time: 0.268903578\npid: 39069, changed min_moves to: 16, 4000001111223334, time: 0.272185431\npid: 39069, changed min_moves to: 15, 400000114313122, time: 0.278042101\npid: 39068, changed min_moves to: 16, 3000001114431224, time: 0.337434568\npid: 39068, changed min_moves to: 15, 300000114131224, time: 0.351929327\npid: 39069, changed min_moves to: 14, 40000432310211, time: 1.06737366\npid: 39068, changed min_moves to: 14, 30000423104211, time: 1.918205703\npid: 39067, changed min_moves to: 13, 1004430003122, time: 2.159644669\npid: 39066, changed min_moves to: 14, 00004432310211, time: 4.574963226\nNo more jobs: pid: 39067, final count: 171415, time: 52.145841877\npid: 39069, changed min_moves to: 13, 4433100000122, time: 96.061340938\nNo more jobs: pid: 39069, final count: 418533, time: 115.766546083\npid: 39068, changed min_moves to: 13, 3431000001224, time: 217.433144061\nNo more jobs: pid: 39068, final count: 1166097, time: 256.900814533\nNo more jobs: pid: 39066, final count: 2631991, time: 479.720000441\ngot results: 479.826363948\nresult: {'min_moves': '00004432310211', 'min_moves_len': 14, 'count': 2631991, 'pid': 39066}\nresult: {'min_moves': '1004430003122', 'min_moves_len': 13, 'count': 171415, 'pid': 39067}\nresult: {'min_moves': '3431000001224', 'min_moves_len': 13, 'count': 1166097, 'pid': 39068}\nresult: {'min_moves': '4433100000122', 'min_moves_len': 13, 'count': 418533, 'pid': 39069}\n",[59,50357,50355],{"__ignoreMap":274},[11,50359,50360,50363],{},[94,50361,50362],{},"Wow! This is bad..."," We took close to 8 minutes, to finish executing the program, and did almost 4.4 million end to end sequences. Please notice that the program was running in parallel for sometime with process ids 39066-39069. We found 3 different sequences all giving us 13 as the minimum number of moves.",[11,50365,50366],{},"So what went wrong as compared to the previous case?",[11,50368,50369,50370,50373,50374,50377],{},"The thing is, in the case of a single process, the ",[59,50371,50372],{},"min_moves"," variable was same for all the executions, but here that is not the case. We had different variables (",[59,50375,50376],{},"result[\"min_moves_len\"]",") for different starting moves. So, even though the code ran in parallel, individually every process did more end-to-end sequences because of the forced silos.",[11,50379,50380],{},"**How do we fix this? **",[11,50382,50383,50384,50386],{},"We somehow need to have a common ",[59,50385,50372],{}," variable between these processes. We can't simply use a global variable for the same as different processes have their own memory space. We need to take help from multiprocessing module itself for achieving data sharing.",[24,50388,50390],{"id":50389},"the-final-optimization","The final optimization",[11,50392,50393,50394,50397,50398,50401],{},"We will use a synchronized shared object ",[59,50395,50396],{},"Value()"," for storing the min_moves_len, and then we will use it in our processes to make a decision. Since knowing the current min moves length in our processes is sufficient for us, and so, using ",[59,50399,50400],{},"Value"," makes more sense (for storing a number, and it is faster too) as compared to other ways of sharing data between processes.",[32,50403,49814,50405,919,50407,49821],{"id":50404},"modified-play-and-find_min_moves-functions",[59,50406,49817],{},[59,50408,49820],{},[11,50410,50411,50412,50415,50416,50418,50419,50421,50422,50425,50426,183],{},"We are using another function called ",[59,50413,50414],{},"init_globals"," to initialize each process with a ",[59,50417,50372],{}," global variable for that process. We need to pass this function as initializer while creating the processes pool. We also create one ",[59,50420,50396],{}," object (",[59,50423,50424],{},"min_val = mp.Value('i', 0)",", where 'i' denotes a signed integer), and pass that to the initializer function as ",[59,50427,50428],{},"initargs",[269,50430,50432],{"className":35072,"code":50431,"language":35074,"meta":274,"style":274},"def init_globals(min_val):\n    global min_moves\n    min_moves = min_val\n\n# To use the min_moves variable we just need to use its `value` attribute\n# like, min_moves.value\ndef find_min_moves(task):\n    # Create an interal job list for this process and add the incoming task to it\n    jobs = [task]\n    pid = os.getpid()\n    result = {'min_moves': None, 'min_moves_len': 0, 'count': 0, 'pid': pid}\n\n    print(\n        f'entered pid: {pid}: for moves: {task[\"curr_move\"]}, time: {timer()}')\n\n    while True:\n        # See if we've any jobs left. If there is no job, break the loop\n        job = jobs.pop() if len(jobs) > 0 else None\n        if job is None:\n            print(\n                f'No more jobs: pid: {pid}, final count: {result[\"count\"]}, time: {timer()}')\n            break\n\n        # Handle the current job. This will take of the combinations till its logical\n        # end (until the board is clear). Other encountered combinations will be added\n        # to the job list for processing in due course\n        final_moves_seq = AutoPlay.handle_job_recurse(\n            job, jobs, min_moves.value)\n\n        result['count'] += 1\n\n        # If the one processed combination has minimum length, then that is the minimum\n        # numbers of moves needed to solve the puzzle\n        if min_moves.value == 0 or (final_moves_seq is not None\n                                    and len(final_moves_seq) \u003C min_moves.value):\n            min_moves.value = len(final_moves_seq)\n            result['min_moves'] = final_moves_seq\n            result['min_moves_len'] = min_moves.value\n            print(\n                f'pid: {pid}, changed min_moves to: {result[\"min_moves_len\"]}, {final_moves_seq}, time: {timer()}')\n\n    return result\n\ndef play(colors):\n    # Single game object which holds the tiles in play,\n    # and the current connections groups and clickables\n    game = {\n        'tiles': [],\n        'clickables': [],\n        'connection_groups': []\n    }\n\n    # Set the board as per the input colors\n    for col in range(AutoPlay.MAX_COLS):\n        game['tiles'].append([])\n        for row in range(AutoPlay.MAX_ROWS):\n            tile = {'id': (row, col), \"connections\": [],\n                    \"clickable\": False, \"color\": colors[col][row]}\n            game['tiles'][col].append(tile)\n\n    # Go through the tiles and find out the connections\n    # between them, and also save the clickables\n    for col in range(AutoPlay.MAX_COLS):\n        AutoPlay.process_tile(game, 0, col)\n\n    start = timer()\n    print(f'start time: {start}')\n\n    # Create as many tasks as there are connection groups.\n    # We're using deepcopy to create a deeply cloned game\n    # object for each task. The current move is the first\n    # entry of every connection group (the lowest column\n    # index in the bottom row)\n    tasks = []\n    for connections in game['connection_groups']:\n        g = copy.deepcopy(game)\n        tasks.append(\n            {'game': g, 'curr_move': connections[0], 'past_moves': None})\n\n    min_val = mp.Value('i', 0)\n    # Get a managed pool from multiprocessing, and distribute the tasks to these\n    # pools. By default it will create processes equal to the number returned by\n    # os.cpu_count(). Also initialize min_moves global for each process using a \n    # Value() shared object\n    with mp.Pool(initializer=init_globals, initargs=(min_val,)) as pool:\n        results = pool.map(find_min_moves, tasks)\n        print('got results:', timer())\n        for result in results:\n            print('result:', result)\n",[59,50433,50434,50439,50444,50449,50453,50458,50463,50467,50472,50476,50480,50484,50488,50492,50496,50500,50504,50508,50512,50516,50520,50524,50528,50532,50536,50540,50544,50548,50553,50557,50561,50565,50569,50573,50578,50583,50588,50592,50597,50601,50605,50609,50613,50617,50621,50625,50629,50633,50637,50641,50645,50649,50653,50657,50661,50665,50669,50673,50677,50681,50685,50689,50693,50697,50701,50705,50709,50713,50717,50721,50725,50729,50733,50737,50741,50745,50749,50753,50757,50761,50766,50770,50774,50779,50784,50789,50793,50797,50801],{"__ignoreMap":274},[278,50435,50436],{"class":280,"line":281},[278,50437,50438],{},"def init_globals(min_val):\n",[278,50440,50441],{"class":280,"line":288},[278,50442,50443],{},"    global min_moves\n",[278,50445,50446],{"class":280,"line":295},[278,50447,50448],{},"    min_moves = min_val\n",[278,50450,50451],{"class":280,"line":316},[278,50452,292],{"emptyLinePlaceholder":291},[278,50454,50455],{"class":280,"line":322},[278,50456,50457],{},"# To use the min_moves variable we just need to use its `value` attribute\n",[278,50459,50460],{"class":280,"line":327},[278,50461,50462],{},"# like, min_moves.value\n",[278,50464,50465],{"class":280,"line":340},[278,50466,49871],{},[278,50468,50469],{"class":280,"line":349},[278,50470,50471],{},"    # Create an interal job list for this process and add the incoming task to it\n",[278,50473,50474],{"class":280,"line":375},[278,50475,49881],{},[278,50477,50478],{"class":280,"line":386},[278,50479,49886],{},[278,50481,50482],{"class":280,"line":397},[278,50483,49891],{},[278,50485,50486],{"class":280,"line":408},[278,50487,292],{"emptyLinePlaceholder":291},[278,50489,50490],{"class":280,"line":433},[278,50491,49900],{},[278,50493,50494],{"class":280,"line":454},[278,50495,49905],{},[278,50497,50498],{"class":280,"line":475},[278,50499,292],{"emptyLinePlaceholder":291},[278,50501,50502],{"class":280,"line":496},[278,50503,36239],{},[278,50505,50506],{"class":280,"line":505},[278,50507,49918],{},[278,50509,50510],{"class":280,"line":516},[278,50511,49923],{},[278,50513,50514],{"class":280,"line":527},[278,50515,49928],{},[278,50517,50518],{"class":280,"line":533},[278,50519,49933],{},[278,50521,50522],{"class":280,"line":539},[278,50523,49938],{},[278,50525,50526],{"class":280,"line":545},[278,50527,36379],{},[278,50529,50530],{"class":280,"line":551},[278,50531,292],{"emptyLinePlaceholder":291},[278,50533,50534],{"class":280,"line":557},[278,50535,49951],{},[278,50537,50538],{"class":280,"line":567},[278,50539,49956],{},[278,50541,50542],{"class":280,"line":577},[278,50543,49961],{},[278,50545,50546],{"class":280,"line":587},[278,50547,49966],{},[278,50549,50550],{"class":280,"line":597},[278,50551,50552],{},"            job, jobs, min_moves.value)\n",[278,50554,50555],{"class":280,"line":608},[278,50556,292],{"emptyLinePlaceholder":291},[278,50558,50559],{"class":280,"line":614},[278,50560,49980],{},[278,50562,50563],{"class":280,"line":620},[278,50564,292],{"emptyLinePlaceholder":291},[278,50566,50567],{"class":280,"line":625},[278,50568,49989],{},[278,50570,50571],{"class":280,"line":640},[278,50572,49994],{},[278,50574,50575],{"class":280,"line":663},[278,50576,50577],{},"        if min_moves.value == 0 or (final_moves_seq is not None\n",[278,50579,50580],{"class":280,"line":669},[278,50581,50582],{},"                                    and len(final_moves_seq) \u003C min_moves.value):\n",[278,50584,50585],{"class":280,"line":680},[278,50586,50587],{},"            min_moves.value = len(final_moves_seq)\n",[278,50589,50590],{"class":280,"line":686},[278,50591,50009],{},[278,50593,50594],{"class":280,"line":1334},[278,50595,50596],{},"            result['min_moves_len'] = min_moves.value\n",[278,50598,50599],{"class":280,"line":1375},[278,50600,49933],{},[278,50602,50603],{"class":280,"line":1381},[278,50604,50023],{},[278,50606,50607],{"class":280,"line":1386},[278,50608,292],{"emptyLinePlaceholder":291},[278,50610,50611],{"class":280,"line":1394},[278,50612,50032],{},[278,50614,50615],{"class":280,"line":1406},[278,50616,292],{"emptyLinePlaceholder":291},[278,50618,50619],{"class":280,"line":1423},[278,50620,50041],{},[278,50622,50623],{"class":280,"line":1432},[278,50624,50046],{},[278,50626,50627],{"class":280,"line":1437},[278,50628,50051],{},[278,50630,50631],{"class":280,"line":1916},[278,50632,50056],{},[278,50634,50635],{"class":280,"line":1939},[278,50636,50061],{},[278,50638,50639],{"class":280,"line":1949},[278,50640,50066],{},[278,50642,50643],{"class":280,"line":1954},[278,50644,50071],{},[278,50646,50647],{"class":280,"line":1959},[278,50648,1285],{},[278,50650,50651],{"class":280,"line":1985},[278,50652,292],{"emptyLinePlaceholder":291},[278,50654,50655],{"class":280,"line":1990},[278,50656,50084],{},[278,50658,50659],{"class":280,"line":1997},[278,50660,50089],{},[278,50662,50663],{"class":280,"line":2006},[278,50664,50094],{},[278,50666,50667],{"class":280,"line":2018},[278,50668,50099],{},[278,50670,50671],{"class":280,"line":2029},[278,50672,50104],{},[278,50674,50675],{"class":280,"line":2034},[278,50676,50109],{},[278,50678,50679],{"class":280,"line":2040},[278,50680,50114],{},[278,50682,50683],{"class":280,"line":2045},[278,50684,292],{"emptyLinePlaceholder":291},[278,50686,50687],{"class":280,"line":2068},[278,50688,50123],{},[278,50690,50691],{"class":280,"line":2099},[278,50692,50128],{},[278,50694,50695],{"class":280,"line":6428},[278,50696,50089],{},[278,50698,50699],{"class":280,"line":6439},[278,50700,50137],{},[278,50702,50703],{"class":280,"line":6450},[278,50704,292],{"emptyLinePlaceholder":291},[278,50706,50707],{"class":280,"line":6455},[278,50708,50146],{},[278,50710,50711],{"class":280,"line":6460},[278,50712,50151],{},[278,50714,50715],{"class":280,"line":6475},[278,50716,292],{"emptyLinePlaceholder":291},[278,50718,50719],{"class":280,"line":6486},[278,50720,50160],{},[278,50722,50723],{"class":280,"line":6491},[278,50724,50165],{},[278,50726,50727],{"class":280,"line":6518},[278,50728,50170],{},[278,50730,50731],{"class":280,"line":6530},[278,50732,50175],{},[278,50734,50735],{"class":280,"line":6542},[278,50736,50180],{},[278,50738,50739],{"class":280,"line":6547},[278,50740,50185],{},[278,50742,50743],{"class":280,"line":6552},[278,50744,50190],{},[278,50746,50747],{"class":280,"line":6567},[278,50748,50195],{},[278,50750,50751],{"class":280,"line":6580},[278,50752,50200],{},[278,50754,50755],{"class":280,"line":6593},[278,50756,50205],{},[278,50758,50759],{"class":280,"line":6605},[278,50760,292],{"emptyLinePlaceholder":291},[278,50762,50763],{"class":280,"line":6620},[278,50764,50765],{},"    min_val = mp.Value('i', 0)\n",[278,50767,50768],{"class":280,"line":6625},[278,50769,50214],{},[278,50771,50772],{"class":280,"line":6633},[278,50773,50219],{},[278,50775,50776],{"class":280,"line":6643},[278,50777,50778],{},"    # os.cpu_count(). Also initialize min_moves global for each process using a \n",[278,50780,50781],{"class":280,"line":6657},[278,50782,50783],{},"    # Value() shared object\n",[278,50785,50786],{"class":280,"line":6665},[278,50787,50788],{},"    with mp.Pool(initializer=init_globals, initargs=(min_val,)) as pool:\n",[278,50790,50791],{"class":280,"line":6670},[278,50792,50234],{},[278,50794,50795],{"class":280,"line":6675},[278,50796,50239],{},[278,50798,50799],{"class":280,"line":6680},[278,50800,50244],{},[278,50802,50803],{"class":280,"line":6698},[278,50804,50249],{},[32,50806,50284],{"id":50807},"the-result-1",[269,50809,50812],{"className":50810,"code":50811,"language":4582},[49795],"start time: 0.057702366\nentered pid: 39420: for moves: (0, 0), time: 0.14133282\npid: 39420, changed min_moves to: 18, 000001111223334444, time: 0.149716058\npid: 39420, changed min_moves to: 17, 00000111122344334, time: 0.151552746\npid: 39420, changed min_moves to: 16, 0000011112423334, time: 0.153877295\nentered pid: 39419: for moves: (0, 1), time: 0.195602278\npid: 39419, changed min_moves to: 15, 100000122344334, time: 0.203628052\npid: 39419, changed min_moves to: 14, 10000012423334, time: 0.205814167\nentered pid: 39421: for moves: (0, 3), time: 0.175389625\nentered pid: 39422: for moves: (0, 4), time: 0.230612233\npid: 39419, changed min_moves to: 13, 1004430003122, time: 2.607543404\nNo more jobs: pid: 39419, final count: 171415, time: 61.43746481\nNo more jobs: pid: 39422, final count: 210959, time: 77.746001708\nNo more jobs: pid: 39421, final count: 536682, time: 144.053795224\nNo more jobs: pid: 39420, final count: 895965, time: 210.756625277\ngot results: 211.040409704\nresult: {'min_moves': '0000011112423334', 'min_moves_len': 16, 'count': 895965, 'pid': 39420}\nresult: {'min_moves': '1004430003122', 'min_moves_len': 13, 'count': 171415, 'pid': 39419}\nresult: {'min_moves': None, 'min_moves_len': 0, 'count': 536682, 'pid': 39421}\nresult: {'min_moves': None, 'min_moves_len': 0, 'count': 210959, 'pid': 39422}\n",[59,50813,50811],{"__ignoreMap":274},[11,50815,50816],{},"Yay! We have made progress. Now it takes only 3 minutes 31 seconds to do the job (as compared to around 5 minutes 25 seconds with single process), with around 1.81 million end to end sequences.",[24,50818,50820],{"id":50819},"the-cpu-count-and-its-implications","The CPU count and its implications",[11,50822,50823,50824,50827],{},"Please note that all of the results were collected on my macbook pro having a dual core processor. So ",[59,50825,50826],{},"os.cpu_count()"," returns 4 in my case, 2 logical cores for each physical core. That is why 4 processes are getting created in the above examples. We can play around with the number of processes and see if that makes any further difference. In my case, I've found that running 2 processes gives me the best result (The value may be different for you, depending on the number of cores your workhorse has).",[11,50829,50830,50831,748],{},"Changing one line in the ",[59,50832,49817],{},[11,50834,50835],{},[59,50836,50837],{},"with mp.Pool(processes=2, initializer=init_globals, initargs=(min_val,)) as pool:",[11,50839,50840],{},"We get the following results:",[269,50842,50845],{"className":50843,"code":50844,"language":4582},[49795],"start time: 0.103170482\nentered pid: 39540: for moves: (0, 0), time: 0.226195533\npid: 39540, changed min_moves to: 18, 000001111223334444, time: 0.238348483\npid: 39540, changed min_moves to: 17, 00000111122344334, time: 0.239980543\npid: 39540, changed min_moves to: 16, 0000011112423334, time: 0.265787826\nentered pid: 39541: for moves: (0, 1), time: 0.239561019\npid: 39541, changed min_moves to: 15, 100000122344334, time: 0.246132816\npid: 39541, changed min_moves to: 14, 10000012423334, time: 0.247991375\npid: 39541, changed min_moves to: 13, 1004430003122, time: 1.319787871\nNo more jobs: pid: 39541, final count: 171415, time: 27.815133454\nentered pid: 39541: for moves: (0, 3), time: 27.816530592\nNo more jobs: pid: 39541, final count: 533304, time: 135.0901852\nentered pid: 39541: for moves: (0, 4), time: 135.090335439\nNo more jobs: pid: 39541, final count: 207783, time: 172.9650854\nNo more jobs: pid: 39540, final count: 895570, time: 183.029733485\ngot results: 183.21633772\nresult: {'min_moves': '0000011112423334', 'min_moves_len': 16, 'count': 895570, 'pid': 39540}\nresult: {'min_moves': '1004430003122', 'min_moves_len': 13, 'count': 171415, 'pid': 39541}\nresult: {'min_moves': None, 'min_moves_len': 0, 'count': 533304, 'pid': 39541}\nresult: {'min_moves': None, 'min_moves_len': 0, 'count': 207783, 'pid': 39541}\n",[59,50846,50844],{"__ignoreMap":274},[11,50848,50849],{},"Only 2 processes with pids 39540 & 39541 were used in this case. When the process with pid 39540 was going through sequences starting with 0 column id, process with pid 39541 finished processing the sequences starting with remaining column ids (1, 3 & 4 in this case). It took nearly 3 minutes (so a saving of around 30 seconds) to finish the job, with nearly the same 1.8 million end-to-end sequences.",[11,50851,50852],{},"And we've have a winner in our midst :-)",[40,50854],{"url":50855},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fl44Q6Etd5kdSGttXa\u002Fgiphy.gif",[24,50857,10634],{"id":10633},[11,50859,50860,50861,50864,50865,50868,50869,50871],{},"That was one long article with a lot of code, and repeated optimizations. I'm sure further optimizations can be done. I also tried to distribute jobs between different processes using a shared ",[59,50862,50863],{},"queue"," object (sgain from multiprocessing), but didn't get favorable results. There is one ",[59,50866,50867],{},"Manager"," class also available in the ",[59,50870,49827],{}," module, which allows us to create shared proxy objects. Using that we wouldn't need to use the initializer and initargs, and can share the object as argument like we are doing for tasks, but the documentation says that it will be slow, so I haven't covered that (of course I tried it myself :-)).",[11,50873,50874],{},"There must be other ways to solve the problem, after all, there are multiple ways to solve any problem in general, and programming in particular. It might be possible that there is a very simple trick to solve this instantly.",[11,50876,50877],{},"Do share how would you solve this problem?",[11,50879,50880],{},"Please hit me up if you have any questions, or if you find any error anywhere.",[11,50882,50883],{},"Enjoy :-)",[3065,50885,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":50887},[50888,50889,50894,50899,50900],{"id":22771,"depth":288,"text":22772},{"id":49738,"depth":288,"text":49739,"children":50890},[50891,50893],{"id":49813,"depth":295,"text":50892},"Modified play & find_min_moves functions",{"id":50283,"depth":295,"text":50284},{"id":50389,"depth":288,"text":50390,"children":50895},[50896,50898],{"id":50404,"depth":295,"text":50897},"Modified play and find_min_moves functions",{"id":50807,"depth":295,"text":50284},{"id":50819,"depth":288,"text":50820},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fusing-python-multiprocessing-to-optimize-puzzle-solving\u002FiAsHu7b-l-07a58e8ed5.png","2022-11-21T12:48:43.209Z","Introduction This is the final part of the Python puzzle game series. In the first part we learnt to create a tiles puzzle game using Turtle module. And then in the second part...","claqsasw9001c08jk2nppbp9y",{},"\u002Fusing-python-multiprocessing-to-optimize-puzzle-solving",{"title":49713,"description":50903},"using-python-multiprocessing-to-optimize-puzzle-solving",[38595,35074,49708,49827],"L3aboVrZFNptZQhfNV6JEQqgIP08H6GyKRi7I6pdJ4A",{"id":50912,"title":50913,"body":50914,"cover":52277,"date":52278,"description":52279,"draft":3086,"extension":3087,"hashnodeId":52280,"meta":52281,"navigation":291,"path":52282,"seo":52283,"slug":52284,"stem":52284,"tags":52285,"__hash__":52288},"posts\u002Fsolve-puzzle-game-using-python.md","Solving the turtle tiles puzzle game using Python",{"type":8,"value":50915,"toc":52256},[50916,50918,50926,50929,50932,50936,50939,50945,50948,50965,50968,50970,50979,50985,50988,50993,51177,51182,51301,51307,51310,51855,51857,51860,51898,51904,51911,51914,51917,51921,51924,51941,51944,51949,52076,52082,52229,52231,52234,52240,52243,52246,52248,52251,52254],[24,50917,22772],{"id":22771},[11,50919,50920,50921,50925],{},"This is a follow up of my ",[47,50922,50924],{"href":49723,"rel":50923},[51],"previous article"," where we learnt to create a puzzle game using Python Turtle module. There we could generate as many new games as we wanted, and also replay a particular game unlimited number of times.\nBut there was one important piece missing from the equation: finding the minimum number of moves needed to solve the puzzle (i.e. to remove all the tiles from the screen).",[40,50927],{"url":50928},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002F3o6Mb2KGFpsvvjWbPW\u002Fgiphy.gif",[11,50930,50931],{},"Worry not! In this article we will look at a crude way of solving the puzzle. Then we'll try to optimize our solution and make it faster. Strap your seatbelts, here we go...",[24,50933,50935],{"id":50934},"approach","Approach",[11,50937,50938],{},"As we had learnt in the last article, to clear the board we need to click on the solid color tiles. Any tile which is in the bottom row is clickable (and hence, solid), and tiles which are connected to these bottom row tiles become clickable if they have the same color (see the below image for understanding). When we click on a solid tile, that tile and all the connected tiles get removed from the screen. New connections are formed after every click following the earlier logic.",[11,50940,50941],{},[3135,50942],{"alt":50943,"src":50944},"figure.jpg","\u002Fimages\u002Fposts\u002Fsolve-puzzle-game-using-python\u002F08SxOhgmR-4844fb2c0f.jpg",[11,50946,50947],{},"Now that we understand the game, let's try to come up with a logic to find out the minimum number of moves needed to clear the screen. Below are my observations:",[123,50949,50950,50953,50956,50959,50962],{},[74,50951,50952],{},"Even though we can click on any of the solid tiles (even if that tile is in, say 2nd or 3rd row), it is enough to always click on the bottom row tiles only",[74,50954,50955],{},"If more than one tile in the bottom row are connected to each other, it is enough to click on the tile having the lowest column index",[74,50957,50958],{},"For every move, we will have at most 5 choices (the 5 bottom row tiles). Overall there will be a lot of combinations of moves, and we'll need to go through all of them to find out the solution",[74,50960,50961],{},"After clicking a tile new connections are formed, and if we keep repeating the first 2 steps for any of the possible combinations, there will come a time when the board is clear",[74,50963,50964],{},"Finding one solution (one sequence of moves giving empty board) is sufficient for us",[11,50966,50967],{},"With these observations in mind, let's implement our algorithm.",[24,50969,23958],{"id":23957},[11,50971,50972,50973,50978],{},"Below is a screenshot of Nov 16-17, 2022's puzzle from the ",[47,50974,50977],{"href":50975,"rel":50976},"https:\u002F\u002Ffigure.game\u002F",[51],"original site",". The same combination has been used to create this article's cover image. We'll use this as a reference to verify if our solution is correct (our solution should give us 8 minimum moves as the answer).",[11,50980,50981],{},[3135,50982],{"alt":50983,"src":50984},"Screen Shot 2022-11-16 at 10.59.22 PM.png","\u002Fimages\u002Fposts\u002Fsolve-puzzle-game-using-python\u002FRMiUT4MyE-45f5a2e90a.png",[11,50986,50987],{},"We won't be using any custom classes for this implementation, and will rely on good old lists and dictionaries.",[32,50989,4796,50991,748],{"id":50990},"the-play-function",[59,50992,49817],{},[269,50994,50996],{"className":35072,"code":50995,"language":35074,"meta":274,"style":274},"from timeit import default_timer as timer\nimport copy\n\nimport AutoPlay\n\ndef play(colors):\n    # Single game object which holds the tiles in play,\n    # and the current connections groups and clickables\n    game = {\n        'tiles': [],\n        'clickables': [],\n        'connection_groups': []\n    }\n\n    # Set the board as per the input colors\n    for col in range(AutoPlay.MAX_COLS):\n        game['tiles'].append([])\n        for row in range(AutoPlay.MAX_ROWS):\n            tile = {'id': (row, col), \"connections\": [],\n                    \"clickable\": False, \"color\": colors[col][row]}\n            game['tiles'][col].append(tile)\n\n    # Go through the tiles and find out the connections\n    # between them, and also save the clickables\n    for col in range(AutoPlay.MAX_COLS):\n        AutoPlay.process_tile(game, 0, col)\n\n    start = timer()\n    print(f'start time: {start}')\n\n    # Create as many tasks as there are connection groups.\n    # We're using deepcopy to create a deeply cloned game\n    # object for each task. The current move is the first\n    # entry of every connection group (the lowest column\n    # index in the bottom row)\n    tasks = []\n    for connections in game['connection_groups']:\n        g = copy.deepcopy(game)\n        tasks.append(\n            {'game': g, 'curr_move': connections[0], 'past_moves': None})\n\n    # Find the minimum number of moves for the above tasks\n    result = find_min_moves(tasks)\n    print('result of operation:', result)\n",[59,50997,50998,51002,51006,51010,51014,51018,51022,51026,51030,51034,51038,51042,51046,51050,51054,51058,51062,51066,51070,51074,51078,51082,51086,51090,51094,51098,51102,51106,51110,51114,51118,51122,51126,51130,51134,51138,51142,51146,51150,51154,51158,51162,51167,51172],{"__ignoreMap":274},[278,50999,51000],{"class":280,"line":281},[278,51001,49838],{},[278,51003,51004],{"class":280,"line":288},[278,51005,49843],{},[278,51007,51008],{"class":280,"line":295},[278,51009,292],{"emptyLinePlaceholder":291},[278,51011,51012],{"class":280,"line":316},[278,51013,49862],{},[278,51015,51016],{"class":280,"line":322},[278,51017,292],{"emptyLinePlaceholder":291},[278,51019,51020],{"class":280,"line":327},[278,51021,50041],{},[278,51023,51024],{"class":280,"line":340},[278,51025,50046],{},[278,51027,51028],{"class":280,"line":349},[278,51029,50051],{},[278,51031,51032],{"class":280,"line":375},[278,51033,50056],{},[278,51035,51036],{"class":280,"line":386},[278,51037,50061],{},[278,51039,51040],{"class":280,"line":397},[278,51041,50066],{},[278,51043,51044],{"class":280,"line":408},[278,51045,50071],{},[278,51047,51048],{"class":280,"line":433},[278,51049,1285],{},[278,51051,51052],{"class":280,"line":454},[278,51053,292],{"emptyLinePlaceholder":291},[278,51055,51056],{"class":280,"line":475},[278,51057,50084],{},[278,51059,51060],{"class":280,"line":496},[278,51061,50089],{},[278,51063,51064],{"class":280,"line":505},[278,51065,50094],{},[278,51067,51068],{"class":280,"line":516},[278,51069,50099],{},[278,51071,51072],{"class":280,"line":527},[278,51073,50104],{},[278,51075,51076],{"class":280,"line":533},[278,51077,50109],{},[278,51079,51080],{"class":280,"line":539},[278,51081,50114],{},[278,51083,51084],{"class":280,"line":545},[278,51085,292],{"emptyLinePlaceholder":291},[278,51087,51088],{"class":280,"line":551},[278,51089,50123],{},[278,51091,51092],{"class":280,"line":557},[278,51093,50128],{},[278,51095,51096],{"class":280,"line":567},[278,51097,50089],{},[278,51099,51100],{"class":280,"line":577},[278,51101,50137],{},[278,51103,51104],{"class":280,"line":587},[278,51105,292],{"emptyLinePlaceholder":291},[278,51107,51108],{"class":280,"line":597},[278,51109,50146],{},[278,51111,51112],{"class":280,"line":608},[278,51113,50151],{},[278,51115,51116],{"class":280,"line":614},[278,51117,292],{"emptyLinePlaceholder":291},[278,51119,51120],{"class":280,"line":620},[278,51121,50160],{},[278,51123,51124],{"class":280,"line":625},[278,51125,50165],{},[278,51127,51128],{"class":280,"line":640},[278,51129,50170],{},[278,51131,51132],{"class":280,"line":663},[278,51133,50175],{},[278,51135,51136],{"class":280,"line":669},[278,51137,50180],{},[278,51139,51140],{"class":280,"line":680},[278,51141,50185],{},[278,51143,51144],{"class":280,"line":686},[278,51145,50190],{},[278,51147,51148],{"class":280,"line":1334},[278,51149,50195],{},[278,51151,51152],{"class":280,"line":1375},[278,51153,50200],{},[278,51155,51156],{"class":280,"line":1381},[278,51157,50205],{},[278,51159,51160],{"class":280,"line":1386},[278,51161,292],{"emptyLinePlaceholder":291},[278,51163,51164],{"class":280,"line":1394},[278,51165,51166],{},"    # Find the minimum number of moves for the above tasks\n",[278,51168,51169],{"class":280,"line":1406},[278,51170,51171],{},"    result = find_min_moves(tasks)\n",[278,51173,51174],{"class":280,"line":1423},[278,51175,51176],{},"    print('result of operation:', result)\n",[32,51178,4796,51180,748],{"id":51179},"the-find_min_moves-function",[59,51181,49820],{},[269,51183,51185],{"className":35072,"code":51184,"language":35074,"meta":274,"style":274},"def find_min_moves(jobs):\n    result = {'min_moves': None, 'min_moves_len': 0, 'count': 0}\n\n    while True:\n        # See if we've any jobs left. If there is no job, break the loop\n        job = jobs.pop() if len(jobs) > 0 else None\n        if job is None:\n            print(\n                f'No more jobs: final count: {result[\"count\"]}, time: {timer()}')\n            break\n\n        # Handle the current job. This will take of the combinations till its logical\n        # end (until the board is clear). Other encountered combinations will be added\n        # to the job list for processing in due course\n        final_moves_seq = AutoPlay.handle_job_recurse(job, jobs)\n\n        result['count'] += 1\n\n        # If the one processed combination has minimum length, then that is the minimum\n        # numbers of moves needed to solve the puzzle\n        if result['min_moves_len'] == 0 or len(final_moves_seq) \u003C result['min_moves_len']:\n            result['min_moves'] = final_moves_seq\n            result['min_moves_len'] = len(final_moves_seq)\n            print(\n                f'changed min_moves to: {result[\"min_moves_len\"]}, {final_moves_seq}, time: {timer()}')\n\n    return result\n",[59,51186,51187,51192,51197,51201,51205,51209,51213,51217,51221,51226,51230,51234,51238,51242,51246,51251,51255,51259,51263,51267,51271,51276,51280,51284,51288,51293,51297],{"__ignoreMap":274},[278,51188,51189],{"class":280,"line":281},[278,51190,51191],{},"def find_min_moves(jobs):\n",[278,51193,51194],{"class":280,"line":288},[278,51195,51196],{},"    result = {'min_moves': None, 'min_moves_len': 0, 'count': 0}\n",[278,51198,51199],{"class":280,"line":295},[278,51200,292],{"emptyLinePlaceholder":291},[278,51202,51203],{"class":280,"line":316},[278,51204,36239],{},[278,51206,51207],{"class":280,"line":322},[278,51208,49918],{},[278,51210,51211],{"class":280,"line":327},[278,51212,49923],{},[278,51214,51215],{"class":280,"line":340},[278,51216,49928],{},[278,51218,51219],{"class":280,"line":349},[278,51220,49933],{},[278,51222,51223],{"class":280,"line":375},[278,51224,51225],{},"                f'No more jobs: final count: {result[\"count\"]}, time: {timer()}')\n",[278,51227,51228],{"class":280,"line":386},[278,51229,36379],{},[278,51231,51232],{"class":280,"line":397},[278,51233,292],{"emptyLinePlaceholder":291},[278,51235,51236],{"class":280,"line":408},[278,51237,49951],{},[278,51239,51240],{"class":280,"line":433},[278,51241,49956],{},[278,51243,51244],{"class":280,"line":454},[278,51245,49961],{},[278,51247,51248],{"class":280,"line":475},[278,51249,51250],{},"        final_moves_seq = AutoPlay.handle_job_recurse(job, jobs)\n",[278,51252,51253],{"class":280,"line":496},[278,51254,292],{"emptyLinePlaceholder":291},[278,51256,51257],{"class":280,"line":505},[278,51258,49980],{},[278,51260,51261],{"class":280,"line":516},[278,51262,292],{"emptyLinePlaceholder":291},[278,51264,51265],{"class":280,"line":527},[278,51266,49989],{},[278,51268,51269],{"class":280,"line":533},[278,51270,49994],{},[278,51272,51273],{"class":280,"line":539},[278,51274,51275],{},"        if result['min_moves_len'] == 0 or len(final_moves_seq) \u003C result['min_moves_len']:\n",[278,51277,51278],{"class":280,"line":545},[278,51279,50009],{},[278,51281,51282],{"class":280,"line":551},[278,51283,50014],{},[278,51285,51286],{"class":280,"line":557},[278,51287,49933],{},[278,51289,51290],{"class":280,"line":567},[278,51291,51292],{},"                f'changed min_moves to: {result[\"min_moves_len\"]}, {final_moves_seq}, time: {timer()}')\n",[278,51294,51295],{"class":280,"line":577},[278,51296,292],{"emptyLinePlaceholder":291},[278,51298,51299],{"class":280,"line":587},[278,51300,50032],{},[32,51302,4796,51304,10945],{"id":51303},"the-autoplay-module",[59,51305,51306],{},"AutoPlay",[11,51308,51309],{},"Some of the functions are almost the same as in the previous article (small modifications needed for adjusting to non-class approach). You can refer to the first article to get better clarity on these functions.",[269,51311,51313],{"className":35072,"code":51312,"language":35074,"meta":274,"style":274},"import copy\n\nMAX_COLS = 5\nMAX_ROWS = 5\n\ndef get_node(tiles, row, col):\n    if 0 \u003C= col \u003C= MAX_COLS - 1 and 0 \u003C= row \u003C= MAX_ROWS - 1:\n        col_tiles = tiles[col]\n        return col_tiles[row] if row \u003C len(col_tiles) else None\n\ndef connectable(tiles, first_node, row, col):\n    other_node = get_node(tiles, row, col)\n    if other_node and first_node['color'] == other_node['color']:\n        if other_node['clickable']:\n            return True\n\n        return (row, col)\n\ndef process_tile(game, row, col):\n    curr_node = get_node(game['tiles'], row, col)\n    if not curr_node or curr_node['clickable']:\n        return\n\n    has_clickable_connections = {\n        'prev': connectable(game['tiles'], curr_node, row, col - 1),\n        'next': connectable(game['tiles'], curr_node, row, col + 1),\n        'below': connectable(game['tiles'], curr_node, row - 1, col),\n        'above': connectable(game['tiles'], curr_node, row + 1, col)\n    }\n\n    if row == 0 or True in has_clickable_connections.values():\n        curr_node['clickable'] = True\n        if has_clickable_connections['next']:\n            curr_node['connections'].append((row, col + 1))\n        if has_clickable_connections['above']:\n            curr_node['connections'].append((row + 1, col))\n\n        if (row, col) not in game['clickables']:\n            game['clickables'].append((row, col))\n\n        found = False\n        for connections in game['connection_groups']:\n            if (row, col) in connections:\n                found = True\n                break\n\n        if not found:\n            game['connection_groups'].append([(row, col)])\n\n        for value in has_clickable_connections.values():\n            if isinstance(value, tuple):\n                for connections in game['connection_groups']:\n                    if (row, col) in connections and value not in connections:\n                        connections.append(value)\n                        break\n                process_tile(game, *value)\n\ndef handle_tile_click(game, tile_id):\n    # Go through each of the connection groups and find the one containing this tile\n    for connections in game['connection_groups']:\n        if tile_id in connections:\n            # Sort the tiles in reverse order so that we remove them from screen\n            # from the top right\n            tiles_to_remove = sorted(connections, reverse=True)\n\n            # Make all the clickable tiles as unclickable, as connections will be reformed\n            for clickable in game['clickables']:\n                game['tiles'][clickable[1]][clickable[0]]['clickable'] = False\n\n            # Actually remove the tiles one by one from the screen\n            for tile_to_remove in tiles_to_remove:\n                game['tiles'][tile_to_remove[1]].pop(tile_to_remove[0])\n\n                # Change the id of each of the tiles above the removed tile\n                for row in range(tile_to_remove[0], len(game['tiles'][tile_to_remove[1]])):\n                    game['tiles'][tile_to_remove[1]][row]['id'] = (\n                        row, tile_to_remove[1])\n            break\n\n    game['clickables'].clear()\n    game['connection_groups'].clear()\n    # Form fresh connections and find the clickables\n    for col in range(MAX_COLS):\n        process_tile(game, 0, col)\n\ndef handle_job_recurse(job, job_list):\n    game = job['game']\n\n    handle_tile_click(game, job['curr_move'])\n\n    # Add the currently executed move to the past_moves sequence (Only storing the\n    # column id, as row id will always be 0)\n    if job['past_moves'] is None:\n        job['past_moves'] = f'{job[\"curr_move\"][1]}'\n    else:\n        job['past_moves'] = f'{job[\"past_moves\"]}{job[\"curr_move\"][1]}'\n\n    # If after the click no new connection groups are left, then that means we've\n    # cleared the screen, so return this sequence\n    if len(game['connection_groups']) == 0:\n        return job['past_moves']\n\n    # Add the other possible combinations to the job list (we'll be taking the\n    # 0th index till completion, hence slicing the list from the 1st index)\n    for connections in game['connection_groups'][1:]:\n        job_list.append({'game': copy.deepcopy(\n            game), 'curr_move': connections[0], 'past_moves': job['past_moves']})\n\n    # Take the 0th index to its logical end. 0th index gives us a list which\n    # contains all the tiles connected with each other. We take the first tile\n    # from this list (the 0th index), and use recursion to go further\n    job['curr_move'] = game['connection_groups'][0][0]\n\n    return handle_job_recurse(job, job_list)\n",[59,51314,51315,51319,51323,51328,51333,51337,51342,51347,51352,51357,51361,51366,51371,51376,51381,51386,51390,51395,51399,51404,51409,51414,51418,51422,51427,51432,51437,51442,51447,51451,51455,51460,51465,51470,51475,51480,51485,51489,51494,51499,51503,51508,51513,51518,51523,51527,51531,51536,51541,51545,51550,51555,51560,51565,51570,51575,51580,51584,51589,51594,51598,51603,51608,51613,51618,51622,51627,51632,51637,51641,51646,51651,51656,51660,51665,51670,51675,51680,51684,51688,51693,51698,51703,51708,51713,51717,51722,51727,51731,51736,51740,51745,51750,51755,51760,51764,51769,51773,51778,51783,51788,51793,51797,51802,51807,51812,51817,51822,51826,51831,51836,51841,51846,51850],{"__ignoreMap":274},[278,51316,51317],{"class":280,"line":281},[278,51318,49843],{},[278,51320,51321],{"class":280,"line":288},[278,51322,292],{"emptyLinePlaceholder":291},[278,51324,51325],{"class":280,"line":295},[278,51326,51327],{},"MAX_COLS = 5\n",[278,51329,51330],{"class":280,"line":316},[278,51331,51332],{},"MAX_ROWS = 5\n",[278,51334,51335],{"class":280,"line":322},[278,51336,292],{"emptyLinePlaceholder":291},[278,51338,51339],{"class":280,"line":327},[278,51340,51341],{},"def get_node(tiles, row, col):\n",[278,51343,51344],{"class":280,"line":340},[278,51345,51346],{},"    if 0 \u003C= col \u003C= MAX_COLS - 1 and 0 \u003C= row \u003C= MAX_ROWS - 1:\n",[278,51348,51349],{"class":280,"line":349},[278,51350,51351],{},"        col_tiles = tiles[col]\n",[278,51353,51354],{"class":280,"line":375},[278,51355,51356],{},"        return col_tiles[row] if row \u003C len(col_tiles) else None\n",[278,51358,51359],{"class":280,"line":386},[278,51360,292],{"emptyLinePlaceholder":291},[278,51362,51363],{"class":280,"line":397},[278,51364,51365],{},"def connectable(tiles, first_node, row, col):\n",[278,51367,51368],{"class":280,"line":408},[278,51369,51370],{},"    other_node = get_node(tiles, row, col)\n",[278,51372,51373],{"class":280,"line":433},[278,51374,51375],{},"    if other_node and first_node['color'] == other_node['color']:\n",[278,51377,51378],{"class":280,"line":454},[278,51379,51380],{},"        if other_node['clickable']:\n",[278,51382,51383],{"class":280,"line":475},[278,51384,51385],{},"            return True\n",[278,51387,51388],{"class":280,"line":496},[278,51389,292],{"emptyLinePlaceholder":291},[278,51391,51392],{"class":280,"line":505},[278,51393,51394],{},"        return (row, col)\n",[278,51396,51397],{"class":280,"line":516},[278,51398,292],{"emptyLinePlaceholder":291},[278,51400,51401],{"class":280,"line":527},[278,51402,51403],{},"def process_tile(game, row, col):\n",[278,51405,51406],{"class":280,"line":533},[278,51407,51408],{},"    curr_node = get_node(game['tiles'], row, col)\n",[278,51410,51411],{"class":280,"line":539},[278,51412,51413],{},"    if not curr_node or curr_node['clickable']:\n",[278,51415,51416],{"class":280,"line":545},[278,51417,36527],{},[278,51419,51420],{"class":280,"line":551},[278,51421,292],{"emptyLinePlaceholder":291},[278,51423,51424],{"class":280,"line":557},[278,51425,51426],{},"    has_clickable_connections = {\n",[278,51428,51429],{"class":280,"line":567},[278,51430,51431],{},"        'prev': connectable(game['tiles'], curr_node, row, col - 1),\n",[278,51433,51434],{"class":280,"line":577},[278,51435,51436],{},"        'next': connectable(game['tiles'], curr_node, row, col + 1),\n",[278,51438,51439],{"class":280,"line":587},[278,51440,51441],{},"        'below': connectable(game['tiles'], curr_node, row - 1, col),\n",[278,51443,51444],{"class":280,"line":597},[278,51445,51446],{},"        'above': connectable(game['tiles'], curr_node, row + 1, col)\n",[278,51448,51449],{"class":280,"line":608},[278,51450,1285],{},[278,51452,51453],{"class":280,"line":614},[278,51454,292],{"emptyLinePlaceholder":291},[278,51456,51457],{"class":280,"line":620},[278,51458,51459],{},"    if row == 0 or True in has_clickable_connections.values():\n",[278,51461,51462],{"class":280,"line":625},[278,51463,51464],{},"        curr_node['clickable'] = True\n",[278,51466,51467],{"class":280,"line":640},[278,51468,51469],{},"        if has_clickable_connections['next']:\n",[278,51471,51472],{"class":280,"line":663},[278,51473,51474],{},"            curr_node['connections'].append((row, col + 1))\n",[278,51476,51477],{"class":280,"line":669},[278,51478,51479],{},"        if has_clickable_connections['above']:\n",[278,51481,51482],{"class":280,"line":680},[278,51483,51484],{},"            curr_node['connections'].append((row + 1, col))\n",[278,51486,51487],{"class":280,"line":686},[278,51488,292],{"emptyLinePlaceholder":291},[278,51490,51491],{"class":280,"line":1334},[278,51492,51493],{},"        if (row, col) not in game['clickables']:\n",[278,51495,51496],{"class":280,"line":1375},[278,51497,51498],{},"            game['clickables'].append((row, col))\n",[278,51500,51501],{"class":280,"line":1381},[278,51502,292],{"emptyLinePlaceholder":291},[278,51504,51505],{"class":280,"line":1386},[278,51506,51507],{},"        found = False\n",[278,51509,51510],{"class":280,"line":1394},[278,51511,51512],{},"        for connections in game['connection_groups']:\n",[278,51514,51515],{"class":280,"line":1406},[278,51516,51517],{},"            if (row, col) in connections:\n",[278,51519,51520],{"class":280,"line":1423},[278,51521,51522],{},"                found = True\n",[278,51524,51525],{"class":280,"line":1432},[278,51526,35877],{},[278,51528,51529],{"class":280,"line":1437},[278,51530,292],{"emptyLinePlaceholder":291},[278,51532,51533],{"class":280,"line":1916},[278,51534,51535],{},"        if not found:\n",[278,51537,51538],{"class":280,"line":1939},[278,51539,51540],{},"            game['connection_groups'].append([(row, col)])\n",[278,51542,51543],{"class":280,"line":1949},[278,51544,292],{"emptyLinePlaceholder":291},[278,51546,51547],{"class":280,"line":1954},[278,51548,51549],{},"        for value in has_clickable_connections.values():\n",[278,51551,51552],{"class":280,"line":1959},[278,51553,51554],{},"            if isinstance(value, tuple):\n",[278,51556,51557],{"class":280,"line":1985},[278,51558,51559],{},"                for connections in game['connection_groups']:\n",[278,51561,51562],{"class":280,"line":1990},[278,51563,51564],{},"                    if (row, col) in connections and value not in connections:\n",[278,51566,51567],{"class":280,"line":1997},[278,51568,51569],{},"                        connections.append(value)\n",[278,51571,51572],{"class":280,"line":2006},[278,51573,51574],{},"                        break\n",[278,51576,51577],{"class":280,"line":2018},[278,51578,51579],{},"                process_tile(game, *value)\n",[278,51581,51582],{"class":280,"line":2029},[278,51583,292],{"emptyLinePlaceholder":291},[278,51585,51586],{"class":280,"line":2034},[278,51587,51588],{},"def handle_tile_click(game, tile_id):\n",[278,51590,51591],{"class":280,"line":2040},[278,51592,51593],{},"    # Go through each of the connection groups and find the one containing this tile\n",[278,51595,51596],{"class":280,"line":2045},[278,51597,50190],{},[278,51599,51600],{"class":280,"line":2068},[278,51601,51602],{},"        if tile_id in connections:\n",[278,51604,51605],{"class":280,"line":2099},[278,51606,51607],{},"            # Sort the tiles in reverse order so that we remove them from screen\n",[278,51609,51610],{"class":280,"line":6428},[278,51611,51612],{},"            # from the top right\n",[278,51614,51615],{"class":280,"line":6439},[278,51616,51617],{},"            tiles_to_remove = sorted(connections, reverse=True)\n",[278,51619,51620],{"class":280,"line":6450},[278,51621,292],{"emptyLinePlaceholder":291},[278,51623,51624],{"class":280,"line":6455},[278,51625,51626],{},"            # Make all the clickable tiles as unclickable, as connections will be reformed\n",[278,51628,51629],{"class":280,"line":6460},[278,51630,51631],{},"            for clickable in game['clickables']:\n",[278,51633,51634],{"class":280,"line":6475},[278,51635,51636],{},"                game['tiles'][clickable[1]][clickable[0]]['clickable'] = False\n",[278,51638,51639],{"class":280,"line":6486},[278,51640,292],{"emptyLinePlaceholder":291},[278,51642,51643],{"class":280,"line":6491},[278,51644,51645],{},"            # Actually remove the tiles one by one from the screen\n",[278,51647,51648],{"class":280,"line":6518},[278,51649,51650],{},"            for tile_to_remove in tiles_to_remove:\n",[278,51652,51653],{"class":280,"line":6530},[278,51654,51655],{},"                game['tiles'][tile_to_remove[1]].pop(tile_to_remove[0])\n",[278,51657,51658],{"class":280,"line":6542},[278,51659,292],{"emptyLinePlaceholder":291},[278,51661,51662],{"class":280,"line":6547},[278,51663,51664],{},"                # Change the id of each of the tiles above the removed tile\n",[278,51666,51667],{"class":280,"line":6552},[278,51668,51669],{},"                for row in range(tile_to_remove[0], len(game['tiles'][tile_to_remove[1]])):\n",[278,51671,51672],{"class":280,"line":6567},[278,51673,51674],{},"                    game['tiles'][tile_to_remove[1]][row]['id'] = (\n",[278,51676,51677],{"class":280,"line":6580},[278,51678,51679],{},"                        row, tile_to_remove[1])\n",[278,51681,51682],{"class":280,"line":6593},[278,51683,36379],{},[278,51685,51686],{"class":280,"line":6605},[278,51687,292],{"emptyLinePlaceholder":291},[278,51689,51690],{"class":280,"line":6620},[278,51691,51692],{},"    game['clickables'].clear()\n",[278,51694,51695],{"class":280,"line":6625},[278,51696,51697],{},"    game['connection_groups'].clear()\n",[278,51699,51700],{"class":280,"line":6633},[278,51701,51702],{},"    # Form fresh connections and find the clickables\n",[278,51704,51705],{"class":280,"line":6643},[278,51706,51707],{},"    for col in range(MAX_COLS):\n",[278,51709,51710],{"class":280,"line":6657},[278,51711,51712],{},"        process_tile(game, 0, col)\n",[278,51714,51715],{"class":280,"line":6665},[278,51716,292],{"emptyLinePlaceholder":291},[278,51718,51719],{"class":280,"line":6670},[278,51720,51721],{},"def handle_job_recurse(job, job_list):\n",[278,51723,51724],{"class":280,"line":6675},[278,51725,51726],{},"    game = job['game']\n",[278,51728,51729],{"class":280,"line":6680},[278,51730,292],{"emptyLinePlaceholder":291},[278,51732,51733],{"class":280,"line":6698},[278,51734,51735],{},"    handle_tile_click(game, job['curr_move'])\n",[278,51737,51738],{"class":280,"line":6725},[278,51739,292],{"emptyLinePlaceholder":291},[278,51741,51742],{"class":280,"line":6738},[278,51743,51744],{},"    # Add the currently executed move to the past_moves sequence (Only storing the\n",[278,51746,51747],{"class":280,"line":6752},[278,51748,51749],{},"    # column id, as row id will always be 0)\n",[278,51751,51752],{"class":280,"line":6769},[278,51753,51754],{},"    if job['past_moves'] is None:\n",[278,51756,51757],{"class":280,"line":6786},[278,51758,51759],{},"        job['past_moves'] = f'{job[\"curr_move\"][1]}'\n",[278,51761,51762],{"class":280,"line":6798},[278,51763,36178],{},[278,51765,51766],{"class":280,"line":6803},[278,51767,51768],{},"        job['past_moves'] = f'{job[\"past_moves\"]}{job[\"curr_move\"][1]}'\n",[278,51770,51771],{"class":280,"line":6815},[278,51772,292],{"emptyLinePlaceholder":291},[278,51774,51775],{"class":280,"line":6827},[278,51776,51777],{},"    # If after the click no new connection groups are left, then that means we've\n",[278,51779,51780],{"class":280,"line":6839},[278,51781,51782],{},"    # cleared the screen, so return this sequence\n",[278,51784,51785],{"class":280,"line":6844},[278,51786,51787],{},"    if len(game['connection_groups']) == 0:\n",[278,51789,51790],{"class":280,"line":6853},[278,51791,51792],{},"        return job['past_moves']\n",[278,51794,51795],{"class":280,"line":6859},[278,51796,292],{"emptyLinePlaceholder":291},[278,51798,51799],{"class":280,"line":6864},[278,51800,51801],{},"    # Add the other possible combinations to the job list (we'll be taking the\n",[278,51803,51804],{"class":280,"line":6877},[278,51805,51806],{},"    # 0th index till completion, hence slicing the list from the 1st index)\n",[278,51808,51809],{"class":280,"line":6887},[278,51810,51811],{},"    for connections in game['connection_groups'][1:]:\n",[278,51813,51814],{"class":280,"line":6918},[278,51815,51816],{},"        job_list.append({'game': copy.deepcopy(\n",[278,51818,51819],{"class":280,"line":6923},[278,51820,51821],{},"            game), 'curr_move': connections[0], 'past_moves': job['past_moves']})\n",[278,51823,51824],{"class":280,"line":6931},[278,51825,292],{"emptyLinePlaceholder":291},[278,51827,51828],{"class":280,"line":6939},[278,51829,51830],{},"    # Take the 0th index to its logical end. 0th index gives us a list which\n",[278,51832,51833],{"class":280,"line":6951},[278,51834,51835],{},"    # contains all the tiles connected with each other. We take the first tile\n",[278,51837,51838],{"class":280,"line":6957},[278,51839,51840],{},"    # from this list (the 0th index), and use recursion to go further\n",[278,51842,51843],{"class":280,"line":6962},[278,51844,51845],{},"    job['curr_move'] = game['connection_groups'][0][0]\n",[278,51847,51848],{"class":280,"line":6973},[278,51849,292],{"emptyLinePlaceholder":291},[278,51851,51852],{"class":280,"line":6985},[278,51853,51854],{},"    return handle_job_recurse(job, job_list)\n",[32,51856,50284],{"id":50283},[11,51858,51859],{},"Running the code by using the board from today's puzzle (shown earlier) we get the following results",[269,51861,51863],{"className":35072,"code":51862,"language":35074,"meta":274,"style":274},"play([\n    ['hot pink', 'turquoise', 'hot pink', 'hot pink', 'yellow'],\n    ['white', 'turquoise', 'yellow', 'white', 'hot pink'],\n    ['yellow', 'white', 'hot pink', 'hot pink', 'turquoise'],\n    ['white', 'hot pink', 'turquoise', 'white', 'turquoise'],\n    ['white', 'hot pink', 'white', 'white', 'turquoise']\n])\n",[59,51864,51865,51869,51874,51879,51884,51889,51894],{"__ignoreMap":274},[278,51866,51867],{"class":280,"line":281},[278,51868,49758],{},[278,51870,51871],{"class":280,"line":288},[278,51872,51873],{},"    ['hot pink', 'turquoise', 'hot pink', 'hot pink', 'yellow'],\n",[278,51875,51876],{"class":280,"line":295},[278,51877,51878],{},"    ['white', 'turquoise', 'yellow', 'white', 'hot pink'],\n",[278,51880,51881],{"class":280,"line":316},[278,51882,51883],{},"    ['yellow', 'white', 'hot pink', 'hot pink', 'turquoise'],\n",[278,51885,51886],{"class":280,"line":322},[278,51887,51888],{},"    ['white', 'hot pink', 'turquoise', 'white', 'turquoise'],\n",[278,51890,51891],{"class":280,"line":327},[278,51892,51893],{},"    ['white', 'hot pink', 'white', 'white', 'turquoise']\n",[278,51895,51896],{"class":280,"line":340},[278,51897,49788],{},[269,51899,51902],{"className":51900,"code":51901,"language":4582},[49795],"start time: 0.060289866\nchanged min_moves to: 13, 3000011111233, time: 0.068856064\nchanged min_moves to: 12, 300001111142, time: 0.069172283\nchanged min_moves to: 11, 30003114012, time: 2.164211921\nchanged min_moves to: 10, 3003114002, time: 9.825066422\nchanged min_moves to: 9, 303104002, time: 26.712916026\nchanged min_moves to: 8, 10010012, time: 110.70025369\nNo more jobs: final count: 1389898, time: 214.573611255\nresult of operation: {'min_moves': '10010012', 'min_moves_len': 8, 'count': 1389898}\n",[59,51903,51901],{"__ignoreMap":274},[11,51905,51906,51907,51910],{},"And voila, we got the minimum moves length as 8, same as the original puzzle wanted. It took us around 3 minutes and 35 seconds to get the solution. ",[59,51908,51909],{},"'min_moves': '10010012'"," gives us the column indices of the bottom row which we need to click one after the other to clear the screen. In total, we processed 1389898 sequences, that is close to 1.39 million sequences (Wow!).",[40,51912],{"url":51913},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FUtEUhkfriklonVdweC\u002Fgiphy.gif",[11,51915,51916],{},"Or is it?",[24,51918,51920],{"id":51919},"first-optimization","First optimization",[11,51922,51923],{},"Even though this is working, for a different puzzle which requires, say 13 or 14 moves, our code will take much longer to give us a solution. Can we optimize our solution somehow?",[11,51925,51926,51927,51930,51931,51934,51935,51938,51939,183],{},"If you think about it, we don't need to take all the sequences to their logical end. If any sequence has already had ",[59,51928,51929],{},"\"min_moves_len - 1\""," past moves, and there still are ",[59,51932,51933],{},"connection_groups"," left, then there is no point in pursuing this sequence. That is because, in the best case it will give us the same ",[59,51936,51937],{},"min_moves_len"," (if we assume that the next click will clear the screen), but in all the other cases we'll get a final sequence length which is more than the ",[59,51940,51937],{},[11,51942,51943],{},"Keeping the above observation in mind, we will not be adding such combinations to the job list. Let's make the needed changes:",[32,51945,49814,51947,748],{"id":51946},"modified-find_min_moves-function",[59,51948,49820],{},[269,51950,51952],{"className":35072,"code":51951,"language":35074,"meta":274,"style":274},"def find_min_moves(jobs):\n    result = {'min_moves': None, 'min_moves_len': 0, 'count': 0}\n\n    while True:\n        # See if we've any jobs left. If there is no job, break the loop\n        job = jobs.pop() if len(jobs) > 0 else None\n        if job is None:\n            print(\n                f'No more jobs: final count: {result[\"count\"]}, time: {timer()}')\n            break\n\n        # Handle the current job. This will take of the combinations till its logical\n        # end (until the board is clear). Other encountered combinations will be added\n        # to the job list for processing in due course\n        final_moves_seq = AutoPlay.handle_job_recurse(\n            job, jobs, result['min_moves_len']) # Sending the current min_moves_len\n\n        result['count'] += 1\n\n        # If the one processed combination has minimum length, then that is the minimum\n        # numbers of moves needed to solve the puzzle\n        # Now, final_moves_seq can be returned as None, so taking that into account\n        if result['min_moves_len'] == 0 or (final_moves_seq is not None\n                                            and len(final_moves_seq) \u003C result['min_moves_len']):\n            result['min_moves'] = final_moves_seq\n            result['min_moves_len'] = len(final_moves_seq)\n            print(\n                f'changed min_moves to: {result[\"min_moves_len\"]}, {final_moves_seq}, time: {timer()}')\n\n    return result\n",[59,51953,51954,51958,51962,51966,51970,51974,51978,51982,51986,51990,51994,51998,52002,52006,52010,52014,52019,52023,52027,52031,52035,52039,52044,52048,52052,52056,52060,52064,52068,52072],{"__ignoreMap":274},[278,51955,51956],{"class":280,"line":281},[278,51957,51191],{},[278,51959,51960],{"class":280,"line":288},[278,51961,51196],{},[278,51963,51964],{"class":280,"line":295},[278,51965,292],{"emptyLinePlaceholder":291},[278,51967,51968],{"class":280,"line":316},[278,51969,36239],{},[278,51971,51972],{"class":280,"line":322},[278,51973,49918],{},[278,51975,51976],{"class":280,"line":327},[278,51977,49923],{},[278,51979,51980],{"class":280,"line":340},[278,51981,49928],{},[278,51983,51984],{"class":280,"line":349},[278,51985,49933],{},[278,51987,51988],{"class":280,"line":375},[278,51989,51225],{},[278,51991,51992],{"class":280,"line":386},[278,51993,36379],{},[278,51995,51996],{"class":280,"line":397},[278,51997,292],{"emptyLinePlaceholder":291},[278,51999,52000],{"class":280,"line":408},[278,52001,49951],{},[278,52003,52004],{"class":280,"line":433},[278,52005,49956],{},[278,52007,52008],{"class":280,"line":454},[278,52009,49961],{},[278,52011,52012],{"class":280,"line":475},[278,52013,49966],{},[278,52015,52016],{"class":280,"line":496},[278,52017,52018],{},"            job, jobs, result['min_moves_len']) # Sending the current min_moves_len\n",[278,52020,52021],{"class":280,"line":505},[278,52022,292],{"emptyLinePlaceholder":291},[278,52024,52025],{"class":280,"line":516},[278,52026,49980],{},[278,52028,52029],{"class":280,"line":527},[278,52030,292],{"emptyLinePlaceholder":291},[278,52032,52033],{"class":280,"line":533},[278,52034,49989],{},[278,52036,52037],{"class":280,"line":539},[278,52038,49994],{},[278,52040,52041],{"class":280,"line":545},[278,52042,52043],{},"        # Now, final_moves_seq can be returned as None, so taking that into account\n",[278,52045,52046],{"class":280,"line":551},[278,52047,49999],{},[278,52049,52050],{"class":280,"line":557},[278,52051,50004],{},[278,52053,52054],{"class":280,"line":567},[278,52055,50009],{},[278,52057,52058],{"class":280,"line":577},[278,52059,50014],{},[278,52061,52062],{"class":280,"line":587},[278,52063,49933],{},[278,52065,52066],{"class":280,"line":597},[278,52067,51292],{},[278,52069,52070],{"class":280,"line":608},[278,52071,292],{"emptyLinePlaceholder":291},[278,52073,52074],{"class":280,"line":614},[278,52075,50032],{},[32,52077,49814,52079,748],{"id":52078},"modified-handle_job_recurse-function",[59,52080,52081],{},"handle_job_recurse",[269,52083,52085],{"className":35072,"code":52084,"language":35074,"meta":274,"style":274},"def handle_job_recurse(job, job_list, curr_min_moves_len):\n    game = job['game']\n\n    handle_tile_click(game, job['curr_move'])\n\n    # Add the currently executed move to the past_moves sequence (Only storing the\n    # column id, as row id will always be 0)\n    if job['past_moves'] is None:\n        job['past_moves'] = f'{job[\"curr_move\"][1]}'\n    else:\n        job['past_moves'] = f'{job[\"past_moves\"]}{job[\"curr_move\"][1]}'\n\n    # If after the click no new connection groups are left, then that means we've\n    # cleared the screen, so return this sequence\n    if len(game['connection_groups']) == 0:\n        return job['past_moves']\n\n    # If there is some min_moves_len and past_moves sequence length is more than \n    # or equal to min_moves_len - 1, then discard all further combinations in this sequence\n    if curr_min_moves_len != 0 and len(job['past_moves']) >= (curr_min_moves_len - 1):\n        return None\n\n    # Add the other possible combinations to the job list (we'll be taking the\n    # 0th index till completion, hence slicing the list from the 1st index)\n    for connections in game['connection_groups'][1:]:\n        job_list.append({'game': copy.deepcopy(\n            game), 'curr_move': connections[0], 'past_moves': job['past_moves']})\n\n    # Take the 0th index to its logical end. 0th index gives us a list which\n    # contains all the tiles connected with each other. We take the first tile\n    # from this list (the 0th index), and use recursion to go further\n    job['curr_move'] = game['connection_groups'][0][0]\n\n    return handle_job_recurse(job, job_list, curr_min_moves_len)\n",[59,52086,52087,52092,52096,52100,52104,52108,52112,52116,52120,52124,52128,52132,52136,52140,52144,52148,52152,52156,52161,52166,52171,52176,52180,52184,52188,52192,52196,52200,52204,52208,52212,52216,52220,52224],{"__ignoreMap":274},[278,52088,52089],{"class":280,"line":281},[278,52090,52091],{},"def handle_job_recurse(job, job_list, curr_min_moves_len):\n",[278,52093,52094],{"class":280,"line":288},[278,52095,51726],{},[278,52097,52098],{"class":280,"line":295},[278,52099,292],{"emptyLinePlaceholder":291},[278,52101,52102],{"class":280,"line":316},[278,52103,51735],{},[278,52105,52106],{"class":280,"line":322},[278,52107,292],{"emptyLinePlaceholder":291},[278,52109,52110],{"class":280,"line":327},[278,52111,51744],{},[278,52113,52114],{"class":280,"line":340},[278,52115,51749],{},[278,52117,52118],{"class":280,"line":349},[278,52119,51754],{},[278,52121,52122],{"class":280,"line":375},[278,52123,51759],{},[278,52125,52126],{"class":280,"line":386},[278,52127,36178],{},[278,52129,52130],{"class":280,"line":397},[278,52131,51768],{},[278,52133,52134],{"class":280,"line":408},[278,52135,292],{"emptyLinePlaceholder":291},[278,52137,52138],{"class":280,"line":433},[278,52139,51777],{},[278,52141,52142],{"class":280,"line":454},[278,52143,51782],{},[278,52145,52146],{"class":280,"line":475},[278,52147,51787],{},[278,52149,52150],{"class":280,"line":496},[278,52151,51792],{},[278,52153,52154],{"class":280,"line":505},[278,52155,292],{"emptyLinePlaceholder":291},[278,52157,52158],{"class":280,"line":516},[278,52159,52160],{},"    # If there is some min_moves_len and past_moves sequence length is more than \n",[278,52162,52163],{"class":280,"line":527},[278,52164,52165],{},"    # or equal to min_moves_len - 1, then discard all further combinations in this sequence\n",[278,52167,52168],{"class":280,"line":533},[278,52169,52170],{},"    if curr_min_moves_len != 0 and len(job['past_moves']) >= (curr_min_moves_len - 1):\n",[278,52172,52173],{"class":280,"line":539},[278,52174,52175],{},"        return None\n",[278,52177,52178],{"class":280,"line":545},[278,52179,292],{"emptyLinePlaceholder":291},[278,52181,52182],{"class":280,"line":551},[278,52183,51801],{},[278,52185,52186],{"class":280,"line":557},[278,52187,51806],{},[278,52189,52190],{"class":280,"line":567},[278,52191,51811],{},[278,52193,52194],{"class":280,"line":577},[278,52195,51816],{},[278,52197,52198],{"class":280,"line":587},[278,52199,51821],{},[278,52201,52202],{"class":280,"line":597},[278,52203,292],{"emptyLinePlaceholder":291},[278,52205,52206],{"class":280,"line":608},[278,52207,51830],{},[278,52209,52210],{"class":280,"line":614},[278,52211,51835],{},[278,52213,52214],{"class":280,"line":620},[278,52215,51840],{},[278,52217,52218],{"class":280,"line":625},[278,52219,51845],{},[278,52221,52222],{"class":280,"line":640},[278,52223,292],{"emptyLinePlaceholder":291},[278,52225,52226],{"class":280,"line":663},[278,52227,52228],{},"    return handle_job_recurse(job, job_list, curr_min_moves_len)\n",[32,52230,50284],{"id":50807},[11,52232,52233],{},"Running the same starting board as in the previous run, we get the following results",[269,52235,52238],{"className":52236,"code":52237,"language":4582},[49795],"start time: 0.116954505\nchanged min_moves to: 13, 3000011111233, time: 0.129312172\nchanged min_moves to: 12, 300001111142, time: 0.130486732\nchanged min_moves to: 11, 30003114012, time: 0.642993931\nchanged min_moves to: 10, 3003114002, time: 1.395034809\nchanged min_moves to: 9, 303104002, time: 2.094061232\nchanged min_moves to: 8, 10010012, time: 3.78454475\nNo more jobs: final count: 16724, time: 4.46511333\nresult of operation: {'min_moves': '10010012', 'min_moves_len': 8, 'count': 16724}\n",[59,52239,52237],{"__ignoreMap":274},[11,52241,52242],{},"It took us only 4.46 seconds to complete the job, and there were only 16.7K sequences which were taken to their logical end. Now we're talking :-)",[40,52244],{"url":52245},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FKnKSXq9qxgZDa\u002Fgiphy.gif",[24,52247,10634],{"id":10633},[11,52249,52250],{},"In this post we looked at one crude way of solving the puzzle which we'd created in the last post. The solution is by no means complete, or fully optimized. We can do many optimizations to find the solution sooner. We will look at some of those optimizations in the next and the final post of this series.",[11,52252,52253],{},"Hope you enjoyed reading the post. Do let me know how would you solve the problem :-).",[3065,52255,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":52257},[52258,52259,52260,52269,52276],{"id":22771,"depth":288,"text":22772},{"id":50934,"depth":288,"text":50935},{"id":23957,"depth":288,"text":23958,"children":52261},[52262,52264,52266,52268],{"id":50990,"depth":295,"text":52263},"The play function",{"id":51179,"depth":295,"text":52265},"The find_min_moves function",{"id":51303,"depth":295,"text":52267},"The AutoPlay module",{"id":50283,"depth":295,"text":50284},{"id":51919,"depth":288,"text":51920,"children":52270},[52271,52273,52275],{"id":51946,"depth":295,"text":52272},"Modified find_min_moves function",{"id":52078,"depth":295,"text":52274},"Modified handle_job_recurse function",{"id":50807,"depth":295,"text":50284},{"id":10633,"depth":288,"text":10634},"\u002Fimages\u002Fposts\u002Fsolve-puzzle-game-using-python\u002FMtqYZMQ4g-ccc02810d2.png","2022-11-17T12:18:20.988Z","Learn how to solve a tiles puzzle game using python. A step by step guide listing the observations made for creating the algorithm with code.","clal1gc70000508l51ya1ahj8",{},"\u002Fsolve-puzzle-game-using-python",{"title":50913,"description":52279},"solve-puzzle-game-using-python",[35074,52286,49708,52287],"python3","python-projects","pVQktb1KPASZxu8_wy_4fAFvW2yN5tRsZb9LkfXiTuE",{"id":52290,"title":52291,"body":52292,"cover":54690,"date":54691,"description":54692,"draft":3086,"extension":3087,"hashnodeId":54693,"meta":54694,"navigation":291,"path":54695,"seo":54696,"slug":54697,"stem":54697,"tags":54698,"__hash__":54700},"posts\u002Fcreating-puzzle-game-using-python-turtle-1.md","Creating a puzzle game using Python Turtle module",{"type":8,"value":52293,"toc":54676},[52294,52301,52305,52317,52320,52323,52327,52330,52333,52336,52339,52342,52345,52348,52352,52355,52476,52487,52491,52500,52520,52527,52610,52619,52676,52687,52690,52696,52717,53014,53020,53023,53034,53134,53147,53170,53176,53189,53304,53309,53312,53315,53318,53408,53414,53417,53432,53439,53449,53645,53652,53672,53675,53678,53712,53719,53756,53761,53764,53770,53930,53933,53941,53944,54210,54216,54219,54325,54331,54334,54509,54514,54662,54665,54668,54671,54674],[11,52295,52296,52297],{},"To read the second installment of this series, please visit ",[47,52298,52300],{"href":49729,"rel":52299},[51],"this link",[24,52302,52304],{"id":52303},"the-inspiration","The inspiration",[11,52306,52307,52308,52313,52314],{},"Couple of months back came across a ",[47,52309,52312],{"href":52310,"rel":52311},"https:\u002F\u002Ftwitter.com\u002Fsumul\u002Fstatus\u002F1545430273113866240?s=20&t=Z60P7bP3Y40QxmZf9PfhVA",[51],"twitter post"," announcing a daily puzzle game. Maybe it was the FOMO created by the Wordle bandwagon (never played that), but I really liked this puzzle game. I've been a regular player of it since then, breaking the streak only 3-4 times. You can try out the original game ",[47,52315,3286],{"href":50975,"rel":52316},[51],[40,52318],{"url":52319},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002Fu10GReM6igVGg\u002Fgiphy.gif",[11,52321,52322],{},"Over the period I've been getting the itch to implement the game logic myself. I've also been dabbling in basic Python in recent times, so I thought why not implement it using Python Turtle module itself? And hence the post :-)",[24,52324,52326],{"id":52325},"the-game","The game",[11,52328,52329],{},"The game idea is very simple. There are some tiles (5 x 5 grid) on the screen. All the tiles present in the bottom row are clickable (represented as solid color tiles), and any tile of same color connected to these bottom row tiles are also clickable (see the cover image for an example). When we click on these clickable tiles, the clicked tile as well as the connected tiles get removed from the screen. New connections are formed after every click, following the same earlier logic.",[11,52331,52332],{},"The game ends when all the tiles are removed from the screen in a given number of moves (the minimum moves needed to remove all the tiles).",[24,52334,52335],{"id":45825},"The implementation",[11,52337,52338],{},"To keep it simple, we're not going to worry about the minimum moves part in this article, and will only implement the new game creation and its game play part. In the next article we will look at a crude implementation of how we can find out the minimum number of moves needed for a given game board.",[40,52340],{"url":52341},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FIHnROpQICe4kE\u002Fgiphy.gif",[11,52343,52344],{},"You can check out the screen grab of the actual game play of this below",[40,52346],{"url":52347},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=cRfjimU5KyQ",[32,52349,52351],{"id":52350},"setting-up-the-screen","Setting up the screen",[11,52353,52354],{},"This part is pretty easy. We import the Turtle module and create the screen, and a turtle (named pen) for drawing everything on the screen.",[269,52356,52358],{"className":35072,"code":52357,"language":35074,"meta":274,"style":274},"import turtle\nfrom random import choice\n\nimport settings\nfrom Game import Game\n\ndef init_game_screen():\n    '''Init the turtle screen and create a pen for drawing on \n      that screen. Also, hiding the pen as we don't really need \n      to see it.\n    '''\n    screen = turtle.Screen()\n    screen.screensize(settings.canv_width(),\n                      settings.canv_height(), 'midnight blue')\n    screen.setup(settings.win_width(), settings.win_height())\n    screen.title('Figure')\n    screen.tracer(0)\n\n    pen = turtle.Turtle()\n    pen.pensize(settings.OUTER_OUTLINE)\n    pen.penup()\n    pen.hideturtle()\n\n    return screen, pen\n",[59,52359,52360,52365,52370,52374,52379,52384,52388,52393,52398,52403,52408,52413,52418,52423,52428,52433,52438,52443,52447,52452,52457,52462,52467,52471],{"__ignoreMap":274},[278,52361,52362],{"class":280,"line":281},[278,52363,52364],{},"import turtle\n",[278,52366,52367],{"class":280,"line":288},[278,52368,52369],{},"from random import choice\n",[278,52371,52372],{"class":280,"line":295},[278,52373,292],{"emptyLinePlaceholder":291},[278,52375,52376],{"class":280,"line":316},[278,52377,52378],{},"import settings\n",[278,52380,52381],{"class":280,"line":322},[278,52382,52383],{},"from Game import Game\n",[278,52385,52386],{"class":280,"line":327},[278,52387,292],{"emptyLinePlaceholder":291},[278,52389,52390],{"class":280,"line":340},[278,52391,52392],{},"def init_game_screen():\n",[278,52394,52395],{"class":280,"line":349},[278,52396,52397],{},"    '''Init the turtle screen and create a pen for drawing on \n",[278,52399,52400],{"class":280,"line":375},[278,52401,52402],{},"      that screen. Also, hiding the pen as we don't really need \n",[278,52404,52405],{"class":280,"line":386},[278,52406,52407],{},"      to see it.\n",[278,52409,52410],{"class":280,"line":397},[278,52411,52412],{},"    '''\n",[278,52414,52415],{"class":280,"line":408},[278,52416,52417],{},"    screen = turtle.Screen()\n",[278,52419,52420],{"class":280,"line":433},[278,52421,52422],{},"    screen.screensize(settings.canv_width(),\n",[278,52424,52425],{"class":280,"line":454},[278,52426,52427],{},"                      settings.canv_height(), 'midnight blue')\n",[278,52429,52430],{"class":280,"line":475},[278,52431,52432],{},"    screen.setup(settings.win_width(), settings.win_height())\n",[278,52434,52435],{"class":280,"line":496},[278,52436,52437],{},"    screen.title('Figure')\n",[278,52439,52440],{"class":280,"line":505},[278,52441,52442],{},"    screen.tracer(0)\n",[278,52444,52445],{"class":280,"line":516},[278,52446,292],{"emptyLinePlaceholder":291},[278,52448,52449],{"class":280,"line":527},[278,52450,52451],{},"    pen = turtle.Turtle()\n",[278,52453,52454],{"class":280,"line":533},[278,52455,52456],{},"    pen.pensize(settings.OUTER_OUTLINE)\n",[278,52458,52459],{"class":280,"line":539},[278,52460,52461],{},"    pen.penup()\n",[278,52463,52464],{"class":280,"line":545},[278,52465,52466],{},"    pen.hideturtle()\n",[278,52468,52469],{"class":280,"line":551},[278,52470,292],{"emptyLinePlaceholder":291},[278,52472,52473],{"class":280,"line":557},[278,52474,52475],{},"    return screen, pen\n",[11,52477,52478,52479,52482,52483,52486],{},"Here ",[59,52480,52481],{},"settings"," is a simple module where the game constants, and some utility methods are present. Note that we've put the screen tracer value to 0 (",[59,52484,52485],{},"screen.tracer(0)",") as we don't want the inbuilt turtle screen refresh delay to make our game sluggish.",[32,52488,52490],{"id":52489},"generating-the-board","Generating the board",[11,52492,21841,52493,52495,52496,52499],{},[59,52494,46335],{}," module to get a random color (from ",[59,52497,52498],{},"COLORS = ['hot pink', 'white', 'yellow', 'turquoise']",") for each tile. List comprehension makes the code shorter, or we can use nested for loops to get the same result.",[269,52501,52503],{"className":35072,"code":52502,"language":35074,"meta":274,"style":274},"def init_game_colors():\n    return [[choice(settings.COLORS) for _ in range(settings.MAX_ROWS)]\n            for _ in range(settings.MAX_COLS)]\n",[59,52504,52505,52510,52515],{"__ignoreMap":274},[278,52506,52507],{"class":280,"line":281},[278,52508,52509],{},"def init_game_colors():\n",[278,52511,52512],{"class":280,"line":288},[278,52513,52514],{},"    return [[choice(settings.COLORS) for _ in range(settings.MAX_ROWS)]\n",[278,52516,52517],{"class":280,"line":295},[278,52518,52519],{},"            for _ in range(settings.MAX_COLS)]\n",[11,52521,52522,52523,52526],{},"Note that we are storing the tiles colors as rows of columns (",[59,52524,52525],{},"colors[col][row]"," will give us the color of a particular tile). This will helps us in breaking out early while iterating over the tiles during the game play (if there is no tile in a column at say row index 1, then there won't be any tile above it at indices 2, 3 etc. also).",[269,52528,52530],{"className":35072,"code":52529,"language":35074,"meta":274,"style":274},"def play():\n    screen, pen = init_game_screen()\n    game = {\n        'obj': None,\n        'colors': []\n    }\n\n    start_game(screen, pen, game)\n\n    screen.onkeypress(lambda: start_game(screen, pen, game, True), 'space')\n    screen.onkeypress(lambda: start_game(screen, pen, game), 'n')\n    screen.listen()\n\n    screen.mainloop()\n\nif __name__ == '__main__':\n    play()\n",[59,52531,52532,52537,52542,52546,52551,52556,52560,52564,52569,52573,52578,52583,52588,52592,52597,52601,52605],{"__ignoreMap":274},[278,52533,52534],{"class":280,"line":281},[278,52535,52536],{},"def play():\n",[278,52538,52539],{"class":280,"line":288},[278,52540,52541],{},"    screen, pen = init_game_screen()\n",[278,52543,52544],{"class":280,"line":295},[278,52545,50056],{},[278,52547,52548],{"class":280,"line":316},[278,52549,52550],{},"        'obj': None,\n",[278,52552,52553],{"class":280,"line":322},[278,52554,52555],{},"        'colors': []\n",[278,52557,52558],{"class":280,"line":327},[278,52559,1285],{},[278,52561,52562],{"class":280,"line":340},[278,52563,292],{"emptyLinePlaceholder":291},[278,52565,52566],{"class":280,"line":349},[278,52567,52568],{},"    start_game(screen, pen, game)\n",[278,52570,52571],{"class":280,"line":375},[278,52572,292],{"emptyLinePlaceholder":291},[278,52574,52575],{"class":280,"line":386},[278,52576,52577],{},"    screen.onkeypress(lambda: start_game(screen, pen, game, True), 'space')\n",[278,52579,52580],{"class":280,"line":397},[278,52581,52582],{},"    screen.onkeypress(lambda: start_game(screen, pen, game), 'n')\n",[278,52584,52585],{"class":280,"line":408},[278,52586,52587],{},"    screen.listen()\n",[278,52589,52590],{"class":280,"line":433},[278,52591,292],{"emptyLinePlaceholder":291},[278,52593,52594],{"class":280,"line":454},[278,52595,52596],{},"    screen.mainloop()\n",[278,52598,52599],{"class":280,"line":475},[278,52600,292],{"emptyLinePlaceholder":291},[278,52602,52603],{"class":280,"line":496},[278,52604,50313],{},[278,52606,52607],{"class":280,"line":505},[278,52608,52609],{},"    play()\n",[11,52611,52612,52613,35030,52615,52618],{},"We've also added the options to start a new game, or to replay the current game. We've used the ",[59,52614,24574],{},[59,52616,52617],{},"space"," keys for the corresponding actions. The same screen and pen is reused over multiple game plays.",[269,52620,52622],{"className":35072,"code":52621,"language":35074,"meta":274,"style":274},"def start_game(screen, pen, game, replay=False):\n    if not replay or len(game['colors']) == 0:\n        game['colors'] = init_game_colors()\n\n    if game['obj'] is None:\n        game['obj'] = Game(game['colors'], screen, pen)\n    else:\n        game['obj'].reset(game['colors'])\n\n    pen.clear()\n    game['obj'].start()\n",[59,52623,52624,52629,52634,52639,52643,52648,52653,52657,52662,52666,52671],{"__ignoreMap":274},[278,52625,52626],{"class":280,"line":281},[278,52627,52628],{},"def start_game(screen, pen, game, replay=False):\n",[278,52630,52631],{"class":280,"line":288},[278,52632,52633],{},"    if not replay or len(game['colors']) == 0:\n",[278,52635,52636],{"class":280,"line":295},[278,52637,52638],{},"        game['colors'] = init_game_colors()\n",[278,52640,52641],{"class":280,"line":316},[278,52642,292],{"emptyLinePlaceholder":291},[278,52644,52645],{"class":280,"line":322},[278,52646,52647],{},"    if game['obj'] is None:\n",[278,52649,52650],{"class":280,"line":327},[278,52651,52652],{},"        game['obj'] = Game(game['colors'], screen, pen)\n",[278,52654,52655],{"class":280,"line":340},[278,52656,36178],{},[278,52658,52659],{"class":280,"line":349},[278,52660,52661],{},"        game['obj'].reset(game['colors'])\n",[278,52663,52664],{"class":280,"line":375},[278,52665,292],{"emptyLinePlaceholder":291},[278,52667,52668],{"class":280,"line":386},[278,52669,52670],{},"    pen.clear()\n",[278,52672,52673],{"class":280,"line":397},[278,52674,52675],{},"    game['obj'].start()\n",[11,52677,52678,52679,52682,52683,52686],{},"Since the same function is being used for a new, or a replay game, we clear the drawings made by the pen (",[59,52680,52681],{},"pen.clear()",") before actually starting the game. I wanted to reuse the existing game class obj even for a new game, that's why we see a ",[59,52684,52685],{},"reset()"," method lurking there.",[40,52688],{"url":52689},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002F3o6YfXtqIrtPrApeuI\u002Fgiphy.gif",[32,52691,4796,52693,16622],{"id":52692},"the-tile-class",[59,52694,52695],{},"Tile",[11,52697,4796,52698,52700,52701,52704,52705,52708,52709,52712,52713,52716],{},[59,52699,52695],{}," class just stores the information related to a particular tile, and should be self explanatory. The only important thing to note is the ",[59,52702,52703],{},"tile_id",", stored as (",[59,52706,52707],{},"self._id","). Tile id is being saved as a tuple of form (row, col). This is why when we calculate the x, y position of the tile on the screen, we use ",[59,52710,52711],{},"self._id[1]"," for getting the column index, and ",[59,52714,52715],{},"self._id[0]"," for getting the row index. This is opposite to how we are iterating for generating the colors, or how we'll iterate during the game play. We can change this for consistency if we want to. I haven't changed it, as initially my iterations were columns of rows instead of the current rows of columns, and I am too lazy to change everything now.",[269,52718,52720],{"className":35072,"code":52719,"language":35074,"meta":274,"style":274},"import settings\n\nclass Tile:\n    def __init__(self):\n        self._id = None\n        self._clickable = False\n        self._color = None\n        self._shape = None\n        self._x = 0\n        self._y = 0\n        self._x_bounds = [0, 0]\n        self._y_bounds = [0, 0]\n        self._connections = []\n\n    def set_tile_props(self, tile_id, color=None):\n        self._id = tile_id\n        self._clickable = False\n        self._connections.clear()\n\n        if color:\n            self._color = color\n\n            index = settings.COLORS.index(color)\n            self._shape = settings.INNER_SHAPES[index]\n\n        self._x = (self._id[1] - (settings.MAX_COLS - 1) \u002F 2) * \\\n            (settings.OUTER_TILE_SIZE + settings.TILES_GAP)\n        self._y = (self._id[0] - (settings.MAX_ROWS - 1) \u002F 2) * \\\n            (settings.OUTER_TILE_SIZE + settings.TILES_GAP)\n\n        self._x_bounds[0] = self._x - settings.OUTER_TILE_SIZE \u002F 2\n        self._x_bounds[1] = self._x + settings.OUTER_TILE_SIZE \u002F 2\n        self._y_bounds[0] = self._y - settings.OUTER_TILE_SIZE \u002F 2\n        self._y_bounds[1] = self._y + settings.OUTER_TILE_SIZE \u002F 2\n\n    def add_connection(self, conn_id):\n        self._connections.append(conn_id)\n\n    def connections(self):\n        return self._connections\n\n    def in_bounds(self, x, y):\n        return self._x_bounds[0] \u003C= x \u003C= self._x_bounds[1] and \\\n            self._y_bounds[0] \u003C= y \u003C= self._y_bounds[1]\n\n    def id(self):\n        return self._id\n\n    def color(self):\n        return self._color\n\n    def pos(self):\n        return self._x, self._y\n\n    def shape(self):\n        return self._shape\n\n    def clickable(self, can_click=None):\n        if can_click is not None:\n            self._clickable = can_click\n        else:\n            return self._clickable\n",[59,52721,52722,52726,52730,52735,52740,52745,52750,52755,52760,52765,52770,52775,52780,52785,52789,52794,52799,52803,52808,52812,52817,52822,52826,52831,52836,52840,52845,52850,52855,52859,52863,52868,52873,52878,52883,52887,52892,52897,52901,52906,52911,52915,52920,52925,52930,52934,52939,52944,52948,52953,52958,52962,52967,52972,52976,52981,52986,52990,52995,53000,53005,53009],{"__ignoreMap":274},[278,52723,52724],{"class":280,"line":281},[278,52725,52378],{},[278,52727,52728],{"class":280,"line":288},[278,52729,292],{"emptyLinePlaceholder":291},[278,52731,52732],{"class":280,"line":295},[278,52733,52734],{},"class Tile:\n",[278,52736,52737],{"class":280,"line":316},[278,52738,52739],{},"    def __init__(self):\n",[278,52741,52742],{"class":280,"line":322},[278,52743,52744],{},"        self._id = None\n",[278,52746,52747],{"class":280,"line":327},[278,52748,52749],{},"        self._clickable = False\n",[278,52751,52752],{"class":280,"line":340},[278,52753,52754],{},"        self._color = None\n",[278,52756,52757],{"class":280,"line":349},[278,52758,52759],{},"        self._shape = None\n",[278,52761,52762],{"class":280,"line":375},[278,52763,52764],{},"        self._x = 0\n",[278,52766,52767],{"class":280,"line":386},[278,52768,52769],{},"        self._y = 0\n",[278,52771,52772],{"class":280,"line":397},[278,52773,52774],{},"        self._x_bounds = [0, 0]\n",[278,52776,52777],{"class":280,"line":408},[278,52778,52779],{},"        self._y_bounds = [0, 0]\n",[278,52781,52782],{"class":280,"line":433},[278,52783,52784],{},"        self._connections = []\n",[278,52786,52787],{"class":280,"line":454},[278,52788,292],{"emptyLinePlaceholder":291},[278,52790,52791],{"class":280,"line":475},[278,52792,52793],{},"    def set_tile_props(self, tile_id, color=None):\n",[278,52795,52796],{"class":280,"line":496},[278,52797,52798],{},"        self._id = tile_id\n",[278,52800,52801],{"class":280,"line":505},[278,52802,52749],{},[278,52804,52805],{"class":280,"line":516},[278,52806,52807],{},"        self._connections.clear()\n",[278,52809,52810],{"class":280,"line":527},[278,52811,292],{"emptyLinePlaceholder":291},[278,52813,52814],{"class":280,"line":533},[278,52815,52816],{},"        if color:\n",[278,52818,52819],{"class":280,"line":539},[278,52820,52821],{},"            self._color = color\n",[278,52823,52824],{"class":280,"line":545},[278,52825,292],{"emptyLinePlaceholder":291},[278,52827,52828],{"class":280,"line":551},[278,52829,52830],{},"            index = settings.COLORS.index(color)\n",[278,52832,52833],{"class":280,"line":557},[278,52834,52835],{},"            self._shape = settings.INNER_SHAPES[index]\n",[278,52837,52838],{"class":280,"line":567},[278,52839,292],{"emptyLinePlaceholder":291},[278,52841,52842],{"class":280,"line":577},[278,52843,52844],{},"        self._x = (self._id[1] - (settings.MAX_COLS - 1) \u002F 2) * \\\n",[278,52846,52847],{"class":280,"line":587},[278,52848,52849],{},"            (settings.OUTER_TILE_SIZE + settings.TILES_GAP)\n",[278,52851,52852],{"class":280,"line":597},[278,52853,52854],{},"        self._y = (self._id[0] - (settings.MAX_ROWS - 1) \u002F 2) * \\\n",[278,52856,52857],{"class":280,"line":608},[278,52858,52849],{},[278,52860,52861],{"class":280,"line":614},[278,52862,292],{"emptyLinePlaceholder":291},[278,52864,52865],{"class":280,"line":620},[278,52866,52867],{},"        self._x_bounds[0] = self._x - settings.OUTER_TILE_SIZE \u002F 2\n",[278,52869,52870],{"class":280,"line":625},[278,52871,52872],{},"        self._x_bounds[1] = self._x + settings.OUTER_TILE_SIZE \u002F 2\n",[278,52874,52875],{"class":280,"line":640},[278,52876,52877],{},"        self._y_bounds[0] = self._y - settings.OUTER_TILE_SIZE \u002F 2\n",[278,52879,52880],{"class":280,"line":663},[278,52881,52882],{},"        self._y_bounds[1] = self._y + settings.OUTER_TILE_SIZE \u002F 2\n",[278,52884,52885],{"class":280,"line":669},[278,52886,292],{"emptyLinePlaceholder":291},[278,52888,52889],{"class":280,"line":680},[278,52890,52891],{},"    def add_connection(self, conn_id):\n",[278,52893,52894],{"class":280,"line":686},[278,52895,52896],{},"        self._connections.append(conn_id)\n",[278,52898,52899],{"class":280,"line":1334},[278,52900,292],{"emptyLinePlaceholder":291},[278,52902,52903],{"class":280,"line":1375},[278,52904,52905],{},"    def connections(self):\n",[278,52907,52908],{"class":280,"line":1381},[278,52909,52910],{},"        return self._connections\n",[278,52912,52913],{"class":280,"line":1386},[278,52914,292],{"emptyLinePlaceholder":291},[278,52916,52917],{"class":280,"line":1394},[278,52918,52919],{},"    def in_bounds(self, x, y):\n",[278,52921,52922],{"class":280,"line":1406},[278,52923,52924],{},"        return self._x_bounds[0] \u003C= x \u003C= self._x_bounds[1] and \\\n",[278,52926,52927],{"class":280,"line":1423},[278,52928,52929],{},"            self._y_bounds[0] \u003C= y \u003C= self._y_bounds[1]\n",[278,52931,52932],{"class":280,"line":1432},[278,52933,292],{"emptyLinePlaceholder":291},[278,52935,52936],{"class":280,"line":1437},[278,52937,52938],{},"    def id(self):\n",[278,52940,52941],{"class":280,"line":1916},[278,52942,52943],{},"        return self._id\n",[278,52945,52946],{"class":280,"line":1939},[278,52947,292],{"emptyLinePlaceholder":291},[278,52949,52950],{"class":280,"line":1949},[278,52951,52952],{},"    def color(self):\n",[278,52954,52955],{"class":280,"line":1954},[278,52956,52957],{},"        return self._color\n",[278,52959,52960],{"class":280,"line":1959},[278,52961,292],{"emptyLinePlaceholder":291},[278,52963,52964],{"class":280,"line":1985},[278,52965,52966],{},"    def pos(self):\n",[278,52968,52969],{"class":280,"line":1990},[278,52970,52971],{},"        return self._x, self._y\n",[278,52973,52974],{"class":280,"line":1997},[278,52975,292],{"emptyLinePlaceholder":291},[278,52977,52978],{"class":280,"line":2006},[278,52979,52980],{},"    def shape(self):\n",[278,52982,52983],{"class":280,"line":2018},[278,52984,52985],{},"        return self._shape\n",[278,52987,52988],{"class":280,"line":2029},[278,52989,292],{"emptyLinePlaceholder":291},[278,52991,52992],{"class":280,"line":2034},[278,52993,52994],{},"    def clickable(self, can_click=None):\n",[278,52996,52997],{"class":280,"line":2040},[278,52998,52999],{},"        if can_click is not None:\n",[278,53001,53002],{"class":280,"line":2045},[278,53003,53004],{},"            self._clickable = can_click\n",[278,53006,53007],{"class":280,"line":2068},[278,53008,35371],{},[278,53010,53011],{"class":280,"line":2099},[278,53012,53013],{},"            return self._clickable\n",[32,53015,4796,53017,16622],{"id":53016},"the-game-class",[59,53018,53019],{},"Game",[11,53021,53022],{},"This is the brain of the game. We will go through this class step by step.",[53024,53025,4796,53027,53030,53031,35414],"h4",{"id":53026},"the-constructor-the-reset-method",[59,53028,53029],{},"constructor"," & the ",[59,53032,53033],{},"reset",[269,53035,53037],{"className":35072,"code":53036,"language":35074,"meta":274,"style":274},"from Tile import Tile\nimport settings\n\nclass Game:\n    def __init__(self, colors, screen, pen):\n        self.screen = screen\n        self.pen = pen\n        self.tiles = []\n        self.cache = []\n        self.colors = colors\n        self.clickables = []\n        self.connection_groups = []\n        self.moves = 0\n\n    def reset(self, colors):\n        self.tiles.clear()\n        self.colors = colors\n        self.clickables.clear()\n        self.connection_groups.clear()\n        self.moves = 0\n",[59,53038,53039,53044,53048,53052,53057,53062,53067,53072,53077,53082,53087,53092,53097,53102,53106,53111,53116,53120,53125,53130],{"__ignoreMap":274},[278,53040,53041],{"class":280,"line":281},[278,53042,53043],{},"from Tile import Tile\n",[278,53045,53046],{"class":280,"line":288},[278,53047,52378],{},[278,53049,53050],{"class":280,"line":295},[278,53051,292],{"emptyLinePlaceholder":291},[278,53053,53054],{"class":280,"line":316},[278,53055,53056],{},"class Game:\n",[278,53058,53059],{"class":280,"line":322},[278,53060,53061],{},"    def __init__(self, colors, screen, pen):\n",[278,53063,53064],{"class":280,"line":327},[278,53065,53066],{},"        self.screen = screen\n",[278,53068,53069],{"class":280,"line":340},[278,53070,53071],{},"        self.pen = pen\n",[278,53073,53074],{"class":280,"line":349},[278,53075,53076],{},"        self.tiles = []\n",[278,53078,53079],{"class":280,"line":375},[278,53080,53081],{},"        self.cache = []\n",[278,53083,53084],{"class":280,"line":386},[278,53085,53086],{},"        self.colors = colors\n",[278,53088,53089],{"class":280,"line":397},[278,53090,53091],{},"        self.clickables = []\n",[278,53093,53094],{"class":280,"line":408},[278,53095,53096],{},"        self.connection_groups = []\n",[278,53098,53099],{"class":280,"line":433},[278,53100,53101],{},"        self.moves = 0\n",[278,53103,53104],{"class":280,"line":454},[278,53105,292],{"emptyLinePlaceholder":291},[278,53107,53108],{"class":280,"line":475},[278,53109,53110],{},"    def reset(self, colors):\n",[278,53112,53113],{"class":280,"line":496},[278,53114,53115],{},"        self.tiles.clear()\n",[278,53117,53118],{"class":280,"line":505},[278,53119,53086],{},[278,53121,53122],{"class":280,"line":516},[278,53123,53124],{},"        self.clickables.clear()\n",[278,53126,53127],{"class":280,"line":527},[278,53128,53129],{},"        self.connection_groups.clear()\n",[278,53131,53132],{"class":280,"line":533},[278,53133,53101],{},[11,53135,53136,53137,1708,53140,1708,53142,19634,53145,183],{},"The variables to note here are ",[59,53138,53139],{},"tiles",[59,53141,10999],{},[59,53143,53144],{},"clickables",[59,53146,51933],{},[71,53148,53149,53154,53159,53164],{},[74,53150,53151,53153],{},[59,53152,53139],{},": stores the tiles currently being shown on the screen",[74,53155,53156,53158],{},[59,53157,10999],{},": stores the tiles which have been removed from the screen (Don't really need it, but we'll be reusing the tiles for a new game or a replay, and hence the presence).",[74,53160,53161,53163],{},[59,53162,53144],{},": stores the ids of tiles which are clickable at the moment",[74,53165,53166,53169],{},[59,53167,53168],{},"connections_groups",": is a list which stores lists of ids, of clickable interconnected tiles",[53024,53171,4796,53173,53175],{"id":53172},"the-start-and-other-relevant-methods",[59,53174,6610],{}," and other relevant methods",[11,53177,53178,53179,53181,53182,19634,53185,53188],{},"Below is the code for the ",[59,53180,6610],{}," method which internally calls ",[59,53183,53184],{},"create_tiles",[59,53186,53187],{},"draw_board"," methods. Notice that till now we haven't really stated listening to tiles clicks events (no point if the game hasn't started yet, right?). We start doing this by listening to screen clicks, and then figuring out which tile was clicked.",[269,53190,53192],{"className":35072,"code":53191,"language":35074,"meta":274,"style":274},"def start(self):\n    self.create_tiles()\n\n    self.draw_board()\n\n    self.write_text(0, -self.screen.window_height() \u002F\n                    2 + 100, 'Click any of the colored tiles', 18)\n    self.screen.onclick(self.on_screen_click)\n\ndef create_tiles(self):\n    for col in range(settings.MAX_COLS):\n        self.tiles.append([])\n        for row in range(settings.MAX_ROWS):\n            tile = self.cache.pop() if len(self.cache) > 0 else Tile()\n            tile.set_tile_props((row, col), self.colors[col][row]) # tile_id being set as (row, col)\n            self.tiles[col].append(tile)\n\ndef write_text(self, x, y, text, size):\n    if self.pen.color() != 'white':\n        self.pen.color('white')\n\n    self.pen.goto(x, y)\n    self.pen.write(text, align='center', font=('Courier', size, 'normal'))\n",[59,53193,53194,53199,53204,53208,53213,53217,53222,53227,53232,53236,53241,53246,53251,53256,53261,53266,53271,53275,53280,53285,53290,53294,53299],{"__ignoreMap":274},[278,53195,53196],{"class":280,"line":281},[278,53197,53198],{},"def start(self):\n",[278,53200,53201],{"class":280,"line":288},[278,53202,53203],{},"    self.create_tiles()\n",[278,53205,53206],{"class":280,"line":295},[278,53207,292],{"emptyLinePlaceholder":291},[278,53209,53210],{"class":280,"line":316},[278,53211,53212],{},"    self.draw_board()\n",[278,53214,53215],{"class":280,"line":322},[278,53216,292],{"emptyLinePlaceholder":291},[278,53218,53219],{"class":280,"line":327},[278,53220,53221],{},"    self.write_text(0, -self.screen.window_height() \u002F\n",[278,53223,53224],{"class":280,"line":340},[278,53225,53226],{},"                    2 + 100, 'Click any of the colored tiles', 18)\n",[278,53228,53229],{"class":280,"line":349},[278,53230,53231],{},"    self.screen.onclick(self.on_screen_click)\n",[278,53233,53234],{"class":280,"line":375},[278,53235,292],{"emptyLinePlaceholder":291},[278,53237,53238],{"class":280,"line":386},[278,53239,53240],{},"def create_tiles(self):\n",[278,53242,53243],{"class":280,"line":397},[278,53244,53245],{},"    for col in range(settings.MAX_COLS):\n",[278,53247,53248],{"class":280,"line":408},[278,53249,53250],{},"        self.tiles.append([])\n",[278,53252,53253],{"class":280,"line":433},[278,53254,53255],{},"        for row in range(settings.MAX_ROWS):\n",[278,53257,53258],{"class":280,"line":454},[278,53259,53260],{},"            tile = self.cache.pop() if len(self.cache) > 0 else Tile()\n",[278,53262,53263],{"class":280,"line":475},[278,53264,53265],{},"            tile.set_tile_props((row, col), self.colors[col][row]) # tile_id being set as (row, col)\n",[278,53267,53268],{"class":280,"line":496},[278,53269,53270],{},"            self.tiles[col].append(tile)\n",[278,53272,53273],{"class":280,"line":505},[278,53274,292],{"emptyLinePlaceholder":291},[278,53276,53277],{"class":280,"line":516},[278,53278,53279],{},"def write_text(self, x, y, text, size):\n",[278,53281,53282],{"class":280,"line":527},[278,53283,53284],{},"    if self.pen.color() != 'white':\n",[278,53286,53287],{"class":280,"line":533},[278,53288,53289],{},"        self.pen.color('white')\n",[278,53291,53292],{"class":280,"line":539},[278,53293,292],{"emptyLinePlaceholder":291},[278,53295,53296],{"class":280,"line":545},[278,53297,53298],{},"    self.pen.goto(x, y)\n",[278,53300,53301],{"class":280,"line":551},[278,53302,53303],{},"    self.pen.write(text, align='center', font=('Courier', size, 'normal'))\n",[53024,53305,4796,53307,35414],{"id":53306},"the-draw_board-method",[59,53308,53187],{},[11,53310,53311],{},"This is an important method. Its job is to figure out the connections, draw the tiles and make the screen ready for the user. It's like a manager who is going to give the demo, hoping that everyone has done their job correctly.",[11,53313,53314],{},"We don't want no broken windows, do we? ;-)",[40,53316],{"url":53317},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FQWjyvdpMDYKbOFLdIv\u002Fgiphy.gif",[269,53319,53321],{"className":35072,"code":53320,"language":35074,"meta":274,"style":274},"def draw_board(self):\n    for col in range(settings.MAX_COLS):\n        self.process_tile(0, col)\n\n    self.draw_tiles()\n\n    self.write_text(0, self.screen.window_height() \u002F 2 - 80, 'Figure', 42)\n    if len(self.clickables) == 0:\n        self.screen.onclick(None)\n        self.write_text(0, 80, 'Game Over', 36)\n        self.write_text(0, 50, f'Total {self.moves} moves', 20)\n        self.write_text(0, -60, 'Press \"space\" to replay', 20)\n        self.write_text(0, -90, 'Press \"n\" to start a new game', 20)\n    else:\n        self.write_text(0, self.screen.window_height() \u002F\n                        2 - 140, f'{self.moves} moves', 20)\n\n    self.screen.update()\n",[59,53322,53323,53328,53332,53337,53341,53346,53350,53355,53360,53365,53370,53375,53380,53385,53389,53394,53399,53403],{"__ignoreMap":274},[278,53324,53325],{"class":280,"line":281},[278,53326,53327],{},"def draw_board(self):\n",[278,53329,53330],{"class":280,"line":288},[278,53331,53245],{},[278,53333,53334],{"class":280,"line":295},[278,53335,53336],{},"        self.process_tile(0, col)\n",[278,53338,53339],{"class":280,"line":316},[278,53340,292],{"emptyLinePlaceholder":291},[278,53342,53343],{"class":280,"line":322},[278,53344,53345],{},"    self.draw_tiles()\n",[278,53347,53348],{"class":280,"line":327},[278,53349,292],{"emptyLinePlaceholder":291},[278,53351,53352],{"class":280,"line":340},[278,53353,53354],{},"    self.write_text(0, self.screen.window_height() \u002F 2 - 80, 'Figure', 42)\n",[278,53356,53357],{"class":280,"line":349},[278,53358,53359],{},"    if len(self.clickables) == 0:\n",[278,53361,53362],{"class":280,"line":375},[278,53363,53364],{},"        self.screen.onclick(None)\n",[278,53366,53367],{"class":280,"line":386},[278,53368,53369],{},"        self.write_text(0, 80, 'Game Over', 36)\n",[278,53371,53372],{"class":280,"line":397},[278,53373,53374],{},"        self.write_text(0, 50, f'Total {self.moves} moves', 20)\n",[278,53376,53377],{"class":280,"line":408},[278,53378,53379],{},"        self.write_text(0, -60, 'Press \"space\" to replay', 20)\n",[278,53381,53382],{"class":280,"line":433},[278,53383,53384],{},"        self.write_text(0, -90, 'Press \"n\" to start a new game', 20)\n",[278,53386,53387],{"class":280,"line":454},[278,53388,36178],{},[278,53390,53391],{"class":280,"line":475},[278,53392,53393],{},"        self.write_text(0, self.screen.window_height() \u002F\n",[278,53395,53396],{"class":280,"line":496},[278,53397,53398],{},"                        2 - 140, f'{self.moves} moves', 20)\n",[278,53400,53401],{"class":280,"line":505},[278,53402,292],{"emptyLinePlaceholder":291},[278,53404,53405],{"class":280,"line":516},[278,53406,53407],{},"    self.screen.update()\n",[11,53409,16745,53410,53413],{},[59,53411,53412],{},"self.screen.update()"," call at the end. Since we had turned off the tracer while creating the screen, we will need to refresh the screen ourselves, the method call does precisely that.",[11,53415,53416],{},"The code below iterates over the bottom row of the tiles, and makes the tiles present as clickable. Every tile, in turn, figures out if we need to go further down the tree and find out other connectable tiles.",[269,53418,53420],{"className":35072,"code":53419,"language":35074,"meta":274,"style":274},"for col in range(settings.MAX_COLS):\n    self.process_tile(0, col)\n",[59,53421,53422,53427],{"__ignoreMap":274},[278,53423,53424],{"class":280,"line":281},[278,53425,53426],{},"for col in range(settings.MAX_COLS):\n",[278,53428,53429],{"class":280,"line":288},[278,53430,53431],{},"    self.process_tile(0, col)\n",[53024,53433,4796,53435,53438],{"id":53434},"the-process_tile-other-related-methods",[59,53436,53437],{},"process_tile"," & other related methods",[11,53440,53441,53442,53445,53446,53448],{},"This method figures out which of the tiles are interconnected. We need to call this method before calling ",[59,53443,53444],{},"draw_tiles",", as ",[59,53447,53444],{}," will also draw the connections on the screen.",[269,53450,53452],{"className":35072,"code":53451,"language":35074,"meta":274,"style":274},"def get_node(self, row, col):\n    if 0 \u003C= col \u003C= settings.MAX_COLS - 1 and 0 \u003C= row \u003C= settings.MAX_ROWS - 1:\n        col_tiles = self.tiles[col]\n        return col_tiles[row] if row \u003C len(col_tiles) else None\n\ndef process_tile(self, row, col):\n    curr_node = self.get_node(row, col)\n    if not curr_node or curr_node.clickable():\n        return\n\n    has_clickable_connections = {\n        'prev': self.connectable(curr_node, row, col - 1),\n        'next': self.connectable(curr_node, row, col + 1),\n        'below': self.connectable(curr_node, row - 1, col),\n        'above': self.connectable(curr_node, row + 1, col)\n    }\n\n    if row == 0 or True in has_clickable_connections.values():\n        curr_node.clickable(True)\n        if has_clickable_connections['next']:\n            curr_node.add_connection((row, col + 1))\n        if has_clickable_connections['above']:\n            curr_node.add_connection((row + 1, col))\n\n        if (row, col) not in self.clickables:\n            self.clickables.append((row, col))\n\n        found = False\n        for connections in self.connection_groups:\n            if (row, col) in connections:\n                found = True\n                break\n\n        if not found:\n            self.connection_groups.append([(row, col)])\n\n        for value in has_clickable_connections.values():\n            if isinstance(value, tuple):\n                for connections in self.connection_groups:\n                    if (row, col) in connections and value not in connections:\n                        connections.append(value)\n                        break\n                self.process_tile(*value)\n",[59,53453,53454,53459,53464,53469,53473,53477,53482,53487,53492,53496,53500,53504,53509,53514,53519,53524,53528,53532,53536,53541,53545,53550,53554,53559,53563,53568,53573,53577,53581,53586,53590,53594,53598,53602,53606,53611,53615,53619,53623,53628,53632,53636,53640],{"__ignoreMap":274},[278,53455,53456],{"class":280,"line":281},[278,53457,53458],{},"def get_node(self, row, col):\n",[278,53460,53461],{"class":280,"line":288},[278,53462,53463],{},"    if 0 \u003C= col \u003C= settings.MAX_COLS - 1 and 0 \u003C= row \u003C= settings.MAX_ROWS - 1:\n",[278,53465,53466],{"class":280,"line":295},[278,53467,53468],{},"        col_tiles = self.tiles[col]\n",[278,53470,53471],{"class":280,"line":316},[278,53472,51356],{},[278,53474,53475],{"class":280,"line":322},[278,53476,292],{"emptyLinePlaceholder":291},[278,53478,53479],{"class":280,"line":327},[278,53480,53481],{},"def process_tile(self, row, col):\n",[278,53483,53484],{"class":280,"line":340},[278,53485,53486],{},"    curr_node = self.get_node(row, col)\n",[278,53488,53489],{"class":280,"line":349},[278,53490,53491],{},"    if not curr_node or curr_node.clickable():\n",[278,53493,53494],{"class":280,"line":375},[278,53495,36527],{},[278,53497,53498],{"class":280,"line":386},[278,53499,292],{"emptyLinePlaceholder":291},[278,53501,53502],{"class":280,"line":397},[278,53503,51426],{},[278,53505,53506],{"class":280,"line":408},[278,53507,53508],{},"        'prev': self.connectable(curr_node, row, col - 1),\n",[278,53510,53511],{"class":280,"line":433},[278,53512,53513],{},"        'next': self.connectable(curr_node, row, col + 1),\n",[278,53515,53516],{"class":280,"line":454},[278,53517,53518],{},"        'below': self.connectable(curr_node, row - 1, col),\n",[278,53520,53521],{"class":280,"line":475},[278,53522,53523],{},"        'above': self.connectable(curr_node, row + 1, col)\n",[278,53525,53526],{"class":280,"line":496},[278,53527,1285],{},[278,53529,53530],{"class":280,"line":505},[278,53531,292],{"emptyLinePlaceholder":291},[278,53533,53534],{"class":280,"line":516},[278,53535,51459],{},[278,53537,53538],{"class":280,"line":527},[278,53539,53540],{},"        curr_node.clickable(True)\n",[278,53542,53543],{"class":280,"line":533},[278,53544,51469],{},[278,53546,53547],{"class":280,"line":539},[278,53548,53549],{},"            curr_node.add_connection((row, col + 1))\n",[278,53551,53552],{"class":280,"line":545},[278,53553,51479],{},[278,53555,53556],{"class":280,"line":551},[278,53557,53558],{},"            curr_node.add_connection((row + 1, col))\n",[278,53560,53561],{"class":280,"line":557},[278,53562,292],{"emptyLinePlaceholder":291},[278,53564,53565],{"class":280,"line":567},[278,53566,53567],{},"        if (row, col) not in self.clickables:\n",[278,53569,53570],{"class":280,"line":577},[278,53571,53572],{},"            self.clickables.append((row, col))\n",[278,53574,53575],{"class":280,"line":587},[278,53576,292],{"emptyLinePlaceholder":291},[278,53578,53579],{"class":280,"line":597},[278,53580,51507],{},[278,53582,53583],{"class":280,"line":608},[278,53584,53585],{},"        for connections in self.connection_groups:\n",[278,53587,53588],{"class":280,"line":614},[278,53589,51517],{},[278,53591,53592],{"class":280,"line":620},[278,53593,51522],{},[278,53595,53596],{"class":280,"line":625},[278,53597,35877],{},[278,53599,53600],{"class":280,"line":640},[278,53601,292],{"emptyLinePlaceholder":291},[278,53603,53604],{"class":280,"line":663},[278,53605,51535],{},[278,53607,53608],{"class":280,"line":669},[278,53609,53610],{},"            self.connection_groups.append([(row, col)])\n",[278,53612,53613],{"class":280,"line":680},[278,53614,292],{"emptyLinePlaceholder":291},[278,53616,53617],{"class":280,"line":686},[278,53618,51549],{},[278,53620,53621],{"class":280,"line":1334},[278,53622,51554],{},[278,53624,53625],{"class":280,"line":1375},[278,53626,53627],{},"                for connections in self.connection_groups:\n",[278,53629,53630],{"class":280,"line":1381},[278,53631,51564],{},[278,53633,53634],{"class":280,"line":1386},[278,53635,51569],{},[278,53637,53638],{"class":280,"line":1394},[278,53639,51574],{},[278,53641,53642],{"class":280,"line":1406},[278,53643,53644],{},"                self.process_tile(*value)\n",[11,53646,53647,53648,53651],{},"We get the ",[59,53649,53650],{},"current_node"," and if the node is not there, or if it is already clickable then we return from the function as no further processing is needed.",[269,53653,53655],{"className":35072,"code":53654,"language":35074,"meta":274,"style":274},"curr_node = self.get_node(row, col)\nif not curr_node or curr_node.clickable():\n    return\n",[59,53656,53657,53662,53667],{"__ignoreMap":274},[278,53658,53659],{"class":280,"line":281},[278,53660,53661],{},"curr_node = self.get_node(row, col)\n",[278,53663,53664],{"class":280,"line":288},[278,53665,53666],{},"if not curr_node or curr_node.clickable():\n",[278,53668,53669],{"class":280,"line":295},[278,53670,53671],{},"    return\n",[11,53673,53674],{},"Then we look around the current node and see if we can become a clickable node by virtue of being connected to another clickable node of same color.",[40,53676],{"url":53677},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FmfGkfzHM3KfdI0OmIW\u002Fgiphy.gif",[269,53679,53681],{"className":35072,"code":53680,"language":35074,"meta":274,"style":274},"has_clickable_connections = {\n    'prev': self.connectable(curr_node, row, col - 1),\n    'next': self.connectable(curr_node, row, col + 1),\n    'below': self.connectable(curr_node, row - 1, col),\n    'above': self.connectable(curr_node, row + 1, col)\n}\n",[59,53682,53683,53688,53693,53698,53703,53708],{"__ignoreMap":274},[278,53684,53685],{"class":280,"line":281},[278,53686,53687],{},"has_clickable_connections = {\n",[278,53689,53690],{"class":280,"line":288},[278,53691,53692],{},"    'prev': self.connectable(curr_node, row, col - 1),\n",[278,53694,53695],{"class":280,"line":295},[278,53696,53697],{},"    'next': self.connectable(curr_node, row, col + 1),\n",[278,53699,53700],{"class":280,"line":316},[278,53701,53702],{},"    'below': self.connectable(curr_node, row - 1, col),\n",[278,53704,53705],{"class":280,"line":322},[278,53706,53707],{},"    'above': self.connectable(curr_node, row + 1, col)\n",[278,53709,53710],{"class":280,"line":327},[278,53711,617],{},[11,53713,53714],{},[3061,53715,4796,53716,35414],{},[59,53717,53718],{},"connectable",[269,53720,53722],{"className":35072,"code":53721,"language":35074,"meta":274,"style":274},"def connectable(self, first_node, row, col):\n    other_node = self.get_node(row, col)\n    if other_node and first_node.color() == other_node.color():\n        if other_node.clickable():\n            return True\n\n        return (row, col)\n",[59,53723,53724,53729,53734,53739,53744,53748,53752],{"__ignoreMap":274},[278,53725,53726],{"class":280,"line":281},[278,53727,53728],{},"def connectable(self, first_node, row, col):\n",[278,53730,53731],{"class":280,"line":288},[278,53732,53733],{},"    other_node = self.get_node(row, col)\n",[278,53735,53736],{"class":280,"line":295},[278,53737,53738],{},"    if other_node and first_node.color() == other_node.color():\n",[278,53740,53741],{"class":280,"line":316},[278,53742,53743],{},"        if other_node.clickable():\n",[278,53745,53746],{"class":280,"line":322},[278,53747,51385],{},[278,53749,53750],{"class":280,"line":327},[278,53751,292],{"emptyLinePlaceholder":291},[278,53753,53754],{"class":280,"line":340},[278,53755,51394],{},[11,53757,4796,53758,53760],{},[59,53759,53718],{}," method returns true if the other node exists, is clickable and of the same color. If it is of the same color but not currently clickable, then it returns its calling card (the tile_id). We do this because we need to traverse the tree, and if possible, make this node also clickable.",[40,53762],{"url":53763},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002FYpvufSuDWxOEB9LwNW\u002Fgiphy.gif",[11,53765,53766,53767,53769],{},"Rest of the code in the ",[59,53768,53437],{}," method is simply about making the current_node clickable based on above findings, and then traverse the tree based on whoever gave their calling cards. We also save the respective ids of the clickable and connected nodes in appropriate locations for game play.",[269,53771,53773],{"className":35072,"code":53772,"language":35074,"meta":274,"style":274},"if row == 0 or True in has_clickable_connections.values():\n    curr_node.clickable(True)\n    # Every clickable node will only store the forward connections (next or above)\n    if has_clickable_connections['next']: \n        curr_node.add_connection((row, col + 1))\n    if has_clickable_connections['above']:\n        curr_node.add_connection((row + 1, col))\n\n    if (row, col) not in self.clickables:\n        self.clickables.append((row, col))\n\n    # We find the appropriate list where this id is already present\n    found = False\n    for connections in self.connection_groups:\n        if (row, col) in connections:\n            found = True\n            break\n\n    if not found:\n        # Append a new list containing the tile_id to the connection_groups\n        self.connection_groups.append([(row, col)])\n\n    for value in has_clickable_connections.values():\n        # Here if we've a tuple, means we need to traverse that node\n        # Also, we need to add this node to the connection list\n        if isinstance(value, tuple):\n            for connections in self.connection_groups:\n                if (row, col) in connections and value not in connections:\n                    connections.append(value)\n                    break\n            # Recursive call to traverse this node\n            self.process_tile(*value)\n",[59,53774,53775,53780,53785,53790,53795,53800,53805,53810,53814,53819,53824,53828,53833,53838,53843,53848,53853,53857,53861,53866,53871,53876,53880,53885,53890,53895,53900,53905,53910,53915,53920,53925],{"__ignoreMap":274},[278,53776,53777],{"class":280,"line":281},[278,53778,53779],{},"if row == 0 or True in has_clickable_connections.values():\n",[278,53781,53782],{"class":280,"line":288},[278,53783,53784],{},"    curr_node.clickable(True)\n",[278,53786,53787],{"class":280,"line":295},[278,53788,53789],{},"    # Every clickable node will only store the forward connections (next or above)\n",[278,53791,53792],{"class":280,"line":316},[278,53793,53794],{},"    if has_clickable_connections['next']: \n",[278,53796,53797],{"class":280,"line":322},[278,53798,53799],{},"        curr_node.add_connection((row, col + 1))\n",[278,53801,53802],{"class":280,"line":327},[278,53803,53804],{},"    if has_clickable_connections['above']:\n",[278,53806,53807],{"class":280,"line":340},[278,53808,53809],{},"        curr_node.add_connection((row + 1, col))\n",[278,53811,53812],{"class":280,"line":349},[278,53813,292],{"emptyLinePlaceholder":291},[278,53815,53816],{"class":280,"line":375},[278,53817,53818],{},"    if (row, col) not in self.clickables:\n",[278,53820,53821],{"class":280,"line":386},[278,53822,53823],{},"        self.clickables.append((row, col))\n",[278,53825,53826],{"class":280,"line":397},[278,53827,292],{"emptyLinePlaceholder":291},[278,53829,53830],{"class":280,"line":408},[278,53831,53832],{},"    # We find the appropriate list where this id is already present\n",[278,53834,53835],{"class":280,"line":433},[278,53836,53837],{},"    found = False\n",[278,53839,53840],{"class":280,"line":454},[278,53841,53842],{},"    for connections in self.connection_groups:\n",[278,53844,53845],{"class":280,"line":475},[278,53846,53847],{},"        if (row, col) in connections:\n",[278,53849,53850],{"class":280,"line":496},[278,53851,53852],{},"            found = True\n",[278,53854,53855],{"class":280,"line":505},[278,53856,36379],{},[278,53858,53859],{"class":280,"line":516},[278,53860,292],{"emptyLinePlaceholder":291},[278,53862,53863],{"class":280,"line":527},[278,53864,53865],{},"    if not found:\n",[278,53867,53868],{"class":280,"line":533},[278,53869,53870],{},"        # Append a new list containing the tile_id to the connection_groups\n",[278,53872,53873],{"class":280,"line":539},[278,53874,53875],{},"        self.connection_groups.append([(row, col)])\n",[278,53877,53878],{"class":280,"line":545},[278,53879,292],{"emptyLinePlaceholder":291},[278,53881,53882],{"class":280,"line":551},[278,53883,53884],{},"    for value in has_clickable_connections.values():\n",[278,53886,53887],{"class":280,"line":557},[278,53888,53889],{},"        # Here if we've a tuple, means we need to traverse that node\n",[278,53891,53892],{"class":280,"line":567},[278,53893,53894],{},"        # Also, we need to add this node to the connection list\n",[278,53896,53897],{"class":280,"line":577},[278,53898,53899],{},"        if isinstance(value, tuple):\n",[278,53901,53902],{"class":280,"line":587},[278,53903,53904],{},"            for connections in self.connection_groups:\n",[278,53906,53907],{"class":280,"line":597},[278,53908,53909],{},"                if (row, col) in connections and value not in connections:\n",[278,53911,53912],{"class":280,"line":608},[278,53913,53914],{},"                    connections.append(value)\n",[278,53916,53917],{"class":280,"line":614},[278,53918,53919],{},"                    break\n",[278,53921,53922],{"class":280,"line":620},[278,53923,53924],{},"            # Recursive call to traverse this node\n",[278,53926,53927],{"class":280,"line":625},[278,53928,53929],{},"            self.process_tile(*value)\n",[11,53931,53932],{},"We can optimize the finding of appropriate connection_group list where we need to append the next node. We can do this by finding and storing the list index from the previous call and use that later.",[53024,53934,4796,53936,19634,53938,35414],{"id":53935},"the-draw_tiles-draw_tile-method",[59,53937,53444],{},[59,53939,53940],{},"draw_tile",[11,53942,53943],{},"This is quite straight forward.",[269,53945,53947],{"className":35072,"code":53946,"language":35074,"meta":274,"style":274},"def draw_tiles(self):\n    for col_tiles in self.tiles:\n        for tile in col_tiles:\n            if not tile: # there won't be any tile above it also, so break\n                break\n\n            self.draw_tile(tile)\n\ndef draw_tile(self, tile):\n    pen = self.pen\n    pos = tile.pos()\n    pen.goto(pos)\n    pen.shape('square')\n    pen.color(tile.color())\n    if not tile.clickable():\n        pen.fillcolor('midnight blue')\n    pen.shapesize(settings.OUTER_SIZE_MULTIPLIER,\n                  settings.OUTER_SIZE_MULTIPLIER, settings.OUTER_OUTLINE)\n    pen.stamp()\n\n    if tile.clickable():\n        pen.color('midnight blue')\n    else:\n        pen.color(tile.color())\n    pen.shapesize(settings.INNER_SIZE_MULTIPLIER,\n                  settings.INNER_SIZE_MULTIPLIER, settings.INNER_OUTLINE)\n\n    tilt = 0\n    if tile.shape() == 'diamond':\n        pen.shape('square')\n        pen.tilt(45)\n        tilt = -45\n    else:\n        pen.shape(tile.shape())\n        if tile.shape() == 'triangle':\n            pen.tilt(90)\n            tilt = -90\n\n    pen.stamp()\n    pen.tilt(tilt)\n\n    tile_id = tile.id()\n    connections = tile.connections()\n    if (tile_id[0], tile_id[1] + 1) in connections:\n        pen.goto(pos[0] + settings.OUTER_TILE_SIZE \u002F 2, pos[1])\n        pen.color(tile.color())\n        pen.pendown()\n        pen.setx(pen.xcor() + settings.TILES_GAP)\n        pen.penup()\n    if (tile_id[0] + 1, tile_id[1]) in connections:\n        pen.goto(pos[0], pos[1] + settings.OUTER_TILE_SIZE \u002F 2)\n        pen.color(tile.color())\n        pen.pendown()\n        pen.sety(pen.ycor() + settings.TILES_GAP)\n        pen.penup()\n",[59,53948,53949,53954,53959,53964,53969,53973,53977,53982,53986,53991,53996,54001,54006,54011,54016,54021,54026,54031,54036,54041,54045,54050,54055,54059,54064,54069,54074,54078,54083,54088,54093,54098,54103,54107,54112,54117,54122,54127,54131,54135,54140,54144,54149,54154,54159,54164,54168,54173,54178,54183,54188,54193,54197,54201,54206],{"__ignoreMap":274},[278,53950,53951],{"class":280,"line":281},[278,53952,53953],{},"def draw_tiles(self):\n",[278,53955,53956],{"class":280,"line":288},[278,53957,53958],{},"    for col_tiles in self.tiles:\n",[278,53960,53961],{"class":280,"line":295},[278,53962,53963],{},"        for tile in col_tiles:\n",[278,53965,53966],{"class":280,"line":316},[278,53967,53968],{},"            if not tile: # there won't be any tile above it also, so break\n",[278,53970,53971],{"class":280,"line":322},[278,53972,35877],{},[278,53974,53975],{"class":280,"line":327},[278,53976,292],{"emptyLinePlaceholder":291},[278,53978,53979],{"class":280,"line":340},[278,53980,53981],{},"            self.draw_tile(tile)\n",[278,53983,53984],{"class":280,"line":349},[278,53985,292],{"emptyLinePlaceholder":291},[278,53987,53988],{"class":280,"line":375},[278,53989,53990],{},"def draw_tile(self, tile):\n",[278,53992,53993],{"class":280,"line":386},[278,53994,53995],{},"    pen = self.pen\n",[278,53997,53998],{"class":280,"line":397},[278,53999,54000],{},"    pos = tile.pos()\n",[278,54002,54003],{"class":280,"line":408},[278,54004,54005],{},"    pen.goto(pos)\n",[278,54007,54008],{"class":280,"line":433},[278,54009,54010],{},"    pen.shape('square')\n",[278,54012,54013],{"class":280,"line":454},[278,54014,54015],{},"    pen.color(tile.color())\n",[278,54017,54018],{"class":280,"line":475},[278,54019,54020],{},"    if not tile.clickable():\n",[278,54022,54023],{"class":280,"line":496},[278,54024,54025],{},"        pen.fillcolor('midnight blue')\n",[278,54027,54028],{"class":280,"line":505},[278,54029,54030],{},"    pen.shapesize(settings.OUTER_SIZE_MULTIPLIER,\n",[278,54032,54033],{"class":280,"line":516},[278,54034,54035],{},"                  settings.OUTER_SIZE_MULTIPLIER, settings.OUTER_OUTLINE)\n",[278,54037,54038],{"class":280,"line":527},[278,54039,54040],{},"    pen.stamp()\n",[278,54042,54043],{"class":280,"line":533},[278,54044,292],{"emptyLinePlaceholder":291},[278,54046,54047],{"class":280,"line":539},[278,54048,54049],{},"    if tile.clickable():\n",[278,54051,54052],{"class":280,"line":545},[278,54053,54054],{},"        pen.color('midnight blue')\n",[278,54056,54057],{"class":280,"line":551},[278,54058,36178],{},[278,54060,54061],{"class":280,"line":557},[278,54062,54063],{},"        pen.color(tile.color())\n",[278,54065,54066],{"class":280,"line":567},[278,54067,54068],{},"    pen.shapesize(settings.INNER_SIZE_MULTIPLIER,\n",[278,54070,54071],{"class":280,"line":577},[278,54072,54073],{},"                  settings.INNER_SIZE_MULTIPLIER, settings.INNER_OUTLINE)\n",[278,54075,54076],{"class":280,"line":587},[278,54077,292],{"emptyLinePlaceholder":291},[278,54079,54080],{"class":280,"line":597},[278,54081,54082],{},"    tilt = 0\n",[278,54084,54085],{"class":280,"line":608},[278,54086,54087],{},"    if tile.shape() == 'diamond':\n",[278,54089,54090],{"class":280,"line":614},[278,54091,54092],{},"        pen.shape('square')\n",[278,54094,54095],{"class":280,"line":620},[278,54096,54097],{},"        pen.tilt(45)\n",[278,54099,54100],{"class":280,"line":625},[278,54101,54102],{},"        tilt = -45\n",[278,54104,54105],{"class":280,"line":640},[278,54106,36178],{},[278,54108,54109],{"class":280,"line":663},[278,54110,54111],{},"        pen.shape(tile.shape())\n",[278,54113,54114],{"class":280,"line":669},[278,54115,54116],{},"        if tile.shape() == 'triangle':\n",[278,54118,54119],{"class":280,"line":680},[278,54120,54121],{},"            pen.tilt(90)\n",[278,54123,54124],{"class":280,"line":686},[278,54125,54126],{},"            tilt = -90\n",[278,54128,54129],{"class":280,"line":1334},[278,54130,292],{"emptyLinePlaceholder":291},[278,54132,54133],{"class":280,"line":1375},[278,54134,54040],{},[278,54136,54137],{"class":280,"line":1381},[278,54138,54139],{},"    pen.tilt(tilt)\n",[278,54141,54142],{"class":280,"line":1386},[278,54143,292],{"emptyLinePlaceholder":291},[278,54145,54146],{"class":280,"line":1394},[278,54147,54148],{},"    tile_id = tile.id()\n",[278,54150,54151],{"class":280,"line":1406},[278,54152,54153],{},"    connections = tile.connections()\n",[278,54155,54156],{"class":280,"line":1423},[278,54157,54158],{},"    if (tile_id[0], tile_id[1] + 1) in connections:\n",[278,54160,54161],{"class":280,"line":1432},[278,54162,54163],{},"        pen.goto(pos[0] + settings.OUTER_TILE_SIZE \u002F 2, pos[1])\n",[278,54165,54166],{"class":280,"line":1437},[278,54167,54063],{},[278,54169,54170],{"class":280,"line":1916},[278,54171,54172],{},"        pen.pendown()\n",[278,54174,54175],{"class":280,"line":1939},[278,54176,54177],{},"        pen.setx(pen.xcor() + settings.TILES_GAP)\n",[278,54179,54180],{"class":280,"line":1949},[278,54181,54182],{},"        pen.penup()\n",[278,54184,54185],{"class":280,"line":1954},[278,54186,54187],{},"    if (tile_id[0] + 1, tile_id[1]) in connections:\n",[278,54189,54190],{"class":280,"line":1959},[278,54191,54192],{},"        pen.goto(pos[0], pos[1] + settings.OUTER_TILE_SIZE \u002F 2)\n",[278,54194,54195],{"class":280,"line":1985},[278,54196,54063],{},[278,54198,54199],{"class":280,"line":1990},[278,54200,54172],{},[278,54202,54203],{"class":280,"line":1997},[278,54204,54205],{},"        pen.sety(pen.ycor() + settings.TILES_GAP)\n",[278,54207,54208],{"class":280,"line":2006},[278,54209,54182],{},[53024,54211,4796,54213,35414],{"id":54212},"the-on_screen_click-method",[59,54214,54215],{},"on_screen_click",[11,54217,54218],{},"This is the method which gets called when we click anywhere on the screen. It gives us the (x, y) co-ordinates of the point where the click occurred.",[269,54220,54222],{"className":35072,"code":54221,"language":35074,"meta":274,"style":274},"def on_screen_click(self, x, y):\n    # Step size is nothing but one tile size + the gap \n    # between two consecutive tiles\n    extreme_x = (settings.MAX_COLS - 1) \u002F 2 * \\\n        settings.STEP_SIZE + settings.OUTER_TILE_SIZE \u002F 2\n    extreme_y = (settings.MAX_ROWS - 1) \u002F 2 * \\\n        settings.STEP_SIZE + settings.OUTER_TILE_SIZE \u002F 2\n\n    # If the click is within the tiles area, then only we'll proceed further\n    if -extreme_x \u003C= x \u003C= extreme_x and -extreme_y \u003C= y \u003C= extreme_y:\n        clicked_tile_id = None\n        # To proceed further, we only look at the clickable tiles \n        for clickable in self.clickables:\n            if self.tiles[clickable[1]][clickable[0]].in_bounds(x, y):\n                clicked_tile_id = clickable\n                break\n\n        if clicked_tile_id:\n            # Increment the total moves if we've a valid tile click\n            self.moves += 1\n            self.handle_tile_click(clicked_tile_id)\n",[59,54223,54224,54229,54234,54239,54244,54249,54254,54258,54262,54267,54272,54277,54282,54287,54292,54297,54301,54305,54310,54315,54320],{"__ignoreMap":274},[278,54225,54226],{"class":280,"line":281},[278,54227,54228],{},"def on_screen_click(self, x, y):\n",[278,54230,54231],{"class":280,"line":288},[278,54232,54233],{},"    # Step size is nothing but one tile size + the gap \n",[278,54235,54236],{"class":280,"line":295},[278,54237,54238],{},"    # between two consecutive tiles\n",[278,54240,54241],{"class":280,"line":316},[278,54242,54243],{},"    extreme_x = (settings.MAX_COLS - 1) \u002F 2 * \\\n",[278,54245,54246],{"class":280,"line":322},[278,54247,54248],{},"        settings.STEP_SIZE + settings.OUTER_TILE_SIZE \u002F 2\n",[278,54250,54251],{"class":280,"line":327},[278,54252,54253],{},"    extreme_y = (settings.MAX_ROWS - 1) \u002F 2 * \\\n",[278,54255,54256],{"class":280,"line":340},[278,54257,54248],{},[278,54259,54260],{"class":280,"line":349},[278,54261,292],{"emptyLinePlaceholder":291},[278,54263,54264],{"class":280,"line":375},[278,54265,54266],{},"    # If the click is within the tiles area, then only we'll proceed further\n",[278,54268,54269],{"class":280,"line":386},[278,54270,54271],{},"    if -extreme_x \u003C= x \u003C= extreme_x and -extreme_y \u003C= y \u003C= extreme_y:\n",[278,54273,54274],{"class":280,"line":397},[278,54275,54276],{},"        clicked_tile_id = None\n",[278,54278,54279],{"class":280,"line":408},[278,54280,54281],{},"        # To proceed further, we only look at the clickable tiles \n",[278,54283,54284],{"class":280,"line":433},[278,54285,54286],{},"        for clickable in self.clickables:\n",[278,54288,54289],{"class":280,"line":454},[278,54290,54291],{},"            if self.tiles[clickable[1]][clickable[0]].in_bounds(x, y):\n",[278,54293,54294],{"class":280,"line":475},[278,54295,54296],{},"                clicked_tile_id = clickable\n",[278,54298,54299],{"class":280,"line":496},[278,54300,35877],{},[278,54302,54303],{"class":280,"line":505},[278,54304,292],{"emptyLinePlaceholder":291},[278,54306,54307],{"class":280,"line":516},[278,54308,54309],{},"        if clicked_tile_id:\n",[278,54311,54312],{"class":280,"line":527},[278,54313,54314],{},"            # Increment the total moves if we've a valid tile click\n",[278,54316,54317],{"class":280,"line":533},[278,54318,54319],{},"            self.moves += 1\n",[278,54321,54322],{"class":280,"line":539},[278,54323,54324],{},"            self.handle_tile_click(clicked_tile_id)\n",[53024,54326,4796,54328,35414],{"id":54327},"the-handle_tile_click-method",[59,54329,54330],{},"handle_tile_click",[11,54332,54333],{},"Here we take care of the tile click by deleting that tile and the other connected tiles.",[269,54335,54337],{"className":35072,"code":54336,"language":35074,"meta":274,"style":274},"def handle_tile_click(self, tile_id):\n    # Find out the connection group which this tile belongs to\n    for connections in self.connection_groups:\n        if tile_id in connections:\n            # We'll be deleting the tiles from top to bottom so we sort \n            # in reverse order. The reason is the same, we don't want \n            # to delete a lower row index tile and then find out that we\n            # need to delete the above one as well\n            tiles_to_remove = sorted(connections, reverse=True)\n\n            # Make all the nodes unclickable as we'll be reprocessing\n            # all the remaining tiles\n            for clickable in self.clickables:\n                self.tiles[clickable[1]][clickable[0]].clickable(False)\n\n            for tile_to_remove in tiles_to_remove:\n                # using pop() to get the removed item so that we can add\n                # it to the cache\n                tile = self.tiles[tile_to_remove[1]].pop(tile_to_remove[0])\n                self.cache.append(tile)\n\n                # After removing a tile, we need to change the tile_id (basically \n                # the row index) of all the tiles above it\n                # Notice the appropriate use of row and col indices while getting \n                # the tile from tiles list, and while using it as tile_id (opposite)\n                for row in range(tile_to_remove[0], len(self.tiles[tile_to_remove[1]])):\n                    self.tiles[tile_to_remove[1]][row].set_tile_props(\n                        (row, tile_to_remove[1]))\n            break # if we found the appropriate connection_group then need to break\n\n    # Clear everything as we need to remake the connections and redraw the board\n    self.clickables.clear()\n    self.connection_groups.clear()\n    self.pen.clear()\n\n    self.draw_board()\n",[59,54338,54339,54344,54349,54353,54357,54362,54367,54372,54377,54381,54385,54390,54395,54400,54405,54409,54413,54418,54423,54428,54433,54437,54442,54447,54452,54457,54462,54467,54472,54477,54481,54486,54491,54496,54501,54505],{"__ignoreMap":274},[278,54340,54341],{"class":280,"line":281},[278,54342,54343],{},"def handle_tile_click(self, tile_id):\n",[278,54345,54346],{"class":280,"line":288},[278,54347,54348],{},"    # Find out the connection group which this tile belongs to\n",[278,54350,54351],{"class":280,"line":295},[278,54352,53842],{},[278,54354,54355],{"class":280,"line":316},[278,54356,51602],{},[278,54358,54359],{"class":280,"line":322},[278,54360,54361],{},"            # We'll be deleting the tiles from top to bottom so we sort \n",[278,54363,54364],{"class":280,"line":327},[278,54365,54366],{},"            # in reverse order. The reason is the same, we don't want \n",[278,54368,54369],{"class":280,"line":340},[278,54370,54371],{},"            # to delete a lower row index tile and then find out that we\n",[278,54373,54374],{"class":280,"line":349},[278,54375,54376],{},"            # need to delete the above one as well\n",[278,54378,54379],{"class":280,"line":375},[278,54380,51617],{},[278,54382,54383],{"class":280,"line":386},[278,54384,292],{"emptyLinePlaceholder":291},[278,54386,54387],{"class":280,"line":397},[278,54388,54389],{},"            # Make all the nodes unclickable as we'll be reprocessing\n",[278,54391,54392],{"class":280,"line":408},[278,54393,54394],{},"            # all the remaining tiles\n",[278,54396,54397],{"class":280,"line":433},[278,54398,54399],{},"            for clickable in self.clickables:\n",[278,54401,54402],{"class":280,"line":454},[278,54403,54404],{},"                self.tiles[clickable[1]][clickable[0]].clickable(False)\n",[278,54406,54407],{"class":280,"line":475},[278,54408,292],{"emptyLinePlaceholder":291},[278,54410,54411],{"class":280,"line":496},[278,54412,51650],{},[278,54414,54415],{"class":280,"line":505},[278,54416,54417],{},"                # using pop() to get the removed item so that we can add\n",[278,54419,54420],{"class":280,"line":516},[278,54421,54422],{},"                # it to the cache\n",[278,54424,54425],{"class":280,"line":527},[278,54426,54427],{},"                tile = self.tiles[tile_to_remove[1]].pop(tile_to_remove[0])\n",[278,54429,54430],{"class":280,"line":533},[278,54431,54432],{},"                self.cache.append(tile)\n",[278,54434,54435],{"class":280,"line":539},[278,54436,292],{"emptyLinePlaceholder":291},[278,54438,54439],{"class":280,"line":545},[278,54440,54441],{},"                # After removing a tile, we need to change the tile_id (basically \n",[278,54443,54444],{"class":280,"line":551},[278,54445,54446],{},"                # the row index) of all the tiles above it\n",[278,54448,54449],{"class":280,"line":557},[278,54450,54451],{},"                # Notice the appropriate use of row and col indices while getting \n",[278,54453,54454],{"class":280,"line":567},[278,54455,54456],{},"                # the tile from tiles list, and while using it as tile_id (opposite)\n",[278,54458,54459],{"class":280,"line":577},[278,54460,54461],{},"                for row in range(tile_to_remove[0], len(self.tiles[tile_to_remove[1]])):\n",[278,54463,54464],{"class":280,"line":587},[278,54465,54466],{},"                    self.tiles[tile_to_remove[1]][row].set_tile_props(\n",[278,54468,54469],{"class":280,"line":597},[278,54470,54471],{},"                        (row, tile_to_remove[1]))\n",[278,54473,54474],{"class":280,"line":608},[278,54475,54476],{},"            break # if we found the appropriate connection_group then need to break\n",[278,54478,54479],{"class":280,"line":614},[278,54480,292],{"emptyLinePlaceholder":291},[278,54482,54483],{"class":280,"line":620},[278,54484,54485],{},"    # Clear everything as we need to remake the connections and redraw the board\n",[278,54487,54488],{"class":280,"line":625},[278,54489,54490],{},"    self.clickables.clear()\n",[278,54492,54493],{"class":280,"line":640},[278,54494,54495],{},"    self.connection_groups.clear()\n",[278,54497,54498],{"class":280,"line":663},[278,54499,54500],{},"    self.pen.clear()\n",[278,54502,54503],{"class":280,"line":669},[278,54504,292],{"emptyLinePlaceholder":291},[278,54506,54507],{"class":280,"line":680},[278,54508,53212],{},[32,54510,4796,54512,10945],{"id":54511},"the-settings-module",[59,54513,52481],{},[269,54515,54517],{"className":35072,"code":54516,"language":35074,"meta":274,"style":274},"DEF_TILE_SIZE = 20 # This is the default turtle size\n\nMAX_COLS = 5\nMAX_ROWS = 5\n\nTILES_GAP = 12\nOUTER_SIZE_MULTIPLIER = 2 # We make the tile 2X the default turtle size\nOUTER_OUTLINE = 4\n\nINNER_SIZE_MULTIPLIER = 0.6 # For the inner shapes (triangle, circle etc.)\nINNER_OUTLINE = 1\n\nCOLORS = ['hot pink', 'white', 'yellow', 'turquoise']\nINNER_SHAPES = ['triangle', 'square', 'circle', 'diamond']\n\nOUTER_TILE_SIZE = DEF_TILE_SIZE * OUTER_SIZE_MULTIPLIER\nINNER_TILE_SIZE = DEF_TILE_SIZE * INNER_SIZE_MULTIPLIER\n\nSTEP_SIZE = OUTER_TILE_SIZE + TILES_GAP\n\ndef canv_width():\n    return MAX_COLS * OUTER_TILE_SIZE + (MAX_COLS - 1) * TILES_GAP\n\ndef canv_height():\n    return MAX_ROWS * OUTER_TILE_SIZE + (MAX_ROWS - 1) * TILES_GAP\n\ndef win_width():\n    return canv_width() + 4 * OUTER_TILE_SIZE\n\ndef win_height():\n    return canv_height() + 8 * OUTER_TILE_SIZE\n",[59,54518,54519,54524,54528,54532,54536,54540,54545,54550,54555,54559,54564,54569,54573,54578,54583,54587,54592,54597,54601,54606,54610,54615,54620,54624,54629,54634,54638,54643,54648,54652,54657],{"__ignoreMap":274},[278,54520,54521],{"class":280,"line":281},[278,54522,54523],{},"DEF_TILE_SIZE = 20 # This is the default turtle size\n",[278,54525,54526],{"class":280,"line":288},[278,54527,292],{"emptyLinePlaceholder":291},[278,54529,54530],{"class":280,"line":295},[278,54531,51327],{},[278,54533,54534],{"class":280,"line":316},[278,54535,51332],{},[278,54537,54538],{"class":280,"line":322},[278,54539,292],{"emptyLinePlaceholder":291},[278,54541,54542],{"class":280,"line":327},[278,54543,54544],{},"TILES_GAP = 12\n",[278,54546,54547],{"class":280,"line":340},[278,54548,54549],{},"OUTER_SIZE_MULTIPLIER = 2 # We make the tile 2X the default turtle size\n",[278,54551,54552],{"class":280,"line":349},[278,54553,54554],{},"OUTER_OUTLINE = 4\n",[278,54556,54557],{"class":280,"line":375},[278,54558,292],{"emptyLinePlaceholder":291},[278,54560,54561],{"class":280,"line":386},[278,54562,54563],{},"INNER_SIZE_MULTIPLIER = 0.6 # For the inner shapes (triangle, circle etc.)\n",[278,54565,54566],{"class":280,"line":397},[278,54567,54568],{},"INNER_OUTLINE = 1\n",[278,54570,54571],{"class":280,"line":408},[278,54572,292],{"emptyLinePlaceholder":291},[278,54574,54575],{"class":280,"line":433},[278,54576,54577],{},"COLORS = ['hot pink', 'white', 'yellow', 'turquoise']\n",[278,54579,54580],{"class":280,"line":454},[278,54581,54582],{},"INNER_SHAPES = ['triangle', 'square', 'circle', 'diamond']\n",[278,54584,54585],{"class":280,"line":475},[278,54586,292],{"emptyLinePlaceholder":291},[278,54588,54589],{"class":280,"line":496},[278,54590,54591],{},"OUTER_TILE_SIZE = DEF_TILE_SIZE * OUTER_SIZE_MULTIPLIER\n",[278,54593,54594],{"class":280,"line":505},[278,54595,54596],{},"INNER_TILE_SIZE = DEF_TILE_SIZE * INNER_SIZE_MULTIPLIER\n",[278,54598,54599],{"class":280,"line":516},[278,54600,292],{"emptyLinePlaceholder":291},[278,54602,54603],{"class":280,"line":527},[278,54604,54605],{},"STEP_SIZE = OUTER_TILE_SIZE + TILES_GAP\n",[278,54607,54608],{"class":280,"line":533},[278,54609,292],{"emptyLinePlaceholder":291},[278,54611,54612],{"class":280,"line":539},[278,54613,54614],{},"def canv_width():\n",[278,54616,54617],{"class":280,"line":545},[278,54618,54619],{},"    return MAX_COLS * OUTER_TILE_SIZE + (MAX_COLS - 1) * TILES_GAP\n",[278,54621,54622],{"class":280,"line":551},[278,54623,292],{"emptyLinePlaceholder":291},[278,54625,54626],{"class":280,"line":557},[278,54627,54628],{},"def canv_height():\n",[278,54630,54631],{"class":280,"line":567},[278,54632,54633],{},"    return MAX_ROWS * OUTER_TILE_SIZE + (MAX_ROWS - 1) * TILES_GAP\n",[278,54635,54636],{"class":280,"line":577},[278,54637,292],{"emptyLinePlaceholder":291},[278,54639,54640],{"class":280,"line":587},[278,54641,54642],{},"def win_width():\n",[278,54644,54645],{"class":280,"line":597},[278,54646,54647],{},"    return canv_width() + 4 * OUTER_TILE_SIZE\n",[278,54649,54650],{"class":280,"line":608},[278,54651,292],{"emptyLinePlaceholder":291},[278,54653,54654],{"class":280,"line":614},[278,54655,54656],{},"def win_height():\n",[278,54658,54659],{"class":280,"line":620},[278,54660,54661],{},"    return canv_height() + 8 * OUTER_TILE_SIZE\n",[11,54663,54664],{},"And that's it. The game is done.",[40,54666],{"url":54667},"https:\u002F\u002Fmedia.giphy.com\u002Fmedia\u002F3oKIPf3C7HqqYBVcCk\u002Fgiphy.gif",[11,54669,54670],{},"Thanks for sticking through the article. Please feel free to reach out if you've any questions, or if you find any mistake anywhere.",[11,54672,54673],{},"Have fun playing the game :-)",[3065,54675,24393],{},{"title":274,"searchDepth":288,"depth":288,"links":54677},[54678,54679,54680],{"id":52303,"depth":288,"text":52304},{"id":52325,"depth":288,"text":52326},{"id":45825,"depth":288,"text":52335,"children":54681},[54682,54683,54684,54686,54688],{"id":52350,"depth":295,"text":52351},{"id":52489,"depth":295,"text":52490},{"id":52692,"depth":295,"text":54685},"The Tile class",{"id":53016,"depth":295,"text":54687},"The Game class",{"id":54511,"depth":295,"text":54689},"The settings module","\u002Fimages\u002Fposts\u002Fcreating-puzzle-game-using-python-turtle-1\u002F_9kdsc3MI-dbef1a3c76.jpg","2022-11-09T19:01:41.374Z","Learn how to make a tiles puzzle game using classes and objects with Python Turtle module.","claa0c7zy000108kx8sa03omt",{},"\u002Fcreating-puzzle-game-using-python-turtle-1",{"title":52291,"description":54692},"creating-puzzle-game-using-python-turtle-1",[35074,52286,49708,52287,54699],"python-turtle","gailAW2eqM5yKRf4HDJQPIVazrO1BRMr43gAFz1cIeA",{"id":54702,"title":54703,"body":54704,"cover":55242,"date":55243,"description":55244,"draft":3086,"extension":3087,"hashnodeId":55245,"meta":55246,"navigation":291,"path":55247,"seo":55248,"slug":55249,"stem":55249,"tags":55250,"__hash__":55253},"posts\u002Fnew-years-promise-to-my-son.md","Working on my new year's promise to my son",{"type":8,"value":54705,"toc":55219},[54706,54710,54713,54717,54720,54724,54727,54731,54734,54738,54741,54745,54752,54756,54790,54794,54800,54806,54819,54825,54831,54837,54840,54846,54849,54855,54861,54871,54877,54880,54883,54889,54892,54902,54905,54910,54914,54923,54937,54946,54948,54951,54960,54967,54970,54973,54979,55000,55010,55013,55019,55032,55036,55039,55043,55053,55056,55062,55072,55078,55081,55087,55090,55096,55099,55105,55116,55119,55126,55130,55147,55151,55155,55161,55165,55171,55177,55181,55187,55190,55196,55198,55201,55205],[24,54707,54709],{"id":54708},"the-back-story","The back story",[11,54711,54712],{},"There is always one, isn't it? Let's start from the beginning. We're in the dying days of December 2021. My son will be turning 8 in a couple of months. In his mind 8 is some sort of milestone, and he wants to get started with his pocket money. So we do a discussion on what he understands about money, and how he can make more if he saves. He is excited, and wants me to make an app for him to handle all this. And that is where the new year promise cum resolution kicks in.",[53024,54714,54716],{"id":54715},"fast-forward-to-march-april-2022","Fast forward to March \u002F April 2022",[11,54718,54719],{},"The birthday has come and gone. Not even a single line of code has been written for the app. That's how new year resolutions are supposed to be, right? He reminds me one of these days, so to make good on my promise, I take a Nuxt template, create a couple of cards and then...crickets!! :-)",[53024,54721,54723],{"id":54722},"september-2022","September 2022",[11,54725,54726],{},"I see this tweet from AWS Amplify of a hackathon, snd that is why I'm writing this post. This hackathon marks a lot of firsts for me: first tech blog post (though I do have some blogging experience but that is another lifetime, and non tech), first time working with react, first time working with Amplify Studio, first encounter with AppSync \u002F Graphql, and so on...",[24,54728,54730],{"id":54729},"the-what","The what",[11,54732,54733],{},"A basic app where you can create piggy bank accounts for your kids. Allows you to add \u002F deduct money on ad-hoc basis. You can also configure their pocket money amount, and schedule, for auto credit to their account. A simple dashboard showing the status of different accounts and transactions, as well as basic settings page.",[24,54735,54737],{"id":54736},"the-journey","The journey",[11,54739,54740],{},"Initial thought was to have more bells and whistles in the app but that is not how products are built. You keep cutting down the scope of work until you arrive at an MVP or MUP (minimum usable product, remember my customer is my son ;-)), and this project is no exception to the rule. For me, creating an account with some initial balance, add\u002Fremove ad-hoc money and auto handling of pocket money credit per the defined schedule is a good place to start.",[32,54742,54744],{"id":54743},"deciding-the-stack","Deciding the stack",[11,54746,54747,54748,54751],{},"As the backend is already decided per the hackathon rules, the decision was for the frontend. I am comfortable with Vue\u002FNuxt ecosystem but have been wanting to start React. This project seemed an ideal candidate for this exploration, more so with the promise of ",[59,54749,54750],{},"ui-react"," components library from the Amplify team. Wanted to try out Amplify's Figma integration also, but finally decided against it as my intention was to be with React + Amplify ui-react components more.",[32,54753,54755],{"id":54754},"the-initial-steps","The initial steps",[123,54757,54758,54767,54776,54779],{},[74,54759,54760,54761,54766],{},"Went through the react docs, tried out their ",[47,54762,54765],{"href":54763,"rel":54764},"https:\u002F\u002Freactjs.org\u002Ftutorial\u002Ftutorial.html",[51],"intro to react tutorial"," (along with the improvements suggested at the end).",[74,54768,54769,54770,54775],{},"Had a go at ",[47,54771,54774],{"href":54772,"rel":54773},"https:\u002F\u002Fdocs.amplify.aws\u002Fconsole\u002Fadminui\u002Fstart\u002F",[51],"Amplify Studio docs",". Setup a local project following the instructions.",[74,54777,54778],{},"Created a new AWS account to skip the sandbox experience. Went to Amplify, created a new backend project and launched Amplify Studio from there.",[74,54780,54781,54782,54785,54786,54789],{},"Create basic data models and pulled the whole thing to my local machine using ",[59,54783,54784],{},"amplify pull",". It asks for your AWS credentials and after logging in, you can select the project which you created in step 3 (or you can directly pull by mentioning your appId using ",[59,54787,54788],{},"--appid"," option).",[32,54791,54793],{"id":54792},"the-data-models","The data models",[11,54795,54796,54799],{},[94,54797,54798],{},"The user model:"," The first model to be decided was the account owner's (the parent). After a little bit of tinkering around, settled with the following model",[11,54801,54802],{},[3135,54803],{"alt":54804,"src":54805},"Screen Shot 2022-10-01 at 2.30.16 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FFVhtXQQiQ-29b99bd6cd.png",[11,54807,54808,54809,54812,54813,54818],{},"where Currency is an enum supporting some major currencies. This was needed for showing the account balances and transactions with the corresponding currency symbol. The enum values are important (",[59,54810,54811],{},"ISO 4217 currency codes",") as we'll be using ",[47,54814,54817],{"href":54815,"rel":54816},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FIntl\u002FNumberFormat",[51],"Intl.NumberFormat"," to get the correct symbols and formatting.",[11,54820,54821],{},[3135,54822],{"alt":54823,"src":54824},"Screen Shot 2022-10-01 at 2.30.53 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FIQcf8WgIM-995c9187d3.png",[11,54826,54827,54830],{},[94,54828,54829],{},"The child model:"," Child model had some more fields as it needed to have a balance (remember we're creating a bank account?). Then it needed a pocket money amount and a schedule for the same (instead of enforcing my schedule, we're giving the parent a choice to select their own). This needs to be in the child model as pocket money and schedule may differ based on kids' ages",[11,54832,54833],{},[3135,54834],{"alt":54835,"src":54836},"Screen Shot 2022-10-01 at 2.31.35 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FwIXy2bH65-9031d7ef06.png",[11,54838,54839],{},"where Frequency is again an enum with the following values",[11,54841,54842],{},[3135,54843],{"alt":54844,"src":54845},"Screen Shot 2022-10-01 at 2.31.13 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002Fx12Xhzj_G-d571178726.png",[11,54847,54848],{},"There are some extra fields present in the model and we'll come to those in a bit.",[11,54850,54851,54854],{},[94,54852,54853],{},"The transaction model:"," This is quite simple. The transaction amount and a comment for the transaction.",[11,54856,54857],{},[3135,54858],{"alt":54859,"src":54860},"Screen Shot 2022-10-01 at 2.56.05 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FwzIqcrGex-308f6969b4.png",[11,54862,54863,54864,19634,54867,54870],{},"This model also has 2 extra fields called ",[59,54865,54866],{},"userID",[59,54868,54869],{},"childID",". The thing is: when someone is making a transaction, that transaction belongs to him so we need a reference to the user who made this transaction. Also, we'll be making the transaction for a particular child so we need a reference to the child as well.",[11,54872,54873,54874,54876],{},"Similarly, a child needs to have a reference to their parent, that is why we had a ",[59,54875,54866],{}," field in the child model.",[11,54878,54879],{},"These relationships are achieved through relationships in Amplify Studio. You just tell the data modeling, what kind of relationship you want and it will do the heavy lifting on our behalf. It is quite a breeze to design models and create relationships with Amplify Studio.",[11,54881,54882],{},"In our case, a parent can have multiple children, as well as have multiple transactions, so we did the following",[11,54884,54885],{},[3135,54886],{"alt":54887,"src":54888},"Screen Shot 2022-10-01 at 3.07.28 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FClvrcxV1x-1639850047.png",[11,54890,54891],{},"We created similar relationships for user -> transactions, as well as child -> transactions also. After doing so we can see it in our models too",[11,54893,54894,54898],{},[3135,54895],{"alt":54896,"src":54897},"Screen Shot 2022-10-01 at 3.12.29 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FO81uzoG9y-8169e1c1ca.png",[3135,54899],{"alt":54900,"src":54901},"Screen Shot 2022-10-01 at 3.12.13 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FAWLTuYD1x-b1d254af9e.png",[11,54903,54904],{},"Enough with modeling. Time to save and deploy. It takes a bit to deploy, maybe take a break and get a cup of coffee? :-)",[11,54906,54907,54908,183],{},"Once it is deployed you can pull these changes to your local machine using ",[59,54909,54784],{},[32,54911,54913],{"id":54912},"frontend-awaits","Frontend awaits",[11,54915,54916,54917,54922],{},"Now that we've our datastore, we can create the UI and integrate both. This is where ",[47,54918,54921],{"href":54919,"rel":54920},"https:\u002F\u002Fui.docs.amplify.aws\u002Freact\u002Fgetting-started\u002Fintroduction",[51],"Amplify UI"," comes in. Follow the getting started guide and the code given alongside. It is enough to get you started quickly, the intricacies we can always dive into later on.",[11,54924,54925,54926,54929,54930,54932,54933,54936],{},"Saw the usage of ",[59,54927,54928],{},"useState"," there, so went on another tangent and read through the react docs on ",[59,54931,54928],{}," as well as ",[59,54934,54935],{},"useEffect",". This is how I prefer to learn, you get the initial basic idea first, start building, then you hit a roadblock or see something interesting and you dive deeper.",[11,54938,54939,54940,54945],{},"I needed different routes for different screens. Saw the ",[47,54941,54944],{"href":54942,"rel":54943},"https:\u002F\u002Fui.docs.amplify.aws\u002Freact\u002Fguides\u002Fauth-protected",[51],"protected routes"," guide on Amplify UI, went on another tangent to learn little bit of react-router.  All this while our project keeps on changing by trying out these different examples.",[32,54947,42736],{"id":42735},[11,54949,54950],{},"This didn't require any UI as it comes prebuilt. But to use it we need to set it up first using Amplify Studio. I needed the user's name as part of sign up (see user data model), so I configured that in the sign up attributes section. After deploying and pulling the changes I could create an account and login.",[11,54952,54953,54954,54959],{},"Now I needed to copy the user details from Cognito to my datastore. Two choices here: do it form the frontend or the backend. For frontend we need to find a proper hook to latch on to for doing this. Authenticator provides ",[47,54955,54958],{"href":54956,"rel":54957},"https:\u002F\u002Fui.docs.amplify.aws\u002Freact\u002Fconnected-components\u002Fauthenticator\u002Fcustomization#override-function-calls",[51],"some such hooks"," which are suitable for the purpose.",[11,54961,54962,54963,54966],{},"I picked ",[59,54964,54965],{},"handleSignUp"," for my purpose and created user in datastore. All fine and good. But then I had another concern, till now I hadn't given authorization and thought. My children and transactions are my own, other users should not be able to read \u002F change these.",[11,54968,54969],{},"So again I went to data setup page in Amplify Studio. This time I saw things which I had missed in my initial visits. When you click on a particular field in nay model, you can mark it as required \u002F optional. Also when you click on any model name, you can decide who is authorized to read \u002Fchange it.",[11,54971,54972],{},"Since we'd configured Cognito, we had the option to restrict down the access to only the owner (which is what I wanted). Made some changes (ownerField) in the graphql schema (found at amplify -> backend -> api -> \u003CAPP_NAME> -> schema.graphql) to reflect this reality (These changes are not necessarily needed)",[269,54974,54977],{"className":54975,"code":54976,"language":4582},[49795],"type User @model @auth(rules: [{allow: owner, ownerField: \"id\"}]) {\n  id: ID!\n  name: String!\n  email: AWSEmail!\n  currency: Currency\n  Transactions: [Child] @hasMany(indexName: \"byUser\", fields: [\"id\"])\n  Children: [Child] @hasMany(indexName: \"byUser\", fields: [\"id\"])\n  onBoarded: Boolean\n}\n\ntype Transaction @model @auth(rules: [{allow: owner, ownerField: \"userID\"}]) {\n  id: ID!\n  amount: Float!\n  comment: String!\n  userID: ID! @index(name: \"byUser\")\n  childID: ID! @index(name: \"byChild\")\n} \u002F\u002F and same for child model as well\n",[59,54978,54976],{"__ignoreMap":274},[11,54980,54981,54982,54985,54986,54989,54990,54995,54996,54999],{},"Anyhow, now that we've restricted access I found another issue. You can't set an ",[59,54983,54984],{},"**Id**"," for a user through datastore. So now I've one ",[94,54987,54988],{},"user id"," from cognito and another from datastore. I wanted both to have the same id, so here come the hallowed lambda functions. After a bit of searching, found ",[47,54991,54994],{"href":54992,"rel":54993},"https:\u002F\u002Fdocs.amplify.aws\u002Fcli\u002Fusage\u002Flambda-triggers\u002F",[51],"lambda triggers",". ",[59,54997,54998],{},"Post Confirmation"," hook suited my needs.",[11,55001,55002,55003,55005,55006,55009],{},"So, I removed the ",[59,55004,54965],{}," code from earlier and created the Post Confirmation trigger using ",[59,55007,55008],{},"amplify add function"," command. It has one prebuilt template of adding user to a group (which you should select. If you select custom then it leaves you out hanging to dry with no hand holding. I had to redo the steps 2-3 times to realize this, better to have some starting files as compared to you yourself figuring out where to add the files).",[11,55011,55012],{},"Then updated the function and added correct permissions. We want to write to datastore, which, in a loose sense, is a layer on top of DynamoDB (so we need to give DynamoDB write permission to our lambda function). Here is the code for creating a user:",[269,55014,55017],{"className":55015,"code":55016,"language":4582},[49795],"const aws = require('aws-sdk');\nconst docClient = new aws.DynamoDB.DocumentClient();\nconst tableName = process.env.API_\u003CAPP_NAME>_USERTABLE_NAME;\n\nexports.handler = async (event, context) => {\n  const date = new Date()\n\n  const params = {\n    TableName: tableName,\n    Item: {\n      __typename: 'User',\n      id: event.request.userAttributes.sub,\n      name: event.request.userAttributes.name,\n      email: event.request.userAttributes.email,\n      createdAt: date.toISOString(),\n      updatedAt: date.toISOString(),\n      _version: 1,\n      _lastChangedAt: date.getTime()\n    },\n  };\n\n  try {\n    const result = await docClient.put(params).promise();\n    console.log(\n      `Saved user data in DB`, JSON.stringify(result, null, 2)\n    );\n  } catch (err) {\n    console.error(\n      `Unable to save user data for ${event.request.userAttributes.sub}. Error JSON: `,\n      JSON.stringify(err, null, 2)\n    );\n  }\n\n  return event;\n};\n",[59,55018,55016],{"__ignoreMap":274},[11,55020,55021,55022,919,55027],{},"__typename, _version, _lastChangedAt, createdAt, updatedAt fields are automatically created in the backend when you try to create a user through datastore, so we need to maintain the same structure. The above code was taken from ",[47,55023,55026],{"href":55024,"rel":55025},"https:\u002F\u002Fhashnode.com\u002F@aspittel",[51],"Ali Spittel's Hashnode profile",[47,55028,55031],{"href":55029,"rel":55030},"https:\u002F\u002Fgithub.com\u002Faspittel\u002Famplify-workshop",[51],"GitHub repo",[32,55033,55035],{"id":55034},"other-screens","Other screens",[11,55037,55038],{},"Once the auth piece was sorted out, quickly created the onboarding screens (which they see when they create an account), the dashboard, transaction form and the settings screens.",[32,55040,55042],{"id":55041},"auto-credit-of-pocket-money","Auto credit of pocket money",[11,55044,55045,55046,55048,55049,55052],{},"One major event remaining was the auto credit of pocket money to a child's account. Created another lambda function which runs on a schedule (",[59,55047,55008],{}," gives you such a choice). This function runs everyday at 12:00 AM GMT\u002FUTC, fetches all the eligible children (this is where the last data field ",[59,55050,55051],{},"nextMoneyAt"," comes into play) whose pocket monies needs to be credited, and then creates transactions for all of them on their behalf. It also updates their balances at the same time, so for every eligible child two graphql queries are executed.",[11,55054,55055],{},"Code to fetch eligible children (timestamp is 12:00 AM GMT in seconds for that day, taking this directly from the lambda event itself. Giving the filter a range of 4 minutes which is not needed, but still...). Creating this lambda taught me everything I know about graphql \u002F appsync (which is of course not much :-))",[269,55057,55060],{"className":55058,"code":55059,"language":4582},[49795],"const getEligibleChildren = async (timestamp) => {\n  const variables = {\n    filter: {\n      nextMoneyAt: { ge: timestamp - 120, le: timestamp + 120 },\n    },\n  };\n\n  const body = JSON.stringify({\n    query: `\n      query List_Children(\n          $filter: ModelChildFilterInput\n          $limit: Int\n          $nextToken: String\n        ) {\n          listChildren(filter: $filter, limit: $limit, nextToken: $nextToken) {\n            items {\n              id\n              name\n              balance\n              nextMoneyAt\n              pocketMoney\n              schedule\n              userID\n              _version\n            }\n            nextToken\n          }\n        }\n        `,\n    operationName: 'List_Children',\n    variables,\n  });\n\n  const req = getSignedRequest(body);\n  const res = await executeReq(req);\n\n  console.log('eligible children for payout: %j', res);\n  return res.data.listChildren.items;\n};\n",[59,55061,55059],{"__ignoreMap":274},[11,55063,55064,55065,919,55068,55071],{},"Where ",[59,55066,55067],{},"getSignedRequest",[59,55069,55070],{},"executeReq"," functions are as shown",[269,55073,55076],{"className":55074,"code":55075,"language":4582},[49795],"const https = require('https');\nconst AWS = require('aws-sdk');\nconst { randomUUID } = require('crypto');\nconst urlParse = require('url').URL;\n\nconst GRAPHQL_ENDPOINT = process.env.API_\u003CAPP_NAME>_GRAPHQLAPIENDPOINTOUTPUT;\n\nconst REGION = process.env.REGION;\nconst endpoint = new urlParse(GRAPHQL_ENDPOINT).hostname.toString();\n\nconst getSignedRequest = (body) => {\n  const req = new AWS.HttpRequest(GRAPHQL_ENDPOINT, REGION);\n  req.method = 'POST';\n  req.path = '\u002Fgraphql';\n  req.headers.host = endpoint;\n  req.headers['Content-Type'] = 'application\u002Fjson';\n  req.body = body;\n\n  console.log('request body is: ', body);\n\n  const signer = new AWS.Signers.V4(req, 'appsync', true);\n  signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());\n\n  return req;\n};\n\nconst executeReq = (req) => {\n  return new Promise((resolve, reject) => {\n    const httpRequest = https.request({ ...req, host: endpoint }, (result) => {\n      let data = '';\n\n      result.on('data', (chunk) => {\n        data += chunk;\n      });\n\n      result.on('end', () => {\n        resolve(JSON.parse(data.toString()));\n      });\n    });\n\n    httpRequest.write(req.body);\n    httpRequest.end();\n  });\n};\n",[59,55077,55075],{"__ignoreMap":274},[11,55079,55080],{},"Code for creating a transaction and updating the balance for a child. Need to pass the existing _version of child model for updating it, else it will be rejected",[269,55082,55085],{"className":55083,"code":55084,"language":4582},[49795],"const depositPocketMoney = (child, timestamp, eventDate) => {\n  const transactionVariables = {\n    input: {\n      id: randomUUID(),\n      amount: child.pocketMoney,\n      comment: '👏 Pocket money added',\n      childID: child.id,\n      userID: child.userID,\n    },\n  };\n\n  const transactionBody = JSON.stringify({\n    query: `\n      mutation Create_Transaction($input: CreateTransactionInput!) {\n          createTransaction(input: $input) {\n            id\n            amount\n            comment\n            userID\n            childID\n            createdAt\n            updatedAt\n            _version\n            _lastChangedAt\n            _deleted\n          }\n        }\n    `,\n    operationName: 'Create_Transaction',\n    variables: transactionVariables,\n  });\n\n  const createTransactionReq = getSignedRequest(transactionBody);\n  const childVariables = {\n    input: {\n      id: child.id,\n      balance: child.balance + child.pocketMoney,\n      _version: child._version,\n    },\n  };\n\n  if (child.schedule === 'DAILY') {\n    childVariables.input.nextMoneyAt = timestamp + 86400;\n  } else if (child.schedule === 'WEEKLY') {\n    childVariables.input.nextMoneyAt = timestamp + 7 * 86400;\n  } else if (child.schedule === 'MONTHLY') {\n    const nextMoneyDate = new Date();\n    nextMoneyDate.setUTCHours(0, 0, 0);\n    nextMoneyDate.setUTCMonth(eventDate.getMonth() + 1, 1);\n    childVariables.input.nextMoneyAt = parseInt(nextMoneyDate.getTime() \u002F 1000);\n  } else {\n    console.error(`Unsupported child payout schedule: ${child.schedule}`);\n  }\n\n  const childUpdateBody = JSON.stringify({\n    query: `\n      mutation Update_Child($input: UpdateChildInput!, $condition: ModelChildConditionInput) {\n        updateChild(input: $input, condition: $condition) {\n          id\n          name\n          balance\n          userID\n          pocketMoney\n          schedule\n          nextMoneyAt\n          createdAt\n          updatedAt\n          _version\n          _lastChangedAt\n          _deleted\n        }\n      }\n    `,\n    operationName: 'Update_Child',\n    variables: childVariables,\n    condition: null,\n  });\n  const childUpdateReq = getSignedRequest(childUpdateBody);\n  return [executeReq(createTransactionReq), executeReq(childUpdateReq)];\n};\n",[59,55086,55084],{"__ignoreMap":274},[11,55088,55089],{},"The important piece here is this (together with balance we need to update the nextMoneyAt field as well, so that the money can be credited in the next cycle)",[269,55091,55094],{"className":55092,"code":55093,"language":4582},[49795],"  if (child.schedule === 'DAILY') {\n    childVariables.input.nextMoneyAt = timestamp + 86400;\n  } else if (child.schedule === 'WEEKLY') {\n    childVariables.input.nextMoneyAt = timestamp + 7 * 86400;\n  } else if (child.schedule === 'MONTHLY') {\n    const nextMoneyDate = new Date();\n    nextMoneyDate.setUTCHours(0, 0, 0);\n    nextMoneyDate.setUTCMonth(eventDate.getMonth() + 1, 1);\n    childVariables.input.nextMoneyAt = parseInt(nextMoneyDate.getTime() \u002F 1000);\n  } else {\n    console.error(`Unsupported child payout schedule: ${child.schedule}`);\n  }\n",[59,55095,55093],{"__ignoreMap":274},[11,55097,55098],{},"Corresponding frontend code to calculated the first payout time (done at the time of onboarding, as well as if the frequency is changed for the child)",[269,55100,55103],{"className":55101,"code":55102,"language":4582},[49795],"const calculateNextPayout = (schedule) => {\n  if (schedule) {\n    const date = new Date();\n    const currDate = date.getDate();\n    const currMonth = date.getMonth();\n\n    const nextMoneyDate = new Date();\n    nextMoneyDate.setUTCHours(0, 0, 0);\n\n    if (schedule === Frequency.DAILY) {\n      nextMoneyDate.setUTCDate(currDate + 1);\n    } else if (schedule === Frequency.WEEKLY) {\n      \u002F\u002F +1 is for getting Monday, doesn't care if today is Sunday,\n      \u002F\u002F next payout will be after 8 days\n      const nextMondayInDays = 7 - date.getDay() + 1;\n      nextMoneyDate.setUTCDate(currDate + nextMondayInDays);\n    } else if (schedule === Frequency.MONTHLY) {\n      nextMoneyDate.setUTCMonth(currMonth + 1, 1);\n    }\n\n    return nextMoneyDate;\n  }\n",[59,55104,55102],{"__ignoreMap":274},[11,55106,55107,55108,55111,55112,4633],{},"One important thing with graphql mutations from lambda functions is: you need to select the required fields (including ",[59,55109,55110],{},"_version, _lastChangedAt, _deleted"," etc) while making the api call, else your datastore in the frontend won't be getting the updates immediately. I spent a lot of time in figuring this out (even though it is written in plain English ",[47,55113,3286],{"href":55114,"rel":55115},"https:\u002F\u002Fdocs.amplify.aws\u002Flib\u002Fdatastore\u002Fhow-it-works\u002Fq\u002Fplatform\u002Fjs\u002F#writing-data-from-the-appsync-console",[51],[24,55117,55118],{"id":38720},"Hosting",[11,55120,55121,55122,55125],{},"Now it is time to make the app live. Simply run ",[59,55123,55124],{},"amplify add hosting"," from the command line, and it will prompt you with different hosting options. I went with \"hosting with amplify console\", configured the github repo for continuous deployment, and done...! The console automatically recognizes that the project is using reactJs, suggests the correct build settings, and finally builds and deploys the app for you. It takes a while to do all this, but saves you a lot of pain later on :-).",[24,55127,55129],{"id":55128},"improvements","Improvements",[123,55131,55132,55135,55138,55141,55144],{},[74,55133,55134],{},"Right now many things are happening from frontend. Like when you create a transaction, child's balance is also updated from there. Ideally it should happen from backend",[74,55136,55137],{},"The pocket money credit happens on a single schedule for all children, pretty soon it will start failing as users grow and lambda starts hitting its limits. Will need to split out the functionality. Also right now not taking care of pagination while querying for eligible children, need to add that.",[74,55139,55140],{},"Pocket money credit is happening only at 12:00 AM GMT. Ideally it should be according to the user's local time.",[74,55142,55143],{},"At present it is not possible to create or delete an account from the settings page. The functionality needs to be added.",[74,55145,55146],{},"Dashboard should show a limited number of transactions, with an option to show more",[24,55148,55150],{"id":55149},"screenshots","Screenshots",[32,55152,55154],{"id":55153},"homepage","Homepage",[11,55156,55157],{},[3135,55158],{"alt":55159,"src":55160},"Screenshot 2022-10-01 at 05-38-29 Kids Piggy App.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FyTwUgFMWs-2a61a5325c.png",[32,55162,55164],{"id":55163},"onboarding","Onboarding",[11,55166,55167],{},[3135,55168],{"alt":55169,"src":55170},"Screen Shot 2022-10-01 at 5.41.40 AM.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FhbKE1Mfr3-c36363673a.png",[11,55172,55173],{},[3135,55174],{"alt":55175,"src":55176},"Screenshot 2022-10-01 at 05-42-50 Kids Piggy App.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FKRY0-11jA-1960b71989.png",[32,55178,55180],{"id":55179},"dashboard","Dashboard",[11,55182,55183],{},[3135,55184],{"alt":55185,"src":55186},"Screenshot 2022-10-01 at 05-37-14 Kids Piggy App.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002F6owtelnqv-c189055561.png",[32,55188,55189],{"id":52481},"Settings",[11,55191,55192],{},[3135,55193],{"alt":55194,"src":55195},"Screenshot 2022-10-01 at 05-37-48 Kids Piggy App.png","\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002Flyq4Ua-UE-51693044ed.png",[24,55197,10634],{"id":10633},[11,55199,55200],{},"I thoroughly enjoyed creating this app with Amplify Studio. Learnt many new things. And finally there is at least one new year resolution which I have achieved :-)",[24,55202,55204],{"id":55203},"links","Links:",[11,55206,55207,55208,55213,55214],{},"Repo link: ",[47,55209,55212],{"href":55210,"rel":55211},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fkids-piggy-app",[51],"Github","\nLive preview: ",[47,55215,55218],{"href":55216,"rel":55217},"https:\u002F\u002Fwww.mypiggyjar.com",[51],"MyPiggyJar.com",{"title":274,"searchDepth":288,"depth":288,"links":55220},[55221,55222,55223,55232,55233,55234,55240,55241],{"id":54708,"depth":288,"text":54709},{"id":54729,"depth":288,"text":54730},{"id":54736,"depth":288,"text":54737,"children":55224},[55225,55226,55227,55228,55229,55230,55231],{"id":54743,"depth":295,"text":54744},{"id":54754,"depth":295,"text":54755},{"id":54792,"depth":295,"text":54793},{"id":54912,"depth":295,"text":54913},{"id":42735,"depth":295,"text":42736},{"id":55034,"depth":295,"text":55035},{"id":55041,"depth":295,"text":55042},{"id":38720,"depth":288,"text":55118},{"id":55128,"depth":288,"text":55129},{"id":55149,"depth":288,"text":55150,"children":55235},[55236,55237,55238,55239],{"id":55153,"depth":295,"text":55154},{"id":55163,"depth":295,"text":55164},{"id":55179,"depth":295,"text":55180},{"id":52481,"depth":295,"text":55189},{"id":10633,"depth":288,"text":10634},{"id":55203,"depth":288,"text":55204},"\u002Fimages\u002Fposts\u002Fnew-years-promise-to-my-son\u002FK0V3gE4z8-e228d60ea2.png","2022-09-30T23:59:01.772Z","The back story There is always one, isn't it? Let's start from the beginning. We're in the dying days of December 2021. My son will be turning 8 in a couple of months. In his mi...","cl8p5cj2k000509m8de428qva",{},"\u002Fnew-years-promise-to-my-son",{"title":54703,"description":55244},"new-years-promise-to-my-son",[3095,55251,55252],"awsamplify","awsamplifyhackathon","7-3SGDfbhXTENyF5Xt-HBDrtKtQqeQw6RvfUGEJiIfo",1780400660362]