Skip to main content

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 navigate tool that uses vue-router's router.push()
  • Navigation observation — watching route.fullPath to 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

FilePurpose
composables/useDiosc.tsFull Diosc SDK integration (config, auth, tools, navigation)
composables/useAuth.tsReactive auth state with login/logout
App.vueRenders <diosc-chat v-if="isAuthenticated" />
lib/api.tsFramework-agnostic API client (shared with React app)
vite.config.tsVue + 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