Vue 3 AI chat component, with composables — @aparte/vue
@aparte/vue wraps @aparte/core for Vue 3.5+: an ergonomic <AparteChat> component, composables for
state and the client, and a generic <AparteUi> escape hatch.
npm install @aparte/vue @aparte/core vue@aparte/core and vue are peer dependencies.
<AparteChat> + useAparteChat
Section titled “<AparteChat> + useAparteChat”The useAparteChat composable owns the messages ref and the component ref, so you bind them and
skip the manual @messages-change → messages round-trip:
<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>The user’s message is appended to the thread automatically on send — don’t add it yourself.
@message-sent is optional and fires after that append, for side-effects only (scroll, analytics,
a backend call).
Slots are named slots: empty-state, composer, above-composer,
toolbar (the composer’s bottom row — mode picker, model selector: see
The composer toolbar for an example), and
the scoped bubble slot (#bubble="{ message }") for a fully custom bubble. The imperative handle
(chat.chatRef) exposes streaming, branch/edit and scrollToBottom — also available as plain
methods straight off the chat object.
The six callbacks are @message-sent, @action, @messages-change, @message-appended, @typing-change and @conversation-created. Vue hands you the payload directly — the table with all four frameworks side by side is generated from the wrapper source: Wrapper surface.
Wiring a real model
Section titled “Wiring a real model”The wrapper is provider-agnostic. Register a provider + transport once (see
Providers) and mount an AparteClient with useAparteClient — it bridges composer
sends to the model:
<script setup lang="ts">import { aparteGlobalConfig, AparteDirectTransport } from '@aparte/core';import { createOpenAICompatProvider, presets } from '@aparte/provider-openai-compat';import { AparteChat, useAparteChat, useAparteClient } from '@aparte/vue';
aparteGlobalConfig.registerAIProvider(createOpenAICompatProvider(presets.OPENROUTER));aparteGlobalConfig.setTransport(new AparteDirectTransport({ byok: true }));
const chat = useAparteChat();useAparteClient(); // streams replies from the configured provider</script>
<template> <AparteChat :ref="chat.chatRef" :messages="chat.messages.value" @messages-change="chat.onMessagesChange" /></template>Pass a per-instance config prop to scope providers/transport to a single <AparteChat> instead of
aparteGlobalConfig.
Any aparté element: typed in the template
Section titled “Any aparté element: typed in the template”The aparte-* tags are declared through Vue’s GlobalComponents, so vue-tsc checks them in any
template once the package is imported — no AparteUi, no isCustomElement guesswork about names:
<template> <aparte-select searchable="" placeholder="Pick a model" @aparte-select-change="(e) => use(e.detail.value)" > <aparte-option value="gpt-4o-mini">GPT-4o mini</aparte-option> </aparte-select></template>Presence attributes take '' to set and null to remove, never false — Vue stringifies what it
sets on a custom element, so :searchable="false" would render searchable="false" and an element
testing hasAttribute reads that as on. The rules and the full set are on
Placing elements, typed.
Any OTHER element: <AparteUi>
Section titled “Any OTHER element: <AparteUi>”For an element aparté does not define — one of yours, or a third party’s:
<script setup lang="ts">import { AparteUi } from '@aparte/vue';</script>
<template> <AparteUi name="my-token-counter" :props="{ 'data-budget': '8000' }" @element-event="(e) => console.log(e.type, e.detail)" /></template>It mounts any tag name, which is what a foreign element needs and what aparté’s own no longer do.
Also exported
Section titled “Also exported”useConversationManager— Vue-reactive view over the coreAparteConversationManager(list / create / archive), for a multi-conversation sidebar.
Testing it
Section titled “Testing it”Vitest — every runner — executes on Node, so @aparte/core resolves to its DOM-free
entry and no <aparte-*> element upgrades under jsdom: the tag stays a plain
HTMLElement and every assertion about it fails for a reason nothing explains. Alias the
specifier to @aparte/core/browser, the
entry with the elements in it.