Files
krow_talent_app/src/components/ai-assistant/capabilities/admin.js
2026-08-25 18:56:07 +05:30

2143 lines
91 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 {
Activity, AlertTriangle, ArrowLeftRight, Award, BellRing, Clock, Eye, FileText, Filter, Gauge,
GitBranch, Layers, Lightbulb, Lock, ScrollText, ShieldAlert, ShieldCheck, Star, Target,
TrendingUp, UserCheck, Users,
} from 'lucide-react';
import {
actions, answer, doc, funnel, heading, insights, kpis, list, meters, note, status, table,
text, timeline,
} from '../blocks';
import { pct, plural, verb } from '../insights';
/* The proof-method wording is the Forge page's own, so a skill is never named
one thing in the library and another in the panel beside it. */
import { TYPE_LABEL } from '@/components/forge/challengeMeta';
import { toFICO } from '@/lib/talentHome';
/* The vetting criteria's own labels. Read from the position model rather than
restated here, so the panel calls a weight what the form calls it. */
import { CRITERIA_LABELS } from '@/lib/positionModel';
import { PERMISSIONS, PROFILE_ACTIONS } from '@/lib/admin/permissions';
/**
* Admin capabilities — Control Center, Candidates Analysis, Activity.
*
* Platform-wide rather than pipeline-specific: data integrity, talent supply,
* audit reconciliation.
*/
/* ── Control Center ─────────────────────────────────────────────────────── */
const platformHealth = (f) => {
const checks = [
{
label: 'Data integrity',
ok: f.applications.every((a) => a.job_posting_id),
good: 'Every application links to a live posting.',
bad: 'Some applications reference a posting that no longer exists.',
},
{
label: 'Screening coverage',
value: `${f.standardizedPct}%`,
ok: f.standardizedPct >= 80,
good: 'Applicants are scored consistently.',
bad: `${plural(f.unscreened.length, 'record')} unstandardized.`,
},
{
label: 'Interview integrity',
ok: f.flagged.length === 0,
good: 'No interviews flagged.',
bad: `${plural(f.flagged.length, 'interview')} below full integrity.`,
},
{
label: 'Profile completeness',
value: `${f.profiles.length - f.unscoredProfiles.length}/${f.profiles.length}`,
ok: f.unscoredProfiles.length <= f.profiles.length / 2,
good: 'Most talent profiles carry a career score.',
bad: `${plural(f.unscoredProfiles.length, 'profile')} without a score cannot be matched or ranked.`,
},
{
label: 'Throughput',
value: `${f.hireRate}%`,
ok: f.hired.length > 0,
good: `${plural(f.hired.length, 'hire')} closed.`,
bad: 'No hires closed yet, so there is no conversion baseline.',
},
].map((c) => ({ label: c.label, value: c.value, ok: c.ok, note: c.ok ? c.good : c.bad }));
const passing = checks.filter((c) => c.ok).length;
return doc(
heading(`Platform health: ${passing}/${checks.length} passing`),
status(checks),
heading('Scale'),
kpis([
{ label: 'Positions', value: f.postings.length, sub: `${f.openPositions.length} open` },
{ label: 'Applications', value: f.total },
{ label: 'Interviews', value: f.interviews.length, sub: `${f.completedInterviews.length} scored` },
{ label: 'Talent pool', value: f.profiles.length, sub: `${f.elite.length} Elite` },
]),
text(
passing === checks.length
? 'Nothing needs intervention at the platform level.'
: 'The failing checks are data-quality issues, not outages — they degrade matching quality rather than break anything.'
)
);
};
/**
* Workforce summary — the platform's scale in one read.
*
* Answers "how big is this thing and what is it made of", which is a different
* question from "is it healthy": no checks, no verdicts, just the composition.
*/
const workforceSummary = (f) => doc(
heading('Workforce summary', `${plural(f.postings.length, 'position')} · ${plural(f.profiles.length, 'worker')}`),
kpis([
{ label: 'Open roles', value: f.openPositions.length, sub: `of ${f.postings.length}` },
{ label: 'Applicants', value: f.total, sub: `${f.scored.length} scored` },
{ label: 'On staff', value: f.staff.length, sub: `${f.hired.length} hired here` },
{ label: 'Talent pool', value: f.profiles.length, sub: `${f.elite.length} Elite` },
]),
heading('Where the people are'),
meters([
{ label: 'In the pipeline', value: f.total, max: f.total + f.profiles.length },
{ label: 'In the talent pool', value: f.profiles.length, max: f.total + f.profiles.length },
{ label: 'Placed on staff', value: f.staff.length, max: f.total + f.profiles.length, tone: 'success' },
]),
heading('Demand by role category'),
table(
[
{ key: 'category', label: 'Category' },
{ key: 'roles', label: 'Roles', align: 'right' },
{ key: 'applied', label: 'Applicants', align: 'right' },
],
Object.values(f.byRole.reduce((acc, r) => {
acc[r.category] ||= { category: r.category, roles: 0, applied: 0 };
acc[r.category].roles += 1;
acc[r.category].applied += r.applied;
return acc;
}, {})).sort((a, b) => b.applied - a.applied)
),
text(
f.profiles.length > f.total
? `The talent pool is larger than the live pipeline — ${f.profiles.length} registered workers against ${f.total} applications. Most of the supply is not applying to anything.`
: 'Most of the platform\'s people are attached to a live application rather than sitting in the pool.'
)
);
/**
* Hiring operations — throughput and speed.
*
* The operational question is whether work is moving, so this leads with rate
* and duration rather than totals.
*/
const hiringOperations = (f) => {
const perRole = f.byRole
.slice()
.sort((a, b) => b.applied - a.applied)
.map((r) => ({
title: r.title,
applied: r.applied,
qualified: r.qualified,
hired: r.hired || '—',
rate: { value: `${r.applied ? Math.round((r.qualified / r.applied) * 100) : 0}%`, tone: r.applied && r.qualified / r.applied >= 0.5 ? 'success' : 'warning' },
}));
return doc(
heading('Hiring operations'),
kpis([
{ label: 'Applicants', value: f.total },
{ label: 'Hires', value: f.hired.length, tone: 'success', sub: `${f.hireRate}% of applicants` },
{ label: 'Time to hire', value: f.timeToHire ? `${f.timeToHire}d` : '—' },
{ label: 'Screening', value: `${f.standardizedPct}%`, tone: f.standardizedPct >= 80 ? 'success' : 'warning', sub: 'coverage' },
]),
heading('Throughput by role'),
table(
[
{ key: 'title', label: 'Role' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'qualified', label: '70+', align: 'right' },
{ key: 'hired', label: 'Hired', align: 'right' },
{ key: 'rate', label: 'Qualified', align: 'right' },
],
perRole
),
insights([
f.starvedPositions.length && {
tone: 'warning',
title: `${plural(f.starvedPositions.length, 'role')} attracting nobody`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Throughput cannot improve on a role with no input.`,
},
f.timeToHire > 0 && {
tone: f.timeToHire <= 7 ? 'success' : 'warning',
title: `Hires close in ${plural(f.timeToHire, 'day')}`,
body: f.timeToHire <= 7
? 'Fast enough that strong candidates are unlikely to be lost to a competing offer.'
: 'Long enough that the best candidates may accept elsewhere first.',
},
]),
note('Time-to-hire is measured from application to decision, so it reflects this platform\'s part of the process only.')
);
};
/**
* Pipeline health — the funnel and its worst step.
*
* A funnel block rather than a table: the shape is the finding, and the single
* weakest transition is what someone can actually act on.
*/
const pipelineHealth = (f) => {
if (!f.total) return answer('Nothing has entered the pipeline yet, so there is no funnel to read.');
const worst = f.bottleneck;
return doc(
heading('Pipeline health', `${plural(f.total, 'candidate')} entered`),
funnel(f.funnel.map((stage, i) => ({
label: stage.label,
count: stage.count,
rate: i === 0 ? null : f.transitions[i - 1]?.rate,
lost: i === 0 ? 0 : f.transitions[i - 1]?.lost,
}))),
worst && worst.lost > 0
? insights([{
tone: worst.rate < 50 ? 'risk' : 'warning',
title: `${worst.from} → ${worst.to} is the bottleneck`,
body: `Only ${worst.rate}% pass through, losing ${plural(worst.lost, 'candidate')}. Every other step performs better, so this is where recovered conversion is cheapest.`,
}])
: insights([{
tone: 'success',
title: 'No single step is losing candidates',
body: 'Conversion is even across the funnel — there is no cheap win to recover here.',
}]),
heading('Stage-to-stage rates'),
table(
[
{ key: 'step', label: 'Transition' },
{ key: 'rate', label: 'Pass', align: 'right' },
{ key: 'lost', label: 'Lost', align: 'right' },
],
f.transitions.map((t) => ({
step: `${t.from} → ${t.to}`,
rate: { value: `${t.rate}%`, tone: t.rate < 60 ? 'warning' : 'success' },
lost: t.lost ? { value: `−${t.lost}`, tone: 'risk' } : '0',
}))
)
);
};
/**
* Attention required — the triage queue, ordered by cost of inaction.
*
* The same items the Control Center lists, because the panel must never rank
* things differently from the page beside it.
*/
const attentionRequired = (f) => {
const items = [
f.starvedPositions.length && {
tone: 'risk',
title: `${plural(f.starvedPositions.length, 'role')} with no candidate supply`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Live and attracting nobody — usually a pay band or distribution problem.`,
},
f.stalled.length && {
tone: 'risk',
title: `${plural(f.stalled.length, 'strong candidate')} not actioned`,
body: `${f.stalled.map((a) => a.applicant_name).join(', ')} scored 80 or above and ${verb(f.stalled.length, 'is', 'are')} still sitting at AI Screened. The most expensive kind of delay.`,
},
f.unscreened.length && {
tone: 'warning',
title: `${plural(f.unscreened.length, 'applicant')} waiting on screening`,
body: 'Each unscored application is a decision made without a standard.',
},
f.unscoredProfiles.length && {
tone: 'warning',
title: `${plural(f.unscoredProfiles.length, 'talent profile')} without a score`,
body: 'Invisible to matching, so employers cannot find them even when they fit.',
},
f.unratedStaff.length && {
tone: 'info',
title: `${plural(f.unratedStaff.length, 'hire')} awaiting review`,
body: 'Employer ratings are the only verified performance signal feeding scores.',
},
f.flagged.length && {
tone: 'warning',
title: `${plural(f.flagged.length, 'interview')} flagged for integrity`,
body: 'Worth clearing before those candidates progress.',
},
].filter(Boolean);
if (!items.length) {
return answer('Nothing needs intervention. Every open role has candidates, every applicant is scored, and no interview is flagged.');
}
return doc(
heading(`${plural(items.length, 'item')} need attention`, 'Ordered by cost of leaving it'),
insights(items),
text(`Start with ${items[0].title.toLowerCase()} — it is the one where waiting costs the most.`)
);
};
const recommendations = (f) => {
const items = [
f.unscoredProfiles.length && {
title: `Prompt ${plural(f.unscoredProfiles.length, 'worker')} to finish onboarding`,
body: 'Without a career score they are invisible to matching, so employers cannot find them even when they fit.',
},
f.unscreened.length && {
title: `${plural(f.unscreened.length, 'application')} unscored`,
body: 'Each one is a decision made without a standard.',
},
f.starvedPositions.length && {
title: `${plural(f.starvedPositions.length, 'posting')} with zero applicants`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Check distribution and pay bands.`,
},
f.unratedStaff.length && {
title: `${plural(f.unratedStaff.length, 'hire')} unrated`,
body: 'Employer ratings are the only verified performance signal feeding scores; without them the scores stay theoretical.',
},
f.flagged.length && {
title: `Review ${plural(f.flagged.length, 'flagged interview')}`,
body: 'Worth clearing before those candidates progress.',
},
].filter(Boolean);
if (!items.length) return answer('No platform actions outstanding. Data is complete and nothing is flagged.');
return doc(heading('Recommended actions', 'Ordered by impact on matching quality'), actions(items));
};
/* ── Candidates Analysis ────────────────────────────────────────────────── */
const recruitmentInsights = (f) => {
const band = (min, max) => f.profiles.filter((p) => {
const s = p.krow_score || 0;
return s >= min && (max == null || s < max);
}).length;
const bands = [
{ label: 'Elite', count: band(90), tone: 'info' },
{ label: 'Excellent', count: band(75, 90), tone: 'success' },
{ label: 'Solid', count: band(60, 75) },
{ label: 'Building', count: band(1, 60), tone: 'warning' },
{ label: 'Unscored', count: f.unscoredProfiles.length, tone: 'risk' },
];
const verified = f.profiles.filter((p) => (p.capabilities || []).length).length;
const topHeavy = band(90) + band(75, 90);
return doc(
heading('Talent supply', `${plural(f.profiles.length, 'worker')} in the pool`),
table(
[{ key: 'label', label: 'Band' }, { key: 'count', label: 'Workers', align: 'right' }],
bands.map((b) => ({ label: b.label, count: { value: b.count, tone: b.tone } }))
),
heading('Pipeline'),
kpis([
{ label: 'Applicants', value: f.total, sub: `avg ${f.avgScore}` },
{ label: 'At 80+', value: f.ranked.filter((a) => a.ai_score >= 80).length, tone: 'success' },
{ label: 'Verified skills', value: verified, sub: 'with evidence' },
{ label: 'Applications', value: f.eventCounts.apply_job || 0, sub: 'logged' },
]),
insights([
f.unscoredProfiles.length > topHeavy
? {
tone: 'warning',
title: 'The pool is bottom-heavy',
body: `${f.unscoredProfiles.length} unscored against ${topHeavy} at Excellent or above. Supply is not the constraint — verification is.`,
}
: {
tone: 'success',
title: 'Verification is keeping pace',
body: 'The scored portion of the pool outweighs the unscored.',
},
]),
heading('By role category'),
table(
[
{ key: 'title', label: 'Role' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'qualified', label: '70+', align: 'right' },
],
f.byRole.slice().sort((a, b) => b.applied - a.applied)
.map((r) => ({ title: r.title, applied: r.applied, qualified: r.qualified }))
)
);
};
const candidateRiskAnalysis = (f) => {
const noHistory = f.scored.filter((a) => !a.years_experience);
const risks = [
f.missingCredentials.length && {
tone: 'risk',
title: `${plural(f.missingCredentials.length, 'candidate')} with no credentials`,
body: 'Compliance risk if placed on a role that requires certification.',
},
f.narrowAvailability.length && {
tone: 'warning',
title: `${plural(f.narrowAvailability.length, 'candidate')} with narrow availability`,
body: 'One slot or none — most likely to fall through on the day.',
},
f.flagged.length && {
tone: 'risk',
title: `${plural(f.flagged.length, 'interview')} flagged for integrity`,
body: 'Usually suspiciously fast responses. Worth a human re-interview.',
},
noHistory.length && {
tone: 'warning',
title: `${plural(noHistory.length, 'candidate')} with no recorded experience`,
body: 'Not disqualifying, but unverifiable on paper.',
},
f.singleCandidateRoles.length && {
tone: 'warning',
title: 'Concentration risk',
body: `${f.singleCandidateRoles.map((e) => e.posting.title).join(', ')} ${verb(f.singleCandidateRoles.length, 'depends', 'depend')} on a single viable candidate.`,
},
].filter(Boolean);
if (!risks.length) {
return answer('No material risk flags: credentials, availability and interview integrity all check out across the scored pool.');
}
return doc(
heading(`${plural(risks.length, 'risk flag')}`),
insights(risks),
note('Flags to check, not judgements. Each is a question for a human, not a rejection.')
);
};
/**
* Screening gaps — where the record is incomplete.
*
* Distinct from risk: risk is what the data says about a candidate, this is what
* the data fails to say at all. An unscored applicant is not a weak applicant,
* and conflating the two is how good people get passed over.
*/
const screeningGaps = (f) => {
const gaps = [
{
label: 'Unscored applications',
count: f.unscreened.length,
note: 'Cannot be ranked or compared against anyone.',
},
{
label: 'Scored without credentials',
count: f.missingCredentials.length,
note: 'No certifications on file to verify against a role requirement.',
},
{
label: 'No recorded experience',
count: f.scored.filter((a) => !a.years_experience).length,
note: 'Scored on other dimensions, unverifiable on history.',
},
{
label: 'Interviews started, not scored',
count: f.interviews.length - f.completedInterviews.length,
note: 'Abandoned before a verdict, so the interview signal is missing.',
},
{
label: 'Availability not set',
count: f.narrowAvailability.length,
note: 'One slot or none recorded, so scheduling is guesswork.',
},
].filter((g) => g.count > 0);
if (!gaps.length) {
return answer('No screening gaps. Every applicant is scored, credentialed and has availability recorded, and every interview reached a verdict.');
}
return doc(
heading('Screening gaps', `${plural(gaps.length, 'kind')} of missing data`),
table(
[
{ key: 'label', label: 'Gap' },
{ key: 'count', label: 'Records', align: 'right' },
],
gaps.map((g) => ({ label: g.label, count: { value: g.count, tone: 'warning' } }))
),
heading('Coverage'),
status([
{
label: 'Screening coverage',
value: `${f.standardizedPct}%`,
ok: f.standardizedPct >= 80,
note: f.standardizedPct >= 80
? 'Enough of the pool is scored for comparisons to mean something.'
: `${plural(f.unscreened.length, 'applicant')} outside the standard.`,
},
{
label: 'Interview completion',
value: `${f.interviewCompletion}%`,
ok: f.interviewCompletion >= 80,
note: f.interviewCompletion >= 80
? 'Most interviews reach a verdict.'
: 'A meaningful share of interviews stop before scoring.',
},
]),
list(gaps.map((g) => `**${g.label}** — ${g.note}`)),
note('A gap is missing evidence, not a negative finding. These candidates are unassessed, not unsuitable.')
);
};
/**
* Hiring recommendations — who to move on, and in what order.
*
* Named people with a stated reason. A recommendation that does not name anyone
* is an observation, and the Admin already has plenty of those.
*/
const hiringRecommendations = (f) => {
if (!f.scored.length) {
return answer('Nothing is scored yet, so there is no basis for a recommendation. Screen the pipeline first.');
}
const advance = f.ranked.filter((a) => a.ai_score >= 80 && a.status === 'ai_screened').slice(0, 4);
const interview = f.ranked.filter((a) => a.ai_score >= 60 && a.ai_score < 80 && a.status === 'ai_screened').slice(0, 4);
const review = f.unscreened.slice(0, 4);
const rows = [
...advance.map((a) => ({
name: a.applicant_name,
role: a.job_title,
score: { value: a.ai_score, tone: 'success' },
action: { badge: 'Advance', tone: 'success' },
})),
...interview.map((a) => ({
name: a.applicant_name,
role: a.job_title,
score: { value: a.ai_score, tone: 'info' },
action: { badge: 'Interview', tone: 'info' },
})),
...review.map((a) => ({
name: a.applicant_name,
role: a.job_title,
score: '—',
action: { badge: 'Screen', tone: 'warning' },
})),
];
return doc(
heading('Recommended next actions', `${plural(rows.length, 'candidate')}`),
table(
[
{ key: 'name', label: 'Candidate' },
{ key: 'role', label: 'Role' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'action', label: 'Action', align: 'right' },
],
rows
),
actions([
advance.length && {
title: `Advance ${plural(advance.length, 'candidate')} scoring 80 or above`,
body: `${advance.map((a) => a.applicant_name).join(', ')}. Already screened and waiting — the delay, not the assessment, is what loses these people.`,
},
review.length && {
title: `Screen ${plural(review.length, 'waiting applicant')}`,
body: 'Until they are scored they cannot be compared with anyone already in the funnel.',
},
f.singleCandidateRoles.length && {
title: `Widen supply on ${plural(f.singleCandidateRoles.length, 'role')}`,
body: `${f.singleCandidateRoles.map((e) => e.posting.title).join(', ')} ${verb(f.singleCandidateRoles.length, 'rests', 'rest')} on one viable candidate. One withdrawal reopens the search.`,
},
]),
note('Ranked on screening data only. Nothing here accounts for a conversation you have had that the platform has not seen.')
);
};
/**
* Top candidates side by side.
*
* A comparison is a table — the whole point is reading the same dimension across
* people, which prose makes impossible.
*/
const topCandidates = (f) => {
const top = f.ranked.slice(0, 5);
if (!top.length) return answer('No scored candidates yet, so there is nobody to compare.');
const DIMENSIONS = ['experience', 'english', 'reliability', 'certifications', 'availability'];
return doc(
heading('Strongest candidates', `Top ${top.length} by KROW score`),
table(
[
{ key: 'name', label: 'Candidate' },
{ key: 'role', label: 'Applied for' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'stage', label: 'Stage', align: 'right' },
],
top.map((a) => ({
name: a.applicant_name,
role: a.job_title,
score: { value: a.ai_score, tone: a.ai_score >= 80 ? 'success' : 'info' },
stage: { badge: String(a.status).replace(/_/g, ' '), tone: a.status === 'hired' ? 'success' : 'neutral' },
}))
),
heading('Score breakdown', top[0].applicant_name),
meters(DIMENSIONS.map((d) => ({
label: d[0].toUpperCase() + d.slice(1),
value: top[0].score_breakdown?.[d] ?? 0,
max: 100,
}))),
insights([
top.length >= 2 && {
tone: 'info',
title: `${top[0].applicant_name} leads by ${top[0].ai_score - top[1].ai_score} points`,
body: top[0].ai_score - top[1].ai_score <= 5
? `Effectively level with ${top[1].applicant_name} — close enough that the choice should come down to a conversation, not the score.`
: `A clear enough margin over ${top[1].applicant_name} to act on.`,
},
f.stalled.length && {
tone: 'warning',
title: `${plural(f.stalled.length, 'candidate')} at 80+ waiting on a decision`,
body: f.stalled.map((a) => a.applicant_name).join(', '),
},
])
);
};
/* ── Activity ───────────────────────────────────────────────────────────── */
const auditSummary = (f) => {
const count = (type) => f.activity.filter((e) => e.event_type === type).length;
const hires = count('hire_candidate');
const screens = count('screen_candidate');
return doc(
heading('Audit summary'),
kpis([
{ label: 'Total events', value: f.activity.length },
{ label: 'Last 7 days', value: f.activity7d.length },
{ label: 'Last 24 hours', value: f.activity24h.length, tone: f.activity24h.length ? undefined : 'warning' },
{ label: 'Distinct users', value: new Set(f.activity.map((e) => e.user_email)).size },
]),
heading('Privileged actions'),
table(
[{ key: 'action', label: 'Action' }, { key: 'count', label: 'Events', align: 'right' }],
[
{ action: 'Position created', count: count('create_position') },
{ action: 'Candidate screened', count: screens },
{ action: 'Candidate hired', count: hires },
{ action: 'Interview started', count: count('start_interview') },
]
),
heading('Reconciliation'),
status([
{
label: 'Hires match staff records',
ok: hires === f.staff.length,
value: `${hires}/${f.staff.length}`,
note: hires === f.staff.length
? 'Every person on staff has a matching hire event.'
: `${Math.abs(f.staff.length - hires)} unexplained.`,
},
{
label: 'Screening events match scores',
ok: screens >= f.scored.length,
value: `${screens}/${f.scored.length}`,
note: screens < f.scored.length
? 'Batch screening records one event per run, not per candidate.'
: 'Screening events reconcile with scored candidates.',
},
]),
note('An audit trail is only as good as its gaps. The reconciliation above is what I would check first in a real review.')
);
};
const userActivityInsights = (f) => {
const perUser = f.activity.reduce((acc, e) => {
acc[e.user_email] ||= { name: e.user_name, role: e.account_type, count: 0, types: new Set() };
acc[e.user_email].count += 1;
acc[e.user_email].types.add(e.event_type);
return acc;
}, {});
const ranked = Object.entries(perUser).sort((a, b) => b[1].count - a[1].count);
if (!ranked.length) return answer('No activity to profile yet.');
const [topEmail, top] = ranked[0];
const share = Math.round((top.count / f.activity.length) * 100);
return doc(
heading('Most active accounts'),
table(
[
{ key: 'name', label: 'Account' },
{ key: 'role', label: 'Role' },
{ key: 'events', label: 'Events', align: 'right' },
],
ranked.slice(0, 6).map(([email, d]) => ({
name: d.name || email,
role: { badge: d.role || 'unknown', tone: d.role === 'employer' ? 'info' : 'neutral' },
events: { value: d.count, tone: d.count === top.count ? 'info' : undefined },
}))
),
insights([
share >= 50
? {
tone: 'warning',
title: `One account is ${share}% of all activity`,
body: `${top.name || topEmail} accounts for ${top.count} of ${f.activity.length} events. On a live platform, that concentration is worth confirming is a person and not an integration.`,
}
: {
tone: 'success',
title: 'Activity is spread across accounts',
body: `No single account dominates the log — the busiest is ${share}%.`,
},
f.activity24h.length === 0 && {
tone: 'warning',
title: 'Nothing in the last 24 hours',
body: 'Expected for a demo dataset; on a live platform it would be worth checking ingestion.',
},
])
);
};
/**
* Privileged actions carry more audit weight than reads. Kept in step with the
* Activity page's own severity table — one definition of "privileged", so the
* panel and the log can never disagree about what matters.
*/
const HIGH_SEVERITY = ['hire_candidate', 'create_position'];
const MEDIUM_SEVERITY = ['assign_employee', 'screen_candidate', 'start_interview'];
/**
* Unusual activity — what is out of pattern, stated as a pattern.
*
* Deliberately not "suspicious". These are deviations from the platform's own
* baseline, and on a real deployment most of them turn out to be an integration
* or a busy afternoon. Saying otherwise would manufacture alarm.
*/
const unusualActivity = (f) => {
if (!f.activity.length) return answer('No activity logged yet, so there is no baseline to deviate from.');
const perUser = f.activity.reduce((acc, e) => {
acc[e.user_email] ||= { name: e.user_name, count: 0, privileged: 0, types: new Set() };
acc[e.user_email].count += 1;
acc[e.user_email].types.add(e.event_type);
if (HIGH_SEVERITY.includes(e.event_type)) acc[e.user_email].privileged += 1;
return acc;
}, {});
const accounts = Object.entries(perUser);
const busiest = accounts.slice().sort((a, b) => b[1].count - a[1].count)[0];
const share = busiest ? Math.round((busiest[1].count / f.activity.length) * 100) : 0;
/* Bursts: more than three events from one account inside an hour. */
const byHour = f.activity.reduce((acc, e) => {
const key = `${e.user_email}|${String(e.created_date).slice(0, 13)}`;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const bursts = Object.entries(byHour).filter(([, n]) => n > 3);
const privileged = f.activity.filter((e) => HIGH_SEVERITY.includes(e.event_type));
const offHours = f.activity.filter((e) => {
const hour = new Date(e.created_date).getHours();
return hour < 6 || hour >= 22;
});
const patterns = [
share >= 50 && {
tone: 'warning',
title: `One account is ${share}% of all activity`,
body: `${busiest[1].name || busiest[0]} accounts for ${busiest[1].count} of ${f.activity.length} events. Worth confirming that is a person and not an integration running under someone's credentials.`,
},
bursts.length && {
tone: 'warning',
title: `${plural(bursts.length, 'burst')} of rapid activity`,
body: 'More than three actions from one account inside a single hour. Normal for batch work, worth a glance if it is not expected.',
},
offHours.length && {
tone: 'info',
title: `${plural(offHours.length, 'event')} outside working hours`,
body: 'Logged before 06:00 or after 22:00 local time. Not a problem on its own; a pattern of privileged actions at those hours would be.',
},
f.activity24h.length === 0 && {
tone: 'warning',
title: 'Nothing in the last 24 hours',
body: 'Expected for a seeded dataset. On a live platform, silence is an ingestion question rather than a quiet day.',
},
privileged.length > f.activity.length * 0.3 && {
tone: 'warning',
title: `Privileged actions are ${Math.round((privileged.length / f.activity.length) * 100)}% of the log`,
body: 'A log dominated by hires and position changes usually means routine reads are not being captured, which weakens the audit trail.',
},
].filter(Boolean);
if (!patterns.length) {
return doc(
heading('Nothing out of pattern'),
kpis([
{ label: 'Events', value: f.activity.length },
{ label: 'Accounts', value: accounts.length },
{ label: 'Busiest share', value: `${share}%`, tone: 'success' },
]),
text('Activity is spread across accounts, there are no bursts, and privileged actions are a normal share of the log.')
);
}
return doc(
heading(`${plural(patterns.length, 'pattern')} worth attention`),
insights(patterns),
heading('Per account'),
table(
[
{ key: 'name', label: 'Account' },
{ key: 'events', label: 'Events', align: 'right' },
{ key: 'privileged', label: 'Privileged', align: 'right' },
],
accounts
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 6)
.map(([email, d]) => ({
name: d.name || email,
events: { value: d.count, tone: d.count === busiest[1].count ? 'info' : undefined },
privileged: d.privileged ? { value: d.privileged, tone: 'warning' } : '0',
}))
),
note('Deviations from this platform\'s own baseline, not accusations. Each one is a question to ask, and most have a dull answer.')
);
};
/**
* Security insights — the audit trail judged as a control, not as a feed.
*
* The question a security reviewer actually asks is whether the log would stand
* up: are privileged actions attributable, is coverage complete, is anything
* unaccounted for. So this reports on the trail itself.
*/
const securityInsights = (f) => {
const privileged = f.activity.filter((e) => HIGH_SEVERITY.includes(e.event_type));
const attributed = privileged.filter((e) => e.user_email && e.user_name);
const named = f.activity.filter((e) => e.user_email);
const hires = f.activity.filter((e) => e.event_type === 'hire_candidate').length;
const checks = [
{
label: 'Privileged actions attributable',
value: `${attributed.length}/${privileged.length}`,
ok: privileged.length === attributed.length,
note: privileged.length === attributed.length
? 'Every hire and position change traces to a named account.'
: `${privileged.length - attributed.length} cannot be traced to a named person.`,
},
{
label: 'Events carry an actor',
value: `${named.length}/${f.activity.length}`,
ok: named.length === f.activity.length,
note: named.length === f.activity.length
? 'No anonymous entries in the log.'
: `${f.activity.length - named.length} entries have no account attached.`,
},
{
label: 'Hires reconcile with staff',
value: `${hires}/${f.staff.length}`,
ok: hires === f.staff.length,
note: hires === f.staff.length
? 'Every person on staff has a matching hire event.'
: `${Math.abs(f.staff.length - hires)} on staff without a corresponding hire event, or the reverse.`,
},
{
label: 'Recent coverage',
value: `${f.activity24h.length} in 24h`,
ok: f.activity24h.length > 0,
note: f.activity24h.length
? 'The log is being written to.'
: 'No events in the last day — on a live platform, check ingestion before concluding it was quiet.',
},
{
label: 'Interview integrity',
value: `${f.interviews.length - f.flagged.length}/${f.interviews.length}`,
ok: f.flagged.length === 0,
note: f.flagged.length
? `${plural(f.flagged.length, 'interview')} below full integrity — usually implausibly fast responses.`
: 'No interview flagged for integrity.',
},
];
const passing = checks.filter((c) => c.ok).length;
return doc(
heading(`Security posture: ${passing}/${checks.length} checks passing`),
status(checks),
heading('Privileged action log', `${plural(privileged.length, 'event')}`),
privileged.length
? timeline(
privileged.slice(0, 6).map((e) => ({
title: String(e.event_type).replace(/_/g, ' '),
description: `${e.user_name || e.user_email}${e.details ? ` — ${e.details}` : ''}`,
timestamp: new Date(e.created_date).toLocaleString(undefined, {
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
}),
tone: MEDIUM_SEVERITY.includes(e.event_type) ? 'warning' : 'risk',
}))
)
: text('No privileged actions in this log.'),
text(
passing === checks.length
? 'The trail would hold up to review: every privileged action is attributable and the records reconcile.'
: 'The failing checks are completeness problems. They do not mean something happened — they mean you could not prove it had not.'
)
);
};
/* ── Positions ──────────────────────────────────────────────────────────── */
/**
* Position strength — which roles are actually working.
*
* Ranked by qualified conversion rather than raw applicants: a role with twelve
* applications and nobody above the bar is in worse shape than one with two who
* both clear it, and sorting by volume hides exactly that.
*/
const positionStrength = (f) => {
if (!f.openPositions.length) return answer('There are no open positions to compare.');
const rows = f.byRole
.map((r) => ({ ...r, rate: r.applied ? Math.round((r.qualified / r.applied) * 100) : 0 }))
.sort((a, b) => b.rate - a.rate || b.applied - a.applied);
const best = rows.find((r) => r.applied > 0);
return doc(
heading('Pipeline strength by position', `${plural(rows.length, 'open role')}`),
table(
[
{ key: 'title', label: 'Role' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'qualified', label: '70+', align: 'right' },
{ key: 'rate', label: 'Qualified', align: 'right' },
],
rows.map((r) => ({
title: r.title,
applied: r.applied ? r.applied : { value: 0, tone: 'risk' },
qualified: r.qualified,
rate: { value: `${r.rate}%`, tone: r.rate >= 50 ? 'success' : r.rate > 0 ? 'warning' : 'risk' },
}))
),
insights([
best && {
tone: 'success',
title: `${best.title} has the strongest pipeline`,
body: `${best.qualified} of ${best.applied} applicants clear the bar — ${best.rate}%. If you are filling one role first, this is the one that can actually close.`,
},
f.starvedPositions.length && {
tone: 'risk',
title: `${plural(f.starvedPositions.length, 'role')} with no supply at all`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. No amount of screening improves a role nobody has applied to.`,
},
])
);
};
/** Which roles need a person, and why. */
const positionsNeedingAttention = (f) => {
const items = [
f.starvedPositions.length && {
tone: 'risk',
title: `${plural(f.starvedPositions.length, 'role')} with no applicants`,
body: `${f.starvedPositions.map((p) => p.title).join(', ')}. Usually a pay band or distribution problem rather than a hiring one.`,
},
f.singleCandidateRoles.length && {
tone: 'warning',
title: `${plural(f.singleCandidateRoles.length, 'role')} resting on one viable candidate`,
body: `${f.singleCandidateRoles.map((e) => e.posting.title).join(', ')}. One withdrawal reopens the search.`,
},
f.unscreened.length && {
tone: 'warning',
title: `${plural(f.unscreened.length, 'applicant')} waiting on screening`,
body: 'Spread across the open roles. Until they are scored the funnels below them are incomplete.',
},
f.stalled.length && {
tone: 'risk',
title: `${plural(f.stalled.length, 'strong candidate')} not actioned`,
body: `${f.stalled.map((a) => a.applicant_name).join(', ')} scored 80 or above and ${verb(f.stalled.length, 'is', 'are')} still waiting.`,
},
].filter(Boolean);
if (!items.length) {
return answer('Every open role has candidates, everyone is screened, and no role depends on a single person. Nothing needs intervention.');
}
return doc(
heading(`${plural(items.length, 'position issue')}`, 'Ordered by cost of leaving it'),
insights(items),
note('Ranked on supply and screening state. A role can look healthy here and still be hard to fill for reasons the platform cannot see.')
);
};
/**
* Positions that were saved but never published.
*
* A draft is a real record with real values on it and no way for anyone to
* apply to it, which makes it the one thing on this page that is genuinely
* unfinished. Each one is named, with what it already holds.
*
* It reports, it does not reopen. This used to attach "Continue to save" to
* every draft, routing to the authoring form — so a question asked in the panel
* could end with the reader back inside Create Position, and a position that
* had been saved minutes earlier was presented as still needing saving. The
* draft is on the Positions list marked Draft and its own card opens the form;
* the panel says where it is rather than taking anyone there.
*
* Read from `postings` by status — no list of drafts is kept anywhere, and a
* position stops appearing here the moment it is published.
*/
const positionDrafts = (f) => {
const drafts = (f.postings || []).filter((p) => p.status === 'draft');
if (!drafts.length) {
return answer('Nothing is sitting in draft — every position in the workspace has been published or closed.');
}
return doc(
heading(`${plural(drafts.length, 'position')} still in draft`, 'Saved, but not published'),
insights(drafts.map((p) => ({
tone: 'warning',
title: p.title || 'Untitled position',
body: [
p.company,
p.location,
'Not published yet, so nobody can apply and no screening runs.',
].filter(Boolean).join(' · '),
}))),
note('Each one is on the Positions list marked Draft. Opening its card reopens the form with everything already entered — the vetting weights included — and publishing it there is what makes it an active position.')
);
};
/** Which role to fill first. */
const hiringPriority = (f) => {
const rows = f.byRole
.map((r) => ({
...r,
ready: r.qualified,
rate: r.applied ? Math.round((r.qualified / r.applied) * 100) : 0,
}))
.sort((a, b) => b.ready - a.ready || b.rate - a.rate);
if (!rows.length) return answer('No open positions to prioritise.');
const first = rows[0];
return doc(
heading('What to fill first'),
text(
first.ready
? `**${first.title}** — ${plural(first.ready, 'candidate')} already clear the bar, so this is the role where a decision converts into a hire today rather than starting a search.`
: 'No role currently has a candidate above the bar, so the first move is screening rather than hiring.'
),
heading('Ranked by candidates ready now'),
table(
[
{ key: 'title', label: 'Role' },
{ key: 'ready', label: 'Ready', align: 'right' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'hired', label: 'Hired', align: 'right' },
],
rows.map((r) => ({
title: r.title,
ready: r.ready ? { value: r.ready, tone: 'success' } : '0',
applied: r.applied,
hired: r.hired || '—',
}))
)
);
};
/* ── Candidates list ────────────────────────────────────────────────────── */
/** Who is waiting on a decision, most consequential first. */
const candidatesNeedingAttention = (f) => {
const items = [
f.stalled.length && {
tone: 'risk',
title: `${plural(f.stalled.length, 'candidate')} at 80+ waiting on a decision`,
body: `${f.stalled.map((a) => a.applicant_name).join(', ')}. Screened, strong, and still sitting at AI Screened — the delay, not the assessment, is what loses these people.`,
},
f.unscreened.length && {
tone: 'warning',
title: `${plural(f.unscreened.length, 'candidate')} with no score`,
body: 'Cannot be ranked or compared until screening runs, so they are invisible to every sort on this page.',
},
f.missingCredentials.length && {
tone: 'warning',
title: `${plural(f.missingCredentials.length, 'scored candidate')} with no credentials`,
body: 'Compliance exposure if placed on a role that requires certification.',
},
f.narrowAvailability.length && {
tone: 'info',
title: `${plural(f.narrowAvailability.length, 'candidate')} with narrow availability`,
body: 'One slot or none recorded — the placements most likely to fall through on the day.',
},
].filter(Boolean);
if (!items.length) {
return answer('Nobody is waiting. Every candidate is scored, credentialed, and either actioned or already moving.');
}
return doc(
heading(`${plural(items.length, 'thing')} to look at`),
insights(items),
text(`Start with ${items[0].title.toLowerCase()} — it is the one where waiting costs the most.`)
);
};
/** Who is ready to interview right now. */
const interviewReady = (f) => {
const ready = f.ranked.filter(
(a) => a.ai_score >= 70 && ['ai_screened', 'shortlisted'].includes(a.status)
);
if (!ready.length) {
return answer('Nobody is interview-ready: no scored candidate above 70 is sitting at a stage where an interview is the next step.');
}
return doc(
heading('Ready to interview', `${plural(ready.length, 'candidate')} scoring 70 or above`),
table(
[
{ key: 'name', label: 'Candidate' },
{ key: 'role', label: 'Applied for' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'stage', label: 'Stage', align: 'right' },
],
ready.map((a) => ({
name: a.applicant_name,
role: a.job_title,
score: { value: a.ai_score, tone: a.ai_score >= 80 ? 'success' : 'info' },
stage: { badge: String(a.status).replace(/_/g, ' '), tone: 'neutral' },
}))
),
note('Screened and above the bar. Availability still needs checking against the event calendar.')
);
};
/** The pipeline as it stands, by stage and score band. */
const candidatePipelineSummary = (f) => {
if (!f.total) return answer('No candidates in the pipeline yet.');
const band = (min, max) =>
f.scored.filter((a) => a.ai_score >= min && (max == null || a.ai_score < max)).length;
return doc(
heading('Candidate pipeline', `${plural(f.total, 'candidate')}`),
kpis([
{ label: 'Scored', value: f.scored.length, sub: `${f.standardizedPct}% coverage` },
{ label: 'At 80+', value: band(80), tone: 'success' },
{ label: 'Interviewing', value: f.interviewing.length },
{ label: 'Hired', value: f.hired.length, tone: 'info' },
]),
heading('By score band'),
table(
[{ key: 'band', label: 'Band' }, { key: 'count', label: 'Candidates', align: 'right' }],
[
{ band: 'Strong (80+)', count: { value: band(80), tone: 'success' } },
{ band: 'Good (60–79)', count: band(60, 80) },
{ band: 'Review (40–59)', count: { value: band(40, 60), tone: 'warning' } },
{ band: 'Low (under 40)', count: band(1, 40) },
{ band: 'Not scored', count: { value: f.unscreened.length, tone: 'risk' } },
]
),
heading('By stage'),
funnel(f.funnel.map((stage, i) => ({
label: stage.label,
count: stage.count,
rate: i === 0 ? null : f.transitions[i - 1]?.rate,
lost: i === 0 ? 0 : f.transitions[i - 1]?.lost,
})))
);
};
/* ── Analytics ──────────────────────────────────────────────────────────── */
const DAY_MS = 1000 * 60 * 60 * 24;
/**
* Hiring trend — direction of travel, not another chart.
*
* The page already draws the line; what it cannot say is whether the line is
* going up. So this compares the last four weeks against the four before them
* and reports the change, using the same created/updated dates the chart buckets.
*/
const hiringTrend = (f) => {
if (!f.total) return answer('No applications yet, so there is no trend to read.');
const inWindow = (value, from, to) => {
const t = new Date(value).getTime();
return t >= f.today.getTime() - from * DAY_MS && t < f.today.getTime() - to * DAY_MS;
};
const applied = (from, to) => f.applications.filter((a) => inWindow(a.created_date, from, to)).length;
/* Screening is dated by the update that carried the score, exactly as the
Hiring trend chart buckets it. */
const screened = (from, to) => f.scored.filter((a) => inWindow(a.updated_date, from, to)).length;
const recent = applied(28, 0);
const previous = applied(56, 28);
const change = previous ? Math.round(((recent - previous) / previous) * 100) : null;
const weeks = [3, 2, 1, 0].map((w) => ({
week: w === 0 ? 'This week' : `${w} week${w === 1 ? '' : 's'} ago`,
applied: applied((w + 1) * 7, w * 7),
screened: screened((w + 1) * 7, w * 7),
}));
return doc(
heading('Hiring trend', 'Last 4 weeks against the 4 before'),
kpis([
{ label: 'Applications', value: recent, sub: `${previous} in the prior period` },
{
label: 'Change',
value: change === null ? '—' : `${change > 0 ? '+' : ''}${change}%`,
tone: change === null ? undefined : change >= 0 ? 'success' : 'warning',
},
{ label: 'Screened', value: screened(28, 0), sub: `${f.standardizedPct}% coverage` },
{ label: 'Hires', value: f.hired.length, sub: f.timeToHire ? `${f.timeToHire}d to close` : 'none closed' },
]),
heading('Week by week'),
table(
[
{ key: 'week', label: 'Week' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'screened', label: 'Screened', align: 'right' },
],
weeks
),
insights([
change !== null && {
tone: change >= 0 ? 'success' : 'warning',
title: change >= 0
? `Applications up ${change}% on the previous four weeks`
: `Applications down ${Math.abs(change)}% on the previous four weeks`,
body: change >= 0
? 'Supply is growing, so the constraint moves to screening throughput rather than reach.'
: 'Fewer people are arriving. Distribution and pay band are the two things that move this, not screening.',
},
f.unscreened.length && {
tone: 'warning',
title: `${plural(f.unscreened.length, 'application')} still unscored`,
body: 'The screened line lags the applied line by exactly this much — the gap is a queue, not a rejection rate.',
},
]),
note('Windows are counted from today, so this is independent of the 7d/30d/90d control on the chart.')
);
};
/**
* Department performance — which role category is actually converting.
*
* The page's department section ranks by volume. Volume is demand, not
* performance: the category worth copying is the one turning applicants into
* qualified candidates, which is a rate.
*/
const departmentPerformance = (f) => {
if (!f.byRole.length) return answer('No open positions, so there are no departments to compare.');
const rows = Object.values(f.byRole.reduce((acc, r) => {
acc[r.category] ||= { category: r.category, roles: 0, applied: 0, qualified: 0, hired: 0 };
acc[r.category].roles += 1;
acc[r.category].applied += r.applied;
acc[r.category].qualified += r.qualified;
acc[r.category].hired += r.hired;
return acc;
}, {})).map((d) => ({ ...d, rate: d.applied ? Math.round((d.qualified / d.applied) * 100) : 0 }))
.sort((a, b) => b.rate - a.rate || b.applied - a.applied);
const best = rows.find((d) => d.applied > 0);
const starved = rows.filter((d) => d.applied === 0);
return doc(
heading('Department performance', `${plural(rows.length, 'category')}`),
table(
[
{ key: 'category', label: 'Category' },
{ key: 'applied', label: 'Applied', align: 'right' },
{ key: 'qualified', label: '70+', align: 'right' },
{ key: 'hired', label: 'Hired', align: 'right' },
{ key: 'rate', label: 'Qualified', align: 'right' },
],
rows.map((d) => ({
category: d.category,
applied: d.applied ? d.applied : { value: 0, tone: 'risk' },
qualified: d.qualified,
hired: d.hired || '—',
rate: { value: `${d.rate}%`, tone: d.rate >= 50 ? 'success' : d.rate > 0 ? 'warning' : 'risk' },
}))
),
insights([
best && {
tone: 'success',
title: `${best.category} is performing best`,
body: `${best.qualified} of ${best.applied} applicants clear 70 — ${best.rate}%${best.hired ? `, and ${plural(best.hired, 'hire')} closed` : ''}. Whatever sourcing this category uses is the one worth copying.`,
},
starved.length && {
tone: 'risk',
title: `${plural(starved.length, 'category')} attracting nobody`,
body: `${starved.map((d) => d.category).join(', ')}. A conversion rate cannot be read off zero applicants.`,
},
])
);
};
/* ── Free-text responders ───────────────────────────────────────────────── */
/**
* Free-text routing.
*
* Keyword matching, ordered most specific first, because a question mentioning
* both "risk" and "candidates" is a risk question. The ordering *is* the logic
* here, so each responder reads top to bottom as a priority list.
*/
const has = (q, ...words) => words.some((w) => q.includes(w));
export const respondControlCenter = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'attention', 'urgent', 'blocked', 'stuck', 'wrong')) return attentionRequired(f);
if (has(q, 'bottleneck', 'funnel', 'pipeline', 'conversion', 'drop')) return pipelineHealth(f);
if (has(q, 'recommend', 'should', 'fix', 'improve', 'risk', 'next')) return recommendations(f);
if (has(q, 'usage', 'summary', 'summarize', 'workforce', 'scale', 'how many')) return workforceSummary(f);
if (has(q, 'operation', 'velocity', 'speed', 'throughput', 'time to hire', 'hiring')) return hiringOperations(f);
if (has(q, 'unusual', 'anomal', 'suspicious')) return unusualActivity(f);
return platformHealth(f);
};
export const respondPositions = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'draft', 'unfinished', 'not published', 'continue')) return positionDrafts(f);
if (has(q, 'attention', 'urgent', 'problem', 'wrong', 'risk')) return positionsNeedingAttention(f);
if (has(q, 'first', 'priorit', 'which position', 'should i hire')) return hiringPriority(f);
if (has(q, 'bottleneck', 'funnel', 'drop', 'conversion')) return pipelineHealth(f);
if (has(q, 'waiting', 'review', 'screen')) return candidatesNeedingAttention(f);
if (has(q, 'activity', 'summar', 'operation', 'velocity')) return hiringOperations(f);
return positionStrength(f);
};
export const respondCandidateList = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'attention', 'waiting', 'need', 'stuck')) return candidatesNeedingAttention(f);
if (has(q, 'interview', 'ready')) return interviewReady(f);
if (has(q, 'no score', 'unscored', 'not scored', 'missing')) return screeningGaps(f);
if (has(q, 'risk', 'flag', 'concern', 'compliance')) return candidateRiskAnalysis(f);
if (has(q, 'strongest', 'best', 'top', 'compare')) return topCandidates(f);
if (has(q, 'summar', 'pipeline', 'overview')) return candidatePipelineSummary(f);
return topCandidates(f);
};
export const respondAdminCandidates = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'gap', 'missing', 'incomplete', 'coverage', 'unscored')) return screeningGaps(f);
if (has(q, 'risk', 'flag', 'concern', 'compliance')) return candidateRiskAnalysis(f);
if (has(q, 'compare', 'top', 'best', 'strongest', 'shortlist')) return topCandidates(f);
if (has(q, 'recommend', 'should', 'next', 'action', 'hire', 'who')) return hiringRecommendations(f);
return recruitmentInsights(f);
};
export const respondAdminAnalytics = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'trend', 'over time', 'growing', 'slow', 'week', 'month')) return hiringTrend(f);
if (has(q, 'department', 'category', 'team', 'best perform')) return departmentPerformance(f);
if (has(q, 'bottleneck', 'funnel', 'drop', 'conversion', 'stuck', 'lose')) return pipelineHealth(f);
if (has(q, 'position', 'role', 'converting', 'strongest')) return positionStrength(f);
if (has(q, 'attention', 'urgent', 'wrong', 'fix', 'risk')) return attentionRequired(f);
return hiringOperations(f);
};
export const respondActivity = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'security', 'breach', 'attributab', 'compliance', 'integrity', 'trace')) return securityInsights(f);
if (has(q, 'unusual', 'anomal', 'suspicious', 'odd', 'spike', 'burst')) return unusualActivity(f);
if (has(q, 'user', 'who', 'account', 'behaviour', 'behavior', 'busiest')) return userActivityInsights(f);
return auditSummary(f);
};
/* ── Capability manifests ───────────────────────────────────────────────── */
export const CONTROL_CENTER_CAPABILITIES = [
{ id: 'platform-health', label: 'Platform Health', icon: Gauge, run: platformHealth },
{ id: 'workforce-summary', label: 'Workforce Summary', icon: Users, run: workforceSummary },
{ id: 'hiring-operations', label: 'Hiring Operations', icon: Activity, run: hiringOperations },
{ id: 'pipeline-health', label: 'Pipeline Health', icon: GitBranch, run: pipelineHealth },
{ id: 'attention-required', label: 'Attention Required', icon: BellRing, run: attentionRequired },
{ id: 'recommendations', label: 'Recommendations', icon: Lightbulb, run: recommendations },
];
export const POSITIONS_CAPABILITIES = [
{ id: 'position-drafts', label: 'Unfinished Drafts', icon: FileText, run: positionDrafts },
{ id: 'position-strength', label: 'Pipeline Strength', icon: TrendingUp, run: positionStrength },
{ id: 'positions-attention', label: 'Needs Attention', icon: BellRing, run: positionsNeedingAttention },
{ id: 'hiring-priority', label: 'What To Fill First', icon: Target, run: hiringPriority },
{ id: 'pipeline-health', label: 'Hiring Bottlenecks', icon: GitBranch, run: pipelineHealth },
{ id: 'hiring-operations', label: 'Hiring Activity', icon: Activity, run: hiringOperations },
{ id: 'candidates-waiting', label: 'Candidates Waiting', icon: Users, run: candidatesNeedingAttention },
];
export const CANDIDATE_LIST_CAPABILITIES = [
{ id: 'candidates-attention', label: 'Who Needs Attention', icon: BellRing, run: candidatesNeedingAttention },
{ id: 'top-candidates', label: 'Strongest Candidates', icon: ArrowLeftRight, run: topCandidates },
{ id: 'interview-ready', label: 'Interview Ready', icon: UserCheck, run: interviewReady },
{ id: 'screening-gaps', label: 'Unscored Candidates', icon: Filter, run: screeningGaps },
{ id: 'pipeline-summary', label: 'Pipeline Summary', icon: GitBranch, run: candidatePipelineSummary },
{ id: 'candidate-risk', label: 'Candidate Risks', icon: AlertTriangle, run: candidateRiskAnalysis },
];
export const ADMIN_CANDIDATE_CAPABILITIES = [
{ id: 'candidate-risk', label: 'Candidate Risk', icon: AlertTriangle, run: candidateRiskAnalysis },
{ id: 'recruitment-insights', label: 'Recruitment Insights', icon: TrendingUp, run: recruitmentInsights },
{ id: 'screening-gaps', label: 'Screening Gaps', icon: Filter, run: screeningGaps },
{ id: 'hiring-recommendations', label: 'Hiring Recommendations', icon: Lightbulb, run: hiringRecommendations },
{ id: 'top-candidates', label: 'Compare Top Candidates', icon: ArrowLeftRight, run: topCandidates },
];
export const ADMIN_ANALYTICS_CAPABILITIES = [
{ id: 'hiring-trend', label: 'Hiring Trend', icon: TrendingUp, run: hiringTrend },
{ id: 'department-performance', label: 'Department Performance', icon: Layers, run: departmentPerformance },
{ id: 'pipeline-health', label: 'Hiring Bottlenecks', icon: GitBranch, run: pipelineHealth },
{ id: 'position-conversion', label: 'Position Conversion', icon: Target, run: positionStrength },
{ id: 'hiring-operations', label: 'Hiring Performance', icon: Activity, run: hiringOperations },
{ id: 'attention-required', label: 'Needs Attention', icon: BellRing, run: attentionRequired },
];
export const ACTIVITY_CAPABILITIES = [
{ id: 'audit-summary', label: 'Audit Summary', icon: ScrollText, run: auditSummary },
{ id: 'user-activity', label: 'User Activity', icon: Eye, run: userActivityInsights },
{ id: 'unusual-activity', label: 'Unusual Activity', icon: AlertTriangle, run: unusualActivity },
{ id: 'security-insights', label: 'Security Insights', icon: ShieldAlert, run: securityInsights },
];
/* ── KROW Forge ─────────────────────────────────────────────────────────── */
/**
* Forge capabilities — managing workforce skill verification.
*
* The Forge questions an administrator asks are not a worker's. A worker asks
* what to prove next; the person who owns the library asks what is in it, what
* is live, what Owliver is checking, and whether any of it is reaching the
* workforce. Every figure comes from `f.forge`, derived from the same Course
* records and worker profiles the page renders from.
*/
const forgeLibrary = (f) => {
const g = f.forge;
if (!g.library.length) {
return answer('The library is empty. A skill defines what the workforce learns, how they prove it, and what Owliver checks — start one from Add Skill Training.');
}
return doc(
heading('Skill library', `${plural(g.library.length, 'skill')} defined`),
kpis([
{ label: 'Active skills', value: g.published.length, sub: `of ${g.library.length} in the library` },
{ label: 'Training modules', value: g.withTraining.length, sub: 'carrying material' },
{ label: 'Challenges', value: g.withChallenge.length, sub: 'evidence-based' },
{ label: 'Verified skills', value: g.workforceVerified, sub: g.workforceVerified ? 'earned by the workforce' : 'none earned yet' },
]),
g.byCategory.length ? heading('By category') : null,
meters(g.byCategory.map((c) => ({
label: c.category,
value: c.count,
max: g.library.length,
}))),
g.drafts.length
? note(`${plural(g.drafts.length, 'skill')} ${verb(g.drafts.length, 'is', 'are')} still in draft and not visible to the workforce.`)
: null
);
};
const forgePublished = (f) => {
const g = f.forge;
if (!g.published.length) {
return answer(`Nothing is published. ${g.drafts.length ? `${plural(g.drafts.length, 'draft')} ${verb(g.drafts.length, 'is', 'are')} waiting on a decision.` : 'The library has no skills yet.'}`);
}
return doc(
heading(`${plural(g.published.length, 'skill')} published`, 'live to the workforce'),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'category', label: 'Category' },
{ key: 'proof', label: 'Proof' },
{ key: 'minutes', label: 'Min', align: 'right' },
],
g.published.map((c) => ({
skill: c.title,
category: c.category || '—',
proof: TYPE_LABEL[c.challenge?.type] || 'Not set',
minutes: c.estimated_minutes || '—',
}))
),
g.drafts.length
? insights([{
tone: 'warning',
title: `${plural(g.drafts.length, 'skill')} in draft`,
body: `${g.drafts.map((c) => c.title).join(', ')}. A draft is invisible to the workforce until it is published.`,
}])
: note('Every skill in the library is published.')
);
};
const forgeEvaluation = (f) => {
const g = f.forge;
const scored = g.library.filter((c) => c.challenge?.rubric?.length);
if (!scored.length) {
return answer('No skill has evaluation criteria yet. Owliver scores a submission against the criteria set on the skill, so a skill without them cannot verify anything.');
}
return doc(
heading('What Owliver evaluates', `across ${plural(scored.length, 'skill')}`),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'criteria', label: 'Criteria' },
{ key: 'pass', label: 'Passes at', align: 'right' },
],
scored.map((c) => ({
skill: c.title,
criteria: c.challenge.rubric.map((r) => r.criterion).join(', '),
pass: c.pass_score || '—',
}))
),
g.library.length > scored.length
? insights([{
tone: 'warning',
title: 'Skills with nothing to score against',
body: `${g.library.filter((c) => !c.challenge?.rubric?.length).map((c) => c.title).join(', ')}. Owliver needs criteria to return a verdict.`,
}])
: note('Every skill in the library tells Owliver what to check.')
);
};
const forgeWorkforce = (f) => {
const g = f.forge;
const touched = g.usage.filter((u) => u.completed > 0 || u.verified > 0);
if (!touched.length) {
return doc(
text('No skill in the library has been completed by anyone yet.'),
note('Completion and verification are read from worker profiles, so figures appear here as the workforce works through the library.')
);
}
return doc(
heading('Workforce usage', `${plural(touched.length, 'skill')} in use`),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'completed', label: 'Completed', align: 'right' },
{ key: 'verified', label: 'Verified', align: 'right' },
],
touched
.sort((a, b) => b.verified - a.verified || b.completed - a.completed)
.map((u) => ({
skill: u.course.title,
completed: u.completed,
verified: u.verified,
}))
),
g.library.length > touched.length
? note(`${plural(g.library.length - touched.length, 'skill')} ${verb(g.library.length - touched.length, 'has', 'have')} no completions on record.`)
: null
);
};
const forgeGaps = (f) => {
const g = f.forge;
const gaps = [
...g.drafts.map((c) => ({ skill: c.title, gap: 'Unpublished draft' })),
...g.library.filter((c) => !c.challenge?.type).map((c) => ({ skill: c.title, gap: 'No proof method' })),
...g.library.filter((c) => !c.challenge?.rubric?.length).map((c) => ({ skill: c.title, gap: 'No Owliver criteria' })),
...g.library.filter((c) => !c.training_outline?.length && !c.quiz?.length).map((c) => ({ skill: c.title, gap: 'No training material' })),
];
if (!gaps.length) {
return answer(`All ${plural(g.library.length, 'skill')} carry training, a proof method and evaluation criteria, and every one is published.`);
}
return doc(
heading(`${plural(gaps.length, 'gap')} to close`, 'before these verify anything'),
table(
[
{ key: 'skill', label: 'Skill' },
{ key: 'gap', label: 'Missing' },
],
gaps
),
note('A skill needs training, a way to submit evidence, and criteria for Owliver to score against before it can verify a workforce skill.')
);
};
/* ── Talent pool ────────────────────────────────────────────────────────── */
const talentPoolSummary = (f) => {
const t = f.talent;
return doc(
heading('Talent pool', `${plural(f.profiles.length, 'registered worker')}`),
kpis([
{ label: 'Profiles', value: f.profiles.length, sub: `${t.available.length} available` },
{ label: 'Elite', value: f.elite.length, sub: '90+ career score' },
{ label: 'Average score', value: t.avgScore ? toFICO(t.avgScore) : '—', sub: 'scored profiles only' },
{ label: 'Unscored', value: f.unscoredProfiles.length, sub: 'cannot be ranked', tone: f.unscoredProfiles.length ? 'warning' : undefined },
]),
heading('Score bands'),
meters(t.bands.map((b) => ({ label: b.label, value: b.count, max: f.profiles.length }))),
text(
f.unscoredProfiles.length
? `${plural(f.unscoredProfiles.length, 'profile')} ${verb(f.unscoredProfiles.length, 'has', 'have')} no career score, so ${verb(f.unscoredProfiles.length, 'it is', 'they are')} invisible to ranking and matching.`
: 'Every profile carries a career score, so the whole pool is rankable.'
)
);
};
const talentPriorities = (f) => {
const t = f.talent;
if (!f.profiles.length) return answer('No talent profiles registered yet.');
return doc(
heading('Who to prioritize', 'by career score, availability and verification'),
table(
[
{ key: 'name', label: 'Worker' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'experience', label: 'Years', align: 'right' },
{ key: 'availability', label: 'Available' },
],
t.ranked.slice(0, 6).map((p) => ({
name: p.full_name || p.email,
/* The page shows career score on the 300–850 scale, so this must too —
the same worker cannot be 94 here and 817 in the row beside it. */
score: p.krow_score ? toFICO(p.krow_score) : '—',
experience: p.experience_years ?? '—',
availability: (p.availability || []).length ? (p.availability || []).join(', ') : '—',
}))
),
insights([
t.top[0] && {
tone: 'success',
title: `${t.top[0].full_name || t.top[0].email} leads the pool`,
body: `Career score ${toFICO(t.top[0].krow_score || 0)}${t.top[0].experience_years ? `, ${plural(t.top[0].experience_years, 'year')} of experience` : ''}.`,
},
t.available.length < f.profiles.length && {
tone: 'info',
title: `${t.available.length} of ${f.profiles.length} list availability`,
body: 'The rest cannot be matched to a shift until availability is on the record.',
},
])
);
};
const talentVerification = (f) => {
const t = f.talent;
if (!t.unverified.length) {
return answer(`All ${plural(f.profiles.length, 'profile')} carry at least one verified skill.`);
}
return doc(
heading(`${plural(t.unverified.length, 'profile')} without verified skills`),
table(
[
{ key: 'name', label: 'Worker' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'skills', label: 'Claimed skills', align: 'right' },
],
t.unverified.slice(0, 8).map((p) => ({
name: p.full_name || p.email,
score: p.krow_score ? toFICO(p.krow_score) : '—',
skills: (p.skills || []).length,
}))
),
insights([{
tone: 'warning',
title: 'Claimed is not proven',
body: 'These profiles carry skills nobody has verified. A Forge challenge turns each claim into evidence an employer can read.',
}]),
note(`${pct(t.unverified.length, f.profiles.length)}% of the pool is unverified.`)
);
};
const talentAvailability = (f) => {
const t = f.talent;
const buckets = Object.values(f.profiles.reduce((acc, p) => {
(p.availability || ['Not stated']).forEach((slot) => {
acc[slot] ||= { slot, count: 0 };
acc[slot].count += 1;
});
return acc;
}, {})).sort((a, b) => b.count - a.count);
return doc(
heading('Availability across the pool', `${t.available.length} of ${f.profiles.length} stated`),
meters(buckets.map((b) => ({ label: b.slot, value: b.count, max: f.profiles.length }))),
text(
buckets[0]
? `Deepest cover is **${buckets[0].slot}** at ${plural(buckets[0].count, 'worker')}. Thinnest is **${buckets[buckets.length - 1].slot}**.`
: 'No availability recorded yet.'
)
);
};
/* ── Hired history ──────────────────────────────────────────────────────── */
const hiringOutcomes = (f) => {
const h = f.hiring;
if (!h.hires.length) return answer('Nobody has been hired yet, so there is no history to read.');
return doc(
heading('Hiring outcomes', plural(h.hires.length, 'hire')),
kpis([
{ label: 'Hires', value: h.hires.length, sub: `${h.onboarding.length} onboarding` },
{ label: 'Average quality', value: h.avgQuality || '—', sub: 'AI score at hire' },
{ label: 'Average speed', value: h.avgSpeed ? `${h.avgSpeed}d` : '—', sub: 'apply to hire' },
{ label: 'Rated', value: `${h.rated.length}/${h.hires.length}`, sub: 'client rating on file', tone: h.unrated.length ? 'warning' : undefined },
]),
heading('By department'),
table(
[
{ key: 'name', label: 'Department' },
{ key: 'count', label: 'Hires', align: 'right' },
{ key: 'avgScore', label: 'Avg score', align: 'right' },
{ key: 'avgDays', label: 'Avg days', align: 'right' },
],
h.byDepartment.map((d) => ({
name: d.name,
count: d.count,
avgScore: d.avgScore || '—',
avgDays: d.avgDays || '—',
}))
),
h.unrated.length
? note(`${plural(h.unrated.length, 'hire')} ${verb(h.unrated.length, 'has', 'have')} no client rating yet, so outcome quality rests on the ${h.rated.length} that do.`)
: null
);
};
const hiringStrongest = (f) => {
const h = f.hiring;
if (!h.hires.length) return answer('No hires on record yet.');
const ranked = [...h.hires].sort((a, b) => (b.score || 0) - (a.score || 0));
return doc(
heading('Strongest hires', 'by score at the point of hire'),
table(
[
{ key: 'name', label: 'Hire' },
{ key: 'role', label: 'Role' },
{ key: 'department', label: 'Department' },
{ key: 'score', label: 'Score', align: 'right' },
{ key: 'rating', label: 'Rating', align: 'right' },
],
ranked.slice(0, 6).map((x) => ({
name: x.name,
role: x.role,
department: x.department,
score: x.score || '—',
rating: x.client_rating || '—',
}))
),
insights([
h.strongest && {
tone: 'success',
title: `${h.strongest.name} scored highest at ${h.strongest.score}`,
body: `${h.strongest.role} in ${h.strongest.department}${h.strongest.client_rating ? `, rated ${h.strongest.client_rating} since` : ', not yet rated by the client'}.`,
},
h.byRoleTitle[0] && {
tone: 'info',
title: `${h.byRoleTitle[0].name} is the most-hired role`,
body: `${plural(h.byRoleTitle[0].count, 'hire')}, averaging ${h.byRoleTitle[0].avgScore || 0} at hire.`,
},
])
);
};
const hiringPatterns = (f) => {
const h = f.hiring;
if (!h.hires.length) return answer('No hires on record, so there is no pattern to read.');
const fastest = [...h.byRoleTitle].filter((r) => r.avgDays).sort((a, b) => a.avgDays - b.avgDays)[0];
const slowest = [...h.byRoleTitle].filter((r) => r.avgDays).sort((a, b) => b.avgDays - a.avgDays)[0];
return doc(
heading('What stands out', `${plural(h.hires.length, 'hire')} across ${plural(h.byDepartment.length, 'department')}`),
meters(h.byDepartment.map((d) => ({
label: d.name,
value: d.count,
max: Math.max(...h.byDepartment.map((x) => x.count), 1),
}))),
insights([
h.byDepartment[0] && {
tone: 'info',
title: `${h.byDepartment[0].name} accounts for the most hires`,
body: `${plural(h.byDepartment[0].count, 'hire')} of ${h.hires.length} — ${pct(h.byDepartment[0].count, h.hires.length)}% of everyone placed.`,
},
fastest && slowest && fastest.name !== slowest.name && {
tone: 'warning',
title: `${slowest.name} takes ${slowest.avgDays - fastest.avgDays} days longer than ${fastest.name}`,
body: `${slowest.avgDays} days against ${fastest.avgDays}. The gap is where the process, not the candidate, is the constraint.`,
},
h.unrated.length && {
tone: 'warning',
title: `${plural(h.unrated.length, 'hire')} without a client rating`,
body: 'Outcome quality cannot be judged on hires nobody has rated.',
},
]),
actions([
h.unrated.length ? { title: 'Chase the missing ratings', body: `${plural(h.unrated.length, 'placement')} ${verb(h.unrated.length, 'has', 'have')} no client feedback on file.` } : null,
h.avgSpeed ? { title: 'Compare speed against quality', body: `Average ${h.avgSpeed} days to hire at an average score of ${h.avgQuality}. Faster is only better while the score holds.` } : null,
])
);
};
const hiringRecentHires = (f) => {
const h = f.hiring;
if (!h.hires.length) return answer('No hires on record yet.');
const recent = [...h.hires]
.filter((x) => x.hire_date)
.sort((a, b) => new Date(b.hire_date).getTime() - new Date(a.hire_date).getTime())
.slice(0, 6);
return doc(
heading('Recent hires', 'most recent first'),
timeline(recent.map((x) => ({
title: `${x.name} — ${x.role}`,
description: `${x.department}${x.score ? ` · scored ${x.score}` : ''}${x.timeToHire ? ` · ${plural(x.timeToHire, 'day')} to hire` : ''}`,
timestamp: x.hire_date,
tone: x.status === 'onboarding' ? 'warning' : 'success',
}))),
h.onboarding.length
? note(`${plural(h.onboarding.length, 'hire')} ${verb(h.onboarding.length, 'is', 'are')} still onboarding.`)
: null
);
};
/* ── Free-text responders for the three new contexts ────────────────────── */
export const respondForge = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'publish', 'live', 'draft', 'unpublish', 'archived', 'status')) return forgePublished(f);
if (has(q, 'owliver', 'evaluat', 'criteri', 'rubric', 'grade', 'score against', 'pass')) return forgeEvaluation(f);
if (has(q, 'gap', 'missing', 'incomplete', 'unfinished', 'needs', 'ready to publish')) return forgeGaps(f);
if (has(q, 'workforce', 'usage', 'completed', 'verified', 'earned', 'badge', 'who has', 'uptake')) return forgeWorkforce(f);
if (has(q, 'skill', 'library', 'challenge', 'course', 'training', 'category', 'summar', 'overview', 'how many', 'what do we have')) return forgeLibrary(f);
return forgeLibrary(f);
};
export const respondTalentPool = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'verif', 'unverified', 'proof', 'evidence', 'badge')) return talentVerification(f);
if (has(q, 'available', 'availability', 'shift', 'weekend', 'evening')) return talentAvailability(f);
if (has(q, 'priorit', 'strongest', 'best', 'top', 'who should')) return talentPriorities(f);
if (has(q, 'unscored', 'no score', 'missing', 'gap')) return talentPoolSummary(f);
if (has(q, 'summar', 'overview', 'pool', 'how many')) return talentPoolSummary(f);
return talentPriorities(f);
};
export const respondHiredHistory = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'pattern', 'stand out', 'trend', 'insight', 'attention', 'concern')) return hiringPatterns(f);
if (has(q, 'strongest', 'best', 'top', 'quality', 'highest')) return hiringStrongest(f);
if (has(q, 'recent', 'latest', 'summar', 'who did we hire')) return hiringRecentHires(f);
if (has(q, 'department', 'position', 'role', 'team', 'most hires')) return hiringOutcomes(f);
return hiringOutcomes(f);
};
/* ── Capability manifests for the three new contexts ────────────────────── */
export const FORGE_CAPABILITIES = [
{ id: 'forge-library', label: 'Skill Library', icon: Gauge, run: forgeLibrary },
{ id: 'forge-published', label: 'Published Skills', icon: Target, run: forgePublished },
{ id: 'forge-evaluation', label: 'Owliver Evaluation', icon: ShieldCheck, run: forgeEvaluation },
{ id: 'forge-workforce', label: 'Workforce Usage', icon: UserCheck, run: forgeWorkforce },
{ id: 'forge-gaps', label: 'Gaps To Close', icon: Lock, run: forgeGaps },
];
export const TALENT_POOL_CAPABILITIES = [
{ id: 'talent-priorities', label: 'Who To Prioritize', icon: Star, run: talentPriorities },
{ id: 'talent-summary', label: 'Pool Summary', icon: Users, run: talentPoolSummary },
{ id: 'talent-verification', label: 'Verification Gaps', icon: ShieldCheck, run: talentVerification },
{ id: 'talent-availability', label: 'Availability', icon: Clock, run: talentAvailability },
];
export const HIRED_HISTORY_CAPABILITIES = [
{ id: 'hiring-outcomes', label: 'Hiring Outcomes', icon: Award, run: hiringOutcomes },
{ id: 'hiring-strongest', label: 'Strongest Hires', icon: TrendingUp, run: hiringStrongest },
{ id: 'hiring-patterns', label: 'What Stands Out', icon: Layers, run: hiringPatterns },
{ id: 'hiring-recent', label: 'Recent Hires', icon: ScrollText, run: hiringRecentHires },
];
/* ── Profile ────────────────────────────────────────────────────────────────
*
* The account page answers questions about the operator rather than the
* workforce: what this account may do, how it is secured, what it has been
* doing, and which switches are on. Everything below reads the same constants
* the page renders, so the panel can never describe a scope or a control that
* is not on screen.
*/
const profilePermissions = () => doc(
heading('Your permissions', `${PERMISSIONS.length} scopes on this account`),
table(
[{ key: 'scope', label: 'Scope' }, { key: 'level', label: 'Level' }, { key: 'detail', label: 'Covers' }],
PERMISSIONS.map((p) => ({
scope: p.scope,
level: {
value: p.level,
tone: p.level === 'Full access' ? 'success' : p.level === 'Read only' ? 'info' : 'muted',
},
detail: p.detail,
}))
),
note('Scopes are set by the account owner. Billing stays restricted to owners.')
);
const profileIdentity = (f) => {
const user = f.user || {};
return doc(
heading('This account'),
table(
[{ key: 'field', label: 'Field' }, { key: 'value', label: 'Value' }],
[
{ field: 'Name', value: user.full_name || '—' },
{ field: 'Email', value: user.email || '—' },
{ field: 'Role', value: 'Platform Administrator' },
{ field: 'Department', value: 'Workforce Operations' },
{ field: 'Account status', value: { value: 'Active · verified', tone: 'success' } },
{
field: 'Member since',
value: user.created_date
? new Date(user.created_date).toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
: '—',
},
]
),
text('Use **Edit profile** at the top of the page to change your display name. Email and role are set by the account owner.')
);
};
const profileSecurity = () => doc(
heading('Security on this account'),
status([
{ label: 'Two-factor authentication', ok: true, value: 'On', note: 'Required for privileged actions.' },
{ label: 'Password age', ok: true, value: '42 days', note: 'Administrator passwords expire every 90 days.' },
{ label: 'Session', ok: true, value: 'Browser tab', note: 'Closing the tab signs this account out.' },
]),
text('**Change password** and **Manage sessions** sit in the Security card on the right.')
);
const profilePreferences = (f) => {
const p = f.user?.preferences || {};
const state = (on) => ({ value: on ? 'On' : 'Off', tone: on ? 'success' : 'muted' });
return doc(
heading('Your preferences'),
table(
[{ key: 'setting', label: 'Setting' }, { key: 'state', label: 'State' }, { key: 'detail', label: 'Effect' }],
[
{ setting: 'Owliver workspace', state: state(p.owliverDefault !== false), detail: 'Opens this panel by default on supported pages' },
{ setting: 'Compact density', state: state(Boolean(p.compactDensity)), detail: 'Tighter row heights across Admin tables' },
{ setting: 'Email digest', state: state(p.emailDigest !== false), detail: 'A daily summary of what needs attention' },
]
),
note('Preferences save to your account as soon as you flip a switch.')
);
};
const profileActivity = (f) => {
const email = f.user?.email;
const mine = f.activity.filter((e) => e.user_email === email).slice(0, 6);
if (!mine.length) {
return doc(text('No activity is recorded against this account yet.'));
}
return doc(
heading('Your recent activity', `${plural(mine.length, 'event')} on this account`),
timeline(mine.map((e) => ({
title: e.event_type.replace(/_/g, ' '),
description: e.details || undefined,
timestamp: new Date(e.created_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }),
})))
);
};
const profileActions = () => doc(
heading('What you can do here'),
list(PROFILE_ACTIONS.map((a) => `**${a.label}** — ${a.detail}`)),
note('Everything above is on this page. Nothing here changes another operator’s account.')
);
export const respondProfile = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'permission', 'scope', 'access', 'allowed', 'can i')) return profilePermissions(f);
if (has(q, 'password', 'two-factor', 'two factor', '2fa', 'security', 'session', 'sign out', 'log out')) return profileSecurity(f);
if (has(q, 'preference', 'setting', 'digest', 'density', 'workspace', 'owliver')) return profilePreferences(f);
if (has(q, 'activity', 'recent', 'history', 'what have i')) return profileActivity(f);
if (has(q, 'do here', 'change my', 'edit', 'update', 'how do i')) return profileActions(f);
return profileIdentity(f);
};
export const PROFILE_CAPABILITIES = [
{ id: 'profile-permissions', label: 'My Permissions', icon: ShieldCheck, run: profilePermissions },
{ id: 'profile-identity', label: 'Account Details', icon: Users, run: profileIdentity },
{ id: 'profile-security', label: 'Security', icon: Lock, run: profileSecurity },
{ id: 'profile-preferences', label: 'My Preferences', icon: Gauge, run: profilePreferences },
{ id: 'profile-activity', label: 'My Recent Activity', icon: ScrollText, run: profileActivity },
{ id: 'profile-actions', label: 'What I Can Do Here', icon: Lightbulb, run: profileActions },
];
/* ── Create Position ─────────────────────────────────────────────────────
The authoring surface. Every answer here is about the role being specified,
read against what this workspace has already posted — so the guidance is the
deployment's own history rather than an industry number written into a file. */
/** The average of a numeric field across postings that state it. */
const across = (postings, read) => {
const values = postings.map(read).filter((n) => Number.isFinite(n) && n > 0);
return values.length ? Math.round(values.reduce((a, b) => a + b, 0) / values.length) : 0;
};
/**
* What the five vetting criteria mean, and how this workspace usually weights
* them.
*
* The typical column is counted from the positions already posted, so it says
* what this operator does rather than what a template suggests.
*/
const vettingWeights = (f) => {
const posted = f.postings.filter((p) => p.vetting_criteria);
const keys = ['experience', 'english', 'reliability', 'certifications', 'availability'];
const meaning = {
experience: 'Years in the role, weighted against the minimum this position asks for',
english: 'Spoken level, checked in the AI interview',
reliability: 'Attendance and completion history on previous assignments',
certifications: 'Whether the credentials this role requires are held and current',
availability: 'How much of the shift pattern the candidate can actually cover',
};
if (!posted.length) {
return doc(
heading('AI vetting weights', 'What each criterion scores'),
list(keys.map((k) => `**${CRITERIA_LABELS[k]}** — ${meaning[k]}`)),
note('The five must total 100%. No position has been posted yet, so there is no house pattern to compare against.')
);
}
const typical = keys.map((key) => ({
criterion: CRITERIA_LABELS[key],
typical: `${across(posted, (p) => Number(p.vetting_criteria?.[key]))}%`,
scores: meaning[key],
}));
return doc(
heading('AI vetting weights', `Typical across ${plural(posted.length, 'posted position')}`),
table(
[
{ key: 'criterion', label: 'Criterion' },
{ key: 'typical', label: 'Usually', align: 'right' },
{ key: 'scores', label: 'What it scores' },
],
typical
),
note('These are this workspace’s own averages, not a recommendation. The five weights on this form must total 100%.')
);
};
/**
* What comparable roles in this workspace ask for.
*
* Grouped by role category, which is the only comparison the data supports —
* pay and experience are not transferable between a chef and a security post.
*/
const positionBenchmarks = (f) => {
const posted = f.postings.filter((p) => p.role_category);
if (!posted.length) {
return doc(
text('No positions have been posted yet, so there is nothing to benchmark this one against.'),
note('Once a few roles exist I can compare pay, experience and requirements by category.')
);
}
const byCategory = new Map();
posted.forEach((p) => {
const entry = byCategory.get(p.role_category) || { category: p.role_category, list: [] };
entry.list.push(p);
byCategory.set(p.role_category, entry);
});
const rows = [...byCategory.values()]
.sort((a, b) => b.list.length - a.list.length)
.slice(0, 6)
.map(({ category, list: roles }) => ({
category,
posted: roles.length,
pay: across(roles, (p) => Number(p.pay_range_min))
? `$${across(roles, (p) => Number(p.pay_range_min))}–${across(roles, (p) => Number(p.pay_range_max))}/hr`
: '—',
experience: `${across(roles, (p) => Number(p.min_experience_years))} yrs`,
}));
return doc(
heading('What comparable roles ask for', `${plural(posted.length, 'position')} on file`),
table(
[
{ key: 'category', label: 'Category' },
{ key: 'posted', label: 'Posted', align: 'right' },
{ key: 'pay', label: 'Pay', align: 'right' },
{ key: 'experience', label: 'Min experience', align: 'right' },
],
rows
),
note('Averages across positions already posted in this workspace.')
);
};
/** The credentials this workspace actually asks for, ranked by how often. */
const positionRequirements = (f) => {
const counts = new Map();
f.postings.forEach((p) => {
(p.certifications_required || []).forEach((c) => counts.set(c, (counts.get(c) || 0) + 1));
});
if (!counts.size) {
return doc(
text('No position on file requires a certification, so there is no pattern to follow here.'),
note('Add only the credentials this role genuinely needs — every one narrows the pool.')
);
}
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8);
return doc(
heading('Credentials this workspace asks for', `${plural(counts.size, 'certification')} in use`),
meters(ranked.map(([label, count]) => ({
label,
value: count,
max: f.postings.length || count,
}))),
note('Ranked by how many positions require them. Each requirement narrows the pool it is added to.')
);
};
/** How the form's own steps fit together, for someone new to it. */
const positionSpecSteps = () => doc(
heading('Specifying a position'),
list([
'Role category and job title — what the matching engine files this under',
'Client, requirements and pay — what the posting states',
'Certifications and required skills — what a person must hold',
'Generate Job Description with AI — writes the prose from the fields above',
'AI vetting weights — how the screening score is composed, totalling 100%',
], { ordered: true }),
note('Nothing is posted until you choose Save as Draft or Publish.')
);
export const respondCreatePosition = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'weight', 'vetting', 'criteria', 'scoring', 'score')) return vettingWeights(f);
if (has(q, 'certification', 'credential', 'requirement', 'skill')) return positionRequirements(f);
if (has(q, 'pay', 'rate', 'salary', 'benchmark', 'compare', 'experience', 'typical')) return positionBenchmarks(f);
if (has(q, 'how do i', 'step', 'what do i', 'fill', 'form', 'explain')) return positionSpecSteps(f);
return positionSpecSteps(f);
};
export const CREATE_POSITION_CAPABILITIES = [
{ id: 'vetting-weights', label: 'Explain Vetting Weights', icon: Gauge, run: vettingWeights },
{ id: 'position-benchmarks', label: 'Compare With Similar Roles', icon: ArrowLeftRight, run: positionBenchmarks },
{ id: 'position-requirements', label: 'Credentials In Use', icon: ShieldCheck, run: positionRequirements },
{ id: 'position-spec-steps', label: 'How This Form Works', icon: Lightbulb, run: positionSpecSteps },
];