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 };