Vue Integration Example#
Integrate InterviewRelay AI interviews into your Vue.js application. This guide covers Vue 3 (Composition API), Vue 2 (Options API), Vue Router, Nuxt 3, and reusable composables.
Installation#
Install the InterviewRelay Vue SDK:
npm install @interviewrelay/vue-sdk
Or with yarn:
yarn add @interviewrelay/vue-sdk
Or with pnpm:
pnpm add @interviewrelay/vue-sdk
Vue 3 Composition API#
Here's a complete interview component using Vue 3's Composition API with TypeScript:
<script setup lang="ts">
import { ref, computed } from 'vue';
import { InterviewWidget } from '@interviewrelay/vue-sdk';
import type { InterviewResult, InterviewError } from '@interviewrelay/vue-sdk';
const props = defineProps<{
inviteToken: string;
}>();
const emit = defineEmits<{
complete: [result: InterviewResult];
error: [error: InterviewError];
}>();
const status = ref<'loading' | 'ready' | 'active' | 'completed' | 'error'>('loading');
const result = ref<InterviewResult | null>(null);
const errorMessage = ref<string | null>(null);
const isCompleted = computed(() => status.value === 'completed');
const hasError = computed(() => status.value === 'error');
const isLoading = computed(() => status.value === 'loading');
const theme = {
primaryColor: '#6366f1',
backgroundColor: '#111111',
textColor: '#ffffff',
borderRadius: '12px',
};
function onReady() {
status.value = 'ready';
}
function onStart() {
status.value = 'active';
}
function onComplete(interviewResult: InterviewResult) {
status.value = 'completed';
result.value = interviewResult;
emit('complete', interviewResult);
}
function onError(error: InterviewError) {
status.value = 'error';
errorMessage.value = error.message;
emit('error', error);
console.error('Interview error:', error);
}
function retry() {
window.location.reload();
}
</script>
<template>
<div class="interview-wrapper">
<!-- Completed State -->
<div v-if="isCompleted" class="status-screen success">
<div class="icon">✓</div>
<h2>Thank You!</h2>
<p>Your interview has been recorded successfully.</p>
<div v-if="result" class="details">
<p>Duration: {{ Math.round(result.duration / 60) }} minutes</p>
<p>Questions answered: {{ result.questionsAnswered }}</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="hasError" class="status-screen error">
<div class="icon">✕</div>
<h2>Something Went Wrong</h2>
<p>{{ errorMessage }}</p>
<button @click="retry" class="retry-button">Try Again</button>
</div>
<!-- Interview Widget -->
<template v-else>
<div v-if="isLoading" class="loading-overlay">
<div class="spinner" />
<p>Preparing your interview...</p>
</div>
<InterviewWidget
:invite-token="props.inviteToken"
:theme="theme"
@ready="onReady"
@start="onStart"
@complete="onComplete"
@error="onError"
/>
</template>
</div>
</template>
<style scoped>
.interview-wrapper {
width: 100%;
max-width: 800px;
margin: 0 auto;
min-height: 500px;
position: relative;
}
.status-screen {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 500px;
text-align: center;
padding: 2rem;
}
.status-screen .icon {
font-size: 3rem;
margin-bottom: 1rem;
}
.status-screen.success .icon {
color: #22c55e;
}
.status-screen.error .icon {
color: #ef4444;
}
.status-screen h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.status-screen p {
color: #888;
margin-bottom: 0.25rem;
}
.details {
margin-top: 1rem;
font-size: 0.875rem;
color: #666;
}
.retry-button {
margin-top: 1rem;
padding: 0.5rem 1.5rem;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 0.875rem;
}
.retry-button:hover {
background: #4f46e5;
}
.loading-overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #0a0a0a;
z-index: 10;
}
.spinner {
width: 2rem;
height: 2rem;
border: 2px solid transparent;
border-bottom-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 1rem;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
Vue 2 Options API#
If you're using Vue 2, here's the equivalent component with the Options API:
<template>
<div class="interview-wrapper">
<!-- Completed State -->
<div v-if="status === 'completed'" class="status-screen success">
<div class="icon">✓</div>
<h2>Thank You!</h2>
<p>Your interview has been recorded successfully.</p>
<div v-if="result" class="details">
<p>Duration: {{ Math.round(result.duration / 60) }} minutes</p>
<p>Questions answered: {{ result.questionsAnswered }}</p>
</div>
</div>
<!-- Error State -->
<div v-else-if="status === 'error'" class="status-screen error">
<div class="icon">✕</div>
<h2>Something Went Wrong</h2>
<p>{{ errorMessage }}</p>
<button @click="retry" class="retry-button">Try Again</button>
</div>
<!-- Interview Widget -->
<template v-else>
<div v-if="status === 'loading'" class="loading-overlay">
<div class="spinner" />
<p>Preparing your interview...</p>
</div>
<InterviewWidget
:invite-token="inviteToken"
:theme="theme"
@ready="onReady"
@start="onStart"
@complete="onComplete"
@error="onError"
/>
</template>
</div>
</template>
<script>
import { InterviewWidget } from '@interviewrelay/vue-sdk';
export default {
name: 'InterviewPage',
components: {
InterviewWidget,
},
props: {
inviteToken: {
type: String,
required: true,
},
},
data() {
return {
status: 'loading',
result: null,
errorMessage: null,
theme: {
primaryColor: '#6366f1',
backgroundColor: '#111111',
textColor: '#ffffff',
borderRadius: '12px',
},
};
},
methods: {
onReady() {
this.status = 'ready';
},
onStart() {
this.status = 'active';
},
onComplete(result) {
this.status = 'completed';
this.result = result;
this.$emit('complete', result);
},
onError(error) {
this.status = 'error';
this.errorMessage = error.message;
this.$emit('error', error);
console.error('Interview error:', error);
},
retry() {
window.location.reload();
},
},
};
</script>
Vue 2 Support
The @interviewrelay/vue-sdk package supports both Vue 2 and Vue 3. Vue 2 users should use version ^1.x, while Vue 3 users should use ^2.x.
Usage in Parent Component#
Vue 3#
<script setup lang="ts">
import InterviewPage from './components/InterviewPage.vue';
import type { InterviewResult } from '@interviewrelay/vue-sdk';
function handleComplete(result: InterviewResult) {
console.log('Interview completed:', result);
// Navigate, track analytics, etc.
}
</script>
<template>
<div class="min-h-screen bg-gray-950 text-white">
<header class="p-4 border-b border-gray-800">
<h1 class="text-xl font-semibold">Your Company</h1>
</header>
<main class="p-8">
<InterviewPage
invite-token="inv_abc123xyz"
@complete="handleComplete"
/>
</main>
</div>
</template>
Vue 2#
<template>
<div class="app">
<header>
<h1>Your Company</h1>
</header>
<main>
<InterviewPage
:invite-token="token"
@complete="handleComplete"
/>
</main>
</div>
</template>
<script>
import InterviewPage from './components/InterviewPage.vue';
export default {
components: { InterviewPage },
data() {
return {
token: 'inv_abc123xyz',
};
},
methods: {
handleComplete(result) {
console.log('Interview completed:', result);
},
},
};
</script>
With Vue Router#
Set up a dedicated interview route with the token as a query parameter:
Vue 3 + Vue Router 4#
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/interview',
name: 'interview',
component: () => import('../views/InterviewView.vue'),
},
{
path: '/thank-you',
name: 'thank-you',
component: () => import('../views/ThankYouView.vue'),
},
],
});
export default router;
<!-- views/InterviewView.vue -->
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import InterviewPage from '../components/InterviewPage.vue';
import type { InterviewResult } from '@interviewrelay/vue-sdk';
const route = useRoute();
const router = useRouter();
const token = computed(() => route.query.token as string | undefined);
function handleComplete(result: InterviewResult) {
router.push({ name: 'thank-you', query: { session: result.sessionId } });
}
</script>
<template>
<div class="interview-view">
<div v-if="!token" class="no-token">
<h2>Missing Token</h2>
<p>No invite token found. Please use the link from your invitation email.</p>
</div>
<InterviewPage
v-else
:invite-token="token"
@complete="handleComplete"
/>
</div>
</template>
<style scoped>
.interview-view {
min-height: 100vh;
background: #0a0a0a;
color: #ffffff;
padding: 2rem;
}
.no-token {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
text-align: center;
}
.no-token h2 {
color: #ef4444;
margin-bottom: 0.5rem;
}
.no-token p {
color: #888;
}
</style>
Invite links would look like: https://yourapp.com/interview?token=inv_abc123xyz
Fetching Tokens Dynamically#
Fetch invite tokens from your backend on demand:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import InterviewPage from './components/InterviewPage.vue';
const props = defineProps<{
participantEmail: string;
campaignId: string;
}>();
const token = ref<string | null>(null);
const loading = ref(true);
const error = ref<string | null>(null);
onMounted(async () => {
try {
const response = await fetch('/api/create-invite', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: props.participantEmail,
campaignId: props.campaignId,
}),
});
if (!response.ok) {
throw new Error('Failed to create invite');
}
const data = await response.json();
token.value = data.invite_token;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error';
} finally {
loading.value = false;
}
});
</script>
<template>
<div>
<!-- Loading -->
<div v-if="loading" class="loading">
<div class="spinner" />
<p>Creating your interview session...</p>
</div>
<!-- Error -->
<div v-else-if="error" class="error">
<p>{{ error }}</p>
</div>
<!-- Interview -->
<InterviewPage
v-else-if="token"
:invite-token="token"
/>
</div>
</template>
Nuxt 3 Integration#
Plugin Setup#
Register the InterviewRelay SDK as a Nuxt plugin:
// plugins/interviewrelay.client.ts
import { InterviewWidget } from '@interviewrelay/vue-sdk';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.component('InterviewWidget', InterviewWidget);
});
Client-Side Only
The InterviewRelay widget requires browser APIs (DOM, WebRTC). Name the plugin file with .client.ts to ensure it only runs on the client side.
Page Component#
<!-- pages/interview.vue -->
<script setup lang="ts">
import type { InterviewResult, InterviewError } from '@interviewrelay/vue-sdk';
const route = useRoute();
const router = useRouter();
const token = computed(() => route.query.token as string | undefined);
const status = ref<'loading' | 'ready' | 'active' | 'completed' | 'error'>('loading');
const errorMessage = ref<string | null>(null);
function onReady() {
status.value = 'ready';
}
function onStart() {
status.value = 'active';
}
function onComplete(result: InterviewResult) {
status.value = 'completed';
router.push(`/thank-you?session=${result.sessionId}`);
}
function onError(error: InterviewError) {
status.value = 'error';
errorMessage.value = error.message;
}
useHead({
title: 'Your Interview - InterviewRelay',
});
</script>
<template>
<div class="min-h-screen bg-gray-950 text-white p-8">
<div v-if="!token" class="flex items-center justify-center min-h-[80vh]">
<div class="text-center">
<h2 class="text-2xl font-bold text-red-400 mb-2">Missing Token</h2>
<p class="text-gray-400">No invite token provided.</p>
</div>
</div>
<ClientOnly v-else>
<InterviewWidget
:invite-token="token"
:theme="{
primaryColor: '#6366f1',
backgroundColor: '#111111',
textColor: '#ffffff',
}"
@ready="onReady"
@start="onStart"
@complete="onComplete"
@error="onError"
/>
<template #fallback>
<div class="flex items-center justify-center min-h-[500px]">
<p class="text-gray-400">Loading interview widget...</p>
</div>
</template>
</ClientOnly>
</div>
</template>
Server API Route#
Generate tokens server-side in Nuxt:
// server/api/create-invite.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const config = useRuntimeConfig();
const response = await $fetch('https://api.interviewrelay.com/v1/invites', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.interviewrelayApiKey}`,
'Content-Type': 'application/json',
},
body: {
campaign_id: body.campaignId,
participant_email: body.email,
},
});
return {
invite_token: response.invite_token,
expires_at: response.expires_at,
};
});
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
interviewrelayApiKey: process.env.INTERVIEWRELAY_API_KEY,
},
});
Composable Hook#
useInterview#
Create a reusable composable to manage interview state:
// composables/useInterview.ts
import { ref, computed } from 'vue';
import type { InterviewResult, InterviewError } from '@interviewrelay/vue-sdk';
type InterviewStatus = 'idle' | 'loading' | 'ready' | 'active' | 'completed' | 'error';
export function useInterview() {
const status = ref<InterviewStatus>('idle');
const result = ref<InterviewResult | null>(null);
const error = ref<string | null>(null);
const isLoading = computed(() => status.value === 'loading' || status.value === 'idle');
const isActive = computed(() => status.value === 'active');
const isCompleted = computed(() => status.value === 'completed');
const hasError = computed(() => status.value === 'error');
function onReady() {
status.value = 'ready';
}
function onStart() {
status.value = 'active';
}
function onComplete(interviewResult: InterviewResult) {
status.value = 'completed';
result.value = interviewResult;
}
function onError(interviewError: InterviewError) {
status.value = 'error';
error.value = interviewError.message;
}
function reset() {
status.value = 'idle';
result.value = null;
error.value = null;
}
return {
// State
status,
result,
error,
// Computed
isLoading,
isActive,
isCompleted,
hasError,
// Event handlers
handlers: {
onReady,
onStart,
onComplete,
onError,
},
// Actions
reset,
};
}
Usage with the composable:
<script setup lang="ts">
import { InterviewWidget } from '@interviewrelay/vue-sdk';
import { useInterview } from '../composables/useInterview';
const props = defineProps<{ token: string }>();
const { status, result, error, isCompleted, hasError, isLoading, handlers, reset } = useInterview();
</script>
<template>
<div>
<div v-if="isCompleted" class="text-center py-16">
<h2 class="text-2xl font-bold text-green-400">Interview Complete</h2>
<p class="text-gray-400 mt-2">
Duration: {{ Math.round(result!.duration / 60) }} min
</p>
<button @click="reset" class="mt-4 px-4 py-2 bg-indigo-600 rounded-lg">
Start Over
</button>
</div>
<div v-else-if="hasError" class="text-center py-16">
<p class="text-red-400">{{ error }}</p>
<button @click="reset" class="mt-4 px-4 py-2 bg-indigo-600 rounded-lg">
Retry
</button>
</div>
<div v-else class="relative">
<div v-if="isLoading" class="absolute inset-0 flex items-center justify-center bg-gray-950 z-10">
<div class="spinner" />
</div>
<InterviewWidget
:invite-token="props.token"
v-on="handlers"
/>
</div>
</div>
</template>
Best Practices#
Error Handling#
Always handle errors gracefully. The SDK can emit errors for network issues, expired tokens, and browser incompatibilities:
<script setup lang="ts">
import type { InterviewError } from '@interviewrelay/vue-sdk';
function onError(error: InterviewError) {
switch (error.code) {
case 'NETWORK_ERROR':
// Show reconnection UI
break;
case 'TOKEN_EXPIRED':
// Redirect to get a new token
break;
case 'TOKEN_INVALID':
// Show invalid link message
break;
case 'BROWSER_UNSUPPORTED':
// Show browser upgrade message
break;
default:
// Generic error handling
console.error('Interview error:', error);
}
}
</script>
Loading States#
Show meaningful loading indicators while the widget initializes:
<template>
<div class="relative min-h-[500px]">
<Transition name="fade">
<div v-if="!isReady" class="absolute inset-0 flex flex-col items-center justify-center bg-gray-950 z-10">
<div class="animate-spin rounded-full h-10 w-10 border-b-2 border-indigo-500 mb-4" />
<p class="text-gray-400 text-sm">Preparing your interview...</p>
<p class="text-gray-600 text-xs mt-1">This may take a few seconds</p>
</div>
</Transition>
<InterviewWidget
:invite-token="token"
@ready="isReady = true"
/>
</div>
</template>
<style scoped>
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-leave-to {
opacity: 0;
}
</style>
TypeScript Support#
The SDK ships with full TypeScript declarations. Enable strict typing in your components:
// types/interview.ts
import type { InterviewResult, InterviewError, InterviewTheme } from '@interviewrelay/vue-sdk';
export interface InterviewPageProps {
inviteToken: string;
theme?: InterviewTheme;
locale?: string;
autoStart?: boolean;
}
export interface InterviewPageEmits {
complete: [result: InterviewResult];
error: [error: InterviewError];
start: [];
ready: [];
}
<script setup lang="ts">
import type { InterviewPageProps, InterviewPageEmits } from '../types/interview';
const props = defineProps<InterviewPageProps>();
const emit = defineEmits<InterviewPageEmits>();
</script>
Full Type Safety
Using TypeScript with the InterviewRelay Vue SDK gives you autocompletion for all props, events, and callback payloads in your IDE.
Next Steps#
- React Integration — See the React equivalent
- Static HTML Example — Use the SDK without a framework
- SDK Reference — Full SDK API documentation
- Webhooks — Get notified server-side when interviews complete
- Authentication — Secure your API requests