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) ?? []; }