[{"data":1,"prerenderedAt":7806},["ShallowReactive",2],{"post-building-voice-notes-app-with-ai-transcription-and-post-processing":3},{"id":4,"title":5,"body":6,"canonicalUrl":7788,"cover":7789,"date":7790,"description":7791,"draft":7792,"extension":7793,"hashnodeId":7794,"meta":7795,"navigation":479,"path":7796,"seo":7797,"slug":7798,"stem":7798,"tags":7799,"__hash__":7805},"posts\u002Fbuilding-voice-notes-app-with-ai-transcription-and-post-processing.md","Building Vhisper: Voice Notes App with AI Transcription and Post-Processing",{"type":7,"value":8,"toc":7744},"minimark",[9,27,35,42,49,54,60,88,91,95,102,117,121,124,184,189,192,221,245,252,255,301,312,334,337,420,427,667,670,685,695,722,726,729,741,755,777,781,784,787,791,794,813,816,823,834,1149,1152,1186,1189,1203,1209,1220,1351,1357,1385,1392,1400,1411,1486,1493,1504,1803,1809,1818,1934,1947,1954,1960,1973,1977,2247,2250,2254,2407,2414,2419,2428,2456,2466,2687,2698,2702,2709,2753,2764,2770,2773,2777,2780,2787,2796,4154,4156,4195,4202,4210,4772,4775,4819,4824,4832,5761,5764,5807,5812,5815,5825,5832,6134,6139,6152,6507,6521,6647,6651,6658,6874,6877,6899,6902,6910,7000,7011,7054,7059,7062,7070,7422,7425,7428,7434,7438,7441,7450,7522,7536,7603,7615,7618,7622,7625,7628,7646,7649,7653,7656,7660,7671,7678,7682,7696,7703,7707,7713,7716,7720,7723,7726,7729,7732,7740],[10,11,12,13,20,21,26],"p",{},"After wrapping up my last project—a ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Frajeev.dev\u002Fbuilding-a-chat-interface-to-search-github",[18],"nofollow","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 ",[14,22,25],{"href":23,"rel":24},"https:\u002F\u002Fgithub.com\u002Fegoist\u002Fwhispo",[18],"Electron-based voice notes app"," designed for macOS.",[10,28,29,30,34],{},"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 ",[31,32,33],"strong",{},"Vhisper",", a voice notes app with AI transcription and post-processing, built with the Nuxt ecosystem and powered by Cloudflare.",[10,36,37,38],{},"And before you say it, I must make a confession: ",[39,40,41],"em",{},"“Hi! My name is Rajeev, and I am addicted to talking\u002Fchatting.”.",[10,43,44],{},[45,46],"img",{"alt":47,"src":48},"A bear confessing to its addiction","https:\u002F\u002Fi.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExNDJzcGdtYWJkeHhobDRzYzJ3MzhmZG8zNHczcThobmcxZnVrd3EyZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002FC4CI97S8sKstW\u002Fgiphy.gif",[50,51,53],"h2",{"id":52},"project-overview","Project Overview",[10,55,56,57,59],{},"Now that the formalities are done, let’s focus on what we’ll be building in this project. The goal is to create ",[31,58,33],{},", a web-based voice notes application with the following core features:",[61,62,63,70,76,82],"ul",{},[64,65,66,69],"li",{},[31,67,68],{},"Recording Voice Notes",": Users can record voice notes directly in the browser.",[64,71,72,75],{},[31,73,74],{},"AI-Powered Transcription",": Each recording is processed via Cloudflare Workers AI, converting speech to text.",[64,77,78,81],{},[31,79,80],{},"Post-Processing with Custom Prompts",": Users can customize how transcriptions are refined using an AI-driven post-processing step.",[64,83,84,87],{},[31,85,86],{},"Seamless Data Management (CRUD)",": Notes and audio files are efficiently stored using Cloudflare’s D1 database and R2 storage.",[10,89,90],{},"To give you a better sense of what we’re aiming for, here’s a quick demo showcasing Vhisper’s main features:",[92,93],"media-embed",{"url":94},"https:\u002F\u002Fyoutu.be\u002F7IcwTIrHIPg",[10,96,97,98],{},"You can experience it live here: ",[14,99,100],{"href":100,"rel":101},"https:\u002F\u002Fvhisper.nuxt.dev",[18],[10,103,104,105,108,109,112,113,116],{},"By the end of this guide, you’ll know exactly how to build and deploy this voice notes app using ",[31,106,107],{},"Nuxt",", ",[31,110,111],{},"NuxtHub"," and ",[31,114,115],{},"Cloudflare services","—a stack that combines innovation with developer-first simplicity. Ready to build it? Let’s get started!",[50,118,120],{"id":119},"project-setup","Project Setup",[10,122,123],{},"Before setting up the project let’s review the technologies used to build this app:",[125,126,127,134,142,150,158,176],"ol",{},[64,128,129,133],{},[14,130,107],{"href":131,"rel":132},"https:\u002F\u002Fnuxt.com",[18],": Vue.js framework for the application foundation",[64,135,136,141],{},[14,137,140],{"href":138,"rel":139},"https:\u002F\u002Fui3.nuxt.com",[18],"Nuxt UI (v3)",": For creating a polished and professional frontend",[64,143,144,149],{},[14,145,148],{"href":146,"rel":147},"https:\u002F\u002Form.drizzle.team",[18],"Drizzle",": Database ORM",[64,151,152,157],{},[14,153,156],{"href":154,"rel":155},"https:\u002F\u002Fzod.dev",[18],"Zod",": For client\u002Fserver side data validation",[64,159,160,164,165,108,169,108,172,175],{},[14,161,111],{"href":162,"rel":163},"https:\u002F\u002Fhub.nuxt.com",[18],": Backend (",[166,167,168],"code",{},"database",[166,170,171],{},"storage",[166,173,174],{},"AI"," etc.), deployment and administration platform for Nuxt",[64,177,178,183],{},[14,179,182],{"href":180,"rel":181},"https:\u002F\u002Fdevelopers.cloudflare.com",[18],"Cloudflare",": Powers NuxtHub to provide various services",[185,186,188],"h3",{"id":187},"prerequisites","Prerequisites",[10,190,191],{},"To follow along, apart from basic necessities like Node.js, npm, and some Nuxt knowledge, you’ll need:",[125,193,194,209],{},[64,195,196,197,200,201,208],{},"A ",[31,198,199],{},"Cloudflare account"," to use Workers AI and deploy your project. If you don’t have one, you can set it up ",[14,202,205],{"href":203,"rel":204},"https:\u002F\u002Fwww.cloudflare.com\u002F",[18],[31,206,207],{},"here",".",[64,210,196,211,214,215,208],{},[31,212,213],{},"NuxtHub Admin Account"," for managing apps via the NuxtHub dashboard. Sign up ",[14,216,219],{"href":217,"rel":218},"https:\u002F\u002Fadmin.hub.nuxt.com\u002F",[18],[31,220,207],{},[222,223,225,229],"div",{"dataNodeType":224},"callout",[222,226,228],{"dataNodeType":227},"callout-emoji","ℹ",[222,230,232,235,236,208],{"dataNodeType":231},"callout-text",[31,233,234],{},"Note:"," Workers AI models will run in your Cloudflare account even during local development. Check out their ",[14,237,244],{"target":238,"rel":239,"href":242,"style":243},"_self",[240,241,18],"noopener","noreferrer","https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers-ai\u002Fplatform\u002Fpricing","pointer-events: none","pricing and free quota",[185,246,248,249],{"id":247},"project-init","Project ",[31,250,251],{},"Init",[10,253,254],{},"We’ll start with the NuxtHub starter template. Run the following command to create and navigate to your new project directory:",[256,257,262],"pre",{"className":258,"code":259,"language":260,"meta":261,"style":261},"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","",[166,263,264,273],{"__ignoreMap":261},[265,266,269],"span",{"class":267,"line":268},"line",1,[265,270,272],{"class":271},"sJ8bj","# Create project and change into the project dir\n",[265,274,276,280,284,287,290,294,298],{"class":267,"line":275},2,[265,277,279],{"class":278},"sScJk","npx",[265,281,283],{"class":282},"sZZnC"," nuxthub",[265,285,286],{"class":282}," init",[265,288,289],{"class":282}," voice-notes",[265,291,293],{"class":292},"sVt8B"," && ",[265,295,297],{"class":296},"sj4cs","cd",[265,299,300],{"class":296}," $_\n",[10,302,303,304,307,308,311],{},"If you plan to use ",[31,305,306],{},"pnpm"," as your package manager, add a ",[166,309,310],{},".npmrc"," file at the root of your project with this line to hoist dependencies:",[256,313,315],{"className":258,"code":314,"language":260,"meta":261,"style":261},"# .npmrc\nshamefully-hoist=true\n",[166,316,317,322],{"__ignoreMap":261},[265,318,319],{"class":267,"line":268},[265,320,321],{"class":271},"# .npmrc\n",[265,323,324,327,331],{"class":267,"line":275},[265,325,326],{"class":292},"shamefully-hoist",[265,328,330],{"class":329},"szBVR","=",[265,332,333],{"class":282},"true\n",[10,335,336],{},"Now, install the dependencies:",[125,338,339,357,380,400],{},[64,340,341,342],{},"Nuxt modules:",[256,343,345],{"className":258,"code":344,"language":260,"meta":261,"style":261},"pnpm add @nuxt\u002Fui@next\n",[166,346,347],{"__ignoreMap":261},[265,348,349,351,354],{"class":267,"line":268},[265,350,306],{"class":278},[265,352,353],{"class":282}," add",[265,355,356],{"class":282}," @nuxt\u002Fui@next\n",[64,358,359,360],{},"Drizzle and related tools:",[256,361,363],{"className":258,"code":362,"language":260,"meta":261,"style":261},"pnpm add drizzle-orm drizzle-zod @vueuse\u002Fcore\n",[166,364,365],{"__ignoreMap":261},[265,366,367,369,371,374,377],{"class":267,"line":268},[265,368,306],{"class":278},[265,370,353],{"class":282},[265,372,373],{"class":282}," drizzle-orm",[265,375,376],{"class":282}," drizzle-zod",[265,378,379],{"class":282}," @vueuse\u002Fcore\n",[64,381,382,383],{},"Icon packs:",[256,384,386],{"className":258,"code":385,"language":260,"meta":261,"style":261},"pnpm add @iconify-json\u002Flucide @iconify-json\u002Fsimple-icons\n",[166,387,388],{"__ignoreMap":261},[265,389,390,392,394,397],{"class":267,"line":268},[265,391,306],{"class":278},[265,393,353],{"class":282},[265,395,396],{"class":282}," @iconify-json\u002Flucide",[265,398,399],{"class":282}," @iconify-json\u002Fsimple-icons\n",[64,401,402,403],{},"Dev dependencies:",[256,404,406],{"className":258,"code":405,"language":260,"meta":261,"style":261},"pnpm add -D drizzle-kit\n",[166,407,408],{"__ignoreMap":261},[265,409,410,412,414,417],{"class":267,"line":268},[265,411,306],{"class":278},[265,413,353],{"class":282},[265,415,416],{"class":296}," -D",[265,418,419],{"class":282}," drizzle-kit\n",[10,421,422,423,426],{},"Update your ",[166,424,425],{},"nuxt.config.ts"," file as follows:",[256,428,432],{"className":429,"code":430,"language":431,"meta":261,"style":261},"language-ts shiki shiki-themes github-light github-dark","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","ts",[166,433,434,448,474,481,493,498,504,510,522,528,534,539,550,561,566,572,582,592,602,607,612,623,628,634,640,651,656,661],{"__ignoreMap":261},[265,435,436,439,442,445],{"class":267,"line":268},[265,437,438],{"class":329},"export",[265,440,441],{"class":329}," default",[265,443,444],{"class":278}," defineNuxtConfig",[265,446,447],{"class":292},"({\n",[265,449,450,453,456,458,461,463,466,468,471],{"class":267,"line":275},[265,451,452],{"class":292},"  modules: [",[265,454,455],{"class":282},"\"@nuxthub\u002Fcore\"",[265,457,108],{"class":292},[265,459,460],{"class":282},"\"@nuxt\u002Feslint\"",[265,462,108],{"class":292},[265,464,465],{"class":282},"\"nuxt-auth-utils\"",[265,467,108],{"class":292},[265,469,470],{"class":282},"\"@nuxt\u002Fui\"",[265,472,473],{"class":292},"],\n",[265,475,477],{"class":267,"line":476},3,[265,478,480],{"emptyLinePlaceholder":479},true,"\n",[265,482,484,487,490],{"class":267,"line":483},4,[265,485,486],{"class":292},"  devtools: { enabled: ",[265,488,489],{"class":296},"true",[265,491,492],{"class":292}," },\n",[265,494,496],{"class":267,"line":495},5,[265,497,480],{"emptyLinePlaceholder":479},[265,499,501],{"class":267,"line":500},6,[265,502,503],{"class":292},"  runtimeConfig: {\n",[265,505,507],{"class":267,"line":506},7,[265,508,509],{"class":292},"    public: {\n",[265,511,513,516,519],{"class":267,"line":512},8,[265,514,515],{"class":292},"      helloText: ",[265,517,518],{"class":282},"\"Hello from the Edge 👋\"",[265,520,521],{"class":292},",\n",[265,523,525],{"class":267,"line":524},9,[265,526,527],{"class":292},"    },\n",[265,529,531],{"class":267,"line":530},10,[265,532,533],{"class":292},"  },\n",[265,535,537],{"class":267,"line":536},11,[265,538,480],{"emptyLinePlaceholder":479},[265,540,542,545,548],{"class":267,"line":541},12,[265,543,544],{"class":292},"  future: { compatibilityVersion: ",[265,546,547],{"class":296},"4",[265,549,492],{"class":292},[265,551,553,556,559],{"class":267,"line":552},13,[265,554,555],{"class":292},"  compatibilityDate: ",[265,557,558],{"class":282},"\"2024-07-30\"",[265,560,521],{"class":292},[265,562,564],{"class":267,"line":563},14,[265,565,480],{"emptyLinePlaceholder":479},[265,567,569],{"class":267,"line":568},15,[265,570,571],{"class":292},"  hub: {\n",[265,573,575,578,580],{"class":267,"line":574},16,[265,576,577],{"class":292},"    ai: ",[265,579,489],{"class":296},[265,581,521],{"class":292},[265,583,585,588,590],{"class":267,"line":584},17,[265,586,587],{"class":292},"    blob: ",[265,589,489],{"class":296},[265,591,521],{"class":292},[265,593,595,598,600],{"class":267,"line":594},18,[265,596,597],{"class":292},"    database: ",[265,599,489],{"class":296},[265,601,521],{"class":292},[265,603,605],{"class":267,"line":604},19,[265,606,533],{"class":292},[265,608,610],{"class":267,"line":609},20,[265,611,480],{"emptyLinePlaceholder":479},[265,613,615,618,621],{"class":267,"line":614},21,[265,616,617],{"class":292},"  css: [",[265,619,620],{"class":282},"\"~\u002Fassets\u002Fcss\u002Fmain.css\"",[265,622,473],{"class":292},[265,624,626],{"class":267,"line":625},22,[265,627,480],{"emptyLinePlaceholder":479},[265,629,631],{"class":267,"line":630},23,[265,632,633],{"class":292},"  eslint: {\n",[265,635,637],{"class":267,"line":636},24,[265,638,639],{"class":292},"    config: {\n",[265,641,643,646,649],{"class":267,"line":642},25,[265,644,645],{"class":292},"      stylistic: ",[265,647,648],{"class":296},"false",[265,650,521],{"class":292},[265,652,654],{"class":267,"line":653},26,[265,655,527],{"class":292},[265,657,659],{"class":267,"line":658},27,[265,660,533],{"class":292},[265,662,664],{"class":267,"line":663},28,[265,665,666],{"class":292},"});\n",[10,668,669],{},"We’ve made the following changes to the Nuxt config file:",[125,671,672,675,678],{},[64,673,674],{},"Updated the Nuxt modules used in the app",[64,676,677],{},"Enabled required NuxtHub features",[64,679,680,681,684],{},"And, added the ",[166,682,683],{},"main.css"," file path.",[10,686,687,688,690,691,694],{},"Create the ",[166,689,683],{}," file in the ",[166,692,693],{},"app\u002Fassets\u002Fcss"," folder with this content:",[256,696,700],{"className":697,"code":698,"language":699,"meta":261,"style":261},"language-css shiki shiki-themes github-light github-dark","@import \"tailwindcss\";\n@import \"@nuxt\u002Fui\";\n","css",[166,701,702,713],{"__ignoreMap":261},[265,703,704,707,710],{"class":267,"line":268},[265,705,706],{"class":329},"@import",[265,708,709],{"class":282}," \"tailwindcss\"",[265,711,712],{"class":292},";\n",[265,714,715,717,720],{"class":267,"line":275},[265,716,706],{"class":329},[265,718,719],{"class":282}," \"@nuxt\u002Fui\"",[265,721,712],{"class":292},[185,723,725],{"id":724},"testing-the-setup","Testing the Setup",[10,727,728],{},"Run the development server:",[256,730,732],{"className":258,"code":731,"language":260,"meta":261,"style":261},"pnpm dev\n",[166,733,734],{"__ignoreMap":261},[265,735,736,738],{"class":267,"line":268},[265,737,306],{"class":278},[265,739,740],{"class":282}," dev\n",[10,742,743,744,750,751,754],{},"Visit ",[14,745,748],{"href":746,"rel":747},"http:\u002F\u002Flocalhost:3000",[18],[166,749,746],{}," in your browser. If everything is set up correctly, you’ll see the message: ",[39,752,753],{},"“Hello from the Edge 👋”"," with a refresh button.",[222,756,757,760],{"dataNodeType":224},[222,758,759],{"dataNodeType":227},"💡",[222,761,762,765,766,112,769,772,773,776],{"dataNodeType":231},[31,763,764],{},"Troubleshooting Tip:"," If you encounter issues with TailwindCSS, try deleting ",[166,767,768],{},"node_modules",[166,770,771],{},"pnpm-lock.yaml",", and then run ",[166,774,775],{},"pnpm install"," to re-install the dependecies.",[50,778,780],{"id":779},"building-the-basic-backend","Building the Basic Backend",[10,782,783],{},"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.",[10,785,786],{},"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?",[185,788,790],{"id":789},"what-is-nuxthub","What is NuxtHub?",[10,792,793],{},"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).",[10,795,796,797,800,801,804,805,808,809,812],{},"You started with a NuxtHub template, so the project comes preconfigured with the ",[166,798,799],{},"@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 ",[166,802,803],{},"hub",". For example, ",[166,806,807],{},"hubAI"," is used for AI features, ",[166,810,811],{},"hubBlob"," for object storage, and so on.",[10,814,815],{},"Time is ripe now to work on the first API endpoint.",[185,817,819,822],{"id":818},"apitranscribe-endpoint",[166,820,821],{},"\u002Fapi\u002Ftranscribe"," Endpoint",[10,824,825,826,829,830,833],{},"Create a new file named ",[166,827,828],{},"transcribe.post.ts"," inside the ",[166,831,832],{},"server\u002Fapi"," directory, and add the following code to it:",[256,835,839],{"className":836,"code":837,"language":838,"meta":261,"style":261},"language-typescript shiki shiki-themes github-light github-dark","\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","typescript",[166,840,841,846,877,897,927,940,950,960,970,975,980,984,1003,1007,1014,1043,1068,1072,1076,1084,1095,1111,1119,1128,1137,1141,1145],{"__ignoreMap":261},[265,842,843],{"class":267,"line":268},[265,844,845],{"class":271},"\u002F\u002F server\u002Fapi\u002Ftranscribe.post.ts \n",[265,847,848,850,852,855,858,861,864,868,871,874],{"class":267,"line":275},[265,849,438],{"class":329},[265,851,441],{"class":329},[265,853,854],{"class":278}," defineEventHandler",[265,856,857],{"class":292},"(",[265,859,860],{"class":329},"async",[265,862,863],{"class":292}," (",[265,865,867],{"class":866},"s4XuR","event",[265,869,870],{"class":292},") ",[265,872,873],{"class":329},"=>",[265,875,876],{"class":292}," {\n",[265,878,879,882,885,888,891,894],{"class":267,"line":476},[265,880,881],{"class":329},"  const",[265,883,884],{"class":296}," form",[265,886,887],{"class":329}," =",[265,889,890],{"class":329}," await",[265,892,893],{"class":278}," readFormData",[265,895,896],{"class":292},"(event);\n",[265,898,899,901,904,906,909,912,914,917,919,922,925],{"class":267,"line":483},[265,900,881],{"class":329},[265,902,903],{"class":296}," blob",[265,905,887],{"class":329},[265,907,908],{"class":292}," form.",[265,910,911],{"class":278},"get",[265,913,857],{"class":292},[265,915,916],{"class":282},"\"audio\"",[265,918,870],{"class":292},[265,920,921],{"class":329},"as",[265,923,924],{"class":278}," Blob",[265,926,712],{"class":292},[265,928,929,932,934,937],{"class":267,"line":495},[265,930,931],{"class":329},"  if",[265,933,863],{"class":292},[265,935,936],{"class":329},"!",[265,938,939],{"class":292},"blob) {\n",[265,941,942,945,948],{"class":267,"line":500},[265,943,944],{"class":329},"    throw",[265,946,947],{"class":278}," createError",[265,949,447],{"class":292},[265,951,952,955,958],{"class":267,"line":506},[265,953,954],{"class":292},"      statusCode: ",[265,956,957],{"class":296},"400",[265,959,521],{"class":292},[265,961,962,965,968],{"class":267,"line":512},[265,963,964],{"class":292},"      message: ",[265,966,967],{"class":282},"\"Missing audio blob to transcribe\"",[265,969,521],{"class":292},[265,971,972],{"class":267,"line":524},[265,973,974],{"class":292},"    });\n",[265,976,977],{"class":267,"line":530},[265,978,979],{"class":292},"  }\n",[265,981,982],{"class":267,"line":536},[265,983,480],{"emptyLinePlaceholder":479},[265,985,986,989,992,995,998,1000],{"class":267,"line":541},[265,987,988],{"class":278},"  ensureBlob",[265,990,991],{"class":292},"(blob, { maxSize: ",[265,993,994],{"class":282},"\"8MB\"",[265,996,997],{"class":292},", types: [",[265,999,916],{"class":282},[265,1001,1002],{"class":292},"] });\n",[265,1004,1005],{"class":267,"line":552},[265,1006,480],{"emptyLinePlaceholder":479},[265,1008,1009,1012],{"class":267,"line":563},[265,1010,1011],{"class":329},"  try",[265,1013,876],{"class":292},[265,1015,1016,1019,1022,1024,1026,1029,1032,1035,1037,1040],{"class":267,"line":568},[265,1017,1018],{"class":329},"    const",[265,1020,1021],{"class":296}," response",[265,1023,887],{"class":329},[265,1025,890],{"class":329},[265,1027,1028],{"class":278}," hubAI",[265,1030,1031],{"class":292},"().",[265,1033,1034],{"class":278},"run",[265,1036,857],{"class":292},[265,1038,1039],{"class":282},"\"@cf\u002Fopenai\u002Fwhisper\"",[265,1041,1042],{"class":292},", {\n",[265,1044,1045,1048,1051,1054,1056,1059,1062,1065],{"class":267,"line":574},[265,1046,1047],{"class":292},"      audio: [",[265,1049,1050],{"class":329},"...new",[265,1052,1053],{"class":278}," Uint8Array",[265,1055,857],{"class":292},[265,1057,1058],{"class":329},"await",[265,1060,1061],{"class":292}," blob.",[265,1063,1064],{"class":278},"arrayBuffer",[265,1066,1067],{"class":292},"())],\n",[265,1069,1070],{"class":267,"line":584},[265,1071,974],{"class":292},[265,1073,1074],{"class":267,"line":594},[265,1075,480],{"emptyLinePlaceholder":479},[265,1077,1078,1081],{"class":267,"line":604},[265,1079,1080],{"class":329},"    return",[265,1082,1083],{"class":292}," response.text;\n",[265,1085,1086,1089,1092],{"class":267,"line":609},[265,1087,1088],{"class":292},"  } ",[265,1090,1091],{"class":329},"catch",[265,1093,1094],{"class":292}," (err) {\n",[265,1096,1097,1100,1103,1105,1108],{"class":267,"line":614},[265,1098,1099],{"class":292},"    console.",[265,1101,1102],{"class":278},"error",[265,1104,857],{"class":292},[265,1106,1107],{"class":282},"\"Error transcribing audio:\"",[265,1109,1110],{"class":292},", err);\n",[265,1112,1113,1115,1117],{"class":267,"line":625},[265,1114,944],{"class":329},[265,1116,947],{"class":278},[265,1118,447],{"class":292},[265,1120,1121,1123,1126],{"class":267,"line":630},[265,1122,954],{"class":292},[265,1124,1125],{"class":296},"500",[265,1127,521],{"class":292},[265,1129,1130,1132,1135],{"class":267,"line":636},[265,1131,964],{"class":292},[265,1133,1134],{"class":282},"\"Failed to transcribe audio. Please try again.\"",[265,1136,521],{"class":292},[265,1138,1139],{"class":267,"line":642},[265,1140,974],{"class":292},[265,1142,1143],{"class":267,"line":653},[265,1144,979],{"class":292},[265,1146,1147],{"class":267,"line":658},[265,1148,666],{"class":292},[10,1150,1151],{},"The above code does the following:",[125,1153,1154,1160,1173,1183],{},[64,1155,1156,1157],{},"Parses incoming form data to extract the audio as a ",[166,1158,1159],{},"Blob",[64,1161,1162,1163,1166,1167,1169,1170],{},"Verifies that it’s an audio blob and is less than ",[166,1164,1165],{},"8MB"," in size using a ",[166,1168,799],{}," utility function ",[166,1171,1172],{},"ensureBlob",[64,1174,1175,1176,1179,1180,1182],{},"Passes on the array buffer to the ",[166,1177,1178],{},"Whisper"," model through ",[166,1181,807],{}," for transcription",[64,1184,1185],{},"Returns the transcribed text to the client",[10,1187,1188],{},"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.",[256,1190,1192],{"className":258,"code":1191,"language":260,"meta":261,"style":261},"npx nuxthub link\n",[166,1193,1194],{"__ignoreMap":261},[265,1195,1196,1198,1200],{"class":267,"line":268},[265,1197,279],{"class":278},[265,1199,283],{"class":282},[265,1201,1202],{"class":282}," link\n",[185,1204,1206,822],{"id":1205},"apiupload-endpoint",[166,1207,1208],{},"\u002Fapi\u002Fupload",[10,1210,1211,1212,1215,1216,1219],{},"Next, create an endpoint to upload the audio recordings to the R2 storage. Create a new file ",[166,1213,1214],{},"upload.put.ts"," in your ",[166,1217,1218],{},"\u002Fserver\u002Fapi"," folder and add the following code to it:",[256,1221,1223],{"className":836,"code":1222,"language":838,"meta":261,"style":261},"\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",[166,1224,1225,1230,1252,1268,1278,1287,1292,1301,1310,1314,1319,1328,1338,1342,1347],{"__ignoreMap":261},[265,1226,1227],{"class":267,"line":268},[265,1228,1229],{"class":271},"\u002F\u002F server\u002Fapi\u002Fupload.put.ts\n",[265,1231,1232,1234,1236,1238,1240,1242,1244,1246,1248,1250],{"class":267,"line":275},[265,1233,438],{"class":329},[265,1235,441],{"class":329},[265,1237,854],{"class":278},[265,1239,857],{"class":292},[265,1241,860],{"class":329},[265,1243,863],{"class":292},[265,1245,867],{"class":866},[265,1247,870],{"class":292},[265,1249,873],{"class":329},[265,1251,876],{"class":292},[265,1253,1254,1257,1260,1262,1265],{"class":267,"line":476},[265,1255,1256],{"class":329},"  return",[265,1258,1259],{"class":278}," hubBlob",[265,1261,1031],{"class":292},[265,1263,1264],{"class":278},"handleUpload",[265,1266,1267],{"class":292},"(event, {\n",[265,1269,1270,1273,1276],{"class":267,"line":483},[265,1271,1272],{"class":292},"    formKey: ",[265,1274,1275],{"class":282},"\"files\"",[265,1277,521],{"class":292},[265,1279,1280,1283,1285],{"class":267,"line":495},[265,1281,1282],{"class":292},"    multiple: ",[265,1284,489],{"class":296},[265,1286,521],{"class":292},[265,1288,1289],{"class":267,"line":500},[265,1290,1291],{"class":292},"    ensure: {\n",[265,1293,1294,1297,1299],{"class":267,"line":506},[265,1295,1296],{"class":292},"      maxSize: ",[265,1298,994],{"class":282},[265,1300,521],{"class":292},[265,1302,1303,1306,1308],{"class":267,"line":512},[265,1304,1305],{"class":292},"      types: [",[265,1307,916],{"class":282},[265,1309,473],{"class":292},[265,1311,1312],{"class":267,"line":524},[265,1313,527],{"class":292},[265,1315,1316],{"class":267,"line":530},[265,1317,1318],{"class":292},"    put: {\n",[265,1320,1321,1324,1326],{"class":267,"line":536},[265,1322,1323],{"class":292},"      addRandomSuffix: ",[265,1325,489],{"class":296},[265,1327,521],{"class":292},[265,1329,1330,1333,1336],{"class":267,"line":541},[265,1331,1332],{"class":292},"      prefix: ",[265,1334,1335],{"class":282},"\"recordings\"",[265,1337,521],{"class":292},[265,1339,1340],{"class":267,"line":552},[265,1341,527],{"class":292},[265,1343,1344],{"class":267,"line":563},[265,1345,1346],{"class":292},"  });\n",[265,1348,1349],{"class":267,"line":568},[265,1350,666],{"class":292},[10,1352,1353,1354,1356],{},"The above code uses another utility method from the NuxtHub core module to upload the incoming audio files to R2. ",[166,1355,1264],{}," does the following:",[125,1358,1359,1366,1369,1375,1382],{},[64,1360,1361,1362,1365],{},"Looks for the ",[166,1363,1364],{},"files"," key in the incoming form data to extract blob data",[64,1367,1368],{},"Supports multiple files per event",[64,1370,1371,1372,1374],{},"Ensures that the files are audio and under ",[166,1373,1165],{}," in size",[64,1376,1377,1378,1381],{},"And, finally uploads them to your R2 bucket inside ",[166,1379,1380],{},"recordings"," folder while also adding a random suffix to the final names",[64,1383,1384],{},"Returns a promise to the client that resolves once all the files are uploaded",[10,1386,1387,1388,1391],{},"Now we just need ",[166,1389,1390],{},"\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.",[185,1393,1395,1396,1399],{"id":1394},"defining-the-notes-table-schema","Defining the ",[166,1397,1398],{},"notes"," Table Schema",[10,1401,1402,1403,1406,1407,1410],{},"As we will use ",[166,1404,1405],{},"drizzle"," to manage and interact with the database, we need to configure it first. Create a new file ",[166,1408,1409],{},"drizzle.config.ts"," in the project root, and add the following to it:",[256,1412,1414],{"className":836,"code":1413,"language":838,"meta":261,"style":261},"\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",[166,1415,1416,1421,1437,1441,1452,1462,1472,1482],{"__ignoreMap":261},[265,1417,1418],{"class":267,"line":268},[265,1419,1420],{"class":271},"\u002F\u002F drizzle.config.ts\n",[265,1422,1423,1426,1429,1432,1435],{"class":267,"line":275},[265,1424,1425],{"class":329},"import",[265,1427,1428],{"class":292}," { defineConfig } ",[265,1430,1431],{"class":329},"from",[265,1433,1434],{"class":282}," 'drizzle-kit'",[265,1436,712],{"class":292},[265,1438,1439],{"class":267,"line":476},[265,1440,480],{"emptyLinePlaceholder":479},[265,1442,1443,1445,1447,1450],{"class":267,"line":483},[265,1444,438],{"class":329},[265,1446,441],{"class":329},[265,1448,1449],{"class":278}," defineConfig",[265,1451,447],{"class":292},[265,1453,1454,1457,1460],{"class":267,"line":495},[265,1455,1456],{"class":292},"  dialect: ",[265,1458,1459],{"class":282},"'sqlite'",[265,1461,521],{"class":292},[265,1463,1464,1467,1470],{"class":267,"line":500},[265,1465,1466],{"class":292},"  schema: ",[265,1468,1469],{"class":282},"'.\u002Fserver\u002Fdatabase\u002Fschema.ts'",[265,1471,521],{"class":292},[265,1473,1474,1477,1480],{"class":267,"line":506},[265,1475,1476],{"class":292},"  out: ",[265,1478,1479],{"class":282},"'.\u002Fserver\u002Fdatabase\u002Fmigrations'",[265,1481,521],{"class":292},[265,1483,1484],{"class":267,"line":512},[265,1485,666],{"class":292},[10,1487,1488,1489,1492],{},"The config above mentions where the database schema is located, and where should the database migrations be generated. The database dialect is set to ",[166,1490,1491],{},"sqlite"," as that is what Cloudflare’s D1 database supports.",[10,1494,1495,1496,1499,1500,1503],{},"Next, create a new file ",[166,1497,1498],{},"schema.ts"," in the ",[166,1501,1502],{},"server\u002Fdatabase"," folder, and add the following to it:",[256,1505,1507],{"className":836,"code":1506,"language":838,"meta":261,"style":261},"\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",[166,1508,1509,1514,1528,1542,1556,1560,1582,1598,1609,1652,1672,1686,1694,1712,1726,1734,1748,1766,1799],{"__ignoreMap":261},[265,1510,1511],{"class":267,"line":268},[265,1512,1513],{"class":271},"\u002F\u002F server\u002Fdatabase\u002Fschema.ts\n",[265,1515,1516,1518,1521,1523,1526],{"class":267,"line":275},[265,1517,1425],{"class":329},[265,1519,1520],{"class":292}," crypto ",[265,1522,1431],{"class":329},[265,1524,1525],{"class":282}," \"node:crypto\"",[265,1527,712],{"class":292},[265,1529,1530,1532,1535,1537,1540],{"class":267,"line":476},[265,1531,1425],{"class":329},[265,1533,1534],{"class":292}," { sql } ",[265,1536,1431],{"class":329},[265,1538,1539],{"class":282}," \"drizzle-orm\"",[265,1541,712],{"class":292},[265,1543,1544,1546,1549,1551,1554],{"class":267,"line":483},[265,1545,1425],{"class":329},[265,1547,1548],{"class":292}," { sqliteTable, text } ",[265,1550,1431],{"class":329},[265,1552,1553],{"class":282}," \"drizzle-orm\u002Fsqlite-core\"",[265,1555,712],{"class":292},[265,1557,1558],{"class":267,"line":495},[265,1559,480],{"emptyLinePlaceholder":479},[265,1561,1562,1564,1567,1570,1572,1575,1577,1580],{"class":267,"line":500},[265,1563,438],{"class":329},[265,1565,1566],{"class":329}," const",[265,1568,1569],{"class":296}," notes",[265,1571,887],{"class":329},[265,1573,1574],{"class":278}," sqliteTable",[265,1576,857],{"class":292},[265,1578,1579],{"class":282},"\"notes\"",[265,1581,1042],{"class":292},[265,1583,1584,1587,1590,1592,1595],{"class":267,"line":506},[265,1585,1586],{"class":292},"  id: ",[265,1588,1589],{"class":278},"text",[265,1591,857],{"class":292},[265,1593,1594],{"class":282},"\"id\"",[265,1596,1597],{"class":292},")\n",[265,1599,1600,1603,1606],{"class":267,"line":512},[265,1601,1602],{"class":292},"    .",[265,1604,1605],{"class":278},"primaryKey",[265,1607,1608],{"class":292},"()\n",[265,1610,1611,1613,1616,1619,1621,1624,1627,1630,1633,1635,1638,1641,1644,1646,1649],{"class":267,"line":524},[265,1612,1602],{"class":292},[265,1614,1615],{"class":278},"$defaultFn",[265,1617,1618],{"class":292},"(() ",[265,1620,873],{"class":329},[265,1622,1623],{"class":282}," \"nt_\"",[265,1625,1626],{"class":329}," +",[265,1628,1629],{"class":292}," crypto.",[265,1631,1632],{"class":278},"randomBytes",[265,1634,857],{"class":292},[265,1636,1637],{"class":296},"12",[265,1639,1640],{"class":292},").",[265,1642,1643],{"class":278},"toString",[265,1645,857],{"class":292},[265,1647,1648],{"class":282},"\"hex\"",[265,1650,1651],{"class":292},")),\n",[265,1653,1654,1657,1659,1661,1664,1666,1669],{"class":267,"line":530},[265,1655,1656],{"class":292},"  text: ",[265,1658,1589],{"class":278},[265,1660,857],{"class":292},[265,1662,1663],{"class":282},"\"text\"",[265,1665,1640],{"class":292},[265,1667,1668],{"class":278},"notNull",[265,1670,1671],{"class":292},"(),\n",[265,1673,1674,1677,1679,1681,1684],{"class":267,"line":536},[265,1675,1676],{"class":292},"  createdAt: ",[265,1678,1589],{"class":278},[265,1680,857],{"class":292},[265,1682,1683],{"class":282},"\"created_at\"",[265,1685,1597],{"class":292},[265,1687,1688,1690,1692],{"class":267,"line":541},[265,1689,1602],{"class":292},[265,1691,1668],{"class":278},[265,1693,1608],{"class":292},[265,1695,1696,1698,1701,1703,1706,1709],{"class":267,"line":552},[265,1697,1602],{"class":292},[265,1699,1700],{"class":278},"default",[265,1702,857],{"class":292},[265,1704,1705],{"class":278},"sql",[265,1707,1708],{"class":282},"`(CURRENT_TIMESTAMP)`",[265,1710,1711],{"class":292},"),\n",[265,1713,1714,1717,1719,1721,1724],{"class":267,"line":563},[265,1715,1716],{"class":292},"  updatedAt: ",[265,1718,1589],{"class":278},[265,1720,857],{"class":292},[265,1722,1723],{"class":282},"\"updated_at\"",[265,1725,1597],{"class":292},[265,1727,1728,1730,1732],{"class":267,"line":568},[265,1729,1602],{"class":292},[265,1731,1668],{"class":278},[265,1733,1608],{"class":292},[265,1735,1736,1738,1740,1742,1744,1746],{"class":267,"line":574},[265,1737,1602],{"class":292},[265,1739,1700],{"class":278},[265,1741,857],{"class":292},[265,1743,1705],{"class":278},[265,1745,1708],{"class":282},[265,1747,1597],{"class":292},[265,1749,1750,1752,1755,1757,1759,1762,1764],{"class":267,"line":584},[265,1751,1602],{"class":292},[265,1753,1754],{"class":278},"$onUpdate",[265,1756,1618],{"class":292},[265,1758,873],{"class":329},[265,1760,1761],{"class":278}," sql",[265,1763,1708],{"class":282},[265,1765,1711],{"class":292},[265,1767,1768,1771,1773,1775,1778,1781,1784,1787,1790,1793,1796],{"class":267,"line":594},[265,1769,1770],{"class":292},"  audioUrls: ",[265,1772,1589],{"class":278},[265,1774,857],{"class":292},[265,1776,1777],{"class":282},"\"audio_urls\"",[265,1779,1780],{"class":292},", { mode: ",[265,1782,1783],{"class":282},"\"json\"",[265,1785,1786],{"class":292}," }).",[265,1788,1789],{"class":278},"$type",[265,1791,1792],{"class":292},"\u003C",[265,1794,1795],{"class":296},"string",[265,1797,1798],{"class":292},"[]>(),\n",[265,1800,1801],{"class":267,"line":604},[265,1802,666],{"class":292},[10,1804,1805,1806,1808],{},"The ",[166,1807,1398],{}," table schema is straightforward. It includes the note text and optional audio recording URLs stored as a JSON string array.",[10,1810,1811,1812,1499,1815,1503],{},"Finally, create a new file ",[166,1813,1814],{},"drizzle.ts",[166,1816,1817],{},"server\u002Futils",[256,1819,1821],{"className":836,"code":1820,"language":838,"meta":261,"style":261},"\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",[166,1822,1823,1828,1842,1862,1866,1879,1883,1897,1901,1914,1929],{"__ignoreMap":261},[265,1824,1825],{"class":267,"line":268},[265,1826,1827],{"class":271},"\u002F\u002F server\u002Futils\u002Fdrizzle.ts\n",[265,1829,1830,1832,1835,1837,1840],{"class":267,"line":275},[265,1831,1425],{"class":329},[265,1833,1834],{"class":292}," { drizzle } ",[265,1836,1431],{"class":329},[265,1838,1839],{"class":282}," \"drizzle-orm\u002Fd1\"",[265,1841,712],{"class":292},[265,1843,1844,1846,1849,1852,1855,1857,1860],{"class":267,"line":476},[265,1845,1425],{"class":329},[265,1847,1848],{"class":296}," *",[265,1850,1851],{"class":329}," as",[265,1853,1854],{"class":292}," schema ",[265,1856,1431],{"class":329},[265,1858,1859],{"class":282}," \"..\u002Fdatabase\u002Fschema\"",[265,1861,712],{"class":292},[265,1863,1864],{"class":267,"line":483},[265,1865,480],{"emptyLinePlaceholder":479},[265,1867,1868,1870,1873,1875,1877],{"class":267,"line":495},[265,1869,438],{"class":329},[265,1871,1872],{"class":292}," { sql, eq, and, or, desc } ",[265,1874,1431],{"class":329},[265,1876,1539],{"class":282},[265,1878,712],{"class":292},[265,1880,1881],{"class":267,"line":500},[265,1882,480],{"emptyLinePlaceholder":479},[265,1884,1885,1887,1889,1892,1894],{"class":267,"line":506},[265,1886,438],{"class":329},[265,1888,1566],{"class":329},[265,1890,1891],{"class":296}," tables",[265,1893,887],{"class":329},[265,1895,1896],{"class":292}," schema;\n",[265,1898,1899],{"class":267,"line":512},[265,1900,480],{"emptyLinePlaceholder":479},[265,1902,1903,1905,1908,1911],{"class":267,"line":524},[265,1904,438],{"class":329},[265,1906,1907],{"class":329}," function",[265,1909,1910],{"class":278}," useDrizzle",[265,1912,1913],{"class":292},"() {\n",[265,1915,1916,1918,1921,1923,1926],{"class":267,"line":530},[265,1917,1256],{"class":329},[265,1919,1920],{"class":278}," drizzle",[265,1922,857],{"class":292},[265,1924,1925],{"class":278},"hubDatabase",[265,1927,1928],{"class":292},"(), { schema });\n",[265,1930,1931],{"class":267,"line":536},[265,1932,1933],{"class":292},"}\n",[10,1935,1936,1937,1939,1940,1942,1943,1946],{},"Here we hook up ",[166,1938,1925],{}," with the tables schema through ",[166,1941,1405],{}," and export the server composable ",[166,1944,1945],{},"useDrizzle"," along with the needed operators.",[10,1948,1949,1950,1953],{},"Now we are ready to create the ",[166,1951,1952],{},"\u002Fapi\u002Fnotes"," endpoints which we will be doing in the next section.",[185,1955,1957,1959],{"id":1956},"apinotes-endpoints",[166,1958,1952],{}," Endpoints",[10,1961,1962,1963,112,1966,1499,1969,1972],{},"Create two new files ",[166,1964,1965],{},"index.post.ts",[166,1967,1968],{},"index.get.ts",[166,1970,1971],{},"server\u002Fapi\u002Fnotes"," folder and add the respective codes to them as shown below.",[10,1974,1975],{},[31,1976,1965],{},[256,1978,1980],{"className":836,"code":1979,"language":838,"meta":261,"style":261},"\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",[166,1981,1982,1987,2001,2005,2027,2049,2053,2078,2082,2088,2097,2108,2117,2122,2164,2169,2173,2189,2197,2210,2218,2226,2235,2239,2243],{"__ignoreMap":261},[265,1983,1984],{"class":267,"line":268},[265,1985,1986],{"class":271},"\u002F\u002F server\u002Fapi\u002Fnotes\u002Findex.post.ts\n",[265,1988,1989,1991,1994,1996,1999],{"class":267,"line":275},[265,1990,1425],{"class":329},[265,1992,1993],{"class":292}," { noteSchema } ",[265,1995,1431],{"class":329},[265,1997,1998],{"class":282}," \"#shared\u002Fschemas\u002Fnote.schema\"",[265,2000,712],{"class":292},[265,2002,2003],{"class":267,"line":476},[265,2004,480],{"emptyLinePlaceholder":479},[265,2006,2007,2009,2011,2013,2015,2017,2019,2021,2023,2025],{"class":267,"line":483},[265,2008,438],{"class":329},[265,2010,441],{"class":329},[265,2012,854],{"class":278},[265,2014,857],{"class":292},[265,2016,860],{"class":329},[265,2018,863],{"class":292},[265,2020,867],{"class":866},[265,2022,870],{"class":292},[265,2024,873],{"class":329},[265,2026,876],{"class":292},[265,2028,2029,2031,2034,2037,2040,2042,2044,2047],{"class":267,"line":495},[265,2030,881],{"class":329},[265,2032,2033],{"class":292}," { ",[265,2035,2036],{"class":296},"user",[265,2038,2039],{"class":292}," } ",[265,2041,330],{"class":329},[265,2043,890],{"class":329},[265,2045,2046],{"class":278}," requireUserSession",[265,2048,896],{"class":292},[265,2050,2051],{"class":267,"line":500},[265,2052,480],{"emptyLinePlaceholder":479},[265,2054,2055,2057,2059,2061,2063,2066,2068,2070,2072,2075],{"class":267,"line":506},[265,2056,881],{"class":329},[265,2058,2033],{"class":292},[265,2060,1589],{"class":296},[265,2062,108],{"class":292},[265,2064,2065],{"class":296},"audioUrls",[265,2067,2039],{"class":292},[265,2069,330],{"class":329},[265,2071,890],{"class":329},[265,2073,2074],{"class":278}," readValidatedBody",[265,2076,2077],{"class":292},"(event, noteSchema.parse);\n",[265,2079,2080],{"class":267,"line":512},[265,2081,480],{"emptyLinePlaceholder":479},[265,2083,2084,2086],{"class":267,"line":524},[265,2085,1011],{"class":329},[265,2087,876],{"class":292},[265,2089,2090,2093,2095],{"class":267,"line":530},[265,2091,2092],{"class":329},"    await",[265,2094,1910],{"class":278},[265,2096,1608],{"class":292},[265,2098,2099,2102,2105],{"class":267,"line":536},[265,2100,2101],{"class":292},"      .",[265,2103,2104],{"class":278},"insert",[265,2106,2107],{"class":292},"(tables.notes)\n",[265,2109,2110,2112,2115],{"class":267,"line":541},[265,2111,2101],{"class":292},[265,2113,2114],{"class":278},"values",[265,2116,447],{"class":292},[265,2118,2119],{"class":267,"line":552},[265,2120,2121],{"class":292},"        text,\n",[265,2123,2124,2127,2130,2133,2136,2139,2142,2144,2146,2149,2151,2154,2156,2159,2162],{"class":267,"line":563},[265,2125,2126],{"class":292},"        audioUrls: audioUrls ",[265,2128,2129],{"class":329},"?",[265,2131,2132],{"class":292}," audioUrls.",[265,2134,2135],{"class":278},"map",[265,2137,2138],{"class":292},"((",[265,2140,2141],{"class":866},"url",[265,2143,870],{"class":292},[265,2145,873],{"class":329},[265,2147,2148],{"class":282}," `\u002Faudio\u002F${",[265,2150,2141],{"class":292},[265,2152,2153],{"class":282},"}`",[265,2155,870],{"class":292},[265,2157,2158],{"class":329},":",[265,2160,2161],{"class":296}," null",[265,2163,521],{"class":292},[265,2165,2166],{"class":267,"line":568},[265,2167,2168],{"class":292},"      });\n",[265,2170,2171],{"class":267,"line":574},[265,2172,480],{"emptyLinePlaceholder":479},[265,2174,2175,2177,2180,2183,2186],{"class":267,"line":584},[265,2176,1080],{"class":329},[265,2178,2179],{"class":278}," setResponseStatus",[265,2181,2182],{"class":292},"(event, ",[265,2184,2185],{"class":296},"201",[265,2187,2188],{"class":292},");\n",[265,2190,2191,2193,2195],{"class":267,"line":594},[265,2192,1088],{"class":292},[265,2194,1091],{"class":329},[265,2196,1094],{"class":292},[265,2198,2199,2201,2203,2205,2208],{"class":267,"line":604},[265,2200,1099],{"class":292},[265,2202,1102],{"class":278},[265,2204,857],{"class":292},[265,2206,2207],{"class":282},"\"Error creating note:\"",[265,2209,1110],{"class":292},[265,2211,2212,2214,2216],{"class":267,"line":609},[265,2213,944],{"class":329},[265,2215,947],{"class":278},[265,2217,447],{"class":292},[265,2219,2220,2222,2224],{"class":267,"line":614},[265,2221,954],{"class":292},[265,2223,1125],{"class":296},[265,2225,521],{"class":292},[265,2227,2228,2230,2233],{"class":267,"line":625},[265,2229,964],{"class":292},[265,2231,2232],{"class":282},"\"Failed to create note. Please try again.\"",[265,2234,521],{"class":292},[265,2236,2237],{"class":267,"line":630},[265,2238,974],{"class":292},[265,2240,2241],{"class":267,"line":636},[265,2242,979],{"class":292},[265,2244,2245],{"class":267,"line":642},[265,2246,666],{"class":292},[10,2248,2249],{},"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.",[10,2251,2252],{},[31,2253,1968],{},[256,2255,2257],{"className":836,"code":2256,"language":838,"meta":261,"style":261},"\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",[166,2258,2259,2264,2286,2292,2306,2315,2323,2338,2342,2349,2357,2370,2378,2386,2395,2399,2403],{"__ignoreMap":261},[265,2260,2261],{"class":267,"line":268},[265,2262,2263],{"class":271},"\u002F\u002F server\u002Fapi\u002Fnotes\u002Findex.get.ts\n",[265,2265,2266,2268,2270,2272,2274,2276,2278,2280,2282,2284],{"class":267,"line":275},[265,2267,438],{"class":329},[265,2269,441],{"class":329},[265,2271,854],{"class":278},[265,2273,857],{"class":292},[265,2275,860],{"class":329},[265,2277,863],{"class":292},[265,2279,867],{"class":866},[265,2281,870],{"class":292},[265,2283,873],{"class":329},[265,2285,876],{"class":292},[265,2287,2288,2290],{"class":267,"line":476},[265,2289,1011],{"class":329},[265,2291,876],{"class":292},[265,2293,2294,2296,2298,2300,2302,2304],{"class":267,"line":483},[265,2295,1018],{"class":329},[265,2297,1569],{"class":296},[265,2299,887],{"class":329},[265,2301,890],{"class":329},[265,2303,1910],{"class":278},[265,2305,1608],{"class":292},[265,2307,2308,2310,2313],{"class":267,"line":495},[265,2309,2101],{"class":292},[265,2311,2312],{"class":278},"select",[265,2314,1608],{"class":292},[265,2316,2317,2319,2321],{"class":267,"line":500},[265,2318,2101],{"class":292},[265,2320,1431],{"class":278},[265,2322,2107],{"class":292},[265,2324,2325,2327,2330,2332,2335],{"class":267,"line":506},[265,2326,2101],{"class":292},[265,2328,2329],{"class":278},"orderBy",[265,2331,857],{"class":292},[265,2333,2334],{"class":278},"desc",[265,2336,2337],{"class":292},"(tables.notes.updatedAt));\n",[265,2339,2340],{"class":267,"line":512},[265,2341,480],{"emptyLinePlaceholder":479},[265,2343,2344,2346],{"class":267,"line":524},[265,2345,1080],{"class":329},[265,2347,2348],{"class":292}," notes;\n",[265,2350,2351,2353,2355],{"class":267,"line":530},[265,2352,1088],{"class":292},[265,2354,1091],{"class":329},[265,2356,1094],{"class":292},[265,2358,2359,2361,2363,2365,2368],{"class":267,"line":536},[265,2360,1099],{"class":292},[265,2362,1102],{"class":278},[265,2364,857],{"class":292},[265,2366,2367],{"class":282},"\"Error retrieving note:\"",[265,2369,1110],{"class":292},[265,2371,2372,2374,2376],{"class":267,"line":541},[265,2373,944],{"class":329},[265,2375,947],{"class":278},[265,2377,447],{"class":292},[265,2379,2380,2382,2384],{"class":267,"line":552},[265,2381,954],{"class":292},[265,2383,1125],{"class":296},[265,2385,521],{"class":292},[265,2387,2388,2390,2393],{"class":267,"line":563},[265,2389,964],{"class":292},[265,2391,2392],{"class":282},"\"Failed to get notes. Please try again.\"",[265,2394,521],{"class":292},[265,2396,2397],{"class":267,"line":568},[265,2398,974],{"class":292},[265,2400,2401],{"class":267,"line":574},[265,2402,979],{"class":292},[265,2404,2405],{"class":267,"line":584},[265,2406,666],{"class":292},[10,2408,2409,2410,2413],{},"Here we fetch the notes entries from the table in descending order of ",[166,2411,2412],{},"updatedAt"," field.",[10,2415,2416],{},[31,2417,2418],{},"Incoming data validation",[10,2420,2421,2422,2424,2425,2427],{},"As mentioned in the beginning, we’ll use ",[166,2423,156],{}," for data validation. Here is the relevant code from ",[166,2426,1965],{}," that validates the incoming client data.",[256,2429,2431],{"className":836,"code":2430,"language":838,"meta":261,"style":261},"const { text, audioUrls } = await readValidatedBody(event, noteSchema.parse);\n",[166,2432,2433],{"__ignoreMap":261},[265,2434,2435,2438,2440,2442,2444,2446,2448,2450,2452,2454],{"class":267,"line":268},[265,2436,2437],{"class":329},"const",[265,2439,2033],{"class":292},[265,2441,1589],{"class":296},[265,2443,108],{"class":292},[265,2445,2065],{"class":296},[265,2447,2039],{"class":292},[265,2449,330],{"class":329},[265,2451,890],{"class":329},[265,2453,2074],{"class":278},[265,2455,2077],{"class":292},[10,2457,2458,2459,1499,2462,2465],{},"Create a new file ",[166,2460,2461],{},"note.schema.ts",[166,2463,2464],{},"shared\u002Fschemas"," folder in the project root directory with the following content:",[256,2467,2469],{"className":836,"code":2468,"language":838,"meta":261,"style":261},"\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",[166,2470,2471,2476,2490,2504,2518,2522,2539,2555,2560,2579,2598,2617,2627,2635,2643,2647,2651,2667,2683],{"__ignoreMap":261},[265,2472,2473],{"class":267,"line":268},[265,2474,2475],{"class":271},"\u002F\u002F shared\u002Fschemas\u002Fnote.schema.ts\n",[265,2477,2478,2480,2483,2485,2488],{"class":267,"line":275},[265,2479,1425],{"class":329},[265,2481,2482],{"class":292}," { createInsertSchema, createSelectSchema } ",[265,2484,1431],{"class":329},[265,2486,2487],{"class":282}," \"drizzle-zod\"",[265,2489,712],{"class":292},[265,2491,2492,2494,2497,2499,2502],{"class":267,"line":476},[265,2493,1425],{"class":329},[265,2495,2496],{"class":292}," { z } ",[265,2498,1431],{"class":329},[265,2500,2501],{"class":282}," \"zod\"",[265,2503,712],{"class":292},[265,2505,2506,2508,2511,2513,2516],{"class":267,"line":483},[265,2507,1425],{"class":329},[265,2509,2510],{"class":292}," { notes } ",[265,2512,1431],{"class":329},[265,2514,2515],{"class":282}," \"~~\u002Fserver\u002Fdatabase\u002Fschema\"",[265,2517,712],{"class":292},[265,2519,2520],{"class":267,"line":495},[265,2521,480],{"emptyLinePlaceholder":479},[265,2523,2524,2526,2528,2531,2533,2536],{"class":267,"line":500},[265,2525,438],{"class":329},[265,2527,1566],{"class":329},[265,2529,2530],{"class":296}," noteSchema",[265,2532,887],{"class":329},[265,2534,2535],{"class":278}," createInsertSchema",[265,2537,2538],{"class":292},"(notes, {\n",[265,2540,2541,2544,2547,2550,2552],{"class":267,"line":506},[265,2542,2543],{"class":278},"  text",[265,2545,2546],{"class":292},": (",[265,2548,2549],{"class":866},"schema",[265,2551,870],{"class":292},[265,2553,2554],{"class":329},"=>\n",[265,2556,2557],{"class":267,"line":512},[265,2558,2559],{"class":292},"    schema.text\n",[265,2561,2562,2564,2567,2569,2572,2574,2577],{"class":267,"line":524},[265,2563,2101],{"class":292},[265,2565,2566],{"class":278},"min",[265,2568,857],{"class":292},[265,2570,2571],{"class":296},"3",[265,2573,108],{"class":292},[265,2575,2576],{"class":282},"\"Note must be at least 3 characters long\"",[265,2578,1597],{"class":292},[265,2580,2581,2583,2586,2588,2591,2593,2596],{"class":267,"line":530},[265,2582,2101],{"class":292},[265,2584,2585],{"class":278},"max",[265,2587,857],{"class":292},[265,2589,2590],{"class":296},"5000",[265,2592,108],{"class":292},[265,2594,2595],{"class":282},"\"Note cannot exceed 5000 characters\"",[265,2597,1711],{"class":292},[265,2599,2600,2603,2605,2607,2610,2612,2615],{"class":267,"line":536},[265,2601,2602],{"class":292},"  audioUrls: z.",[265,2604,1795],{"class":278},[265,2606,1031],{"class":292},[265,2608,2609],{"class":278},"array",[265,2611,1031],{"class":292},[265,2613,2614],{"class":278},"optional",[265,2616,1671],{"class":292},[265,2618,2619,2622,2625],{"class":267,"line":541},[265,2620,2621],{"class":292},"}).",[265,2623,2624],{"class":278},"pick",[265,2626,447],{"class":292},[265,2628,2629,2631,2633],{"class":267,"line":552},[265,2630,1656],{"class":292},[265,2632,489],{"class":296},[265,2634,521],{"class":292},[265,2636,2637,2639,2641],{"class":267,"line":563},[265,2638,1770],{"class":292},[265,2640,489],{"class":296},[265,2642,521],{"class":292},[265,2644,2645],{"class":267,"line":568},[265,2646,666],{"class":292},[265,2648,2649],{"class":267,"line":574},[265,2650,480],{"emptyLinePlaceholder":479},[265,2652,2653,2655,2657,2660,2662,2665],{"class":267,"line":584},[265,2654,438],{"class":329},[265,2656,1566],{"class":329},[265,2658,2659],{"class":296}," noteSelectSchema",[265,2661,887],{"class":329},[265,2663,2664],{"class":278}," createSelectSchema",[265,2666,2538],{"class":292},[265,2668,2669,2671,2673,2675,2677,2679,2681],{"class":267,"line":594},[265,2670,2602],{"class":292},[265,2672,1795],{"class":278},[265,2674,1031],{"class":292},[265,2676,2609],{"class":278},[265,2678,1031],{"class":292},[265,2680,2614],{"class":278},[265,2682,1671],{"class":292},[265,2684,2685],{"class":267,"line":604},[265,2686,666],{"class":292},[10,2688,2689,2690,2693,2694,2697],{},"The above code uses the ",[166,2691,2692],{},"drizzle-zod"," plugin to create the ",[166,2695,2696],{},"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.).",[185,2699,2701],{"id":2700},"creating-db-migrations","Creating DB Migrations",[10,2703,2704,2705,2708],{},"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 ",[166,2706,2707],{},"package.json","'s scripts:",[256,2710,2714],{"className":2711,"code":2712,"language":2713,"meta":261,"style":261},"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","json",[166,2715,2716,2721,2729,2734,2745,2749],{"__ignoreMap":261},[265,2717,2718],{"class":267,"line":268},[265,2719,2720],{"class":271},"\u002F\u002F ..\n",[265,2722,2723,2726],{"class":267,"line":275},[265,2724,2725],{"class":282},"\"scripts\"",[265,2727,2728],{"class":292},": {\n",[265,2730,2731],{"class":267,"line":476},[265,2732,2733],{"class":271},"  \u002F\u002F ..\n",[265,2735,2736,2739,2742],{"class":267,"line":483},[265,2737,2738],{"class":296},"  \"db:generate\"",[265,2740,2741],{"class":292},": ",[265,2743,2744],{"class":282},"\"drizzle-kit generate\"\n",[265,2746,2747],{"class":267,"line":495},[265,2748,1933],{"class":292},[265,2750,2751],{"class":267,"line":500},[265,2752,2720],{"class":271},[10,2754,2755,2756,2759,2760,2763],{},"Next, run ",[166,2757,2758],{},"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 ",[166,2761,2762],{},"pnpm dev"," and checking the Nuxt Dev Tools as shown below (this is a local sqlite database that is used in the dev mode).",[10,2765,2766],{},[45,2767],{"alt":2768,"src":2769},"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",[10,2771,2772],{},"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,",[50,2774,2776],{"id":2775},"creating-the-basic-frontend","Creating the Basic Frontend",[10,2778,2779],{},"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.",[185,2781,2783,2786],{"id":2782},"usemediarecorder-composable",[166,2784,2785],{},"useMediaRecorder"," Composable",[10,2788,2789,2790,1215,2793,1219],{},"Let’s create a composable to handle the media recording functionality. Create a new file ",[166,2791,2792],{},"useMediaRecorder.ts",[166,2794,2795],{},"app\u002Fcomposables",[256,2797,2799],{"className":836,"code":2798,"language":838,"meta":261,"style":261},"\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",[166,2800,2801,2806,2816,2828,2840,2856,2867,2871,2875,2891,2903,2910,2917,2924,2931,2938,2943,2947,2954,2986,2991,2996,3000,3011,3031,3040,3050,3060,3069,3074,3079,3103,3126,3149,3171,3198,3203,3219,3247,3256,3265,3277,3283,3288,3296,3302,3307,3319,3333,3347,3353,3358,3377,3385,3412,3417,3433,3449,3454,3472,3484,3489,3501,3512,3523,3529,3534,3549,3560,3565,3592,3604,3616,3621,3626,3641,3654,3667,3679,3694,3699,3707,3717,3732,3741,3746,3751,3756,3774,3802,3816,3830,3847,3864,3876,3881,3893,3905,3917,3922,3931,3937,3942,3955,3965,3996,4001,4009,4017,4029,4035,4040,4051,4063,4068,4073,4078,4083,4095,4103,4108,4113,4120,4132,4138,4144,4149],{"__ignoreMap":261},[265,2802,2803],{"class":267,"line":268},[265,2804,2805],{"class":271},"\u002F\u002F app\u002Fcomposables\u002FuseMediaRecorder.ts\n",[265,2807,2808,2811,2814],{"class":267,"line":275},[265,2809,2810],{"class":329},"interface",[265,2812,2813],{"class":278}," MediaRecorderState",[265,2815,876],{"class":292},[265,2817,2818,2821,2823,2826],{"class":267,"line":476},[265,2819,2820],{"class":866},"  isRecording",[265,2822,2158],{"class":329},[265,2824,2825],{"class":296}," boolean",[265,2827,712],{"class":292},[265,2829,2830,2833,2835,2838],{"class":267,"line":483},[265,2831,2832],{"class":866},"  recordingDuration",[265,2834,2158],{"class":329},[265,2836,2837],{"class":296}," number",[265,2839,712],{"class":292},[265,2841,2842,2845,2847,2849,2852,2854],{"class":267,"line":495},[265,2843,2844],{"class":866},"  audioData",[265,2846,2158],{"class":329},[265,2848,1053],{"class":278},[265,2850,2851],{"class":329}," |",[265,2853,2161],{"class":296},[265,2855,712],{"class":292},[265,2857,2858,2861,2863,2865],{"class":267,"line":500},[265,2859,2860],{"class":866},"  updateTrigger",[265,2862,2158],{"class":329},[265,2864,2837],{"class":296},[265,2866,712],{"class":292},[265,2868,2869],{"class":267,"line":506},[265,2870,1933],{"class":292},[265,2872,2873],{"class":267,"line":512},[265,2874,480],{"emptyLinePlaceholder":479},[265,2876,2877,2879,2882,2884,2887,2889],{"class":267,"line":524},[265,2878,2437],{"class":329},[265,2880,2881],{"class":278}," getSupportedMimeType",[265,2883,887],{"class":329},[265,2885,2886],{"class":292}," () ",[265,2888,873],{"class":329},[265,2890,876],{"class":292},[265,2892,2893,2895,2898,2900],{"class":267,"line":530},[265,2894,881],{"class":329},[265,2896,2897],{"class":296}," types",[265,2899,887],{"class":329},[265,2901,2902],{"class":292}," [\n",[265,2904,2905,2908],{"class":267,"line":536},[265,2906,2907],{"class":282},"    \"audio\u002Fmp4\"",[265,2909,521],{"class":292},[265,2911,2912,2915],{"class":267,"line":541},[265,2913,2914],{"class":282},"    \"audio\u002Fmp4;codecs=mp4a\"",[265,2916,521],{"class":292},[265,2918,2919,2922],{"class":267,"line":552},[265,2920,2921],{"class":282},"    \"audio\u002Fmpeg\"",[265,2923,521],{"class":292},[265,2925,2926,2929],{"class":267,"line":563},[265,2927,2928],{"class":282},"    \"audio\u002Fwebm;codecs=opus\"",[265,2930,521],{"class":292},[265,2932,2933,2936],{"class":267,"line":568},[265,2934,2935],{"class":282},"    \"audio\u002Fwebm\"",[265,2937,521],{"class":292},[265,2939,2940],{"class":267,"line":574},[265,2941,2942],{"class":292},"  ];\n",[265,2944,2945],{"class":267,"line":584},[265,2946,480],{"emptyLinePlaceholder":479},[265,2948,2949,2951],{"class":267,"line":594},[265,2950,1256],{"class":329},[265,2952,2953],{"class":292}," (\n",[265,2955,2956,2959,2962,2964,2967,2969,2971,2974,2977,2980,2983],{"class":267,"line":604},[265,2957,2958],{"class":292},"    types.",[265,2960,2961],{"class":278},"find",[265,2963,2138],{"class":292},[265,2965,2966],{"class":866},"type",[265,2968,870],{"class":292},[265,2970,873],{"class":329},[265,2972,2973],{"class":292}," MediaRecorder.",[265,2975,2976],{"class":278},"isTypeSupported",[265,2978,2979],{"class":292},"(type)) ",[265,2981,2982],{"class":329},"||",[265,2984,2985],{"class":282}," \"audio\u002Fwebm\"\n",[265,2987,2988],{"class":267,"line":609},[265,2989,2990],{"class":292},"  );\n",[265,2992,2993],{"class":267,"line":614},[265,2994,2995],{"class":292},"};\n",[265,2997,2998],{"class":267,"line":625},[265,2999,480],{"emptyLinePlaceholder":479},[265,3001,3002,3004,3006,3009],{"class":267,"line":630},[265,3003,438],{"class":329},[265,3005,1907],{"class":329},[265,3007,3008],{"class":278}," useMediaRecorder",[265,3010,1913],{"class":292},[265,3012,3013,3015,3018,3020,3023,3025,3028],{"class":267,"line":636},[265,3014,881],{"class":329},[265,3016,3017],{"class":296}," state",[265,3019,887],{"class":329},[265,3021,3022],{"class":278}," ref",[265,3024,1792],{"class":292},[265,3026,3027],{"class":278},"MediaRecorderState",[265,3029,3030],{"class":292},">({\n",[265,3032,3033,3036,3038],{"class":267,"line":642},[265,3034,3035],{"class":292},"    isRecording: ",[265,3037,648],{"class":296},[265,3039,521],{"class":292},[265,3041,3042,3045,3048],{"class":267,"line":653},[265,3043,3044],{"class":292},"    recordingDuration: ",[265,3046,3047],{"class":296},"0",[265,3049,521],{"class":292},[265,3051,3052,3055,3058],{"class":267,"line":658},[265,3053,3054],{"class":292},"    audioData: ",[265,3056,3057],{"class":296},"null",[265,3059,521],{"class":292},[265,3061,3062,3065,3067],{"class":267,"line":663},[265,3063,3064],{"class":292},"    updateTrigger: ",[265,3066,3047],{"class":296},[265,3068,521],{"class":292},[265,3070,3072],{"class":267,"line":3071},29,[265,3073,1346],{"class":292},[265,3075,3077],{"class":267,"line":3076},30,[265,3078,480],{"emptyLinePlaceholder":479},[265,3080,3082,3085,3088,3090,3093,3095,3097,3099,3101],{"class":267,"line":3081},31,[265,3083,3084],{"class":329},"  let",[265,3086,3087],{"class":292}," mediaRecorder",[265,3089,2158],{"class":329},[265,3091,3092],{"class":278}," MediaRecorder",[265,3094,2851],{"class":329},[265,3096,2161],{"class":296},[265,3098,887],{"class":329},[265,3100,2161],{"class":296},[265,3102,712],{"class":292},[265,3104,3106,3108,3111,3113,3116,3118,3120,3122,3124],{"class":267,"line":3105},32,[265,3107,3084],{"class":329},[265,3109,3110],{"class":292}," audioContext",[265,3112,2158],{"class":329},[265,3114,3115],{"class":278}," AudioContext",[265,3117,2851],{"class":329},[265,3119,2161],{"class":296},[265,3121,887],{"class":329},[265,3123,2161],{"class":296},[265,3125,712],{"class":292},[265,3127,3129,3131,3134,3136,3139,3141,3143,3145,3147],{"class":267,"line":3128},33,[265,3130,3084],{"class":329},[265,3132,3133],{"class":292}," analyser",[265,3135,2158],{"class":329},[265,3137,3138],{"class":278}," AnalyserNode",[265,3140,2851],{"class":329},[265,3142,2161],{"class":296},[265,3144,887],{"class":329},[265,3146,2161],{"class":296},[265,3148,712],{"class":292},[265,3150,3152,3154,3157,3159,3161,3163,3165,3167,3169],{"class":267,"line":3151},34,[265,3153,3084],{"class":329},[265,3155,3156],{"class":292}," animationFrame",[265,3158,2158],{"class":329},[265,3160,2837],{"class":296},[265,3162,2851],{"class":329},[265,3164,2161],{"class":296},[265,3166,887],{"class":329},[265,3168,2161],{"class":296},[265,3170,712],{"class":292},[265,3172,3174,3176,3179,3181,3183,3186,3189,3192,3194,3196],{"class":267,"line":3173},35,[265,3175,3084],{"class":329},[265,3177,3178],{"class":292}," audioChunks",[265,3180,2158],{"class":329},[265,3182,924],{"class":278},[265,3184,3185],{"class":292},"[] ",[265,3187,3188],{"class":329},"|",[265,3190,3191],{"class":296}," undefined",[265,3193,887],{"class":329},[265,3195,3191],{"class":296},[265,3197,712],{"class":292},[265,3199,3201],{"class":267,"line":3200},36,[265,3202,480],{"emptyLinePlaceholder":479},[265,3204,3206,3208,3211,3213,3215,3217],{"class":267,"line":3205},37,[265,3207,881],{"class":329},[265,3209,3210],{"class":278}," updateAudioData",[265,3212,887],{"class":329},[265,3214,2886],{"class":292},[265,3216,873],{"class":329},[265,3218,876],{"class":292},[265,3220,3222,3225,3227,3229,3232,3234,3237,3240,3242,3244],{"class":267,"line":3221},38,[265,3223,3224],{"class":329},"    if",[265,3226,863],{"class":292},[265,3228,936],{"class":329},[265,3230,3231],{"class":292},"analyser ",[265,3233,2982],{"class":329},[265,3235,3236],{"class":329}," !",[265,3238,3239],{"class":292},"state.value.isRecording ",[265,3241,2982],{"class":329},[265,3243,3236],{"class":329},[265,3245,3246],{"class":292},"state.value.audioData) {\n",[265,3248,3250,3253],{"class":267,"line":3249},39,[265,3251,3252],{"class":329},"      if",[265,3254,3255],{"class":292}," (animationFrame) {\n",[265,3257,3259,3262],{"class":267,"line":3258},40,[265,3260,3261],{"class":278},"        cancelAnimationFrame",[265,3263,3264],{"class":292},"(animationFrame);\n",[265,3266,3268,3271,3273,3275],{"class":267,"line":3267},41,[265,3269,3270],{"class":292},"        animationFrame ",[265,3272,330],{"class":329},[265,3274,2161],{"class":296},[265,3276,712],{"class":292},[265,3278,3280],{"class":267,"line":3279},42,[265,3281,3282],{"class":292},"      }\n",[265,3284,3286],{"class":267,"line":3285},43,[265,3287,480],{"emptyLinePlaceholder":479},[265,3289,3291,3294],{"class":267,"line":3290},44,[265,3292,3293],{"class":329},"      return",[265,3295,712],{"class":292},[265,3297,3299],{"class":267,"line":3298},45,[265,3300,3301],{"class":292},"    }\n",[265,3303,3305],{"class":267,"line":3304},46,[265,3306,480],{"emptyLinePlaceholder":479},[265,3308,3310,3313,3316],{"class":267,"line":3309},47,[265,3311,3312],{"class":292},"    analyser.",[265,3314,3315],{"class":278},"getByteTimeDomainData",[265,3317,3318],{"class":292},"(state.value.audioData);\n",[265,3320,3322,3325,3328,3331],{"class":267,"line":3321},48,[265,3323,3324],{"class":292},"    state.value.updateTrigger ",[265,3326,3327],{"class":329},"+=",[265,3329,3330],{"class":296}," 1",[265,3332,712],{"class":292},[265,3334,3336,3339,3341,3344],{"class":267,"line":3335},49,[265,3337,3338],{"class":292},"    animationFrame ",[265,3340,330],{"class":329},[265,3342,3343],{"class":278}," requestAnimationFrame",[265,3345,3346],{"class":292},"(updateAudioData);\n",[265,3348,3350],{"class":267,"line":3349},50,[265,3351,3352],{"class":292},"  };\n",[265,3354,3356],{"class":267,"line":3355},51,[265,3357,480],{"emptyLinePlaceholder":479},[265,3359,3361,3363,3366,3368,3371,3373,3375],{"class":267,"line":3360},52,[265,3362,881],{"class":329},[265,3364,3365],{"class":278}," startRecording",[265,3367,887],{"class":329},[265,3369,3370],{"class":329}," async",[265,3372,2886],{"class":292},[265,3374,873],{"class":329},[265,3376,876],{"class":292},[265,3378,3380,3383],{"class":267,"line":3379},53,[265,3381,3382],{"class":329},"    try",[265,3384,876],{"class":292},[265,3386,3388,3391,3394,3396,3398,3401,3404,3407,3409],{"class":267,"line":3387},54,[265,3389,3390],{"class":329},"      const",[265,3392,3393],{"class":296}," stream",[265,3395,887],{"class":329},[265,3397,890],{"class":329},[265,3399,3400],{"class":292}," navigator.mediaDevices.",[265,3402,3403],{"class":278},"getUserMedia",[265,3405,3406],{"class":292},"({ audio: ",[265,3408,489],{"class":296},[265,3410,3411],{"class":292}," });\n",[265,3413,3415],{"class":267,"line":3414},55,[265,3416,480],{"emptyLinePlaceholder":479},[265,3418,3420,3423,3425,3428,3430],{"class":267,"line":3419},56,[265,3421,3422],{"class":292},"      audioContext ",[265,3424,330],{"class":329},[265,3426,3427],{"class":329}," new",[265,3429,3115],{"class":278},[265,3431,3432],{"class":292},"();\n",[265,3434,3436,3439,3441,3444,3447],{"class":267,"line":3435},57,[265,3437,3438],{"class":292},"      analyser ",[265,3440,330],{"class":329},[265,3442,3443],{"class":292}," audioContext.",[265,3445,3446],{"class":278},"createAnalyser",[265,3448,3432],{"class":292},[265,3450,3452],{"class":267,"line":3451},58,[265,3453,480],{"emptyLinePlaceholder":479},[265,3455,3457,3459,3462,3464,3466,3469],{"class":267,"line":3456},59,[265,3458,3390],{"class":329},[265,3460,3461],{"class":296}," source",[265,3463,887],{"class":329},[265,3465,3443],{"class":292},[265,3467,3468],{"class":278},"createMediaStreamSource",[265,3470,3471],{"class":292},"(stream);\n",[265,3473,3475,3478,3481],{"class":267,"line":3474},60,[265,3476,3477],{"class":292},"      source.",[265,3479,3480],{"class":278},"connect",[265,3482,3483],{"class":292},"(analyser);\n",[265,3485,3487],{"class":267,"line":3486},61,[265,3488,480],{"emptyLinePlaceholder":479},[265,3490,3492,3494,3497,3499],{"class":267,"line":3491},62,[265,3493,3390],{"class":329},[265,3495,3496],{"class":296}," options",[265,3498,887],{"class":329},[265,3500,876],{"class":292},[265,3502,3504,3507,3510],{"class":267,"line":3503},63,[265,3505,3506],{"class":292},"        mimeType: ",[265,3508,3509],{"class":278},"getSupportedMimeType",[265,3511,1671],{"class":292},[265,3513,3515,3518,3521],{"class":267,"line":3514},64,[265,3516,3517],{"class":292},"        audioBitsPerSecond: ",[265,3519,3520],{"class":296},"64000",[265,3522,521],{"class":292},[265,3524,3526],{"class":267,"line":3525},65,[265,3527,3528],{"class":292},"      };\n",[265,3530,3532],{"class":267,"line":3531},66,[265,3533,480],{"emptyLinePlaceholder":479},[265,3535,3537,3540,3542,3544,3546],{"class":267,"line":3536},67,[265,3538,3539],{"class":292},"      mediaRecorder ",[265,3541,330],{"class":329},[265,3543,3427],{"class":329},[265,3545,3092],{"class":278},[265,3547,3548],{"class":292},"(stream, options);\n",[265,3550,3552,3555,3557],{"class":267,"line":3551},68,[265,3553,3554],{"class":292},"      audioChunks ",[265,3556,330],{"class":329},[265,3558,3559],{"class":292}," [];\n",[265,3561,3563],{"class":267,"line":3562},69,[265,3564,480],{"emptyLinePlaceholder":479},[265,3566,3568,3571,3574,3576,3578,3581,3583,3586,3588,3590],{"class":267,"line":3567},70,[265,3569,3570],{"class":292},"      mediaRecorder.",[265,3572,3573],{"class":278},"ondataavailable",[265,3575,887],{"class":329},[265,3577,863],{"class":292},[265,3579,3580],{"class":866},"e",[265,3582,2158],{"class":329},[265,3584,3585],{"class":278}," BlobEvent",[265,3587,870],{"class":292},[265,3589,873],{"class":329},[265,3591,876],{"class":292},[265,3593,3595,3598,3601],{"class":267,"line":3594},71,[265,3596,3597],{"class":292},"        audioChunks?.",[265,3599,3600],{"class":278},"push",[265,3602,3603],{"class":292},"(e.data);\n",[265,3605,3607,3610,3612,3614],{"class":267,"line":3606},72,[265,3608,3609],{"class":292},"        state.value.recordingDuration ",[265,3611,3327],{"class":329},[265,3613,3330],{"class":296},[265,3615,712],{"class":292},[265,3617,3619],{"class":267,"line":3618},73,[265,3620,3528],{"class":292},[265,3622,3624],{"class":267,"line":3623},74,[265,3625,480],{"emptyLinePlaceholder":479},[265,3627,3629,3632,3634,3636,3638],{"class":267,"line":3628},75,[265,3630,3631],{"class":292},"      state.value.audioData ",[265,3633,330],{"class":329},[265,3635,3427],{"class":329},[265,3637,1053],{"class":278},[265,3639,3640],{"class":292},"(analyser.frequencyBinCount);\n",[265,3642,3644,3647,3649,3652],{"class":267,"line":3643},76,[265,3645,3646],{"class":292},"      state.value.isRecording ",[265,3648,330],{"class":329},[265,3650,3651],{"class":296}," true",[265,3653,712],{"class":292},[265,3655,3657,3660,3662,3665],{"class":267,"line":3656},77,[265,3658,3659],{"class":292},"      state.value.recordingDuration ",[265,3661,330],{"class":329},[265,3663,3664],{"class":296}," 0",[265,3666,712],{"class":292},[265,3668,3670,3673,3675,3677],{"class":267,"line":3669},78,[265,3671,3672],{"class":292},"      state.value.updateTrigger ",[265,3674,330],{"class":329},[265,3676,3664],{"class":296},[265,3678,712],{"class":292},[265,3680,3682,3684,3687,3689,3692],{"class":267,"line":3681},79,[265,3683,3570],{"class":292},[265,3685,3686],{"class":278},"start",[265,3688,857],{"class":292},[265,3690,3691],{"class":296},"1000",[265,3693,2188],{"class":292},[265,3695,3697],{"class":267,"line":3696},80,[265,3698,480],{"emptyLinePlaceholder":479},[265,3700,3702,3705],{"class":267,"line":3701},81,[265,3703,3704],{"class":278},"      updateAudioData",[265,3706,3432],{"class":292},[265,3708,3710,3713,3715],{"class":267,"line":3709},82,[265,3711,3712],{"class":292},"    } ",[265,3714,1091],{"class":329},[265,3716,1094],{"class":292},[265,3718,3720,3723,3725,3727,3730],{"class":267,"line":3719},83,[265,3721,3722],{"class":292},"      console.",[265,3724,1102],{"class":278},[265,3726,857],{"class":292},[265,3728,3729],{"class":282},"\"Error accessing microphone:\"",[265,3731,1110],{"class":292},[265,3733,3735,3738],{"class":267,"line":3734},84,[265,3736,3737],{"class":329},"      throw",[265,3739,3740],{"class":292}," err;\n",[265,3742,3744],{"class":267,"line":3743},85,[265,3745,3301],{"class":292},[265,3747,3749],{"class":267,"line":3748},86,[265,3750,3352],{"class":292},[265,3752,3754],{"class":267,"line":3753},87,[265,3755,480],{"emptyLinePlaceholder":479},[265,3757,3759,3761,3764,3766,3768,3770,3772],{"class":267,"line":3758},88,[265,3760,881],{"class":329},[265,3762,3763],{"class":278}," stopRecording",[265,3765,887],{"class":329},[265,3767,3370],{"class":329},[265,3769,2886],{"class":292},[265,3771,873],{"class":329},[265,3773,876],{"class":292},[265,3775,3777,3779,3781,3783,3786,3788,3790,3793,3796,3798,3800],{"class":267,"line":3776},89,[265,3778,1080],{"class":329},[265,3780,890],{"class":329},[265,3782,3427],{"class":329},[265,3784,3785],{"class":296}," Promise",[265,3787,1792],{"class":292},[265,3789,1159],{"class":278},[265,3791,3792],{"class":292},">((",[265,3794,3795],{"class":866},"resolve",[265,3797,870],{"class":292},[265,3799,873],{"class":329},[265,3801,876],{"class":292},[265,3803,3805,3807,3810,3813],{"class":267,"line":3804},90,[265,3806,3252],{"class":329},[265,3808,3809],{"class":292}," (mediaRecorder ",[265,3811,3812],{"class":329},"&&",[265,3814,3815],{"class":292}," state.value.isRecording) {\n",[265,3817,3819,3822,3825,3827],{"class":267,"line":3818},91,[265,3820,3821],{"class":329},"        const",[265,3823,3824],{"class":296}," mimeType",[265,3826,887],{"class":329},[265,3828,3829],{"class":292}," mediaRecorder.mimeType;\n",[265,3831,3833,3836,3839,3841,3843,3845],{"class":267,"line":3832},92,[265,3834,3835],{"class":292},"        mediaRecorder.",[265,3837,3838],{"class":278},"onstop",[265,3840,887],{"class":329},[265,3842,2886],{"class":292},[265,3844,873],{"class":329},[265,3846,876],{"class":292},[265,3848,3850,3853,3855,3857,3859,3861],{"class":267,"line":3849},93,[265,3851,3852],{"class":329},"          const",[265,3854,903],{"class":296},[265,3856,887],{"class":329},[265,3858,3427],{"class":329},[265,3860,924],{"class":278},[265,3862,3863],{"class":292},"(audioChunks, { type: mimeType });\n",[265,3865,3867,3870,3872,3874],{"class":267,"line":3866},94,[265,3868,3869],{"class":292},"          audioChunks ",[265,3871,330],{"class":329},[265,3873,3191],{"class":296},[265,3875,712],{"class":292},[265,3877,3879],{"class":267,"line":3878},95,[265,3880,480],{"emptyLinePlaceholder":479},[265,3882,3884,3887,3889,3891],{"class":267,"line":3883},96,[265,3885,3886],{"class":292},"          state.value.recordingDuration ",[265,3888,330],{"class":329},[265,3890,3664],{"class":296},[265,3892,712],{"class":292},[265,3894,3896,3899,3901,3903],{"class":267,"line":3895},97,[265,3897,3898],{"class":292},"          state.value.updateTrigger ",[265,3900,330],{"class":329},[265,3902,3664],{"class":296},[265,3904,712],{"class":292},[265,3906,3908,3911,3913,3915],{"class":267,"line":3907},98,[265,3909,3910],{"class":292},"          state.value.audioData ",[265,3912,330],{"class":329},[265,3914,2161],{"class":296},[265,3916,712],{"class":292},[265,3918,3920],{"class":267,"line":3919},99,[265,3921,480],{"emptyLinePlaceholder":479},[265,3923,3925,3928],{"class":267,"line":3924},100,[265,3926,3927],{"class":278},"          resolve",[265,3929,3930],{"class":292},"(blob);\n",[265,3932,3934],{"class":267,"line":3933},101,[265,3935,3936],{"class":292},"        };\n",[265,3938,3940],{"class":267,"line":3939},102,[265,3941,480],{"emptyLinePlaceholder":479},[265,3943,3945,3948,3950,3953],{"class":267,"line":3944},103,[265,3946,3947],{"class":292},"        state.value.isRecording ",[265,3949,330],{"class":329},[265,3951,3952],{"class":296}," false",[265,3954,712],{"class":292},[265,3956,3958,3960,3963],{"class":267,"line":3957},104,[265,3959,3835],{"class":292},[265,3961,3962],{"class":278},"stop",[265,3964,3432],{"class":292},[265,3966,3968,3971,3974,3976,3979,3981,3984,3986,3988,3991,3993],{"class":267,"line":3967},105,[265,3969,3970],{"class":292},"        mediaRecorder.stream.",[265,3972,3973],{"class":278},"getTracks",[265,3975,1031],{"class":292},[265,3977,3978],{"class":278},"forEach",[265,3980,2138],{"class":292},[265,3982,3983],{"class":866},"track",[265,3985,870],{"class":292},[265,3987,873],{"class":329},[265,3989,3990],{"class":292}," track.",[265,3992,3962],{"class":278},[265,3994,3995],{"class":292},"());\n",[265,3997,3999],{"class":267,"line":3998},106,[265,4000,480],{"emptyLinePlaceholder":479},[265,4002,4004,4007],{"class":267,"line":4003},107,[265,4005,4006],{"class":329},"        if",[265,4008,3255],{"class":292},[265,4010,4012,4015],{"class":267,"line":4011},108,[265,4013,4014],{"class":278},"          cancelAnimationFrame",[265,4016,3264],{"class":292},[265,4018,4020,4023,4025,4027],{"class":267,"line":4019},109,[265,4021,4022],{"class":292},"          animationFrame ",[265,4024,330],{"class":329},[265,4026,2161],{"class":296},[265,4028,712],{"class":292},[265,4030,4032],{"class":267,"line":4031},110,[265,4033,4034],{"class":292},"        }\n",[265,4036,4038],{"class":267,"line":4037},111,[265,4039,480],{"emptyLinePlaceholder":479},[265,4041,4043,4046,4049],{"class":267,"line":4042},112,[265,4044,4045],{"class":292},"        audioContext?.",[265,4047,4048],{"class":278},"close",[265,4050,3432],{"class":292},[265,4052,4054,4057,4059,4061],{"class":267,"line":4053},113,[265,4055,4056],{"class":292},"        audioContext ",[265,4058,330],{"class":329},[265,4060,2161],{"class":296},[265,4062,712],{"class":292},[265,4064,4066],{"class":267,"line":4065},114,[265,4067,3282],{"class":292},[265,4069,4071],{"class":267,"line":4070},115,[265,4072,974],{"class":292},[265,4074,4076],{"class":267,"line":4075},116,[265,4077,3352],{"class":292},[265,4079,4081],{"class":267,"line":4080},117,[265,4082,480],{"emptyLinePlaceholder":479},[265,4084,4086,4089,4091,4093],{"class":267,"line":4085},118,[265,4087,4088],{"class":278},"  onUnmounted",[265,4090,1618],{"class":292},[265,4092,873],{"class":329},[265,4094,876],{"class":292},[265,4096,4098,4101],{"class":267,"line":4097},119,[265,4099,4100],{"class":278},"    stopRecording",[265,4102,3432],{"class":292},[265,4104,4106],{"class":267,"line":4105},120,[265,4107,1346],{"class":292},[265,4109,4111],{"class":267,"line":4110},121,[265,4112,480],{"emptyLinePlaceholder":479},[265,4114,4116,4118],{"class":267,"line":4115},122,[265,4117,1256],{"class":329},[265,4119,876],{"class":292},[265,4121,4123,4126,4129],{"class":267,"line":4122},123,[265,4124,4125],{"class":292},"    state: ",[265,4127,4128],{"class":278},"readonly",[265,4130,4131],{"class":292},"(state),\n",[265,4133,4135],{"class":267,"line":4134},124,[265,4136,4137],{"class":292},"    startRecording,\n",[265,4139,4141],{"class":267,"line":4140},125,[265,4142,4143],{"class":292},"    stopRecording,\n",[265,4145,4147],{"class":267,"line":4146},126,[265,4148,3352],{"class":292},[265,4150,4152],{"class":267,"line":4151},127,[265,4153,1933],{"class":292},[10,4155,1151],{},[125,4157,4158,4161,4175,4185],{},[64,4159,4160],{},"Exposes recording start\u002Fstop functionality along with the current recording readonly state",[64,4162,4163,4164,4167,4168,4171,4172,4174],{},"Captures user’s voice using the ",[166,4165,4166],{},"MediaRecorder"," API when ",[166,4169,4170],{},"startRecording"," function is invoked. The ",[166,4173,4166],{}," API is a simple and efficient way to handle media capture in modern browsers, making it ideal for our use case.",[64,4176,4177,4178,112,4181,4184],{},"Captures audio visualization data using ",[166,4179,4180],{},"AudioContext",[166,4182,4183],{},"AnalyserNode"," and updates it in real-time using animation frames",[64,4186,4187,4188,4190,4191,4194],{},"Cleans up resources and returns the captured audio as a ",[166,4189,1159],{}," when ",[166,4192,4193],{},"stopRecording"," is called or if the component unmounts",[185,4196,4198,4201],{"id":4197},"noteeditormodal-component",[166,4199,4200],{},"NoteEditorModal"," Component",[10,4203,1495,4204,1499,4207,1219],{},[166,4205,4206],{},"NoteEditorModal.vue",[166,4208,4209],{},"app\u002Fcomponents",[256,4211,4215],{"className":4212,"code":4213,"language":4214,"meta":261,"style":261},"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",[166,4216,4217,4222,4227,4232,4237,4242,4247,4252,4257,4262,4267,4272,4276,4281,4286,4291,4296,4301,4306,4311,4316,4320,4325,4330,4335,4340,4345,4350,4355,4360,4364,4369,4374,4379,4384,4389,4394,4398,4403,4408,4413,4418,4423,4428,4433,4438,4443,4448,4452,4456,4461,4466,4471,4476,4480,4485,4489,4493,4498,4503,4507,4512,4517,4521,4526,4530,4535,4540,4545,4550,4555,4559,4563,4568,4573,4578,4583,4587,4591,4596,4601,4606,4611,4616,4620,4625,4629,4634,4638,4643,4648,4653,4658,4662,4666,4671,4676,4681,4686,4690,4694,4699,4704,4708,4712,4717,4722,4727,4731,4736,4741,4746,4750,4754,4758,4763,4767],{"__ignoreMap":261},[265,4218,4219],{"class":267,"line":268},[265,4220,4221],{},"\u003C!-- app\u002Fcomponents\u002FNoteEditorModal.vue -->\n",[265,4223,4224],{"class":267,"line":275},[265,4225,4226],{},"\u003Ctemplate>\n",[265,4228,4229],{"class":267,"line":476},[265,4230,4231],{},"  \u003CUModal\n",[265,4233,4234],{"class":267,"line":483},[265,4235,4236],{},"    fullscreen\n",[265,4238,4239],{"class":267,"line":495},[265,4240,4241],{},"    :close=\"{\n",[265,4243,4244],{"class":267,"line":500},[265,4245,4246],{},"      disabled: isSaving || noteRecorder?.isBusy,\n",[265,4248,4249],{"class":267,"line":506},[265,4250,4251],{},"    }\"\n",[265,4253,4254],{"class":267,"line":512},[265,4255,4256],{},"    :prevent-close=\"isSaving || noteRecorder?.isBusy\"\n",[265,4258,4259],{"class":267,"line":524},[265,4260,4261],{},"    title=\"Create Note\"\n",[265,4263,4264],{"class":267,"line":530},[265,4265,4266],{},"    :ui=\"{\n",[265,4268,4269],{"class":267,"line":536},[265,4270,4271],{},"      body: 'flex-1 w-full max-w-7xl mx-auto flex flex-col md:flex-row gap-4 sm:gap-6 overflow-hidden',\n",[265,4273,4274],{"class":267,"line":541},[265,4275,4251],{},[265,4277,4278],{"class":267,"line":552},[265,4279,4280],{},"  >\n",[265,4282,4283],{"class":267,"line":563},[265,4284,4285],{},"    \u003Ctemplate #body>\n",[265,4287,4288],{"class":267,"line":568},[265,4289,4290],{},"      \u003CUCard class=\"flex-1 flex flex-col\" :ui=\"{ body: 'flex-1' }\">\n",[265,4292,4293],{"class":267,"line":574},[265,4294,4295],{},"        \u003Ctemplate #header>\n",[265,4297,4298],{"class":267,"line":584},[265,4299,4300],{},"          \u003Ch3 class=\"h-8 font-medium text-gray-600 dark:text-gray-300\">\n",[265,4302,4303],{"class":267,"line":594},[265,4304,4305],{},"            Note transcript\n",[265,4307,4308],{"class":267,"line":604},[265,4309,4310],{},"          \u003C\u002Fh3>\n",[265,4312,4313],{"class":267,"line":609},[265,4314,4315],{},"        \u003C\u002Ftemplate>\n",[265,4317,4318],{"class":267,"line":614},[265,4319,480],{"emptyLinePlaceholder":479},[265,4321,4322],{"class":267,"line":625},[265,4323,4324],{},"        \u003CUTextarea\n",[265,4326,4327],{"class":267,"line":630},[265,4328,4329],{},"          v-model=\"noteText\"\n",[265,4331,4332],{"class":267,"line":636},[265,4333,4334],{},"          placeholder=\"Type your note here, or use voice recording...\"\n",[265,4336,4337],{"class":267,"line":642},[265,4338,4339],{},"          size=\"lg\"\n",[265,4341,4342],{"class":267,"line":653},[265,4343,4344],{},"          :disabled=\"isSaving || noteRecorder?.isBusy\"\n",[265,4346,4347],{"class":267,"line":658},[265,4348,4349],{},"          :ui=\"{ root: 'w-full h-full', base: ['h-full resize-none'] }\"\n",[265,4351,4352],{"class":267,"line":663},[265,4353,4354],{},"        \u002F>\n",[265,4356,4357],{"class":267,"line":3071},[265,4358,4359],{},"      \u003C\u002FUCard>\n",[265,4361,4362],{"class":267,"line":3076},[265,4363,480],{"emptyLinePlaceholder":479},[265,4365,4366],{"class":267,"line":3081},[265,4367,4368],{},"      \u003CNoteRecorder\n",[265,4370,4371],{"class":267,"line":3105},[265,4372,4373],{},"        ref=\"recorder\"\n",[265,4375,4376],{"class":267,"line":3128},[265,4377,4378],{},"        class=\"md:h-full md:flex md:flex-col md:w-96 shrink-0 order-first md:order-none\"\n",[265,4380,4381],{"class":267,"line":3151},[265,4382,4383],{},"        @transcription=\"handleTranscription\"\n",[265,4385,4386],{"class":267,"line":3173},[265,4387,4388],{},"      \u002F>\n",[265,4390,4391],{"class":267,"line":3200},[265,4392,4393],{},"    \u003C\u002Ftemplate>\n",[265,4395,4396],{"class":267,"line":3205},[265,4397,480],{"emptyLinePlaceholder":479},[265,4399,4400],{"class":267,"line":3221},[265,4401,4402],{},"    \u003Ctemplate #footer>\n",[265,4404,4405],{"class":267,"line":3249},[265,4406,4407],{},"      \u003CUButton\n",[265,4409,4410],{"class":267,"line":3258},[265,4411,4412],{},"        icon=\"i-lucide-undo-2\"\n",[265,4414,4415],{"class":267,"line":3267},[265,4416,4417],{},"        color=\"neutral\"\n",[265,4419,4420],{"class":267,"line":3279},[265,4421,4422],{},"        variant=\"outline\"\n",[265,4424,4425],{"class":267,"line":3285},[265,4426,4427],{},"        :disabled=\"isSaving\"\n",[265,4429,4430],{"class":267,"line":3290},[265,4431,4432],{},"        @click=\"resetNote\"\n",[265,4434,4435],{"class":267,"line":3298},[265,4436,4437],{},"      >\n",[265,4439,4440],{"class":267,"line":3304},[265,4441,4442],{},"        Reset\n",[265,4444,4445],{"class":267,"line":3309},[265,4446,4447],{},"      \u003C\u002FUButton>\n",[265,4449,4450],{"class":267,"line":3321},[265,4451,480],{"emptyLinePlaceholder":479},[265,4453,4454],{"class":267,"line":3335},[265,4455,4407],{},[265,4457,4458],{"class":267,"line":3349},[265,4459,4460],{},"        icon=\"i-lucide-cloud-upload\"\n",[265,4462,4463],{"class":267,"line":3355},[265,4464,4465],{},"        :disabled=\"!noteText.trim() || noteRecorder?.isBusy || isSaving\"\n",[265,4467,4468],{"class":267,"line":3360},[265,4469,4470],{},"        :loading=\"isSaving\"\n",[265,4472,4473],{"class":267,"line":3379},[265,4474,4475],{},"        @click=\"saveNote\"\n",[265,4477,4478],{"class":267,"line":3387},[265,4479,4437],{},[265,4481,4482],{"class":267,"line":3414},[265,4483,4484],{},"        Save Note\n",[265,4486,4487],{"class":267,"line":3419},[265,4488,4447],{},[265,4490,4491],{"class":267,"line":3435},[265,4492,4393],{},[265,4494,4495],{"class":267,"line":3451},[265,4496,4497],{},"  \u003C\u002FUModal>\n",[265,4499,4500],{"class":267,"line":3456},[265,4501,4502],{},"\u003C\u002Ftemplate>\n",[265,4504,4505],{"class":267,"line":3474},[265,4506,480],{"emptyLinePlaceholder":479},[265,4508,4509],{"class":267,"line":3486},[265,4510,4511],{},"\u003Cscript setup lang=\"ts\">\n",[265,4513,4514],{"class":267,"line":3491},[265,4515,4516],{},"import { NoteRecorder } from \"#components\";\n",[265,4518,4519],{"class":267,"line":3503},[265,4520,480],{"emptyLinePlaceholder":479},[265,4522,4523],{"class":267,"line":3514},[265,4524,4525],{},"const props = defineProps\u003C{ onNewNote: () => void }>();\n",[265,4527,4528],{"class":267,"line":3525},[265,4529,480],{"emptyLinePlaceholder":479},[265,4531,4532],{"class":267,"line":3531},[265,4533,4534],{},"type NoteRecorderType = InstanceType\u003Ctypeof NoteRecorder>;\n",[265,4536,4537],{"class":267,"line":3536},[265,4538,4539],{},"const noteRecorder = useTemplateRef\u003CNoteRecorderType>(\"recorder\");\n",[265,4541,4542],{"class":267,"line":3551},[265,4543,4544],{},"const resetNote = () => {\n",[265,4546,4547],{"class":267,"line":3562},[265,4548,4549],{},"  noteText.value = \"\";\n",[265,4551,4552],{"class":267,"line":3567},[265,4553,4554],{},"  noteRecorder.value?.resetRecordings();\n",[265,4556,4557],{"class":267,"line":3594},[265,4558,2995],{},[265,4560,4561],{"class":267,"line":3606},[265,4562,480],{"emptyLinePlaceholder":479},[265,4564,4565],{"class":267,"line":3618},[265,4566,4567],{},"const noteText = ref(\"\");\n",[265,4569,4570],{"class":267,"line":3623},[265,4571,4572],{},"const handleTranscription = (text: string) => {\n",[265,4574,4575],{"class":267,"line":3628},[265,4576,4577],{},"  noteText.value += noteText.value ? \"\\n\\n\" : \"\";\n",[265,4579,4580],{"class":267,"line":3643},[265,4581,4582],{},"  noteText.value += text ?? \"\";\n",[265,4584,4585],{"class":267,"line":3656},[265,4586,2995],{},[265,4588,4589],{"class":267,"line":3669},[265,4590,480],{"emptyLinePlaceholder":479},[265,4592,4593],{"class":267,"line":3681},[265,4594,4595],{},"const modal = useModal();\n",[265,4597,4598],{"class":267,"line":3696},[265,4599,4600],{},"const isSaving = ref(false);\n",[265,4602,4603],{"class":267,"line":3701},[265,4604,4605],{},"const saveNote = async () => {\n",[265,4607,4608],{"class":267,"line":3709},[265,4609,4610],{},"  const text = noteText.value.trim();\n",[265,4612,4613],{"class":267,"line":3719},[265,4614,4615],{},"  if (!text) return;\n",[265,4617,4618],{"class":267,"line":3734},[265,4619,480],{"emptyLinePlaceholder":479},[265,4621,4622],{"class":267,"line":3743},[265,4623,4624],{},"  isSaving.value = true;\n",[265,4626,4627],{"class":267,"line":3748},[265,4628,480],{"emptyLinePlaceholder":479},[265,4630,4631],{"class":267,"line":3753},[265,4632,4633],{},"  const audioUrls = await noteRecorder.value?.uploadRecordings();\n",[265,4635,4636],{"class":267,"line":3758},[265,4637,480],{"emptyLinePlaceholder":479},[265,4639,4640],{"class":267,"line":3776},[265,4641,4642],{},"  try {\n",[265,4644,4645],{"class":267,"line":3804},[265,4646,4647],{},"    await $fetch(\"\u002Fapi\u002Fnotes\", {\n",[265,4649,4650],{"class":267,"line":3818},[265,4651,4652],{},"      method: \"POST\",\n",[265,4654,4655],{"class":267,"line":3832},[265,4656,4657],{},"      body: { text, audioUrls },\n",[265,4659,4660],{"class":267,"line":3849},[265,4661,974],{},[265,4663,4664],{"class":267,"line":3866},[265,4665,480],{"emptyLinePlaceholder":479},[265,4667,4668],{"class":267,"line":3878},[265,4669,4670],{},"    useToast().add({\n",[265,4672,4673],{"class":267,"line":3883},[265,4674,4675],{},"      title: \"Note Saved\",\n",[265,4677,4678],{"class":267,"line":3895},[265,4679,4680],{},"      description: \"Your note was saved successfully.\",\n",[265,4682,4683],{"class":267,"line":3907},[265,4684,4685],{},"      color: \"success\",\n",[265,4687,4688],{"class":267,"line":3919},[265,4689,974],{},[265,4691,4692],{"class":267,"line":3924},[265,4693,480],{"emptyLinePlaceholder":479},[265,4695,4696],{"class":267,"line":3933},[265,4697,4698],{},"    if (props.onNewNote) {\n",[265,4700,4701],{"class":267,"line":3939},[265,4702,4703],{},"      props.onNewNote();\n",[265,4705,4706],{"class":267,"line":3944},[265,4707,3301],{},[265,4709,4710],{"class":267,"line":3957},[265,4711,480],{"emptyLinePlaceholder":479},[265,4713,4714],{"class":267,"line":3967},[265,4715,4716],{},"    modal.close();\n",[265,4718,4719],{"class":267,"line":3998},[265,4720,4721],{},"  } catch (err) {\n",[265,4723,4724],{"class":267,"line":4003},[265,4725,4726],{},"    console.error(\"Error saving note:\", err);\n",[265,4728,4729],{"class":267,"line":4011},[265,4730,4670],{},[265,4732,4733],{"class":267,"line":4019},[265,4734,4735],{},"      title: \"Save Failed\",\n",[265,4737,4738],{"class":267,"line":4031},[265,4739,4740],{},"      description: \"Failed to save the note.\",\n",[265,4742,4743],{"class":267,"line":4037},[265,4744,4745],{},"      color: \"error\",\n",[265,4747,4748],{"class":267,"line":4042},[265,4749,974],{},[265,4751,4752],{"class":267,"line":4053},[265,4753,979],{},[265,4755,4756],{"class":267,"line":4065},[265,4757,480],{"emptyLinePlaceholder":479},[265,4759,4760],{"class":267,"line":4070},[265,4761,4762],{},"  isSaving.value = false;\n",[265,4764,4765],{"class":267,"line":4075},[265,4766,2995],{},[265,4768,4769],{"class":267,"line":4080},[265,4770,4771],{},"\u003C\u002Fscript>\n",[10,4773,4774],{},"The above modal component does the following:",[125,4776,4777,4784,4794,4800,4810],{},[64,4778,4779,4780,4783],{},"Displays a ",[166,4781,4782],{},"textarea"," for allowing a manual note entry",[64,4785,4786,4787,4790,4791,4793],{},"The modal integrates the ",[166,4788,4789],{},"NoteRecorder"," component for voice recordings and manages the data flow between the recordings and the ",[166,4792,4782],{}," for user notes.",[64,4795,4796,4797,4799],{},"Whenever a new recording is created, it captures the emitted event from the note recorder component, and appends the transcription text to the ",[166,4798,4782],{}," content",[64,4801,4802,4803,4806,4807,4809],{},"When the user clicks the save note button, its first uploads all recordings (if any) by calling the note recorder’s ",[166,4804,4805],{},"uploadRecordings"," method, and then save the note by calling the ",[166,4808,1398],{}," API endpoint created earlier.",[64,4811,4812,4813,4815,4816,4818],{},"The save note button first uploads all recordings (if any) asynchronously by calling the ",[166,4814,4805],{}," method, then sends the note data to the ",[166,4817,1952],{}," endpoint. Upon success, it notifies the parent by executing the callback passed by it, and then closes the modal.",[185,4820,4822,4201],{"id":4821},"noterecorder-component",[166,4823,4789],{},[10,4825,2458,4826,1499,4829,4831],{},[166,4827,4828],{},"NoteRecorder.vue",[166,4830,4209],{}," folder and add the following content to it:",[256,4833,4835],{"className":4212,"code":4834,"language":4214,"meta":261,"style":261},"\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",[166,4836,4837,4842,4846,4851,4855,4860,4864,4868,4873,4878,4882,4887,4892,4897,4902,4907,4912,4916,4920,4925,4930,4935,4940,4945,4949,4954,4958,4962,4967,4972,4977,4982,4987,4992,4996,5001,5006,5011,5016,5021,5026,5031,5035,5040,5045,5050,5055,5060,5064,5069,5073,5077,5082,5087,5092,5097,5102,5106,5110,5114,5118,5122,5127,5132,5136,5141,5146,5150,5155,5159,5163,5167,5172,5176,5181,5186,5191,5196,5201,5206,5210,5214,5218,5223,5227,5232,5236,5241,5245,5250,5255,5259,5263,5267,5271,5275,5280,5285,5289,5294,5299,5303,5307,5312,5316,5321,5325,5329,5334,5338,5342,5346,5350,5355,5360,5365,5369,5374,5379,5383,5388,5393,5398,5403,5408,5412,5417,5422,5428,5434,5440,5446,5451,5456,5461,5466,5471,5477,5483,5488,5494,5500,5506,5511,5517,5522,5528,5533,5539,5545,5550,5555,5560,5566,5572,5577,5583,5589,5595,5601,5607,5613,5619,5625,5630,5635,5640,5645,5651,5657,5662,5667,5672,5678,5684,5690,5695,5700,5705,5711,5716,5722,5727,5733,5739,5745,5751,5756],{"__ignoreMap":261},[265,4838,4839],{"class":267,"line":268},[265,4840,4841],{},"\u003C!-- app\u002Fcomponents\u002FNoteRecorder.vue --> \n",[265,4843,4844],{"class":267,"line":275},[265,4845,4226],{},[265,4847,4848],{"class":267,"line":476},[265,4849,4850],{},"  \u003CUCard\n",[265,4852,4853],{"class":267,"line":483},[265,4854,4266],{},[265,4856,4857],{"class":267,"line":495},[265,4858,4859],{},"      body: 'max-h-36 md:max-h-none md:flex-1 overflow-y-auto',\n",[265,4861,4862],{"class":267,"line":500},[265,4863,4251],{},[265,4865,4866],{"class":267,"line":506},[265,4867,4280],{},[265,4869,4870],{"class":267,"line":512},[265,4871,4872],{},"    \u003Ctemplate #header>\n",[265,4874,4875],{"class":267,"line":524},[265,4876,4877],{},"      \u003Ch3 class=\"font-medium text-gray-600 dark:text-gray-300\">Recordings\u003C\u002Fh3>\n",[265,4879,4880],{"class":267,"line":530},[265,4881,480],{"emptyLinePlaceholder":479},[265,4883,4884],{"class":267,"line":536},[265,4885,4886],{},"      \u003Cdiv class=\"flex items-center gap-x-2\">\n",[265,4888,4889],{"class":267,"line":541},[265,4890,4891],{},"        \u003Ctemplate v-if=\"state.isRecording\">\n",[265,4893,4894],{"class":267,"line":552},[265,4895,4896],{},"          \u003Cdiv class=\"w-2 h-2 rounded-full bg-red-500 animate-pulse\" \u002F>\n",[265,4898,4899],{"class":267,"line":563},[265,4900,4901],{},"          \u003Cspan class=\"mr-2 text-sm\">\n",[265,4903,4904],{"class":267,"line":568},[265,4905,4906],{},"            {{ formatDuration(state.recordingDuration) }}\n",[265,4908,4909],{"class":267,"line":574},[265,4910,4911],{},"          \u003C\u002Fspan>\n",[265,4913,4914],{"class":267,"line":584},[265,4915,4315],{},[265,4917,4918],{"class":267,"line":594},[265,4919,480],{"emptyLinePlaceholder":479},[265,4921,4922],{"class":267,"line":604},[265,4923,4924],{},"        \u003CUButton\n",[265,4926,4927],{"class":267,"line":609},[265,4928,4929],{},"          :icon=\"state.isRecording ? 'i-lucide-circle-stop' : 'i-lucide-mic'\"\n",[265,4931,4932],{"class":267,"line":614},[265,4933,4934],{},"          :color=\"state.isRecording ? 'error' : 'primary'\"\n",[265,4936,4937],{"class":267,"line":625},[265,4938,4939],{},"          :loading=\"isTranscribing\"\n",[265,4941,4942],{"class":267,"line":630},[265,4943,4944],{},"          @click=\"toggleRecording\"\n",[265,4946,4947],{"class":267,"line":636},[265,4948,4354],{},[265,4950,4951],{"class":267,"line":642},[265,4952,4953],{},"      \u003C\u002Fdiv>\n",[265,4955,4956],{"class":267,"line":653},[265,4957,4393],{},[265,4959,4960],{"class":267,"line":658},[265,4961,480],{"emptyLinePlaceholder":479},[265,4963,4964],{"class":267,"line":663},[265,4965,4966],{},"    \u003CAudioVisualizer\n",[265,4968,4969],{"class":267,"line":3071},[265,4970,4971],{},"      v-if=\"state.isRecording\"\n",[265,4973,4974],{"class":267,"line":3076},[265,4975,4976],{},"      class=\"w-full h-14 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg mb-2\"\n",[265,4978,4979],{"class":267,"line":3081},[265,4980,4981],{},"      :audio-data=\"state.audioData\"\n",[265,4983,4984],{"class":267,"line":3105},[265,4985,4986],{},"      :data-update-trigger=\"state.updateTrigger\"\n",[265,4988,4989],{"class":267,"line":3128},[265,4990,4991],{},"    \u002F>\n",[265,4993,4994],{"class":267,"line":3151},[265,4995,480],{"emptyLinePlaceholder":479},[265,4997,4998],{"class":267,"line":3173},[265,4999,5000],{},"    \u003Cdiv\n",[265,5002,5003],{"class":267,"line":3200},[265,5004,5005],{},"      v-else-if=\"isTranscribing\"\n",[265,5007,5008],{"class":267,"line":3205},[265,5009,5010],{},"      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",[265,5012,5013],{"class":267,"line":3221},[265,5014,5015],{},"    >\n",[265,5017,5018],{"class":267,"line":3249},[265,5019,5020],{},"      \u003CUIcon name=\"i-lucide-refresh-cw\" size=\"size-6\" class=\"animate-spin\" \u002F>\n",[265,5022,5023],{"class":267,"line":3258},[265,5024,5025],{},"      Transcribing...\n",[265,5027,5028],{"class":267,"line":3267},[265,5029,5030],{},"    \u003C\u002Fdiv>\n",[265,5032,5033],{"class":267,"line":3279},[265,5034,480],{"emptyLinePlaceholder":479},[265,5036,5037],{"class":267,"line":3285},[265,5038,5039],{},"    \u003Cdiv class=\"space-y-2\">\n",[265,5041,5042],{"class":267,"line":3290},[265,5043,5044],{},"      \u003Cdiv\n",[265,5046,5047],{"class":267,"line":3298},[265,5048,5049],{},"        v-for=\"recording in recordings\"\n",[265,5051,5052],{"class":267,"line":3304},[265,5053,5054],{},"        :key=\"recording.id\"\n",[265,5056,5057],{"class":267,"line":3309},[265,5058,5059],{},"        class=\"flex items-center gap-x-3 p-2 bg-gray-50 dark:bg-gray-800 rounded-lg\"\n",[265,5061,5062],{"class":267,"line":3321},[265,5063,4437],{},[265,5065,5066],{"class":267,"line":3335},[265,5067,5068],{},"        \u003Caudio :src=\"recording.url\" controls class=\"w-full h-10\" \u002F>\n",[265,5070,5071],{"class":267,"line":3349},[265,5072,480],{"emptyLinePlaceholder":479},[265,5074,5075],{"class":267,"line":3355},[265,5076,4924],{},[265,5078,5079],{"class":267,"line":3360},[265,5080,5081],{},"          icon=\"i-lucide-trash-2\"\n",[265,5083,5084],{"class":267,"line":3379},[265,5085,5086],{},"          color=\"error\"\n",[265,5088,5089],{"class":267,"line":3387},[265,5090,5091],{},"          variant=\"ghost\"\n",[265,5093,5094],{"class":267,"line":3414},[265,5095,5096],{},"          size=\"sm\"\n",[265,5098,5099],{"class":267,"line":3419},[265,5100,5101],{},"          @click=\"removeRecording(recording)\"\n",[265,5103,5104],{"class":267,"line":3435},[265,5105,4354],{},[265,5107,5108],{"class":267,"line":3451},[265,5109,4953],{},[265,5111,5112],{"class":267,"line":3456},[265,5113,5030],{},[265,5115,5116],{"class":267,"line":3474},[265,5117,480],{"emptyLinePlaceholder":479},[265,5119,5120],{"class":267,"line":3486},[265,5121,5000],{},[265,5123,5124],{"class":267,"line":3491},[265,5125,5126],{},"      v-if=\"!recordings.length && !state.isRecording && !isTranscribing\"\n",[265,5128,5129],{"class":267,"line":3503},[265,5130,5131],{},"      class=\"h-full flex flex-col items-center justify-center text-gray-500 dark:text-gray-400\"\n",[265,5133,5134],{"class":267,"line":3514},[265,5135,5015],{},[265,5137,5138],{"class":267,"line":3525},[265,5139,5140],{},"      \u003Cp>No recordings...!\u003C\u002Fp>\n",[265,5142,5143],{"class":267,"line":3531},[265,5144,5145],{},"      \u003Cp class=\"text-sm mt-1\">Tap the mic icon to create one.\u003C\u002Fp>\n",[265,5147,5148],{"class":267,"line":3536},[265,5149,5030],{},[265,5151,5152],{"class":267,"line":3551},[265,5153,5154],{},"  \u003C\u002FUCard>\n",[265,5156,5157],{"class":267,"line":3562},[265,5158,4502],{},[265,5160,5161],{"class":267,"line":3567},[265,5162,480],{"emptyLinePlaceholder":479},[265,5164,5165],{"class":267,"line":3594},[265,5166,4511],{},[265,5168,5169],{"class":267,"line":3606},[265,5170,5171],{},"const emit = defineEmits\u003C{ transcription: [text: string] }>();\n",[265,5173,5174],{"class":267,"line":3618},[265,5175,480],{"emptyLinePlaceholder":479},[265,5177,5178],{"class":267,"line":3623},[265,5179,5180],{},"const { state, startRecording, stopRecording } = useMediaRecorder();\n",[265,5182,5183],{"class":267,"line":3628},[265,5184,5185],{},"const toggleRecording = () => {\n",[265,5187,5188],{"class":267,"line":3643},[265,5189,5190],{},"  if (state.value.isRecording) {\n",[265,5192,5193],{"class":267,"line":3656},[265,5194,5195],{},"    handleRecordingStop();\n",[265,5197,5198],{"class":267,"line":3669},[265,5199,5200],{},"  } else {\n",[265,5202,5203],{"class":267,"line":3681},[265,5204,5205],{},"    handleRecordingStart();\n",[265,5207,5208],{"class":267,"line":3696},[265,5209,979],{},[265,5211,5212],{"class":267,"line":3701},[265,5213,2995],{},[265,5215,5216],{"class":267,"line":3709},[265,5217,480],{"emptyLinePlaceholder":479},[265,5219,5220],{"class":267,"line":3719},[265,5221,5222],{},"const handleRecordingStart = async () => {\n",[265,5224,5225],{"class":267,"line":3734},[265,5226,4642],{},[265,5228,5229],{"class":267,"line":3743},[265,5230,5231],{},"    await startRecording();\n",[265,5233,5234],{"class":267,"line":3748},[265,5235,4721],{},[265,5237,5238],{"class":267,"line":3753},[265,5239,5240],{},"    console.error(\"Error accessing microphone:\", err);\n",[265,5242,5243],{"class":267,"line":3758},[265,5244,4670],{},[265,5246,5247],{"class":267,"line":3776},[265,5248,5249],{},"      title: \"Error\",\n",[265,5251,5252],{"class":267,"line":3804},[265,5253,5254],{},"      description: \"Could not access microphone. Please check permissions.\",\n",[265,5256,5257],{"class":267,"line":3818},[265,5258,4745],{},[265,5260,5261],{"class":267,"line":3832},[265,5262,974],{},[265,5264,5265],{"class":267,"line":3849},[265,5266,979],{},[265,5268,5269],{"class":267,"line":3866},[265,5270,2995],{},[265,5272,5273],{"class":267,"line":3878},[265,5274,480],{"emptyLinePlaceholder":479},[265,5276,5277],{"class":267,"line":3883},[265,5278,5279],{},"const { recordings, addRecording, removeRecording, resetRecordings } =\n",[265,5281,5282],{"class":267,"line":3895},[265,5283,5284],{},"  useRecordings();\n",[265,5286,5287],{"class":267,"line":3907},[265,5288,480],{"emptyLinePlaceholder":479},[265,5290,5291],{"class":267,"line":3919},[265,5292,5293],{},"const handleRecordingStop = async () => {\n",[265,5295,5296],{"class":267,"line":3924},[265,5297,5298],{},"  let blob: Blob | undefined;\n",[265,5300,5301],{"class":267,"line":3933},[265,5302,480],{"emptyLinePlaceholder":479},[265,5304,5305],{"class":267,"line":3939},[265,5306,4642],{},[265,5308,5309],{"class":267,"line":3944},[265,5310,5311],{},"    blob = await stopRecording();\n",[265,5313,5314],{"class":267,"line":3957},[265,5315,4721],{},[265,5317,5318],{"class":267,"line":3967},[265,5319,5320],{},"    console.error(\"Error stopping recording:\", err);\n",[265,5322,5323],{"class":267,"line":3998},[265,5324,4670],{},[265,5326,5327],{"class":267,"line":4003},[265,5328,5249],{},[265,5330,5331],{"class":267,"line":4011},[265,5332,5333],{},"      description: \"Failed to record audio. Please try again.\",\n",[265,5335,5336],{"class":267,"line":4019},[265,5337,4745],{},[265,5339,5340],{"class":267,"line":4031},[265,5341,974],{},[265,5343,5344],{"class":267,"line":4037},[265,5345,979],{},[265,5347,5348],{"class":267,"line":4042},[265,5349,480],{"emptyLinePlaceholder":479},[265,5351,5352],{"class":267,"line":4053},[265,5353,5354],{},"  if (blob) {\n",[265,5356,5357],{"class":267,"line":4065},[265,5358,5359],{},"    try {\n",[265,5361,5362],{"class":267,"line":4070},[265,5363,5364],{},"      const transcription = await transcribeAudio(blob);\n",[265,5366,5367],{"class":267,"line":4075},[265,5368,480],{"emptyLinePlaceholder":479},[265,5370,5371],{"class":267,"line":4080},[265,5372,5373],{},"      if (transcription) {\n",[265,5375,5376],{"class":267,"line":4085},[265,5377,5378],{},"        emit(\"transcription\", transcription);\n",[265,5380,5381],{"class":267,"line":4097},[265,5382,480],{"emptyLinePlaceholder":479},[265,5384,5385],{"class":267,"line":4105},[265,5386,5387],{},"        addRecording({\n",[265,5389,5390],{"class":267,"line":4110},[265,5391,5392],{},"          url: URL.createObjectURL(blob),\n",[265,5394,5395],{"class":267,"line":4115},[265,5396,5397],{},"          blob,\n",[265,5399,5400],{"class":267,"line":4122},[265,5401,5402],{},"          id: `${Date.now()}`,\n",[265,5404,5405],{"class":267,"line":4134},[265,5406,5407],{},"        });\n",[265,5409,5410],{"class":267,"line":4140},[265,5411,3282],{},[265,5413,5414],{"class":267,"line":4146},[265,5415,5416],{},"    } catch (err) {\n",[265,5418,5419],{"class":267,"line":4151},[265,5420,5421],{},"      console.error(\"Error transcribing audio:\", err);\n",[265,5423,5425],{"class":267,"line":5424},128,[265,5426,5427],{},"      useToast().add({\n",[265,5429,5431],{"class":267,"line":5430},129,[265,5432,5433],{},"        title: \"Error\",\n",[265,5435,5437],{"class":267,"line":5436},130,[265,5438,5439],{},"        description: \"Failed to transcribe audio. Please try again.\",\n",[265,5441,5443],{"class":267,"line":5442},131,[265,5444,5445],{},"        color: \"error\",\n",[265,5447,5449],{"class":267,"line":5448},132,[265,5450,2168],{},[265,5452,5454],{"class":267,"line":5453},133,[265,5455,3301],{},[265,5457,5459],{"class":267,"line":5458},134,[265,5460,979],{},[265,5462,5464],{"class":267,"line":5463},135,[265,5465,2995],{},[265,5467,5469],{"class":267,"line":5468},136,[265,5470,480],{"emptyLinePlaceholder":479},[265,5472,5474],{"class":267,"line":5473},137,[265,5475,5476],{},"const isTranscribing = ref(false);\n",[265,5478,5480],{"class":267,"line":5479},138,[265,5481,5482],{},"const transcribeAudio = async (blob: Blob) => {\n",[265,5484,5486],{"class":267,"line":5485},139,[265,5487,4642],{},[265,5489,5491],{"class":267,"line":5490},140,[265,5492,5493],{},"    isTranscribing.value = true;\n",[265,5495,5497],{"class":267,"line":5496},141,[265,5498,5499],{},"    const formData = new FormData();\n",[265,5501,5503],{"class":267,"line":5502},142,[265,5504,5505],{},"    formData.append(\"audio\", blob);\n",[265,5507,5509],{"class":267,"line":5508},143,[265,5510,480],{"emptyLinePlaceholder":479},[265,5512,5514],{"class":267,"line":5513},144,[265,5515,5516],{},"    return await $fetch(\"\u002Fapi\u002Ftranscribe\", {\n",[265,5518,5520],{"class":267,"line":5519},145,[265,5521,4652],{},[265,5523,5525],{"class":267,"line":5524},146,[265,5526,5527],{},"      body: formData,\n",[265,5529,5531],{"class":267,"line":5530},147,[265,5532,974],{},[265,5534,5536],{"class":267,"line":5535},148,[265,5537,5538],{},"  } finally {\n",[265,5540,5542],{"class":267,"line":5541},149,[265,5543,5544],{},"    isTranscribing.value = false;\n",[265,5546,5548],{"class":267,"line":5547},150,[265,5549,979],{},[265,5551,5553],{"class":267,"line":5552},151,[265,5554,2995],{},[265,5556,5558],{"class":267,"line":5557},152,[265,5559,480],{"emptyLinePlaceholder":479},[265,5561,5563],{"class":267,"line":5562},153,[265,5564,5565],{},"const uploadRecordings = async () => {\n",[265,5567,5569],{"class":267,"line":5568},154,[265,5570,5571],{},"  if (!recordings.value.length) return;\n",[265,5573,5575],{"class":267,"line":5574},155,[265,5576,480],{"emptyLinePlaceholder":479},[265,5578,5580],{"class":267,"line":5579},156,[265,5581,5582],{},"  const formData = new FormData();\n",[265,5584,5586],{"class":267,"line":5585},157,[265,5587,5588],{},"  recordings.value.forEach((recording) => {\n",[265,5590,5592],{"class":267,"line":5591},158,[265,5593,5594],{},"    if (recording.blob) {\n",[265,5596,5598],{"class":267,"line":5597},159,[265,5599,5600],{},"      formData.append(\n",[265,5602,5604],{"class":267,"line":5603},160,[265,5605,5606],{},"        \"files\",\n",[265,5608,5610],{"class":267,"line":5609},161,[265,5611,5612],{},"        recording.blob,\n",[265,5614,5616],{"class":267,"line":5615},162,[265,5617,5618],{},"        `${recording.id}.${recording.blob.type.split(\"\u002F\")[1]}`,\n",[265,5620,5622],{"class":267,"line":5621},163,[265,5623,5624],{},"      );\n",[265,5626,5628],{"class":267,"line":5627},164,[265,5629,3301],{},[265,5631,5633],{"class":267,"line":5632},165,[265,5634,1346],{},[265,5636,5638],{"class":267,"line":5637},166,[265,5639,480],{"emptyLinePlaceholder":479},[265,5641,5643],{"class":267,"line":5642},167,[265,5644,4642],{},[265,5646,5648],{"class":267,"line":5647},168,[265,5649,5650],{},"    const result = await $fetch(\"\u002Fapi\u002Fupload\", {\n",[265,5652,5654],{"class":267,"line":5653},169,[265,5655,5656],{},"      method: \"PUT\",\n",[265,5658,5660],{"class":267,"line":5659},170,[265,5661,5527],{},[265,5663,5665],{"class":267,"line":5664},171,[265,5666,974],{},[265,5668,5670],{"class":267,"line":5669},172,[265,5671,480],{"emptyLinePlaceholder":479},[265,5673,5675],{"class":267,"line":5674},173,[265,5676,5677],{},"    return result.map((obj) => obj.pathname);\n",[265,5679,5681],{"class":267,"line":5680},174,[265,5682,5683],{},"  } catch (error) {\n",[265,5685,5687],{"class":267,"line":5686},175,[265,5688,5689],{},"    console.error(\"Failed to upload audio recordings\", error);\n",[265,5691,5693],{"class":267,"line":5692},176,[265,5694,979],{},[265,5696,5698],{"class":267,"line":5697},177,[265,5699,2995],{},[265,5701,5703],{"class":267,"line":5702},178,[265,5704,480],{"emptyLinePlaceholder":479},[265,5706,5708],{"class":267,"line":5707},179,[265,5709,5710],{},"const isBusy = computed(() => state.value.isRecording || isTranscribing.value);\n",[265,5712,5714],{"class":267,"line":5713},180,[265,5715,480],{"emptyLinePlaceholder":479},[265,5717,5719],{"class":267,"line":5718},181,[265,5720,5721],{},"defineExpose({ uploadRecordings, resetRecordings, isBusy });\n",[265,5723,5725],{"class":267,"line":5724},182,[265,5726,480],{"emptyLinePlaceholder":479},[265,5728,5730],{"class":267,"line":5729},183,[265,5731,5732],{},"const formatDuration = (seconds: number) => {\n",[265,5734,5736],{"class":267,"line":5735},184,[265,5737,5738],{},"  const mins = Math.floor(seconds \u002F 60);\n",[265,5740,5742],{"class":267,"line":5741},185,[265,5743,5744],{},"  const secs = seconds % 60;\n",[265,5746,5748],{"class":267,"line":5747},186,[265,5749,5750],{},"  return `${mins}:${secs.toString().padStart(2, \"0\")}`;\n",[265,5752,5754],{"class":267,"line":5753},187,[265,5755,2995],{},[265,5757,5759],{"class":267,"line":5758},188,[265,5760,4771],{},[10,5762,5763],{},"This component does the following:",[125,5765,5766,5776,5783,5798],{},[64,5767,5768,5769,5771,5772,5775],{},"Allows recording the user’s voice with the help of ",[166,5770,2785],{}," composable created earlier. It also integrates the ",[166,5773,5774],{},"AudioVisualizer"," component to enhance the user experience by providing real-time audio feedback during recordings.",[64,5777,5778,5779,5782],{},"On a new recording, sends the recorded blob for transcription to the ",[166,5780,5781],{},"transcribe"," API endpoint, and emits the transcription text on success",[64,5784,5785,5786,5789,5790,5793,5794,5797],{},"Displays all recordings as ",[166,5787,5788],{},"audio"," elements for users perusal (using ",[166,5791,5792],{},"URL.createObjectURL(blob)","). It utilizes the ",[166,5795,5796],{},"useRecordings"," composable to manage the recordings",[64,5799,5800,5801,5803,5804,5806],{},"Uploads the final recordings to R2 (the local disk in dev mode) using the ",[166,5802,1208],{}," endpoint, and returns the pathnames of these recordings to the caller (the ",[166,5805,4200],{}," component)",[185,5808,5810,4201],{"id":5809},"audiovisualizer-component",[166,5811,5774],{},[10,5813,5814],{},"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.",[10,5816,5817,5818,5821,5822,5824],{},"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 ",[166,5819,5820],{},"updateTrigger"," state variable exposed by ",[166,5823,2785],{}," to redraw the canvas on audio data changes.",[10,5826,2458,5827,1499,5830,1219],{},[166,5828,5829],{},"AudioVisualizer.vue",[166,5831,4209],{},[256,5833,5835],{"className":4212,"code":5834,"language":4214,"meta":261,"style":261},"\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",[166,5836,5837,5842,5846,5851,5855,5859,5863,5868,5873,5878,5883,5887,5892,5897,5902,5907,5911,5916,5921,5926,5931,5936,5940,5944,5948,5953,5958,5963,5967,5971,5976,5981,5986,5990,5995,6000,6005,6010,6014,6019,6024,6029,6034,6038,6043,6048,6053,6058,6062,6066,6071,6075,6079,6084,6089,6093,6097,6102,6107,6112,6117,6121,6126,6130],{"__ignoreMap":261},[265,5838,5839],{"class":267,"line":268},[265,5840,5841],{},"\u003C!-- app\u002Fcomponents\u002FAudioVisualizer.vue -->\n",[265,5843,5844],{"class":267,"line":275},[265,5845,4226],{},[265,5847,5848],{"class":267,"line":476},[265,5849,5850],{},"  \u003Ccanvas ref=\"canvas\" width=\"640\" height=\"100\" \u002F>\n",[265,5852,5853],{"class":267,"line":483},[265,5854,4502],{},[265,5856,5857],{"class":267,"line":495},[265,5858,480],{"emptyLinePlaceholder":479},[265,5860,5861],{"class":267,"line":500},[265,5862,4511],{},[265,5864,5865],{"class":267,"line":506},[265,5866,5867],{},"const props = defineProps\u003C{\n",[265,5869,5870],{"class":267,"line":512},[265,5871,5872],{},"  audioData: Uint8Array | null;\n",[265,5874,5875],{"class":267,"line":524},[265,5876,5877],{},"  dataUpdateTrigger: number;\n",[265,5879,5880],{"class":267,"line":530},[265,5881,5882],{},"}>();\n",[265,5884,5885],{"class":267,"line":536},[265,5886,480],{"emptyLinePlaceholder":479},[265,5888,5889],{"class":267,"line":541},[265,5890,5891],{},"let width = 0;\n",[265,5893,5894],{"class":267,"line":552},[265,5895,5896],{},"let height = 0;\n",[265,5898,5899],{"class":267,"line":563},[265,5900,5901],{},"const audioCanvas = useTemplateRef\u003CHTMLCanvasElement>(\"canvas\");\n",[265,5903,5904],{"class":267,"line":568},[265,5905,5906],{},"const canvasCtx = ref\u003CCanvasRenderingContext2D | null>(null);\n",[265,5908,5909],{"class":267,"line":574},[265,5910,480],{"emptyLinePlaceholder":479},[265,5912,5913],{"class":267,"line":584},[265,5914,5915],{},"onMounted(() => {\n",[265,5917,5918],{"class":267,"line":594},[265,5919,5920],{},"  if (audioCanvas.value) {\n",[265,5922,5923],{"class":267,"line":604},[265,5924,5925],{},"    canvasCtx.value = audioCanvas.value.getContext(\"2d\");\n",[265,5927,5928],{"class":267,"line":609},[265,5929,5930],{},"    width = audioCanvas.value.width;\n",[265,5932,5933],{"class":267,"line":614},[265,5934,5935],{},"    height = audioCanvas.value.height;\n",[265,5937,5938],{"class":267,"line":625},[265,5939,979],{},[265,5941,5942],{"class":267,"line":630},[265,5943,666],{},[265,5945,5946],{"class":267,"line":636},[265,5947,480],{"emptyLinePlaceholder":479},[265,5949,5950],{"class":267,"line":642},[265,5951,5952],{},"const drawCanvas = () => {\n",[265,5954,5955],{"class":267,"line":653},[265,5956,5957],{},"  if (!canvasCtx.value || !props.audioData) {\n",[265,5959,5960],{"class":267,"line":658},[265,5961,5962],{},"    return;\n",[265,5964,5965],{"class":267,"line":663},[265,5966,979],{},[265,5968,5969],{"class":267,"line":3071},[265,5970,480],{"emptyLinePlaceholder":479},[265,5972,5973],{"class":267,"line":3076},[265,5974,5975],{},"  const data = props.audioData;\n",[265,5977,5978],{"class":267,"line":3081},[265,5979,5980],{},"  const ctx = canvasCtx.value;\n",[265,5982,5983],{"class":267,"line":3105},[265,5984,5985],{},"  const sliceWidth = width \u002F data.length;\n",[265,5987,5988],{"class":267,"line":3128},[265,5989,480],{"emptyLinePlaceholder":479},[265,5991,5992],{"class":267,"line":3151},[265,5993,5994],{},"  ctx.clearRect(0, 0, width, height);\n",[265,5996,5997],{"class":267,"line":3173},[265,5998,5999],{},"  ctx.lineWidth = 2;\n",[265,6001,6002],{"class":267,"line":3200},[265,6003,6004],{},"  ctx.strokeStyle = \"rgb(221, 72, 49)\";\n",[265,6006,6007],{"class":267,"line":3205},[265,6008,6009],{},"  ctx.beginPath();\n",[265,6011,6012],{"class":267,"line":3221},[265,6013,480],{"emptyLinePlaceholder":479},[265,6015,6016],{"class":267,"line":3249},[265,6017,6018],{},"  let x = 0;\n",[265,6020,6021],{"class":267,"line":3258},[265,6022,6023],{},"  for (let i = 0; i \u003C data.length; i++) {\n",[265,6025,6026],{"class":267,"line":3267},[265,6027,6028],{},"    const v = (data[i] ?? 0) \u002F 128.0;\n",[265,6030,6031],{"class":267,"line":3279},[265,6032,6033],{},"    const y = (v * height) \u002F 2;\n",[265,6035,6036],{"class":267,"line":3285},[265,6037,480],{"emptyLinePlaceholder":479},[265,6039,6040],{"class":267,"line":3290},[265,6041,6042],{},"    if (i === 0) {\n",[265,6044,6045],{"class":267,"line":3298},[265,6046,6047],{},"      ctx.moveTo(x, y);\n",[265,6049,6050],{"class":267,"line":3304},[265,6051,6052],{},"    } else {\n",[265,6054,6055],{"class":267,"line":3309},[265,6056,6057],{},"      ctx.lineTo(x, y);\n",[265,6059,6060],{"class":267,"line":3321},[265,6061,3301],{},[265,6063,6064],{"class":267,"line":3335},[265,6065,480],{"emptyLinePlaceholder":479},[265,6067,6068],{"class":267,"line":3349},[265,6069,6070],{},"    x += sliceWidth;\n",[265,6072,6073],{"class":267,"line":3355},[265,6074,979],{},[265,6076,6077],{"class":267,"line":3360},[265,6078,480],{"emptyLinePlaceholder":479},[265,6080,6081],{"class":267,"line":3379},[265,6082,6083],{},"  ctx.lineTo(width, height \u002F 2);\n",[265,6085,6086],{"class":267,"line":3387},[265,6087,6088],{},"  ctx.stroke();\n",[265,6090,6091],{"class":267,"line":3414},[265,6092,2995],{},[265,6094,6095],{"class":267,"line":3419},[265,6096,480],{"emptyLinePlaceholder":479},[265,6098,6099],{"class":267,"line":3435},[265,6100,6101],{},"watch(\n",[265,6103,6104],{"class":267,"line":3451},[265,6105,6106],{},"  () => props.dataUpdateTrigger,\n",[265,6108,6109],{"class":267,"line":3456},[265,6110,6111],{},"  () => {\n",[265,6113,6114],{"class":267,"line":3474},[265,6115,6116],{},"    drawCanvas();\n",[265,6118,6119],{"class":267,"line":3486},[265,6120,533],{},[265,6122,6123],{"class":267,"line":3491},[265,6124,6125],{},"  { immediate: true },\n",[265,6127,6128],{"class":267,"line":3503},[265,6129,2188],{},[265,6131,6132],{"class":267,"line":3514},[265,6133,4771],{},[185,6135,6137,2786],{"id":6136},"userecordings-composable",[166,6138,5796],{},[10,6140,1805,6141,6143,6144,6146,6147,1499,6150,1219],{},[166,6142,4789],{}," component uses the ",[166,6145,5796],{}," composable to manage the list of recordings, and to clear any used resources. Create a new file ",[166,6148,6149],{},"useRecordings.ts",[166,6151,2795],{},[256,6153,6155],{"className":836,"code":6154,"language":838,"meta":261,"style":261},"\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",[166,6156,6157,6162,6179,6198,6202,6227,6234,6247,6251,6255,6259,6274,6291,6299,6303,6307,6311,6334,6343,6347,6351,6374,6405,6412,6416,6420,6435,6442,6446,6454,6458,6462,6469,6473,6479,6484,6489,6494,6499,6503],{"__ignoreMap":261},[265,6158,6159],{"class":267,"line":268},[265,6160,6161],{"class":271},"\u002F\u002F app\u002Fcomposables\u002FuseRecordings.ts\n",[265,6163,6164,6166,6168,6171,6173,6175,6177],{"class":267,"line":275},[265,6165,438],{"class":329},[265,6167,1566],{"class":329},[265,6169,6170],{"class":278}," useRecordings",[265,6172,887],{"class":329},[265,6174,2886],{"class":292},[265,6176,873],{"class":329},[265,6178,876],{"class":292},[265,6180,6181,6183,6186,6188,6190,6192,6195],{"class":267,"line":476},[265,6182,881],{"class":329},[265,6184,6185],{"class":296}," recordings",[265,6187,887],{"class":329},[265,6189,3022],{"class":278},[265,6191,1792],{"class":292},[265,6193,6194],{"class":278},"Recording",[265,6196,6197],{"class":292},"[]>([]);\n",[265,6199,6200],{"class":267,"line":483},[265,6201,480],{"emptyLinePlaceholder":479},[265,6203,6204,6206,6209,6211,6213,6216,6218,6221,6223,6225],{"class":267,"line":495},[265,6205,881],{"class":329},[265,6207,6208],{"class":278}," cleanupResource",[265,6210,887],{"class":329},[265,6212,863],{"class":292},[265,6214,6215],{"class":866},"recording",[265,6217,2158],{"class":329},[265,6219,6220],{"class":278}," Recording",[265,6222,870],{"class":292},[265,6224,873],{"class":329},[265,6226,876],{"class":292},[265,6228,6229,6231],{"class":267,"line":500},[265,6230,3224],{"class":329},[265,6232,6233],{"class":292}," (recording.blob) {\n",[265,6235,6236,6239,6241,6244],{"class":267,"line":506},[265,6237,6238],{"class":296},"      URL",[265,6240,208],{"class":292},[265,6242,6243],{"class":278},"revokeObjectURL",[265,6245,6246],{"class":292},"(recording.url);\n",[265,6248,6249],{"class":267,"line":512},[265,6250,3301],{"class":292},[265,6252,6253],{"class":267,"line":524},[265,6254,3352],{"class":292},[265,6256,6257],{"class":267,"line":530},[265,6258,480],{"emptyLinePlaceholder":479},[265,6260,6261,6263,6266,6268,6270,6272],{"class":267,"line":536},[265,6262,881],{"class":329},[265,6264,6265],{"class":278}," cleanupResources",[265,6267,887],{"class":329},[265,6269,2886],{"class":292},[265,6271,873],{"class":329},[265,6273,876],{"class":292},[265,6275,6276,6279,6281,6283,6285,6287,6289],{"class":267,"line":541},[265,6277,6278],{"class":292},"    recordings.value.",[265,6280,3978],{"class":278},[265,6282,2138],{"class":292},[265,6284,6215],{"class":866},[265,6286,870],{"class":292},[265,6288,873],{"class":329},[265,6290,876],{"class":292},[265,6292,6293,6296],{"class":267,"line":552},[265,6294,6295],{"class":278},"      cleanupResource",[265,6297,6298],{"class":292},"(recording);\n",[265,6300,6301],{"class":267,"line":563},[265,6302,974],{"class":292},[265,6304,6305],{"class":267,"line":568},[265,6306,3352],{"class":292},[265,6308,6309],{"class":267,"line":574},[265,6310,480],{"emptyLinePlaceholder":479},[265,6312,6313,6315,6318,6320,6322,6324,6326,6328,6330,6332],{"class":267,"line":584},[265,6314,881],{"class":329},[265,6316,6317],{"class":278}," addRecording",[265,6319,887],{"class":329},[265,6321,863],{"class":292},[265,6323,6215],{"class":866},[265,6325,2158],{"class":329},[265,6327,6220],{"class":278},[265,6329,870],{"class":292},[265,6331,873],{"class":329},[265,6333,876],{"class":292},[265,6335,6336,6338,6341],{"class":267,"line":594},[265,6337,6278],{"class":292},[265,6339,6340],{"class":278},"unshift",[265,6342,6298],{"class":292},[265,6344,6345],{"class":267,"line":604},[265,6346,3352],{"class":292},[265,6348,6349],{"class":267,"line":609},[265,6350,480],{"emptyLinePlaceholder":479},[265,6352,6353,6355,6358,6360,6362,6364,6366,6368,6370,6372],{"class":267,"line":614},[265,6354,881],{"class":329},[265,6356,6357],{"class":278}," removeRecording",[265,6359,887],{"class":329},[265,6361,863],{"class":292},[265,6363,6215],{"class":866},[265,6365,2158],{"class":329},[265,6367,6220],{"class":278},[265,6369,870],{"class":292},[265,6371,873],{"class":329},[265,6373,876],{"class":292},[265,6375,6376,6379,6381,6384,6387,6389,6392,6394,6396,6399,6402],{"class":267,"line":625},[265,6377,6378],{"class":292},"    recordings.value ",[265,6380,330],{"class":329},[265,6382,6383],{"class":292}," recordings.value.",[265,6385,6386],{"class":278},"filter",[265,6388,2138],{"class":292},[265,6390,6391],{"class":866},"r",[265,6393,870],{"class":292},[265,6395,873],{"class":329},[265,6397,6398],{"class":292}," r.id ",[265,6400,6401],{"class":329},"!==",[265,6403,6404],{"class":292}," recording.id);\n",[265,6406,6407,6410],{"class":267,"line":630},[265,6408,6409],{"class":278},"    cleanupResource",[265,6411,6298],{"class":292},[265,6413,6414],{"class":267,"line":636},[265,6415,3352],{"class":292},[265,6417,6418],{"class":267,"line":642},[265,6419,480],{"emptyLinePlaceholder":479},[265,6421,6422,6424,6427,6429,6431,6433],{"class":267,"line":653},[265,6423,881],{"class":329},[265,6425,6426],{"class":278}," resetRecordings",[265,6428,887],{"class":329},[265,6430,2886],{"class":292},[265,6432,873],{"class":329},[265,6434,876],{"class":292},[265,6436,6437,6440],{"class":267,"line":658},[265,6438,6439],{"class":278},"    cleanupResources",[265,6441,3432],{"class":292},[265,6443,6444],{"class":267,"line":663},[265,6445,480],{"emptyLinePlaceholder":479},[265,6447,6448,6450,6452],{"class":267,"line":3071},[265,6449,6378],{"class":292},[265,6451,330],{"class":329},[265,6453,3559],{"class":292},[265,6455,6456],{"class":267,"line":3076},[265,6457,3352],{"class":292},[265,6459,6460],{"class":267,"line":3081},[265,6461,480],{"emptyLinePlaceholder":479},[265,6463,6464,6466],{"class":267,"line":3105},[265,6465,4088],{"class":278},[265,6467,6468],{"class":292},"(cleanupResources);\n",[265,6470,6471],{"class":267,"line":3128},[265,6472,480],{"emptyLinePlaceholder":479},[265,6474,6475,6477],{"class":267,"line":3151},[265,6476,1256],{"class":329},[265,6478,876],{"class":292},[265,6480,6481],{"class":267,"line":3173},[265,6482,6483],{"class":292},"    recordings,\n",[265,6485,6486],{"class":267,"line":3200},[265,6487,6488],{"class":292},"    addRecording,\n",[265,6490,6491],{"class":267,"line":3205},[265,6492,6493],{"class":292},"    removeRecording,\n",[265,6495,6496],{"class":267,"line":3221},[265,6497,6498],{"class":292},"    resetRecordings,\n",[265,6500,6501],{"class":267,"line":3249},[265,6502,3352],{"class":292},[265,6504,6505],{"class":267,"line":3258},[265,6506,2995],{"class":292},[10,6508,6509,6510,6512,6513,6516,6517,6520],{},"You can define the ",[166,6511,6194],{}," type definition in the ",[166,6514,6515],{},"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 ",[166,6518,6519],{},"Note"," type.",[256,6522,6524],{"className":836,"code":6523,"language":838,"meta":261,"style":261},"\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",[166,6525,6526,6531,6546,6561,6565,6577,6589,6601,6612,6616,6620],{"__ignoreMap":261},[265,6527,6528],{"class":267,"line":268},[265,6529,6530],{"class":271},"\u002F\u002F shared\u002Ftypes\u002Findex.ts\n",[265,6532,6533,6535,6538,6540,6542,6544],{"class":267,"line":275},[265,6534,1425],{"class":329},[265,6536,6537],{"class":329}," type",[265,6539,2496],{"class":292},[265,6541,1431],{"class":329},[265,6543,2501],{"class":282},[265,6545,712],{"class":292},[265,6547,6548,6550,6552,6555,6557,6559],{"class":267,"line":476},[265,6549,1425],{"class":329},[265,6551,6537],{"class":329},[265,6553,6554],{"class":292}," { noteSelectSchema } ",[265,6556,1431],{"class":329},[265,6558,1998],{"class":282},[265,6560,712],{"class":292},[265,6562,6563],{"class":267,"line":483},[265,6564,480],{"emptyLinePlaceholder":479},[265,6566,6567,6569,6571,6573,6575],{"class":267,"line":495},[265,6568,438],{"class":329},[265,6570,6537],{"class":329},[265,6572,6220],{"class":278},[265,6574,887],{"class":329},[265,6576,876],{"class":292},[265,6578,6579,6582,6584,6587],{"class":267,"line":500},[265,6580,6581],{"class":866},"  url",[265,6583,2158],{"class":329},[265,6585,6586],{"class":296}," string",[265,6588,712],{"class":292},[265,6590,6591,6594,6597,6599],{"class":267,"line":506},[265,6592,6593],{"class":866},"  blob",[265,6595,6596],{"class":329},"?:",[265,6598,924],{"class":278},[265,6600,712],{"class":292},[265,6602,6603,6606,6608,6610],{"class":267,"line":512},[265,6604,6605],{"class":866},"  id",[265,6607,2158],{"class":329},[265,6609,6586],{"class":296},[265,6611,712],{"class":292},[265,6613,6614],{"class":267,"line":524},[265,6615,2995],{"class":292},[265,6617,6618],{"class":267,"line":530},[265,6619,480],{"emptyLinePlaceholder":479},[265,6621,6622,6624,6626,6629,6631,6634,6636,6639,6641,6644],{"class":267,"line":536},[265,6623,438],{"class":329},[265,6625,6537],{"class":329},[265,6627,6628],{"class":278}," Note",[265,6630,887],{"class":329},[265,6632,6633],{"class":278}," z",[265,6635,208],{"class":292},[265,6637,6638],{"class":278},"output",[265,6640,1792],{"class":292},[265,6642,6643],{"class":329},"typeof",[265,6645,6646],{"class":292}," noteSelectSchema>;\n",[185,6648,6650],{"id":6649},"creating-the-home-page","Creating the Home Page",[10,6652,6653,6654,6657],{},"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 (",[166,6655,6656],{},"app\u002Fpages\u002Findex.vue","), and put the following content to it:",[256,6659,6661],{"className":4212,"code":6660,"language":4214,"meta":261,"style":261},"\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",[166,6662,6663,6668,6672,6677,6682,6687,6692,6696,6701,6706,6711,6716,6721,6726,6730,6735,6740,6744,6748,6753,6758,6762,6767,6772,6776,6781,6786,6790,6794,6798,6803,6807,6812,6816,6820,6825,6830,6835,6839,6843,6847,6852,6857,6862,6866,6870],{"__ignoreMap":261},[265,6664,6665],{"class":267,"line":268},[265,6666,6667],{},"\u003C!-- app\u002Fpages\u002Findex.vue -->\n",[265,6669,6670],{"class":267,"line":275},[265,6671,4226],{},[265,6673,6674],{"class":267,"line":476},[265,6675,6676],{},"  \u003CUContainer class=\"h-screen flex justify-center items-center\">\n",[265,6678,6679],{"class":267,"line":483},[265,6680,6681],{},"    \u003CUCard\n",[265,6683,6684],{"class":267,"line":495},[265,6685,6686],{},"      class=\"w-full max-h-full overflow-hidden max-w-4xl mx-auto\"\n",[265,6688,6689],{"class":267,"line":500},[265,6690,6691],{},"      :ui=\"{ body: 'h-[calc(100vh-4rem)] overflow-y-auto' }\"\n",[265,6693,6694],{"class":267,"line":506},[265,6695,5015],{},[265,6697,6698],{"class":267,"line":512},[265,6699,6700],{},"      \u003Ctemplate #header>\n",[265,6702,6703],{"class":267,"line":524},[265,6704,6705],{},"        \u003Cspan class=\"font-bold text-xl md:text-2xl\">Voice Notes\u003C\u002Fspan>\n",[265,6707,6708],{"class":267,"line":530},[265,6709,6710],{},"        \u003CUButton icon=\"i-lucide-plus\" @click=\"showNoteModal\">\n",[265,6712,6713],{"class":267,"line":536},[265,6714,6715],{},"          New Note\n",[265,6717,6718],{"class":267,"line":541},[265,6719,6720],{},"        \u003C\u002FUButton>\n",[265,6722,6723],{"class":267,"line":552},[265,6724,6725],{},"      \u003C\u002Ftemplate>\n",[265,6727,6728],{"class":267,"line":563},[265,6729,480],{"emptyLinePlaceholder":479},[265,6731,6732],{"class":267,"line":568},[265,6733,6734],{},"      \u003Cdiv v-if=\"notes?.length\" class=\"space-y-4\">\n",[265,6736,6737],{"class":267,"line":574},[265,6738,6739],{},"        \u003CNoteCard v-for=\"note in notes\" :key=\"note.id\" :note=\"note\" \u002F>\n",[265,6741,6742],{"class":267,"line":584},[265,6743,4953],{},[265,6745,6746],{"class":267,"line":594},[265,6747,5044],{},[265,6749,6750],{"class":267,"line":604},[265,6751,6752],{},"        v-else\n",[265,6754,6755],{"class":267,"line":609},[265,6756,6757],{},"        class=\"my-12 text-center text-gray-500 dark:text-gray-400 space-y-2\"\n",[265,6759,6760],{"class":267,"line":614},[265,6761,4437],{},[265,6763,6764],{"class":267,"line":625},[265,6765,6766],{},"        \u003Ch2 class=\"text-2xl md:text-3xl\">No notes created\u003C\u002Fh2>\n",[265,6768,6769],{"class":267,"line":630},[265,6770,6771],{},"        \u003Cp>Get started by creating your first note\u003C\u002Fp>\n",[265,6773,6774],{"class":267,"line":636},[265,6775,4953],{},[265,6777,6778],{"class":267,"line":642},[265,6779,6780],{},"    \u003C\u002FUCard>\n",[265,6782,6783],{"class":267,"line":653},[265,6784,6785],{},"  \u003C\u002FUContainer>\n",[265,6787,6788],{"class":267,"line":658},[265,6789,4502],{},[265,6791,6792],{"class":267,"line":663},[265,6793,480],{"emptyLinePlaceholder":479},[265,6795,6796],{"class":267,"line":3071},[265,6797,4511],{},[265,6799,6800],{"class":267,"line":3076},[265,6801,6802],{},"import { LazyNoteEditorModal } from \"#components\";\n",[265,6804,6805],{"class":267,"line":3081},[265,6806,480],{"emptyLinePlaceholder":479},[265,6808,6809],{"class":267,"line":3105},[265,6810,6811],{},"const { data: notes, refresh } = await useFetch(\"\u002Fapi\u002Fnotes\");\n",[265,6813,6814],{"class":267,"line":3128},[265,6815,480],{"emptyLinePlaceholder":479},[265,6817,6818],{"class":267,"line":3151},[265,6819,4595],{},[265,6821,6822],{"class":267,"line":3173},[265,6823,6824],{},"const showNoteModal = () => {\n",[265,6826,6827],{"class":267,"line":3200},[265,6828,6829],{},"  modal.open(LazyNoteEditorModal, {\n",[265,6831,6832],{"class":267,"line":3205},[265,6833,6834],{},"    onNewNote: refresh,\n",[265,6836,6837],{"class":267,"line":3221},[265,6838,1346],{},[265,6840,6841],{"class":267,"line":3249},[265,6842,2995],{},[265,6844,6845],{"class":267,"line":3258},[265,6846,480],{"emptyLinePlaceholder":479},[265,6848,6849],{"class":267,"line":3267},[265,6850,6851],{},"watch(modal.isOpen, (newState) => {\n",[265,6853,6854],{"class":267,"line":3279},[265,6855,6856],{},"  if (!newState) {\n",[265,6858,6859],{"class":267,"line":3285},[265,6860,6861],{},"    modal.reset();\n",[265,6863,6864],{"class":267,"line":3290},[265,6865,979],{},[265,6867,6868],{"class":267,"line":3298},[265,6869,666],{},[265,6871,6872],{"class":267,"line":3304},[265,6873,4771],{},[10,6875,6876],{},"On this page we’re doing the following:",[125,6878,6879,6886,6896],{},[64,6880,6881,6882,6885],{},"Fetch the list of existing notes from the database and display them using the ",[166,6883,6884],{},"NoteCard"," component",[64,6887,6888,6889,6891,6892,6895],{},"Shows a new note button which when clicked opens the ",[166,6890,4200],{},". On successful note creation the ",[166,6893,6894],{},"refresh"," function is called to refetch the notes",[64,6897,6898],{},"The modal state is reset on closure to ensure a clean slate for the next note creation",[10,6900,6901],{},"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.",[10,6903,2458,6904,829,6907,1503],{},[166,6905,6906],{},"app.config.ts",[166,6908,6909],{},"app",[256,6911,6913],{"className":836,"code":6912,"language":838,"meta":261,"style":261},"\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",[166,6914,6915,6920,6931,6936,6941,6946,6956,6961,6965,6970,6974,6984,6988,6992,6996],{"__ignoreMap":261},[265,6916,6917],{"class":267,"line":268},[265,6918,6919],{"class":271},"\u002F\u002F app\u002Fapp.config.ts\n",[265,6921,6922,6924,6926,6929],{"class":267,"line":275},[265,6923,438],{"class":329},[265,6925,441],{"class":329},[265,6927,6928],{"class":278}," defineAppConfig",[265,6930,447],{"class":292},[265,6932,6933],{"class":267,"line":476},[265,6934,6935],{"class":292},"  ui: {\n",[265,6937,6938],{"class":267,"line":483},[265,6939,6940],{"class":292},"    card: {\n",[265,6942,6943],{"class":267,"line":495},[265,6944,6945],{"class":292},"      slots: {\n",[265,6947,6948,6951,6954],{"class":267,"line":500},[265,6949,6950],{"class":292},"        header: ",[265,6952,6953],{"class":282},"\"flex items-center justify-between gap-3 flex-wrap\"",[265,6955,521],{"class":292},[265,6957,6958],{"class":267,"line":506},[265,6959,6960],{"class":292},"      },\n",[265,6962,6963],{"class":267,"line":512},[265,6964,527],{"class":292},[265,6966,6967],{"class":267,"line":524},[265,6968,6969],{"class":292},"    modal: {\n",[265,6971,6972],{"class":267,"line":530},[265,6973,6945],{"class":292},[265,6975,6976,6979,6982],{"class":267,"line":536},[265,6977,6978],{"class":292},"        footer: ",[265,6980,6981],{"class":282},"\"justify-end gap-x-3\"",[265,6983,521],{"class":292},[265,6985,6986],{"class":267,"line":541},[265,6987,6960],{"class":292},[265,6989,6990],{"class":267,"line":552},[265,6991,527],{"class":292},[265,6993,6994],{"class":267,"line":563},[265,6995,533],{"class":292},[265,6997,6998],{"class":267,"line":568},[265,6999,666],{"class":292},[10,7001,7002,7003,7006,7007,7010],{},"You’ll also need to wrap your ",[166,7004,7005],{},"NuxtPage"," component with the ",[166,7008,7009],{},"UApp"," component for the modals and toast notifications to work as shown below:",[256,7012,7014],{"className":4212,"code":7013,"language":4214,"meta":261,"style":261},"\u003C!-- app\u002Fapp.vue -->\n\u003Ctemplate>\n  \u003CNuxtRouteAnnouncer \u002F>\n  \u003CNuxtLoadingIndicator \u002F>\n  \u003CUApp>\n    \u003CNuxtPage \u002F>\n  \u003C\u002FUApp>\n\u003C\u002Ftemplate>\n",[166,7015,7016,7021,7025,7030,7035,7040,7045,7050],{"__ignoreMap":261},[265,7017,7018],{"class":267,"line":268},[265,7019,7020],{},"\u003C!-- app\u002Fapp.vue -->\n",[265,7022,7023],{"class":267,"line":275},[265,7024,4226],{},[265,7026,7027],{"class":267,"line":476},[265,7028,7029],{},"  \u003CNuxtRouteAnnouncer \u002F>\n",[265,7031,7032],{"class":267,"line":483},[265,7033,7034],{},"  \u003CNuxtLoadingIndicator \u002F>\n",[265,7036,7037],{"class":267,"line":495},[265,7038,7039],{},"  \u003CUApp>\n",[265,7041,7042],{"class":267,"line":500},[265,7043,7044],{},"    \u003CNuxtPage \u002F>\n",[265,7046,7047],{"class":267,"line":506},[265,7048,7049],{},"  \u003C\u002FUApp>\n",[265,7051,7052],{"class":267,"line":512},[265,7053,4502],{},[185,7055,7057,6885],{"id":7056},"notecard-component",[166,7058,6884],{},[10,7060,7061],{},"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.",[10,7063,2458,7064,1499,7067,7069],{},[166,7065,7066],{},"NoteCard.vue",[166,7068,4209],{}," folder, and add the following code to it:",[256,7071,7073],{"className":4212,"code":7072,"language":4214,"meta":261,"style":261},"\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",[166,7074,7075,7079,7084,7089,7094,7099,7104,7108,7113,7118,7122,7127,7132,7137,7142,7146,7151,7155,7159,7163,7167,7172,7177,7181,7186,7191,7196,7201,7206,7211,7215,7219,7223,7228,7233,7237,7242,7247,7252,7257,7262,7267,7272,7277,7282,7286,7290,7294,7298,7303,7307,7312,7316,7321,7326,7330,7335,7340,7344,7349,7353,7358,7363,7368,7373,7378,7383,7388,7392,7396,7400,7404,7409,7413,7418],{"__ignoreMap":261},[265,7076,7077],{"class":267,"line":268},[265,7078,4226],{},[265,7080,7081],{"class":267,"line":275},[265,7082,7083],{},"  \u003CUCard class=\"hover:shadow-lg transition-shadow\">\n",[265,7085,7086],{"class":267,"line":476},[265,7087,7088],{},"    \u003Cdiv class=\"flex-1\">\n",[265,7090,7091],{"class":267,"line":483},[265,7092,7093],{},"      \u003Cp\n",[265,7095,7096],{"class":267,"line":495},[265,7097,7098],{},"        ref=\"text\"\n",[265,7100,7101],{"class":267,"line":500},[265,7102,7103],{},"        :class=\"['whitespace-pre-wrap', !showFullText && 'line-clamp-3']\"\n",[265,7105,7106],{"class":267,"line":506},[265,7107,4437],{},[265,7109,7110],{"class":267,"line":512},[265,7111,7112],{},"        {{ note.text }}\n",[265,7114,7115],{"class":267,"line":524},[265,7116,7117],{},"      \u003C\u002Fp>\n",[265,7119,7120],{"class":267,"line":530},[265,7121,4407],{},[265,7123,7124],{"class":267,"line":536},[265,7125,7126],{},"        v-if=\"shouldShowExpandBtn\"\n",[265,7128,7129],{"class":267,"line":541},[265,7130,7131],{},"        variant=\"link\"\n",[265,7133,7134],{"class":267,"line":552},[265,7135,7136],{},"        :padded=\"false\"\n",[265,7138,7139],{"class":267,"line":563},[265,7140,7141],{},"        @click=\"showFullText = !showFullText\"\n",[265,7143,7144],{"class":267,"line":568},[265,7145,4437],{},[265,7147,7148],{"class":267,"line":574},[265,7149,7150],{},"        {{ showFullText ? \"Show less\" : \"Show more\" }}\n",[265,7152,7153],{"class":267,"line":584},[265,7154,4447],{},[265,7156,7157],{"class":267,"line":594},[265,7158,5030],{},[265,7160,7161],{"class":267,"line":604},[265,7162,480],{"emptyLinePlaceholder":479},[265,7164,7165],{"class":267,"line":609},[265,7166,5000],{},[265,7168,7169],{"class":267,"line":614},[265,7170,7171],{},"      v-if=\"note.audioUrls && note.audioUrls.length > 0\"\n",[265,7173,7174],{"class":267,"line":625},[265,7175,7176],{},"      class=\"mt-4 flex gap-x-2 overflow-x-auto\"\n",[265,7178,7179],{"class":267,"line":630},[265,7180,5015],{},[265,7182,7183],{"class":267,"line":636},[265,7184,7185],{},"      \u003Caudio\n",[265,7187,7188],{"class":267,"line":642},[265,7189,7190],{},"        v-for=\"url in note.audioUrls\"\n",[265,7192,7193],{"class":267,"line":653},[265,7194,7195],{},"        :key=\"url\"\n",[265,7197,7198],{"class":267,"line":658},[265,7199,7200],{},"        :src=\"url\"\n",[265,7202,7203],{"class":267,"line":663},[265,7204,7205],{},"        controls\n",[265,7207,7208],{"class":267,"line":3071},[265,7209,7210],{},"        class=\"w-60 shrink-0 h-10\"\n",[265,7212,7213],{"class":267,"line":3076},[265,7214,4388],{},[265,7216,7217],{"class":267,"line":3081},[265,7218,5030],{},[265,7220,7221],{"class":267,"line":3105},[265,7222,480],{"emptyLinePlaceholder":479},[265,7224,7225],{"class":267,"line":3128},[265,7226,7227],{},"    \u003Cp\n",[265,7229,7230],{"class":267,"line":3151},[265,7231,7232],{},"      class=\"flex items-center text-sm text-gray-500 dark:text-gray-400 gap-x-2 mt-6\"\n",[265,7234,7235],{"class":267,"line":3173},[265,7236,5015],{},[265,7238,7239],{"class":267,"line":3200},[265,7240,7241],{},"      \u003CUIcon name=\"i-lucide-clock\" size=\"size-4\" \u002F>\n",[265,7243,7244],{"class":267,"line":3205},[265,7245,7246],{},"      \u003Cspan>\n",[265,7248,7249],{"class":267,"line":3221},[265,7250,7251],{},"        {{\n",[265,7253,7254],{"class":267,"line":3249},[265,7255,7256],{},"          note.updatedAt && note.updatedAt !== note.createdAt\n",[265,7258,7259],{"class":267,"line":3258},[265,7260,7261],{},"            ? `Updated ${updated}`\n",[265,7263,7264],{"class":267,"line":3267},[265,7265,7266],{},"            : `Created ${created}`\n",[265,7268,7269],{"class":267,"line":3279},[265,7270,7271],{},"        }}\n",[265,7273,7274],{"class":267,"line":3285},[265,7275,7276],{},"      \u003C\u002Fspan>\n",[265,7278,7279],{"class":267,"line":3290},[265,7280,7281],{},"    \u003C\u002Fp>\n",[265,7283,7284],{"class":267,"line":3298},[265,7285,5154],{},[265,7287,7288],{"class":267,"line":3304},[265,7289,4502],{},[265,7291,7292],{"class":267,"line":3309},[265,7293,480],{"emptyLinePlaceholder":479},[265,7295,7296],{"class":267,"line":3321},[265,7297,4511],{},[265,7299,7300],{"class":267,"line":3335},[265,7301,7302],{},"import { useTimeAgo } from \"@vueuse\u002Fcore\";\n",[265,7304,7305],{"class":267,"line":3349},[265,7306,480],{"emptyLinePlaceholder":479},[265,7308,7309],{"class":267,"line":3355},[265,7310,7311],{},"const props = defineProps\u003C{ note: Note }>();\n",[265,7313,7314],{"class":267,"line":3360},[265,7315,480],{"emptyLinePlaceholder":479},[265,7317,7318],{"class":267,"line":3379},[265,7319,7320],{},"const createdAt = computed(() => props.note.createdAt + \"Z\");\n",[265,7322,7323],{"class":267,"line":3387},[265,7324,7325],{},"const updatedAt = computed(() => props.note.updatedAt + \"Z\");\n",[265,7327,7328],{"class":267,"line":3414},[265,7329,480],{"emptyLinePlaceholder":479},[265,7331,7332],{"class":267,"line":3419},[265,7333,7334],{},"const created = useTimeAgo(createdAt);\n",[265,7336,7337],{"class":267,"line":3435},[265,7338,7339],{},"const updated = useTimeAgo(updatedAt);\n",[265,7341,7342],{"class":267,"line":3451},[265,7343,480],{"emptyLinePlaceholder":479},[265,7345,7346],{"class":267,"line":3456},[265,7347,7348],{},"const showFullText = ref(false);\n",[265,7350,7351],{"class":267,"line":3474},[265,7352,480],{"emptyLinePlaceholder":479},[265,7354,7355],{"class":267,"line":3486},[265,7356,7357],{},"const shouldShowExpandBtn = ref(false);\n",[265,7359,7360],{"class":267,"line":3491},[265,7361,7362],{},"const noteText = useTemplateRef\u003CHTMLParagraphElement>(\"text\");\n",[265,7364,7365],{"class":267,"line":3503},[265,7366,7367],{},"const checkTextExpansion = () => {\n",[265,7369,7370],{"class":267,"line":3514},[265,7371,7372],{},"  nextTick(() => {\n",[265,7374,7375],{"class":267,"line":3525},[265,7376,7377],{},"    if (noteText.value) {\n",[265,7379,7380],{"class":267,"line":3531},[265,7381,7382],{},"      shouldShowExpandBtn.value =\n",[265,7384,7385],{"class":267,"line":3536},[265,7386,7387],{},"        noteText.value.scrollHeight > noteText.value.clientHeight;\n",[265,7389,7390],{"class":267,"line":3551},[265,7391,3301],{},[265,7393,7394],{"class":267,"line":3562},[265,7395,1346],{},[265,7397,7398],{"class":267,"line":3567},[265,7399,2995],{},[265,7401,7402],{"class":267,"line":3594},[265,7403,480],{"emptyLinePlaceholder":479},[265,7405,7406],{"class":267,"line":3606},[265,7407,7408],{},"onMounted(checkTextExpansion);\n",[265,7410,7411],{"class":267,"line":3618},[265,7412,480],{"emptyLinePlaceholder":479},[265,7414,7415],{"class":267,"line":3623},[265,7416,7417],{},"watch(() => props.note.text, checkTextExpansion);\n",[265,7419,7420],{"class":267,"line":3628},[265,7421,4771],{},[10,7423,7424],{},"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?",[10,7426,7427],{},"Try playing the audio recordings of the saved notes, are these playable?",[10,7429,7430],{},[45,7431],{"alt":7432,"src":7433},"Houston, we have a problem","https:\u002F\u002Fi.giphy.com\u002Fmedia\u002Fv1.Y2lkPTc5MGI3NjExbTFvbTQ4Z3h1aXlyNHhvOW9ibW9oM2M5OWE2Y3MyczRxazRqN3E2YSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw\u002F3oEjHWzZQaCrZW2aWs\u002Fgiphy.gif",[185,7435,7437],{"id":7436},"serving-the-audio-recordings","Serving the Audio Recordings",[10,7439,7440],{},"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.",[10,7442,7443,7444,7446,7447,7449],{},"If you look at the ",[166,7445,1952],{}," code, we save the audio urls\u002Fpathnames with an ",[166,7448,5788],{}," prefix",[256,7451,7453],{"className":836,"code":7452,"language":838,"meta":261,"style":261},"await useDrizzle()\n  .insert(tables.notes)\n  .values({\n    text,\n    audioUrls: audioUrls ? audioUrls.map((url) => `\u002Faudio\u002F${url}`) : null,\n  });\n",[166,7454,7455,7463,7472,7480,7485,7518],{"__ignoreMap":261},[265,7456,7457,7459,7461],{"class":267,"line":268},[265,7458,1058],{"class":329},[265,7460,1910],{"class":278},[265,7462,1608],{"class":292},[265,7464,7465,7468,7470],{"class":267,"line":275},[265,7466,7467],{"class":292},"  .",[265,7469,2104],{"class":278},[265,7471,2107],{"class":292},[265,7473,7474,7476,7478],{"class":267,"line":476},[265,7475,7467],{"class":292},[265,7477,2114],{"class":278},[265,7479,447],{"class":292},[265,7481,7482],{"class":267,"line":483},[265,7483,7484],{"class":292},"    text,\n",[265,7486,7487,7490,7492,7494,7496,7498,7500,7502,7504,7506,7508,7510,7512,7514,7516],{"class":267,"line":495},[265,7488,7489],{"class":292},"    audioUrls: audioUrls ",[265,7491,2129],{"class":329},[265,7493,2132],{"class":292},[265,7495,2135],{"class":278},[265,7497,2138],{"class":292},[265,7499,2141],{"class":866},[265,7501,870],{"class":292},[265,7503,873],{"class":329},[265,7505,2148],{"class":282},[265,7507,2141],{"class":292},[265,7509,2153],{"class":282},[265,7511,870],{"class":292},[265,7513,2158],{"class":329},[265,7515,2161],{"class":296},[265,7517,521],{"class":292},[265,7519,7520],{"class":267,"line":500},[265,7521,1346],{"class":292},[10,7523,7524,7525,7528,7529,1499,7532,7535],{},"The reason to do so was to serve all audio recordings through an ",[166,7526,7527],{},"\u002Faudio"," path. Create a new file ",[166,7530,7531],{},"[…pathname].get.ts",[166,7533,7534],{},"server\u002Froutes\u002Faudio"," folder and add the following to it:",[256,7537,7539],{"className":836,"code":7538,"language":838,"meta":261,"style":261},"export default defineEventHandler(async (event) => {\n  const { pathname } = getRouterParams(event);\n\n  return hubBlob().serve(event, pathname);\n});\n",[166,7540,7541,7563,7581,7585,7599],{"__ignoreMap":261},[265,7542,7543,7545,7547,7549,7551,7553,7555,7557,7559,7561],{"class":267,"line":268},[265,7544,438],{"class":329},[265,7546,441],{"class":329},[265,7548,854],{"class":278},[265,7550,857],{"class":292},[265,7552,860],{"class":329},[265,7554,863],{"class":292},[265,7556,867],{"class":866},[265,7558,870],{"class":292},[265,7560,873],{"class":329},[265,7562,876],{"class":292},[265,7564,7565,7567,7569,7572,7574,7576,7579],{"class":267,"line":275},[265,7566,881],{"class":329},[265,7568,2033],{"class":292},[265,7570,7571],{"class":296},"pathname",[265,7573,2039],{"class":292},[265,7575,330],{"class":329},[265,7577,7578],{"class":278}," getRouterParams",[265,7580,896],{"class":292},[265,7582,7583],{"class":267,"line":476},[265,7584,480],{"emptyLinePlaceholder":479},[265,7586,7587,7589,7591,7593,7596],{"class":267,"line":483},[265,7588,1256],{"class":329},[265,7590,1259],{"class":278},[265,7592,1031],{"class":292},[265,7594,7595],{"class":278},"serve",[265,7597,7598],{"class":292},"(event, pathname);\n",[265,7600,7601],{"class":267,"line":495},[265,7602,666],{"class":292},[10,7604,7605,7606,7608,7609,7612,7613,208],{},"What we’ve done above is to catch all requests to the ",[166,7607,7527],{}," path (by using the wildcard ",[166,7610,7611],{},"[…pathname]"," in the filename), and serve the requested recording from the storage using ",[166,7614,811],{},[10,7616,7617],{},"With this, the frontend is complete, and all functionalities should now work seamlessly.",[50,7619,7621],{"id":7620},"further-enhancements","Further Enhancements",[10,7623,7624],{},"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:",[10,7626,7627],{},"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:",[125,7629,7630,7633,7640,7643],{},[64,7631,7632],{},"Adding a settings page to save post processing settings",[64,7634,7635,7636,7639],{},"Handle post processing in the ",[166,7637,7638],{},"\u002Ftranscribe"," api route",[64,7641,7642],{},"Allowing edit\u002Fdelete of saved notes",[64,7644,7645],{},"Experimenting with additional features that fit your use case or user needs.",[10,7647,7648],{},"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.",[50,7650,7652],{"id":7651},"deploying-the-application","Deploying the Application",[10,7654,7655],{},"You can deploy the application using either the NuxtHub admin dashboard or through the NuxtHub CLI.",[185,7657,7659],{"id":7658},"deploy-via-nuxthub-admin","Deploy via NuxtHub Admin",[61,7661,7662,7665,7668],{},[64,7663,7664],{},"Push your code to a GitHub repository.",[64,7666,7667],{},"Link the repository with NuxtHub.",[64,7669,7670],{},"Deploy from the Admin console.",[10,7672,7673],{},[14,7674,7677],{"href":7675,"rel":7676},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fdeploy#cloudflare-pages-ci",[18],"Learn more about NuxtHub Git integration",[185,7679,7681],{"id":7680},"deploy-via-nuxthub-cli","Deploy via NuxtHub CLI",[256,7683,7685],{"className":258,"code":7684,"language":260,"meta":261,"style":261},"npx nuxthub deploy\n",[166,7686,7687],{"__ignoreMap":261},[265,7688,7689,7691,7693],{"class":267,"line":268},[265,7690,279],{"class":278},[265,7692,283],{"class":282},[265,7694,7695],{"class":282}," deploy\n",[10,7697,7698],{},[14,7699,7702],{"href":7700,"rel":7701},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fdeploy#nuxthub-cli",[18],"Learn more about CLI deployment",[50,7704,7706],{"id":7705},"source-code","Source Code",[10,7708,7709,7710,7712],{},"You can find the source code of ",[166,7711,33],{}," application on GitHub. The source code includes all the features discussed in this article, along with additional configurations and optimizations shown in the demo.",[92,7714],{"url":7715},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fvhisper",[50,7717,7719],{"id":7718},"conclusion","Conclusion",[10,7721,7722],{},"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.",[10,7724,7725],{},"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.",[10,7727,7728],{},"Until next time!",[7730,7731],"hr",{},[7733,7734,7735],"blockquote",{},[10,7736,7737],{},[39,7738,7739],{},"Keep adding the bits and soon you'll have a lot of bytes to share with the world.",[7741,7742,7743],"style",{},"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":261,"searchDepth":275,"depth":275,"links":7745},[7746,7747,7753,7765,7781,7782,7786,7787],{"id":52,"depth":275,"text":53},{"id":119,"depth":275,"text":120,"children":7748},[7749,7750,7752],{"id":187,"depth":476,"text":188},{"id":247,"depth":476,"text":7751},"Project Init",{"id":724,"depth":476,"text":725},{"id":779,"depth":275,"text":780,"children":7754},[7755,7756,7758,7760,7762,7764],{"id":789,"depth":476,"text":790},{"id":818,"depth":476,"text":7757},"\u002Fapi\u002Ftranscribe Endpoint",{"id":1205,"depth":476,"text":7759},"\u002Fapi\u002Fupload Endpoint",{"id":1394,"depth":476,"text":7761},"Defining the notes Table Schema",{"id":1956,"depth":476,"text":7763},"\u002Fapi\u002Fnotes Endpoints",{"id":2700,"depth":476,"text":2701},{"id":2775,"depth":275,"text":2776,"children":7766},[7767,7769,7771,7773,7775,7777,7778,7780],{"id":2782,"depth":476,"text":7768},"useMediaRecorder Composable",{"id":4197,"depth":476,"text":7770},"NoteEditorModal Component",{"id":4821,"depth":476,"text":7772},"NoteRecorder Component",{"id":5809,"depth":476,"text":7774},"AudioVisualizer Component",{"id":6136,"depth":476,"text":7776},"useRecordings Composable",{"id":6649,"depth":476,"text":6650},{"id":7056,"depth":476,"text":7779},"NoteCard component",{"id":7436,"depth":476,"text":7437},{"id":7620,"depth":275,"text":7621},{"id":7651,"depth":275,"text":7652,"children":7783},[7784,7785],{"id":7658,"depth":476,"text":7659},{"id":7680,"depth":476,"text":7681},{"id":7705,"depth":275,"text":7706},{"id":7718,"depth":275,"text":7719},null,"\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...",false,"md","cm41pk3m3000k09l8g5kre7ep",{},"\u002Fbuilding-voice-notes-app-with-ai-transcription-and-post-processing",{"title":5,"description":7791},"building-voice-notes-app-with-ai-transcription-and-post-processing",[7800,7801,7802,7803,7804],"cloudflare","ai","nuxt","nuxthub","nuxtui","F2sAwrWwQifuhruoNFjtPxFdDSKeOa-CJAFIOqnpj1w",1780470200095]