Chatière
Une palette de chat pour créer une interface de chatbot à l
Utilisation
Le composant ChatPalette est un enveloppeur de mise en page structuré qui organise ChatMessages dans une zone de contenu déroulable et ChatPrompt dans une section inférieure fixe, créant ainsi des interfaces chatbot cohérentes pour les modaux, les diapositives ou les tiroirs.
<template>
<UChatPalette>
<UChatMessages />
<template #prompt>
<UChatPrompt />
</template>
</UChatPalette>
</template>
Exemples
Consultez la page d'aperçu Chat pour les instructions d'installation, la configuration du serveur et les exemples d'utilisation.
Dans un modal
Vous pouvez utiliser le composant ChatPalette à l'intérieur du contenu d'un Modal.
<script setup lang="ts">
import { isTextUIPart } from 'ai'
import type { UIMessage } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming } from '@nuxt/ui/utils/ai'
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import security from '@comark/vue/plugins/security'
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })]
const initialMessages: UIMessage[] = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'What is Nuxt UI?' }]
}, {
id: '2',
role: 'assistant',
parts: [{ type: 'text', text: 'Nuxt UI is a Vue component library built on Reka UI, Tailwind CSS, and Tailwind Variants. It provides 125+ accessible components for building modern web apps.' }]
}]
const input = ref('')
const { messages, status, error, sendMessage } = useChat({
messages: initialMessages
})
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' }
}
}
</script>
<template>
<UModal open :ui="{ content: 'sm:max-w-3xl sm:h-[28rem]' }">
<template #content>
<UTheme :ui="ui">
<UChatPalette>
<UChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap leading-6">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
<template #prompt>
<UChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
/>
</template>
</UChatPalette>
</UTheme>
</template>
</UModal>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { isTextUIPart } from 'ai'
import type { UIMessage } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming } from '@nuxt/ui/utils/ai'
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import security from '@comark/vue/plugins/security'
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })]
const initialMessages: UIMessage[] = [{
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'What is Nuxt UI?' }]
}, {
id: '2',
role: 'assistant',
parts: [{ type: 'text', text: 'Nuxt UI is a Vue component library built on Reka UI, Tailwind CSS, and Tailwind Variants. It provides 125+ accessible components for building modern web apps.' }]
}]
const input = ref('')
const { messages, status, error, sendMessage } = useChat({
messages: initialMessages
})
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' }
}
}
</script>
<template>
<UModal open :ui="{ content: 'sm:max-w-3xl sm:h-[28rem]' }">
<template #content>
<UTheme :ui="ui">
<UChatPalette>
<UChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap leading-6">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
<template #prompt>
<UChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
/>
</template>
</UChatPalette>
</UTheme>
</template>
</UModal>
</template>
Dans ContentSearch
Vous pouvez utiliser le composant ChatPalette conditionnellement à l'intérieur du contenu de ContentSearch pour afficher une interface de chatbot lorsqu 'un utilisateur sélectionne un élément.
<script setup lang="ts">
import { isTextUIPart } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming } from '@nuxt/ui/utils/ai'
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import security from '@comark/vue/plugins/security'
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })]
const input = ref('')
const { messages, status, error, sendMessage, regenerate } = useChat()
const groups = computed(() => [{
id: 'ai',
ignoreFilter: true,
items: [{
label: searchTerm.value ? `Ask AI for “${searchTerm.value}”` : 'Ask AI',
icon: 'i-lucide-bot',
onSelect: (e: any) => {
e.preventDefault()
ai.value = true
if (searchTerm.value) {
messages.value = [...messages.value, {
id: '1',
role: 'user',
parts: [{ type: 'text', text: searchTerm.value }]
}]
regenerate()
}
}
}]
}])
const ai = ref(false)
const searchTerm = ref('')
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
function onClose(e: Event) {
e.preventDefault()
ai.value = false
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' }
}
}
</script>
<template>
<UContentSearch v-model:search-term="searchTerm" open :groups="groups">
<template v-if="ai" #content>
<UTheme :ui="ui">
<UChatPalette>
<UChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap leading-6">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
<template #prompt>
<UChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
@close="onClose"
/>
</template>
</UChatPalette>
</UTheme>
</template>
</UContentSearch>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { isTextUIPart } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming } from '@nuxt/ui/utils/ai'
import { Markdown } from '@comark/vue'
import shiki from '@comark/vue/plugins/shiki'
import security from '@comark/vue/plugins/security'
const plugins = [shiki(), security({ blockedTags: ['script', 'style', 'iframe', 'object', 'embed', 'form'] })]
const input = ref('')
const { messages, status, error, sendMessage, regenerate } = useChat()
const groups = computed(() => [{
id: 'ai',
ignoreFilter: true,
items: [{
label: searchTerm.value ? `Ask AI for “${searchTerm.value}”` : 'Ask AI',
icon: 'i-lucide-bot',
onSelect: (e: any) => {
e.preventDefault()
ai.value = true
if (searchTerm.value) {
messages.value = [...messages.value, {
id: '1',
role: 'user',
parts: [{ type: 'text', text: searchTerm.value }]
}]
regenerate()
}
}
}]
}])
const ai = ref(false)
const searchTerm = ref('')
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
function onClose(e: Event) {
e.preventDefault()
ai.value = false
}
const ui = {
prose: {
p: { base: 'my-2 leading-6' },
li: { base: 'my-0.5 leading-6' },
ul: { base: 'my-2' },
ol: { base: 'my-2' },
h1: { base: 'text-xl my-2' },
h2: { base: 'text-lg my-2' },
h3: { base: 'text-base my-2' },
h4: { base: 'text-sm my-2' },
pre: { root: 'my-2' },
table: { root: 'my-2' },
hr: { base: 'my-2' }
}
}
</script>
<template>
<UContentSearch v-model:search-term="searchTerm" open :groups="groups">
<template v-if="ai" #content>
<UTheme :ui="ui">
<UChatPalette>
<UChatMessages
:messages="messages"
:status="status"
:user="{ side: 'left', variant: 'naked', avatar: { src: 'https://github.com/benjamincanac.png', loading: 'lazy' as const } }"
:assistant="{ icon: 'i-lucide-bot' }"
>
<template #content="{ message }">
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
<template v-if="isTextUIPart(part)">
<Markdown
v-if="message.role === 'assistant'"
:value="part.text"
:streaming="isPartStreaming(part)"
:plugins="plugins"
class="*:first:mt-0 *:last:mb-0"
/>
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap leading-6">
{{ part.text }}
</p>
</template>
</template>
</template>
</UChatMessages>
<template #prompt>
<UChatPrompt
v-model="input"
icon="i-lucide-search"
variant="naked"
:error="error"
@submit="onSubmit"
@close="onClose"
/>
</template>
</UChatPalette>
</UTheme>
</template>
</UContentSearch>
</template>
api
Props
| Prop | Default | Type |
|---|---|---|
as | 'div' | anyThe element or component this component should render as. |
ui | { root?: SlotClass; prompt?: SlotClass; close?: SlotClass; content?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
default | {} |
prompt | {} |
Thème
app.config.ts
export default defineAppConfig({
ui: {
chatPalette: {
slots: {
root: 'relative flex-1 flex flex-col min-h-0 min-w-0',
prompt: 'px-0 rounded-t-none border-t border-default',
close: '',
content: 'overflow-y-auto flex-1 flex flex-col py-3'
}
}
}
})vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
chatPalette: {
slots: {
root: 'relative flex-1 flex flex-col min-h-0 min-w-0',
prompt: 'px-0 rounded-t-none border-t border-default',
close: '',
content: 'overflow-y-auto flex-1 flex flex-col py-3'
}
}
}
})
]
})Changelog
No recent changes