Vue Integration
This guide covers integrating Diosc into Vue 3 applications using the Composition API, including Nuxt.js specifics.
Basic Setup
Installation
npm install @diosc-ai/assistant-kit
npm install -D @types/diosc-client
Configure Vue for Web Components
Tell Vue to ignore diosc-* elements in vite.config.ts:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('diosc-')
}
}
})
]
});
For Vue CLI (vue.config.js):
module.exports = {
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => ({
...options,
compilerOptions: {
isCustomElement: tag => tag.startsWith('diosc-')
}
}));
}
};
Basic Component
<template>
<div class="app">
<h1>My Application</h1>
<diosc-chat />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { diosc } from '@diosc-ai/assistant-kit';
onMounted(() => {
diosc('config', {
backendUrl: import.meta.env.VITE_DIOSC_URL,
apiKey: import.meta.env.VITE_DIOSC_API_KEY,
autoConnect: true
});
});
onUnmounted(() => {
diosc('disconnect');
});
</script>
Composables
useDiosc Composable
// composables/useDiosc.ts
import { ref, onMounted, onUnmounted } from 'vue';
import { diosc } from '@diosc-ai/assistant-kit';
import type { DioscMessage } from '@types/diosc-client';
export function useDiosc() {
const isConnected = ref(false);
const messages = ref<DioscMessage[]>([]);
const isLoading = ref(false);
let unsubscribers: (() => void)[] = [];
onMounted(() => {
unsubscribers = [
diosc('on', 'connected', () => {
isConnected.value = true;
}),
diosc('on', 'disconnected', () => {
isConnected.value = false;
}),
diosc('on', 'message', (msg: DioscMessage) => {
messages.value.push(msg);
isLoading.value = false;
}),
diosc('on', 'streaming_start', () => {
isLoading.value = true;
})
];
});
onUnmounted(() => {
unsubscribers.forEach(unsub => unsub());
});
function sendMessage(content: string) {
messages.value.push({ role: 'user', content });
isLoading.value = true;
diosc('send', content);
}
function clearMessages() {
messages.value = [];
}
return {
isConnected,
messages,
isLoading,
sendMessage,
clearMessages
};
}
useDioscAuth Composable
// composables/useDioscAuth.ts
import { watch, onMounted } from 'vue';
import { diosc } from '@diosc-ai/assistant-kit';
import { useAuth } from './useAuth'; // Your auth composable
export function useDioscAuth() {
const { accessToken, user, refreshToken } = useAuth();
function setupAuth() {
diosc('auth', async () => {
let token = accessToken.value;
// Refresh if expiring
if (isTokenExpiring(token)) {
token = await refreshToken();
}
return {
headers: {
'Authorization': `Bearer ${token}`
},
userId: user.value?.id
};
});
}
onMounted(setupAuth);
// Re-setup if user changes
watch(user, setupAuth);
}
usePageContext Composable
// composables/usePageContext.ts
import { watch, onMounted } from 'vue';
import { useRoute } from 'vue-router';
import { diosc } from '@diosc-ai/assistant-kit';
export function usePageContext(pageData?: () => Record<string, any>) {
const route = useRoute();
function updateContext() {
diosc('pageContext', {
path: route.path,
query: route.query,
params: route.params,
pageData: pageData?.()
});
}
onMounted(updateContext);
watch(() => route.fullPath, updateContext);
}
Plugin Pattern
Create a Vue plugin for app-wide configuration:
// plugins/diosc.ts
import type { App, Plugin } from 'vue';
import { diosc } from '@diosc-ai/assistant-kit';
export interface DioscPluginOptions {
backendUrl: string;
apiKey: string;
autoConnect?: boolean;
}
export const DioscPlugin: Plugin = {
install(app: App, options: DioscPluginOptions) {
// Configure
diosc('config', {
backendUrl: options.backendUrl,
apiKey: options.apiKey,
autoConnect: options.autoConnect ?? true
});
// Make available globally
app.config.globalProperties.$diosc = diosc;
// Provide for composition API
app.provide('diosc', diosc);
}
};
Usage:
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import { DioscPlugin } from './plugins/diosc';
const app = createApp(App);
app.use(DioscPlugin, {
backendUrl: import.meta.env.VITE_DIOSC_URL,
apiKey: import.meta.env.VITE_DIOSC_API_KEY
});
app.mount('#app');
<!-- Any component -->
<script setup lang="ts">
import { inject } from 'vue';
const diosc = inject('diosc');
function askAI() {
diosc('send', 'Help me with this page');
}
</script>
Nuxt.js Integration
Nuxt 3 Plugin
// plugins/diosc.client.ts
import { diosc } from '@diosc-ai/assistant-kit';
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
diosc('config', {
backendUrl: config.public.dioscUrl,
apiKey: config.public.dioscApiKey,
autoConnect: true
});
return {
provide: {
diosc
}
};
});
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
dioscUrl: process.env.DIOSC_URL,
dioscApiKey: process.env.DIOSC_API_KEY
}
},
vue: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('diosc-')
}
}
});
Client-Only Rendering
Wrap components in <ClientOnly>:
<template>
<div>
<h1>My Nuxt App</h1>
<ClientOnly>
<diosc-chat />
</ClientOnly>
</div>
</template>
Composable with Nuxt
// composables/useDiosc.ts
export function useDiosc() {
const { $diosc } = useNuxtApp();
const isConnected = ref(false);
if (process.client) {
$diosc('on', 'connected', () => {
isConnected.value = true;
});
}
return {
isConnected,
send: (msg: string) => $diosc('send', msg)
};
}
Custom Chat Component
Build your own chat interface:
<template>
<div class="custom-chat">
<div class="messages" ref="messagesContainer">
<div
v-for="(msg, index) in messages"
:key="index"
:class="['message', msg.role]"
>
<div class="content" v-html="renderMarkdown(msg.content)" />
</div>
<div v-if="isLoading" class="message assistant loading">
<span class="typing-indicator">...</span>
</div>
</div>
<form @submit.prevent="handleSend" class="input-form">
<input
v-model="input"
type="text"
placeholder="Type a message..."
:disabled="!isConnected"
/>
<button type="submit" :disabled="!input.trim() || isLoading">
Send
</button>
</form>
<!-- Headless agent -->
<diosc-agent />
</div>
</template>
<script setup lang="ts">
import { ref, nextTick, watch } from 'vue';
import { useDiosc } from '@/composables/useDiosc';
import { marked } from 'marked';
const { messages, isConnected, isLoading, sendMessage } = useDiosc();
const input = ref('');
const messagesContainer = ref<HTMLElement>();
function handleSend() {
if (!input.value.trim()) return;
sendMessage(input.value);
input.value = '';
}
function renderMarkdown(content: string) {
return marked.parse(content);
}
// Auto-scroll to bottom
watch(messages, async () => {
await nextTick();
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
}, { deep: true });
</script>
<style scoped>
.custom-chat {
display: flex;
flex-direction: column;
height: 100%;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
}
.message {
margin-bottom: 1rem;
padding: 0.75rem 1rem;
border-radius: 8px;
max-width: 80%;
}
.message.user {
background: #0066cc;
color: white;
margin-left: auto;
}
.message.assistant {
background: #f0f0f0;
}
.input-form {
display: flex;
padding: 1rem;
border-top: 1px solid #e0e0e0;
}
.input-form input {
flex: 1;
padding: 0.75rem;
border: 1px solid #e0e0e0;
border-radius: 4px;
margin-right: 0.5rem;
}
.input-form button {
padding: 0.75rem 1.5rem;
background: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.input-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.typing-indicator {
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.3; }
}
</style>
Event Handling
Template Refs
<template>
<diosc-chat ref="chatRef" />
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const chatRef = ref<HTMLElement>();
onMounted(() => {
chatRef.value?.addEventListener('dioscMessage', handleMessage);
});
onUnmounted(() => {
chatRef.value?.removeEventListener('dioscMessage', handleMessage);
});
function handleMessage(event: CustomEvent) {
console.log('Message:', event.detail);
}
</script>
Global Events
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { diosc } from '@diosc-ai/assistant-kit';
const unsubscribers: (() => void)[] = [];
onMounted(() => {
unsubscribers.push(
diosc('on', 'message', handleMessage),
diosc('on', 'error', handleError),
diosc('on', 'approval_required', handleApproval)
);
});
onUnmounted(() => {
unsubscribers.forEach(fn => fn());
});
function handleMessage(msg) {
console.log('AI said:', msg.content);
}
function handleError(err) {
console.error('Error:', err);
}
function handleApproval(request) {
// Show approval dialog
}
</script>
Pinia Store (Optional)
For complex state management:
// stores/diosc.ts
import { defineStore } from 'pinia';
import { diosc } from '@diosc-ai/assistant-kit';
import type { DioscMessage } from '@types/diosc-client';
export const useDioscStore = defineStore('diosc', {
state: () => ({
messages: [] as DioscMessage[],
isConnected: false,
isLoading: false,
error: null as Error | null
}),
actions: {
initialize(backendUrl: string, apiKey: string) {
diosc('config', { backendUrl, apiKey });
diosc('on', 'connected', () => {
this.isConnected = true;
});
diosc('on', 'disconnected', () => {
this.isConnected = false;
});
diosc('on', 'message', (msg) => {
this.messages.push(msg);
this.isLoading = false;
});
diosc('on', 'error', (err) => {
this.error = err;
this.isLoading = false;
});
},
sendMessage(content: string) {
this.messages.push({ role: 'user', content });
this.isLoading = true;
this.error = null;
diosc('send', content);
},
clearMessages() {
this.messages = [];
}
}
});
Troubleshooting
Unknown Custom Element Warning
Ensure Vue compiler options are set correctly:
// vite.config.ts
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('diosc-')
}
}
})
SSR Hydration Mismatch
Use <ClientOnly> in Nuxt or conditional rendering:
<template>
<diosc-chat v-if="mounted" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
const mounted = ref(false);
onMounted(() => { mounted.value = true; });
</script>
Reactivity with Web Components
Web components don't automatically respond to Vue reactivity:
<!-- This won't update reactively -->
<diosc-chat :backend-url="url" />
<!-- Use diosc() API instead -->
<script setup>
watch(url, (newUrl) => {
diosc('config', { backendUrl: newUrl });
});
</script>
Real-World Example: ACME Helpdesk
The ACME Helpdesk sample app includes a complete Vue 3 frontend (packages/vue-app/) that demonstrates a full Diosc integration with:
- BYOA authentication — passing OAuth tokens from a helpdesk login system to Diosc
- Navigation tool — registering a
navigatetool that usesvue-router'srouter.push() - Navigation observation — watching
route.fullPathto notify Diosc of page changes - Connect/disconnect lifecycle — watching auth state to manage the Diosc connection
- Singleton composable pattern — module-level
ref()state instead of Vue plugins or Pinia
Key files
| File | Purpose |
|---|---|
composables/useDiosc.ts | Full Diosc SDK integration (config, auth, tools, navigation) |
composables/useAuth.ts | Reactive auth state with login/logout |
App.vue | Renders <diosc-chat v-if="isAuthenticated" /> |
lib/api.ts | Framework-agnostic API client (shared with React app) |
vite.config.ts | Vue + Tailwind plugins, @diosc-ai/client alias, API proxy |
Running the sample
cd samples/acme-helpdesk
npm install
npm run dev:vue # Vue app on http://localhost:3002
npm run dev:api # Backend on http://localhost:3003
The React version runs on port 3001 (npm run dev:app) for side-by-side comparison.
Next Steps
- Angular Integration - Angular-specific patterns
- Common Integration Patterns - Framework-agnostic patterns
- Styling - Customize appearance