Une application IA puissante réalisée avec Nitro et Nuxt UI
Fait partie de la série Agents IA et serveur MCP au service du web agentique
Notre backend est prêt, nous pouvons maintenant nous concentrer sur le frontend. Une fois terminé, nous aurons une application IA totalement fonctionnelle.
Pour la construire, nous utiliserons Nuxt avec Nuxt UI, qui contient un ensemble de composants spécialement conçus pour construire des applications IA.
De Nitro à Nuxt
Nuxt utilise Nitro comme moteur serveur, la transition de Nitro vers Nuxt ne sera donc pas difficile.
Comme cette partie n'est pas la plus intéressante, j'ai créé ce script pour simplifier le processus :
# Supprimer les dépendances Nitro
pnpm remove nitropack h3
# Installer les dépendances Nuxt
pnpm add nuxt vue vue-router
# Mettre à jour les scripts de package.json
jq '.scripts = {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
}' package.json > tmp.json && mv tmp.json package.json
# Mettre à jour tsconfig.json pour Nuxt
cat > tsconfig.json <<'EOF'
{
"files": [],
"references": [
{ "path": "./.nuxt/tsconfig.app.json" },
{ "path": "./.nuxt/tsconfig.server.json" },
{ "path": "./.nuxt/tsconfig.shared.json" },
{ "path": "./.nuxt/tsconfig.node.json" }
]
}
EOF
# Créer la configuration Nuxt
cat > nuxt.config.ts <<'EOF'
import { defineNuxtConfig } from "nuxt/config"
export default defineNuxtConfig({
runtimeConfig: {
openAiApiKey: '',
mcpEndpoint: '',
},
compatibilityDate: '2025-10-05',
})
EOF
# Supprimer l'ancienne config Nitro
rm nitro.config.ts
# Mettre en place la structure de base de l'app Nuxt
mkdir -p app/pages
cat > app/app.vue <<'EOF'
<template>
<div>
<NuxtRouteAnnouncer />
<NuxtPage />
</div>
</template>
EOF
cat > app/pages/index.vue <<'EOF'
<template>
<div>
<h1>Bienvenue dans l'application IA</h1>
</div>
</template>
EOF
# Mettre à jour .env et .gitignore
sed -i '' 's/NITRO/NUXT/g' .env
echo ".nuxt" >> .gitignore
# Préparer Nuxt
pnpm run postinstall
Ajouter Nuxt UI
Installer Nuxt UI ne fait pas partie de cette série, vous pouvez simplement exécuter ce script :
# Installer Nuxt UI et Tailwind CSS
pnpm add @nuxt/ui tailwindcss
# Créer le fichier CSS principal et importer les styles
mkdir -p app/assets/css
cat > app/assets/css/main.css <<'EOF'
@import 'tailwindcss';
@import '@nuxt/ui';
EOF
# Mettre à jour la config Nuxt pour activer Nuxt UI et inclure le CSS
cat > nuxt.config.ts <<'EOF'
import { defineNuxtConfig } from "nuxt/config"
export default defineNuxtConfig({
modules: ['@nuxt/ui'],
css: ['~/assets/css/main.css'],
runtimeConfig: {
openAiApiKey: '',
mcpEndpoint: '',
},
compatibilityDate: '2025-10-05',
})
EOF
# Mettre à jour la mise en page principale pour utiliser le composant UApp de Nuxt UI
cat > app/app.vue <<'EOF'
<template>
<UApp>
<NuxtPage />
</UApp>
</template>
EOF
Une fois terminé, nous pouvons démarrer le serveur de développement avec :
pnpm run dev
Tout devrait fonctionner comme prévu.
Construire l'application
Maintenant que nous avons une application Nuxt fonctionnelle avec Nuxt UI, nous pouvons utiliser ses composants pour construire l'interface utilisateur. Dans cet article, nous n'entrerons pas dans les détails de chaque composant ni ne construirons l'UI parfaite. Nous nous concentrerons plutôt sur la structure globale et les fonctionnalités de l'application.
Dans la documentation de Nuxt, il existe un exemple entièrement fonctionnel d'une page avec une interface de chat que nous pouvons utiliser. Nous n'avons pas besoin de plus pour l'implémentation d'aujourd'hui.
Avant de l'utiliser, nous devons installer l'AI SDK pour Vue.
pnpm add @ai-sdk/vue
Nous devons également installer @nuxtjs/mdc
pour parser et rendre le Markdown à la volée.
pnpm dlx nuxt module add @nuxtjs/mdc
Ensuite, nous pouvons copier‑coller l'exemple de code dans notre fichier app/pages/index.vue
:
<script setup lang="ts">
import { Chat } from '@ai-sdk/vue'
import { getTextFromMessage } from '@nuxt/ui/utils/ai'
const input = ref('')
const chat = new Chat({
onError(error) {
console.error('Chat error:', error)
}
})
function handleSubmit(e: Event) {
e.preventDefault()
chat.sendMessage({ text: input.value })
input.value = ''
}
</script>
<template>
<UDashboardPanel>
<template #body>
<UContainer>
<UChatMessages :messages="chat.messages" :status="chat.status">
<template #content="{ message }">
<MDC :value="getTextFromMessage(message)" :cache-key="message.id" unwrap="p" />
</template>
</UChatMessages>
</UContainer>
</template>
<template #footer>
<UContainer>
<UChatPrompt v-model="input" :error="chat.error" @submit="handleSubmit">
<UChatPromptSubmit :status="chat.status" @stop="chat.stop" @reload="chat.regenerate" />
</UChatPrompt>
</UContainer>
</template>
</UDashboardPanel>
</template>
Et vous savez le meilleur ? Ça marche immédiatement.
Tapez "What is 2+2" dans le champ de saisie et vous devriez voir la réponse du modèle d'IA.

