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>

Additional Resources