[{"data":1,"prerenderedAt":4872},["ShallowReactive",2],{"post-create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui":3},{"id":4,"title":5,"body":6,"cover":4855,"date":4856,"description":4857,"draft":4858,"extension":4859,"hashnodeId":4860,"meta":4861,"navigation":216,"path":4862,"seo":4863,"slug":4864,"stem":4864,"tags":4865,"__hash__":4871},"posts\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui.md","Create Your Own Cloudflare Workers AI LLM Playground Using NuxtHub and NuxtUI",{"type":7,"value":8,"toc":4826},"minimark",[9,21,24,29,44,66,69,76,83,86,90,93,98,131,135,138,163,167,170,278,290,293,297,303,307,310,531,538,542,545,548,1132,1136,1139,1144,1147,1322,1327,1330,1490,1505,1510,1513,1624,1635,1638,1642,1649,1866,1892,1895,1909,1912,1916,1934,2362,2368,2375,2378,2382,2385,2389,2392,2400,2412,2415,2426,2433,2436,2439,2443,2452,2667,2687,2690,2720,2733,3376,3382,3389,3393,3399,3856,3859,3867,3870,3874,3877,3881,3891,3915,3938,4042,4063,4072,4190,4196,4209,4213,4223,4232,4238,4241,4457,4460,4464,4471,4525,4532,4568,4579,4713,4719,4722,4726,4729,4732,4746,4753,4761,4765,4768,4772,4776,4779,4790,4793,4804,4807,4810,4813,4822],[10,11,12,13,20],"p",{},"You might be wondering, 'Another LLM (Large Language Model) playground? Aren't there plenty of these already?' Fair question. But here's the thing: the world of AI is constantly evolving, and every day one new tool or another pops up. One such AI offering from ",[14,15,19],"a",{"href":16,"rel":17},"https:\u002F\u002Fhub.nuxt.com\u002F",[18],"nofollow","NuxtHub"," was launched recently, and I couldn’t resist taking it for a spin. And let’s be honest—some of us just can't resist adding a 'dark mode', something the Cloudflare Workers AI models playground hasn’t embraced yet.",[10,22,23],{},"But beyond these practical reasons, there’s something even more satisfying: the joy of creation and learning. So, if you're ready to dive in, maybe you'll pick up some new concepts like Server-Sent Events, response streaming, markdown parsing, and, of course, deploying your own chat playground without any of the usual fuss. So, get comfy—however you like—and let's get started!",[25,26,28],"h2",{"id":27},"project-overview","Project Overview",[10,30,31,32,37,38,43],{},"Alright, so what exactly are we going to create here? As you might have guessed already, we will be creating a chat interface to talk to different ",[14,33,36],{"href":34,"rel":35},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers-ai\u002Fmodels#text-generation",[18],"text generation models"," supported by ",[14,39,42],{"href":40,"rel":41},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers-ai\u002F",[18],"Cloudflare Workers AI",". Below is a brief list of capabilities we will build along the way:",[45,46,47,51,54,57,60,63],"ul",{},[48,49,50],"li",{},"Ability to set different LLM params, like temperature, max tokens, system prompt, top_p, top_k etc while keeping some of these optional",[48,52,53],{},"Ability to turn LLM response streaming on\u002Foff",[48,55,56],{},"Handle streaming\u002Fnon-streaming LLM responses on both, the server and the client side",[48,58,59],{},"Parsing LLM responses for markdown and display it appropriately",[48,61,62],{},"Auto-scrolling the chat container as the response is streamed from the LLM endpoint",[48,64,65],{},"Adding the dark mode (this one is trivial but let's add it here for completeness)",[10,67,68],{},"This is how the interface will look when we are through this article:",[10,70,71],{},[72,73],"img",{"alt":74,"src":75},"LLM playground chat interface ","\u002Fimages\u002Fposts\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui\u002F4dc571f5-3c82-4496-a75b-6a70d4dbbd0c-f1a7f42690.png",[10,77,78,79],{},"You can try it out live here: ",[14,80,81],{"href":81,"rel":82},"https:\u002F\u002Fhub-chat.nuxt.dev\u002F",[18],[10,84,85],{},"We will cover each of the tasks in detail in the following sections.",[25,87,89],{"id":88},"project-setup","Project Setup",[10,91,92],{},"Now that we've set the stage for our LLM playground project, let's dive into the technologies we'll be using and get our development environment ready.",[94,95,97],"h3",{"id":96},"technologies-well-use","Technologies we'll use",[99,100,101,109,117,123],"ol",{},[48,102,103,108],{},[14,104,107],{"href":105,"rel":106},"https:\u002F\u002Fnuxt.com\u002F",[18],"Nuxt 3",": Nuxt 3 is a powerful Vue.js framework that will serve as the foundation of our application.",[48,110,111,116],{},[14,112,115],{"href":113,"rel":114},"https:\u002F\u002Fui.nuxt.com\u002F",[18],"Nuxt UI",": A Nuxt module that will help us create a sleek and responsive interface.",[48,118,119,122],{},[14,120,19],{"href":16,"rel":121},[18],": NuxtHub is a deployment and administration platform for Nuxt, powered by Cloudflare. We can use NuxtHub to access different Cloudflare offerings like D1 database, Workers KV, R2 Storage, Workers AI etc. In this project, we'll use it to access the LLMs as well as to deploy our project",[48,124,125,130],{},[14,126,129],{"href":127,"rel":128},"https:\u002F\u002Fgithub.com\u002Fnuxt-modules\u002Fmdc",[18],"Nuxt MDC",": For parsing and displaying the chat messages",[94,132,134],{"id":133},"prerequisites","Prerequisites",[10,136,137],{},"Apart from the basic prerequisites like Node\u002FNpm, Code Editors, and some VueJs\u002FNuxt knowledge, you'll need the following to follow along:",[99,139,140,153],{},[48,141,142,146,147,152],{},[143,144,145],"strong",{},"A Cloudflare account:"," To use the Workers AI models, as well as to deploy your project on Cloudflare Pages for free. If you don't have it already, you can set it up ",[14,148,151],{"href":149,"rel":150},"https:\u002F\u002Fwww.cloudflare.com\u002F",[18],"here",".",[48,154,155,158,159,152],{},[143,156,157],{},"A NuxtHub Admin Account:"," NuxtHub admin is a web based dashboard to manage NuxtHub apps. You can create your account ",[14,160,151],{"href":161,"rel":162},"https:\u002F\u002Fadmin.hub.nuxt.com\u002F",[18],[94,164,166],{"id":165},"setting-up-the-project","Setting up the project",[10,168,169],{},"We can either start with a Nuxt (or Nuxt UI) template and add NuxtHub on top of it, or we can use the NuxtHub starter template and add Nuxt UI to it. We'll take the second approach:",[99,171,172,233,257],{},[48,173,174,175],{},"Create a new NuxtHub project",[176,177,182],"pre",{"className":178,"code":179,"language":180,"meta":181,"style":181},"language-bash shiki shiki-themes github-light github-dark","# Init the project and install dependencies\nnpx nuxthub init cf-playground\n\n# Change into the created dir\ncd cf-playground\n","bash","",[183,184,185,194,211,218,224],"code",{"__ignoreMap":181},[186,187,190],"span",{"class":188,"line":189},"line",1,[186,191,193],{"class":192},"sJ8bj","# Init the project and install dependencies\n",[186,195,197,201,205,208],{"class":188,"line":196},2,[186,198,200],{"class":199},"sScJk","npx",[186,202,204],{"class":203},"sZZnC"," nuxthub",[186,206,207],{"class":203}," init",[186,209,210],{"class":203}," cf-playground\n",[186,212,214],{"class":188,"line":213},3,[186,215,217],{"emptyLinePlaceholder":216},true,"\n",[186,219,221],{"class":188,"line":220},4,[186,222,223],{"class":192},"# Change into the created dir\n",[186,225,227,231],{"class":188,"line":226},5,[186,228,230],{"class":229},"sj4cs","cd",[186,232,210],{"class":203},[48,234,235,236],{},"Add the Nuxt UI module. The below command will install the @nuxt\u002Fui dependency as well as add it as a module in your nuxt config file",[176,237,239],{"className":178,"code":238,"language":180,"meta":181,"style":181},"npx nuxi module add ui\n",[183,240,241],{"__ignoreMap":181},[186,242,243,245,248,251,254],{"class":188,"line":189},[186,244,200],{"class":199},[186,246,247],{"class":203}," nuxi",[186,249,250],{"class":203}," module",[186,252,253],{"class":203}," add",[186,255,256],{"class":203}," ui\n",[48,258,259,260],{},"Similarly, add the Nuxt MDC module",[176,261,263],{"className":178,"code":262,"language":180,"meta":181,"style":181},"npx nuxi module add mdc\n",[183,264,265],{"__ignoreMap":181},[186,266,267,269,271,273,275],{"class":188,"line":189},[186,268,200],{"class":199},[186,270,247],{"class":203},[186,272,250],{"class":203},[186,274,253],{"class":203},[186,276,277],{"class":203}," mdc\n",[10,279,280,281,284,285,289],{},"Now we have setup everything that we need for this project. You can try running the project with ",[183,282,283],{},"pnpm dev"," (or an equivalent command if you're using a different package manager) and visit ",[14,286,287],{"href":287,"rel":288},"http:\u002F\u002Flocalhost:3000",[18]," in your browser. If you've dark mode enabled on your system then you'll notice that dark mode is already up and running in the project (We just need a way to toggle it).",[10,291,292],{},"With our development environment set up, we're ready to start building our LLM playground. In the next section, we'll begin implementing our user interface using NuxtUI components.",[25,294,296],{"id":295},"creating-the-ui-components","Creating the UI Components",[10,298,299,300,152],{},"First, we will create a component that will let us set numerical values for LLM settings using either a range slider or an input box. Let's call it ",[183,301,302],{},"RangeInput",[94,304,306],{"id":305},"the-rangeinput-component","The RangeInput Component",[10,308,309],{},"We utilize a FormGroup component from NuxtUI to create this component. We put a Range slider in the default slot and an input in the hint slot to achieve the desired outcome. Here is the complete component code",[176,311,315],{"className":312,"code":313,"language":314,"meta":181,"style":181},"language-xml shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003CUFormGroup :label=\"label\" :ui=\"{ container: 'mt-2' }\">\n    \u003Ctemplate #hint>\n      \u003CUInput\n        v-model=\"model\"\n        class=\"w-[72px]\"\n        type=\"number\"\n        :min=\"min\"\n        :max=\"max\"\n        :step=\"step\"\n      \u002F>\n    \u003C\u002Ftemplate>\n    \u003CURange v-model=\"model\" :min=\"min\" :max=\"max\" :step=\"step\" size=\"sm\" \u002F>\n  \u003C\u002FUFormGroup>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst model = defineModel({ type: Number, default: undefined });\n\ndefineProps({\n  label: {\n    type: String,\n    required: true,\n  },\n  min: {\n    type: Number,\n    default: undefined,\n  },\n  max: {\n    type: Number,\n    default: undefined,\n  },\n  step: {\n    type: Number,\n    default: undefined,\n  },\n});\n\u003C\u002Fscript>\n","xml",[183,316,317,322,327,332,337,342,348,354,360,366,372,378,384,390,396,402,407,413,419,424,430,436,442,448,454,460,466,472,477,483,488,493,498,504,509,514,519,525],{"__ignoreMap":181},[186,318,319],{"class":188,"line":189},[186,320,321],{},"\u003Ctemplate>\n",[186,323,324],{"class":188,"line":196},[186,325,326],{},"  \u003CUFormGroup :label=\"label\" :ui=\"{ container: 'mt-2' }\">\n",[186,328,329],{"class":188,"line":213},[186,330,331],{},"    \u003Ctemplate #hint>\n",[186,333,334],{"class":188,"line":220},[186,335,336],{},"      \u003CUInput\n",[186,338,339],{"class":188,"line":226},[186,340,341],{},"        v-model=\"model\"\n",[186,343,345],{"class":188,"line":344},6,[186,346,347],{},"        class=\"w-[72px]\"\n",[186,349,351],{"class":188,"line":350},7,[186,352,353],{},"        type=\"number\"\n",[186,355,357],{"class":188,"line":356},8,[186,358,359],{},"        :min=\"min\"\n",[186,361,363],{"class":188,"line":362},9,[186,364,365],{},"        :max=\"max\"\n",[186,367,369],{"class":188,"line":368},10,[186,370,371],{},"        :step=\"step\"\n",[186,373,375],{"class":188,"line":374},11,[186,376,377],{},"      \u002F>\n",[186,379,381],{"class":188,"line":380},12,[186,382,383],{},"    \u003C\u002Ftemplate>\n",[186,385,387],{"class":188,"line":386},13,[186,388,389],{},"    \u003CURange v-model=\"model\" :min=\"min\" :max=\"max\" :step=\"step\" size=\"sm\" \u002F>\n",[186,391,393],{"class":188,"line":392},14,[186,394,395],{},"  \u003C\u002FUFormGroup>\n",[186,397,399],{"class":188,"line":398},15,[186,400,401],{},"\u003C\u002Ftemplate>\n",[186,403,405],{"class":188,"line":404},16,[186,406,217],{"emptyLinePlaceholder":216},[186,408,410],{"class":188,"line":409},17,[186,411,412],{},"\u003Cscript setup lang=\"ts\">\n",[186,414,416],{"class":188,"line":415},18,[186,417,418],{},"const model = defineModel({ type: Number, default: undefined });\n",[186,420,422],{"class":188,"line":421},19,[186,423,217],{"emptyLinePlaceholder":216},[186,425,427],{"class":188,"line":426},20,[186,428,429],{},"defineProps({\n",[186,431,433],{"class":188,"line":432},21,[186,434,435],{},"  label: {\n",[186,437,439],{"class":188,"line":438},22,[186,440,441],{},"    type: String,\n",[186,443,445],{"class":188,"line":444},23,[186,446,447],{},"    required: true,\n",[186,449,451],{"class":188,"line":450},24,[186,452,453],{},"  },\n",[186,455,457],{"class":188,"line":456},25,[186,458,459],{},"  min: {\n",[186,461,463],{"class":188,"line":462},26,[186,464,465],{},"    type: Number,\n",[186,467,469],{"class":188,"line":468},27,[186,470,471],{},"    default: undefined,\n",[186,473,475],{"class":188,"line":474},28,[186,476,453],{},[186,478,480],{"class":188,"line":479},29,[186,481,482],{},"  max: {\n",[186,484,486],{"class":188,"line":485},30,[186,487,465],{},[186,489,491],{"class":188,"line":490},31,[186,492,471],{},[186,494,496],{"class":188,"line":495},32,[186,497,453],{},[186,499,501],{"class":188,"line":500},33,[186,502,503],{},"  step: {\n",[186,505,507],{"class":188,"line":506},34,[186,508,465],{},[186,510,512],{"class":188,"line":511},35,[186,513,471],{},[186,515,517],{"class":188,"line":516},36,[186,518,453],{},[186,520,522],{"class":188,"line":521},37,[186,523,524],{},"});\n",[186,526,528],{"class":188,"line":527},38,[186,529,530],{},"\u003C\u002Fscript>\n",[10,532,533,534,537],{},"Instead of taking the model value as a prop and then emitting the changes manually, we use the ",[183,535,536],{},"defineModel"," macro to achieve two way binding. If the initial model value is undefined the input box will be empty. This will help in making some of the LLM params optional.",[94,539,541],{"id":540},"the-llmsettings-component","The LLMSettings Component",[10,543,544],{},"LLMSettings component utilizes many RangeInputs that we created above as most of the settings are numerical values. Apart from RangeInputs we use a textarea for the system prompt, a toggle for enabling\u002Fdisabling response streaming, a select menu for choosing the LLM model, and an accordion for hiding the optional params.",[10,546,547],{},"Here are the relevant parts of the component",[176,549,551],{"className":312,"code":550,"language":314,"meta":181,"style":181},"\u003Ctemplate>\n  \u003Cdiv class=\"h-full flex flex-col overflow-hidden\">\n    \u003C!-- Settings Header Code -->\n    \u003CUDivider \u002F>\n    \u003Cdiv class=\"p-4 flex-1 space-y-6 overflow-y-auto\">\n      \u003CUFormGroup label=\"Model\">\n        \u003CUSelectMenu\n          v-model=\"llmParams.model\"\n          size=\"md\"\n          :options=\"models\"\n          value-attribute=\"id\"\n          option-attribute=\"name\"\n        \u002F>\n      \u003C\u002FUFormGroup>\n\n      \u003CRangeInput\n        v-model=\"llmParams.temperature\"\n        label=\"Temperature\"\n        :min=\"0\"\n        :max=\"5\"\n        :step=\"0.1\"\n      \u002F>\n\n      \u003CRangeInput\n        v-model=\"llmParams.maxTokens\"\n        label=\"Max Tokens\"\n        :min=\"1\"\n        :max=\"4096\"\n      \u002F>\n\n      \u003CUFormGroup label=\"System Prompt\">\n        \u003CUTextarea\n          v-model=\"llmParams.systemPrompt\"\n          :rows=\"3\"\n          :maxrows=\"8\"\n          autoresize\n        \u002F>\n      \u003C\u002FUFormGroup>\n\n      \u003Cdiv class=\"flex items-center justify-between\">\n        \u003Cspan>Stream Response\u003C\u002Fspan>\n        \u003CUToggle v-model=\"llmParams.stream\" \u002F>\n      \u003C\u002Fdiv>\n\n      \u003CUAccordion\n        :items=\"accordionItems\"\n        color=\"white\"\n        variant=\"solid\"\n        size=\"md\"\n      >\n        \u003Ctemplate #item>\n          \u003CUCard :ui=\"{ body: { base: 'space-y-6', padding: 'p-4 sm:p-4' } }\">\n            \u003CRangeInput\n              v-model=\"llmParams.topP\"\n              label=\"Top P\"\n              :min=\"0\"\n              :max=\"2\"\n              :step=\"0.1\"\n            \u002F>\n\n            \u003C!-- Other optional params -->\n          \u003C\u002FUCard>\n        \u003C\u002Ftemplate>\n      \u003C\u002FUAccordion>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ntype LlmParams = {\n  model: string;\n  temperature: number;\n  maxTokens: number;\n  topP?: number;\n  topK?: number;\n  frequencyPenalty?: number;\n  presencePenalty?: number;\n  systemPrompt: string;\n  stream: boolean;\n};\n\nconst llmParams = defineModel('llmParams', {\n  type: Object as () => LlmParams,\n  required: true,\n});\n\ndefineEmits(['hideDrawer', 'reset']);\n\nconst accordionItems = [\n  {\n    label: 'Advanced Settings',\n    defaultOpen: false,\n  },\n];\n\nconst models = [\n  {\n    name: 'deepseek-coder-6.7b-base-awq',\n    id: '@hf\u002Fthebloke\u002Fdeepseek-coder-6.7b-base-awq',\n  },\n  { \n    name: 'llama-3-8b-instruct', \n    id: '@cf\u002Fmeta\u002Fllama-3-8b-instruct', \n  },\n  \u002F\u002F ...other models\n]\n\u003C\u002Fscript>\n",[183,552,553,557,562,567,572,577,582,587,592,597,602,607,612,617,622,626,631,636,641,646,651,656,660,664,668,673,678,683,688,692,696,701,706,711,716,721,726,730,734,739,745,751,757,763,768,774,780,786,792,798,804,810,816,822,828,834,840,846,852,858,863,869,875,881,887,893,899,904,909,914,920,926,932,938,944,950,956,962,968,974,980,985,991,997,1003,1008,1013,1019,1024,1030,1036,1042,1048,1053,1059,1064,1070,1075,1081,1087,1092,1098,1104,1110,1115,1121,1127],{"__ignoreMap":181},[186,554,555],{"class":188,"line":189},[186,556,321],{},[186,558,559],{"class":188,"line":196},[186,560,561],{},"  \u003Cdiv class=\"h-full flex flex-col overflow-hidden\">\n",[186,563,564],{"class":188,"line":213},[186,565,566],{},"    \u003C!-- Settings Header Code -->\n",[186,568,569],{"class":188,"line":220},[186,570,571],{},"    \u003CUDivider \u002F>\n",[186,573,574],{"class":188,"line":226},[186,575,576],{},"    \u003Cdiv class=\"p-4 flex-1 space-y-6 overflow-y-auto\">\n",[186,578,579],{"class":188,"line":344},[186,580,581],{},"      \u003CUFormGroup label=\"Model\">\n",[186,583,584],{"class":188,"line":350},[186,585,586],{},"        \u003CUSelectMenu\n",[186,588,589],{"class":188,"line":356},[186,590,591],{},"          v-model=\"llmParams.model\"\n",[186,593,594],{"class":188,"line":362},[186,595,596],{},"          size=\"md\"\n",[186,598,599],{"class":188,"line":368},[186,600,601],{},"          :options=\"models\"\n",[186,603,604],{"class":188,"line":374},[186,605,606],{},"          value-attribute=\"id\"\n",[186,608,609],{"class":188,"line":380},[186,610,611],{},"          option-attribute=\"name\"\n",[186,613,614],{"class":188,"line":386},[186,615,616],{},"        \u002F>\n",[186,618,619],{"class":188,"line":392},[186,620,621],{},"      \u003C\u002FUFormGroup>\n",[186,623,624],{"class":188,"line":398},[186,625,217],{"emptyLinePlaceholder":216},[186,627,628],{"class":188,"line":404},[186,629,630],{},"      \u003CRangeInput\n",[186,632,633],{"class":188,"line":409},[186,634,635],{},"        v-model=\"llmParams.temperature\"\n",[186,637,638],{"class":188,"line":415},[186,639,640],{},"        label=\"Temperature\"\n",[186,642,643],{"class":188,"line":421},[186,644,645],{},"        :min=\"0\"\n",[186,647,648],{"class":188,"line":426},[186,649,650],{},"        :max=\"5\"\n",[186,652,653],{"class":188,"line":432},[186,654,655],{},"        :step=\"0.1\"\n",[186,657,658],{"class":188,"line":438},[186,659,377],{},[186,661,662],{"class":188,"line":444},[186,663,217],{"emptyLinePlaceholder":216},[186,665,666],{"class":188,"line":450},[186,667,630],{},[186,669,670],{"class":188,"line":456},[186,671,672],{},"        v-model=\"llmParams.maxTokens\"\n",[186,674,675],{"class":188,"line":462},[186,676,677],{},"        label=\"Max Tokens\"\n",[186,679,680],{"class":188,"line":468},[186,681,682],{},"        :min=\"1\"\n",[186,684,685],{"class":188,"line":474},[186,686,687],{},"        :max=\"4096\"\n",[186,689,690],{"class":188,"line":479},[186,691,377],{},[186,693,694],{"class":188,"line":485},[186,695,217],{"emptyLinePlaceholder":216},[186,697,698],{"class":188,"line":490},[186,699,700],{},"      \u003CUFormGroup label=\"System Prompt\">\n",[186,702,703],{"class":188,"line":495},[186,704,705],{},"        \u003CUTextarea\n",[186,707,708],{"class":188,"line":500},[186,709,710],{},"          v-model=\"llmParams.systemPrompt\"\n",[186,712,713],{"class":188,"line":506},[186,714,715],{},"          :rows=\"3\"\n",[186,717,718],{"class":188,"line":511},[186,719,720],{},"          :maxrows=\"8\"\n",[186,722,723],{"class":188,"line":516},[186,724,725],{},"          autoresize\n",[186,727,728],{"class":188,"line":521},[186,729,616],{},[186,731,732],{"class":188,"line":527},[186,733,621],{},[186,735,737],{"class":188,"line":736},39,[186,738,217],{"emptyLinePlaceholder":216},[186,740,742],{"class":188,"line":741},40,[186,743,744],{},"      \u003Cdiv class=\"flex items-center justify-between\">\n",[186,746,748],{"class":188,"line":747},41,[186,749,750],{},"        \u003Cspan>Stream Response\u003C\u002Fspan>\n",[186,752,754],{"class":188,"line":753},42,[186,755,756],{},"        \u003CUToggle v-model=\"llmParams.stream\" \u002F>\n",[186,758,760],{"class":188,"line":759},43,[186,761,762],{},"      \u003C\u002Fdiv>\n",[186,764,766],{"class":188,"line":765},44,[186,767,217],{"emptyLinePlaceholder":216},[186,769,771],{"class":188,"line":770},45,[186,772,773],{},"      \u003CUAccordion\n",[186,775,777],{"class":188,"line":776},46,[186,778,779],{},"        :items=\"accordionItems\"\n",[186,781,783],{"class":188,"line":782},47,[186,784,785],{},"        color=\"white\"\n",[186,787,789],{"class":188,"line":788},48,[186,790,791],{},"        variant=\"solid\"\n",[186,793,795],{"class":188,"line":794},49,[186,796,797],{},"        size=\"md\"\n",[186,799,801],{"class":188,"line":800},50,[186,802,803],{},"      >\n",[186,805,807],{"class":188,"line":806},51,[186,808,809],{},"        \u003Ctemplate #item>\n",[186,811,813],{"class":188,"line":812},52,[186,814,815],{},"          \u003CUCard :ui=\"{ body: { base: 'space-y-6', padding: 'p-4 sm:p-4' } }\">\n",[186,817,819],{"class":188,"line":818},53,[186,820,821],{},"            \u003CRangeInput\n",[186,823,825],{"class":188,"line":824},54,[186,826,827],{},"              v-model=\"llmParams.topP\"\n",[186,829,831],{"class":188,"line":830},55,[186,832,833],{},"              label=\"Top P\"\n",[186,835,837],{"class":188,"line":836},56,[186,838,839],{},"              :min=\"0\"\n",[186,841,843],{"class":188,"line":842},57,[186,844,845],{},"              :max=\"2\"\n",[186,847,849],{"class":188,"line":848},58,[186,850,851],{},"              :step=\"0.1\"\n",[186,853,855],{"class":188,"line":854},59,[186,856,857],{},"            \u002F>\n",[186,859,861],{"class":188,"line":860},60,[186,862,217],{"emptyLinePlaceholder":216},[186,864,866],{"class":188,"line":865},61,[186,867,868],{},"            \u003C!-- Other optional params -->\n",[186,870,872],{"class":188,"line":871},62,[186,873,874],{},"          \u003C\u002FUCard>\n",[186,876,878],{"class":188,"line":877},63,[186,879,880],{},"        \u003C\u002Ftemplate>\n",[186,882,884],{"class":188,"line":883},64,[186,885,886],{},"      \u003C\u002FUAccordion>\n",[186,888,890],{"class":188,"line":889},65,[186,891,892],{},"    \u003C\u002Fdiv>\n",[186,894,896],{"class":188,"line":895},66,[186,897,898],{},"  \u003C\u002Fdiv>\n",[186,900,902],{"class":188,"line":901},67,[186,903,401],{},[186,905,907],{"class":188,"line":906},68,[186,908,217],{"emptyLinePlaceholder":216},[186,910,912],{"class":188,"line":911},69,[186,913,412],{},[186,915,917],{"class":188,"line":916},70,[186,918,919],{},"type LlmParams = {\n",[186,921,923],{"class":188,"line":922},71,[186,924,925],{},"  model: string;\n",[186,927,929],{"class":188,"line":928},72,[186,930,931],{},"  temperature: number;\n",[186,933,935],{"class":188,"line":934},73,[186,936,937],{},"  maxTokens: number;\n",[186,939,941],{"class":188,"line":940},74,[186,942,943],{},"  topP?: number;\n",[186,945,947],{"class":188,"line":946},75,[186,948,949],{},"  topK?: number;\n",[186,951,953],{"class":188,"line":952},76,[186,954,955],{},"  frequencyPenalty?: number;\n",[186,957,959],{"class":188,"line":958},77,[186,960,961],{},"  presencePenalty?: number;\n",[186,963,965],{"class":188,"line":964},78,[186,966,967],{},"  systemPrompt: string;\n",[186,969,971],{"class":188,"line":970},79,[186,972,973],{},"  stream: boolean;\n",[186,975,977],{"class":188,"line":976},80,[186,978,979],{},"};\n",[186,981,983],{"class":188,"line":982},81,[186,984,217],{"emptyLinePlaceholder":216},[186,986,988],{"class":188,"line":987},82,[186,989,990],{},"const llmParams = defineModel('llmParams', {\n",[186,992,994],{"class":188,"line":993},83,[186,995,996],{},"  type: Object as () => LlmParams,\n",[186,998,1000],{"class":188,"line":999},84,[186,1001,1002],{},"  required: true,\n",[186,1004,1006],{"class":188,"line":1005},85,[186,1007,524],{},[186,1009,1011],{"class":188,"line":1010},86,[186,1012,217],{"emptyLinePlaceholder":216},[186,1014,1016],{"class":188,"line":1015},87,[186,1017,1018],{},"defineEmits(['hideDrawer', 'reset']);\n",[186,1020,1022],{"class":188,"line":1021},88,[186,1023,217],{"emptyLinePlaceholder":216},[186,1025,1027],{"class":188,"line":1026},89,[186,1028,1029],{},"const accordionItems = [\n",[186,1031,1033],{"class":188,"line":1032},90,[186,1034,1035],{},"  {\n",[186,1037,1039],{"class":188,"line":1038},91,[186,1040,1041],{},"    label: 'Advanced Settings',\n",[186,1043,1045],{"class":188,"line":1044},92,[186,1046,1047],{},"    defaultOpen: false,\n",[186,1049,1051],{"class":188,"line":1050},93,[186,1052,453],{},[186,1054,1056],{"class":188,"line":1055},94,[186,1057,1058],{},"];\n",[186,1060,1062],{"class":188,"line":1061},95,[186,1063,217],{"emptyLinePlaceholder":216},[186,1065,1067],{"class":188,"line":1066},96,[186,1068,1069],{},"const models = [\n",[186,1071,1073],{"class":188,"line":1072},97,[186,1074,1035],{},[186,1076,1078],{"class":188,"line":1077},98,[186,1079,1080],{},"    name: 'deepseek-coder-6.7b-base-awq',\n",[186,1082,1084],{"class":188,"line":1083},99,[186,1085,1086],{},"    id: '@hf\u002Fthebloke\u002Fdeepseek-coder-6.7b-base-awq',\n",[186,1088,1090],{"class":188,"line":1089},100,[186,1091,453],{},[186,1093,1095],{"class":188,"line":1094},101,[186,1096,1097],{},"  { \n",[186,1099,1101],{"class":188,"line":1100},102,[186,1102,1103],{},"    name: 'llama-3-8b-instruct', \n",[186,1105,1107],{"class":188,"line":1106},103,[186,1108,1109],{},"    id: '@cf\u002Fmeta\u002Fllama-3-8b-instruct', \n",[186,1111,1113],{"class":188,"line":1112},104,[186,1114,453],{},[186,1116,1118],{"class":188,"line":1117},105,[186,1119,1120],{},"  \u002F\u002F ...other models\n",[186,1122,1124],{"class":188,"line":1123},106,[186,1125,1126],{},"]\n",[186,1128,1130],{"class":188,"line":1129},107,[186,1131,530],{},[94,1133,1135],{"id":1134},"the-chatpanel-component","The ChatPanel Component",[10,1137,1138],{},"This is the most important component of all as it handles the core chat functionality of the app. It consists of three parts:",[10,1140,1141],{},[143,1142,1143],{},"Chat Header",[10,1145,1146],{},"It displays the app name\u002Flabel apart from some global buttons for clearing chats, dark mode toggling and for showing the settings drawer on mobile devices",[176,1148,1150],{"className":312,"code":1149,"language":314,"meta":181,"style":181},"\u003Ctemplate>\n  \u003Cdiv class=\"flex items-center justify-between p-4\">\n    \u003Cdiv class=\"flex items-center gap-x-4\">\n      \u003Ch2 class=\"text-xl md:text-2xl text-primary font-bold\">Hub Chat\u003C\u002Fh2>\n      \u003CUTooltip text=\"Clear chat\">\n        \u003CUButton\n          color=\"gray\"\n          icon=\"i-heroicons-trash\"\n          size=\"xs\"\n          :disabled=\"clearDisabled\"\n          @click=\"$emit('clear')\"\n        \u002F>\n      \u003C\u002FUTooltip>\n    \u003C\u002Fdiv>\n    \u003Cdiv class=\"flex items-center gap-x-4\">\n      \u003CColorMode \u002F>\n      \u003CUButton\n        icon=\"i-heroicons-cog-6-tooth\"\n        color=\"gray\"\n        variant=\"ghost\"\n        class=\"md:hidden\"\n        @click=\"$emit('showDrawer')\"\n      \u002F>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\ndefineEmits(['clear', 'showDrawer']);\n\ndefineProps({\n  clearDisabled: {\n    type: Boolean,\n    default: true,\n  },\n});\n\u003C\u002Fscript>\n",[183,1151,1152,1156,1161,1166,1171,1176,1181,1186,1191,1196,1201,1206,1210,1215,1219,1223,1228,1233,1238,1243,1248,1253,1258,1262,1266,1270,1274,1278,1282,1287,1291,1295,1300,1305,1310,1314,1318],{"__ignoreMap":181},[186,1153,1154],{"class":188,"line":189},[186,1155,321],{},[186,1157,1158],{"class":188,"line":196},[186,1159,1160],{},"  \u003Cdiv class=\"flex items-center justify-between p-4\">\n",[186,1162,1163],{"class":188,"line":213},[186,1164,1165],{},"    \u003Cdiv class=\"flex items-center gap-x-4\">\n",[186,1167,1168],{"class":188,"line":220},[186,1169,1170],{},"      \u003Ch2 class=\"text-xl md:text-2xl text-primary font-bold\">Hub Chat\u003C\u002Fh2>\n",[186,1172,1173],{"class":188,"line":226},[186,1174,1175],{},"      \u003CUTooltip text=\"Clear chat\">\n",[186,1177,1178],{"class":188,"line":344},[186,1179,1180],{},"        \u003CUButton\n",[186,1182,1183],{"class":188,"line":350},[186,1184,1185],{},"          color=\"gray\"\n",[186,1187,1188],{"class":188,"line":356},[186,1189,1190],{},"          icon=\"i-heroicons-trash\"\n",[186,1192,1193],{"class":188,"line":362},[186,1194,1195],{},"          size=\"xs\"\n",[186,1197,1198],{"class":188,"line":368},[186,1199,1200],{},"          :disabled=\"clearDisabled\"\n",[186,1202,1203],{"class":188,"line":374},[186,1204,1205],{},"          @click=\"$emit('clear')\"\n",[186,1207,1208],{"class":188,"line":380},[186,1209,616],{},[186,1211,1212],{"class":188,"line":386},[186,1213,1214],{},"      \u003C\u002FUTooltip>\n",[186,1216,1217],{"class":188,"line":392},[186,1218,892],{},[186,1220,1221],{"class":188,"line":398},[186,1222,1165],{},[186,1224,1225],{"class":188,"line":404},[186,1226,1227],{},"      \u003CColorMode \u002F>\n",[186,1229,1230],{"class":188,"line":409},[186,1231,1232],{},"      \u003CUButton\n",[186,1234,1235],{"class":188,"line":415},[186,1236,1237],{},"        icon=\"i-heroicons-cog-6-tooth\"\n",[186,1239,1240],{"class":188,"line":421},[186,1241,1242],{},"        color=\"gray\"\n",[186,1244,1245],{"class":188,"line":426},[186,1246,1247],{},"        variant=\"ghost\"\n",[186,1249,1250],{"class":188,"line":432},[186,1251,1252],{},"        class=\"md:hidden\"\n",[186,1254,1255],{"class":188,"line":438},[186,1256,1257],{},"        @click=\"$emit('showDrawer')\"\n",[186,1259,1260],{"class":188,"line":444},[186,1261,377],{},[186,1263,1264],{"class":188,"line":450},[186,1265,892],{},[186,1267,1268],{"class":188,"line":456},[186,1269,898],{},[186,1271,1272],{"class":188,"line":462},[186,1273,401],{},[186,1275,1276],{"class":188,"line":468},[186,1277,217],{"emptyLinePlaceholder":216},[186,1279,1280],{"class":188,"line":474},[186,1281,412],{},[186,1283,1284],{"class":188,"line":479},[186,1285,1286],{},"defineEmits(['clear', 'showDrawer']);\n",[186,1288,1289],{"class":188,"line":485},[186,1290,217],{"emptyLinePlaceholder":216},[186,1292,1293],{"class":188,"line":490},[186,1294,429],{},[186,1296,1297],{"class":188,"line":495},[186,1298,1299],{},"  clearDisabled: {\n",[186,1301,1302],{"class":188,"line":500},[186,1303,1304],{},"    type: Boolean,\n",[186,1306,1307],{"class":188,"line":506},[186,1308,1309],{},"    default: true,\n",[186,1311,1312],{"class":188,"line":511},[186,1313,453],{},[186,1315,1316],{"class":188,"line":516},[186,1317,524],{},[186,1319,1320],{"class":188,"line":521},[186,1321,530],{},[10,1323,1324],{},[143,1325,1326],{},"Chats Container",[10,1328,1329],{},"For parsing and displaying the chat messages. It also shows a message loading skeleton when streaming is off, as well as a NoChats placeholder when needed.",[176,1331,1333],{"className":312,"code":1332,"language":314,"meta":181,"style":181},"\u003Cdiv ref=\"chatContainer\" class=\"flex-1 overflow-y-auto p-4 space-y-5\">\n  \u003Cdiv\n    v-for=\"(message, index) in chatHistory\"\n    :key=\"`message-${index}`\"\n    class=\"flex items-start gap-x-4\"\n  >\n    \u003Cdiv\n      class=\"w-12 h-12 p-2 rounded-full\"\n      :class=\"`${\n        message.role === 'user' ? 'bg-primary\u002F20' : 'bg-blue-500\u002F20'\n      }`\"\n    >\n      \u003CUIcon\n        :name=\"`${\n          message.role === 'user'\n            ? 'i-mdi-user'\n            : 'i-heroicons-sparkles-solid'\n        }`\"\n        class=\"w-8 h-8\"\n        :class=\"`${\n          message.role === 'user' ? 'text-primary-400' : 'text-blue-400'\n        }`\"\n      \u002F>\n    \u003C\u002Fdiv>\n    \u003Cdiv v-if=\"message.role === 'user'\">\n      {{ message.content }}\n    \u003C\u002Fdiv>\n    \u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n  \u003C\u002Fdiv>\n  \u003CChatLoadingSkeleton v-if=\"loading === 'message'\" \u002F>\n  \u003CNoChats v-if=\"chatHistory.length === 0\" class=\"h-full\" \u002F>\n\u003C\u002Fdiv>\n",[183,1334,1335,1340,1345,1350,1355,1360,1365,1370,1375,1380,1385,1390,1395,1400,1405,1410,1415,1420,1425,1430,1435,1440,1444,1448,1452,1457,1462,1466,1471,1475,1480,1485],{"__ignoreMap":181},[186,1336,1337],{"class":188,"line":189},[186,1338,1339],{},"\u003Cdiv ref=\"chatContainer\" class=\"flex-1 overflow-y-auto p-4 space-y-5\">\n",[186,1341,1342],{"class":188,"line":196},[186,1343,1344],{},"  \u003Cdiv\n",[186,1346,1347],{"class":188,"line":213},[186,1348,1349],{},"    v-for=\"(message, index) in chatHistory\"\n",[186,1351,1352],{"class":188,"line":220},[186,1353,1354],{},"    :key=\"`message-${index}`\"\n",[186,1356,1357],{"class":188,"line":226},[186,1358,1359],{},"    class=\"flex items-start gap-x-4\"\n",[186,1361,1362],{"class":188,"line":344},[186,1363,1364],{},"  >\n",[186,1366,1367],{"class":188,"line":350},[186,1368,1369],{},"    \u003Cdiv\n",[186,1371,1372],{"class":188,"line":356},[186,1373,1374],{},"      class=\"w-12 h-12 p-2 rounded-full\"\n",[186,1376,1377],{"class":188,"line":362},[186,1378,1379],{},"      :class=\"`${\n",[186,1381,1382],{"class":188,"line":368},[186,1383,1384],{},"        message.role === 'user' ? 'bg-primary\u002F20' : 'bg-blue-500\u002F20'\n",[186,1386,1387],{"class":188,"line":374},[186,1388,1389],{},"      }`\"\n",[186,1391,1392],{"class":188,"line":380},[186,1393,1394],{},"    >\n",[186,1396,1397],{"class":188,"line":386},[186,1398,1399],{},"      \u003CUIcon\n",[186,1401,1402],{"class":188,"line":392},[186,1403,1404],{},"        :name=\"`${\n",[186,1406,1407],{"class":188,"line":398},[186,1408,1409],{},"          message.role === 'user'\n",[186,1411,1412],{"class":188,"line":404},[186,1413,1414],{},"            ? 'i-mdi-user'\n",[186,1416,1417],{"class":188,"line":409},[186,1418,1419],{},"            : 'i-heroicons-sparkles-solid'\n",[186,1421,1422],{"class":188,"line":415},[186,1423,1424],{},"        }`\"\n",[186,1426,1427],{"class":188,"line":421},[186,1428,1429],{},"        class=\"w-8 h-8\"\n",[186,1431,1432],{"class":188,"line":426},[186,1433,1434],{},"        :class=\"`${\n",[186,1436,1437],{"class":188,"line":432},[186,1438,1439],{},"          message.role === 'user' ? 'text-primary-400' : 'text-blue-400'\n",[186,1441,1442],{"class":188,"line":438},[186,1443,1424],{},[186,1445,1446],{"class":188,"line":444},[186,1447,377],{},[186,1449,1450],{"class":188,"line":450},[186,1451,892],{},[186,1453,1454],{"class":188,"line":456},[186,1455,1456],{},"    \u003Cdiv v-if=\"message.role === 'user'\">\n",[186,1458,1459],{"class":188,"line":462},[186,1460,1461],{},"      {{ message.content }}\n",[186,1463,1464],{"class":188,"line":468},[186,1465,892],{},[186,1467,1468],{"class":188,"line":474},[186,1469,1470],{},"    \u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n",[186,1472,1473],{"class":188,"line":479},[186,1474,898],{},[186,1476,1477],{"class":188,"line":485},[186,1478,1479],{},"  \u003CChatLoadingSkeleton v-if=\"loading === 'message'\" \u002F>\n",[186,1481,1482],{"class":188,"line":490},[186,1483,1484],{},"  \u003CNoChats v-if=\"chatHistory.length === 0\" class=\"h-full\" \u002F>\n",[186,1486,1487],{"class":188,"line":495},[186,1488,1489],{},"\u003C\u002Fdiv>\n",[10,1491,1492,1493,1496,1497,1500,1501,1504],{},"To keep the user informed of the progress, we show a message loading skeleton when streaming is off. To do this we utilize a loading variable which can have three states: ",[183,1494,1495],{},"idle",", ",[183,1498,1499],{},"message"," & ",[183,1502,1503],{},"stream",". So when a non-streaming request is made we set the ref to message and the loading skeleton is shown.",[10,1506,1507],{},[143,1508,1509],{},"User Message Textbox",[10,1511,1512],{},"For entering user message. It shows up as a single line textarea that resizes automatically when needed.",[176,1514,1516],{"className":312,"code":1515,"language":314,"meta":181,"style":181},"\u003Cdiv class=\"flex items-start p-3.5 relative\">\n  \u003CUTextarea\n    v-model=\"userMessage\"\n    placeholder=\"How can I help you today?\"\n    class=\"w-full\"\n    :ui=\"{ padding: { xl: 'pr-11' } }\"\n    :rows=\"1\"\n    :maxrows=\"5\"\n    :disabled=\"loading !== 'idle'\"\n    autoresize\n    size=\"xl\"\n    @keydown.enter.exact.prevent=\"sendMessage\"\n    @keydown.enter.shift.exact.prevent=\"userMessage += '\\n'\"\n  \u002F>\n\n  \u003CUButton\n    icon=\"i-heroicons-arrow-up-20-solid\"\n    class=\"absolute top-5 right-5\"\n    :disabled=\"loading !== 'idle'\"\n    @click=\"sendMessage\"\n  \u002F>\n\u003C\u002Fdiv>\n",[183,1517,1518,1523,1528,1533,1538,1543,1548,1553,1558,1563,1568,1573,1578,1583,1588,1592,1597,1602,1607,1611,1616,1620],{"__ignoreMap":181},[186,1519,1520],{"class":188,"line":189},[186,1521,1522],{},"\u003Cdiv class=\"flex items-start p-3.5 relative\">\n",[186,1524,1525],{"class":188,"line":196},[186,1526,1527],{},"  \u003CUTextarea\n",[186,1529,1530],{"class":188,"line":213},[186,1531,1532],{},"    v-model=\"userMessage\"\n",[186,1534,1535],{"class":188,"line":220},[186,1536,1537],{},"    placeholder=\"How can I help you today?\"\n",[186,1539,1540],{"class":188,"line":226},[186,1541,1542],{},"    class=\"w-full\"\n",[186,1544,1545],{"class":188,"line":344},[186,1546,1547],{},"    :ui=\"{ padding: { xl: 'pr-11' } }\"\n",[186,1549,1550],{"class":188,"line":350},[186,1551,1552],{},"    :rows=\"1\"\n",[186,1554,1555],{"class":188,"line":356},[186,1556,1557],{},"    :maxrows=\"5\"\n",[186,1559,1560],{"class":188,"line":362},[186,1561,1562],{},"    :disabled=\"loading !== 'idle'\"\n",[186,1564,1565],{"class":188,"line":368},[186,1566,1567],{},"    autoresize\n",[186,1569,1570],{"class":188,"line":374},[186,1571,1572],{},"    size=\"xl\"\n",[186,1574,1575],{"class":188,"line":380},[186,1576,1577],{},"    @keydown.enter.exact.prevent=\"sendMessage\"\n",[186,1579,1580],{"class":188,"line":386},[186,1581,1582],{},"    @keydown.enter.shift.exact.prevent=\"userMessage += '\\n'\"\n",[186,1584,1585],{"class":188,"line":392},[186,1586,1587],{},"  \u002F>\n",[186,1589,1590],{"class":188,"line":398},[186,1591,217],{"emptyLinePlaceholder":216},[186,1593,1594],{"class":188,"line":404},[186,1595,1596],{},"  \u003CUButton\n",[186,1598,1599],{"class":188,"line":409},[186,1600,1601],{},"    icon=\"i-heroicons-arrow-up-20-solid\"\n",[186,1603,1604],{"class":188,"line":415},[186,1605,1606],{},"    class=\"absolute top-5 right-5\"\n",[186,1608,1609],{"class":188,"line":421},[186,1610,1562],{},[186,1612,1613],{"class":188,"line":426},[186,1614,1615],{},"    @click=\"sendMessage\"\n",[186,1617,1618],{"class":188,"line":432},[186,1619,1587],{},[186,1621,1622],{"class":188,"line":438},[186,1623,1489],{},[10,1625,1626,1627,1630,1631,1634],{},"For allowing the user to send message on hitting enter, we use the keydown event listener with the needed modifiers (",[183,1628,1629],{},"@keydown.enter.exact.prevent","). Similarly, for adding a newline we use ",[183,1632,1633],{},"enter + shift"," keys.",[10,1636,1637],{},"Now that we have our UI components in place, let's shift our focus to the backend. We'll set up the AI integration and create an API endpoint to handle our chat requests. This will bridge the gap between our frontend interface and the AI model.",[25,1639,1641],{"id":1640},"setting-up-ai-and-api-endpoint","Setting up AI and API Endpoint",[10,1643,1644,1645,1648],{},"Integrating AI into our project is very simple thanks to NuxtHub. Let's look at our ",[183,1646,1647],{},"nuxt.config.ts"," file in the root dir of the project.",[176,1650,1654],{"className":1651,"code":1652,"language":1653,"meta":181,"style":181},"language-typescript shiki shiki-themes github-light github-dark","\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config\nexport default defineNuxtConfig({\n  compatibilityDate: '2024-07-30',\n  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fupgrade#testing-nuxt-4\n  future: { compatibilityVersion: 4 },\n\n  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fmodules\n  modules: ['@nuxthub\u002Fcore', '@nuxt\u002Feslint', '@nuxtjs\u002Fmdc', \"@nuxt\u002Fui\"],\n\n  \u002F\u002F https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Finstallation#options\n  hub: {},\n\n  \u002F\u002F Env variables - https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fconfiguration#environment-variables-and-private-tokens\n  runtimeConfig: {\n    public: {\n      \u002F\u002F Can be overridden by NUXT_PUBLIC_HELLO_TEXT environment variable\n      helloText: 'Hello from the Edge 👋',\n    },\n  },\n\n  \u002F\u002F https:\u002F\u002Feslint.nuxt.com\n  eslint: {\n    config: {\n      stylistic: {\n        quotes: 'single',\n      },\n    },\n  },\n\n  \u002F\u002F https:\u002F\u002Fdevtools.nuxt.com\n  devtools: { enabled: true },\n});\n","typescript",[183,1655,1656,1661,1677,1688,1693,1704,1708,1713,1739,1743,1748,1753,1757,1762,1767,1772,1777,1787,1792,1796,1800,1805,1810,1815,1820,1830,1835,1839,1843,1847,1852,1862],{"__ignoreMap":181},[186,1657,1658],{"class":188,"line":189},[186,1659,1660],{"class":192},"\u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fconfiguration\u002Fnuxt-config\n",[186,1662,1663,1667,1670,1673],{"class":188,"line":196},[186,1664,1666],{"class":1665},"szBVR","export",[186,1668,1669],{"class":1665}," default",[186,1671,1672],{"class":199}," defineNuxtConfig",[186,1674,1676],{"class":1675},"sVt8B","({\n",[186,1678,1679,1682,1685],{"class":188,"line":213},[186,1680,1681],{"class":1675},"  compatibilityDate: ",[186,1683,1684],{"class":203},"'2024-07-30'",[186,1686,1687],{"class":1675},",\n",[186,1689,1690],{"class":188,"line":220},[186,1691,1692],{"class":192},"  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fupgrade#testing-nuxt-4\n",[186,1694,1695,1698,1701],{"class":188,"line":226},[186,1696,1697],{"class":1675},"  future: { compatibilityVersion: ",[186,1699,1700],{"class":229},"4",[186,1702,1703],{"class":1675}," },\n",[186,1705,1706],{"class":188,"line":344},[186,1707,217],{"emptyLinePlaceholder":216},[186,1709,1710],{"class":188,"line":350},[186,1711,1712],{"class":192},"  \u002F\u002F https:\u002F\u002Fnuxt.com\u002Fmodules\n",[186,1714,1715,1718,1721,1723,1726,1728,1731,1733,1736],{"class":188,"line":356},[186,1716,1717],{"class":1675},"  modules: [",[186,1719,1720],{"class":203},"'@nuxthub\u002Fcore'",[186,1722,1496],{"class":1675},[186,1724,1725],{"class":203},"'@nuxt\u002Feslint'",[186,1727,1496],{"class":1675},[186,1729,1730],{"class":203},"'@nuxtjs\u002Fmdc'",[186,1732,1496],{"class":1675},[186,1734,1735],{"class":203},"\"@nuxt\u002Fui\"",[186,1737,1738],{"class":1675},"],\n",[186,1740,1741],{"class":188,"line":362},[186,1742,217],{"emptyLinePlaceholder":216},[186,1744,1745],{"class":188,"line":368},[186,1746,1747],{"class":192},"  \u002F\u002F https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Finstallation#options\n",[186,1749,1750],{"class":188,"line":374},[186,1751,1752],{"class":1675},"  hub: {},\n",[186,1754,1755],{"class":188,"line":380},[186,1756,217],{"emptyLinePlaceholder":216},[186,1758,1759],{"class":188,"line":386},[186,1760,1761],{"class":192},"  \u002F\u002F Env variables - https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fconfiguration#environment-variables-and-private-tokens\n",[186,1763,1764],{"class":188,"line":392},[186,1765,1766],{"class":1675},"  runtimeConfig: {\n",[186,1768,1769],{"class":188,"line":398},[186,1770,1771],{"class":1675},"    public: {\n",[186,1773,1774],{"class":188,"line":404},[186,1775,1776],{"class":192},"      \u002F\u002F Can be overridden by NUXT_PUBLIC_HELLO_TEXT environment variable\n",[186,1778,1779,1782,1785],{"class":188,"line":409},[186,1780,1781],{"class":1675},"      helloText: ",[186,1783,1784],{"class":203},"'Hello from the Edge 👋'",[186,1786,1687],{"class":1675},[186,1788,1789],{"class":188,"line":415},[186,1790,1791],{"class":1675},"    },\n",[186,1793,1794],{"class":188,"line":421},[186,1795,453],{"class":1675},[186,1797,1798],{"class":188,"line":426},[186,1799,217],{"emptyLinePlaceholder":216},[186,1801,1802],{"class":188,"line":432},[186,1803,1804],{"class":192},"  \u002F\u002F https:\u002F\u002Feslint.nuxt.com\n",[186,1806,1807],{"class":188,"line":438},[186,1808,1809],{"class":1675},"  eslint: {\n",[186,1811,1812],{"class":188,"line":444},[186,1813,1814],{"class":1675},"    config: {\n",[186,1816,1817],{"class":188,"line":450},[186,1818,1819],{"class":1675},"      stylistic: {\n",[186,1821,1822,1825,1828],{"class":188,"line":456},[186,1823,1824],{"class":1675},"        quotes: ",[186,1826,1827],{"class":203},"'single'",[186,1829,1687],{"class":1675},[186,1831,1832],{"class":188,"line":462},[186,1833,1834],{"class":1675},"      },\n",[186,1836,1837],{"class":188,"line":468},[186,1838,1791],{"class":1675},[186,1840,1841],{"class":188,"line":474},[186,1842,453],{"class":1675},[186,1844,1845],{"class":188,"line":479},[186,1846,217],{"emptyLinePlaceholder":216},[186,1848,1849],{"class":188,"line":485},[186,1850,1851],{"class":192},"  \u002F\u002F https:\u002F\u002Fdevtools.nuxt.com\n",[186,1853,1854,1857,1860],{"class":188,"line":490},[186,1855,1856],{"class":1675},"  devtools: { enabled: ",[186,1858,1859],{"class":229},"true",[186,1861,1703],{"class":1675},[186,1863,1864],{"class":188,"line":495},[186,1865,524],{"class":1675},[10,1867,1868,1869,1872,1873,1876,1877,1880,1881,1876,1884,1887,1888,1891],{},"To enable AI, we just need to add the ",[183,1870,1871],{},"ai: true"," flag under hub config options above. If needed, we can enable other NuxtHub Cloudflare integrations like ",[183,1874,1875],{},"D1 database"," (",[183,1878,1879],{},"database: true","), ",[183,1882,1883],{},"Workers KV",[183,1885,1886],{},"kv: true",") etc. While we are here, we can remove runtimeConfig as we don't need it. (You can also remove\u002Fmodify the ",[183,1889,1890],{},"eslint"," config as per your code editor settings).",[10,1893,1894],{},"If we want to use AI in dev mode (which we definitely do), we need to link this project to a Cloudflare project. This is needed as we will be talking directly to the Cloudflare APIs as no workers are running yet (and also because the AI usage will be linked to your Cloudflare account). This is a straight forward task, just run the following command",[176,1896,1898],{"className":178,"code":1897,"language":180,"meta":181,"style":181},"npx nuxthub link\n",[183,1899,1900],{"__ignoreMap":181},[186,1901,1902,1904,1906],{"class":188,"line":189},[186,1903,200],{"class":199},[186,1905,204],{"class":203},[186,1907,1908],{"class":203}," link\n",[10,1910,1911],{},"Running the link command will make sure that we are logged into to our NuxtHub account, and will allow us to create a new NuxtHub project (backed by Cloudflare) or choose an existing one.",[94,1913,1915],{"id":1914},"creating-chat-api-endpoint","Creating Chat API Endpoint",[10,1917,1918,1919,1876,1922,1925,1926,1929,1930,1933],{},"Let's create a chat api endpoint. Create a new file ",[183,1920,1921],{},"chat.post.ts",[183,1923,1924],{},"\"post\""," in the file name signifies that this endpoint will only accept ",[183,1927,1928],{},"HTTP POST"," requests) in the ",[183,1931,1932],{},"server\u002Fapi"," directory and add the following code to it",[176,1935,1937],{"className":1651,"code":1936,"language":1653,"meta":181,"style":181},"export default defineEventHandler(async (event) => {\n  const { messages, params } = await readBody(event);\n  if (!messages || messages.length === 0 || !params) {\n    throw createError({\n      statusCode: 400,\n      statusMessage: 'Missing messages or LLM params',\n    });\n  }\n\n  const config = {\n    max_tokens: params.maxTokens,\n    temperature: params.temperature,\n    top_p: params.topP,\n    top_k: params.topK,\n    frequency_penalty: params.frequencyPenalty,\n    presence_penalty: params.presencePenalty,\n    stream: params.stream,\n  };\n\n  const ai = hubAI();\n\n  try {\n    const result = await ai.run(params.model, {\n      messages: params.systemPrompt\n        ? [{ role: 'system', content: params.systemPrompt }, ...messages]\n        : messages,\n      ...config,\n    });\n\n    return params.stream\n      ? sendStream(event, result as ReadableStream)\n      : (\n          result as {\n            response: string;\n          }\n        ).response;\n  } catch (error) {\n    console.error(error);\n    throw createError({\n      statusCode: 500,\n      statusMessage: 'Error processing request',\n    });\n  }\n});\n",[183,1938,1939,1969,2000,2037,2047,2057,2067,2072,2077,2081,2093,2098,2103,2108,2113,2118,2123,2128,2133,2137,2152,2156,2163,2184,2189,2209,2217,2225,2229,2233,2241,2261,2269,2278,2292,2297,2302,2313,2324,2332,2341,2350,2354,2358],{"__ignoreMap":181},[186,1940,1941,1943,1945,1948,1951,1954,1956,1960,1963,1966],{"class":188,"line":189},[186,1942,1666],{"class":1665},[186,1944,1669],{"class":1665},[186,1946,1947],{"class":199}," defineEventHandler",[186,1949,1950],{"class":1675},"(",[186,1952,1953],{"class":1665},"async",[186,1955,1876],{"class":1675},[186,1957,1959],{"class":1958},"s4XuR","event",[186,1961,1962],{"class":1675},") ",[186,1964,1965],{"class":1665},"=>",[186,1967,1968],{"class":1675}," {\n",[186,1970,1971,1974,1977,1980,1982,1985,1988,1991,1994,1997],{"class":188,"line":196},[186,1972,1973],{"class":1665},"  const",[186,1975,1976],{"class":1675}," { ",[186,1978,1979],{"class":229},"messages",[186,1981,1496],{"class":1675},[186,1983,1984],{"class":229},"params",[186,1986,1987],{"class":1675}," } ",[186,1989,1990],{"class":1665},"=",[186,1992,1993],{"class":1665}," await",[186,1995,1996],{"class":199}," readBody",[186,1998,1999],{"class":1675},"(event);\n",[186,2001,2002,2005,2007,2010,2013,2016,2019,2022,2025,2028,2031,2034],{"class":188,"line":213},[186,2003,2004],{"class":1665},"  if",[186,2006,1876],{"class":1675},[186,2008,2009],{"class":1665},"!",[186,2011,2012],{"class":1675},"messages ",[186,2014,2015],{"class":1665},"||",[186,2017,2018],{"class":1675}," messages.",[186,2020,2021],{"class":229},"length",[186,2023,2024],{"class":1665}," ===",[186,2026,2027],{"class":229}," 0",[186,2029,2030],{"class":1665}," ||",[186,2032,2033],{"class":1665}," !",[186,2035,2036],{"class":1675},"params) {\n",[186,2038,2039,2042,2045],{"class":188,"line":220},[186,2040,2041],{"class":1665},"    throw",[186,2043,2044],{"class":199}," createError",[186,2046,1676],{"class":1675},[186,2048,2049,2052,2055],{"class":188,"line":226},[186,2050,2051],{"class":1675},"      statusCode: ",[186,2053,2054],{"class":229},"400",[186,2056,1687],{"class":1675},[186,2058,2059,2062,2065],{"class":188,"line":344},[186,2060,2061],{"class":1675},"      statusMessage: ",[186,2063,2064],{"class":203},"'Missing messages or LLM params'",[186,2066,1687],{"class":1675},[186,2068,2069],{"class":188,"line":350},[186,2070,2071],{"class":1675},"    });\n",[186,2073,2074],{"class":188,"line":356},[186,2075,2076],{"class":1675},"  }\n",[186,2078,2079],{"class":188,"line":362},[186,2080,217],{"emptyLinePlaceholder":216},[186,2082,2083,2085,2088,2091],{"class":188,"line":368},[186,2084,1973],{"class":1665},[186,2086,2087],{"class":229}," config",[186,2089,2090],{"class":1665}," =",[186,2092,1968],{"class":1675},[186,2094,2095],{"class":188,"line":374},[186,2096,2097],{"class":1675},"    max_tokens: params.maxTokens,\n",[186,2099,2100],{"class":188,"line":380},[186,2101,2102],{"class":1675},"    temperature: params.temperature,\n",[186,2104,2105],{"class":188,"line":386},[186,2106,2107],{"class":1675},"    top_p: params.topP,\n",[186,2109,2110],{"class":188,"line":392},[186,2111,2112],{"class":1675},"    top_k: params.topK,\n",[186,2114,2115],{"class":188,"line":398},[186,2116,2117],{"class":1675},"    frequency_penalty: params.frequencyPenalty,\n",[186,2119,2120],{"class":188,"line":404},[186,2121,2122],{"class":1675},"    presence_penalty: params.presencePenalty,\n",[186,2124,2125],{"class":188,"line":409},[186,2126,2127],{"class":1675},"    stream: params.stream,\n",[186,2129,2130],{"class":188,"line":415},[186,2131,2132],{"class":1675},"  };\n",[186,2134,2135],{"class":188,"line":421},[186,2136,217],{"emptyLinePlaceholder":216},[186,2138,2139,2141,2144,2146,2149],{"class":188,"line":426},[186,2140,1973],{"class":1665},[186,2142,2143],{"class":229}," ai",[186,2145,2090],{"class":1665},[186,2147,2148],{"class":199}," hubAI",[186,2150,2151],{"class":1675},"();\n",[186,2153,2154],{"class":188,"line":432},[186,2155,217],{"emptyLinePlaceholder":216},[186,2157,2158,2161],{"class":188,"line":438},[186,2159,2160],{"class":1665},"  try",[186,2162,1968],{"class":1675},[186,2164,2165,2168,2171,2173,2175,2178,2181],{"class":188,"line":444},[186,2166,2167],{"class":1665},"    const",[186,2169,2170],{"class":229}," result",[186,2172,2090],{"class":1665},[186,2174,1993],{"class":1665},[186,2176,2177],{"class":1675}," ai.",[186,2179,2180],{"class":199},"run",[186,2182,2183],{"class":1675},"(params.model, {\n",[186,2185,2186],{"class":188,"line":450},[186,2187,2188],{"class":1675},"      messages: params.systemPrompt\n",[186,2190,2191,2194,2197,2200,2203,2206],{"class":188,"line":456},[186,2192,2193],{"class":1665},"        ?",[186,2195,2196],{"class":1675}," [{ role: ",[186,2198,2199],{"class":203},"'system'",[186,2201,2202],{"class":1675},", content: params.systemPrompt }, ",[186,2204,2205],{"class":1665},"...",[186,2207,2208],{"class":1675},"messages]\n",[186,2210,2211,2214],{"class":188,"line":462},[186,2212,2213],{"class":1665},"        :",[186,2215,2216],{"class":1675}," messages,\n",[186,2218,2219,2222],{"class":188,"line":468},[186,2220,2221],{"class":1665},"      ...",[186,2223,2224],{"class":1675},"config,\n",[186,2226,2227],{"class":188,"line":474},[186,2228,2071],{"class":1675},[186,2230,2231],{"class":188,"line":479},[186,2232,217],{"emptyLinePlaceholder":216},[186,2234,2235,2238],{"class":188,"line":485},[186,2236,2237],{"class":1665},"    return",[186,2239,2240],{"class":1675}," params.stream\n",[186,2242,2243,2246,2249,2252,2255,2258],{"class":188,"line":490},[186,2244,2245],{"class":1665},"      ?",[186,2247,2248],{"class":199}," sendStream",[186,2250,2251],{"class":1675},"(event, result ",[186,2253,2254],{"class":1665},"as",[186,2256,2257],{"class":199}," ReadableStream",[186,2259,2260],{"class":1675},")\n",[186,2262,2263,2266],{"class":188,"line":495},[186,2264,2265],{"class":1665},"      :",[186,2267,2268],{"class":1675}," (\n",[186,2270,2271,2274,2276],{"class":188,"line":500},[186,2272,2273],{"class":1675},"          result ",[186,2275,2254],{"class":1665},[186,2277,1968],{"class":1675},[186,2279,2280,2283,2286,2289],{"class":188,"line":506},[186,2281,2282],{"class":1958},"            response",[186,2284,2285],{"class":1665},":",[186,2287,2288],{"class":229}," string",[186,2290,2291],{"class":1675},";\n",[186,2293,2294],{"class":188,"line":511},[186,2295,2296],{"class":1675},"          }\n",[186,2298,2299],{"class":188,"line":516},[186,2300,2301],{"class":1675},"        ).response;\n",[186,2303,2304,2307,2310],{"class":188,"line":521},[186,2305,2306],{"class":1675},"  } ",[186,2308,2309],{"class":1665},"catch",[186,2311,2312],{"class":1675}," (error) {\n",[186,2314,2315,2318,2321],{"class":188,"line":527},[186,2316,2317],{"class":1675},"    console.",[186,2319,2320],{"class":199},"error",[186,2322,2323],{"class":1675},"(error);\n",[186,2325,2326,2328,2330],{"class":188,"line":736},[186,2327,2041],{"class":1665},[186,2329,2044],{"class":199},[186,2331,1676],{"class":1675},[186,2333,2334,2336,2339],{"class":188,"line":741},[186,2335,2051],{"class":1675},[186,2337,2338],{"class":229},"500",[186,2340,1687],{"class":1675},[186,2342,2343,2345,2348],{"class":188,"line":747},[186,2344,2061],{"class":1675},[186,2346,2347],{"class":203},"'Error processing request'",[186,2349,1687],{"class":1675},[186,2351,2352],{"class":188,"line":753},[186,2353,2071],{"class":1675},[186,2355,2356],{"class":188,"line":759},[186,2357,2076],{"class":1675},[186,2359,2360],{"class":188,"line":765},[186,2361,524],{"class":1675},[10,2363,2364,2367],{},[183,2365,2366],{},"hubAI"," is a server composable that returns a Workers AI client for interacting with the LLMs. We pass the messages as well as the LLM params that we received from the frontend and run the requested model.",[10,2369,2370,2371,2374],{},"If you look at the above code carefully you'll notice that this endpoint supports both: streaming and non-streaming responses. To return a stream from the endpoint we just need to call the ",[183,2372,2373],{},"sendStream"," utility function.",[10,2376,2377],{},"We are through with our server side code. Let's look at how to handle the stream response on the client side in the next section.",[25,2379,2381],{"id":2380},"consuming-server-sent-events","Consuming Server Sent Events",[10,2383,2384],{},"Before diving into how to handle stream responses, let's understand why streaming is crucial for LLM interactions and how Server Sent Events (SSE) facilitate this process.",[94,2386,2388],{"id":2387},"why-stream-llm-responses-with-server-sent-events","Why Stream LLM Responses with Server Sent Events?",[10,2390,2391],{},"Large Language Models (LLMs) can take considerable time to generate complete responses, especially for complex queries. Traditionally, web applications wait for the entire response before displaying anything to the user. However, this approach can lead to long waiting times and a less engaging user experience.",[10,2393,2394,2399],{},[14,2395,2398],{"href":2396,"rel":2397},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FServer-sent_events",[18],"Server Sent Events (SSE)"," offer a solution to this problem.",[2401,2402,2404,2408],"div",{"dataNodeType":2403},"callout",[2401,2405,2407],{"dataNodeType":2406},"callout-emoji","💡",[2401,2409,2411],{"dataNodeType":2410},"callout-text","SSE is a technology that allows a server to push data to a web page at any time, enabling real-time updates without the need for the client to constantly request new information.",[10,2413,2414],{},"Here's how SSE benefits LLM responses:",[99,2416,2417,2420,2423],{},[48,2418,2419],{},"Immediate Feedback: As soon as the LLM starts generating content, it can be sent to the client and displayed, providing immediate feedback to the user.",[48,2421,2422],{},"Improved Perceived Performance: Users see content appearing progressively, giving the impression of a faster, more responsive system.",[48,2424,2425],{},"Real-time Interaction: The gradual appearance of text mimics human typing, creating a more natural and engaging conversational experience.",[2401,2427,2428,2430],{"dataNodeType":2403},[2401,2429,2407],{"dataNodeType":2406},[2401,2431,2432],{"dataNodeType":2410},"Think of it like ordering at a restaurant: Instead of waiting for all dishes to be prepared before serving, SSE allows the \"kitchen\" (server) to send out each \"dish\" (piece of the response) as soon as it's ready. This way, you start \"eating\" (reading and processing) sooner, and the overall experience feels faster and more satisfying.",[10,2434,2435],{},"Technically, SSE works by establishing a unidirectional channel from the server to the client. The server sends text data encoded in UTF-8, separated by newline characters. This simple yet effective mechanism allows for real-time updates in web applications, making it ideal for streaming LLM responses.",[10,2437,2438],{},"Now that we understand the importance of streaming for LLM responses and how SSE enables this, let's look at how to implement this in our Nuxt 3 application.",[94,2440,2442],{"id":2441},"handling-server-sent-events-with-nuxt-3-post-requests","Handling Server Sent Events with Nuxt 3 POST Requests",[10,2444,2445,2446,2451],{},"Since ours is a POST request, we need handle it differently. ",[14,2447,2450],{"href":2448,"rel":2449},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fgetting-started\u002Fdata-fetching#consuming-sse-server-sent-events-via-post-request",[18],"Nuxt 3 docs"," gives you an excellent starting point to do this. Reproducing the code from the docs",[176,2453,2455],{"className":1651,"code":2454,"language":1653,"meta":181,"style":181},"\u002F\u002F Make a POST request to the SSE endpoint\nconst response = await $fetch\u003CReadableStream>('\u002Fchats\u002Fask-ai', {\n  method: 'POST',\n  body: {\n    query: \"Hello AI, how are you?\",\n  },\n  responseType: 'stream',\n})\n\n\u002F\u002F Create a new ReadableStream from the response with TextDecoderStream to get the data as text\nconst reader = response.pipeThrough(new TextDecoderStream()).getReader()\n\n\u002F\u002F Read the chunk of data as we get it\nwhile (true) {\n  const { value, done } = await reader.read()\n\n  if (done)\n    break\n\n  console.log('Received:', value)\n}\n",[183,2456,2457,2462,2492,2502,2507,2517,2521,2531,2536,2540,2545,2577,2581,2586,2598,2626,2630,2637,2642,2646,2662],{"__ignoreMap":181},[186,2458,2459],{"class":188,"line":189},[186,2460,2461],{"class":192},"\u002F\u002F Make a POST request to the SSE endpoint\n",[186,2463,2464,2467,2470,2472,2474,2477,2480,2483,2486,2489],{"class":188,"line":196},[186,2465,2466],{"class":1665},"const",[186,2468,2469],{"class":229}," response",[186,2471,2090],{"class":1665},[186,2473,1993],{"class":1665},[186,2475,2476],{"class":199}," $fetch",[186,2478,2479],{"class":1675},"\u003C",[186,2481,2482],{"class":199},"ReadableStream",[186,2484,2485],{"class":1675},">(",[186,2487,2488],{"class":203},"'\u002Fchats\u002Fask-ai'",[186,2490,2491],{"class":1675},", {\n",[186,2493,2494,2497,2500],{"class":188,"line":213},[186,2495,2496],{"class":1675},"  method: ",[186,2498,2499],{"class":203},"'POST'",[186,2501,1687],{"class":1675},[186,2503,2504],{"class":188,"line":220},[186,2505,2506],{"class":1675},"  body: {\n",[186,2508,2509,2512,2515],{"class":188,"line":226},[186,2510,2511],{"class":1675},"    query: ",[186,2513,2514],{"class":203},"\"Hello AI, how are you?\"",[186,2516,1687],{"class":1675},[186,2518,2519],{"class":188,"line":344},[186,2520,453],{"class":1675},[186,2522,2523,2526,2529],{"class":188,"line":350},[186,2524,2525],{"class":1675},"  responseType: ",[186,2527,2528],{"class":203},"'stream'",[186,2530,1687],{"class":1675},[186,2532,2533],{"class":188,"line":356},[186,2534,2535],{"class":1675},"})\n",[186,2537,2538],{"class":188,"line":362},[186,2539,217],{"emptyLinePlaceholder":216},[186,2541,2542],{"class":188,"line":368},[186,2543,2544],{"class":192},"\u002F\u002F Create a new ReadableStream from the response with TextDecoderStream to get the data as text\n",[186,2546,2547,2549,2552,2554,2557,2560,2562,2565,2568,2571,2574],{"class":188,"line":374},[186,2548,2466],{"class":1665},[186,2550,2551],{"class":229}," reader",[186,2553,2090],{"class":1665},[186,2555,2556],{"class":1675}," response.",[186,2558,2559],{"class":199},"pipeThrough",[186,2561,1950],{"class":1675},[186,2563,2564],{"class":1665},"new",[186,2566,2567],{"class":199}," TextDecoderStream",[186,2569,2570],{"class":1675},"()).",[186,2572,2573],{"class":199},"getReader",[186,2575,2576],{"class":1675},"()\n",[186,2578,2579],{"class":188,"line":380},[186,2580,217],{"emptyLinePlaceholder":216},[186,2582,2583],{"class":188,"line":386},[186,2584,2585],{"class":192},"\u002F\u002F Read the chunk of data as we get it\n",[186,2587,2588,2591,2593,2595],{"class":188,"line":392},[186,2589,2590],{"class":1665},"while",[186,2592,1876],{"class":1675},[186,2594,1859],{"class":229},[186,2596,2597],{"class":1675},") {\n",[186,2599,2600,2602,2604,2607,2609,2612,2614,2616,2618,2621,2624],{"class":188,"line":398},[186,2601,1973],{"class":1665},[186,2603,1976],{"class":1675},[186,2605,2606],{"class":229},"value",[186,2608,1496],{"class":1675},[186,2610,2611],{"class":229},"done",[186,2613,1987],{"class":1675},[186,2615,1990],{"class":1665},[186,2617,1993],{"class":1665},[186,2619,2620],{"class":1675}," reader.",[186,2622,2623],{"class":199},"read",[186,2625,2576],{"class":1675},[186,2627,2628],{"class":188,"line":404},[186,2629,217],{"emptyLinePlaceholder":216},[186,2631,2632,2634],{"class":188,"line":409},[186,2633,2004],{"class":1665},[186,2635,2636],{"class":1675}," (done)\n",[186,2638,2639],{"class":188,"line":415},[186,2640,2641],{"class":1665},"    break\n",[186,2643,2644],{"class":188,"line":421},[186,2645,217],{"emptyLinePlaceholder":216},[186,2647,2648,2651,2654,2656,2659],{"class":188,"line":426},[186,2649,2650],{"class":1675},"  console.",[186,2652,2653],{"class":199},"log",[186,2655,1950],{"class":1675},[186,2657,2658],{"class":203},"'Received:'",[186,2660,2661],{"class":1675},", value)\n",[186,2663,2664],{"class":188,"line":432},[186,2665,2666],{"class":1675},"}\n",[10,2668,2669,2670,2673,2674,2676,2677,2680,2681,2683,2684,152],{},"We need to set the request ",[183,2671,2672],{},"responseType"," flag as ",[183,2675,1503],{},", and set the type of ",[183,2678,2679],{},"$fetch"," response as ",[183,2682,2482],{},". Then we create a stream reader while decoding the received chunks by piping the events through a ",[183,2685,2686],{},"TextDecoder",[10,2688,2689],{},"Below is a glimpse of the events data received from our chat endpoint (sent by the LLM)",[176,2691,2695],{"className":2692,"code":2693,"language":2694,"meta":181,"style":181},"language-plaintext shiki shiki-themes github-light github-dark","data: {\"response\":\"Hello\",\"p\":\"abcdefghijklmnopqrstuvwxyz0123456789abcdefghij\"}\n\ndata: {\"response\":\"!\",\"p\":\"abcdefgh\"}\n\ndata: [DONE]\n","plaintext",[183,2696,2697,2702,2706,2711,2715],{"__ignoreMap":181},[186,2698,2699],{"class":188,"line":189},[186,2700,2701],{},"data: {\"response\":\"Hello\",\"p\":\"abcdefghijklmnopqrstuvwxyz0123456789abcdefghij\"}\n",[186,2703,2704],{"class":188,"line":196},[186,2705,217],{"emptyLinePlaceholder":216},[186,2707,2708],{"class":188,"line":213},[186,2709,2710],{},"data: {\"response\":\"!\",\"p\":\"abcdefgh\"}\n",[186,2712,2713],{"class":188,"line":220},[186,2714,217],{"emptyLinePlaceholder":216},[186,2716,2717],{"class":188,"line":226},[186,2718,2719],{},"data: [DONE]\n",[10,2721,2722,2723,2726,2727,2732],{},"These events may contain more than one data line per event, and some events may not contain a complete JSON object (rest of the object will arrive with the next event). To handle this stream we can create a composable with a ",[183,2724,2725],{},"streamResponse"," ",[14,2728,2731],{"href":2729,"rel":2730},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FStatements\u002Ffunction*",[18],"generator function"," as shown below",[176,2734,2736],{"className":1651,"code":2735,"language":1653,"meta":181,"style":181},"export function useChat() {\n  async function* streamResponse(\n    url: string,\n    messages: ChatMessage[],\n    llmParams: LlmParams\n  ) {\n    let buffer = '';\n\n    try {\n      const response = await $fetch\u003CReadableStream>(url, {\n        method: 'POST',\n        body: {\n          messages,\n          params: llmParams,\n        },\n        responseType: 'stream',\n      });\n\n      const reader = response.pipeThrough(new TextDecoderStream()).getReader();\n\n      while (true) {\n        const { value, done } = await reader.read();\n\n        if (done) {\n          if (buffer.trim()) {\n            console.warn('Stream ended with unparsed data:', buffer);\n          }\n\n          return;\n        }\n\n        buffer += value;\n        const lines = buffer.split('\\n');\n        buffer = lines.pop() || '';\n\n        for (const line of lines) {\n          if (line.startsWith('data: ')) {\n            const data = line.slice('data: '.length).trim();\n            if (data === '[DONE]') {\n              return;\n            }\n\n            try {\n              const jsonData = JSON.parse(data);\n              if (jsonData.response) {\n                yield jsonData.response;\n              }\n            } catch (parseError) {\n              console.warn('Error parsing JSON:', parseError);\n            }\n          }\n        }\n      }\n    } catch (error) {\n      console.error('Error sending message:', error);\n\n      throw error;\n    }\n  }\n\n  \u002F\u002F For handling non-streaming responses\n  async function getResponse() {}\n\n  return {\n    getResponse,\n    streamResponse,\n  };\n}\n",[183,2737,2738,2751,2765,2776,2789,2799,2804,2819,2823,2830,2850,2859,2864,2869,2874,2879,2888,2893,2897,2921,2925,2936,2961,2965,2973,2987,3003,3007,3011,3018,3023,3027,3038,3066,3087,3091,3109,3127,3158,3174,3181,3186,3190,3197,3218,3226,3234,3239,3249,3264,3268,3272,3276,3281,3290,3305,3309,3317,3322,3326,3330,3335,3347,3351,3358,3363,3368,3372],{"__ignoreMap":181},[186,2739,2740,2742,2745,2748],{"class":188,"line":189},[186,2741,1666],{"class":1665},[186,2743,2744],{"class":1665}," function",[186,2746,2747],{"class":199}," useChat",[186,2749,2750],{"class":1675},"() {\n",[186,2752,2753,2756,2759,2762],{"class":188,"line":196},[186,2754,2755],{"class":1665},"  async",[186,2757,2758],{"class":1665}," function*",[186,2760,2761],{"class":199}," streamResponse",[186,2763,2764],{"class":1675},"(\n",[186,2766,2767,2770,2772,2774],{"class":188,"line":213},[186,2768,2769],{"class":1958},"    url",[186,2771,2285],{"class":1665},[186,2773,2288],{"class":229},[186,2775,1687],{"class":1675},[186,2777,2778,2781,2783,2786],{"class":188,"line":220},[186,2779,2780],{"class":1958},"    messages",[186,2782,2285],{"class":1665},[186,2784,2785],{"class":199}," ChatMessage",[186,2787,2788],{"class":1675},"[],\n",[186,2790,2791,2794,2796],{"class":188,"line":226},[186,2792,2793],{"class":1958},"    llmParams",[186,2795,2285],{"class":1665},[186,2797,2798],{"class":199}," LlmParams\n",[186,2800,2801],{"class":188,"line":344},[186,2802,2803],{"class":1675},"  ) {\n",[186,2805,2806,2809,2812,2814,2817],{"class":188,"line":350},[186,2807,2808],{"class":1665},"    let",[186,2810,2811],{"class":1675}," buffer ",[186,2813,1990],{"class":1665},[186,2815,2816],{"class":203}," ''",[186,2818,2291],{"class":1675},[186,2820,2821],{"class":188,"line":356},[186,2822,217],{"emptyLinePlaceholder":216},[186,2824,2825,2828],{"class":188,"line":362},[186,2826,2827],{"class":1665},"    try",[186,2829,1968],{"class":1675},[186,2831,2832,2835,2837,2839,2841,2843,2845,2847],{"class":188,"line":368},[186,2833,2834],{"class":1665},"      const",[186,2836,2469],{"class":229},[186,2838,2090],{"class":1665},[186,2840,1993],{"class":1665},[186,2842,2476],{"class":199},[186,2844,2479],{"class":1675},[186,2846,2482],{"class":199},[186,2848,2849],{"class":1675},">(url, {\n",[186,2851,2852,2855,2857],{"class":188,"line":374},[186,2853,2854],{"class":1675},"        method: ",[186,2856,2499],{"class":203},[186,2858,1687],{"class":1675},[186,2860,2861],{"class":188,"line":380},[186,2862,2863],{"class":1675},"        body: {\n",[186,2865,2866],{"class":188,"line":386},[186,2867,2868],{"class":1675},"          messages,\n",[186,2870,2871],{"class":188,"line":392},[186,2872,2873],{"class":1675},"          params: llmParams,\n",[186,2875,2876],{"class":188,"line":398},[186,2877,2878],{"class":1675},"        },\n",[186,2880,2881,2884,2886],{"class":188,"line":404},[186,2882,2883],{"class":1675},"        responseType: ",[186,2885,2528],{"class":203},[186,2887,1687],{"class":1675},[186,2889,2890],{"class":188,"line":409},[186,2891,2892],{"class":1675},"      });\n",[186,2894,2895],{"class":188,"line":415},[186,2896,217],{"emptyLinePlaceholder":216},[186,2898,2899,2901,2903,2905,2907,2909,2911,2913,2915,2917,2919],{"class":188,"line":421},[186,2900,2834],{"class":1665},[186,2902,2551],{"class":229},[186,2904,2090],{"class":1665},[186,2906,2556],{"class":1675},[186,2908,2559],{"class":199},[186,2910,1950],{"class":1675},[186,2912,2564],{"class":1665},[186,2914,2567],{"class":199},[186,2916,2570],{"class":1675},[186,2918,2573],{"class":199},[186,2920,2151],{"class":1675},[186,2922,2923],{"class":188,"line":426},[186,2924,217],{"emptyLinePlaceholder":216},[186,2926,2927,2930,2932,2934],{"class":188,"line":432},[186,2928,2929],{"class":1665},"      while",[186,2931,1876],{"class":1675},[186,2933,1859],{"class":229},[186,2935,2597],{"class":1675},[186,2937,2938,2941,2943,2945,2947,2949,2951,2953,2955,2957,2959],{"class":188,"line":438},[186,2939,2940],{"class":1665},"        const",[186,2942,1976],{"class":1675},[186,2944,2606],{"class":229},[186,2946,1496],{"class":1675},[186,2948,2611],{"class":229},[186,2950,1987],{"class":1675},[186,2952,1990],{"class":1665},[186,2954,1993],{"class":1665},[186,2956,2620],{"class":1675},[186,2958,2623],{"class":199},[186,2960,2151],{"class":1675},[186,2962,2963],{"class":188,"line":444},[186,2964,217],{"emptyLinePlaceholder":216},[186,2966,2967,2970],{"class":188,"line":450},[186,2968,2969],{"class":1665},"        if",[186,2971,2972],{"class":1675}," (done) {\n",[186,2974,2975,2978,2981,2984],{"class":188,"line":456},[186,2976,2977],{"class":1665},"          if",[186,2979,2980],{"class":1675}," (buffer.",[186,2982,2983],{"class":199},"trim",[186,2985,2986],{"class":1675},"()) {\n",[186,2988,2989,2992,2995,2997,3000],{"class":188,"line":462},[186,2990,2991],{"class":1675},"            console.",[186,2993,2994],{"class":199},"warn",[186,2996,1950],{"class":1675},[186,2998,2999],{"class":203},"'Stream ended with unparsed data:'",[186,3001,3002],{"class":1675},", buffer);\n",[186,3004,3005],{"class":188,"line":468},[186,3006,2296],{"class":1675},[186,3008,3009],{"class":188,"line":474},[186,3010,217],{"emptyLinePlaceholder":216},[186,3012,3013,3016],{"class":188,"line":479},[186,3014,3015],{"class":1665},"          return",[186,3017,2291],{"class":1675},[186,3019,3020],{"class":188,"line":485},[186,3021,3022],{"class":1675},"        }\n",[186,3024,3025],{"class":188,"line":490},[186,3026,217],{"emptyLinePlaceholder":216},[186,3028,3029,3032,3035],{"class":188,"line":495},[186,3030,3031],{"class":1675},"        buffer ",[186,3033,3034],{"class":1665},"+=",[186,3036,3037],{"class":1675}," value;\n",[186,3039,3040,3042,3045,3047,3050,3053,3055,3058,3061,3063],{"class":188,"line":500},[186,3041,2940],{"class":1665},[186,3043,3044],{"class":229}," lines",[186,3046,2090],{"class":1665},[186,3048,3049],{"class":1675}," buffer.",[186,3051,3052],{"class":199},"split",[186,3054,1950],{"class":1675},[186,3056,3057],{"class":203},"'",[186,3059,3060],{"class":229},"\\n",[186,3062,3057],{"class":203},[186,3064,3065],{"class":1675},");\n",[186,3067,3068,3070,3072,3075,3078,3081,3083,3085],{"class":188,"line":506},[186,3069,3031],{"class":1675},[186,3071,1990],{"class":1665},[186,3073,3074],{"class":1675}," lines.",[186,3076,3077],{"class":199},"pop",[186,3079,3080],{"class":1675},"() ",[186,3082,2015],{"class":1665},[186,3084,2816],{"class":203},[186,3086,2291],{"class":1675},[186,3088,3089],{"class":188,"line":511},[186,3090,217],{"emptyLinePlaceholder":216},[186,3092,3093,3096,3098,3100,3103,3106],{"class":188,"line":516},[186,3094,3095],{"class":1665},"        for",[186,3097,1876],{"class":1675},[186,3099,2466],{"class":1665},[186,3101,3102],{"class":229}," line",[186,3104,3105],{"class":1665}," of",[186,3107,3108],{"class":1675}," lines) {\n",[186,3110,3111,3113,3116,3119,3121,3124],{"class":188,"line":521},[186,3112,2977],{"class":1665},[186,3114,3115],{"class":1675}," (line.",[186,3117,3118],{"class":199},"startsWith",[186,3120,1950],{"class":1675},[186,3122,3123],{"class":203},"'data: '",[186,3125,3126],{"class":1675},")) {\n",[186,3128,3129,3132,3135,3137,3140,3143,3145,3147,3149,3151,3154,3156],{"class":188,"line":527},[186,3130,3131],{"class":1665},"            const",[186,3133,3134],{"class":229}," data",[186,3136,2090],{"class":1665},[186,3138,3139],{"class":1675}," line.",[186,3141,3142],{"class":199},"slice",[186,3144,1950],{"class":1675},[186,3146,3123],{"class":203},[186,3148,152],{"class":1675},[186,3150,2021],{"class":229},[186,3152,3153],{"class":1675},").",[186,3155,2983],{"class":199},[186,3157,2151],{"class":1675},[186,3159,3160,3163,3166,3169,3172],{"class":188,"line":736},[186,3161,3162],{"class":1665},"            if",[186,3164,3165],{"class":1675}," (data ",[186,3167,3168],{"class":1665},"===",[186,3170,3171],{"class":203}," '[DONE]'",[186,3173,2597],{"class":1675},[186,3175,3176,3179],{"class":188,"line":741},[186,3177,3178],{"class":1665},"              return",[186,3180,2291],{"class":1675},[186,3182,3183],{"class":188,"line":747},[186,3184,3185],{"class":1675},"            }\n",[186,3187,3188],{"class":188,"line":753},[186,3189,217],{"emptyLinePlaceholder":216},[186,3191,3192,3195],{"class":188,"line":759},[186,3193,3194],{"class":1665},"            try",[186,3196,1968],{"class":1675},[186,3198,3199,3202,3205,3207,3210,3212,3215],{"class":188,"line":765},[186,3200,3201],{"class":1665},"              const",[186,3203,3204],{"class":229}," jsonData",[186,3206,2090],{"class":1665},[186,3208,3209],{"class":229}," JSON",[186,3211,152],{"class":1675},[186,3213,3214],{"class":199},"parse",[186,3216,3217],{"class":1675},"(data);\n",[186,3219,3220,3223],{"class":188,"line":770},[186,3221,3222],{"class":1665},"              if",[186,3224,3225],{"class":1675}," (jsonData.response) {\n",[186,3227,3228,3231],{"class":188,"line":776},[186,3229,3230],{"class":1665},"                yield",[186,3232,3233],{"class":1675}," jsonData.response;\n",[186,3235,3236],{"class":188,"line":782},[186,3237,3238],{"class":1675},"              }\n",[186,3240,3241,3244,3246],{"class":188,"line":788},[186,3242,3243],{"class":1675},"            } ",[186,3245,2309],{"class":1665},[186,3247,3248],{"class":1675}," (parseError) {\n",[186,3250,3251,3254,3256,3258,3261],{"class":188,"line":794},[186,3252,3253],{"class":1675},"              console.",[186,3255,2994],{"class":199},[186,3257,1950],{"class":1675},[186,3259,3260],{"class":203},"'Error parsing JSON:'",[186,3262,3263],{"class":1675},", parseError);\n",[186,3265,3266],{"class":188,"line":800},[186,3267,3185],{"class":1675},[186,3269,3270],{"class":188,"line":806},[186,3271,2296],{"class":1675},[186,3273,3274],{"class":188,"line":812},[186,3275,3022],{"class":1675},[186,3277,3278],{"class":188,"line":818},[186,3279,3280],{"class":1675},"      }\n",[186,3282,3283,3286,3288],{"class":188,"line":824},[186,3284,3285],{"class":1675},"    } ",[186,3287,2309],{"class":1665},[186,3289,2312],{"class":1675},[186,3291,3292,3295,3297,3299,3302],{"class":188,"line":830},[186,3293,3294],{"class":1675},"      console.",[186,3296,2320],{"class":199},[186,3298,1950],{"class":1675},[186,3300,3301],{"class":203},"'Error sending message:'",[186,3303,3304],{"class":1675},", error);\n",[186,3306,3307],{"class":188,"line":836},[186,3308,217],{"emptyLinePlaceholder":216},[186,3310,3311,3314],{"class":188,"line":842},[186,3312,3313],{"class":1665},"      throw",[186,3315,3316],{"class":1675}," error;\n",[186,3318,3319],{"class":188,"line":848},[186,3320,3321],{"class":1675},"    }\n",[186,3323,3324],{"class":188,"line":854},[186,3325,2076],{"class":1675},[186,3327,3328],{"class":188,"line":860},[186,3329,217],{"emptyLinePlaceholder":216},[186,3331,3332],{"class":188,"line":865},[186,3333,3334],{"class":192},"  \u002F\u002F For handling non-streaming responses\n",[186,3336,3337,3339,3341,3344],{"class":188,"line":871},[186,3338,2755],{"class":1665},[186,3340,2744],{"class":1665},[186,3342,3343],{"class":199}," getResponse",[186,3345,3346],{"class":1675},"() {}\n",[186,3348,3349],{"class":188,"line":877},[186,3350,217],{"emptyLinePlaceholder":216},[186,3352,3353,3356],{"class":188,"line":883},[186,3354,3355],{"class":1665},"  return",[186,3357,1968],{"class":1675},[186,3359,3360],{"class":188,"line":889},[186,3361,3362],{"class":1675},"    getResponse,\n",[186,3364,3365],{"class":188,"line":895},[186,3366,3367],{"class":1675},"    streamResponse,\n",[186,3369,3370],{"class":188,"line":901},[186,3371,2132],{"class":1675},[186,3373,3374],{"class":188,"line":906},[186,3375,2666],{"class":1675},[10,3377,3378,3379,3381],{},"To handle non streaming responses we can also create a simple ",[183,3380,2679],{}," call wrapper function in the same composable (check out the code in the linked Github Repo).",[10,3383,3384,3385,3388],{},"Now that we understand how to consume Server Sent Events and handle streaming responses, let's put all the pieces together in our main chat interface. We'll integrate the components we've built, set up the chat API call, and use our ",[183,3386,3387],{},"useChat"," composable to manage the LLM responses.",[94,3390,3392],{"id":3391},"final-chat-interface-page","Final Chat Interface Page",[10,3394,3395,3396,3398],{},"The below is the final code of the index page (our app has only one page). Here we use all the components that we created before, and call the chat api endpoint. Then we use the ",[183,3397,3387],{}," composable to handle the LLM responses.",[176,3400,3402],{"className":312,"code":3401,"language":314,"meta":181,"style":181},"\u003Ctemplate>\n  \u003Cdiv class=\"h-screen flex flex-col md:flex-row\">\n    \u003CUSlideover\n      v-model=\"isDrawerOpen\"\n      class=\"md:hidden\"\n      :ui=\"{ width: 'max-w-xs' }\"\n    >\n      \u003CLlmSettings\n        v-model:llmParams=\"llmParams\"\n        @hide-drawer=\"isDrawerOpen = false\"\n        @reset=\"resetSettings\"\n      \u002F>\n    \u003C\u002FUSlideover>\n\n    \u003Cdiv class=\"hidden md:block md:w-1\u002F3 lg:w-1\u002F4\">\n      \u003CLlmSettings v-model:llmParams=\"llmParams\" @reset=\"resetSettings\" \u002F>\n    \u003C\u002Fdiv>\n\n    \u003CUDivider orientation=\"vertical\" class=\"hidden md:block\" \u002F>\n\n    \u003Cdiv class=\"flex-grow md:w-2\u002F3 lg:w-3\u002F4\">\n      \u003CChatPanel\n        :chat-history=\"chatHistory\"\n        :loading=\"loading\"\n        @clear=\"chatHistory = []\"\n        @message=\"sendMessage\"\n        @show-drawer=\"isDrawerOpen = true\"\n      \u002F>\n    \u003C\u002Fdiv>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport type { ChatMessage, LlmParams, LoadingType } from '~~\u002Ftypes';\n\nconst isDrawerOpen = ref(false);\n\nconst defaultSettings: LlmParams = {\n  model: '@cf\u002Fmeta\u002Fllama-3.1-8b-instruct',\n  temperature: 0.6,\n  maxTokens: 512,\n  systemPrompt: 'You are a helpful assistant.',\n  stream: true,\n};\n\nconst llmParams = reactive\u003CLlmParams>({ ...defaultSettings });\nconst resetSettings = () => {\n  Object.assign(llmParams, defaultSettings);\n};\n\nconst { getResponse, streamResponse } = useChat();\nconst chatHistory = ref\u003CChatMessage[]>([]);\nconst loading = ref\u003CLoadingType>('idle');\nasync function sendMessage(message: string) {\n  chatHistory.value.push({ role: 'user', content: message });\n\n  try {\n    if (llmParams.stream) {\n      loading.value = 'stream';\n      const messageGenerator = streamResponse(\n        '\u002Fapi\u002Fchat',\n        chatHistory.value,\n        llmParams\n      );\n\n      let responseAdded = false;\n      for await (const chunk of messageGenerator) {\n        if (responseAdded) {\n          \u002F\u002F add the response to the current message\n          chatHistory.value[chatHistory.value.length - 1]!.content += chunk;\n        } else {\n          \u002F\u002F add a new message to the chat history\n          chatHistory.value.push({\n            role: 'assistant',\n            content: chunk,\n          });\n\n          responseAdded = true;\n        }\n      }\n    } else {\n      loading.value = 'message';\n      const response = await getResponse(\n        '\u002Fapi\u002Fchat',\n        chatHistory.value,\n        llmParams\n      );\n\n      chatHistory.value.push({ role: 'assistant', content: response });\n    }\n  } catch (error) {\n    console.error('Error sending message:', error);\n  } finally {\n    loading.value = 'idle';\n  }\n}\n\u003C\u002Fscript>\n",[183,3403,3404,3408,3413,3418,3423,3428,3433,3437,3442,3447,3452,3457,3461,3466,3470,3475,3480,3484,3488,3493,3497,3502,3507,3512,3517,3522,3527,3532,3536,3540,3544,3548,3552,3556,3561,3565,3570,3574,3579,3584,3589,3594,3599,3604,3608,3612,3617,3622,3627,3631,3635,3640,3645,3650,3655,3660,3664,3669,3674,3679,3684,3689,3694,3699,3704,3708,3713,3718,3723,3728,3733,3738,3743,3748,3753,3758,3763,3767,3772,3776,3780,3785,3790,3795,3799,3803,3807,3811,3815,3820,3824,3829,3834,3839,3844,3848,3852],{"__ignoreMap":181},[186,3405,3406],{"class":188,"line":189},[186,3407,321],{},[186,3409,3410],{"class":188,"line":196},[186,3411,3412],{},"  \u003Cdiv class=\"h-screen flex flex-col md:flex-row\">\n",[186,3414,3415],{"class":188,"line":213},[186,3416,3417],{},"    \u003CUSlideover\n",[186,3419,3420],{"class":188,"line":220},[186,3421,3422],{},"      v-model=\"isDrawerOpen\"\n",[186,3424,3425],{"class":188,"line":226},[186,3426,3427],{},"      class=\"md:hidden\"\n",[186,3429,3430],{"class":188,"line":344},[186,3431,3432],{},"      :ui=\"{ width: 'max-w-xs' }\"\n",[186,3434,3435],{"class":188,"line":350},[186,3436,1394],{},[186,3438,3439],{"class":188,"line":356},[186,3440,3441],{},"      \u003CLlmSettings\n",[186,3443,3444],{"class":188,"line":362},[186,3445,3446],{},"        v-model:llmParams=\"llmParams\"\n",[186,3448,3449],{"class":188,"line":368},[186,3450,3451],{},"        @hide-drawer=\"isDrawerOpen = false\"\n",[186,3453,3454],{"class":188,"line":374},[186,3455,3456],{},"        @reset=\"resetSettings\"\n",[186,3458,3459],{"class":188,"line":380},[186,3460,377],{},[186,3462,3463],{"class":188,"line":386},[186,3464,3465],{},"    \u003C\u002FUSlideover>\n",[186,3467,3468],{"class":188,"line":392},[186,3469,217],{"emptyLinePlaceholder":216},[186,3471,3472],{"class":188,"line":398},[186,3473,3474],{},"    \u003Cdiv class=\"hidden md:block md:w-1\u002F3 lg:w-1\u002F4\">\n",[186,3476,3477],{"class":188,"line":404},[186,3478,3479],{},"      \u003CLlmSettings v-model:llmParams=\"llmParams\" @reset=\"resetSettings\" \u002F>\n",[186,3481,3482],{"class":188,"line":409},[186,3483,892],{},[186,3485,3486],{"class":188,"line":415},[186,3487,217],{"emptyLinePlaceholder":216},[186,3489,3490],{"class":188,"line":421},[186,3491,3492],{},"    \u003CUDivider orientation=\"vertical\" class=\"hidden md:block\" \u002F>\n",[186,3494,3495],{"class":188,"line":426},[186,3496,217],{"emptyLinePlaceholder":216},[186,3498,3499],{"class":188,"line":432},[186,3500,3501],{},"    \u003Cdiv class=\"flex-grow md:w-2\u002F3 lg:w-3\u002F4\">\n",[186,3503,3504],{"class":188,"line":438},[186,3505,3506],{},"      \u003CChatPanel\n",[186,3508,3509],{"class":188,"line":444},[186,3510,3511],{},"        :chat-history=\"chatHistory\"\n",[186,3513,3514],{"class":188,"line":450},[186,3515,3516],{},"        :loading=\"loading\"\n",[186,3518,3519],{"class":188,"line":456},[186,3520,3521],{},"        @clear=\"chatHistory = []\"\n",[186,3523,3524],{"class":188,"line":462},[186,3525,3526],{},"        @message=\"sendMessage\"\n",[186,3528,3529],{"class":188,"line":468},[186,3530,3531],{},"        @show-drawer=\"isDrawerOpen = true\"\n",[186,3533,3534],{"class":188,"line":474},[186,3535,377],{},[186,3537,3538],{"class":188,"line":479},[186,3539,892],{},[186,3541,3542],{"class":188,"line":485},[186,3543,898],{},[186,3545,3546],{"class":188,"line":490},[186,3547,401],{},[186,3549,3550],{"class":188,"line":495},[186,3551,217],{"emptyLinePlaceholder":216},[186,3553,3554],{"class":188,"line":500},[186,3555,412],{},[186,3557,3558],{"class":188,"line":506},[186,3559,3560],{},"import type { ChatMessage, LlmParams, LoadingType } from '~~\u002Ftypes';\n",[186,3562,3563],{"class":188,"line":511},[186,3564,217],{"emptyLinePlaceholder":216},[186,3566,3567],{"class":188,"line":516},[186,3568,3569],{},"const isDrawerOpen = ref(false);\n",[186,3571,3572],{"class":188,"line":521},[186,3573,217],{"emptyLinePlaceholder":216},[186,3575,3576],{"class":188,"line":527},[186,3577,3578],{},"const defaultSettings: LlmParams = {\n",[186,3580,3581],{"class":188,"line":736},[186,3582,3583],{},"  model: '@cf\u002Fmeta\u002Fllama-3.1-8b-instruct',\n",[186,3585,3586],{"class":188,"line":741},[186,3587,3588],{},"  temperature: 0.6,\n",[186,3590,3591],{"class":188,"line":747},[186,3592,3593],{},"  maxTokens: 512,\n",[186,3595,3596],{"class":188,"line":753},[186,3597,3598],{},"  systemPrompt: 'You are a helpful assistant.',\n",[186,3600,3601],{"class":188,"line":759},[186,3602,3603],{},"  stream: true,\n",[186,3605,3606],{"class":188,"line":765},[186,3607,979],{},[186,3609,3610],{"class":188,"line":770},[186,3611,217],{"emptyLinePlaceholder":216},[186,3613,3614],{"class":188,"line":776},[186,3615,3616],{},"const llmParams = reactive\u003CLlmParams>({ ...defaultSettings });\n",[186,3618,3619],{"class":188,"line":782},[186,3620,3621],{},"const resetSettings = () => {\n",[186,3623,3624],{"class":188,"line":788},[186,3625,3626],{},"  Object.assign(llmParams, defaultSettings);\n",[186,3628,3629],{"class":188,"line":794},[186,3630,979],{},[186,3632,3633],{"class":188,"line":800},[186,3634,217],{"emptyLinePlaceholder":216},[186,3636,3637],{"class":188,"line":806},[186,3638,3639],{},"const { getResponse, streamResponse } = useChat();\n",[186,3641,3642],{"class":188,"line":812},[186,3643,3644],{},"const chatHistory = ref\u003CChatMessage[]>([]);\n",[186,3646,3647],{"class":188,"line":818},[186,3648,3649],{},"const loading = ref\u003CLoadingType>('idle');\n",[186,3651,3652],{"class":188,"line":824},[186,3653,3654],{},"async function sendMessage(message: string) {\n",[186,3656,3657],{"class":188,"line":830},[186,3658,3659],{},"  chatHistory.value.push({ role: 'user', content: message });\n",[186,3661,3662],{"class":188,"line":836},[186,3663,217],{"emptyLinePlaceholder":216},[186,3665,3666],{"class":188,"line":842},[186,3667,3668],{},"  try {\n",[186,3670,3671],{"class":188,"line":848},[186,3672,3673],{},"    if (llmParams.stream) {\n",[186,3675,3676],{"class":188,"line":854},[186,3677,3678],{},"      loading.value = 'stream';\n",[186,3680,3681],{"class":188,"line":860},[186,3682,3683],{},"      const messageGenerator = streamResponse(\n",[186,3685,3686],{"class":188,"line":865},[186,3687,3688],{},"        '\u002Fapi\u002Fchat',\n",[186,3690,3691],{"class":188,"line":871},[186,3692,3693],{},"        chatHistory.value,\n",[186,3695,3696],{"class":188,"line":877},[186,3697,3698],{},"        llmParams\n",[186,3700,3701],{"class":188,"line":883},[186,3702,3703],{},"      );\n",[186,3705,3706],{"class":188,"line":889},[186,3707,217],{"emptyLinePlaceholder":216},[186,3709,3710],{"class":188,"line":895},[186,3711,3712],{},"      let responseAdded = false;\n",[186,3714,3715],{"class":188,"line":901},[186,3716,3717],{},"      for await (const chunk of messageGenerator) {\n",[186,3719,3720],{"class":188,"line":906},[186,3721,3722],{},"        if (responseAdded) {\n",[186,3724,3725],{"class":188,"line":911},[186,3726,3727],{},"          \u002F\u002F add the response to the current message\n",[186,3729,3730],{"class":188,"line":916},[186,3731,3732],{},"          chatHistory.value[chatHistory.value.length - 1]!.content += chunk;\n",[186,3734,3735],{"class":188,"line":922},[186,3736,3737],{},"        } else {\n",[186,3739,3740],{"class":188,"line":928},[186,3741,3742],{},"          \u002F\u002F add a new message to the chat history\n",[186,3744,3745],{"class":188,"line":934},[186,3746,3747],{},"          chatHistory.value.push({\n",[186,3749,3750],{"class":188,"line":940},[186,3751,3752],{},"            role: 'assistant',\n",[186,3754,3755],{"class":188,"line":946},[186,3756,3757],{},"            content: chunk,\n",[186,3759,3760],{"class":188,"line":952},[186,3761,3762],{},"          });\n",[186,3764,3765],{"class":188,"line":958},[186,3766,217],{"emptyLinePlaceholder":216},[186,3768,3769],{"class":188,"line":964},[186,3770,3771],{},"          responseAdded = true;\n",[186,3773,3774],{"class":188,"line":970},[186,3775,3022],{},[186,3777,3778],{"class":188,"line":976},[186,3779,3280],{},[186,3781,3782],{"class":188,"line":982},[186,3783,3784],{},"    } else {\n",[186,3786,3787],{"class":188,"line":987},[186,3788,3789],{},"      loading.value = 'message';\n",[186,3791,3792],{"class":188,"line":993},[186,3793,3794],{},"      const response = await getResponse(\n",[186,3796,3797],{"class":188,"line":999},[186,3798,3688],{},[186,3800,3801],{"class":188,"line":1005},[186,3802,3693],{},[186,3804,3805],{"class":188,"line":1010},[186,3806,3698],{},[186,3808,3809],{"class":188,"line":1015},[186,3810,3703],{},[186,3812,3813],{"class":188,"line":1021},[186,3814,217],{"emptyLinePlaceholder":216},[186,3816,3817],{"class":188,"line":1026},[186,3818,3819],{},"      chatHistory.value.push({ role: 'assistant', content: response });\n",[186,3821,3822],{"class":188,"line":1032},[186,3823,3321],{},[186,3825,3826],{"class":188,"line":1038},[186,3827,3828],{},"  } catch (error) {\n",[186,3830,3831],{"class":188,"line":1044},[186,3832,3833],{},"    console.error('Error sending message:', error);\n",[186,3835,3836],{"class":188,"line":1050},[186,3837,3838],{},"  } finally {\n",[186,3840,3841],{"class":188,"line":1055},[186,3842,3843],{},"    loading.value = 'idle';\n",[186,3845,3846],{"class":188,"line":1061},[186,3847,2076],{},[186,3849,3850],{"class":188,"line":1066},[186,3851,2666],{},[186,3853,3854],{"class":188,"line":1072},[186,3855,530],{},[10,3857,3858],{},"This completes bulk of the coding. The only things remaining are:",[99,3860,3861,3864],{},[48,3862,3863],{},"Response parsing for markdown and display",[48,3865,3866],{},"Auto scrolling the chat container",[10,3868,3869],{},"Let's tackle these in the next section.",[25,3871,3873],{"id":3872},"polishing-the-chat-interface","Polishing the Chat Interface",[10,3875,3876],{},"We will finish the remaining items in our task list now and call it a day. Stay with me, it won't take long now.",[94,3878,3880],{"id":3879},"using-nuxt-mdc-to-parse-display-messages","Using Nuxt MDC to Parse & Display Messages",[10,3882,3883,3884,3886,3887,3890],{},"If you look at the ",[183,3885,1326],{}," code in one of the previous sections you'll notice a component ",[183,3888,3889],{},"AssistantMessage"," for displaying the response. Reproducing the relevant code here",[176,3892,3894],{"className":312,"code":3893,"language":314,"meta":181,"style":181},"\u003Cdiv v-if=\"message.role === 'user'\">\n  {{ message.content }}\n\u003C\u002Fdiv>\n\u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n",[183,3895,3896,3901,3906,3910],{"__ignoreMap":181},[186,3897,3898],{"class":188,"line":189},[186,3899,3900],{},"\u003Cdiv v-if=\"message.role === 'user'\">\n",[186,3902,3903],{"class":188,"line":196},[186,3904,3905],{},"  {{ message.content }}\n",[186,3907,3908],{"class":188,"line":213},[186,3909,1489],{},[186,3911,3912],{"class":188,"line":220},[186,3913,3914],{},"\u003CAssistantMessage v-else :content=\"message.content\" \u002F>\n",[10,3916,3917,3918,3921,3922,3925,3926,3929,3930,3933,3934,3937],{},"We use the ",[183,3919,3920],{},"parseMarkdown"," utility function from the ",[183,3923,3924],{},"MDC module"," that we included earlier to parse the content, and then use the ",[183,3927,3928],{},"MDCRenderer"," component from the same module for displaying it. To take care of streaming response we add a ",[183,3931,3932],{},"watch"," for the message content and redo the parsing with ",[183,3935,3936],{},"useAsyncData"," composable.",[176,3939,3941],{"className":312,"code":3940,"language":314,"meta":181,"style":181},"\u003Ctemplate>\n  \u003CMDCRenderer class=\"flex-1 prose dark:prose-invert\" :body=\"ast?.body\" \u002F>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nimport { parseMarkdown } from '#imports';\n\nconst props = defineProps\u003C{\n  content: string;\n}>();\n\nconst { data: ast, refresh } = await useAsyncData(useId(), () =>\n  parseMarkdown(props.content)\n);\n\nwatch(\n  () => props.content,\n  () => {\n    refresh();\n  }\n);\n\u003C\u002Fscript>\n",[183,3942,3943,3947,3952,3956,3960,3964,3969,3973,3978,3983,3988,3992,3997,4002,4006,4010,4015,4020,4025,4030,4034,4038],{"__ignoreMap":181},[186,3944,3945],{"class":188,"line":189},[186,3946,321],{},[186,3948,3949],{"class":188,"line":196},[186,3950,3951],{},"  \u003CMDCRenderer class=\"flex-1 prose dark:prose-invert\" :body=\"ast?.body\" \u002F>\n",[186,3953,3954],{"class":188,"line":213},[186,3955,401],{},[186,3957,3958],{"class":188,"line":220},[186,3959,217],{"emptyLinePlaceholder":216},[186,3961,3962],{"class":188,"line":226},[186,3963,412],{},[186,3965,3966],{"class":188,"line":344},[186,3967,3968],{},"import { parseMarkdown } from '#imports';\n",[186,3970,3971],{"class":188,"line":350},[186,3972,217],{"emptyLinePlaceholder":216},[186,3974,3975],{"class":188,"line":356},[186,3976,3977],{},"const props = defineProps\u003C{\n",[186,3979,3980],{"class":188,"line":362},[186,3981,3982],{},"  content: string;\n",[186,3984,3985],{"class":188,"line":368},[186,3986,3987],{},"}>();\n",[186,3989,3990],{"class":188,"line":374},[186,3991,217],{"emptyLinePlaceholder":216},[186,3993,3994],{"class":188,"line":380},[186,3995,3996],{},"const { data: ast, refresh } = await useAsyncData(useId(), () =>\n",[186,3998,3999],{"class":188,"line":386},[186,4000,4001],{},"  parseMarkdown(props.content)\n",[186,4003,4004],{"class":188,"line":392},[186,4005,3065],{},[186,4007,4008],{"class":188,"line":398},[186,4009,217],{"emptyLinePlaceholder":216},[186,4011,4012],{"class":188,"line":404},[186,4013,4014],{},"watch(\n",[186,4016,4017],{"class":188,"line":409},[186,4018,4019],{},"  () => props.content,\n",[186,4021,4022],{"class":188,"line":415},[186,4023,4024],{},"  () => {\n",[186,4026,4027],{"class":188,"line":421},[186,4028,4029],{},"    refresh();\n",[186,4031,4032],{"class":188,"line":426},[186,4033,2076],{},[186,4035,4036],{"class":188,"line":432},[186,4037,3065],{},[186,4039,4040],{"class":188,"line":438},[186,4041,530],{},[2401,4043,4044,4046],{"dataNodeType":2403},[2401,4045,2407],{"dataNodeType":2406},[2401,4047,4048,4049,4052,4053,4056,4057,4059,4060,4062],{"dataNodeType":2410},"We could have used the ",[183,4050,4051],{},"\u003CMDC>"," component instead of using ",[183,4054,4055],{},"parseMarkdown + \u003CMDCRenderer>"," combination. ",[183,4058,4051],{}," handles the parsing internally using the same ",[183,4061,3920],{}," function, then why we didn't use it?",[10,4064,4065,4066,4068,4069,4071],{},"The ",[183,4067,4051],{}," component was causing overwriting of previous messages that were similar (the start of the message) to the latest message from the API endpoint. This happens because we don't have a unique key in our messages. Here is the relevant code from the ",[183,4070,4051],{}," component.",[176,4073,4075],{"className":1651,"code":4074,"language":1653,"meta":181,"style":181},"const key = computed(() => hash(props.value))\n\nconst { data, refresh, error } = await useAsyncData(key.value, async () => {\n  if (typeof props.value !== 'string') {\n    return props.value\n  }\n  return await parseMarkdown(props.value, props.parserOptions)\n})\n",[183,4076,4077,4100,4104,4143,4163,4170,4174,4186],{"__ignoreMap":181},[186,4078,4079,4081,4084,4086,4089,4092,4094,4097],{"class":188,"line":189},[186,4080,2466],{"class":1665},[186,4082,4083],{"class":229}," key",[186,4085,2090],{"class":1665},[186,4087,4088],{"class":199}," computed",[186,4090,4091],{"class":1675},"(() ",[186,4093,1965],{"class":1665},[186,4095,4096],{"class":199}," hash",[186,4098,4099],{"class":1675},"(props.value))\n",[186,4101,4102],{"class":188,"line":196},[186,4103,217],{"emptyLinePlaceholder":216},[186,4105,4106,4108,4110,4113,4115,4118,4120,4122,4124,4126,4128,4131,4134,4136,4139,4141],{"class":188,"line":213},[186,4107,2466],{"class":1665},[186,4109,1976],{"class":1675},[186,4111,4112],{"class":229},"data",[186,4114,1496],{"class":1675},[186,4116,4117],{"class":229},"refresh",[186,4119,1496],{"class":1675},[186,4121,2320],{"class":229},[186,4123,1987],{"class":1675},[186,4125,1990],{"class":1665},[186,4127,1993],{"class":1665},[186,4129,4130],{"class":199}," useAsyncData",[186,4132,4133],{"class":1675},"(key.value, ",[186,4135,1953],{"class":1665},[186,4137,4138],{"class":1675}," () ",[186,4140,1965],{"class":1665},[186,4142,1968],{"class":1675},[186,4144,4145,4147,4149,4152,4155,4158,4161],{"class":188,"line":220},[186,4146,2004],{"class":1665},[186,4148,1876],{"class":1675},[186,4150,4151],{"class":1665},"typeof",[186,4153,4154],{"class":1675}," props.value ",[186,4156,4157],{"class":1665},"!==",[186,4159,4160],{"class":203}," 'string'",[186,4162,2597],{"class":1675},[186,4164,4165,4167],{"class":188,"line":226},[186,4166,2237],{"class":1665},[186,4168,4169],{"class":1675}," props.value\n",[186,4171,4172],{"class":188,"line":344},[186,4173,2076],{"class":1675},[186,4175,4176,4178,4180,4183],{"class":188,"line":350},[186,4177,3355],{"class":1665},[186,4179,1993],{"class":1665},[186,4181,4182],{"class":199}," parseMarkdown",[186,4184,4185],{"class":1675},"(props.value, props.parserOptions)\n",[186,4187,4188],{"class":188,"line":356},[186,4189,2535],{"class":1675},[10,4191,4192,4193,4195],{},"As you can see, the key for ",[183,4194,3936],{}," is generated based on the content props value. So if the server streaming responses start with the same letters (e.g., Here's a joke..., Here's another...), the key will be same, and all similar previous responses from the server gets overwritten in the UI.",[10,4197,4198,4199,4201,4202,4205,4206,4208],{},"In the ",[183,4200,3889],{}," component we are using ",[183,4203,4204],{},"useId"," composable to generate a unique id for each ",[183,4207,3936],{}," call, so the issue doesn't occur.",[94,4210,4212],{"id":4211},"using-mutationobserver-to-auto-scroll-the-chats-container","Using MutationObserver to Auto Scroll the Chats Container",[10,4214,4215,4216,152],{},"In the ChatPanel component, the only scrolling part is the chats container. There can be other ways to observe the changes in the container but the simplest one I found is a ",[14,4217,4220],{"href":4218,"rel":4219},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FMutationObserver",[18],[183,4221,4222],{},"MutationObserver",[2401,4224,4225,4227],{"dataNodeType":2403},[2401,4226,2407],{"dataNodeType":2406},[2401,4228,4065,4229,4231],{"dataNodeType":2410},[183,4230,4222],{}," interface provides the ability to watch for changes being made to the DOM tree.",[10,4233,4234,4235,3153],{},"In the case of streaming, we keep appending new content to the latest message that is being displayed. So to handle this effectively, we create a MutationObserver, give it a target to watch (the ChatsContainer), and some criteria to watch for (",[183,4236,4237],{},"childList, subtree & characterData",[10,4239,4240],{},"Here is the relevant code to do this",[176,4242,4244],{"className":1651,"code":4243,"language":1653,"meta":181,"style":181},"const chatContainer = ref\u003CHTMLElement | null>(null);\nlet observer: MutationObserver | null = null;\n\nonMounted(() => {\n  if (chatContainer.value) {\n    observer = new MutationObserver(() => {\n      if (chatContainer.value) {\n        chatContainer.value.scrollTop = chatContainer.value.scrollHeight;\n      }\n    });\n\n    observer.observe(chatContainer.value, {\n      childList: true,\n      subtree: true,\n      characterData: true,\n    });\n  }\n});\n\nonUnmounted(() => {\n  if (observer) {\n    observer.disconnect();\n  }\n});\n",[183,4245,4246,4276,4299,4303,4314,4321,4339,4346,4356,4360,4364,4368,4379,4388,4397,4406,4410,4414,4418,4422,4433,4440,4449,4453],{"__ignoreMap":181},[186,4247,4248,4250,4253,4255,4258,4260,4263,4266,4269,4271,4274],{"class":188,"line":189},[186,4249,2466],{"class":1665},[186,4251,4252],{"class":229}," chatContainer",[186,4254,2090],{"class":1665},[186,4256,4257],{"class":199}," ref",[186,4259,2479],{"class":1675},[186,4261,4262],{"class":199},"HTMLElement",[186,4264,4265],{"class":1665}," |",[186,4267,4268],{"class":229}," null",[186,4270,2485],{"class":1675},[186,4272,4273],{"class":229},"null",[186,4275,3065],{"class":1675},[186,4277,4278,4281,4284,4286,4289,4291,4293,4295,4297],{"class":188,"line":196},[186,4279,4280],{"class":1665},"let",[186,4282,4283],{"class":1675}," observer",[186,4285,2285],{"class":1665},[186,4287,4288],{"class":199}," MutationObserver",[186,4290,4265],{"class":1665},[186,4292,4268],{"class":229},[186,4294,2090],{"class":1665},[186,4296,4268],{"class":229},[186,4298,2291],{"class":1675},[186,4300,4301],{"class":188,"line":213},[186,4302,217],{"emptyLinePlaceholder":216},[186,4304,4305,4308,4310,4312],{"class":188,"line":220},[186,4306,4307],{"class":199},"onMounted",[186,4309,4091],{"class":1675},[186,4311,1965],{"class":1665},[186,4313,1968],{"class":1675},[186,4315,4316,4318],{"class":188,"line":226},[186,4317,2004],{"class":1665},[186,4319,4320],{"class":1675}," (chatContainer.value) {\n",[186,4322,4323,4326,4328,4331,4333,4335,4337],{"class":188,"line":344},[186,4324,4325],{"class":1675},"    observer ",[186,4327,1990],{"class":1665},[186,4329,4330],{"class":1665}," new",[186,4332,4288],{"class":199},[186,4334,4091],{"class":1675},[186,4336,1965],{"class":1665},[186,4338,1968],{"class":1675},[186,4340,4341,4344],{"class":188,"line":350},[186,4342,4343],{"class":1665},"      if",[186,4345,4320],{"class":1675},[186,4347,4348,4351,4353],{"class":188,"line":356},[186,4349,4350],{"class":1675},"        chatContainer.value.scrollTop ",[186,4352,1990],{"class":1665},[186,4354,4355],{"class":1675}," chatContainer.value.scrollHeight;\n",[186,4357,4358],{"class":188,"line":362},[186,4359,3280],{"class":1675},[186,4361,4362],{"class":188,"line":368},[186,4363,2071],{"class":1675},[186,4365,4366],{"class":188,"line":374},[186,4367,217],{"emptyLinePlaceholder":216},[186,4369,4370,4373,4376],{"class":188,"line":380},[186,4371,4372],{"class":1675},"    observer.",[186,4374,4375],{"class":199},"observe",[186,4377,4378],{"class":1675},"(chatContainer.value, {\n",[186,4380,4381,4384,4386],{"class":188,"line":386},[186,4382,4383],{"class":1675},"      childList: ",[186,4385,1859],{"class":229},[186,4387,1687],{"class":1675},[186,4389,4390,4393,4395],{"class":188,"line":392},[186,4391,4392],{"class":1675},"      subtree: ",[186,4394,1859],{"class":229},[186,4396,1687],{"class":1675},[186,4398,4399,4402,4404],{"class":188,"line":398},[186,4400,4401],{"class":1675},"      characterData: ",[186,4403,1859],{"class":229},[186,4405,1687],{"class":1675},[186,4407,4408],{"class":188,"line":404},[186,4409,2071],{"class":1675},[186,4411,4412],{"class":188,"line":409},[186,4413,2076],{"class":1675},[186,4415,4416],{"class":188,"line":415},[186,4417,524],{"class":1675},[186,4419,4420],{"class":188,"line":421},[186,4421,217],{"emptyLinePlaceholder":216},[186,4423,4424,4427,4429,4431],{"class":188,"line":426},[186,4425,4426],{"class":199},"onUnmounted",[186,4428,4091],{"class":1675},[186,4430,1965],{"class":1665},[186,4432,1968],{"class":1675},[186,4434,4435,4437],{"class":188,"line":432},[186,4436,2004],{"class":1665},[186,4438,4439],{"class":1675}," (observer) {\n",[186,4441,4442,4444,4447],{"class":188,"line":438},[186,4443,4372],{"class":1675},[186,4445,4446],{"class":199},"disconnect",[186,4448,2151],{"class":1675},[186,4450,4451],{"class":188,"line":444},[186,4452,2076],{"class":1675},[186,4454,4455],{"class":188,"line":450},[186,4456,524],{"class":1675},[10,4458,4459],{},"When the ChatsPanel gets mounted we create a new MutationObserver which when based on the given criteria, we make the chats container scroll to its fullest.",[94,4461,4463],{"id":4462},"bonus-handling-the-dark-mode","Bonus: Handling the Dark Mode",[10,4465,4466,4467,4470],{},"As previously mentioned, the dark mode is already working; we just need a way to toggle it. We can also change the flavor of gray we want in the app. This can be done by adding an ",[183,4468,4469],{},"app.config.ts"," file in the app directory.",[176,4472,4474],{"className":1651,"code":4473,"language":1653,"meta":181,"style":181},"\u002F\u002F app.config.ts\nexport default defineAppConfig({\n  ui: {\n    primary: 'orange',\n    gray: 'slate',\n  },\n});\n",[183,4475,4476,4481,4492,4497,4507,4517,4521],{"__ignoreMap":181},[186,4477,4478],{"class":188,"line":189},[186,4479,4480],{"class":192},"\u002F\u002F app.config.ts\n",[186,4482,4483,4485,4487,4490],{"class":188,"line":196},[186,4484,1666],{"class":1665},[186,4486,1669],{"class":1665},[186,4488,4489],{"class":199}," defineAppConfig",[186,4491,1676],{"class":1675},[186,4493,4494],{"class":188,"line":213},[186,4495,4496],{"class":1675},"  ui: {\n",[186,4498,4499,4502,4505],{"class":188,"line":220},[186,4500,4501],{"class":1675},"    primary: ",[186,4503,4504],{"class":203},"'orange'",[186,4506,1687],{"class":1675},[186,4508,4509,4512,4515],{"class":188,"line":226},[186,4510,4511],{"class":1675},"    gray: ",[186,4513,4514],{"class":203},"'slate'",[186,4516,1687],{"class":1675},[186,4518,4519],{"class":188,"line":344},[186,4520,453],{"class":1675},[186,4522,4523],{"class":188,"line":350},[186,4524,524],{"class":1675},[10,4526,4527,4528,4531],{},"Add the following in your ",[183,4529,4530],{},"app.vue"," file for setting the required background colors.",[176,4533,4535],{"className":312,"code":4534,"language":314,"meta":181,"style":181},"\u003Cscript setup lang=\"ts\">\nuseHead({\n  bodyAttrs: {\n    class: 'bg-white dark:bg-gray-900',\n  },\n});\n\u003C\u002Fscript>\n",[183,4536,4537,4541,4546,4551,4556,4560,4564],{"__ignoreMap":181},[186,4538,4539],{"class":188,"line":189},[186,4540,412],{},[186,4542,4543],{"class":188,"line":196},[186,4544,4545],{},"useHead({\n",[186,4547,4548],{"class":188,"line":213},[186,4549,4550],{},"  bodyAttrs: {\n",[186,4552,4553],{"class":188,"line":220},[186,4554,4555],{},"    class: 'bg-white dark:bg-gray-900',\n",[186,4557,4558],{"class":188,"line":226},[186,4559,453],{},[186,4561,4562],{"class":188,"line":344},[186,4563,524],{},[186,4565,4566],{"class":188,"line":350},[186,4567,530],{},[10,4569,4570,4571,4574,4575,4578],{},"And add a new ",[183,4572,4573],{},"ColorMode"," component in your ",[183,4576,4577],{},"app\u002Fcomponents"," directory.",[176,4580,4582],{"className":312,"code":4581,"language":314,"meta":181,"style":181},"\u003Ctemplate>\n  \u003CClientOnly>\n    \u003CUButton\n      :icon=\"isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'\"\n      color=\"gray\"\n      variant=\"ghost\"\n      aria-label=\"Theme\"\n      @click=\"isDark = !isDark\"\n    \u002F>\n\n    \u003Ctemplate #fallback>\n      \u003Cdiv class=\"w-8 h-8\" \u002F>\n    \u003C\u002Ftemplate>\n  \u003C\u002FClientOnly>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup lang=\"ts\">\nconst colorMode = useColorMode();\n\nconst isDark = computed({\n  get() {\n    return colorMode.value === 'dark';\n  },\n  set() {\n    colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';\n  },\n});\n\u003C\u002Fscript>\n",[183,4583,4584,4588,4593,4598,4603,4608,4613,4618,4623,4628,4632,4637,4642,4646,4651,4655,4659,4663,4668,4672,4677,4682,4687,4691,4696,4701,4705,4709],{"__ignoreMap":181},[186,4585,4586],{"class":188,"line":189},[186,4587,321],{},[186,4589,4590],{"class":188,"line":196},[186,4591,4592],{},"  \u003CClientOnly>\n",[186,4594,4595],{"class":188,"line":213},[186,4596,4597],{},"    \u003CUButton\n",[186,4599,4600],{"class":188,"line":220},[186,4601,4602],{},"      :icon=\"isDark ? 'i-heroicons-moon-20-solid' : 'i-heroicons-sun-20-solid'\"\n",[186,4604,4605],{"class":188,"line":226},[186,4606,4607],{},"      color=\"gray\"\n",[186,4609,4610],{"class":188,"line":344},[186,4611,4612],{},"      variant=\"ghost\"\n",[186,4614,4615],{"class":188,"line":350},[186,4616,4617],{},"      aria-label=\"Theme\"\n",[186,4619,4620],{"class":188,"line":356},[186,4621,4622],{},"      @click=\"isDark = !isDark\"\n",[186,4624,4625],{"class":188,"line":362},[186,4626,4627],{},"    \u002F>\n",[186,4629,4630],{"class":188,"line":368},[186,4631,217],{"emptyLinePlaceholder":216},[186,4633,4634],{"class":188,"line":374},[186,4635,4636],{},"    \u003Ctemplate #fallback>\n",[186,4638,4639],{"class":188,"line":380},[186,4640,4641],{},"      \u003Cdiv class=\"w-8 h-8\" \u002F>\n",[186,4643,4644],{"class":188,"line":386},[186,4645,383],{},[186,4647,4648],{"class":188,"line":392},[186,4649,4650],{},"  \u003C\u002FClientOnly>\n",[186,4652,4653],{"class":188,"line":398},[186,4654,401],{},[186,4656,4657],{"class":188,"line":404},[186,4658,217],{"emptyLinePlaceholder":216},[186,4660,4661],{"class":188,"line":409},[186,4662,412],{},[186,4664,4665],{"class":188,"line":415},[186,4666,4667],{},"const colorMode = useColorMode();\n",[186,4669,4670],{"class":188,"line":421},[186,4671,217],{"emptyLinePlaceholder":216},[186,4673,4674],{"class":188,"line":426},[186,4675,4676],{},"const isDark = computed({\n",[186,4678,4679],{"class":188,"line":432},[186,4680,4681],{},"  get() {\n",[186,4683,4684],{"class":188,"line":438},[186,4685,4686],{},"    return colorMode.value === 'dark';\n",[186,4688,4689],{"class":188,"line":444},[186,4690,453],{},[186,4692,4693],{"class":188,"line":450},[186,4694,4695],{},"  set() {\n",[186,4697,4698],{"class":188,"line":456},[186,4699,4700],{},"    colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark';\n",[186,4702,4703],{"class":188,"line":462},[186,4704,453],{},[186,4706,4707],{"class":188,"line":468},[186,4708,524],{},[186,4710,4711],{"class":188,"line":474},[186,4712,530],{},[10,4714,4715,4716,4071],{},"Now you can use this color mode toggle button anywhere you like. In our app we've added it in the ",[183,4717,4718],{},"ChatHeader",[10,4720,4721],{},"Phew! And we have completed all the tasks we had set out to complete at the beginning of this article.",[25,4723,4725],{"id":4724},"deploying-the-app","Deploying the App",[10,4727,4728],{},"You can deploy the project in multiple ways. I would recommend to do it through the NuxtHub Admin console. Push your code to a Github repository, link this repository with NuxtHub and then deploy from the Admin console.",[10,4730,4731],{},"But if you want to see it live right away then you can use the below command",[176,4733,4735],{"className":178,"code":4734,"language":180,"meta":181,"style":181},"npx nuxthub deploy\n",[183,4736,4737],{"__ignoreMap":181},[186,4738,4739,4741,4743],{"class":188,"line":189},[186,4740,200],{"class":199},[186,4742,204],{"class":203},[186,4744,4745],{"class":203}," deploy\n",[2401,4747,4748,4750],{"dataNodeType":2403},[2401,4749,2407],{"dataNodeType":2406},[2401,4751,4752],{"dataNodeType":2410},"If you do your first deployment with the NuxtHub CLI, you won't be able to attach your GitHub\u002FGitLab repository later on due to a Cloudflare limitation.",[10,4754,4755,4756,152],{},"For more details on deployment you can visit the ",[14,4757,4760],{"href":4758,"rel":4759},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs\u002Fgetting-started\u002Fdeploy",[18],"NuxtHub Docs",[25,4762,4764],{"id":4763},"source-code","Source Code",[10,4766,4767],{},"I only covered the most important parts of the source code here. You can visit the linked GIthub Repo for the complete source code. It should be self explanatory, but if you have question then please feel free to drop a comment below.",[4769,4770],"media-embed",{"url":4771},"https:\u002F\u002Fgithub.com\u002Fra-jeev\u002Fhub-chat\u002F",[25,4773,4775],{"id":4774},"conclusion","Conclusion",[10,4777,4778],{},"Congratulations! You've successfully built a feature-rich LLM playground using Nuxt 3, NuxtUI, and Cloudflare Workers AI through NuxtHub. We've covered a wide range of topics, including:",[45,4780,4781,4784,4787],{},[48,4782,4783],{},"Handling streaming responses using Server Sent Events",[48,4785,4786],{},"Parsing and displaying markdown content in chat messages",[48,4788,4789],{},"Implementing auto-scrolling for a better user experience etc.",[10,4791,4792],{},"You can take this project as the starting point and improve it further by:",[45,4794,4795,4798,4801],{},[48,4796,4797],{},"Adding the ability to talks to other types of LLMs, e.g. text-to-image, image-to-text, speech recognition etc.",[48,4799,4800],{},"Implementing user authentication and session management",[48,4802,4803],{},"Adding support for multiple conversation threads etc.",[10,4805,4806],{},"Thank you for staying till the end. I hope you learned some new concepts from this article. Do let me know your learnings in the comments section. Your feedback and experiences are valuable not just to me, but to the entire community of developers exploring this fascinating field.",[10,4808,4809],{},"Until next time!",[4811,4812],"hr",{},[4814,4815,4816],"blockquote",{},[10,4817,4818,152],{},[4819,4820,4821],"em",{},"Keep adding the bits and soon you'll have a lot of bytes to share with the world",[4823,4824,4825],"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":181,"searchDepth":196,"depth":196,"links":4827},[4828,4829,4834,4839,4842,4847,4852,4853,4854],{"id":27,"depth":196,"text":28},{"id":88,"depth":196,"text":89,"children":4830},[4831,4832,4833],{"id":96,"depth":213,"text":97},{"id":133,"depth":213,"text":134},{"id":165,"depth":213,"text":166},{"id":295,"depth":196,"text":296,"children":4835},[4836,4837,4838],{"id":305,"depth":213,"text":306},{"id":540,"depth":213,"text":541},{"id":1134,"depth":213,"text":1135},{"id":1640,"depth":196,"text":1641,"children":4840},[4841],{"id":1914,"depth":213,"text":1915},{"id":2380,"depth":196,"text":2381,"children":4843},[4844,4845,4846],{"id":2387,"depth":213,"text":2388},{"id":2441,"depth":213,"text":2442},{"id":3391,"depth":213,"text":3392},{"id":3872,"depth":196,"text":3873,"children":4848},[4849,4850,4851],{"id":3879,"depth":213,"text":3880},{"id":4211,"depth":213,"text":4212},{"id":4462,"depth":213,"text":4463},{"id":4724,"depth":196,"text":4725},{"id":4763,"depth":196,"text":4764},{"id":4774,"depth":196,"text":4775},"\u002Fimages\u002Fposts\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui\u002F6ae3905c-72e8-4e71-8ce0-b22680856f0d-aa3090c597.png","2024-08-29T07:46:37.681Z","You might be wondering, 'Another LLM (Large Language Model) playground? Aren't there plenty of these already?' Fair question. But here's the thing: the world of AI is constantly...",false,"md","cm0ezeghd00110ama05jhaghe",{},"\u002Fcreate-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui",{"title":5,"description":4857},"create-cloudflare-workers-ai-llm-playground-using-nuxthub-and-nuxtui",[4866,4867,4868,4869,4870],"cloudflare","nuxt","nuxthub","workers-ai","nuxtui","zHfhudcPj8zZvHTxFxdi-yoSYdk9-qT6ZTDwoDySks8",1780400660363]