Scaffolding our frontend

Start by setting up a default Nuxt 3 repo.

We’re going to style this app with TailwindCSS. To do this, I installed the Nuxt Tailwind module and then added it to my nuxt.config.ts:

export default defineNuxtConfig({
  modules: ['@nuxtjs/tailwindcss']
})

Let’s scaffold out a very simple UI with a form input, that we can use to start testing.

To do this, I set up a simple component called ChatWidget, which houses our layout, and a nested component called Chain in the parts folder, where we can handle the UI around the Q&A chain.

// components/ChatWidget.vue
<template>
	<div class="w-screen h-screen flex flex-col bg-gray-50">
	    <div class="w-full relative overflow-hidden flex flex-grow">
	        <div class="flex flex-col w-full h-full">
	            <header
	                class="flex items-center px-6 h-14 w-full bg-white border-b border-gray-200 shadow-sm fixed top-0 z-10"
	            >
	                <h1 class="font-semibold text-lg">My Q&A chat tool</h1>
	            </header>
	
	            <div
	                class="p-6 mt-14 h-full flex flex-col-reverse overflow-y-auto"
	            >
	                <PartsChain />
	            </div>
	        </div>
	    </div>
	</div>
</template>

// components/parts/Chain.vue
<template>
	<form class="flex flex-col w-full">
		<label class="font-medium text-gray-600 mb-2.5"
		>Ask our knowledge base a question</label
		>
		
		<input
		type="text"
		placeholder="Ask your question..."
		class="h-14 px-3 border border-gray-200 max-w-lg"
		/>
		
		<div class="font-medium text-xs text-gray-400 mt-1.5">
			<p>Press enter to ask...</p>
		</div>
	</form>
</template>

Perfect! Now we have this:

Let’s add some logic to Chain.vue to simulate async messaging. This will allow us to scaffold out some basic loading state logic.

I’ll add a <script setup lang="ts"> and mock an askQuestion method with a setTimeout Promise hack.

// in script setup
const error = ref<string | null>(null);
const isLoadingMessage = ref<boolean>(false);

async function askQuestion() {
try {
		isLoadingMessage.value = true;
		await new Promise((resolve) => setTimeout(resolve, 1000));
	} catch (e) {
		console.error(e);
		error.value = 'Unfortunately, there was an issue answering your question.';
	} finally {
		isLoadingMessage.value = false;
	}
}

And now, let’s update the template:

<template>
	<form @submit.prevent="askQuestion" class="flex flex-col w-full">
		<label class="font-medium text-gray-600 mb-2.5"
		>Ask our knowledge base a question</label
		>
		
		<input
			type="text"
			placeholder="Ask your question..."
			class="h-14 px-3 border border-gray-200 max-w-lg"
		/>
		
		<div class="font-medium text-xs text-gray-400 mt-1.5">
			<p v-if="!isLoadingMessage">Press enter to ask...</p>
			<p class="animate-pulse" v-else>Analysing the knowledge base...</p>
		</div>
	</form>
</template>

Voila! We have our scaffolded chat interface. When you press enter in the form, it triggers askQuestion, which displays a loading state for 1000ms.