Chat UI
Полноценный интерфейс чата, код которого принадлежит вам. Компоненты добавляются в проект через shadcn-vue CLI как обычные .vue-файлы, так что их можно перестилизовать или переписать, как любой другой компонент. Поведение, в котором легко ошибиться, поставляется в пакете vue-llm и обновляется вместе с ним.
Демо использует заскриптованную модель. Компоненты — те же, что устанавливает CLI.
Как всё разделено
| Часть | Где находится | Обновления |
|---|---|---|
useChat, провайдеры, StreamMarkdown | пакет vue-llm | npm update vue-llm |
usePromptInput, useAutoScroll | пакет vue-llm | npm update vue-llm |
Chat, ChatMessage, ChatPromptInput, ChatReasoning, ChatToolCall | копируются в ваш проект | ваш код; запустите add ещё раз, чтобы подтянуть изменения из апстрима |
Установка
Нужен проект с shadcn-vue, то есть с Tailwind CSS и lib/utils.ts.
bash
npx shadcn-vue@latest add https://vue-llm.js.org/r/chat.jsonCLI устанавливает vue-llm и вашу библиотеку иконок (по умолчанию Lucide; другие библиотеки иконок CLI сопоставляет сам) и записывает компоненты в components/ui/chat/.
Использование
vue
<script setup lang="ts">
import { useChat } from 'vue-llm'
import { createSSEProvider } from 'vue-llm/providers'
import { Chat } from '@/components/ui/chat'
const chat = useChat({
provider: createSSEProvider({ url: '/api/chat' }),
model: 'gpt-4o',
})
</script>
<template>
<div class="h-dvh">
<Chat :chat="chat" :suggestions="['Summarize this page', 'Write a haiku']" />
</div>
</template>Chat заполняет родителя целиком, поэтому задайте родителю высоту.
Generative UI
Передайте компоненты для своих инструментов. Инструменты без компонента используют ChatToolCall — сворачиваемую карточку с аргументами и результатом.
vue
<Chat :chat="chat" :components="{ get_weather: WeatherCard, confirm_booking: BookingCard }" />Как писать компоненты инструментов, см. в разделе Generative UI.
Компоненты
Chat
Чат целиком: список сообщений с автопрокруткой, пустое состояние с подсказками, индикатор набора, ошибка с кнопкой повтора, кнопка прокрутки вниз и поле ввода.
| Проп | Тип | Описание |
|---|---|---|
chat | UseChatReturn | Значение, возвращённое useChat |
components | Record<string, Component> | Имя инструмента → компонент |
suggestions | string[] | Подсказки, которые показываются, пока чат пуст |
placeholder | string | Плейсхолдер поля ввода |
accept | string | false | Допустимые вложения; false скрывает кнопку прикрепления |
Слот #empty заменяет заголовок пустого состояния.
ChatMessage
Одно сообщение: Markdown, рассуждения, вызовы инструментов, изображения и файлы, источники, а также действия «скопировать» и «сгенерировать заново». Генерирует события tool-result и regenerate.
ChatPromptInput
Авторастущий textarea с вложениями (выбор файла, вставка и drag-and-drop) и кнопками отправки и остановки. Принимает chat, placeholder, accept и maxFiles.
ChatReasoning
Сворачиваемые рассуждения. Блок раскрывается, пока модель думает, и сворачивается, когда начинается ответ.
ChatToolCall
Запасная карточка для вызовов инструментов: состояние и сворачиваемые аргументы и результат.
Темы
Компоненты используют только токены темы shadcn: background, foreground, primary, muted, border, ring, card и destructive. Они следуют вашей теме и тёмному режиму. Попробуйте Vue green в демо выше.
Без Tailwind
Не используйте реестр. Соберите интерфейс на headless-композаблах — в них заключена вся сложная часть компонентов выше:
vue
<script setup lang="ts">
import { useAutoScroll, useChat, usePromptInput } from 'vue-llm'
const chat = useChat({ provider, model: 'gpt-4o' })
const { input, messages } = chat
const prompt = usePromptInput(chat)
const { files, canSubmit, submit, removeFile, handlers, dropzone } = prompt
const scroll = useAutoScroll()
const { isAtBottom, scrollToBottom } = scroll
</script>
<template>
<div :ref="scroll.container" class="messages">
<MyMessage v-for="m in messages" :key="m.id" :message="m" />
</div>
<button v-if="!isAtBottom" @click="scrollToBottom('smooth')">↓</button>
<form v-bind="dropzone" @submit.prevent="submit">
<span v-for="f in files" :key="f.name">{{ f.name }} <button type="button" @click="removeFile(f)">×</button></span>
<textarea :ref="prompt.textarea" v-model="input" rows="1" v-bind="handlers" />
<button :disabled="!canSubmit">Send</button>
</form>
</template>Привязывайте template ref-ы через :ref="scroll.container", а не деструктурируя container. vue-tsc с noUnusedLocals, который включён в шаблонах Vite, не считает строковый ref="container" использованием переменной.
usePromptInput(chat, options?)
- Enter отправляет сообщение, Shift+Enter добавляет перенос строки. Enter, которым подтверждается ввод через IME (китайский, японский, корейский), никогда не отправляет сообщение.
- Textarea растёт вместе с содержимым до
maxRowsстрок (по умолчанию8). - Файлы прикрепляются через
addFiles(для file input), вставкой и перетаскиванием на элементdropzone. С помощьюisDraggingможно показать оверлей для сброса файлов. acceptиmaxFilesфильтруют вложения, аonReject(files, reason)сообщает об отклонённых.submit()отправляет текст и файлы и очищает их.canSubmitравен false, пока чат занят.
На Vue 2.7 привязывайте обработчики явно (@keydown="handlers.onKeydown" @paste="handlers.onPaste"), потому что там v-bind не навешивает слушатели.
useAutoScroll(options?)
- Держит список прижатым к низу, пока стримятся сообщения и пока растут изображения или блоки кода.
- Перестаёт следовать, когда пользователь прокручивает вверх, чтобы он мог спокойно читать.
isAtBottomстановитсяfalse. scrollToBottom('smooth')снова прижимает список к низу.threshold(по умолчанию48px) задаёт, насколько близко к низу ещё считается «внизу».