Chatrápido

Una Textarea mejorada para enviar mensajes en las interfaces de chat de IA.

xph0000xUso

El componente ChatPrompt procesa un elemento <form> y extiende el componente Textarea para que pueda pasar cualquier propiedad como 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>
El ChatPrompt maneja los siguientes eventos:
  • El formulario se envía cuando el usuario presiona: kbd{value="enter"} o cuando el usuario hace clic en el botón enviar. Establezca el prop submit-on-enter en false para enviar con: kbd{value="ctrl"} +: kbd{value="enter"} (o: kbd{value="cmd"} +: kbd{value="enter"} en macOS) en su lugar, permitiendo que: kbd{value="enter"} inserte una nueva línea.
  • El área de texto se difumina cuando: kbd{value="escape"} se presiona y emite un evento close.

Variante

Utilice el prop variant para cambiar el estilo del prompt. Defaults a outline.

<template>
  <UChatPrompt variant="soft" />
</template>

Ejemplos

Consulte la página de descripción general de Chat para obtener instrucciones de instalación, configuración del servidor y ejemplos de uso.

Con un Editor: badge{label="4.10+" class="align-text-top"}

Componga las ranuras #header, #body y #footer para crear un prompt rico: archivos adjuntos, un Editor con menciones @ y comandos / a través de EditorMentionMenu, y un selector de modo.

<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>
La ranura #body reemplaza el área de texto interna y expone los manejadores submit y close, de modo que puede conectar los atajos de teclado del editor al formulario. Cuando se abre un menú de mención, presionando: kbd{value="enter"} selecciona el elemento resaltado en lugar de enviarlo.

Página de inicio ### As

También puede usarlo en la página de inicio de la interfaz de chat.

pages/index.vue
<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 (Edición española)

Accesorios

Prop Default Type
as
'form'
any

The element or component this component should render as.

placeholder
t('chatPrompt.placeholder')
string

The placeholder text for the textarea.

color
'primary'
"error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"
variant
'outline'
"outline" | "soft" | "subtle" | "naked"
submitOnEnter
true
boolean

When true, pressing Enter submits and Shift+Enter inserts a newline. When false, pressing Enter inserts a newline and Ctrl+Enter / Cmd+Enter submits.

error
Error
rows
1
number
autofocus
true
boolean
autofocusDelay
number
autoresize
true
boolean
autoresizeDelay
number
maxrows
number
icon
any

Display an icon based on the leading and trailing props.

avatar
AvatarProps

Display an avatar on the left side.

loading
boolean

When true, the loading icon will be displayed.

loadingIcon
appConfig.ui.icons.loading
any

The icon when the loading prop is true.

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; }
Este componente también admite todos los atributos HTML nativos de <textarea>.

Slots (Edición española)

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; }; }

Emisiones

Event Type
close
[event: Event]
submit
[event: Event]
update:modelValue
[value: string]

Expose

Al acceder al componente a través de una referencia de plantilla, puede utilizar lo siguiente:

NombreTipo
textareaRef (Edición española)Ref<HTMLTextAreaElement | null>x{lang="ts-type"} (Edición española)

Temas

app.config.ts
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'
      }
    }
  }
})
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: {
        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'
          }
        }
      }
    })
  ]
})
Some colors in compoundVariants are omitted for readability. Check out the source code on GitHub.

Changelog (Edición española)

No recent changes