Spaces:
Sleeping
Sleeping
lakshmisravya123
Major upgrade: comprehensive negotiation coaching with EQ scoring and detailed reports
dad7400 | const GROQ_API_KEY = process.env.GROQ_API_KEY; | |
| const OLLAMA_URL = process.env.OLLAMA_URL || "http://localhost:11434"; | |
| const GROQ_MODEL = process.env.GROQ_MODEL || "llama-3.3-70b-versatile"; | |
| const OLLAMA_MODEL = process.env.OLLAMA_MODEL || "llama3.2:3b"; | |
| async function callAI(prompt) { | |
| if (GROQ_API_KEY) { | |
| const res = await fetch("https://api.groq.com/openai/v1/chat/completions", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json", "Authorization": `Bearer ${GROQ_API_KEY}` }, | |
| body: JSON.stringify({ model: GROQ_MODEL, messages: [{ role: "user", content: prompt }], temperature: 0.7, max_tokens: 4096 }), | |
| }); | |
| if (res.ok) { const data = await res.json(); return data.choices[0].message.content; } | |
| console.warn("Groq failed, falling back to Ollama..."); | |
| } | |
| const res = await fetch(`${OLLAMA_URL}/api/generate`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ model: OLLAMA_MODEL, prompt, stream: false }), | |
| }); | |
| if (!res.ok) throw new Error("Both Groq and Ollama failed."); | |
| return (await res.json()).response; | |
| } | |
| function parseJSON(text) { | |
| try { return JSON.parse(text.trim()); } | |
| catch { | |
| const m = text.match(/\{[\s\S]*\}/); | |
| if (m) { | |
| try { return JSON.parse(m[0]); } | |
| catch { /* fall through */ } | |
| } | |
| throw new Error("Failed to parse AI response"); | |
| } | |
| } | |
| const SCENARIOS = { | |
| salary: { | |
| label: "Salary Negotiation", | |
| systemContext: (ctx) => `You are a hiring manager at ${ctx.company || "a tech company"} negotiating salary for a ${ctx.role} position.`, | |
| budgetLow: (target) => Math.round(target * 0.82), | |
| budgetMax: (target) => Math.round(target * 0.95), | |
| }, | |
| promotion: { | |
| label: "Promotion Negotiation", | |
| systemContext: (ctx) => `You are a department director at ${ctx.company || "a tech company"}. The employee (${ctx.role}) is requesting a promotion with a salary increase.`, | |
| budgetLow: (target) => Math.round(target * 0.88), | |
| budgetMax: (target) => Math.round(target * 0.97), | |
| }, | |
| resources: { | |
| label: "Project Resources", | |
| systemContext: (ctx) => `You are a VP of Engineering at ${ctx.company || "a tech company"}. A project lead (${ctx.role}) is negotiating for additional budget/headcount. Numbers represent project budget in thousands.`, | |
| budgetLow: (target) => Math.round(target * 0.70), | |
| budgetMax: (target) => Math.round(target * 0.90), | |
| }, | |
| remote: { | |
| label: "Remote Work Arrangement", | |
| systemContext: (ctx) => `You are an HR Director at ${ctx.company || "a tech company"}. An employee (${ctx.role}) is negotiating for remote work flexibility. Numbers represent flexibility score.`, | |
| budgetLow: (target) => Math.round(target * 0.60), | |
| budgetMax: (target) => Math.round(target * 0.90), | |
| }, | |
| }; | |
| function getScenario(type) { | |
| return SCENARIOS[type] || SCENARIOS.salary; | |
| } | |
| function getMarketContext(role, target) { | |
| return { marketLow: Math.round(target * 0.80), marketMedian: Math.round(target * 0.95), marketHigh: Math.round(target * 1.15) }; | |
| } | |
| async function startNegotiation(role, company, currentSalary, targetSalary, difficulty, scenarioType) { | |
| const scenario = getScenario(scenarioType); | |
| const diffStyle = { | |
| easy: "Be somewhat flexible, show empathy, offer small pushback but concede when the candidate makes reasonable points.", | |
| medium: "Be professional but firm, use standard negotiation tactics, require good justification before conceding.", | |
| hard: "Be very tough. Use aggressive tactics like anchoring low, creating urgency, questioning worth, using silence, referencing budget constraints.", | |
| }; | |
| const initial = scenario.budgetLow(targetSalary); | |
| const maxBudget = scenario.budgetMax(targetSalary); | |
| const text = await callAI(`${scenario.systemContext({ role, company })} | |
| SCENARIO TYPE: ${scenario.label} | |
| DIFFICULTY: ${difficulty} - ${diffStyle[difficulty] || diffStyle.medium} | |
| The candidate current salary is $${currentSalary || "unknown"} and they want $${targetSalary}. | |
| Your initial budget is $${initial} but you can go up to $${maxBudget} max. | |
| Start the negotiation. Make an opening statement and initial offer. Return ONLY valid JSON: | |
| {"hiringManagerName":"<a realistic full name>","openingStatement":"<2-3 sentences opening the discussion>","initialOffer":${initial},"roundNumber":1} | |
| Return ONLY JSON, no markdown, no code fences.`); | |
| const result = parseJSON(text); | |
| result.scenarioType = scenarioType || "salary"; | |
| result.scenarioLabel = scenario.label; | |
| return result; | |
| } | |
| async function negotiationRound(context, candidateResponse) { | |
| const scenario = getScenario(context.scenarioType); | |
| const historyStr = context.history.map(h => `${h.role}: ${h.text}`).join("\n"); | |
| const prompt = `${scenario.systemContext(context)} | |
| You are ${context.hiringManagerName}. | |
| CONTEXT: | |
| - Role: ${context.role}, Company: ${context.company} | |
| - Scenario: ${scenario.label} | |
| - Difficulty: ${context.difficulty} | |
| - Budget: Your max is $${context.maxBudget}. Current offer on the table: $${context.currentOffer}. | |
| - Target the candidate wants: $${context.targetSalary} | |
| - Round: ${context.rounds + 1} | |
| CONVERSATION HISTORY: | |
| ${historyStr} | |
| CANDIDATE JUST SAID: "${candidateResponse}" | |
| You must do TWO things: | |
| 1) RESPOND as the hiring manager (stay in character, use real tactics based on difficulty). | |
| 2) ANALYZE the candidate message as a negotiation coach (out of character). | |
| Return ONLY valid JSON (no markdown, no code fences): | |
| { | |
| "response": "<your 2-4 sentence in-character response as hiring manager>", | |
| "newOffer": <number - the current/updated dollar offer>, | |
| "tactic": "<the tactic YOU (hiring manager) used: anchoring|urgency|questioning|concession|silence|package-deal|final-offer|empathy|deflection|precedent>", | |
| "tacticExplanation": "<1 sentence explaining why you used this tactic>", | |
| "isAccepting": false, | |
| "isFinalOffer": false, | |
| "emotionalIntelligence": { | |
| "empathy": <1-10 score of how well candidate showed understanding of your position>, | |
| "assertiveness": <1-10 score of how confidently candidate stated their case>, | |
| "composure": <1-10 score of how calm and professional candidate remained>, | |
| "rapport": <1-10 score of how well candidate built connection with you> | |
| }, | |
| "candidateTactics": ["<tactics the CANDIDATE used: anchoring, BATNA-reference, value-framing, silence, mirroring, data-citation, emotional-appeal, concession-trading, deadline-pressure, walk-away-threat>"], | |
| "coachingTip": "<1-2 sentence real-time coaching tip: what the candidate should say or do next>", | |
| "coachingWarning": "<1 sentence about what to AVOID doing next, or empty string>", | |
| "momentRating": "<strong|neutral|weak - how effective was the candidate last message>" | |
| } | |
| Rules for newOffer: | |
| - Only increase incrementally if candidate is persuasive. | |
| - If weak argument, hold firm. | |
| - Never exceed $${context.maxBudget}. | |
| - If difficulty is hard, be very stingy with increases.`; | |
| return parseJSON(await callAI(prompt)); | |
| } | |
| async function generateReport(context) { | |
| const market = getMarketContext(context.role, context.targetSalary); | |
| const historyStr = context.history.map(h => `${h.role}: ${h.text}`).join("\n"); | |
| const pct = Math.round((context.finalOffer / context.targetSalary) * 100); | |
| const scenarioLabel = getScenario(context.scenarioType).label; | |
| const prompt = `You are an expert negotiation coach and organizational psychologist analyzing a negotiation practice session. Provide an extremely detailed and insightful performance report. | |
| SESSION DETAILS: | |
| - Scenario: ${scenarioLabel} | |
| - Role: ${context.role} at ${context.company} | |
| - Current Salary: $${context.currentSalary || "Not disclosed"} | |
| - Target: $${context.targetSalary} | |
| - Final Offer: $${context.finalOffer} | |
| - Rounds: ${context.rounds} | |
| - Difficulty: ${context.difficulty} | |
| - Market Data: Low $${market.marketLow} | Median $${market.marketMedian} | High $${market.marketHigh} | |
| FULL TRANSCRIPT: | |
| ${historyStr} | |
| Analyze the ENTIRE conversation deeply. Return ONLY valid JSON (no markdown, no code fences): | |
| { | |
| "overallScore": <1-100>, | |
| "finalSalary": ${context.finalOffer}, | |
| "targetSalary": ${context.targetSalary}, | |
| "percentOfTarget": ${pct}, | |
| "verdict": "<Master Negotiator|Strong Negotiator|Competent Negotiator|Needs Practice|Pushover>", | |
| "summary": "<4-5 sentence detailed assessment>", | |
| "negotiationStyle": { | |
| "primary": "<collaborative|competitive|accommodating|avoiding|compromising>", | |
| "description": "<2-3 sentence style analysis>", | |
| "effectiveness": <1-10> | |
| }, | |
| "emotionalIntelligence": { | |
| "overall": <1-100>, | |
| "empathy": <1-10>, | |
| "assertiveness": <1-10>, | |
| "composure": <1-10>, | |
| "rapport": <1-10>, | |
| "adaptability": <1-10>, | |
| "analysis": "<2-3 sentence EQ assessment>" | |
| }, | |
| "communicationScore": { | |
| "overall": <1-100>, | |
| "clarity": <1-10>, | |
| "persuasiveness": <1-10>, | |
| "activeListening": <1-10>, | |
| "questionQuality": <1-10>, | |
| "analysis": "<2 sentence communication assessment>" | |
| }, | |
| "powerDynamics": { | |
| "candidatePower": <1-10>, | |
| "managerPower": <1-10>, | |
| "shiftMoments": ["<describe 1-2 key moments when power shifted>"], | |
| "assessment": "<2 sentence power dynamics analysis>" | |
| }, | |
| "tacticsUsed": [ | |
| {"name": "<tactic name>", "effectiveness": "<effective|neutral|backfired>", "example": "<brief quote or paraphrase>"} | |
| ], | |
| "bestMoments": [ | |
| {"round": <number>, "quote": "<what candidate said>", "why": "<why effective>"} | |
| ], | |
| "worstMoments": [ | |
| {"round": <number>, "quote": "<what candidate said>", "why": "<why ineffective>"} | |
| ], | |
| "missedOpportunities": [ | |
| {"situation": "<what happened>", "betterApproach": "<what they should have done>", "impact": "<how it would have changed outcome>"} | |
| ], | |
| "marketContext": { | |
| "marketLow": ${market.marketLow}, | |
| "marketMedian": ${market.marketMedian}, | |
| "marketHigh": ${market.marketHigh}, | |
| "candidatePosition": "<below-market|at-market|above-market>", | |
| "analysis": "<1-2 sentence market positioning>" | |
| }, | |
| "strengths": ["<specific strength with example>", "<specific strength with example>"], | |
| "improvements": ["<specific improvement with actionable advice>", "<specific improvement>", "<specific improvement>"], | |
| "personalizedTips": [ | |
| {"tip": "<actionable tip personalized to their style>", "category": "<opening|tactics|closing|emotional|preparation>"}, | |
| {"tip": "<another tip>", "category": "<category>"}, | |
| {"tip": "<another tip>", "category": "<category>"}, | |
| {"tip": "<another tip>", "category": "<category>"}, | |
| {"tip": "<another tip>", "category": "<category>"} | |
| ], | |
| "letterGrade": "<A+|A|A-|B+|B|B-|C+|C|C-|D|F>", | |
| "nextSessionFocus": "<1-2 sentence recommendation for what to practice next time>" | |
| } | |
| Return ONLY JSON, no markdown, no code fences.`; | |
| return parseJSON(await callAI(prompt)); | |
| } | |
| module.exports = { startNegotiation, negotiationRound, generateReport, SCENARIOS }; | |