update chatbox
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { AlertTriangle, Eye, Gauge, Lightbulb, ScrollText, TrendingUp } from 'lucide-react';
|
||||
import { bullets, compose, heading, numbered, plural, verb } from '../insights';
|
||||
import { actions, answer, doc, heading, insights, kpis, note, status, table, text } from '../blocks';
|
||||
import { plural, verb } from '../insights';
|
||||
|
||||
/**
|
||||
* Admin capabilities — Control Center, Candidates Analysis, Activity.
|
||||
*
|
||||
* Admin answers are platform-wide rather than pipeline-specific: data integrity,
|
||||
* talent supply, audit reconciliation.
|
||||
* Platform-wide rather than pipeline-specific: data integrity, talent supply,
|
||||
* audit reconciliation.
|
||||
*/
|
||||
|
||||
/* ── Control Center ─────────────────────────────────────────────────────── */
|
||||
@@ -15,148 +16,184 @@ const platformHealth = (f) => {
|
||||
{
|
||||
label: 'Data integrity',
|
||||
ok: f.applications.every((a) => a.job_posting_id),
|
||||
good: 'Every application is linked to a live posting.',
|
||||
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: `${f.standardizedPct}% of applicants carry a score.`,
|
||||
bad: `Only ${f.standardizedPct}% of applicants are scored — ${plural(f.unscreened.length, 'record')} unstandardized.`,
|
||||
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')} flagged below full integrity.`,
|
||||
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: `${f.profiles.length - f.unscoredProfiles.length} of ${f.profiles.length} talent profiles have a career score.`,
|
||||
bad: `${plural(f.unscoredProfiles.length, 'profile')} of ${f.profiles.length} ${verb(f.unscoredProfiles.length, 'has', 'have')} no score, so ${verb(f.unscoredProfiles.length, 'it cannot', 'they cannot')} be matched or ranked.`,
|
||||
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 at a ${f.hireRate}% hire rate.`,
|
||||
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 compose(
|
||||
heading(`Platform health: ${passing}/${checks.length} checks passing`),
|
||||
'',
|
||||
checks.map((c) => `${c.ok ? '✓' : '⚠'} **${c.label}** — ${c.ok ? c.good : c.bad}`).join('\n'),
|
||||
'',
|
||||
return doc(
|
||||
heading(`Platform health: ${passing}/${checks.length} passing`),
|
||||
status(checks),
|
||||
heading('Scale'),
|
||||
bullets([
|
||||
`${plural(f.postings.length, 'position')}, ${f.openPositions.length} open`,
|
||||
`${plural(f.total, 'application')}`,
|
||||
`${plural(f.interviews.length, 'AI interview')}`,
|
||||
`${plural(f.profiles.length, 'talent profile')}, ${f.elite.length} Elite`,
|
||||
`${plural(f.activity.length, 'logged event')}`,
|
||||
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` },
|
||||
]),
|
||||
'',
|
||||
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.'
|
||||
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.'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const recommendations = (f) => {
|
||||
const actions = [];
|
||||
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 (f.unscoredProfiles.length) {
|
||||
actions.push(`**Prompt ${plural(f.unscoredProfiles.length, 'worker')} to finish onboarding.** Without a career score they are invisible to matching, so employers cannot find them even when they fit.`);
|
||||
}
|
||||
if (f.unscreened.length) {
|
||||
actions.push(`**${plural(f.unscreened.length, 'application')} unscored.** Each one is a decision made without a standard.`);
|
||||
}
|
||||
if (f.starvedPositions.length) {
|
||||
actions.push(`**${plural(f.starvedPositions.length, 'posting')} with zero applicants** — ${f.starvedPositions.map((p) => p.title).join(', ')}. Check distribution and pay bands.`);
|
||||
}
|
||||
if (f.unratedStaff.length) {
|
||||
actions.push(`**${plural(f.unratedStaff.length, 'hire')} unrated.** Employer ratings are the only verified performance signal feeding scores; without them the scores stay theoretical.`);
|
||||
}
|
||||
if (f.flagged.length) {
|
||||
actions.push(`**Review ${plural(f.flagged.length, 'flagged interview')}** before those candidates progress.`);
|
||||
}
|
||||
if (!items.length) return answer('No platform actions outstanding. Data is complete and nothing is flagged.');
|
||||
|
||||
if (!actions.length) return 'No platform actions outstanding. Data is complete and nothing is flagged.';
|
||||
|
||||
return compose(heading('Recommended actions'), '', numbered(actions));
|
||||
return doc(heading('Recommended actions', 'Ordered by impact on matching quality'), actions(items));
|
||||
};
|
||||
|
||||
/* ── Candidates Analysis ────────────────────────────────────────────────── */
|
||||
|
||||
const recruitmentInsights = (f) => {
|
||||
const bands = {
|
||||
Elite: f.profiles.filter((p) => (p.krow_score || 0) >= 90).length,
|
||||
Excellent: f.profiles.filter((p) => (p.krow_score || 0) >= 75 && (p.krow_score || 0) < 90).length,
|
||||
Solid: f.profiles.filter((p) => (p.krow_score || 0) >= 60 && (p.krow_score || 0) < 75).length,
|
||||
Building: f.profiles.filter((p) => (p.krow_score || 0) > 0 && (p.krow_score || 0) < 60).length,
|
||||
Unscored: f.unscoredProfiles.length,
|
||||
};
|
||||
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 = bands.Elite + bands.Excellent;
|
||||
const topHeavy = band(90) + band(75, 90);
|
||||
|
||||
return compose(
|
||||
heading('Talent supply'),
|
||||
'',
|
||||
bullets(Object.entries(bands).map(([band, count]) => `${band} — ${count}`)),
|
||||
'',
|
||||
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'),
|
||||
bullets([
|
||||
`${plural(f.total, 'applicant')} averaging ${f.avgScore}`,
|
||||
`${plural(f.ranked.filter((a) => a.ai_score >= 80).length, 'candidate')} at 80+`,
|
||||
`${plural(verified, 'worker')} with at least one verified capability`,
|
||||
`${plural(f.eventCounts.apply_job || 0, 'application event')} logged`,
|
||||
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.',
|
||||
},
|
||||
]),
|
||||
'',
|
||||
bands.Unscored > topHeavy
|
||||
? `The pool is bottom-heavy: ${bands.Unscored} unscored against ${topHeavy} at Excellent or above. Supply is not the constraint — verification is.`
|
||||
: 'The scored portion outweighs the unscored, so verification is keeping pace with sign-ups.',
|
||||
'',
|
||||
heading('By role category'),
|
||||
bullets(
|
||||
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) => `${r.title} — ${plural(r.applied, 'applicant')}, ${r.qualified} at 70+`)
|
||||
.map((r) => ({ title: r.title, applied: r.applied, qualified: r.qualified }))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const candidateRiskAnalysis = (f) => {
|
||||
const risks = [];
|
||||
|
||||
if (f.missingCredentials.length) {
|
||||
risks.push(`**No credentials on file** — ${plural(f.missingCredentials.length, 'candidate')}. Compliance risk if placed on a role that requires certification.`);
|
||||
}
|
||||
if (f.narrowAvailability.length) {
|
||||
risks.push(`**Narrow availability** — ${plural(f.narrowAvailability.length, 'candidate')} listed one slot or none. Most likely to fall through on the day.`);
|
||||
}
|
||||
if (f.flagged.length) {
|
||||
risks.push(`**Interview integrity** — ${plural(f.flagged.length, 'interview')} below full integrity, usually suspiciously fast responses. Worth a human re-interview.`);
|
||||
}
|
||||
const noHistory = f.scored.filter((a) => !a.years_experience);
|
||||
if (noHistory.length) {
|
||||
risks.push(`**No recorded experience** — ${plural(noHistory.length, 'candidate')}. Not disqualifying, but unverifiable on paper.`);
|
||||
}
|
||||
if (f.singleCandidateRoles.length) {
|
||||
risks.push(`**Concentration risk** — ${f.singleCandidateRoles.map((e) => e.posting.title).join(', ')} ${verb(f.singleCandidateRoles.length, 'depends', 'depend')} on a single viable candidate.`);
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
if (!risks.length) return 'No material risk flags: credentials, availability and interview integrity all check out across the scored pool.';
|
||||
|
||||
return compose(
|
||||
return doc(
|
||||
heading(`${plural(risks.length, 'risk flag')}`),
|
||||
'',
|
||||
numbered(risks),
|
||||
'',
|
||||
'_Flags to check, not judgements. Each is a question for a human, not a rejection._'
|
||||
insights(risks),
|
||||
note('Flags to check, not judgements. Each is a question for a human, not a rejection.')
|
||||
);
|
||||
};
|
||||
|
||||
@@ -166,36 +203,45 @@ 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');
|
||||
const creates = count('create_position');
|
||||
|
||||
return compose(
|
||||
return doc(
|
||||
heading('Audit summary'),
|
||||
'',
|
||||
bullets([
|
||||
`${plural(f.activity.length, 'event')} total`,
|
||||
`${plural(f.activity7d.length, 'event')} in the last 7 days`,
|
||||
`${plural(f.activity24h.length, 'event')} in the last 24 hours`,
|
||||
`${plural(new Set(f.activity.map((e) => e.user_email)).size, 'distinct user')}`,
|
||||
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'),
|
||||
bullets([
|
||||
`${plural(creates, 'position creation')}`,
|
||||
`${plural(screens, 'screening action')}`,
|
||||
`${plural(hires, 'hire')}`,
|
||||
]),
|
||||
'',
|
||||
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'),
|
||||
bullets([
|
||||
hires === f.staff.length
|
||||
? `All ${f.staff.length} people on staff have a matching hire event.`
|
||||
: `${f.staff.length} on staff but ${hires} hire ${verb(hires, 'event', 'events')} — ${Math.abs(f.staff.length - hires)} unexplained.`,
|
||||
screens < f.scored.length
|
||||
? `${f.scored.length} candidates carry scores but only ${plural(screens, 'screening event')} logged. Batch screening records one event per run, not per candidate.`
|
||||
: 'Screening events reconcile with scored candidates.',
|
||||
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.',
|
||||
},
|
||||
]),
|
||||
'',
|
||||
'_An audit trail is only as good as its gaps. The reconciliation above is what I would check first in a real review._'
|
||||
note('An audit trail is only as good as its gaps. The reconciliation above is what I would check first in a real review.')
|
||||
);
|
||||
};
|
||||
|
||||
@@ -208,32 +254,43 @@ const userActivityInsights = (f) => {
|
||||
}, {});
|
||||
|
||||
const ranked = Object.entries(perUser).sort((a, b) => b[1].count - a[1].count);
|
||||
if (!ranked.length) return 'No activity to profile yet.';
|
||||
|
||||
const byRole = f.activity.reduce((acc, e) => {
|
||||
const role = e.account_type || 'unknown';
|
||||
acc[role] = (acc[role] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
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 compose(
|
||||
return doc(
|
||||
heading('Most active accounts'),
|
||||
'',
|
||||
bullets(ranked.slice(0, 6).map(([email, d]) =>
|
||||
`**${d.name || email}** (${d.role || 'unknown'}) — ${plural(d.count, 'event')} across ${plural(d.types.size, 'action type')}`
|
||||
)),
|
||||
'',
|
||||
heading('By role'),
|
||||
bullets(Object.entries(byRole).sort((a, b) => b[1] - a[1])
|
||||
.map(([role, c]) => `${role.charAt(0).toUpperCase() + role.slice(1)} — ${plural(c, 'event')}`)),
|
||||
'',
|
||||
`${top.name || topEmail} accounts for ${top.count} of ${f.activity.length} events. On a live platform, one account dominating the log is worth confirming is a person and not an integration.`,
|
||||
'',
|
||||
f.activity24h.length === 0
|
||||
? 'No activity in the last day. Expected for a demo dataset; on a live platform it would be worth checking ingestion.'
|
||||
: 'Activity is flowing normally.'
|
||||
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.',
|
||||
},
|
||||
])
|
||||
);
|
||||
};
|
||||
|
||||
@@ -244,6 +301,7 @@ 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);
|
||||
return platformHealth(f);
|
||||
};
|
||||
|
||||
@@ -255,7 +313,7 @@ export const respondAdminCandidates = (question, f) => {
|
||||
|
||||
export const respondActivity = (question, f) => {
|
||||
const q = question.toLowerCase();
|
||||
if (has(q, 'user', 'who', 'active', 'behaviour', 'behavior', 'account')) return userActivityInsights(f);
|
||||
if (has(q, 'user', 'who', 'active', 'behaviour', 'behavior', 'account', 'unusual')) return userActivityInsights(f);
|
||||
return auditSummary(f);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user