survey-voice / voice_interview.html
npc0's picture
Upload voice_interview.html
c9ad506 verified
Raw
History Blame Contribute Delete
23.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voice Interview - Project Story</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/driver.js@latest/dist/driver.js.iife.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/driver.js@latest/dist/driver.css"/>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.recording {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
.speaking {
animation: speaking 0.8s infinite;
}
@keyframes speaking {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.transcript-msg {
animation: fadeIn 0.3s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col items-center p-4">
<div class="max-w-4xl w-full">
<!-- Header -->
<div class="gradient-bg text-white p-6 rounded-t-xl shadow-lg">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold mb-2">🎀 Voice Interview</h1>
<p class="text-sm opacity-90">Tell your project story - just speak naturally!</p>
</div>
<button
id="help-btn"
onclick="startTutorial()"
class="bg-white bg-opacity-20 hover:bg-opacity-30 text-white px-4 py-2 rounded-lg text-sm font-medium transition"
title="Show tutorial">
❓ Help
</button>
</div>
</div>
<!-- Main Content -->
<div class="bg-white shadow-lg rounded-b-xl p-6">
<!-- Instructions -->
<div id="instructions-box" class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 class="font-bold text-blue-900 mb-2">How it works:</h3>
<ol class="text-sm text-blue-800 space-y-1 list-decimal list-inside">
<li>Click "Start Interview" to begin</li>
<li>Speak naturally - the AI will ask you questions</li>
<li>When done, click "Generate Article"</li>
<li>Review and submit your story to GitHub</li>
</ol>
</div>
<!-- WebRTC Audio Container -->
<div id="audio-container" class="mb-6">
<!-- FastRTC will inject audio elements here -->
</div>
<!-- Status Display -->
<div class="flex flex-col items-center mb-6">
<button
id="start-btn"
onclick="startInterview()"
class="gradient-bg text-white px-8 py-4 rounded-full text-lg font-bold shadow-lg hover:shadow-xl transition-all mb-4">
πŸŽ™οΈ Start Interview
</button>
<div id="status" class="text-center">
<p class="text-gray-600 text-sm">Click above to begin</p>
</div>
<div id="speaking-indicator" class="hidden mt-4">
<div class="flex items-center gap-2 bg-gradient-to-r from-purple-500 to-indigo-500 text-white px-4 py-2 rounded-full speaking">
<div class="w-3 h-3 bg-white rounded-full"></div>
<span class="text-sm font-medium">AI is speaking...</span>
</div>
</div>
<div id="listening-indicator" class="hidden mt-4">
<div class="flex items-center gap-2 bg-red-500 text-white px-4 py-2 rounded-full recording">
<div class="w-3 h-3 bg-white rounded-full"></div>
<span class="text-sm font-medium">Listening...</span>
</div>
</div>
</div>
<!-- Transcript Display -->
<div id="transcript-section" class="mb-6">
<h3 class="font-bold text-gray-800 mb-3">πŸ“ Transcript:</h3>
<div id="transcript" class="bg-gray-50 border border-gray-200 rounded-lg p-4 min-h-[300px] max-h-[500px] overflow-y-auto space-y-3">
<p class="text-gray-400 italic text-sm">Your conversation will appear here...</p>
</div>
</div>
<!-- Actions -->
<div id="action-buttons" class="flex gap-3">
<button
id="end-btn"
onclick="endInterview()"
disabled
class="flex-1 bg-orange-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-orange-700 transition disabled:opacity-50 disabled:cursor-not-allowed">
⏹️ End Interview
</button>
<button
id="generate-btn"
onclick="generateArticle()"
disabled
class="flex-1 gradient-bg text-white px-6 py-3 rounded-lg font-medium hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed">
πŸ“„ Generate Article
</button>
</div>
</div>
</div>
<!-- Article Modal -->
<div id="article-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center p-4 z-50">
<div class="bg-white rounded-xl max-w-4xl w-full max-h-[90vh] flex flex-col shadow-2xl">
<div class="p-4 border-b gradient-bg text-white rounded-t-xl">
<h2 class="text-xl font-bold">πŸ“„ Your Article</h2>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div id="article-content" class="prose max-w-none">
<p class="text-gray-500 italic">Generating...</p>
</div>
</div>
<div id="article-actions" class="p-4 border-t flex gap-3">
<button onclick="closeArticle()" class="px-6 py-2 text-gray-600 hover:bg-gray-100 rounded-lg font-medium">
Close
</button>
<button onclick="downloadArticle()" class="px-6 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 font-medium">
πŸ’Ύ Download
</button>
<button onclick="submitArticle()" id="submit-btn" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-medium">
πŸš€ Submit to GitHub
</button>
</div>
</div>
</div>
<script type="module">
const RTC_CONFIGURATION = __RTC_CONFIGURATION__;
let pc = null;
let audioContext = null;
let webrtcId = null;
let eventSource = null;
let currentArticle = "";
let driverObj = null;
// Check if user has seen tutorial
function checkFirstVisit() {
const hasSeenTutorial = localStorage.getItem('voiceInterviewTutorialSeen');
if (!hasSeenTutorial) {
// Show tutorial after a brief delay
setTimeout(() => {
startTutorial();
localStorage.setItem('voiceInterviewTutorialSeen', 'true');
}, 800);
}
}
function startTutorial() {
const driver = window.driver.js.driver;
if (driverObj) {
driverObj.destroy();
}
driverObj = driver({
showProgress: true,
steps: [
{
element: '#instructions-box',
popover: {
title: 'πŸ‘‹ Welcome to Voice Interview!',
description: 'This tool helps you create project stories through natural conversation. Let me show you around!',
side: "bottom",
align: 'start'
}
},
{
element: '#start-btn',
popover: {
title: 'πŸŽ™οΈ Start Your Interview',
description: 'Click this button to begin. You\'ll need to allow microphone access when prompted. The AI interviewer will ask you questions about your project.',
side: "bottom",
align: 'center'
}
},
{
element: '#transcript-section',
popover: {
title: 'πŸ“ Live Transcript',
description: 'Watch your conversation unfold here in real-time. You\'ll see both your responses and the interviewer\'s questions.',
side: "top",
align: 'start'
}
},
{
element: '#speaking-indicator',
popover: {
title: 'πŸ”Š Status Indicators',
description: 'Purple indicator = AI is speaking. Red indicator = AI is listening to you. These help you know when to talk.',
side: "top",
align: 'center'
}
},
{
element: '#end-btn',
popover: {
title: '⏹️ End Interview',
description: 'When you\'re done sharing your story, click here to stop the interview. You can then generate your article.',
side: "top",
align: 'start'
}
},
{
element: '#generate-btn',
popover: {
title: 'πŸ“„ Generate Article',
description: 'This transforms your conversation into a polished article. The AI will structure your responses into a compelling project story.',
side: "top",
align: 'end'
}
},
{
element: '#help-btn',
popover: {
title: '❓ Need Help?',
description: 'Click this anytime to see this tutorial again. Now you\'re ready to start your interview!',
side: "bottom",
align: 'end'
}
}
],
onDestroyStarted: () => {
if (!driverObj.hasNextStep()) {
driverObj.destroy();
}
}
});
driverObj.drive();
}
async function startInterview() {
const startBtn = document.getElementById('start-btn');
const endBtn = document.getElementById('end-btn');
const status = document.getElementById('status');
startBtn.disabled = true;
status.innerHTML = '<p class="text-blue-600">πŸ”„ Connecting...</p>';
try {
// Create WebRTC connection
pc = new RTCPeerConnection(RTC_CONFIGURATION);
// Generate session ID
webrtcId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
// Get user media
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks().forEach(track => pc.addTrack(track, stream));
// Handle incoming audio
pc.ontrack = (event) => {
const audio = new Audio();
audio.srcObject = event.streams[0];
audio.autoplay = true;
// Show speaking indicator when AI is speaking
audio.onplay = () => {
document.getElementById('speaking-indicator').classList.remove('hidden');
document.getElementById('listening-indicator').classList.add('hidden');
};
audio.onended = () => {
document.getElementById('speaking-indicator').classList.add('hidden');
document.getElementById('listening-indicator').classList.remove('hidden');
};
};
// Create offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// Send offer to server
const response = await fetch('/webrtc/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
webrtc_id: webrtcId,
sdp_offer: pc.localDescription.sdp
})
});
const data = await response.json();
await pc.setRemoteDescription({
type: 'answer',
sdp: data.sdp_answer
});
status.innerHTML = '<p class="text-green-600">βœ… Connected! Speak naturally.</p>';
endBtn.disabled = false;
document.getElementById('generate-btn').disabled = false;
document.getElementById('listening-indicator').classList.remove('hidden');
// Start listening to transcript updates
listenForTranscript();
} catch (error) {
console.error('Failed to start:', error);
status.innerHTML = '<p class="text-red-600">❌ Failed to connect. Please check microphone permissions.</p>';
startBtn.disabled = false;
}
}
function listenForTranscript() {
eventSource = new EventSource(`/outputs?webrtc_id=${webrtcId}`);
eventSource.addEventListener('output', (event) => {
const data = JSON.parse(event.data);
const msg = data.message;
const transcript = document.getElementById('transcript');
const msgDiv = document.createElement('div');
msgDiv.className = 'transcript-msg';
if (msg.role === 'user') {
msgDiv.innerHTML = `
<div class="flex justify-end">
<div class="bg-purple-100 text-purple-900 px-4 py-2 rounded-lg max-w-[80%]">
<p class="text-xs font-semibold mb-1">You:</p>
<p>${msg.content}</p>
</div>
</div>
`;
} else if (msg.role === 'assistant') {
msgDiv.innerHTML = `
<div class="flex justify-start">
<div class="bg-gray-100 text-gray-900 px-4 py-2 rounded-lg max-w-[80%]">
<p class="text-xs font-semibold mb-1">Interviewer:</p>
<p>${msg.content}</p>
</div>
</div>
`;
}
// Remove placeholder if exists
const placeholder = transcript.querySelector('.text-gray-400');
if (placeholder) placeholder.remove();
transcript.appendChild(msgDiv);
transcript.scrollTop = transcript.scrollHeight;
});
}
function endInterview() {
if (pc) {
pc.close();
pc = null;
}
if (eventSource) {
eventSource.close();
eventSource = null;
}
document.getElementById('start-btn').disabled = false;
document.getElementById('end-btn').disabled = true;
document.getElementById('status').innerHTML = '<p class="text-gray-600">Interview ended. Click "Generate Article" when ready.</p>';
document.getElementById('speaking-indicator').classList.add('hidden');
document.getElementById('listening-indicator').classList.add('hidden');
}
async function generateArticle() {
if (!webrtcId) {
alert('No interview session found!');
return;
}
const modal = document.getElementById('article-modal');
modal.classList.remove('hidden');
modal.classList.add('flex');
document.getElementById('article-content').innerHTML = '<p class="text-gray-500 italic">⏳ Generating article... This may take 10-30 seconds...</p>';
try {
const response = await fetch(`/generate_article?webrtc_id=${webrtcId}`, {
method: 'POST'
});
const data = await response.json();
if (data.status === 'success') {
currentArticle = data.article;
document.getElementById('article-content').innerHTML =
`<div class="prose max-w-none">${marked.parse(currentArticle)}</div>`;
// Highlight article actions for first-time users
const hasSeenArticleTutorial = localStorage.getItem('articleActionsSeen');
if (!hasSeenArticleTutorial) {
setTimeout(() => {
highlightArticleActions();
localStorage.setItem('articleActionsSeen', 'true');
}, 500);
}
} else {
document.getElementById('article-content').innerHTML =
`<p class="text-red-600">❌ Error: ${data.error}</p>`;
}
} catch (error) {
document.getElementById('article-content').innerHTML =
`<p class="text-red-600">❌ Failed to generate: ${error.message}</p>`;
}
}
function highlightArticleActions() {
const driver = window.driver.js.driver;
const articleDriver = driver({
showProgress: false,
steps: [
{
element: '#article-content',
popover: {
title: '✨ Your Article is Ready!',
description: 'Review your AI-generated article. You can edit it if needed before downloading or submitting.',
side: "top",
align: 'center'
}
},
{
element: '#article-actions',
popover: {
title: 'πŸ“€ What\'s Next?',
description: 'Download as markdown, submit to GitHub, or close and generate again if needed.',
side: "top",
align: 'center'
}
}
]
});
articleDriver.drive();
}
async function submitArticle() {
if (!currentArticle) return;
const btn = document.getElementById('submit-btn');
btn.disabled = true;
btn.textContent = '⏳ Submitting...';
try {
const response = await fetch('/submit_article', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ article: currentArticle })
});
const data = await response.json();
if (data.status === 'success') {
alert(`βœ… Submitted!\n\nπŸ“ ${data.filename}\n\nπŸ”— ${data.url}`);
closeArticle();
} else {
alert(`❌ Failed: ${data.error}`);
}
} catch (error) {
alert(`❌ Failed: ${error.message}`);
} finally {
btn.disabled = false;
btn.textContent = 'πŸš€ Submit to GitHub';
}
}
function downloadArticle() {
if (!currentArticle) return;
const blob = new Blob([currentArticle], { type: 'text/markdown' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'voice-interview-' + new Date().toISOString().split('T')[0] + '.md';
a.click();
URL.revokeObjectURL(url);
}
function closeArticle() {
document.getElementById('article-modal').classList.add('hidden');
document.getElementById('article-modal').classList.remove('flex');
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', () => {
checkFirstVisit();
});
window.startInterview = startInterview;
window.endInterview = endInterview;
window.generateArticle = generateArticle;
window.submitArticle = submitArticle;
window.downloadArticle = downloadArticle;
window.closeArticle = closeArticle;
window.startTutorial = startTutorial;
</script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</body>
</html>