update chatbox

This commit is contained in:
2026-08-08 00:39:16 +05:30
parent 90f14a73b2
commit 0cae250049
15 changed files with 1952 additions and 913 deletions

View File

@@ -0,0 +1,265 @@
import { plural, verb } from './insights';
/**
* Dynamic greetings and suggested prompts.
*
* Nothing here is a fixed string chosen in advance. The greeting states what is
* actually true on the page right now, and the suggestions name real records —
* the specific candidates, the specific starved role. That is the difference
* between an assistant that looks aware of context and one that is.
*
* Everything derives from the same fact sheet the dashboard renders from, so a
* greeting can never contradict the numbers beside it.
*/
/* ── Greetings ──────────────────────────────────────────────────────────── */
/**
* Time of day from the viewer's own clock.
*
* `getHours()` is local, so this follows the user's timezone rather than the
* server's. The late band matters: at 00:30 "morning" is wrong, so 21:00–04:59
* reads as evening.
*
* 05:00–11:59 morning
* 12:00–16:59 afternoon
* 17:00–20:59 evening
* 21:00–04:59 evening
*/
function timeOfDay(date) {
const hour = date.getHours();
if (hour >= 5 && hour < 12) return 'morning';
if (hour >= 12 && hour < 17) return 'afternoon';
return 'evening';
}
/**
* Picks the single most useful thing to say about the page.
*
* Ordered by urgency rather than by what sounds cheeriest: something stuck beats
* something healthy, because the greeting is the one line guaranteed to be read.
*/
const GREETINGS = {
'employer.overview': (f) => {
if (!f.total) return 'No applicants yet — publish a position and I will start tracking it.';
if (f.stalled.length) {
return `${plural(f.stalled.length, 'strong candidate')} ${verb(f.stalled.length, 'is', 'are')} screened and waiting on you.`;
}
if (f.unscreened.length) {
return `${plural(f.unscreened.length, 'applicant')} ${verb(f.unscreened.length, 'is', 'are')} waiting to be screened.`;
}
if (f.starvedPositions.length) {
return `${plural(f.starvedPositions.length, 'open role')} ${verb(f.starvedPositions.length, 'has', 'have')} no applicants yet.`;
}
return 'Your hiring pipeline looks healthy today.';
},
'employer.candidates': (f) => {
if (!f.total) return 'No candidates in the pipeline yet.';
if (f.unscreened.length) {
return `You have ${plural(f.unscreened.length, 'candidate')} waiting for review.`;
}
if (f.stalled.length) {
return `${plural(f.stalled.length, 'candidate')} at 80+ ${verb(f.stalled.length, 'is', 'are')} ready for a decision.`;
}
return `All ${f.total} candidates are scored and actioned.`;
},
'employer.analytics': (f) => {
if (!f.scored.length) return 'Nothing scored yet, so the charts are still empty.';
if (f.bottleneck && f.bottleneck.lost > 0) {
return `Your funnel loses most people between ${f.bottleneck.from} and ${f.bottleneck.to}.`;
}
return `${plural(f.scored.length, 'candidate')} scored, averaging ${f.avgScore}. Worth a closer look.`;
},
'admin.controlCenter': (f) => {
if (f.unscoredProfiles.length) {
return `${plural(f.unscoredProfiles.length, 'talent profile')} ${verb(f.unscoredProfiles.length, 'has', 'have')} no score and cannot be matched.`;
}
if (f.activity24h.length) {
return `${plural(f.activity24h.length, 'event')} logged in the last 24 hours.`;
}
return `Platform is steady across ${plural(f.postings.length, 'position')} and ${plural(f.total, 'application')}.`;
},
'admin.candidates': (f) => {
if (f.missingCredentials.length) {
return `${plural(f.missingCredentials.length, 'scored candidate')} ${verb(f.missingCredentials.length, 'has', 'have')} no credentials on file.`;
}
return `${plural(f.profiles.length, 'worker')} in the pool, ${f.elite.length} at Elite.`;
},
'admin.activity': (f) => {
if (!f.activity.length) return 'No activity logged yet.';
if (f.activity24h.length === 0) {
return `Quiet day — ${plural(f.activity7d.length, 'event')} this week, none in the last 24 hours.`;
}
return `${plural(f.activity24h.length, 'event')} today, ${f.activity7d.length} this week.`;
},
'talent.portal': (f) => {
const p = f.profile;
if (!p) return 'Let us get your profile started.';
const completion = p.profile_completion || 0;
if (completion >= 90) return 'Your profile is nearly complete — nice work.';
if (completion > 0) return `You are ${completion}% of the way through your profile.`;
return 'Your profile is empty. A few fields will change what employers see.';
},
};
/** Title lines. First names read better than a formal greeting in a small panel. */
const TITLES = {
'employer.overview': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'employer.candidates': () => 'Candidate intelligence',
'employer.analytics': () => 'Analytics assistant',
'admin.controlCenter': (f) => `Platform check, ${timeOfDay(f.today)}`,
'admin.candidates': () => 'Talent analysis',
'admin.activity': () => 'Activity review',
'talent.portal': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
};
/** Descriptions state what this page's assistant can actually do with the data. */
const DESCRIPTIONS = {
'employer.overview': (f) =>
`Reading ${f.total} applicants across ${plural(f.openPositions.length, 'open role')}.`,
'employer.candidates': (f) => `All ${f.total} candidates and their score breakdowns.`,
'employer.analytics': () => 'Every figure on this page, explained or forecast.',
'admin.controlCenter': () => 'Health checks, data integrity and what needs intervention.',
'admin.candidates': () => 'Talent supply and risk across the candidate pool.',
'admin.activity': (f) => `${plural(f.activity.length, 'logged event')}, reconciled and profiled.`,
'talent.portal': () => 'Your score, your profile, and what to learn next.',
};
/** Placeholders phrased as things this page's user would actually type. */
const PLACEHOLDERS = {
'employer.overview': (f) => [
`Summarize ${timeOfDay(f.today)}'s hiring…`,
f.starvedPositions.length ? 'Which positions need attention?' : 'What should I do first?',
'Why did hiring slow this week?',
],
'employer.candidates': (f) => [
f.ranked.length >= 2 ? 'Compare the top candidates…' : 'Analyse my strongest candidate…',
'Which resumes need review?',
'Recommend interview questions…',
],
'employer.analytics': () => [
'Explain this chart…',
'Forecast hiring…',
'Generate an executive summary…',
],
'admin.controlCenter': () => ['Show unusual activity…', 'Summarize platform usage…'],
'admin.candidates': () => ['Where is the risk in the pool?', 'Summarize recruitment trends…'],
'admin.activity': () => ['Show unusual activity…', 'Summarize platform usage…'],
'talent.portal': () => ['Improve my profile…', 'Recommend learning paths…'],
};
/* ── Suggested prompts ──────────────────────────────────────────────────── */
/**
* Builds suggestions from what is actually on the page.
*
* A suggestion that names two real candidates is a different product from one
* that says "Compare candidates" — it proves the assistant already looked.
* Each carries the capability that answers it best, so a click is precise.
*/
const PROMPTS = {
'employer.overview': (f) => [
{ label: `Summarize ${timeOfDay(f.today)}'s hiring`, prompt: `Summarize hiring as of this ${timeOfDay(f.today)}`, capability: 'hiring-summary' },
f.starvedPositions.length
? { label: 'Which positions need attention?', prompt: `Which positions need attention? ${f.starvedPositions[0].title} has no applicants`, capability: 'pending-actions' }
: { label: 'What needs my attention?', prompt: 'What needs my attention right now?', capability: 'pending-actions' },
{ label: 'Pipeline health', prompt: 'How healthy is my hiring pipeline?', capability: 'hiring-health' },
f.singleCandidateRoles.length || f.missingCredentials.length
? { label: 'Where is the risk?', prompt: 'Where is the risk in my hiring right now?', capability: 'hiring-risks' }
: { label: 'Any hiring risks?', prompt: 'Are there any hiring risks I should know about?', capability: 'hiring-risks' },
],
'employer.candidates': (f) => {
const [a, b] = f.ranked;
return [
a && b
? { label: `Compare ${a.applicant_name.split(' ')[0]} & ${b.applicant_name.split(' ')[0]}`, prompt: `Compare ${a.applicant_name} and ${b.applicant_name}`, capability: 'candidate-comparison' }
: { label: 'Compare top candidates', prompt: 'Compare my top candidates', capability: 'candidate-comparison' },
f.unscreened.length
? { label: `${f.unscreened.length} resumes need review`, prompt: 'Which resumes need review?', capability: 'resume-analysis' }
: { label: 'Analyse top résumé', prompt: 'Analyse my strongest candidate', capability: 'resume-analysis' },
a
? { label: `Questions for ${a.applicant_name.split(' ')[0]}`, prompt: `Recommend interview questions for ${a.applicant_name}`, capability: 'interview-questions' }
: null,
{ label: 'Skill gaps', prompt: 'What skill gaps run across my candidates?', capability: 'skill-gap' },
{ label: 'Who should I hire?', prompt: 'Who should I hire, interview, or pass on?', capability: 'hiring-recommendation' },
].filter(Boolean);
},
'employer.analytics': (f) => [
{ label: 'Explain this chart', prompt: 'Explain the charts on this page', capability: 'explain-charts' },
{ label: 'Forecast hiring', prompt: 'Forecast hiring from the current pipeline', capability: 'forecast-hiring' },
{ label: 'Generate executive summary', prompt: 'Generate an executive hiring report', capability: 'generate-reports' },
f.bottleneck
? {
label: `Why the drop to ${f.bottleneck.to}?`,
prompt: `Why do candidates drop off between ${f.bottleneck.from} and ${f.bottleneck.to}?`,
capability: 'explain-charts',
}
: null,
{ label: 'Break down by role', prompt: 'Break down performance by role category', capability: 'department-insights' },
].filter(Boolean),
'admin.controlCenter': (f) => [
{ label: 'Platform health', prompt: 'Run a platform health check', capability: 'platform-health' },
{ label: 'Summarize platform usage', prompt: 'Summarize platform usage', capability: 'platform-health' },
f.unscoredProfiles.length
? { label: `${f.unscoredProfiles.length} unscored profiles`, prompt: 'What should I fix first?', capability: 'recommendations' }
: { label: 'What should I fix?', prompt: 'What should I fix first?', capability: 'recommendations' },
],
'admin.candidates': (f) => [
{ label: 'Recruitment insights', prompt: 'Show recruitment insights across the platform', capability: 'recruitment-insights' },
f.missingCredentials.length
? { label: `${f.missingCredentials.length} missing credentials`, prompt: 'Analyse risk across the candidate pool', capability: 'candidate-risk' }
: { label: 'Candidate risk', prompt: 'Analyse risk across the candidate pool', capability: 'candidate-risk' },
],
'admin.activity': (f) => [
{ label: 'Audit summary', prompt: 'Summarize the audit trail', capability: 'audit-summary' },
{ label: 'Show unusual activity', prompt: 'Show me unusual account activity', capability: 'user-activity' },
f.activity7d.length
? { label: `${f.activity7d.length} events this week`, prompt: 'Summarize platform usage this week', capability: 'audit-summary' }
: null,
].filter(Boolean),
'talent.portal': (f) => {
const p = f.profile;
return [
{ label: 'Improve my profile', prompt: 'Review my profile and tell me what is missing', capability: 'resume-review' },
{ label: 'Recommend learning paths', prompt: 'What should I learn next?', capability: 'learning-recommendations' },
p && (p.krow_score || 0) > 0
? { label: 'How do I score higher?', prompt: 'How do I raise my career score?', capability: 'career-guidance' }
: { label: 'How do I get started?', prompt: 'How do I raise my career score?', capability: 'career-guidance' },
];
},
};
/* ── Public API ─────────────────────────────────────────────────────────── */
const firstName = (fullName) => String(fullName || '').trim().split(/\s+/)[0] || '';
/**
* Everything the landing screen needs, computed from current data.
* `userName` personalizes the greeting where a name is known.
*/
export function buildIntro(contextId, facts, userName) {
const name = firstName(userName);
return {
title: TITLES[contextId]?.(facts, name) ?? 'Krow AI',
greeting: GREETINGS[contextId]?.(facts) ?? '',
description: DESCRIPTIONS[contextId]?.(facts) ?? '',
placeholders: PLACEHOLDERS[contextId]?.(facts) ?? ['Ask anything…'],
};
}
/** Suggested prompts for the current page and data. */
export function buildPrompts(contextId, facts) {
return PROMPTS[contextId]?.(facts) ?? [];
}