Skip to content

Add an AI chat to a Vue app

Install @aparte/vue, render <AparteChat>, stream a model with useAparteClient, and put your own UI in its slots — in a Vue app, Nuxt included.

On this page
bash
npm install @aparte/vue @aparte/core
npm install @aparte/provider-scenario   # a scripted model, to stream with no key
ts
// vite.config.ts — the aparte-* tags are custom elements, not Vue components
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('aparte-') } } })],
});

@aparte/core and vue are peer dependencies: Vue ^3.5.0. The isCustomElement setting keeps Vue from resolving an aparte-* tag as a component of yours; under Nuxt it goes in nuxt.config.ts, as vue.compilerOptions.isCustomElement.

vue
<script setup lang="ts">
import { AparteChat, useAparteChat } from '@aparte/vue';
import '@aparte/core/styles.css';

const chat = useAparteChat();
</script>

<template>
  <AparteChat
    :ref="chat.chatRef"
    :messages="chat.messages.value"
    center-when-empty
    @messages-change="chat.onMessagesChange"
  >
    <template #empty-state><p>Ask me anything…</p></template>
  </AparteChat>
</template>

useAparteChat holds the messages and the component’s ref: bind both, or bind v-model:messages to a ref of yours. The person’s message is added when they send, so do not add it yourself; @message-sent fires after, for a side effect.

ts
// main.ts — registered once, before the app mounts
import { createApp } from 'vue';
import { aparteGlobalConfig } from '@aparte/core';
import { createScenarioProvider } from '@aparte/provider-scenario';
import { showcase } from '@aparte/provider-scenario/showcase';
import App from './App.vue';

aparteGlobalConfig.registerAIProvider(createScenarioProvider({ scenarios: showcase }));
createApp(App).mount('#app');
vue
<script setup lang="ts">
import { AparteChat, useAparteChat, useAparteClient } from '@aparte/vue';

const chat = useAparteChat();
useAparteClient();
</script>

<template>
  <AparteChat :ref="chat.chatRef" :messages="chat.messages.value" @messages-change="chat.onMessagesChange" />
</template>

useAparteClient starts a client when the component mounts, stops it before it unmounts, and returns { client, abort }; it takes the AparteClientOptions. For a real model, register its provider, name the model and give the key — aparteGlobalConfig.setModelConfig({ defaultProvider: 'openrouter', defaultModel: 'openai/gpt-4o-mini' }) and useAparteClient({ keyResolver }) (Connect a model). A config prop gives one <AparteChat> providers and a transport of its own; give its client the same one, useAparteClient({ config }). With the client running, turn on retry and edit with aparteGlobalConfig.setBubbleActions({ retry: true, edit: true }) (Buttons).

vue
<template>
  <AparteChat :ref="chat.chatRef" :messages="chat.messages.value" @messages-change="chat.onMessagesChange">
    <template #empty-state><p>Ask me anything…</p></template>
    <template #toolbar><span class="hint">Shift + Enter for a new line</span></template>
    <template #bubble="{ message }"><MyBubble :message="message" /></template>
  </AparteChat>
</template>
SlotWhat it fills
empty-statethe chat before the first message
above-composerthe space above the composer
toolbarthe composer’s bottom row, not drawn when you fill composer (The composer toolbar)
composerthe composer, replaced by yours
bubble, with { message }each message, drawn by you (Bubble)

Content outside these slots is not drawn.

vue
<AparteChat id="support" class="flex-1 min-h-0" :loading="isFetchingHistory" attachments />

The ones you reach for: placeholder, disabled and submit-on-enter; center-when-empty; loading, for a wait of your own; is-typing and typing-text, a waiting line you drive yourself; overlay-composer; attachments, off by default (Attachments); :elicitation="false" when you draw the questions yourself; conversation-id, config and layout-transition-ms, how long the list holds its layout after a conversation swap. class and style fall through to the root. id is the chat’s id, which every event carries: for two chats on one page, each answered by its own client, give each an id and its client scopeToTargetId with it. The events — @message-sent, @action, @messages-change (and update:messages, for v-model), @message-appended, @typing-change and @conversation-created — hand you the payload itself (Wrapper surface).

chat.chatRef, and the methods useAparteChat returns beside it, are the chat’s imperative API: appendMessage, addSegment, injectTokenStream, scrollToBottom, focusInput and the rest, to drive the chat from a loop of yours (Your own loop). useConversationManager() lists, creates, archives and pins saved conversations once you call init(adapter) with your storage adapter — and the chat’s config as second argument, if it has one — and the chat switches between them through conversation-id (Save conversations).

vue
<template>
  <aparte-select searchable="" placeholder="Pick a model" @aparte-select-change="(e) => (model = e.detail.value)">
    <aparte-option value="gpt-4o-mini">GPT-4o mini</aparte-option>
  </aparte-select>
</template>

Importing from @aparte/vue declares every element of core in GlobalComponents, so vue-tsc checks its attributes in any template: a presence attribute takes '' to be on and null to be off (Elements, typed). A plugin’s element is typed by the plugin, and one of yours by you (your own element). <AparteUi> mounts any tag and sets its props; it forwards core’s events, so name your element’s own in events:

vue
<template>
  <AparteUi name="my-token-counter" :props="{ 'data-budget': '8000' }" :events="['token-count']" @element-event="(e) => (count = e.detail)" />
</template>

The wrapper has no server guard: under Nuxt, render the chat on the client only, in <ClientOnly> or a .client.vue component. @aparte/core itself imports on a server through its entry without elements (On the server). Vitest runs on Node, so alias @aparte/core to @aparte/core/browser for the elements to upgrade (Testing your components).

A whole chat site in Vue — the conversation sidebar, the header, settings, tools and the scripted model — is the Vue example.