Spaces:
Sleeping
Sleeping
lakshmisravya123
Major upgrade: comprehensive negotiation coaching with EQ scoring and detailed reports
dad7400 | import { useState, useRef, useEffect } from "react"; | |
| import { startNegotiation, sendResponse, acceptOffer } from "./utils/api"; | |
| function EQGauge({ label, value, max = 10 }) { | |
| const pct = Math.round((value / max) * 100); | |
| const color = pct >= 70 ? "#00b894" : pct >= 40 ? "#fdcb6e" : "#e17055"; | |
| return ( | |
| <div className="eq-gauge"> | |
| <div className="eq-gauge-label">{label}</div> | |
| <div className="eq-gauge-bar-bg"> | |
| <div className="eq-gauge-bar-fill" style={{ width: pct + "%", background: color }} /> | |
| </div> | |
| <div className="eq-gauge-value">{value}/{max}</div> | |
| </div> | |
| ); | |
| } | |
| function MomentBadge({ rating }) { | |
| const colors = { strong: "#00b894", neutral: "#fdcb6e", weak: "#e17055" }; | |
| return <span className="moment-badge" style={{ background: colors[rating] || "#555", color: "#000", padding: "2px 10px", borderRadius: "12px", fontSize: "0.75rem", fontWeight: 600, marginLeft: "8px" }}>{rating}</span>; | |
| } | |
| function CoachingPanel({ tip, warning, candidateTactics, momentRating }) { | |
| if (!tip && !warning) return null; | |
| return ( | |
| <div className="coaching-panel"> | |
| <div className="coaching-header">Real-Time Coach{momentRating && <MomentBadge rating={momentRating} />}</div> | |
| {candidateTactics && candidateTactics.length > 0 && ( | |
| <div className="coaching-tactics"> | |
| <span className="coaching-tactics-label">Your tactics: </span> | |
| {candidateTactics.map((t, i) => <span key={i} className="tactic-pill">{t}</span>)} | |
| </div> | |
| )} | |
| {tip && <div className="coaching-tip"><strong>Try next:</strong> {tip}</div>} | |
| {warning && warning.length > 0 && <div className="coaching-warning"><strong>Avoid:</strong> {warning}</div>} | |
| </div> | |
| ); | |
| } | |
| function ScoreCircle({ score, label, size = 80 }) { | |
| const r = (size - 10) / 2; | |
| const circ = 2 * Math.PI * r; | |
| const offset = circ - (score / 100) * circ; | |
| const color = score >= 75 ? "#00b894" : score >= 50 ? "#fdcb6e" : "#e17055"; | |
| return ( | |
| <div className="score-circle" style={{ width: size, height: size, position: "relative", display: "inline-block" }}> | |
| <svg width={size} height={size}> | |
| <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="#2d2d44" strokeWidth="5" /> | |
| <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth="5" | |
| strokeDasharray={circ} strokeDashoffset={offset} strokeLinecap="round" | |
| transform={"rotate(-90 " + size/2 + " " + size/2 + ")"} /> | |
| </svg> | |
| <div style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center" }}> | |
| <div style={{ fontSize: size > 100 ? "2rem" : "1.1rem", fontWeight: 700, color: "#fff" }}>{score}</div> | |
| <div style={{ fontSize: "0.65rem", color: "#888" }}>{label}</div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ReportView({ report, onReset }) { | |
| const r = report; | |
| return ( | |
| <div className="app"> | |
| <h1><span>Performance Report</span></h1> | |
| <div className="report"> | |
| <div className="report-top-grid"> | |
| <div className="report-main-score"> | |
| <ScoreCircle score={r.overallScore || 0} label="Overall" size={120} /> | |
| <div className="report-verdict">{r.verdict}</div> | |
| {r.letterGrade && <div className="report-grade">Grade: {r.letterGrade}</div>} | |
| </div> | |
| <div className="report-score-cards"> | |
| <ScoreCircle score={r.emotionalIntelligence?.overall || 0} label="EQ" size={80} /> | |
| <ScoreCircle score={r.communicationScore?.overall || 0} label="Comms" size={80} /> | |
| <ScoreCircle score={(r.negotiationStyle?.effectiveness || 0) * 10} label="Style" size={80} /> | |
| </div> | |
| </div> | |
| <div className="salary-comparison"> | |
| <div className="salary-box"><div className="label">Target</div><div className="amount">{"$"}{(r.targetSalary||0).toLocaleString()}</div></div> | |
| <div className="salary-box result-box"><div className="label">Final Offer</div><div className="amount">{"$"}{(r.finalSalary||0).toLocaleString()}</div><div className="pct">{r.percentOfTarget}% of target</div></div> | |
| {r.marketContext && <div className="salary-box market-box"><div className="label">Market Range</div><div className="market-range">{"$"}{(r.marketContext.marketLow||0).toLocaleString()} - {"$"}{(r.marketContext.marketHigh||0).toLocaleString()}</div><div className="market-position">{r.marketContext.candidatePosition}</div></div>} | |
| </div> | |
| <p className="report-summary">{r.summary}</p> | |
| {r.negotiationStyle && <div className="report-section style-section"><h3>Negotiation Style: <span className="style-name">{r.negotiationStyle.primary}</span></h3><p>{r.negotiationStyle.description}</p></div>} | |
| {r.emotionalIntelligence && ( | |
| <div className="report-section eq-section"> | |
| <h3>Emotional Intelligence</h3> | |
| <p className="eq-analysis">{r.emotionalIntelligence.analysis}</p> | |
| <div className="eq-grid"> | |
| <EQGauge label="Empathy" value={r.emotionalIntelligence.empathy}/> | |
| <EQGauge label="Assertiveness" value={r.emotionalIntelligence.assertiveness}/> | |
| <EQGauge label="Composure" value={r.emotionalIntelligence.composure}/> | |
| <EQGauge label="Rapport" value={r.emotionalIntelligence.rapport}/> | |
| <EQGauge label="Adaptability" value={r.emotionalIntelligence.adaptability}/> | |
| </div> | |
| </div> | |
| )} | |
| {r.communicationScore && ( | |
| <div className="report-section"> | |
| <h3>Communication</h3> | |
| <p className="eq-analysis">{r.communicationScore.analysis}</p> | |
| <div className="eq-grid"> | |
| <EQGauge label="Clarity" value={r.communicationScore.clarity}/> | |
| <EQGauge label="Persuasiveness" value={r.communicationScore.persuasiveness}/> | |
| <EQGauge label="Active Listening" value={r.communicationScore.activeListening}/> | |
| <EQGauge label="Question Quality" value={r.communicationScore.questionQuality}/> | |
| </div> | |
| </div> | |
| )} | |
| {r.powerDynamics && ( | |
| <div className="report-section power-section"> | |
| <h3>Power Dynamics</h3> | |
| <div className="power-bars"> | |
| <div className="power-row"><span className="power-label">You: {r.powerDynamics.candidatePower}/10</span><div className="power-bar-bg"><div className="power-bar-you" style={{width:(r.powerDynamics.candidatePower*10)+"%"}}/></div></div> | |
| <div className="power-row"><span className="power-label">Manager: {r.powerDynamics.managerPower}/10</span><div className="power-bar-bg"><div className="power-bar-mgr" style={{width:(r.powerDynamics.managerPower*10)+"%"}}/></div></div> | |
| </div> | |
| <p>{r.powerDynamics.assessment}</p> | |
| {r.powerDynamics.shiftMoments?.map((m,i)=><div key={i} className="power-shift">{m}</div>)} | |
| </div> | |
| )} | |
| {r.bestMoments?.length > 0 && ( | |
| <div className="report-section"> | |
| <h3>Best Moments</h3> | |
| {r.bestMoments.map((m,i)=><div key={i} className="moment-card best"><div className="moment-round">Round {m.round}</div><div className="moment-quote">"{m.quote}"</div><div className="moment-why">{m.why}</div></div>)} | |
| </div> | |
| )} | |
| {r.worstMoments?.length > 0 && ( | |
| <div className="report-section"> | |
| <h3>Weakest Moments</h3> | |
| {r.worstMoments.map((m,i)=><div key={i} className="moment-card worst"><div className="moment-round">Round {m.round}</div><div className="moment-quote">"{m.quote}"</div><div className="moment-why">{m.why}</div></div>)} | |
| </div> | |
| )} | |
| {r.tacticsUsed?.length > 0 && ( | |
| <div className="report-section"> | |
| <h3>Tactics Analysis</h3> | |
| <div className="tactics-grid"> | |
| {r.tacticsUsed.map((t,i)=><div key={i} className={"tactic-card "+t.effectiveness}><div className="tactic-card-name">{t.name}</div><div className="tactic-card-eff">{t.effectiveness}</div><div className="tactic-card-example">"{t.example}"</div></div>)} | |
| </div> | |
| </div> | |
| )} | |
| {r.missedOpportunities?.length > 0 && ( | |
| <div className="report-section"> | |
| <h3>Missed Opportunities</h3> | |
| {r.missedOpportunities.map((m,i)=><div key={i} className="missed-card"><div className="missed-situation"><strong>Situation:</strong> {m.situation}</div><div className="missed-better"><strong>Better approach:</strong> {m.betterApproach}</div><div className="missed-impact"><strong>Impact:</strong> {m.impact}</div></div>)} | |
| </div> | |
| )} | |
| {r.marketContext && <div className="report-section"><h3>Market Context</h3><p>{r.marketContext.analysis}</p></div>} | |
| <div className="report-two-col"> | |
| <div className="report-section"><h3>Strengths</h3><ul>{r.strengths?.map((s,i)=><li key={i}>{s}</li>)}</ul></div> | |
| <div className="report-section"><h3>Areas to Improve</h3><ul>{r.improvements?.map((s,i)=><li key={i}>{s}</li>)}</ul></div> | |
| </div> | |
| {r.personalizedTips?.length > 0 && ( | |
| <div className="report-section"> | |
| <h3>Personalized Pro Tips</h3> | |
| <div className="tips-grid"> | |
| {r.personalizedTips.map((t,i)=><div key={i} className="tip-card"><div className="tip-category">{t.category}</div><div className="tip-text">{t.tip}</div></div>)} | |
| </div> | |
| </div> | |
| )} | |
| {r.nextSessionFocus && <div className="report-section next-focus"><h3>Next Session Focus</h3><p>{r.nextSessionFocus}</p></div>} | |
| <button className="btn btn-primary" onClick={onReset}>Practice Again</button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default function App() { | |
| const [phase, setPhase] = useState("setup"); | |
| const [config, setConfig] = useState({ role:"", company:"", currentSalary:"", targetSalary:"", difficulty:"medium", scenarioType:"salary" }); | |
| const [sessionId, setSessionId] = useState(null); | |
| const [messages, setMessages] = useState([]); | |
| const [currentOffer, setCurrentOffer] = useState(0); | |
| const [response, setResponse] = useState(""); | |
| const [loading, setLoading] = useState(false); | |
| const [report, setReport] = useState(null); | |
| const [round, setRound] = useState(1); | |
| const [tactic, setTactic] = useState(""); | |
| const [tacticExplanation, setTacticExplanation] = useState(""); | |
| const [error, setError] = useState(""); | |
| const [coaching, setCoaching] = useState(null); | |
| const [eqScores, setEqScores] = useState(null); | |
| const [scenarioLabel, setScenarioLabel] = useState(""); | |
| const chatRef = useRef(null); | |
| const inputRef = useRef(null); | |
| useEffect(() => { if (chatRef.current) chatRef.current.scrollTop = chatRef.current.scrollHeight; }, [messages, coaching]); | |
| useEffect(() => { if (phase === "negotiation" && !loading && inputRef.current) inputRef.current.focus(); }, [phase, loading, messages]); | |
| const handleStart = async () => { | |
| if (!config.role || !config.targetSalary) { setError("Role and target salary are required"); return; } | |
| setLoading(true); setError(""); | |
| try { | |
| const data = await startNegotiation({ ...config, targetSalary: Number(config.targetSalary), currentSalary: Number(config.currentSalary) || undefined }); | |
| setSessionId(data.sessionId); setCurrentOffer(data.initialOffer); setScenarioLabel(data.scenarioLabel || "Salary Negotiation"); | |
| setMessages([{ role: "Hiring Manager", text: data.openingStatement + " Our initial offer is " + "$" + (data.initialOffer||0).toLocaleString() + ".", name: data.hiringManagerName }]); | |
| setPhase("negotiation"); | |
| } catch (err) { setError(err.message); } finally { setLoading(false); } | |
| }; | |
| const handleRespond = async () => { | |
| if (!response.trim()) return; | |
| setMessages(prev => [...prev, { role: "You", text: response }]); | |
| const myResponse = response; setResponse(""); setLoading(true); setCoaching(null); | |
| try { | |
| const data = await sendResponse(sessionId, myResponse); | |
| setCurrentOffer(data.currentOffer); | |
| setMessages(prev => [...prev, { role: "Hiring Manager", text: data.response }]); | |
| setRound(data.round); | |
| if (data.tactic) setTactic(data.tactic); | |
| if (data.tacticExplanation) setTacticExplanation(data.tacticExplanation); | |
| if (data.emotionalIntelligence) setEqScores(data.emotionalIntelligence); | |
| setCoaching({ tip: data.coachingTip, warning: data.coachingWarning, candidateTactics: data.candidateTactics, momentRating: data.momentRating }); | |
| } catch (err) { setError(err.message); } finally { setLoading(false); } | |
| }; | |
| const handleAccept = async () => { | |
| setLoading(true); | |
| try { const data = await acceptOffer(sessionId); setReport(data); setPhase("report"); } | |
| catch (err) { setError(err.message); } finally { setLoading(false); } | |
| }; | |
| const resetAll = () => { setPhase("setup"); setMessages([]); setReport(null); setRound(1); setCoaching(null); setEqScores(null); setTactic(""); setTacticExplanation(""); setError(""); }; | |
| if (phase === "report" && report) return <ReportView report={report} onReset={resetAll} />; | |
| if (phase === "negotiation") { | |
| return ( | |
| <div className="app"> | |
| <div className="neg-header"><h1><span>Negotiation Simulator</span></h1><div className="scenario-tag">{scenarioLabel}</div></div> | |
| <div className="offer-display"> | |
| <div className="offer-meta">Round {round} | {config.difficulty} difficulty</div> | |
| <div className="offer-amount">{"$"}{(currentOffer||0).toLocaleString()}</div> | |
| <div className="offer-meta">Target: {"$"}{Number(config.targetSalary).toLocaleString()}</div> | |
| <div className="offer-progress-bar"><div className="offer-progress-fill" style={{ width: Math.min(100, Math.round((currentOffer / Number(config.targetSalary)) * 100)) + "%" }} /></div> | |
| <div className="offer-pct">{Math.round((currentOffer / Number(config.targetSalary)) * 100)}% of target</div> | |
| {tactic && <div className="tactic-badge-wrapper"><span className="tactic-badge">{tactic}</span>{tacticExplanation && <span className="tactic-explanation">{tacticExplanation}</span>}</div>} | |
| </div> | |
| {eqScores && <div className="eq-mini-dashboard"><div className="eq-mini-item"><span className="eq-mini-label">Empathy</span><span className="eq-mini-val">{eqScores.empathy}</span></div><div className="eq-mini-item"><span className="eq-mini-label">Assertive</span><span className="eq-mini-val">{eqScores.assertiveness}</span></div><div className="eq-mini-item"><span className="eq-mini-label">Composure</span><span className="eq-mini-val">{eqScores.composure}</span></div><div className="eq-mini-item"><span className="eq-mini-label">Rapport</span><span className="eq-mini-val">{eqScores.rapport}</span></div></div>} | |
| <div className="chat-container" ref={chatRef}> | |
| {messages.map((m,i) => <div key={i} className={"message " + (m.role==="You" ? "candidate" : "manager")}><div className="role">{m.role}{m.name ? " ("+m.name+")" : ""}</div><div className="text">{m.text}</div></div>)} | |
| {loading && <div className="loading"><div className="spinner"></div>Thinking...</div>} | |
| </div> | |
| {coaching && <CoachingPanel tip={coaching.tip} warning={coaching.warning} candidateTactics={coaching.candidateTactics} momentRating={coaching.momentRating} />} | |
| {error && <p className="error-text">{error}</p>} | |
| <div className="response-area"> | |
| <textarea ref={inputRef} placeholder="Your response... (Enter to send, Shift+Enter for new line)" value={response} onChange={e => setResponse(e.target.value)} onKeyDown={e => { if (e.key==="Enter" && !e.shiftKey) { e.preventDefault(); handleRespond(); }}} disabled={loading} /> | |
| <button className="btn btn-primary btn-send" onClick={handleRespond} disabled={loading || !response.trim()}>Send</button> | |
| </div> | |
| <div className="action-row"> | |
| <button className="btn btn-accept" onClick={handleAccept} disabled={loading}>Accept Offer ({"$"}{(currentOffer||0).toLocaleString()})</button> | |
| <button className="btn-walk" onClick={handleAccept} disabled={loading}>Walk Away</button> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="app"> | |
| <h1><span>Negotiation Simulator</span></h1> | |
| <p className="subtitle">Practice real-world negotiations with AI coaching. Get detailed performance analysis with emotional intelligence scoring.</p> | |
| {error && <p className="error-text">{error}</p>} | |
| <div className="setup-form"> | |
| <div className="input-group"><label>Scenario Type</label> | |
| <div className="scenario-selector"> | |
| {[{id:"salary",label:"Salary",desc:"Negotiate compensation"},{id:"promotion",label:"Promotion",desc:"Ask for a raise"},{id:"resources",label:"Resources",desc:"Project budget"},{id:"remote",label:"Remote Work",desc:"Flexibility terms"}].map(s => | |
| <button key={s.id} className={"scenario-btn "+(config.scenarioType===s.id?"active":"")} onClick={()=>setConfig({...config,scenarioType:s.id})}> | |
| <span className="scenario-label">{s.label}</span> | |
| <span className="scenario-desc">{s.desc}</span> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <div className="form-row"> | |
| <div className="input-group"><label>Job Role *</label><input placeholder="e.g., Senior Software Engineer" value={config.role} onChange={e=>setConfig({...config,role:e.target.value})}/></div> | |
| <div className="input-group"><label>Company (optional)</label><input placeholder="e.g., Google" value={config.company} onChange={e=>setConfig({...config,company:e.target.value})}/></div> | |
| </div> | |
| <div className="form-row"> | |
| <div className="input-group"><label>Current Salary</label><input type="number" placeholder="e.g., 120000" value={config.currentSalary} onChange={e=>setConfig({...config,currentSalary:e.target.value})}/></div> | |
| <div className="input-group"><label>Target Salary *</label><input type="number" placeholder="e.g., 150000" value={config.targetSalary} onChange={e=>setConfig({...config,targetSalary:e.target.value})}/></div> | |
| </div> | |
| <div className="input-group"><label>Difficulty</label> | |
| <div className="difficulty-selector"> | |
| {[{id:"easy",label:"Easy",desc:"Flexible manager"},{id:"medium",label:"Medium",desc:"Standard tactics"},{id:"hard",label:"Hard",desc:"Tough negotiator"}].map(d => | |
| <button key={d.id} className={"diff-btn "+(config.difficulty===d.id?"active":"")+" diff-"+d.id} onClick={()=>setConfig({...config,difficulty:d.id})}> | |
| <span className="diff-label">{d.label}</span> | |
| <span className="diff-desc">{d.desc}</span> | |
| </button> | |
| )} | |
| </div> | |
| </div> | |
| <button className="btn btn-primary" onClick={handleStart} disabled={loading}>{loading ? "Starting..." : "Start Negotiation"}</button> | |
| </div> | |
| <div className="features-grid"> | |
| <div className="feature-card"><div className="feature-title">EQ Scoring</div><div className="feature-desc">Real-time emotional intelligence feedback</div></div> | |
| <div className="feature-card"><div className="feature-title">Live Coaching</div><div className="feature-desc">Tips after each round on what to say next</div></div> | |
| <div className="feature-card"><div className="feature-title">Deep Analytics</div><div className="feature-desc">Style analysis, power dynamics, market context</div></div> | |
| <div className="feature-card"><div className="feature-title">4 Scenarios</div><div className="feature-desc">Salary, promotion, resources, remote work</div></div> | |
| </div> | |
| </div> | |
| ); | |
| } | |