Spaces:
Running
Running
| <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> |