update owliver skill
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
Activity, AlertTriangle, ArrowLeftRight, Award, BellRing, Clock, Eye, Filter, Gauge,
|
||||
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';
|
||||
@@ -962,6 +962,41 @@ const positionsNeedingAttention = (f) => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 offered with the control that finishes it: the same
|
||||
* authoring form, told which draft it is continuing, with everything already
|
||||
* entered still in place.
|
||||
*
|
||||
* 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(' · '),
|
||||
action: { label: 'Continue to save', to: `/admin/positions/new?draft=${encodeURIComponent(p.id)}` },
|
||||
}))),
|
||||
note('Continuing reopens the same form with everything already entered — the vetting weights included. Publishing it there is what makes it an active position.')
|
||||
);
|
||||
};
|
||||
|
||||
/** Which role to fill first. */
|
||||
const hiringPriority = (f) => {
|
||||
const rows = f.byRole
|
||||
@@ -1259,6 +1294,7 @@ export const respondControlCenter = (question, 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);
|
||||
@@ -1317,6 +1353,7 @@ export const CONTROL_CENTER_CAPABILITIES = [
|
||||
];
|
||||
|
||||
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 },
|
||||
@@ -1935,3 +1972,164 @@ export const PROFILE_CAPABILITIES = [
|
||||
{ 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 },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user