Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Husky Interview Prep</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Alpine.js --> | |
| <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f3f4f6; | |
| } | |
| .gradient-bg { | |
| background: linear-gradient(to right, #4f46e5, #8b5cf6); | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .orb { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(80px); | |
| z-index: 0; | |
| animation: float 6s ease-in-out infinite; | |
| opacity: 0.5; | |
| } | |
| .orb-1 { | |
| width: 300px; | |
| height: 300px; | |
| background: #4f46e5; | |
| top: -150px; | |
| right: -100px; | |
| animation-delay: 0s; | |
| } | |
| .orb-2 { | |
| width: 250px; | |
| height: 250px; | |
| background: #8b5cf6; | |
| bottom: -100px; | |
| left: -50px; | |
| animation-delay: 3s; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-20px); } | |
| } | |
| .tab-active { | |
| background-color: #4f46e5; | |
| color: white; | |
| } | |
| .question-card { | |
| transition: all 0.3s ease; | |
| } | |
| .question-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); | |
| } | |
| .btn-primary { | |
| background-color: #4f46e5; | |
| color: white; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-primary:hover { | |
| background-color: #4338ca; | |
| transform: translateY(-1px); | |
| } | |
| .btn-secondary { | |
| background-color: #f3f4f6; | |
| color: #1f2937; | |
| border: 1px solid #d1d5db; | |
| transition: all 0.3s ease; | |
| } | |
| .btn-secondary:hover { | |
| background-color: #e5e7eb; | |
| transform: translateY(-1px); | |
| } | |
| .star-rating { | |
| display: inline-block; | |
| } | |
| .recording-pulse { | |
| position: relative; | |
| } | |
| .recording-pulse::before { | |
| content: ''; | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| border-radius: 50%; | |
| background-color: rgba(239, 68, 68, 0.7); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { transform: scale(1); opacity: 1; } | |
| 100% { transform: scale(1.5); opacity: 0; } | |
| } | |
| .section-enter { | |
| transition: all 0.5s ease; | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| .section-enter-active { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| /* Add these to the existing style section in the head */ | |
| .star-1 { color: #FF5252; } /* Red */ | |
| .star-2 { color: #FF7F00; } /* Orange */ | |
| .star-3 { color: #FFFF00; } /* Yellow */ | |
| .star-4 { color: #7FFF00; } /* Chartreuse */ | |
| .star-5 { color: #00FF00; } /* Green */ | |
| .star-6 { color: #00FFFF; } /* Cyan */ | |
| .star-7 { color: #007FFF; } /* Azure */ | |
| .star-8 { color: #0000FF; } /* Blue */ | |
| .star-9 { color: #7F00FF; } /* Violet */ | |
| .star-10 { color: #FF00FF; } /* Magenta */ | |
| .star-empty { color: #cccccc; } /* Gray for empty stars */ | |
| </style> | |
| </head> | |
| <body> | |
| <div class="min-h-screen" x-data="app()"> | |
| <!-- Header --> | |
| <header class="gradient-bg py-12 px-4 relative overflow-hidden"> | |
| <div class="orb orb-1"></div> | |
| <div class="orb orb-2"></div> | |
| <div class="container relative z-10"> | |
| <div class="text-center"> | |
| <div class="inline-block px-4 py-2 rounded-full bg-white/10 backdrop-blur-lg border border-white/20 mb-6"> | |
| <span class="inline-block w-2 h-2 rounded-full bg-green-400 mr-2"></span> | |
| <span class="text-white text-sm font-medium">AI-Powered Interview Prep</span> | |
| </div> | |
| <h1 class="text-4xl md:text-5xl font-bold text-white mb-4">Husky Interview Prep</h1> | |
| <p class="text-xl text-white/80 max-w-2xl mx-auto">Master your interview with confidence. AI-powered preparation for job seekers.</p> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="container px-4 py-8"> | |
| <!-- Step 1: Enter Information --> | |
| <section class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">1</span> | |
| Enter Your Information | |
| </h2> | |
| <div class="flex flex-col gap-4 max-w-3xl mx-auto"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Job Description</label> | |
| <textarea x-model="jobDesc" class="w-full h-32 px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Paste the job description here..."></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Company Information</label> | |
| <textarea x-model="companyInfo" class="w-full h-32 px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Enter information about the company..."></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Your Resume</label> | |
| <textarea x-model="resume" class="w-full h-32 px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" placeholder="Paste your resume or relevant experience here..."></textarea> | |
| </div> | |
| </div> | |
| <div class="mt-6 flex justify-center"> | |
| <button @click="analyzeInfo()" :disabled="isAnalyzing" class="btn-primary px-6 py-3 rounded-lg font-medium flex items-center"> | |
| <span class="spinner" x-show="isAnalyzing"></span> | |
| <i class="fas fa-search mr-2" x-show="!isAnalyzing"></i> | |
| <span x-text="isAnalyzing ? 'Analyzing...' : 'Analyze Information'"></span> | |
| </button> | |
| </div> | |
| <template x-if="parsedInfo.company_values"> | |
| <div class="mt-8"> | |
| <div class="grid md:grid-cols-2 gap-6 mb-8"> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Company Values</h3> | |
| <ul class="list-disc pl-5 space-y-1 text-gray-700"> | |
| <template x-for="(line, index) in parsedInfo.company_values.split('- ').filter(item => item.trim().length > 0)" :key="index"> | |
| <li x-text="line.trim()"></li> | |
| </template> | |
| </ul> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Job Duties</h3> | |
| <ul class="list-disc pl-5 space-y-1 text-gray-700"> | |
| <template x-for="(line, index) in parsedInfo.job_duties.split('- ').filter(item => item.trim().length > 0)" :key="index"> | |
| <li x-text="line.trim()"></li> | |
| </template> | |
| </ul> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Tech Skills</h3> | |
| <ul class="list-disc pl-5 space-y-1 text-gray-700"> | |
| <template x-for="(line, index) in parsedInfo.tech_skills.split('- ').filter(item => item.trim().length > 0)" :key="index"> | |
| <li x-text="line.trim()"></li> | |
| </template> | |
| </ul> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Soft Skills</h3> | |
| <ul class="list-disc pl-5 space-y-1 text-gray-700"> | |
| <template x-for="(line, index) in parsedInfo.soft_skills.split('- ').filter(item => item.trim().length > 0)" :key="index"> | |
| <li x-text="line.trim()"></li> | |
| </template> | |
| </ul> | |
| </div> | |
| </div> | |
| <div class="flex justify-center"> | |
| <button @click="generateQuestions()" :disabled="isGeneratingQuestions" class="btn-primary px-6 py-3 rounded-lg font-medium flex items-center"> | |
| <span class="spinner" x-show="isGeneratingQuestions"></span> | |
| <i class="fas fa-list-ul mr-2" x-show="!isGeneratingQuestions"></i> | |
| <span x-text="isGeneratingQuestions ? 'Generating...' : 'Generate Interview Questions'"></span> | |
| </button> | |
| </div> | |
| </div> | |
| </template> | |
| </section> | |
| <!-- Step 2: Practice Questions --> | |
| <section x-show="questionsGenerated" class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">2</span> | |
| Practice Questions | |
| </h2> | |
| <!-- Question Category Tabs --> | |
| <div class="mb-6"> | |
| <div class="flex flex-wrap space-x-2 border-b border-gray-200"> | |
| <template x-for="(category, index) in Object.keys(questions)" :key="index"> | |
| <button | |
| @click="activeCategory = category" | |
| :class="{'tab-active': activeCategory === category}" | |
| class="px-4 py-2 rounded-t-lg font-medium transition-all duration-200" | |
| x-text="category" | |
| ></button> | |
| </template> | |
| </div> | |
| </div> | |
| <!-- Questions for Selected Category --> | |
| <div> | |
| <template x-for="(categoryQuestions, category) in questions" :key="category"> | |
| <div x-show="activeCategory === category"> | |
| <template x-for="(question, qIndex) in categoryQuestions" :key="qIndex"> | |
| <div | |
| @click="selectQuestion(question)" | |
| class="question-card p-4 rounded-lg border border-gray-200 bg-gray-50 mb-3 cursor-pointer hover:bg-indigo-50 hover:border-indigo-200" | |
| :class="{'border-indigo-500 bg-indigo-50': selectedQuestion === question}" | |
| > | |
| <p class="text-gray-800" x-text="question"></p> | |
| </div> | |
| </template> | |
| </div> | |
| </template> | |
| </div> | |
| </section> | |
| <!-- Step 3: Record Answer --> | |
| <section x-show="selectedQuestion" class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">3</span> | |
| Record Your Answer | |
| </h2> | |
| <div class="grid md:grid-cols-3 gap-6"> | |
| <!-- Interviewer Column --> | |
| <div class="md:col-span-1"> | |
| <div class="text-center"> | |
| <div class="rounded-lg overflow-hidden mb-4 mx-auto w-48 h-48 flex items-center justify-center bg-gray-100"> | |
| <img src="/static/interviewer.png" alt="Interviewer" class="max-w-full max-h-full"> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Voice Accent</label> | |
| <select x-model="voiceOption" class="w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"> | |
| <option value="US English">US English</option> | |
| <option value="UK English">UK English</option> | |
| <option value="Australian English">Australian English</option> | |
| <option value="Indian English">Indian English</option> | |
| <option value="French">French</option> | |
| <option value="German">German</option> | |
| <option value="Spanish">Spanish</option> | |
| <option value="Italian">Italian</option> | |
| <option value="Japanese">Japanese</option> | |
| <option value="Korean">Korean</option> | |
| </select> | |
| </div> | |
| <button @click="readQuestionAloud()" :disabled="isReadingAloud" class="btn-secondary w-full px-4 py-2 rounded-lg font-medium flex items-center justify-center"> | |
| <span class="spinner" x-show="isReadingAloud"></span> | |
| <i class="fas fa-volume-up mr-2" x-show="!isReadingAloud"></i> | |
| <span x-text="isReadingAloud ? 'Reading...' : 'Read Question Aloud'"></span> | |
| </button> | |
| <div x-show="audioPlaying" class="mt-4"> | |
| <audio x-ref="audioPlayer" controls class="w-full"> | |
| Your browser does not support the audio element. | |
| </audio> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Question and Recording Column --> | |
| <div class="md:col-span-2"> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Current Question</label> | |
| <div class="p-4 bg-indigo-50 rounded-lg border border-indigo-100"> | |
| <p class="text-gray-800 font-medium" x-text="selectedQuestion"></p> | |
| </div> | |
| <div class="mt-4 p-4 bg-gray-50 rounded-lg border border-gray-200"> | |
| <h3 class="font-medium text-gray-800 mb-2">How to Answer</h3> | |
| <p class="text-gray-700" x-text="questionHints[selectedQuestion] || 'No specific hint available for this question.'"></p> | |
| </div> | |
| </div> | |
| <div class="mb-6"> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Record Your Answer</label> | |
| <div class="flex flex-col items-center"> | |
| <button @click="toggleRecording()" class="mb-4 relative"> | |
| <div :class="{'recording-pulse': isRecording}" class="w-16 h-16 rounded-full bg-white border-2 border-gray-300 flex items-center justify-center"> | |
| <i :class="isRecording ? 'fa-stop text-red-500' : 'fa-microphone text-gray-700'" class="fas text-2xl"></i> | |
| </div> | |
| </button> | |
| <p x-text="recordingStatus" class="text-sm font-medium text-gray-600"></p> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Your Answer (Transcribed)</label> | |
| <textarea | |
| x-model="answerText" | |
| class="w-full h-40 px-4 py-3 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| placeholder="Your transcribed answer will appear here..." | |
| ></textarea> | |
| </div> | |
| <div class="mt-4"> | |
| <button @click="analyzeAnswer()" :disabled="isAnalyzingAnswer" class="btn-primary px-6 py-3 rounded-lg font-medium flex items-center"> | |
| <span class="spinner" x-show="isAnalyzingAnswer"></span> | |
| <i class="fas fa-chart-line mr-2" x-show="!isAnalyzingAnswer"></i> | |
| <span x-text="isAnalyzingAnswer ? 'Analyzing...' : 'Analyze Answer'"></span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Step 4: Review Analysis --> | |
| <section x-show="feedbackText" class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">4</span> | |
| Review Analysis | |
| </h2> | |
| <div x-show="scores" class="grid md:grid-cols-3 gap-4 mb-6"> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Clarity</h3> | |
| <div class="text-2xl"> | |
| <template x-for="i in 10" :key="i"> | |
| <span :class="i <= scores.clarity ? `star-${i}` : 'star-empty'" | |
| x-text="i <= scores.clarity ? '★' : '☆'"></span> | |
| </template> | |
| </div> | |
| <p class="text-sm text-gray-600 mt-1">Score: <span x-text="scores.clarity"></span>/10</p> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Relevance</h3> | |
| <div class="text-2xl"> | |
| <template x-for="i in 10" :key="i"> | |
| <span :class="i <= scores.relevance ? `star-${i}` : 'star-empty'" | |
| x-text="i <= scores.relevance ? '★' : '☆'"></span> | |
| </template> | |
| </div> | |
| <p class="text-sm text-gray-600 mt-1">Score: <span x-text="scores.relevance"></span>/10</p> | |
| </div> | |
| <div class="bg-gray-50 p-4 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-2">Confidence</h3> | |
| <div class="text-2xl"> | |
| <template x-for="i in 10" :key="i"> | |
| <span :class="i <= scores.confidence ? `star-${i}` : 'star-empty'" | |
| x-text="i <= scores.confidence ? '★' : '☆'"></span> | |
| </template> | |
| </div> | |
| <p class="text-sm text-gray-600 mt-1">Score: <span x-text="scores.confidence"></span>/10</p> | |
| </div> | |
| </div> | |
| <div class="bg-gray-50 p-6 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-3">Detailed Feedback</h3> | |
| <div class="prose max-w-none text-gray-700 whitespace-pre-line leading-relaxed" | |
| x-text="feedbackText"></div> | |
| </div> | |
| </section> | |
| <!-- Step 5: Model Answer --> | |
| <section x-show="feedbackText" class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">5</span> | |
| Get Model Answer | |
| </h2> | |
| <button @click="generateModelAnswer()" :disabled="isGeneratingModel" class="btn-primary px-6 py-3 rounded-lg font-medium flex items-center mb-6"> | |
| <span class="spinner" x-show="isGeneratingModel"></span> | |
| <i class="fas fa-magic mr-2" x-show="!isGeneratingModel"></i> | |
| <span x-text="isGeneratingModel ? 'Generating...' : 'Generate Model Answer'"></span> | |
| </button> | |
| <template x-if="modelAnswer"> | |
| <div class="bg-gray-50 p-6 rounded-lg"> | |
| <h3 class="font-semibold text-gray-800 mb-3">Sample Professional Answer</h3> | |
| <div class="prose max-w-none text-gray-700 whitespace-pre-line leading-relaxed" | |
| x-text="modelAnswer"></div> | |
| </div> | |
| </template> | |
| </section> | |
| <!-- Step 6: Save Work --> | |
| <section x-show="feedbackText && modelAnswer" class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">6</span> | |
| Save Your Work | |
| </h2> | |
| <button @click="saveToHTML()" :disabled="isSaving" class="btn-primary px-6 py-3 rounded-lg font-medium flex items-center"> | |
| <span class="spinner" x-show="isSaving"></span> | |
| <i class="fas fa-download mr-2" x-show="!isSaving"></i> | |
| <span x-text="isSaving ? 'Saving...' : 'Save as HTML'"></span> | |
| </button> | |
| <template x-if="downloadLink"> | |
| <div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg"> | |
| <p class="text-green-800 mb-3">Your file is ready to download!</p> | |
| <a :href="downloadLink" class="btn-primary px-6 py-3 rounded-lg font-medium inline-flex items-center" download> | |
| <i class="fas fa-cloud-download-alt mr-2"></i> Download File | |
| </a> | |
| </div> | |
| </template> | |
| </section> | |
| <!-- Step 7: Continue or Start New --> | |
| <section x-show="feedbackText && modelAnswer" class="bg-white rounded-xl shadow-md p-6 mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6 flex items-center"> | |
| <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 mr-3">7</span> | |
| Continue Your Practice | |
| </h2> | |
| <div class="grid md:grid-cols-2 gap-6"> | |
| <div class="bg-gray-50 p-6 rounded-lg text-center"> | |
| <h3 class="font-semibold text-gray-800 mb-4">Practice Another Question</h3> | |
| <p class="text-gray-600 mb-4">Continue practicing with the same job information and try another interview question.</p> | |
| <button @click="practiceAnotherQuestion()" class="btn-primary px-6 py-3 rounded-lg font-medium"> | |
| <i class="fas fa-redo mr-2"></i> Try Another Question | |
| </button> | |
| </div> | |
| <div class="bg-gray-50 p-6 rounded-lg text-center"> | |
| <h3 class="font-semibold text-gray-800 mb-4">Start Fresh</h3> | |
| <p class="text-gray-600 mb-4">Clear all data and start over with a different company or position.</p> | |
| <button @click="startOver()" class="btn-secondary px-6 py-3 rounded-lg font-medium"> | |
| <i class="fas fa-sync mr-2"></i> Start New Practice | |
| </button> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Footer --> | |
| <footer class="bg-gray-800 py-8 px-4 text-center"> | |
| <div class="container"> | |
| <p class="text-gray-300">Husky Interview Prep © 2023. All rights reserved.</p> | |
| <p class="text-gray-400 text-sm mt-2">Powered by AI and created to help you succeed.</p> | |
| </div> | |
| </footer> | |
| </div> | |
| <script> | |
| function app() { | |
| return { | |
| jobDesc: '', | |
| companyInfo: '', | |
| resume: '', | |
| parsedInfo: {}, | |
| // Questions | |
| questions: {}, | |
| questionHints: {}, | |
| activeCategory: 'Introduction', | |
| selectedQuestion: '', | |
| questionsGenerated: false, | |
| // Loading states | |
| isAnalyzing: false, | |
| isGeneratingQuestions: false, | |
| isReadingAloud: false, | |
| isTranscribing: false, | |
| isAnalyzingAnswer: false, | |
| isGeneratingModel: false, | |
| isSaving: false, | |
| // Recording | |
| isRecording: false, | |
| recorder: null, | |
| audioChunks: [], | |
| recordingStatus: 'Click to start recording', | |
| answerText: '', | |
| // Analysis | |
| scores: null, | |
| feedbackText: '', | |
| modelAnswer: '', | |
| // Audio playback | |
| voiceOption: 'US English', | |
| audioSrc: '', | |
| audioPlaying: false, | |
| // Download | |
| downloadLink: '', | |
| async analyzeInfo() { | |
| if (!this.jobDesc || !this.companyInfo) { | |
| alert('Please enter job description and company information.'); | |
| return; | |
| } | |
| this.isAnalyzing = true; // Start loading indicator | |
| try { | |
| const response = await fetch('/analyze-info', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| job_desc: this.jobDesc, | |
| company_info: this.companyInfo | |
| }), | |
| }); | |
| this.parsedInfo = await response.json(); | |
| } catch (error) { | |
| console.error('Error analyzing information:', error); | |
| alert('Error analyzing information. Please try again.'); | |
| } finally { | |
| this.isAnalyzing = false; // Stop loading indicator | |
| } | |
| }, | |
| async generateQuestions() { | |
| if (!this.jobDesc) { | |
| alert('Please enter at least the job description.'); | |
| return; | |
| } | |
| this.isGeneratingQuestions = true; // Start loading indicator | |
| try { | |
| const response = await fetch('/generate-questions', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| job_desc: this.jobDesc, | |
| company_info: this.companyInfo, | |
| resume: this.resume | |
| }), | |
| }); | |
| const data = await response.json(); | |
| this.questions = data.questions; | |
| this.questionHints = data.hints; | |
| this.questionsGenerated = true; | |
| // Set default active category to the first one | |
| if (Object.keys(this.questions).length > 0) { | |
| this.activeCategory = Object.keys(this.questions)[0]; | |
| } | |
| } catch (error) { | |
| console.error('Error generating questions:', error); | |
| alert('Error generating questions. Please try again.'); | |
| } finally { | |
| this.isGeneratingQuestions = false; // Stop loading indicator | |
| } | |
| }, | |
| selectQuestion(question) { | |
| this.selectedQuestion = question; | |
| // Reset related data | |
| this.answerText = ''; | |
| this.scores = null; | |
| this.feedbackText = ''; | |
| this.modelAnswer = ''; | |
| this.audioSrc = ''; | |
| this.audioPlaying = false; | |
| }, | |
| async readQuestionAloud() { | |
| if (!this.selectedQuestion) return; | |
| this.isReadingAloud = true; // Start loading indicator | |
| try { | |
| const response = await fetch('/text-to-speech', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| text: this.selectedQuestion, | |
| voice_option: this.voiceOption | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (data.audio) { | |
| this.audioSrc = data.audio; | |
| this.audioPlaying = true; | |
| // Give DOM time to update before accessing the audio element | |
| setTimeout(() => { | |
| const audio = this.$refs.audioPlayer; | |
| if (audio) { | |
| audio.src = data.audio; | |
| audio.load(); | |
| audio.play().catch(e => console.error('Error playing audio:', e)); | |
| } | |
| }, 100); | |
| } | |
| } catch (error) { | |
| console.error('Error reading question aloud:', error); | |
| alert('Error reading question aloud. Please try again.'); | |
| } finally { | |
| this.isReadingAloud = false; // Stop loading indicator | |
| } | |
| }, | |
| toggleRecording() { | |
| if (this.isRecording) { | |
| this.stopRecording(); | |
| } else { | |
| this.startRecording(); | |
| } | |
| }, | |
| async startRecording() { | |
| try { | |
| // Set audio constraints for better compatibility | |
| const stream = await navigator.mediaDevices.getUserMedia({ | |
| audio: { | |
| channelCount: 1, | |
| sampleRate: 16000, | |
| sampleSize: 16, | |
| echoCancellation: true, | |
| noiseSuppression: true | |
| } | |
| }); | |
| this.recorder = new MediaRecorder(stream, { | |
| mimeType: 'audio/webm' // More widely supported in browsers | |
| }); | |
| this.audioChunks = []; | |
| this.recorder.ondataavailable = (e) => { | |
| if (e.data.size > 0) { | |
| this.audioChunks.push(e.data); | |
| } | |
| }; | |
| this.recorder.onstop = async () => { | |
| const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' }); | |
| const reader = new FileReader(); | |
| reader.readAsDataURL(audioBlob); | |
| reader.onload = async () => { | |
| const base64Audio = reader.result; | |
| try { | |
| const response = await fetch('/speech-to-text', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ audio: base64Audio }), | |
| }); | |
| const data = await response.json(); | |
| this.answerText = data.text; | |
| this.recordingStatus = 'Recording transcribed'; | |
| } catch (error) { | |
| console.error('Error transcribing audio:', error); | |
| this.recordingStatus = 'Error transcribing audio'; | |
| } finally { | |
| this.isTranscribing = false; // Stop transcribing indicator | |
| } | |
| }; | |
| }; | |
| this.recorder.start(); | |
| this.isRecording = true; | |
| this.recordingStatus = 'Recording... Click to stop'; | |
| } catch (error) { | |
| console.error('Error starting recording:', error); | |
| this.recordingStatus = 'Error accessing microphone'; | |
| } | |
| }, | |
| stopRecording() { | |
| if (this.recorder && this.recorder.state !== 'inactive') { | |
| this.recorder.stop(); | |
| this.isRecording = false; | |
| this.isTranscribing = true; // Start transcribing indicator | |
| this.recordingStatus = 'Processing...'; | |
| // Stop all audio tracks | |
| this.recorder.stream.getTracks().forEach(track => track.stop()); | |
| } | |
| }, | |
| async analyzeAnswer() { | |
| if (!this.answerText) { | |
| alert('Please record or enter an answer to analyze.'); | |
| return; | |
| } | |
| this.isAnalyzingAnswer = true; // Start loading indicator | |
| try { | |
| const response = await fetch('/analyze-answer', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| answer_text: this.answerText, | |
| job_desc: this.jobDesc, | |
| company_values: this.parsedInfo.company_values | |
| }), | |
| }); | |
| const data = await response.json(); | |
| // Ensure scores are properly initialized with numeric values | |
| this.scores = { | |
| clarity: parseInt(data.scores.clarity) || 0, | |
| relevance: parseInt(data.scores.relevance) || 0, | |
| confidence: parseInt(data.scores.confidence) || 0 | |
| }; | |
| this.feedbackText = data.feedback; | |
| } catch (error) { | |
| console.error('Error analyzing answer:', error); | |
| alert('Error analyzing answer. Please try again.'); | |
| } finally { | |
| this.isAnalyzingAnswer = false; // Stop loading indicator | |
| } | |
| }, | |
| async generateModelAnswer() { | |
| if (!this.selectedQuestion) { | |
| alert('Please select a question first.'); | |
| return; | |
| } | |
| this.isGeneratingModel = true; // Start loading indicator | |
| try { | |
| const response = await fetch('/generate-model-answer', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| question: this.selectedQuestion, | |
| company_info: this.companyInfo, | |
| job_desc: this.jobDesc, | |
| resume: this.resume, | |
| answer_text: this.answerText | |
| }), | |
| }); | |
| const data = await response.json(); | |
| this.modelAnswer = data.model_answer; | |
| } catch (error) { | |
| console.error('Error generating model answer:', error); | |
| alert('Error generating model answer. Please try again.'); | |
| } finally { | |
| this.isGeneratingModel = false; // Stop loading indicator | |
| } | |
| }, | |
| async saveToHTML() { | |
| this.isSaving = true; // Start loading indicator | |
| try { | |
| const response = await fetch('/save-to-html', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| job_desc: this.jobDesc, | |
| company_info: this.companyInfo, | |
| resume: this.resume, | |
| company_values: this.parsedInfo.company_values, | |
| tech_skills: this.parsedInfo.tech_skills, | |
| soft_skills: this.parsedInfo.soft_skills, | |
| job_duties: this.parsedInfo.job_duties, | |
| selected_question: this.selectedQuestion, | |
| answer_text: this.answerText, | |
| feedback: this.feedbackText, | |
| model_answer: this.modelAnswer | |
| }), | |
| }); | |
| const data = await response.json(); | |
| this.downloadLink = `/download-html/${data.file_id}`; | |
| } catch (error) { | |
| console.error('Error saving to HTML:', error); | |
| alert('Error saving to HTML. Please try again.'); | |
| } finally { | |
| this.isSaving = false; // Stop loading indicator | |
| } | |
| }, | |
| practiceAnotherQuestion() { | |
| // Reset answer-related data but keep job info | |
| this.selectedQuestion = ''; | |
| this.answerText = ''; | |
| this.scores = null; | |
| this.feedbackText = ''; | |
| this.modelAnswer = ''; | |
| this.audioSrc = ''; | |
| this.audioPlaying = false; | |
| this.downloadLink = ''; | |
| // Scroll to the questions section | |
| document.querySelector('section:nth-of-type(2)').scrollIntoView({ behavior: 'smooth' }); | |
| }, | |
| startOver() { | |
| // Reset everything | |
| this.jobDesc = ''; | |
| this.companyInfo = ''; | |
| this.resume = ''; | |
| this.parsedInfo = {}; | |
| this.questions = {}; | |
| this.questionHints = {}; | |
| this.activeCategory = 'Introduction'; | |
| this.selectedQuestion = ''; | |
| this.questionsGenerated = false; | |
| this.answerText = ''; | |
| this.scores = null; | |
| this.feedbackText = ''; | |
| this.modelAnswer = ''; | |
| this.audioSrc = ''; | |
| this.audioPlaying = false; | |
| this.downloadLink = ''; | |
| // Scroll to top | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| }; | |
| } | |
| </script> | |
| </body> | |
| </html> | |