Spaces:
Running
Running
| import { useState, useMemo } from 'react' | |
| import { useNavigate } from 'react-router-dom' | |
| import { COMPANIES } from '../lib/companies' | |
| import VoiceSettings from './VoiceSettings' | |
| import { buildInterviewUrl, buildWatchUrl } from '../lib/sessionConfig' | |
| import { VX_SERVER } from '../lib/webrtc' | |
| const DEFAULT_KEY = 'vk_Qg95CQBuNFuiiHMCVtgo9bh9kx0N1ACnaj8zdJyg5qsBDWTjxt2ZoegM2UCdKfCY' | |
| export default function InterviewSetup() { | |
| const navigate = useNavigate() | |
| const sessionId = useMemo(() => crypto.randomUUID(), []) | |
| const [phase, setPhase] = useState('setup') // 'setup' | 'ready' | |
| const [name, setName] = useState('') | |
| const [company, setCompany] = useState(null) | |
| const [error, setError] = useState('') | |
| const [voiceSettings, setVoiceSettings] = useState({}) | |
| // Generated URLs — only set after clicking Create | |
| const [interviewUrl, setInterviewUrl] = useState('') | |
| const [watchUrl, setWatchUrl] = useState('') | |
| // Copy states per link | |
| const [copiedInterview, setCopiedInterview] = useState(false) | |
| const [copiedWatch, setCopiedWatch] = useState(false) | |
| function copy(text, setDone) { | |
| navigator.clipboard.writeText(text).then(() => { | |
| setDone(true) | |
| setTimeout(() => setDone(false), 2000) | |
| }) | |
| } | |
| function handleCreate() { | |
| if (!name.trim()) return setError('Enter the candidate name') | |
| if (!company) return setError('Choose a company') | |
| setError('') | |
| const config = { | |
| candidateName: name.trim(), | |
| company, | |
| apiKey: DEFAULT_KEY, | |
| server: VX_SERVER, | |
| voiceSettings: Object.keys(voiceSettings).length ? voiceSettings : null, | |
| } | |
| setInterviewUrl(buildInterviewUrl(sessionId, config)) | |
| setWatchUrl(buildWatchUrl(sessionId)) | |
| setPhase('ready') | |
| } | |
| if (phase === 'ready') { | |
| return ( | |
| <div style={s.page}> | |
| <div style={s.glow1} /> | |
| <div style={s.glow2} /> | |
| <div style={s.inner}> | |
| <button style={s.back} onClick={() => setPhase('setup')}>← reconfigure</button> | |
| <p style={s.pill}>Interview ready</p> | |
| <h1 style={s.title}> | |
| Send the link,<br /> | |
| <span style={{ color: 'var(--accent)' }}>then watch.</span> | |
| </h1> | |
| <p style={s.subtitle}> | |
| Share the interview link with <strong style={{ color: 'var(--text)' }}>{name}</strong>. | |
| Open the watch link on your device to follow the conversation live. | |
| </p> | |
| {/* Interview link */} | |
| <div style={s.linkCard}> | |
| <div style={s.linkCardHeader}> | |
| <span style={s.linkCardLabel}>Interview link</span> | |
| <span style={s.linkCardHint}>Send this to the candidate</span> | |
| </div> | |
| <div style={s.linkRow}> | |
| <span style={s.linkText}>{interviewUrl}</span> | |
| <button | |
| style={{ ...s.copyBtn, ...(copiedInterview ? s.copyBtnDone : {}) }} | |
| onClick={() => copy(interviewUrl, setCopiedInterview)} | |
| > | |
| {copiedInterview ? 'Copied!' : 'Copy'} | |
| </button> | |
| </div> | |
| </div> | |
| {/* Watch link */} | |
| <div style={{ ...s.linkCard, marginBottom: 28 }}> | |
| <div style={s.linkCardHeader}> | |
| <span style={{ ...s.linkCardLabel, color: '#60d0f0' }}>Watch link</span> | |
| <span style={s.linkCardHint}>Your observer view</span> | |
| </div> | |
| <div style={s.linkRow}> | |
| <span style={s.linkText}>{watchUrl}</span> | |
| <button | |
| style={{ ...s.copyBtn, ...(copiedWatch ? s.copyBtnDone : {}) }} | |
| onClick={() => copy(watchUrl, setCopiedWatch)} | |
| > | |
| {copiedWatch ? 'Copied!' : 'Copy'} | |
| </button> | |
| </div> | |
| </div> | |
| <button style={s.watchBtn} onClick={() => navigate(`/watch/${sessionId}`)}> | |
| Open watch room → | |
| </button> | |
| <p style={s.fine}> | |
| The candidate opens their link and the interview starts automatically. | |
| You'll see the full transcript here as it happens. | |
| </p> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| return ( | |
| <div style={s.page}> | |
| <div style={s.glow1} /> | |
| <div style={s.glow2} /> | |
| <div style={s.inner}> | |
| <button style={s.back} onClick={() => navigate('/')}>← back</button> | |
| <p style={s.pill}>AI-powered mock interview</p> | |
| <h1 style={s.title}> | |
| Configure &<br /> | |
| <span style={{ color: 'var(--accent)' }}>send the link.</span> | |
| </h1> | |
| {/* Candidate name */} | |
| <div style={s.fieldGroup}> | |
| <label style={s.label}>Candidate name</label> | |
| <input | |
| style={s.input} | |
| value={name} | |
| onChange={e => setName(e.target.value)} | |
| onKeyDown={e => e.key === 'Enter' && handleCreate()} | |
| placeholder="e.g. Ayush" | |
| autoComplete="off" | |
| /> | |
| </div> | |
| {/* Company picker */} | |
| <div style={s.fieldGroup}> | |
| <label style={s.label}>Interview for</label> | |
| <div style={s.companyGrid}> | |
| {Object.values(COMPANIES).map(c => ( | |
| <CompanyCard | |
| key={c.id} | |
| company={c} | |
| selected={company === c.id} | |
| onSelect={() => { setCompany(c.id); setError('') }} | |
| /> | |
| ))} | |
| </div> | |
| </div> | |
| <VoiceSettings value={voiceSettings} onChange={setVoiceSettings} /> | |
| {error && <p style={s.error}>{error}</p>} | |
| <button style={s.startBtn} onClick={handleCreate}> | |
| Create interview → | |
| </button> | |
| <p style={s.fine}> | |
| Generates a unique link for the candidate and a watch link for you. | |
| </p> | |
| </div> | |
| </div> | |
| ) | |
| } | |
| function CompanyCard({ company, selected, onSelect }) { | |
| return ( | |
| <button | |
| onClick={onSelect} | |
| style={{ | |
| ...s.companyCard, | |
| borderColor: selected ? company.accent : 'var(--border2)', | |
| background: selected ? `${company.accent}10` : 'var(--surface)', | |
| }} | |
| > | |
| <span style={{ ...s.companyIcon, background: `${company.accent}20`, color: company.accent }}> | |
| {company.icon} | |
| </span> | |
| <div style={s.companyInfo}> | |
| <span style={s.companyName}>{company.name}</span> | |
| <span style={s.companyTag}>{company.tagline}</span> | |
| <div style={s.companyPills}> | |
| {company.questionStyle.map(q => ( | |
| <span key={q} style={s.qpill}>{q}</span> | |
| ))} | |
| </div> | |
| </div> | |
| {selected && <span style={{ ...s.checkmark, color: company.accent }}>✓</span>} | |
| </button> | |
| ) | |
| } | |
| const s = { | |
| page: { | |
| minHeight: '100vh', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| padding: '40px 20px', | |
| position: 'relative', | |
| overflowY: 'auto', | |
| overflowX: 'hidden', | |
| }, | |
| glow1: { | |
| position: 'fixed', inset: 0, | |
| background: 'radial-gradient(ellipse 60% 50% at 20% 80%, rgba(200,240,96,.05) 0%, transparent 70%)', | |
| pointerEvents: 'none', | |
| }, | |
| glow2: { | |
| position: 'fixed', inset: 0, | |
| background: 'radial-gradient(ellipse 50% 50% at 80% 20%, rgba(96,208,240,.04) 0%, transparent 70%)', | |
| pointerEvents: 'none', | |
| }, | |
| inner: { | |
| width: '100%', | |
| maxWidth: 560, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| position: 'relative', | |
| zIndex: 1, | |
| }, | |
| back: { | |
| alignSelf: 'flex-start', | |
| fontFamily: 'var(--mono)', | |
| fontSize: 11, | |
| color: 'var(--muted)', | |
| background: 'none', | |
| border: 'none', | |
| cursor: 'pointer', | |
| letterSpacing: '.08em', | |
| padding: 0, | |
| marginBottom: 28, | |
| }, | |
| pill: { | |
| display: 'inline-block', | |
| alignSelf: 'flex-start', | |
| fontFamily: 'var(--mono)', | |
| fontSize: 10, | |
| letterSpacing: '.2em', | |
| textTransform: 'uppercase', | |
| color: 'var(--accent)', | |
| border: '1px solid rgba(200,240,96,.25)', | |
| padding: '4px 10px', | |
| borderRadius: 20, | |
| marginBottom: 20, | |
| }, | |
| title: { | |
| fontSize: 'clamp(34px, 6vw, 56px)', | |
| fontWeight: 800, | |
| lineHeight: .95, | |
| letterSpacing: '-.03em', | |
| marginBottom: 16, | |
| }, | |
| subtitle: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 11, | |
| color: 'var(--muted)', | |
| lineHeight: 1.7, | |
| marginBottom: 32, | |
| letterSpacing: '.04em', | |
| }, | |
| linkCard: { | |
| background: 'var(--surface)', | |
| border: '1px solid var(--border2)', | |
| borderRadius: 10, | |
| padding: '14px 16px', | |
| marginBottom: 14, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: 10, | |
| }, | |
| linkCardHeader: { | |
| display: 'flex', | |
| alignItems: 'baseline', | |
| gap: 10, | |
| }, | |
| linkCardLabel: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 9, | |
| letterSpacing: '.2em', | |
| textTransform: 'uppercase', | |
| color: 'var(--accent)', | |
| }, | |
| linkCardHint: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 9, | |
| color: 'var(--muted)', | |
| letterSpacing: '.08em', | |
| }, | |
| linkRow: { | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 10, | |
| }, | |
| linkText: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 10, | |
| color: 'var(--muted)', | |
| flex: 1, | |
| overflow: 'hidden', | |
| textOverflow: 'ellipsis', | |
| whiteSpace: 'nowrap', | |
| }, | |
| copyBtn: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 10, | |
| letterSpacing: '.1em', | |
| padding: '5px 12px', | |
| borderRadius: 6, | |
| border: '1px solid var(--border2)', | |
| background: 'transparent', | |
| color: 'var(--text)', | |
| cursor: 'pointer', | |
| flexShrink: 0, | |
| transition: 'border-color .15s, color .15s', | |
| }, | |
| copyBtnDone: { | |
| color: 'var(--accent)', | |
| borderColor: 'var(--accent)', | |
| }, | |
| fieldGroup: { | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: 8, | |
| marginBottom: 20, | |
| }, | |
| label: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 10, | |
| letterSpacing: '.18em', | |
| color: 'var(--muted)', | |
| textTransform: 'uppercase', | |
| }, | |
| input: { | |
| background: 'var(--surface)', | |
| border: '1px solid var(--border2)', | |
| color: 'var(--text)', | |
| fontFamily: 'var(--mono)', | |
| fontSize: 13, | |
| padding: '12px 14px', | |
| borderRadius: 8, | |
| outline: 'none', | |
| width: '100%', | |
| }, | |
| companyGrid: { | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: 10, | |
| }, | |
| companyCard: { | |
| display: 'flex', | |
| alignItems: 'flex-start', | |
| gap: 14, | |
| padding: '14px 16px', | |
| borderRadius: 10, | |
| border: '1.5px solid', | |
| cursor: 'pointer', | |
| textAlign: 'left', | |
| transition: 'border-color .15s, background .15s', | |
| fontFamily: 'var(--sans)', | |
| width: '100%', | |
| position: 'relative', | |
| }, | |
| companyIcon: { | |
| width: 40, | |
| height: 40, | |
| borderRadius: 10, | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| fontSize: 18, | |
| fontWeight: 800, | |
| flexShrink: 0, | |
| }, | |
| companyInfo: { | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: 3, | |
| flex: 1, | |
| }, | |
| companyName: { | |
| fontSize: 15, | |
| fontWeight: 700, | |
| color: 'var(--text)', | |
| }, | |
| companyTag: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 10, | |
| color: 'var(--muted)', | |
| letterSpacing: '.12em', | |
| }, | |
| companyPills: { | |
| display: 'flex', | |
| flexWrap: 'wrap', | |
| gap: 4, | |
| marginTop: 6, | |
| }, | |
| qpill: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 9, | |
| letterSpacing: '.1em', | |
| color: 'var(--muted)', | |
| border: '1px solid var(--border)', | |
| padding: '2px 7px', | |
| borderRadius: 20, | |
| }, | |
| checkmark: { | |
| position: 'absolute', | |
| top: 12, | |
| right: 14, | |
| fontSize: 16, | |
| fontWeight: 700, | |
| }, | |
| error: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 11, | |
| color: 'var(--danger)', | |
| marginBottom: 12, | |
| }, | |
| startBtn: { | |
| background: 'var(--accent)', | |
| color: '#0a0a0a', | |
| border: 'none', | |
| fontFamily: 'var(--sans)', | |
| fontSize: 14, | |
| fontWeight: 700, | |
| letterSpacing: '.06em', | |
| padding: '16px 24px', | |
| borderRadius: 8, | |
| cursor: 'pointer', | |
| marginTop: 4, | |
| marginBottom: 16, | |
| }, | |
| watchBtn: { | |
| background: 'transparent', | |
| color: 'var(--text)', | |
| border: '1.5px solid var(--border2)', | |
| fontFamily: 'var(--sans)', | |
| fontSize: 14, | |
| fontWeight: 600, | |
| letterSpacing: '.04em', | |
| padding: '14px 24px', | |
| borderRadius: 8, | |
| cursor: 'pointer', | |
| marginBottom: 20, | |
| transition: 'border-color .15s', | |
| }, | |
| fine: { | |
| fontFamily: 'var(--mono)', | |
| fontSize: 10, | |
| color: 'var(--muted)', | |
| letterSpacing: '.1em', | |
| textAlign: 'center', | |
| lineHeight: 1.6, | |
| }, | |
| } | |