Vue Integration
Learn how to integrate AI Interview into your Vue.js application with a reusable component.
Installation
npm install @ai-interview/sdk
# or
yarn add @ai-interview/sdk
Vue 3 Composition API
<template>
<div class="interview-wrapper">
<div ref="interviewContainer" class="interview-container"></div>
<div v-if="status === 'in-progress' && progress > 0" class="progress-bar">
<div class="progress-fill" :style="{ width: `${progress}%` }"></div>
<span class="progress-text">{{ Math.round(progress) }}% complete</span>
</div>
<div v-if="status === 'completed'" class="status-message success">
✓ Thank you! Your interview has been completed.
</div>
<div v-if="status === 'error'" class="status-message error">
✗ Error: {{ errorMessage }}
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import InterviewSDK from '@ai-interview/sdk';
const props = defineProps({
inviteToken: {
type: String,
required: true,
},
});
const emit = defineEmits(['completed', 'error']);
const interviewContainer = ref(null);
const embed = ref(null);
const status = ref('loading');
const progress = ref(0);
const errorMessage = ref('');
onMounted(() => {
try {
embed.value = InterviewSDK.mount(interviewContainer.value, {
invite: props.inviteToken,
theme: {
primary: '#22d3ee',
background: '#0f172a',
text: '#e5e7eb',
},
});
// Event listeners
embed.value.on('start', () => {
status.value = 'in-progress';
console.log('Interview started');
});
embed.value.on('progress', (payload) => {
progress.value = payload.percentage;
});
embed.value.on('completed', (payload) => {
status.value = 'completed';
console.log('Interview completed:', payload.sessionId);
emit('completed', payload.sessionId);
});
embed.value.on('error', (payload) => {
status.value = 'error';
errorMessage.value = payload.message;
console.error('Interview error:', payload);
emit('error', payload);
});
status.value = 'ready';
} catch (error) {
status.value = 'error';
errorMessage.value = error.message;
console.error('Mount error:', error);
}
});
onBeforeUnmount(() => {
if (embed.value) {
embed.value.destroy();
}
});
</script>
<style scoped>
.interview-wrapper {
width: 100%;
max-width: 900px;
margin: 0 auto;
}
.interview-container {
width: 100%;
height: 600px;
background: #1e293b;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.progress-bar {
margin-top: 20px;
height: 40px;
background: #1e293b;
border-radius: 8px;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #22d3ee, #06b6d4);
transition: width 0.3s ease;
}
.progress-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #e5e7eb;
font-weight: 600;
}
.status-message {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
text-align: center;
font-weight: 500;
}
.status-message.success {
background: #10b981;
color: white;
}
.status-message.error {
background: #ef4444;
color: white;
}
@media (max-width: 768px) {
.interview-container {
height: 500px;
}
}
</style>
Vue 2 Options API
<template>
<div class="interview-wrapper">
<div ref="interviewContainer" class="interview-container"></div>
<div v-if="status === 'in-progress' && progress > 0" class="progress-bar">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
<span class="progress-text">{{ Math.round(progress) }}% complete</span>
</div>
<div v-if="status === 'completed'" class="status-message success">
✓ Thank you! Your interview has been completed.
</div>
<div v-if="status === 'error'" class="status-message error">
✗ Error: {{ errorMessage }}
</div>
</div>
</template>
<script>
import InterviewSDK from '@ai-interview/sdk';
export default {
name: 'Interview',
props: {
inviteToken: {
type: String,
required: true,
},
},
data() {
return {
embed: null,
status: 'loading',
progress: 0,
errorMessage: '',
};
},
mounted() {
try {
this.embed = InterviewSDK.mount(this.$refs.interviewContainer, {
invite: this.inviteToken,
theme: {
primary: '#22d3ee',
background: '#0f172a',
text: '#e5e7eb',
},
});
this.embed.on('start', () => {
this.status = 'in-progress';
});
this.embed.on('progress', (payload) => {
this.progress = payload.percentage;
});
this.embed.on('completed', (payload) => {
this.status = 'completed';
this.$emit('completed', payload.sessionId);
});
this.embed.on('error', (payload) => {
this.status = 'error';
this.errorMessage = payload.message;
this.$emit('error', payload);
});
this.status = 'ready';
} catch (error) {
this.status = 'error';
this.errorMessage = error.message;
}
},
beforeDestroy() {
if (this.embed) {
this.embed.destroy();
}
},
};
</script>
<style scoped>
/* Same styles as Vue 3 example */
</style>
Usage in Parent Component
<template>
<div class="page">
<h1>Customer Feedback Interview</h1>
<p>Share your thoughts in a quick 5-minute voice interview</p>
<Interview
:invite-token="inviteToken"
@completed="handleCompleted"
@error="handleError"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import Interview from '@/components/Interview.vue';
const router = useRouter();
const inviteToken = ref('inv_abc123');
const handleCompleted = (sessionId) => {
console.log('Interview completed:', sessionId);
router.push('/thank-you');
};
const handleError = (error) => {
console.error('Interview error:', error);
// Show error notification
};
</script>
With Vue Router
<!-- pages/Interview.vue -->
<template>
<div v-if="!token" class="error">
Invalid interview link
</div>
<Interview v-else :invite-token="token" @completed="handleCompleted" />
</template>
<script setup>
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import Interview from '@/components/Interview.vue';
const route = useRoute();
const router = useRouter();
const token = computed(() => route.params.token);
const handleCompleted = () => {
router.push('/thank-you');
};
</script>
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import InterviewPage from '@/pages/Interview.vue';
const routes = [
{
path: '/interview/:token',
name: 'Interview',
component: InterviewPage,
},
];
export default createRouter({
history: createWebHistory(),
routes,
});
Fetching Tokens Dynamically
<template>
<div v-if="loading">Loading interview...</div>
<div v-else-if="error">Error: {{ error }}</div>
<Interview v-else-if="token" :invite-token="token" />
</template>
<script setup>
import { ref, onMounted } from 'vue';
import Interview from '@/components/Interview.vue';
const props = defineProps({
userId: {
type: String,
required: true,
},
});
const token = ref(null);
const loading = ref(true);
const error = ref(null);
onMounted(async () => {
try {
const response = await fetch('/api/create-interview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: props.userId }),
});
if (!response.ok) {
throw new Error('Failed to create interview');
}
const data = await response.json();
token.value = data.inviteToken;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
});
</script>
Nuxt 3 Integration
<!-- pages/interview/[token].vue -->
<template>
<div>
<h1>AI Interview</h1>
<ClientOnly>
<Interview
v-if="token"
:invite-token="token"
@completed="handleCompleted"
/>
</ClientOnly>
</div>
</template>
<script setup>
import { useRoute, useRouter } from '#app';
const route = useRoute();
const router = useRouter();
const token = computed(() => route.params.token);
const handleCompleted = (sessionId) => {
console.log('Completed:', sessionId);
router.push('/thank-you');
};
</script>
Use ClientOnly in Nuxt to prevent SSR issues with the interview SDK.
Composable Hook
Create a reusable composable:
// composables/useInterview.js
import { ref, onMounted, onBeforeUnmount } from 'vue';
import InterviewSDK from '@ai-interview/sdk';
export function useInterview(containerRef, options) {
const embed = ref(null);
const status = ref('loading');
const progress = ref(0);
const error = ref(null);
onMounted(() => {
if (!containerRef.value) return;
try {
embed.value = InterviewSDK.mount(containerRef.value, options);
embed.value.on('start', () => {
status.value = 'in-progress';
});
embed.value.on('progress', (p) => {
progress.value = p.percentage;
});
embed.value.on('completed', () => {
status.value = 'completed';
});
embed.value.on('error', (e) => {
status.value = 'error';
error.value = e.message;
});
status.value = 'ready';
} catch (err) {
status.value = 'error';
error.value = err.message;
}
});
onBeforeUnmount(() => {
if (embed.value) {
embed.value.destroy();
}
});
return { embed, status, progress, error };
}
Usage:
<template>
<div>
<div ref="container" style="height: 600px;"></div>
<div v-if="status === 'in-progress'">Progress: {{ progress }}%</div>
<div v-if="error">Error: {{ error }}</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useInterview } from '@/composables/useInterview';
const props = defineProps(['inviteToken']);
const container = ref(null);
const { status, progress, error } = useInterview(container, {
invite: props.inviteToken,
});
</script>
Best Practices
1. Error Handling
Use Vue's error handling:
<script setup>
import { onErrorCaptured } from 'vue';
onErrorCaptured((err) => {
console.error('Interview component error:', err);
return false; // Prevent error from propagating
});
</script>
2. Loading States
Show proper loading indicators:
<template>
<div v-if="status === 'loading'" class="loading">
<LoadingSpinner />
<p>Loading interview...</p>
</div>
</template>
3. TypeScript Support
<script setup lang="ts">
import { ref, Ref } from 'vue';
import type { InterviewEmbed } from '@ai-interview/sdk';
interface Props {
inviteToken: string;
}
const props = defineProps<Props>();
const embed: Ref<InterviewEmbed | null> = ref(null);
</script>