Files
doormilxpress_astryx/src/components/ai-assistant/dynamic.js
2026-08-11 19:17:40 +05:30

512 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.positions': (f) => {
if (!f.openPositions.length) return 'No open positions yet — publish a role and I will start tracking it.';
if (f.starvedPositions.length) {
return `${plural(f.starvedPositions.length, 'open position')} ${verb(f.starvedPositions.length, 'has', 'have')} no applicants and ${verb(f.starvedPositions.length, 'needs', 'need')} attention.`;
}
if (f.singleCandidateRoles.length) {
return `${plural(f.singleCandidateRoles.length, 'role')} ${verb(f.singleCandidateRoles.length, 'rests', 'rest')} on a single viable candidate.`;
}
if (f.unscreened.length) {
return `${plural(f.unscreened.length, 'applicant')} across your roles ${verb(f.unscreened.length, 'is', 'are')} waiting to be screened.`;
}
return `All ${f.openPositions.length} open roles have candidates and a moving pipeline.`;
},
'admin.candidatesList': (f) => {
if (!f.total) return 'No candidates in the pipeline yet.';
if (f.stalled.length) {
return `${plural(f.stalled.length, 'strong candidate')} ${verb(f.stalled.length, 'is', 'are')} screened and waiting on a decision.`;
}
if (f.unscreened.length) {
return `${plural(f.unscreened.length, 'candidate')} ${verb(f.unscreened.length, 'has', 'have')} no score and cannot be ranked.`;
}
if (f.missingCredentials.length) {
return `${plural(f.missingCredentials.length, 'scored candidate')} ${verb(f.missingCredentials.length, 'has', 'have')} no credentials on file.`;
}
return `All ${f.total} candidates are scored and actioned.`;
},
'admin.candidates': (f) => {
if (!f.total) return 'Nothing in the pipeline yet, so there is no pool to analyse.';
if (f.needsDeeperReview.length) {
return `${plural(f.needsDeeperReview.length, 'candidate')} currently ${verb(f.needsDeeperReview.length, 'needs', 'need')} deeper review.`;
}
if (f.singleCandidateRoles.length) {
return `${plural(f.singleCandidateRoles.length, 'role')} ${verb(f.singleCandidateRoles.length, 'rests', 'rest')} on a single viable candidate.`;
}
return `All ${f.total} candidates are scored, credentialed and comparable.`;
},
'admin.analytics': (f) => {
if (!f.total) return 'Nothing has been applied for yet, so the charts have nothing to report.';
if (f.bottleneck && f.bottleneck.lost > 0) {
return `Conversion is weakest between ${f.bottleneck.from} and ${f.bottleneck.to}, at ${f.bottleneck.rate}%.`;
}
if (f.standardizedPct < 80) {
return `Screening covers ${f.standardizedPct}% of applications, so the rest of these figures rest on a partial pool.`;
}
return `${plural(f.hired.length, 'hire')} from ${plural(f.total, 'applicant')} — ${f.hireRate}% conversion.`;
},
'admin.forge': (f) => {
const g = f.forge;
if (!g.courses.length) return 'No challenges published yet, so nothing can be proven here.';
if (g.locked.length && !g.ready.length) {
return `${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} gated by real-work requirements.`;
}
if (!g.verified.length) {
return `Nothing verified yet — ${plural(g.ready.length, 'challenge')} ${verb(g.ready.length, 'is', 'are')} ready to start.`;
}
return `${plural(g.verified.length, 'skill')} verified, ${plural(g.ready.length, 'challenge')} ready to start.`;
},
'admin.talentPool': (f) => {
if (!f.profiles.length) return 'No talent profiles registered yet.';
if (f.unscoredProfiles.length) {
return `${plural(f.unscoredProfiles.length, 'profile')} ${verb(f.unscoredProfiles.length, 'has', 'have')} no career score and cannot be ranked.`;
}
if (f.talent.unverified.length) {
return `${plural(f.talent.unverified.length, 'profile')} ${verb(f.talent.unverified.length, 'carries', 'carry')} no verified skill.`;
}
return `${plural(f.profiles.length, 'worker')} in the pool, ${f.elite.length} Elite.`;
},
'admin.hiredHistory': (f) => {
const h = f.hiring;
if (!h.hires.length) return 'Nobody has been hired yet, so there is no history to read.';
if (h.unrated.length) {
return `${plural(h.unrated.length, 'hire')} ${verb(h.unrated.length, 'has', 'have')} no client rating on file.`;
}
return `${plural(h.hires.length, 'hire')} placed, averaging ${h.avgQuality} at hire.`;
},
'admin.activity': (f) => {
if (!f.activity.length) return 'No activity logged yet.';
const patterns = f.activitySignals.flags.length;
if (patterns) {
return `${plural(patterns, 'unusual activity pattern')} ${verb(patterns, 'needs', 'need')} attention.`;
}
if (f.activity24h.length === 0) {
return `Quiet week — ${plural(f.activity7d.length, 'event')} logged, nothing out of pattern.`;
}
return `${plural(f.activity24h.length, 'event')} today, ${f.activity7d.length} this week, nothing out of pattern.`;
},
};
/**
* Title lines.
*
* Every Admin context greets by the clock. That is not decoration: the greeting
* is the one line guaranteed to be read, so it has to prove the panel knows when
* and where it is before it says anything about the data. A fixed "Good morning"
* on an 11pm shift does the opposite.
*
* First names read better than a formal greeting in a small panel, so the name is
* used where one is known.
*/
const TITLES = {
'employer.overview': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'employer.candidates': () => 'Candidate intelligence',
'employer.analytics': () => 'Analytics assistant',
'admin.controlCenter': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.positions': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.candidatesList': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.candidates': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.analytics': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.activity': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.forge': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.talentPool': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`,
'admin.hiredHistory': (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': (f) =>
`Reading ${plural(f.total, 'application')} across ${plural(f.postings.length, 'position')} — health, operations and what needs intervention.`,
'admin.positions': (f) =>
`Reading ${plural(f.total, 'application')} across ${plural(f.postings.length, 'position')} — hiring health, pipeline strength and what needs intervention.`,
'admin.candidatesList': (f) =>
`Reading ${plural(f.total, 'candidate')} across ${plural(f.postings.length, 'position')} — match quality, screening and hiring readiness.`,
'admin.candidates': (f) =>
`${plural(f.total, 'candidate')} and ${plural(f.profiles.length, 'talent profile')} — supply, quality and risk.`,
'admin.analytics': (f) =>
`Reading hiring performance across ${plural(f.openPositions.length, 'position')}, ${plural(f.total, 'candidate')} and ${plural(new Set(f.byRole.map((r) => r.category)).size, 'department')} — conversion, speed and outcomes.`,
'admin.activity': (f) => `${plural(f.activity.length, 'logged event')}, reconciled and profiled.`,
'admin.forge': (f) =>
`Reading ${plural(f.forge.courses.length, 'challenge')} — verified skills, what is ready to start and what is still locked.`,
'admin.talentPool': (f) =>
`Reading ${plural(f.profiles.length, 'talent profile')} — availability, career score and verification.`,
'admin.hiredHistory': (f) =>
`Reading ${plural(f.hiring.hires.length, 'hire')} across ${plural(f.hiring.byDepartment.length, 'department')} — quality, speed and outcomes.`,
};
/**
* Placeholders phrased as things this page's user would actually type.
*
* The Admin ones name Owliver and name the page's subject, because the panel sits
* permanently beside the dashboard: someone glancing at it needs the input itself
* to say what it is for and what this page can be asked about.
*/
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': () => [
'Ask Owliver about platform health…',
'Ask Owliver what needs attention…',
'Ask Owliver about hiring bottlenecks…',
],
'admin.positions': () => [
'Ask Owliver about your positions…',
'Ask Owliver which role to fill first…',
'Ask Owliver about hiring bottlenecks…',
],
'admin.candidatesList': () => [
'Ask Owliver about candidates…',
'Ask Owliver who is interview ready…',
'Ask Owliver about candidate risks…',
],
'admin.candidates': () => [
'Ask Owliver about candidates…',
'Ask Owliver about screening gaps…',
'Ask Owliver where the risk is…',
],
'admin.analytics': () => [
'Ask Owliver about hiring performance…',
'Ask Owliver about the hiring trend…',
'Ask Owliver where the bottleneck is…',
],
'admin.activity': () => [
'Ask Owliver about recent activity…',
'Ask Owliver about unusual activity…',
'Ask Owliver about security concerns…',
],
'admin.forge': () => [
'Ask Owliver which challenge to take next…',
'Ask Owliver what is blocking a skill…',
'Ask Owliver about Forge progress…',
],
'admin.talentPool': () => [
'Ask Owliver who to prioritize…',
'Ask Owliver about verification gaps…',
'Ask Owliver who is available…',
],
'admin.hiredHistory': () => [
'Ask Owliver about hiring outcomes…',
'Ask Owliver which roles hired best…',
'Ask Owliver what stands out…',
],
};
/* ── 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. The first chip is whatever the data has made urgent, so the set
reorders itself as the platform's state changes rather than offering the
same five questions regardless. */
'admin.controlCenter': (f) => [
f.starvedPositions.length
? { label: `${f.starvedPositions.length} roles with no applicants`, prompt: 'What needs attention right now?', capability: 'attention-required' }
: { label: 'What needs attention?', prompt: 'What needs attention right now?', capability: 'attention-required' },
{ label: 'Platform health', prompt: 'Run a platform health check', capability: 'platform-health' },
f.bottleneck && f.bottleneck.lost > 0
? { label: `Bottleneck at ${f.bottleneck.to}`, prompt: `Show hiring bottlenecks — why do candidates drop between ${f.bottleneck.from} and ${f.bottleneck.to}?`, capability: 'pipeline-health' }
: { label: 'Show hiring bottlenecks', prompt: 'Show hiring bottlenecks in the pipeline', capability: 'pipeline-health' },
{ label: 'Summarize platform usage', prompt: 'Summarize platform usage and workforce scale', capability: 'workforce-summary' },
{ label: 'Hiring operations', prompt: 'How are hiring operations performing?', capability: 'hiring-operations' },
{ label: 'Identify operational risks', prompt: 'Identify operational risks and what I should fix first', capability: 'recommendations' },
],
'admin.positions': (f) => [
f.starvedPositions.length
? { label: `${f.starvedPositions.length} roles need attention`, prompt: 'What needs attention across my positions?', capability: 'positions-attention' }
: { label: 'What needs attention?', prompt: 'What needs attention across my positions?', capability: 'positions-attention' },
{ label: 'Which position has the strongest pipeline?', prompt: 'Which position has the strongest pipeline?', capability: 'position-strength' },
f.unscreened.length
? { label: `${f.unscreened.length} waiting for review`, prompt: 'Show candidates waiting for review', capability: 'candidates-waiting' }
: { label: 'Show candidates waiting for review', prompt: 'Show candidates waiting for review', capability: 'candidates-waiting' },
{ label: 'Which position should I hire for first?', prompt: 'Which position should I hire for first?', capability: 'hiring-priority' },
{ label: 'Summarize hiring activity', prompt: 'Summarize hiring activity across my positions', capability: 'hiring-operations' },
{ label: 'Identify hiring bottlenecks', prompt: 'Identify hiring bottlenecks', capability: 'pipeline-health' },
],
'admin.candidatesList': (f) => [
f.stalled.length
? { label: `${f.stalled.length} waiting on a decision`, prompt: 'Who needs attention?', capability: 'candidates-attention' }
: { label: 'Who needs attention?', prompt: 'Who needs attention?', capability: 'candidates-attention' },
{ label: 'Who are the strongest candidates?', prompt: 'Who are the strongest candidates?', capability: 'top-candidates' },
{ label: 'Which candidates are ready for interview?', prompt: 'Which candidates are ready for interview?', capability: 'interview-ready' },
f.unscreened.length
? { label: `${f.unscreened.length} have no score`, prompt: 'Which candidates have no score?', capability: 'screening-gaps' }
: { label: 'Which candidates have no score?', prompt: 'Which candidates have no score?', capability: 'screening-gaps' },
{ label: 'Summarize the candidate pipeline', prompt: 'Summarize the candidate pipeline', capability: 'pipeline-summary' },
{ label: 'Identify candidate risks', prompt: 'Identify candidate risks', capability: 'candidate-risk' },
],
'admin.candidates': (f) => [
f.ranked.length >= 2
? { label: `Compare ${f.ranked[0].applicant_name.split(' ')[0]} & ${f.ranked[1].applicant_name.split(' ')[0]}`, prompt: 'Compare the top candidates in the pool', capability: 'top-candidates' }
: { label: 'Compare top candidates', prompt: 'Compare the top candidates in the pool', capability: 'top-candidates' },
f.missingCredentials.length
? { label: `${f.missingCredentials.length} missing credentials`, prompt: 'Show candidate risks across the pool', capability: 'candidate-risk' }
: { label: 'Show candidate risks', prompt: 'Show candidate risks across the pool', capability: 'candidate-risk' },
f.unscreened.length
? { label: `${f.unscreened.length} unscored applicants`, prompt: 'Find screening gaps in the candidate pool', capability: 'screening-gaps' }
: { label: 'Find screening gaps', prompt: 'Find screening gaps in the candidate pool', capability: 'screening-gaps' },
{ label: 'Recommend next actions', prompt: 'Recommend next actions for these candidates', capability: 'hiring-recommendations' },
{ label: 'Recruitment insights', prompt: 'Show recruitment insights across the platform', capability: 'recruitment-insights' },
],
'admin.analytics': (f) => [
{ label: 'What is the hiring trend?', prompt: 'What is the hiring trend?', capability: 'hiring-trend' },
{ label: 'Which department is performing best?', prompt: 'Which department is performing best?', capability: 'department-performance' },
f.bottleneck && f.bottleneck.lost > 0
? { label: `Biggest drop at ${f.bottleneck.to}`, prompt: `Where is the biggest hiring bottleneck — why do candidates drop between ${f.bottleneck.from} and ${f.bottleneck.to}?`, capability: 'pipeline-health' }
: { label: 'Where is the biggest bottleneck?', prompt: 'Where is the biggest hiring bottleneck?', capability: 'pipeline-health' },
{ label: 'Which positions are converting best?', prompt: 'Which positions are converting best?', capability: 'position-conversion' },
{ label: 'Summarize hiring performance', prompt: 'Summarize hiring performance', capability: 'hiring-operations' },
f.unscreened.length
? { label: `${f.unscreened.length} unscored applications`, prompt: 'What needs attention?', capability: 'attention-required' }
: { label: 'What needs attention?', prompt: 'What needs attention?', capability: 'attention-required' },
],
/* Forge, Talent Pool and Hired History. Same rule as the contexts above: the
first chip is whatever the data has made urgent, so the set reorders itself
rather than offering the same questions regardless of state. */
'admin.forge': (f) => {
const g = f.forge;
return [
g.next
? { label: `Should I take ${g.next.title}?`, prompt: `Which challenge should I complete next? Start with ${g.next.title}`, capability: 'forge-next' }
: { label: 'Which challenge should I take next?', prompt: 'Which challenge should I complete next?', capability: 'forge-next' },
g.locked.length
? { label: `${g.locked.length} blocked by requirements`, prompt: 'What is blocking my next skill?', capability: 'forge-blockers' }
: { label: 'What is blocking my next skill?', prompt: 'What is blocking my next skill?', capability: 'forge-blockers' },
g.ready.length
? { label: `${g.ready.length} ready to start`, prompt: 'Which challenges can I start right now?', capability: 'forge-ready' }
: { label: 'Which challenges can I start?', prompt: 'Which challenges can I start right now?', capability: 'forge-ready' },
{ label: 'Which skills am I closest to unlocking?', prompt: 'Which skills am I closest to unlocking?', capability: 'forge-skills' },
{ label: 'Summarize my Forge progress', prompt: 'Summarize my Forge progress', capability: 'forge-progress' },
];
},
'admin.talentPool': (f) => {
const t = f.talent;
return [
t.top[0]
? { label: `Why does ${t.top[0].full_name?.split(' ')[0] || 'the top profile'} rank first?`, prompt: 'Which talent should I prioritize?', capability: 'talent-priorities' }
: { label: 'Which talent should I prioritize?', prompt: 'Which talent should I prioritize?', capability: 'talent-priorities' },
f.unscoredProfiles.length
? { label: `${f.unscoredProfiles.length} profiles unscored`, prompt: 'Which profiles are unscored?', capability: 'talent-summary' }
: { label: 'Summarize the talent pool', prompt: 'Summarize the talent pool', capability: 'talent-summary' },
t.unverified.length
? { label: `${t.unverified.length} need verification`, prompt: 'Which talent needs verification?', capability: 'talent-verification' }
: { label: 'Which talent needs verification?', prompt: 'Which talent needs verification?', capability: 'talent-verification' },
{ label: 'Who is currently available?', prompt: 'Who is currently available?', capability: 'talent-availability' },
{ label: 'Who has the strongest score?', prompt: 'Who has the strongest score?', capability: 'talent-priorities' },
];
},
'admin.hiredHistory': (f) => {
const h = f.hiring;
return [
h.unrated.length
? { label: `${h.unrated.length} hires unrated`, prompt: 'What needs attention in hiring outcomes?', capability: 'hiring-patterns' }
: { label: 'What hiring patterns stand out?', prompt: 'What hiring patterns stand out?', capability: 'hiring-patterns' },
h.byDepartment[0]
? { label: `${h.byDepartment[0].name} leads on hires`, prompt: 'Which department has the most hires?', capability: 'hiring-outcomes' }
: { label: 'Which department has the most hires?', prompt: 'Which department has the most hires?', capability: 'hiring-outcomes' },
{ label: 'Which positions hired the strongest talent?', prompt: 'Which positions hired the strongest talent?', capability: 'hiring-strongest' },
{ label: 'Summarize recent hires', prompt: 'Summarize recent hires', capability: 'hiring-recent' },
{ label: 'What hiring outcomes should I review?', prompt: 'What hiring outcomes should I review?', capability: 'hiring-outcomes' },
];
},
'admin.activity': (f) => [
f.activitySignals.flags.length
? { label: `${f.activitySignals.flags.length} unusual patterns`, prompt: 'Show unusual activity on the platform', capability: 'unusual-activity' }
: { label: 'Show unusual activity', prompt: 'Show unusual activity on the platform', capability: 'unusual-activity' },
f.activity24h.length
? { label: `Summarize today's ${f.activity24h.length} events`, prompt: "Summarize today's activity", capability: 'audit-summary' }
: { label: "Summarize today's activity", prompt: "Summarize today's activity", capability: 'audit-summary' },
{ label: 'What changed recently?', prompt: 'What changed recently on the platform?', capability: 'audit-summary' },
{ label: 'Identify security concerns', prompt: 'Identify security concerns in the audit trail', capability: 'security-insights' },
{ label: 'Who is most active?', prompt: 'Which accounts are most active?', capability: 'user-activity' },
],
};
/* ── 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) ?? 'Owliver',
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) ?? [];
}