Vous vous demandez peut‑être comment être sûr que le chat utilise effectivement l'outil d'addition ? Une manière de le vérifier est d'ouvrir l'inspector et de consulter l'Event Stream provenant de l'endpoint /api/chat
. Vous devriez voir quelque chose comme ceci :

C'est la même sortie que lorsque nous utilisons curl dans notre terminal.
Des explications s'imposent
Oui, nous avons construit une application IA qui utilise notre Agent IA. Mais que se passe‑t‑il réellement ici ?
D'abord, la sortie de notre API /api/chat
n'est pas la sortie brute du modèle d'IA. C'est une version modifiée pour faciliter la création d'applications IA. Cette sortie est spécialement conçue pour fonctionner avec une UI, ce qui explique pourquoi la fonction s'appelle toUIMessageStreamResponse
.
UIMessage
est l'interface qui définit la structure d'un message dans une application IA. Le frontend n'a pas besoin de transformer le flux de messages reçu, car ils sont déjà au bon format.
De plus, l'AI SDK fournit une classe appelée Chat
, conçue pour gérer l'état de l'application de chat. Elle gère automatiquement le flux de messages, ajoute les nouveaux messages et envoie des messages avec le bon format via sendMessage({ text: input.value })
. Elle gère également l'état du chat, comme le chargement et les erreurs.
Si nous nous passons de la classe Chat
et gérons tout nous‑mêmes, le code ressemblerait à ceci :
const chat = reactive({
messages: [] as UIMessage[],
status: 'idle',
error: null as Error | null
})
function sendMessage(message: UIMessage) {
chat.messages.push(message)
chat.status = 'loading'
const eventSource = new EventSource('/api/chat')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data)
chat.messages.push(data)
}
eventSource.onerror = (error) => {
chat.error = error
}
eventSource.onopen = () => {
chat.status = 'connected'
}
}
Ce n'est pas très compliqué, mais l'AI SDK fait bien plus que cela et ne pas l'utiliser pour votre prochaine application IA serait une opportunité manquée.
Par exemple, nous pouvons montrer à l'utilisateur les différents outils utilisés, leur statut et leurs entrées en temps réel.
Afficher les outils
Chaque message a la même structure, qu'il provienne de l'utilisateur ou de l'assistant. Un message est composé de "parts". Par exemple, un message utilisateur peut contenir des parts de texte et de fichier, tandis qu'un message assistant peut contenir des parts de texte, de raisonnement, d'invocation d'outil et de fichier.
Sous le capot, l'AI SDK est responsable de la gestion de ces parts lors de la réception des chunks depuis le serveur. La part texte est reçue sous forme de chunks et l'AI SDK les assemble automatiquement en un message. Pour les capacités, l'AI SDK met automatiquement à jour l'état du message, ce qui nous permet de nous concentrer sur l'UI.
Cela signifie que nous devons simplement itérer sur les messages pour les afficher. Nous pouvons donc ajuster légèrement la logique de rendu pour afficher, en plus de chaque message, les outils utilisés.
Actuellement, la logique ressemble à ceci :
<template #content="{ message }">
<MDC :value="getTextFromMessage(message)" :cache-key="message.id" unwrap="p" />
</template>
Au lieu de boucler uniquement sur les messages, nous allons aussi boucler sur les parts du message pour accéder à leur type, leur état et leur contenu.
<template #content="{ message }">
<template v-for="(part, index) in message.parts" :key="index">
<MDC v-if="part.type === 'text'" :value="part.text" :cache-key="message.id + '-' + index" unwrap="p" />
<div v-else-if="part.type === 'reasoning'"> {{ part.state === 'streaming' ? 'Thinking...' : 'Thinking complete' }} </div>
<div v-else-if="part.type === 'dynamic-tool' && part.toolName === 'addition'">
<template v-if="part.state === 'input-streaming'">
<template v-if="part.input && (part.input as { a: number, b: number }).a !== undefined && (part.input as { a: number, b: number }).b !== undefined">
Adding: {{ (part.input as { a: number, b: number }).a }} + {{ (part.input as { a: number, b: number }).b }}
</template>
<template v-else>
Adding...
</template>
</template>
<template v-else>
Addition complete: {{ (part.input as { a: number, b: number }).a }} + {{ (part.input as { a: number, b: number }).b }}
</template>
</div>
</template>
</template>
Avec cette logique de rendu, nous pouvons montrer à l'utilisateur quand le modèle réfléchit, quand un outil est utilisé, l'entrée de l'outil et, bien sûr, la réponse textuelle.
Note
Cette approche est flexible et facile à personnaliser selon vos besoins.
Au final, cela ressemble à ceci :
Le retour visuel pour l'utilisateur fait toute la différence, magnifique !
Il est maintenant temps de la mettre en ligne.
Merci de me lire ! Je m'appelle Estéban, et j'adore écrire sur le développement web et le parcours humain qui l'entoure.
Je code depuis plusieurs années maintenant, et j'apprends encore de nouvelles choses chaque jour. J'aime partager mes connaissances avec les autres, car j'aurais aimé avoir accès à des ressources aussi claires et complètes lorsque j'ai commencé à apprendre la programmation.
Si vous avez des questions ou souhaitez discuter, n'hésitez pas à commenter ci-dessous ou à me contacter sur Bluesky, X, et LinkedIn.
J'espère que vous avez apprécié cet article et appris quelque chose de nouveau. N'hésitez pas à le partager avec vos amis ou sur les réseaux sociaux, et laissez un commentaire ou une réaction ci-dessous—cela me ferait très plaisir ! Si vous souhaitez soutenir mon travail, vous pouvez me sponsoriser sur GitHub !
Discussions
Ajouter un commentaire
Vous devez être connecté pour accéder à cette fonctionnalité.