Utilisation
Le composant ChatPrompt rend un élément <form> et étend le composant Textarea afin que vous puissiez passer n'importe quelle propriété telle que icon, placeholder, autofocus, etc.
<script setup lang="ts">
import type { SelectItem } from '@nuxt/ui'
const input = ref('')
const model = ref('claude-opus-4.6')
const models = [
{ label: 'Claude Opus 4.6', value: 'claude-opus-4.6', icon: 'i-simple-icons-anthropic' },
{ label: 'Gemini 3 Pro', value: 'gemini-3-pro', icon: 'i-simple-icons-googlegemini' },
{ label: 'GPT-5', value: 'gpt-5', icon: 'i-simple-icons-openai' }
] satisfies SelectItem[]
function onSubmit() {
input.value = ''
}
</script>
<template>
<UChatPrompt v-model="input" class="w-full" @submit="onSubmit">
<template #footer>
<UButton icon="i-lucide-plus" color="neutral" variant="ghost" size="sm" />
<div class="flex items-center gap-1.5">
<USelect
v-model="model"
:items="models"
:icon="models.find(item => item.value === model)?.icon"
placeholder="Select a model"
variant="ghost"
size="sm"
square
/>
<UChatPromptSubmit size="sm" />
</div>
</template>
</UChatPrompt>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { SelectItem } from '@nuxt/ui'
const input = ref('')
const model = ref('claude-opus-4.6')
const models = [
{ label: 'Claude Opus 4.6', value: 'claude-opus-4.6', icon: 'i-simple-icons-anthropic' },
{ label: 'Gemini 3 Pro', value: 'gemini-3-pro', icon: 'i-simple-icons-googlegemini' },
{ label: 'GPT-5', value: 'gpt-5', icon: 'i-simple-icons-openai' }
] satisfies SelectItem[]
function onSubmit() {
input.value = ''
}
</script>
<template>
<UChatPrompt v-model="input" class="w-full" @submit="onSubmit">
<template #footer>
<UButton icon="i-lucide-plus" color="neutral" variant="ghost" size="sm" />
<div class="flex items-center gap-1.5">
<USelect
v-model="model"
:items="models"
:icon="models.find(item => item.value === model)?.icon"
placeholder="Select a model"
variant="ghost"
size="sm"
square
/>
<UChatPromptSubmit size="sm" />
</div>
</template>
</UChatPrompt>
</template>
- Le formulaire est soumis lorsque l'utilisateur appuie sur: kbd{value="enter"} ou lorsque l'utilisateur clique sur le bouton soumettre. Définissez la prop
submit-on-entersurfalsepour soumettre avec: kbd{value="ctrl"} +: kbd{value="enter"} (ou: kbd{value="cmd"} +: kbd{value="enter"} sur macOS) à la place, permettant à: kbd{value="enter"} d'insérer un saut de ligne. - La zone de texte est floue lorsque: kbd{value="escape"} est appuyé et émet un événement
close.
Variant
Utilisez la prop variant pour changer le style de l'invite. Defaults à outline.
<template>
<UChatPrompt variant="soft" />
</template>exemples
Avec un éditeur: badge{label="4.10+" class="align-text-top"}
Composez les emplacements #header, #body et #footer pour créer une invite riche: pièces jointes, un Editor avec des mentions @ et des commandes / via xEditorMentionMenu](xph051), et un sélecteur de mode.
<script setup lang="ts">
import { Extension } from '@tiptap/core'
import type { EditorMentionMenuItem, SelectItem } from '@nuxt/ui'
const input = ref('')
const mode = ref('auto')
const attachments = ref<{ name: string, src?: string }[]>([])
const fileInputRef = useTemplateRef('fileInputRef')
const files: EditorMentionMenuItem[] = [
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' },
{ label: 'package.json', icon: 'i-vscode-icons-file-type-json' },
{ label: 'README.md', icon: 'i-vscode-icons-file-type-markdown' },
{ label: 'AuthForm.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'useChat.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
const commands: EditorMentionMenuItem[] = [
{ label: 'init', icon: 'i-lucide-sparkles' },
{ label: 'review', icon: 'i-lucide-search-code' },
{ label: 'security-review', icon: 'i-lucide-shield-check' },
{ label: 'clear', icon: 'i-lucide-eraser' },
{ label: 'compact', icon: 'i-lucide-fold-vertical' },
{ label: 'config', icon: 'i-lucide-settings' }
]
const modes = [
{ label: 'Manual', value: 'manual', icon: 'i-lucide-hand' },
{ label: 'Edit automatically', value: 'edit', icon: 'i-lucide-code-xml' },
{ label: 'Plan mode', value: 'plan', icon: 'i-lucide-list-checks' },
{ label: 'Auto mode', value: 'auto', icon: 'i-lucide-zap' }
] satisfies SelectItem[]
// SSR-safe target so the menus aren't clipped by overflow
const appendToBody = false ? () => document.body : undefined
function onFilesChange(event: Event) {
const target = event.target as HTMLInputElement
for (const file of Array.from(target.files ?? [])) {
attachments.value.push({
name: file.name,
src: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined
})
}
target.value = ''
}
function onSubmit() {
console.log('submit', input.value)
input.value = ''
attachments.value = []
}
</script>
<template>
<UChatPrompt
v-model="input"
class="w-full p-0 gap-0"
placeholder="Press / to open the command menu"
:ui="{
header: 'px-2.5 py-2 border-b border-default',
footer: 'px-2.5 py-2 border-t border-default'
}"
@submit="onSubmit"
>
<template v-if="attachments.length" #header>
<UButton
v-for="(file, index) in attachments"
:key="index"
:label="file.name"
:avatar="file.src ? { src: file.src } : undefined"
:icon="file.src ? undefined : 'i-lucide-file'"
color="neutral"
variant="soft"
size="xs"
square
trailing-icon="i-lucide-x"
@click="attachments.splice(index, 1)"
/>
</template>
<template #body="{ submit, placeholder }">
<UEditor
v-slot="{ editor }"
v-model="input"
content-type="markdown"
:starter-kit="false"
:placeholder="placeholder"
class="w-full min-h-12"
:ui="{ base: 'p-2.5! [&_.mention]:bg-primary/5 [&_.mention]:rounded-sm [&_.mention]:px-0.5 [&_.mention]:py-0.25' }"
:extensions="[Extension.create({
name: 'chatPromptSubmit',
priority: 1000,
addKeyboardShortcuts: () => ({ Enter: () => (submit(), true) })
})]"
>
<UEditorMentionMenu :editor="editor" char="@" plugin-key="mention" :items="files" :append-to="appendToBody" />
<UEditorMentionMenu :editor="editor" char="/" plugin-key="command" :items="commands" :append-to="appendToBody" />
</UEditor>
</template>
<template #footer>
<div class="flex items-center gap-0.5">
<UButton
icon="i-lucide-plus"
color="neutral"
variant="ghost"
aria-label="Attach files"
size="sm"
@click="fileInputRef?.click()"
/>
<input ref="fileInputRef" type="file" multiple class="hidden" @change="onFilesChange">
</div>
<div class="flex items-center gap-1">
<USelect
v-model="mode"
:items="modes"
:icon="modes.find(item => item.value === mode)?.icon"
color="neutral"
variant="ghost"
size="sm"
square
/>
<UChatPromptSubmit size="sm" :disabled="!input.trim()" />
</div>
</template>
</UChatPrompt>
</template>
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { Extension } from '@tiptap/core'
import type { EditorMentionMenuItem, SelectItem } from '@nuxt/ui'
const input = ref('')
const mode = ref('auto')
const attachments = ref<{ name: string, src?: string }[]>([])
const fileInputRef = useTemplateRef('fileInputRef')
const files: EditorMentionMenuItem[] = [
{ label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' },
{ label: 'package.json', icon: 'i-vscode-icons-file-type-json' },
{ label: 'README.md', icon: 'i-vscode-icons-file-type-markdown' },
{ label: 'AuthForm.vue', icon: 'i-vscode-icons-file-type-vue' },
{ label: 'useChat.ts', icon: 'i-vscode-icons-file-type-typescript' }
]
const commands: EditorMentionMenuItem[] = [
{ label: 'init', icon: 'i-lucide-sparkles' },
{ label: 'review', icon: 'i-lucide-search-code' },
{ label: 'security-review', icon: 'i-lucide-shield-check' },
{ label: 'clear', icon: 'i-lucide-eraser' },
{ label: 'compact', icon: 'i-lucide-fold-vertical' },
{ label: 'config', icon: 'i-lucide-settings' }
]
const modes = [
{ label: 'Manual', value: 'manual', icon: 'i-lucide-hand' },
{ label: 'Edit automatically', value: 'edit', icon: 'i-lucide-code-xml' },
{ label: 'Plan mode', value: 'plan', icon: 'i-lucide-list-checks' },
{ label: 'Auto mode', value: 'auto', icon: 'i-lucide-zap' }
] satisfies SelectItem[]
// SSR-safe target so the menus aren't clipped by overflow
const appendToBody = false ? () => document.body : undefined
function onFilesChange(event: Event) {
const target = event.target as HTMLInputElement
for (const file of Array.from(target.files ?? [])) {
attachments.value.push({
name: file.name,
src: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined
})
}
target.value = ''
}
function onSubmit() {
console.log('submit', input.value)
input.value = ''
attachments.value = []
}
</script>
<template>
<UChatPrompt
v-model="input"
class="w-full p-0 gap-0"
placeholder="Press / to open the command menu"
:ui="{
header: 'px-2.5 py-2 border-b border-default',
footer: 'px-2.5 py-2 border-t border-default'
}"
@submit="onSubmit"
>
<template v-if="attachments.length" #header>
<UButton
v-for="(file, index) in attachments"
:key="index"
:label="file.name"
:avatar="file.src ? { src: file.src } : undefined"
:icon="file.src ? undefined : 'i-lucide-file'"
color="neutral"
variant="soft"
size="xs"
square
trailing-icon="i-lucide-x"
@click="attachments.splice(index, 1)"
/>
</template>
<template #body="{ submit, placeholder }">
<UEditor
v-slot="{ editor }"
v-model="input"
content-type="markdown"
:starter-kit="false"
:placeholder="placeholder"
class="w-full min-h-12"
:ui="{ base: 'p-2.5! [&_.mention]:bg-primary/5 [&_.mention]:rounded-sm [&_.mention]:px-0.5 [&_.mention]:py-0.25' }"
:extensions="[Extension.create({
name: 'chatPromptSubmit',
priority: 1000,
addKeyboardShortcuts: () => ({ Enter: () => (submit(), true) })
})]"
>
<UEditorMentionMenu :editor="editor" char="@" plugin-key="mention" :items="files" :append-to="appendToBody" />
<UEditorMentionMenu :editor="editor" char="/" plugin-key="command" :items="commands" :append-to="appendToBody" />
</UEditor>
</template>
<template #footer>
<div class="flex items-center gap-0.5">
<UButton
icon="i-lucide-plus"
color="neutral"
variant="ghost"
aria-label="Attach files"
size="sm"
@click="fileInputRef?.click()"
/>
<input ref="fileInputRef" type="file" multiple class="hidden" @change="onFilesChange">
</div>
<div class="flex items-center gap-1">
<USelect
v-model="mode"
:items="modes"
:icon="modes.find(item => item.value === mode)?.icon"
color="neutral"
variant="ghost"
size="sm"
square
/>
<UChatPromptSubmit size="sm" :disabled="!input.trim()" />
</div>
</template>
</UChatPrompt>
</template>
#body remplace le textarea interne et expose les gestionnaires submit et close, de sorte que vous pouvez câbler les raccourcis clavier de l'éditeur au formulaire. Quand un menu de mention est ouvert, appuyez sur: kbd{value="enter"} pour sélectionner l'élément en surbrillance au lieu de soumettre.As page d'accueil
Vous pouvez également l'utiliser dans votre page d'accueil de l'interface de chat.
<script setup lang="ts">
import { useChat } from '@ai-sdk/vue'
const input = ref('')
const { messages, status, sendMessage } = useChat()
async function onSubmit() {
sendMessage({ text: input.value })
// Navigate to chat page after first message
if (messages.value.length === 1) {
await navigateTo('/chat')
}
}
</script>
<template>
<UDashboardPanel>
<template #body>
<UContainer>
<h1>How can I help you today?</h1>
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="status" />
</UChatPrompt>
</UContainer>
</template>
</UDashboardPanel>
</template>
api
Props équipement
| Prop | Default | Type |
|---|---|---|
as | 'form' | anyThe element or component this component should render as. |
placeholder | t('chatPrompt.placeholder') | stringThe placeholder text for the textarea. |
color | 'primary' | "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral" |
variant | 'outline' | "outline" | "soft" | "subtle" | "naked" |
submitOnEnter | true | boolean When |
error | Error | |
rows | 1 | number |
autofocus | true | boolean |
autofocusDelay | number | |
autoresize | true | boolean |
autoresizeDelay | number | |
maxrows | number | |
icon | anyDisplay an icon based on the | |
avatar | AvatarPropsDisplay an avatar on the left side. | |
loading | boolean When | |
loadingIcon | appConfig.ui.icons.loading | anyThe icon when the |
disabled | boolean | |
modelValue | '' | string |
ui | { root?: SlotClass; header?: SlotClass; body?: SlotClass; footer?: SlotClass; base?: SlotClass; } & { root?: SlotClass; base?: SlotClass; leading?: SlotClass; leadingIcon?: SlotClass; leadingAvatar?: SlotClass; leadingAvatarSize?: SlotClass; trailing?: SlotClass; trailingIcon?: SlotClass; } |
Slots
| Slot | Type |
|---|---|
header | {} |
footer | {} |
body | { submit: (event?: Event | undefined) => void; close: (event?: Event | undefined) => void; placeholder: string; disabled: boolean; ui: object; }Replace the internal textarea, e.g. to render an Editor with mentions. |
leading | { ui: { root: (props?: Record<string, any> | undefined) => string; base: (props?: Record<string, any> | undefined) => string; leading: (props?: Record<string, any> | undefined) => string; leadingIcon: (props?: Record<string, any> | undefined) => string; leadingAvatar: (props?: Record<string, any> | undefined) => string; leadingAvatarSize: (props?: Record<string, any> | undefined) => string; trailing: (props?: Record<string, any> | undefined) => string; trailingIcon: (props?: Record<string, any> | undefined) => string; }; } |
default | { ui: { root: (props?: Record<string, any> | undefined) => string; base: (props?: Record<string, any> | undefined) => string; leading: (props?: Record<string, any> | undefined) => string; leadingIcon: (props?: Record<string, any> | undefined) => string; leadingAvatar: (props?: Record<string, any> | undefined) => string; leadingAvatarSize: (props?: Record<string, any> | undefined) => string; trailing: (props?: Record<string, any> | undefined) => string; trailingIcon: (props?: Record<string, any> | undefined) => string; }; } |
trailing | { ui: { root: (props?: Record<string, any> | undefined) => string; base: (props?: Record<string, any> | undefined) => string; leading: (props?: Record<string, any> | undefined) => string; leadingIcon: (props?: Record<string, any> | undefined) => string; leadingAvatar: (props?: Record<string, any> | undefined) => string; leadingAvatarSize: (props?: Record<string, any> | undefined) => string; trailing: (props?: Record<string, any> | undefined) => string; trailingIcon: (props?: Record<string, any> | undefined) => string; }; } |
Emis
| Event | Type |
|---|---|
close | [event: Event] |
submit | [event: Event] |
update:modelValue | [value: string] |
expose
Lorsque vous accédez au composant via une référence de modèle, vous pouvez utiliser les éléments suivants:
| nom | type |
|---|---|
textareaRef | Ref<HTMLTextAreaElement | null>x{lang="ts-type"} |
Theme
export default defineAppConfig({
ui: {
chatPrompt: {
slots: {
root: 'relative flex flex-col items-stretch gap-2 px-2.5 py-2 w-full rounded-lg backdrop-blur-sm',
header: 'flex items-center gap-1.5',
body: 'items-start gap-1.5',
footer: 'flex items-center justify-between gap-1.5',
base: 'px-0'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
variant: {
outline: {
root: 'bg-default/75 ring ring-default'
},
soft: {
root: 'bg-elevated/50'
},
subtle: {
root: 'bg-elevated/50 ring ring-default'
},
naked: {
root: ''
}
}
},
compoundVariants: [
{
color: 'primary',
variant: [
'outline',
'subtle'
],
class: {
root: 'outline-primary/25 has-[textarea:focus-visible]:outline-3 has-[textarea:focus-visible]:ring-primary has-[[contenteditable]:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:ring-primary'
}
},
{
color: 'primary',
variant: 'soft',
class: {
root: 'outline-primary/25 has-[textarea:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:outline-3'
}
},
{
color: 'neutral',
variant: [
'outline',
'subtle'
],
class: {
root: 'outline-inverted/25 has-[textarea:focus-visible]:outline-3 has-[textarea:focus-visible]:ring-inverted has-[[contenteditable]:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:ring-inverted'
}
},
{
color: 'neutral',
variant: 'soft',
class: {
root: 'outline-inverted/25 has-[textarea:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:outline-3'
}
}
],
defaultVariants: {
color: 'primary',
variant: 'outline'
}
}
}
})import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from '@nuxt/ui/vite'
export default defineConfig({
plugins: [
vue(),
ui({
ui: {
chatPrompt: {
slots: {
root: 'relative flex flex-col items-stretch gap-2 px-2.5 py-2 w-full rounded-lg backdrop-blur-sm',
header: 'flex items-center gap-1.5',
body: 'items-start gap-1.5',
footer: 'flex items-center justify-between gap-1.5',
base: 'px-0'
},
variants: {
color: {
primary: '',
secondary: '',
success: '',
info: '',
warning: '',
error: '',
neutral: ''
},
variant: {
outline: {
root: 'bg-default/75 ring ring-default'
},
soft: {
root: 'bg-elevated/50'
},
subtle: {
root: 'bg-elevated/50 ring ring-default'
},
naked: {
root: ''
}
}
},
compoundVariants: [
{
color: 'primary',
variant: [
'outline',
'subtle'
],
class: {
root: 'outline-primary/25 has-[textarea:focus-visible]:outline-3 has-[textarea:focus-visible]:ring-primary has-[[contenteditable]:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:ring-primary'
}
},
{
color: 'primary',
variant: 'soft',
class: {
root: 'outline-primary/25 has-[textarea:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:outline-3'
}
},
{
color: 'neutral',
variant: [
'outline',
'subtle'
],
class: {
root: 'outline-inverted/25 has-[textarea:focus-visible]:outline-3 has-[textarea:focus-visible]:ring-inverted has-[[contenteditable]:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:ring-inverted'
}
},
{
color: 'neutral',
variant: 'soft',
class: {
root: 'outline-inverted/25 has-[textarea:focus-visible]:outline-3 has-[[contenteditable]:focus-visible]:outline-3'
}
}
],
defaultVariants: {
color: 'primary',
variant: 'outline'
}
}
}
})
]
})