ui changes

This commit is contained in:
2026-08-10 19:36:45 +05:30
parent 409dffe938
commit 5cd49a1655
49 changed files with 7988 additions and 470 deletions

View File

@@ -1,5 +1,11 @@
import { AlertTriangle, Eye, Gauge, Lightbulb, ScrollText, TrendingUp } from 'lucide-react';
import { actions, answer, doc, heading, insights, kpis, note, status, table, text } from '../blocks';
import {
Activity, AlertTriangle, ArrowLeftRight, BellRing, Eye, Filter, Gauge, GitBranch,
Lightbulb, ScrollText, ShieldAlert, TrendingUp, Users,
} from 'lucide-react';
import {
actions, answer, doc, funnel, heading, insights, kpis, list, meters, note, status, table,
text, timeline,
} from '../blocks';
import { plural, verb } from '../insights';
/**
@@ -68,6 +74,199 @@ const platformHealth = (f) => {
);
};
/**
* 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 && {
@@ -197,6 +396,195 @@ const candidateRiskAnalysis = (f) => {
);
};
/**
* 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) => {
@@ -294,26 +682,231 @@ const userActivityInsights = (f) => {
);
};
/**
* 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 = ['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.'
)
);
};
/* ── 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, 'recommend', 'should', 'action', 'fix', 'improve')) return recommendations(f);
if (has(q, 'unusual', 'anomal', 'suspicious')) return userActivityInsights(f);
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 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 respondActivity = (question, f) => {
const q = question.toLowerCase();
if (has(q, 'user', 'who', 'active', 'behaviour', 'behavior', 'account', 'unusual')) return userActivityInsights(f);
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);
};
@@ -321,15 +914,24 @@ export const respondActivity = (question, f) => {
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 ADMIN_CANDIDATE_CAPABILITIES = [
{ id: 'candidate-risk', label: 'Candidate Risk', icon: AlertTriangle, run: candidateRiskAnalysis },
{ id: 'recruitment-insights', label: 'Recruitment Insights', icon: TrendingUp, run: recruitmentInsights },
{ id: 'candidate-risk', label: 'Candidate Risk Analysis', icon: AlertTriangle, run: candidateRiskAnalysis },
{ 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 ACTIVITY_CAPABILITIES = [
{ id: 'audit-summary', label: 'Audit Summary', icon: ScrollText, run: auditSummary },
{ id: 'user-activity', label: 'User Activity Insights', icon: Eye, run: userActivityInsights },
{ 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 },
];