interview / index.html
jaswanthsai1's picture
Update index.html
7ef83ea verified
Raw
History Blame Contribute Delete
43.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
<title>InterviewAI Pro β€” Smart Coach</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=DM+Mono:wght@400;500&family=Syne:wght@700;800&display=swap" rel="stylesheet"/>
<style>
:root {
--bg: #050b15;
--surface: #0a1628;
--card: #0d1f35;
--border: rgba(56, 189, 248, 0.12);
--glow: #38bdf8;
--glow2: #818cf8;
--accent: #06b6d4;
--accent2: #a78bfa;
--green: #34d399;
--orange: #fb923c;
--red: #f87171;
--text: #e2f4ff;
--muted: #7ba5c4;
--font: 'Space Grotesk', sans-serif;
--mono: 'DM Mono', monospace;
--display: 'Syne', sans-serif;
}
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* Animated mesh background */
body::before {
content: '';
position: fixed;
inset: 0;
background:
radial-gradient(ellipse 80% 60% at 20% 10%, rgba(56,189,248,0.07) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 80%, rgba(129,140,248,0.06) 0%, transparent 60%),
radial-gradient(ellipse 40% 30% at 50% 50%, rgba(6,182,212,0.04) 0%, transparent 50%);
pointer-events: none;
z-index: 0;
}
/* Grid dot pattern */
body::after {
content: '';
position: fixed;
inset: 0;
background-image: radial-gradient(circle, rgba(56,189,248,0.06) 1px, transparent 1px);
background-size: 32px 32px;
pointer-events: none;
z-index: 0;
}
.app-wrap {
position: relative;
z-index: 1;
max-width: 430px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* STATUS BAR */
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px 6px;
font-size: 11px;
font-family: var(--mono);
color: var(--muted);
}
.status-bar .time { font-size: 13px; font-weight: 500; color: var(--text); }
.status-icons { display: flex; gap: 6px; align-items: center; }
.status-icons svg { width: 14px; height: 14px; fill: var(--muted); }
/* TOP NAV */
.top-nav {
padding: 4px 20px 12px;
display: flex;
justify-content: space-between;
align-items: center;
}
.app-title {
font-family: var(--display);
font-size: 22px;
font-weight: 800;
background: linear-gradient(135deg, #38bdf8, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
.nav-badge {
width: 36px; height: 36px;
border-radius: 50%;
background: var(--card);
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
font-size: 17px;
cursor: pointer;
transition: all 0.2s;
}
.nav-badge:active { transform: scale(0.92); }
/* HERO CARD */
.hero-card {
margin: 0 16px 16px;
background: linear-gradient(135deg, #0d2a4a, #0f1f3d);
border: 1px solid rgba(56,189,248,0.2);
border-radius: 24px;
padding: 20px;
position: relative;
overflow: hidden;
}
.hero-card::before {
content: '';
position: absolute;
top: -40px; right: -40px;
width: 180px; height: 180px;
border-radius: 50%;
background: radial-gradient(circle, rgba(56,189,248,0.15), transparent 70%);
pointer-events: none;
}
.hero-label {
font-size: 11px;
font-family: var(--mono);
color: var(--accent);
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 6px;
}
.hero-title {
font-family: var(--display);
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
line-height: 1.2;
}
.hero-sub { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
/* Session Score Ring */
.session-row {
display: flex;
gap: 14px;
align-items: center;
}
.score-ring-wrap {
position: relative;
width: 72px; height: 72px;
flex-shrink: 0;
}
.score-ring-wrap svg { transform: rotate(-90deg); }
.score-center {
position: absolute;
inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.score-num {
font-family: var(--display);
font-size: 20px;
font-weight: 800;
color: var(--text);
line-height: 1;
}
.score-label { font-size: 8px; color: var(--muted); letter-spacing: 1px; text-transform: uppercase; }
.hero-stats { flex: 1; display: flex; flex-direction: column; gap: 8px; }
.hstat {
display: flex; justify-content: space-between; align-items: center;
}
.hstat-name { font-size: 12px; color: var(--muted); }
.hstat-bar-wrap { flex: 1; margin: 0 10px; height: 4px; border-radius: 2px; background: rgba(255,255,255,0.06); }
.hstat-bar {
height: 100%; border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
width: 0%;
transition: width 1.2s cubic-bezier(.2,.8,.4,1);
}
.hstat-val { font-size: 12px; font-family: var(--mono); color: var(--text); min-width: 28px; text-align: right; }
/* VIDEO SECTION */
.section-head {
padding: 4px 20px 10px;
display: flex; justify-content: space-between; align-items: center;
}
.section-title {
font-size: 13px;
font-family: var(--mono);
color: var(--muted);
letter-spacing: 1px;
text-transform: uppercase;
}
.section-action {
font-size: 11px; color: var(--accent);
font-family: var(--mono);
letter-spacing: 1px;
}
.video-card {
margin: 0 16px 16px;
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--border);
background: #000;
position: relative;
}
.video-card video { display: block; width: 100%; aspect-ratio: 4/3; object-fit: cover; background: #000; }
.video-card canvas { position: absolute; inset: 0; pointer-events: none; width: 100%; height: 100%; }
/* Overlay pill badges */
.vid-badges {
position: absolute;
top: 12px; left: 12px; right: 12px;
display: flex; gap: 8px; flex-wrap: wrap;
}
.vid-badge {
background: rgba(5,11,21,0.75);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(56,189,248,0.2);
border-radius: 20px;
padding: 5px 12px;
font-size: 11px;
font-family: var(--mono);
color: var(--text);
display: flex; align-items: center; gap: 6px;
}
.vid-badge .dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--green);
animation: pulse-dot 1.5s infinite;
}
@keyframes pulse-dot {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.7); }
}
.vid-badge.mood .dot { background: var(--accent2); }
.vid-badge.conf .dot { background: var(--orange); }
/* REC indicator */
.rec-indicator {
position: absolute;
bottom: 12px; right: 12px;
display: none;
align-items: center; gap: 6px;
background: rgba(248,113,113,0.15);
border: 1px solid rgba(248,113,113,0.3);
border-radius: 20px;
padding: 5px 12px;
font-size: 11px;
font-family: var(--mono);
color: var(--red);
}
.rec-indicator.active { display: flex; }
.rec-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--red);
animation: pulse-dot 1s infinite;
}
/* CONTROLS BAR */
.ctrl-card {
margin: 0 16px 16px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 16px;
display: flex;
gap: 10px;
align-items: center;
}
.btn-start {
flex: 1;
background: linear-gradient(135deg, #0ea5e9, #6366f1);
color: #fff;
border: none;
border-radius: 14px;
padding: 14px;
font-family: var(--display);
font-size: 15px;
font-weight: 700;
cursor: pointer;
letter-spacing: 0.5px;
transition: all 0.25s;
box-shadow: 0 8px 24px rgba(14,165,233,0.3);
position: relative;
overflow: hidden;
}
.btn-start::after {
content: '';
position: absolute;
inset: 0;
background: rgba(255,255,255,0);
transition: background 0.15s;
}
.btn-start:active::after { background: rgba(255,255,255,0.12); }
.btn-start:disabled {
background: rgba(255,255,255,0.05);
box-shadow: none;
color: var(--muted);
cursor: not-allowed;
}
.btn-icon {
width: 48px; height: 48px;
border-radius: 14px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s;
color: var(--muted);
font-size: 18px;
}
.btn-icon:active { transform: scale(0.9); background: rgba(255,255,255,0.08); }
.btn-icon:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-icon.danger { color: var(--red); border-color: rgba(248,113,113,0.2); }
.btn-icon.success { color: var(--green); border-color: rgba(52,211,153,0.2); }
/* MODE TOGGLE */
.mode-card {
margin: 0 16px 16px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 5px;
display: flex;
gap: 4px;
}
.mode-btn {
flex: 1;
padding: 10px;
border-radius: 16px;
border: none;
background: transparent;
color: var(--muted);
font-family: var(--font);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
.mode-btn.active {
background: linear-gradient(135deg, rgba(56,189,248,0.15), rgba(129,140,248,0.15));
color: var(--text);
border: 1px solid rgba(56,189,248,0.2);
}
/* METRICS GRID */
.metrics-grid {
margin: 0 16px 16px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.metric-card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 18px;
padding: 14px;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.metric-card::before {
content: '';
position: absolute;
top: -20px; right: -20px;
width: 80px; height: 80px;
border-radius: 50%;
opacity: 0.08;
pointer-events: none;
}
.metric-card.m-presence::before { background: var(--green); }
.metric-card.m-wpm::before { background: var(--accent); }
.metric-card.m-fillers::before { background: var(--orange); }
.metric-card.m-quality::before { background: var(--accent2); }
.metric-icon { font-size: 20px; margin-bottom: 8px; }
.metric-val {
font-family: var(--display);
font-size: 26px;
font-weight: 800;
line-height: 1;
margin-bottom: 2px;
}
.metric-val.good { color: var(--green); }
.metric-val.warn { color: var(--orange); }
.metric-val.neutral { color: var(--accent); }
.metric-val.purple { color: var(--accent2); }
.metric-name { font-size: 11px; color: var(--muted); letter-spacing: 0.5px; }
.metric-delta {
position: absolute;
top: 12px; right: 14px;
font-size: 10px;
font-family: var(--mono);
color: var(--green);
background: rgba(52,211,153,0.1);
border-radius: 6px;
padding: 2px 6px;
}
/* TRANSCRIPT */
.transcript-card {
margin: 0 16px 16px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 16px;
}
.transcript-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 12px;
}
.transcript-title { font-size: 13px; font-family: var(--mono); color: var(--muted); letter-spacing: 1px; text-transform: uppercase; }
.live-tag {
font-size: 9px;
font-family: var(--mono);
letter-spacing: 2px;
color: var(--accent);
background: rgba(6,182,212,0.1);
border: 1px solid rgba(6,182,212,0.2);
border-radius: 6px;
padding: 2px 7px;
text-transform: uppercase;
display: none;
}
.live-tag.active { display: block; }
.transcript-body {
min-height: 80px;
max-height: 150px;
overflow-y: auto;
font-size: 14px;
line-height: 1.7;
color: var(--text);
font-family: var(--mono);
padding: 2px;
}
.transcript-body::-webkit-scrollbar { width: 3px; }
.transcript-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.placeholder-text { color: var(--muted); font-style: italic; }
/* ANALYSIS */
.analysis-card {
margin: 0 16px 16px;
background: var(--card);
border: 1px solid var(--border);
border-radius: 20px;
padding: 16px;
}
.analysis-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 14px;
}
.analysis-title { font-family: var(--display); font-size: 16px; font-weight: 700; }
/* AI Score chips */
.ai-chips {
display: flex; gap: 8px; flex-wrap: wrap;
margin-bottom: 14px;
}
.ai-chip {
display: flex; align-items: center; gap: 6px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
border-radius: 12px;
padding: 8px 12px;
font-size: 12px;
}
.ai-chip .chip-score {
font-family: var(--display);
font-size: 16px;
font-weight: 700;
}
.ai-chip .chip-label { color: var(--muted); }
.ai-chip.good { border-color: rgba(52,211,153,0.25); }
.ai-chip.good .chip-score { color: var(--green); }
.ai-chip.warn { border-color: rgba(251,146,60,0.25); }
.ai-chip.warn .chip-score { color: var(--orange); }
.ai-chip.info { border-color: rgba(56,189,248,0.25); }
.ai-chip.info .chip-score { color: var(--accent); }
.analysis-pre {
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.04);
border-radius: 12px;
padding: 12px;
font-size: 11px;
font-family: var(--mono);
color: var(--muted);
max-height: 200px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.analysis-pre::-webkit-scrollbar { width: 3px; }
.analysis-pre::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
/* FEEDBACK TIPS */
.tips-card {
margin: 0 16px 16px;
background: linear-gradient(135deg, rgba(6,182,212,0.06), rgba(167,139,250,0.06));
border: 1px solid rgba(56,189,248,0.15);
border-radius: 20px;
padding: 16px;
}
.tip-item {
display: flex; gap: 10px;
margin-bottom: 10px;
align-items: flex-start;
}
.tip-item:last-child { margin-bottom: 0; }
.tip-icon {
width: 30px; height: 30px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.tip-icon.green { background: rgba(52,211,153,0.15); }
.tip-icon.orange { background: rgba(251,146,60,0.15); }
.tip-icon.blue { background: rgba(56,189,248,0.15); }
.tip-icon.purple { background: rgba(167,139,250,0.15); }
.tip-text { font-size: 12px; color: var(--muted); line-height: 1.5; padding-top: 6px; }
.tip-text strong { color: var(--text); display: block; font-size: 13px; margin-bottom: 1px; }
/* DOWNLOAD LINKS */
.links-card {
margin: 0 16px 16px;
display: flex; gap: 8px; flex-wrap: wrap;
}
.dl-btn {
flex: 1;
min-width: 120px;
background: rgba(255,255,255,0.04);
border: 1px solid var(--border);
border-radius: 14px;
padding: 12px;
text-align: center;
color: var(--accent);
text-decoration: none;
font-size: 11px;
font-family: var(--mono);
letter-spacing: 0.5px;
transition: all 0.2s;
display: flex; flex-direction: column; align-items: center; gap: 4px;
}
.dl-btn:active { background: rgba(56,189,248,0.1); transform: scale(0.97); }
.dl-btn .dl-icon { font-size: 18px; }
/* BOTTOM SPACER */
.bottom-spacer { height: 40px; }
/* TOAST */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background: var(--card);
border: 1px solid var(--border);
border-radius: 16px;
padding: 12px 20px;
font-size: 13px;
font-family: var(--mono);
color: var(--text);
z-index: 999;
transition: transform 0.4s cubic-bezier(.2,.8,.4,1);
backdrop-filter: blur(20px);
white-space: nowrap;
}
.toast.show { transform: translateX(-50%) translateY(0); }
/* Loading shimmer */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.shimmer {
background: linear-gradient(90deg, rgba(255,255,255,0.02) 25%, rgba(255,255,255,0.06) 50%, rgba(255,255,255,0.02) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* Pulse ring animation for start */
@keyframes ring-pulse {
0% { box-shadow: 0 8px 24px rgba(14,165,233,0.3); }
50% { box-shadow: 0 8px 40px rgba(14,165,233,0.6), 0 0 0 6px rgba(14,165,233,0.1); }
100% { box-shadow: 0 8px 24px rgba(14,165,233,0.3); }
}
.btn-start.pulsing { animation: ring-pulse 2s infinite; }
/* Responsive tweaks for desktop */
@media (min-width: 768px) {
.app-wrap { max-width: 500px; }
.app-title { font-size: 26px; }
}
/* Slide-in animation */
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.hero-card { animation: slideUp 0.5s ease both; }
.video-card { animation: slideUp 0.5s 0.1s ease both; }
.ctrl-card { animation: slideUp 0.5s 0.15s ease both; }
.mode-card { animation: slideUp 0.5s 0.18s ease both; }
.metrics-grid { animation: slideUp 0.5s 0.22s ease both; }
.transcript-card { animation: slideUp 0.5s 0.26s ease both; }
.tips-card { animation: slideUp 0.5s 0.3s ease both; }
</style>
</head>
<body>
<div class="app-wrap">
<!-- Status Bar -->
<div class="status-bar">
<span class="time" id="clock">9:41</span>
<div class="status-icons">
<svg viewBox="0 0 24 24"><path d="M1.5 8.5l1.5 1.5a12 12 0 0117 0l1.5-1.5A14 14 0 001.5 8.5zm3 3l1.5 1.5a7 7 0 0010 0l1.5-1.5A9 9 0 004.5 11.5zm3 3l1.5 1.5a2 2 0 003 0L13.5 14.5A4 4 0 007.5 14.5zM12 18a1.5 1.5 0 110 3 1.5 1.5 0 010-3z"/></svg>
<svg viewBox="0 0 24 24"><path d="M2 17h20v2H2v-2zm1.15-4.05L4 11.47l.85 1.48A10.95 10.95 0 0012 15c2.88 0 5.51-1.1 7.47-2.9l.92-1.5.85 1.47A12.93 12.93 0 0112 17a12.93 12.93 0 01-8.85-4.05z"/></svg>
<svg viewBox="0 0 24 24" style="fill:none;stroke:var(--muted);stroke-width:2"><rect x="2" y="7" width="18" height="11" rx="2"/><path d="M22 11v2"/></svg>
</div>
</div>
<!-- Top Nav -->
<div class="top-nav">
<div class="app-title">InterviewAI</div>
<div style="display:flex;gap:8px">
<div class="nav-badge" title="History">πŸ“Š</div>
<div class="nav-badge" title="Settings">βš™οΈ</div>
</div>
</div>
<!-- Hero Card -->
<div class="hero-card">
<div class="hero-label">SESSION OVERVIEW</div>
<div class="hero-title">AI Performance Coach</div>
<div class="hero-sub">Real-time analysis Β· Expression tracking Β· Smart scoring</div>
<div class="session-row">
<div class="score-ring-wrap">
<svg width="72" height="72" viewBox="0 0 72 72">
<circle cx="36" cy="36" r="30" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="6"/>
<circle cx="36" cy="36" r="30" fill="none"
stroke="url(#ringGrad)" stroke-width="6"
stroke-linecap="round"
stroke-dasharray="188.5"
stroke-dashoffset="188.5"
id="scoreRingCircle"/>
<defs>
<linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#38bdf8"/>
<stop offset="100%" stop-color="#818cf8"/>
</linearGradient>
</defs>
</svg>
<div class="score-center">
<div class="score-num" id="overallScore">β€”</div>
<div class="score-label">SCORE</div>
</div>
</div>
<div class="hero-stats">
<div class="hstat">
<div class="hstat-name">Confidence</div>
<div class="hstat-bar-wrap"><div class="hstat-bar" id="confBar"></div></div>
<div class="hstat-val" id="confVal">β€”</div>
</div>
<div class="hstat">
<div class="hstat-name">Clarity</div>
<div class="hstat-bar-wrap"><div class="hstat-bar" id="clarityBar" style="background:linear-gradient(90deg,#34d399,#06b6d4)"></div></div>
<div class="hstat-val" id="clarityVal">β€”</div>
</div>
<div class="hstat">
<div class="hstat-name">Engagement</div>
<div class="hstat-bar-wrap"><div class="hstat-bar" id="engBar" style="background:linear-gradient(90deg,#a78bfa,#818cf8)"></div></div>
<div class="hstat-val" id="engVal">β€”</div>
</div>
</div>
</div>
</div>
<!-- Video -->
<div class="section-head">
<span class="section-title">CAMERA FEED</span>
<span class="section-action">HD Β· 30fps</span>
</div>
<div class="video-card">
<video id="player" autoplay muted playsinline></video>
<canvas id="overlay"></canvas>
<div class="vid-badges">
<div class="vid-badge" id="moodBadge"><span class="dot mood"></span><span id="moodText">Mood: β€”</span></div>
<div class="vid-badge conf" id="confBadge"><span class="dot conf"></span><span id="confText">Conf: β€”</span></div>
</div>
<div class="rec-indicator" id="recIndicator">
<div class="rec-dot"></div>REC
</div>
</div>
<!-- Controls -->
<div class="ctrl-card">
<button class="btn-start pulsing" id="startBtn">β–Ά Start Session</button>
<button class="btn-icon danger" id="stopBtn" disabled title="Stop">⏹</button>
<button class="btn-icon success" id="downloadBtn" disabled title="Download JSON">⬇</button>
</div>
<!-- Mode Toggle -->
<div class="mode-card">
<button class="mode-btn active" id="modeInterview" onclick="setMode('interview')">🎯 Interview</button>
<button class="mode-btn" id="modePublic" onclick="setMode('public')">🎀 Public Speaking</button>
<button class="mode-btn" id="modePitch" onclick="setMode('pitch')">πŸš€ Pitch</button>
</div>
<!-- Metrics Grid -->
<div class="section-head">
<span class="section-title">LIVE METRICS</span>
<span class="section-action">AUTO-UPDATED</span>
</div>
<div class="metrics-grid">
<div class="metric-card m-presence">
<div class="metric-icon">πŸ‘οΈ</div>
<div class="metric-val good" id="presenceVal">β€”</div>
<div class="metric-name">Presence Ratio</div>
<div class="metric-delta" id="presenceDelta" style="display:none">+2%</div>
</div>
<div class="metric-card m-wpm">
<div class="metric-icon">⚑</div>
<div class="metric-val neutral" id="wpmVal">β€”</div>
<div class="metric-name">Words / Min</div>
<div class="metric-delta" id="wpmDelta" style="display:none">↑</div>
</div>
<div class="metric-card m-fillers">
<div class="metric-icon">πŸ”‡</div>
<div class="metric-val warn" id="fillersVal">β€”</div>
<div class="metric-name">Filler Words</div>
</div>
<div class="metric-card m-quality">
<div class="metric-icon">✨</div>
<div class="metric-val purple" id="qualityVal">β€”</div>
<div class="metric-name">Text Quality</div>
</div>
</div>
<!-- Live Transcript -->
<div class="transcript-card">
<div class="transcript-header">
<span class="transcript-title">LIVE TRANSCRIPT</span>
<span class="live-tag" id="liveTag">● LIVE</span>
</div>
<div class="transcript-body" id="transcript">
<span class="placeholder-text">Your spoken words will appear here in real time…</span>
</div>
</div>
<!-- Smart Tips -->
<div class="tips-card" id="tipsCard">
<div class="section-title" style="margin-bottom:14px">AI COACH TIPS</div>
<div class="tip-item">
<div class="tip-icon green">πŸ’‘</div>
<div class="tip-text"><strong>Maintain Eye Contact</strong>Look directly at the camera lens, not the screen.</div>
</div>
<div class="tip-item">
<div class="tip-icon orange">🎯</div>
<div class="tip-text"><strong>Reduce Filler Words</strong>Replace "um" and "uh" with confident pauses.</div>
</div>
<div class="tip-item">
<div class="tip-icon blue">πŸ“Š</div>
<div class="tip-text"><strong>Optimal Speaking Rate</strong>Aim for 120–150 words per minute for clarity.</div>
</div>
<div class="tip-item">
<div class="tip-icon purple">😊</div>
<div class="tip-text"><strong>Smile Naturally</strong>A neutral or happy expression builds rapport instantly.</div>
</div>
</div>
<!-- AI Analysis -->
<div class="analysis-card" id="analysisCard">
<div class="analysis-header">
<div class="analysis-title">AI Analysis</div>
<div class="live-tag" id="analysisTag">PROCESSING</div>
</div>
<div class="ai-chips" id="aiChips" style="display:none">
<div class="ai-chip good"><span class="chip-score" id="chipText">β€”</span><span class="chip-label">Text</span></div>
<div class="ai-chip warn"><span class="chip-score" id="chipTech">β€”</span><span class="chip-label">Technical</span></div>
<div class="ai-chip info"><span class="chip-score" id="chipWpm">β€”</span><span class="chip-label">Pace</span></div>
</div>
<div class="analysis-pre" id="analysisPre">No session recorded yet. Start a session to get your AI-powered performance breakdown.</div>
<div id="linksArea" style="margin-top:12px;display:none">
<div class="links-card" id="linksDiv"></div>
</div>
</div>
<div class="bottom-spacer"></div>
</div>
<!-- Toast -->
<div class="toast" id="toast">πŸŽ‰ Session saved successfully!</div>
<!-- TF.js coco-ssd -->
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.6.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/coco-ssd@2.2.2/dist/coco-ssd.min.js"></script>
<script defer src="https://unpkg.com/face-api.js@0.22.2/dist/face-api.min.js"></script>
<script>
// ═══════════════════════════════════════════════
// INTERVIEWAI PRO β€” Core Logic
// ═══════════════════════════════════════════════
// Elements
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const downloadBtn = document.getElementById('downloadBtn');
const player = document.getElementById('player');
const overlay = document.getElementById('overlay');
const ctx2d = overlay.getContext('2d');
const transcriptDiv = document.getElementById('transcript');
const analysisPre = document.getElementById('analysisPre');
const recIndicator = document.getElementById('recIndicator');
const liveTag = document.getElementById('liveTag');
const moodText = document.getElementById('moodText');
const confText = document.getElementById('confText');
const toast = document.getElementById('toast');
// Metric els
const presenceVal = document.getElementById('presenceVal');
const wpmVal = document.getElementById('wpmVal');
const fillersVal = document.getElementById('fillersVal');
const qualityVal = document.getElementById('qualityVal');
const overallScore = document.getElementById('overallScore');
const scoreRing = document.getElementById('scoreRingCircle');
// Bar els
const confBar = document.getElementById('confBar');
const clarityBar = document.getElementById('clarityBar');
const engBar = document.getElementById('engBar');
const confVal2 = document.getElementById('confVal');
const clarityVal2 = document.getElementById('clarityVal');
const engVal2 = document.getElementById('engVal');
// State
let mediaRecorder, recordedBlobs = [];
let detectionModel = null;
let detectionInterval = null;
let faceModelLoaded = false;
let detectionsLog = [];
let isRecording = false;
let currentMode = 'interview';
let sessionStartTime = null;
let lastFaceConf = 0;
// Clock
function updateClock() {
const d = new Date();
document.getElementById('clock').textContent =
d.getHours().toString().padStart(2,'0') + ':' +
d.getMinutes().toString().padStart(2,'0');
}
updateClock();
setInterval(updateClock, 30000);
// Mode
function setMode(m) {
currentMode = m;
['modeInterview','modePublic','modePitch'].forEach(id => {
document.getElementById(id).classList.remove('active');
});
const map = { interview: 'modeInterview', public: 'modePublic', pitch: 'modePitch' };
document.getElementById(map[m]).classList.add('active');
}
// Toast
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 3000);
}
// Models
async function loadModels() {
analysisPre.textContent = "⟳ Loading AI models…";
try {
detectionModel = await cocoSsd.load();
const base = "https://unpkg.com/face-api.js@0.22.2/weights/";
// Wait for faceapi global
let tries = 0;
while (typeof faceapi === 'undefined' && tries < 30) {
await new Promise(r => setTimeout(r, 200));
tries++;
}
if (typeof faceapi !== 'undefined') {
await faceapi.nets.tinyFaceDetector.loadFromUri(base);
await faceapi.nets.faceExpressionNet.loadFromUri(base);
faceModelLoaded = true;
}
analysisPre.textContent = "βœ“ Models loaded β€” ready to record.";
} catch(e) {
analysisPre.textContent = "⚠ Model load warning: " + e.message;
}
}
// Canvas
function resizeCanvas() {
overlay.width = player.videoWidth || player.clientWidth;
overlay.height = player.videoHeight || player.clientHeight;
overlay.style.width = '100%';
overlay.style.height = '100%';
}
// Filler detection (expanded list)
const FILLERS = ['um','uh','like','you','so','actually','basically','right','i','mean','hmm','well','okay','just','kind','sort','literally','honestly','basically','anyway','alright'];
function countFillers(text) {
const words = (text || "").toLowerCase().replace(/[^a-z\s]/g, '').split(/\s+/).filter(Boolean);
let total = 0;
for (const w of words) { if (FILLERS.includes(w)) total++; }
return total;
}
function computeWpm(wordCount, durationSec) {
if (!durationSec || durationSec <= 0) return 0;
return Math.round((wordCount / durationSec) * 60);
}
// Score ring update
function setScoreRing(score) {
const circ = 188.5;
const offset = circ - (score / 100) * circ;
scoreRing.style.strokeDashoffset = offset;
overallScore.textContent = score;
}
// Animated number update
function animateNumber(el, target, suffix = '') {
const start = parseInt(el.textContent) || 0;
const diff = target - start;
const steps = 20;
let step = 0;
const interval = setInterval(() => {
step++;
el.textContent = Math.round(start + diff * (step / steps)) + suffix;
if (step >= steps) clearInterval(interval);
}, 16);
}
// Detection loop
async function runDetection() {
if (!detectionModel || !isRecording) return;
try {
const preds = await detectionModel.detect(player);
resizeCanvas();
ctx2d.clearRect(0, 0, overlay.width, overlay.height);
const now = Date.now();
const persons = [];
preds.forEach(p => {
const [x, y, w, h] = p.bbox;
const lw = Math.max(2, Math.round(overlay.width / 200));
if (p.class === 'person' && p.score > 0.45) {
// Stylized bounding box with corner accents
ctx2d.strokeStyle = 'rgba(52,211,153,0.9)';
ctx2d.lineWidth = lw;
const cs = 20; // corner size
// Draw corner accents instead of full rect
[[x,y],[x+w,y],[x,y+h],[x+w,y+h]].forEach(([cx,cy], i) => {
ctx2d.beginPath();
const dx = i%2===0 ? cs : -cs;
const dy = i<2 ? cs : -cs;
ctx2d.moveTo(cx+dx, cy);
ctx2d.lineTo(cx, cy);
ctx2d.lineTo(cx, cy+dy);
ctx2d.stroke();
});
ctx2d.fillStyle = 'rgba(52,211,153,0.85)';
ctx2d.font = `bold ${Math.max(10,Math.round(overlay.width/32))}px DM Mono, monospace`;
ctx2d.fillText('πŸ‘€ ' + Math.round(p.score*100)+'%', x+6, Math.max(14, y-8));
persons.push(p);
}
});
// Face expression
let faceSummary = null;
if (faceModelLoaded && typeof faceapi !== 'undefined') {
const options = new faceapi.TinyFaceDetectorOptions({ inputSize: 224, scoreThreshold: 0.4 });
const result = await faceapi.detectSingleFace(player, options).withFaceExpressions();
if (result && result.expressions) {
const arr = Object.entries(result.expressions).sort((a,b)=>b[1]-a[1]);
const [expr, score] = arr[0];
faceSummary = { expression: expr, confidence: score };
lastFaceConf = score;
const emojiMap = {happy:'😊',sad:'😒',angry:'😠',surprised:'😲',fearful:'😨',disgusted:'🀒',neutral:'😐'};
moodText.textContent = `${emojiMap[expr]||'😐'} ${expr.charAt(0).toUpperCase()+expr.slice(1)}`;
confText.textContent = `${Math.round(score*100)}% conf`;
// Update hero confidence bar
const cPct = Math.round(score * 100);
confBar.style.width = cPct + '%';
confVal2.textContent = cPct + '%';
} else {
moodText.textContent = 'Mood: β€”';
confText.textContent = 'Conf: β€”';
}
}
detectionsLog.push({ t: now, detections: persons.map(p=>({class:p.class,score:p.score,bbox:p.bbox})), face: faceSummary });
if (detectionsLog.length > 3000) detectionsLog.shift();
// Live stats
const personFrames = detectionsLog.filter(f=>f.detections&&f.detections.length).length;
const presenceRatio = detectionsLog.length ? (personFrames/detectionsLog.length) : 0;
const presencePct = Math.round(presenceRatio * 100);
const text = transcriptDiv.textContent || '';
const isPlaceholder = text.includes('Your spoken words');
const words = isPlaceholder ? [] : text.split(/\s+/).filter(Boolean);
const duration = detectionsLog.length > 1 ? (now - detectionsLog[0].t) / 1000 : 0;
const wpm = computeWpm(words.length, duration);
const fillers = isPlaceholder ? 0 : countFillers(text);
presenceVal.textContent = presencePct + '%';
fillersVal.textContent = fillers;
wpmVal.textContent = wpm || 'β€”';
// Clarity bar based on WPM (ideal 130)
const clarity = wpm ? Math.max(0, Math.min(100, 100 - Math.abs(wpm - 130) * 1.2)) : 0;
clarityBar.style.width = clarity + '%';
clarityVal2.textContent = Math.round(clarity) + '%';
// Engagement = presence + expression confidence mix
const eng = Math.round(presencePct * 0.6 + lastFaceConf * 100 * 0.4);
engBar.style.width = eng + '%';
engVal2.textContent = eng + '%';
// Overall score
const overall = Math.round(presencePct * 0.3 + clarity * 0.35 + eng * 0.35);
setScoreRing(overall);
// Quality heuristic
const q = isPlaceholder ? 'β€”' : (words.length > 5 ? (fillers < 3 ? '🟒 Good' : fillers < 8 ? '🟑 Fair' : 'πŸ”΄ Poor') : '…');
qualityVal.textContent = q;
} catch(e) {
console.warn('detection err', e);
}
}
// Speech
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
let recognition = null;
if (SpeechRecognition) {
recognition = new SpeechRecognition();
recognition.continuous = true;
recognition.interimResults = true;
recognition.lang = 'en-US';
}
// Start Recording
async function startRecording() {
startBtn.disabled = true;
stopBtn.disabled = false;
startBtn.textContent = '⟳ Starting…';
startBtn.classList.remove('pulsing');
isRecording = true;
if (!detectionModel) await loadModels();
if (recognition) {
recognition.onresult = (event) => {
let interim = '', final = '';
for (let i = event.resultIndex; i < event.results.length; ++i) {
const r = event.results[i];
if (r.isFinal) final += r[0].transcript + ' ';
else interim += r[0].transcript;
}
transcriptDiv.textContent = (final + interim).trim();
};
recognition.onerror = e => console.warn('speech err', e);
try { recognition.start(); } catch(e) {}
} else {
transcriptDiv.innerHTML = '<span class="placeholder-text">Web Speech not available β€” Whisper transcription will run server-side after upload.</span>';
}
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: 'user' }
});
player.srcObject = stream;
player.muted = true;
await player.play();
resizeCanvas();
recordedBlobs = [];
detectionsLog = [];
sessionStartTime = Date.now();
let opts = { mimeType: 'video/webm;codecs=vp9,opus' };
try { mediaRecorder = new MediaRecorder(stream, opts); }
catch(e) { mediaRecorder = new MediaRecorder(stream); }
mediaRecorder.ondataavailable = e => { if (e.data && e.data.size > 0) recordedBlobs.push(e.data); };
mediaRecorder.start(200);
detectionInterval = setInterval(runDetection, 500);
recIndicator.classList.add('active');
liveTag.classList.add('active');
document.getElementById('analysisTag').classList.add('active');
analysisPre.textContent = "● Recording in progress…\nAI analysis will appear here when you stop.";
startBtn.textContent = '● Recording…';
} catch(err) {
analysisPre.textContent = "⚠ Camera/mic error: " + err.message;
startBtn.textContent = 'β–Ά Start Session';
startBtn.disabled = false;
startBtn.classList.add('pulsing');
stopBtn.disabled = true;
isRecording = false;
}
}
// Stop Recording
async function stopRecording() {
stopBtn.disabled = true;
isRecording = false;
startBtn.disabled = false;
startBtn.textContent = 'β–Ά Start Session';
startBtn.classList.add('pulsing');
if (recognition) try { recognition.stop(); } catch(e) {}
if (mediaRecorder && mediaRecorder.state !== 'inactive') mediaRecorder.stop();
clearInterval(detectionInterval);
recIndicator.classList.remove('active');
liveTag.classList.remove('active');
if (player.srcObject) {
player.srcObject.getTracks().forEach(t => t.stop());
}
const videoBlob = new Blob(recordedBlobs, { type: recordedBlobs[0]?.type || 'video/webm' });
player.srcObject = null;
player.src = URL.createObjectURL(videoBlob);
player.controls = true;
const text = transcriptDiv.textContent || '';
const words = text.split(/\s+/).filter(Boolean);
const duration = detectionsLog.length > 1
? (detectionsLog[detectionsLog.length-1].t - detectionsLog[0].t) / 1000
: 0;
const wpm = computeWpm(words.length, duration);
const fillers = countFillers(text);
const personFrames = detectionsLog.filter(f=>f.detections&&f.detections.length).length;
const presenceRatio = detectionsLog.length ? (personFrames/detectionsLog.length).toFixed(2) : 0;
const payload = {
started_at: sessionStartTime,
stopped_at: Date.now(),
mode: currentMode,
videoWidth: player.videoWidth || 640,
videoHeight: player.videoHeight || 480,
frames: detectionsLog
};
const form = new FormData();
form.append('transcript', text);
form.append('mode', currentMode);
form.append('detections', JSON.stringify(payload));
form.append('save', '1');
form.append('recording', videoBlob, 'recording.webm');
analysisPre.textContent = "⟳ Uploading & analyzing with AI…";
document.getElementById('analysisTag').classList.add('active');
document.getElementById('analysisTag').textContent = 'PROCESSING';
try {
const resp = await fetch('/analyze', { method: 'POST', body: form });
if (!resp.ok) throw new Error('Server returned ' + resp.status);
const j = await resp.json();
const a = j.analysis;
analysisPre.textContent = JSON.stringify(a, null, 2);
document.getElementById('analysisTag').textContent = 'DONE';
// Update chips
document.getElementById('aiChips').style.display = 'flex';
document.getElementById('chipText').textContent = a.text_quality_score ?? a.text_quality ?? 'β€”';
document.getElementById('chipTech').textContent = a.technical_score_text ?? 'β€”';
document.getElementById('chipWpm').textContent = a.wpm ?? wpm;
// Update metrics from server response
if (a.wpm) animateNumber(wpmVal, a.wpm);
if (a.fillers?.total !== undefined) animateNumber(fillersVal, a.fillers.total);
qualityVal.textContent = a.text_quality_score ?? qualityVal.textContent;
// Download links
const linksDiv = document.getElementById('linksDiv');
linksDiv.innerHTML = '';
const downloadLinks = [
{ key: 'recording_url', icon: 'πŸŽ₯', label: 'Recording' },
{ key: 'detections_url', icon: 'πŸ“Š', label: 'Detections' },
{ key: 'analysis_url', icon: 'πŸ“‹', label: 'Analysis' },
{ key: 'zip_url', icon: 'πŸ“¦', label: 'Full ZIP' }
];
let anyLink = false;
downloadLinks.forEach(({ key, icon, label }) => {
if (a[key]) {
anyLink = true;
const link = document.createElement('a');
link.href = a[key];
link.className = 'dl-btn';
link.target = '_blank';
link.innerHTML = `<span class="dl-icon">${icon}</span>${label}`;
linksDiv.appendChild(link);
}
});
if (anyLink) {
document.getElementById('linksArea').style.display = 'block';
}
downloadBtn.disabled = false;
downloadBtn.onclick = () => {
const blob = new Blob([JSON.stringify(j, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a2 = document.createElement('a');
a2.href = url;
a2.download = 'interviewai_' + Date.now() + '.json';
document.body.appendChild(a2);
a2.click();
a2.remove();
URL.revokeObjectURL(url);
};
showToast('βœ… Analysis complete! ' + (a.overall_score ? `Score: ${a.overall_score}` : ''));
} catch(err) {
console.warn('Upload error (expected without server):', err.message);
// Offline fallback analysis
const localScore = Math.round(
parseFloat(presenceRatio) * 30 +
(wpm >= 100 && wpm <= 160 ? 35 : 20) +
(fillers < 3 ? 35 : fillers < 8 ? 20 : 10)
);
const qualLabel = localScore >= 75 ? '🟒 Excellent' : localScore >= 55 ? '🟑 Good' : 'πŸ”΄ Needs Work';
analysisPre.textContent = [
`╔══ InterviewAI Session Report ══╗`,
``,
` Mode: ${currentMode.toUpperCase()}`,
` Duration: ${Math.round(duration)}s`,
` Words spoken: ${words.length}`,
` WPM: ${wpm}`,
` Filler words: ${fillers}`,
` Presence: ${Math.round(parseFloat(presenceRatio)*100)}%`,
` Session score: ${localScore}/100 ${qualLabel}`,
``,
` Advice:`,
fillers > 5 ? ` ⚠ Reduce filler words (counted ${fillers})` : ` βœ“ Great filler word control`,
wpm > 160 ? ` ⚠ Slow down β€” ${wpm} WPM is fast` : wpm < 90 ? ` ⚠ Speed up β€” ${wpm} WPM is slow` : ` βœ“ Good speaking pace`,
` βœ“ Connect backend server at localhost:5000`,
` for full Whisper + AI analysis`,
``,
`β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•`
].join('\n');
setScoreRing(localScore);
animateNumber(wpmVal, wpm);
animateNumber(fillersVal, fillers);
document.getElementById('aiChips').style.display = 'flex';
document.getElementById('chipText').textContent = qualLabel.split(' ')[1];
document.getElementById('chipTech').textContent = localScore + '/100';
document.getElementById('chipWpm').textContent = wpm + ' WPM';
document.getElementById('analysisTag').textContent = 'LOCAL';
downloadBtn.disabled = false;
downloadBtn.onclick = () => {
const data = { mode: currentMode, duration, words: words.length, wpm, fillers, presence: presenceRatio, score: localScore, transcript: text };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a2 = document.createElement('a');
a2.href = url;
a2.download = 'interviewai_local_' + Date.now() + '.json';
document.body.appendChild(a2);
a2.click();
a2.remove();
URL.revokeObjectURL(url);
};
showToast('πŸ“± Local analysis done β€” no server needed!');
}
}
// Events
startBtn.addEventListener('click', startRecording);
stopBtn.addEventListener('click', stopRecording);
player.addEventListener('loadedmetadata', resizeCanvas);
window.addEventListener('resize', resizeCanvas);
</script>
</body>
</html>