import React, { useCallback, useEffect, useMemo, useState } from 'react' import type { CheckpointReplyMetadata, ChatMessageMeta, RecruitmentProposalEntry, StaffingEmployeeOption, StaffingRoleEntry, StaffingSelectionValue, StaffingTemplateOption, } from '../types/chat' import type { TaskPreferredAgent } from '../types/kanban' const TASK_AGENT_LABELS: Record = { native: 'OpenOPC Native', codex: 'Codex', claude_code: 'Claude Code', cursor: 'Cursor', opencode: 'OpenCode', } const DEFAULT_ROLE_AGENT: TaskPreferredAgent = 'codex' const DEFAULT_RECRUITMENT_AGENT: TaskPreferredAgent = 'native' const TASK_AGENT_OPTIONS: TaskPreferredAgent[] = ['codex', 'native', 'claude_code', 'cursor', 'opencode'] const RECRUITMENT_AGENT_OPTIONS: TaskPreferredAgent[] = ['native', 'codex', 'claude_code', 'cursor', 'opencode'] type StaffingOption = | { kind: 'employee'; id: string; name: string; subtitle: string; category: string; searchText: string } | { kind: 'template'; id: string; name: string; subtitle: string; category: string; searchText: string } | { kind: 'fallback'; id: ''; name: string; subtitle: string; category: string; searchText: string } interface RecruitmentPanelProps { meta: ChatMessageMeta onReply: (text: string, metadata?: CheckpointReplyMetadata) => void responded: boolean } function normalizeSelection(value: StaffingSelectionValue | undefined): StaffingSelectionValue { if (!value) return { kind: 'fallback' } if (value.kind === 'employee') { const id = String(value.id ?? value.employee_id ?? '').trim() return id ? { kind: 'employee', id } : { kind: 'fallback' } } if (value.kind === 'template') { const id = String(value.id ?? value.template_id ?? '').trim() return id ? { kind: 'template', id } : { kind: 'fallback' } } return { kind: 'fallback' } } function selectionKey(value: StaffingSelectionValue | undefined): string { const normalized = normalizeSelection(value) return normalized.kind === 'fallback' ? 'fallback:' : `${normalized.kind}:${normalized.id ?? ''}` } function buildOptions( role: StaffingRoleEntry, employees: StaffingEmployeeOption[], templates: StaffingTemplateOption[], ): StaffingOption[] { const roleId = String(role.role_id ?? '').trim() const roleLabel = String(role.role_label ?? '').trim() const roleText = `${roleId} ${roleLabel} ${role.role_responsibility ?? ''}`.toLowerCase() const sameRoleIds = new Set((role.same_role_employee_ids ?? []).map(item => String(item ?? '').trim()).filter(Boolean)) const employeeOptions = employees.map((employee): StaffingOption & { rank: number } => { const id = String(employee.employee_id ?? '').trim() const name = String(employee.employee_name ?? id).trim() || id const employeeRole = String(employee.role_id ?? '').trim() const category = String(employee.category ?? '').trim() const subtitle = [employeeRole, category].filter(Boolean).join(' · ') || id return { kind: 'employee', id, name, subtitle, category, searchText: `${id} ${name} ${employeeRole} ${category} ${(employee.domains ?? []).join(' ')} ${(employee.tags ?? []).join(' ')}`.toLowerCase(), rank: sameRoleIds.has(id) || employeeRole === roleId ? 0 : 2, } }).filter(option => option.id).sort((a, b) => a.rank - b.rank || a.name.localeCompare(b.name)) const templateOptions = templates.map((template): StaffingOption & { rank: number } => { const id = String(template.template_id ?? '').trim() const name = String(template.template_name ?? id).trim() || id const category = String(template.category ?? '').trim() const subtitle = [category, id].filter(Boolean).join(' · ') || id const templateText = `${id} ${name} ${category} ${(template.domains ?? []).join(' ')} ${(template.tags ?? []).join(' ')}`.toLowerCase() const rank = roleText.split(/[^a-z0-9]+/).filter(token => token.length >= 3).reduce( (score, token) => score + (templateText.includes(token) ? 1 : 0), 0, ) return { kind: 'template', id, name, subtitle, category, searchText: templateText, rank, } }).filter(option => option.id).sort((a, b) => b.rank - a.rank || a.name.localeCompare(b.name)) return [ ...employeeOptions, ...templateOptions, { kind: 'fallback', id: '', name: 'Fallback role-only', subtitle: 'No employee override', category: 'fallback', searchText: 'fallback role only no employee override' }, ] } function optionForSelection(options: StaffingOption[], selection: StaffingSelectionValue | undefined): StaffingOption { const key = selectionKey(selection) return options.find(option => `${option.kind}:${option.id}` === key) ?? options[0] } function optionMatches(option: StaffingOption, query: string): boolean { const terms = query.toLowerCase().split(/\s+/).filter(Boolean) if (terms.length === 0) return true return terms.every(term => option.searchText.includes(term)) } function buildRolesFromProposals(proposals: RecruitmentProposalEntry[]): StaffingRoleEntry[] { return proposals.map((proposal) => { const roleId = String(proposal.role_id ?? '').trim() const existingId = String(proposal.existing_employee?.employee_id ?? '').trim() const templateId = String(proposal.candidate?.template_id ?? '').trim() const defaultSelection: StaffingSelectionValue = existingId ? { kind: 'employee', id: existingId } : templateId ? { kind: 'template', id: templateId } : { kind: 'fallback' } return { role_id: roleId, role_label: proposal.role_labels?.[0] ?? roleId, role_responsibility: '', default_selection: defaultSelection, same_role_employee_ids: proposal.existing_employee_ids ?? [], fallback_available: true, default_agent: proposal.default_agent ?? DEFAULT_ROLE_AGENT, selected_agent: proposal.selected_agent ?? proposal.default_agent ?? DEFAULT_ROLE_AGENT, default_source: 'recruitment', } }).filter(role => role.role_id) } function selectedRecruitmentName( proposal: RecruitmentProposalEntry | undefined, selected: StaffingOption, ): string { if (selected.kind === 'template' && proposal?.candidate?.template_id === selected.id) { return proposal.candidate.proposed_name || proposal.candidate.template_name || selected.name } if (selected.kind === 'employee' && proposal?.existing_employee?.employee_id === selected.id) { return proposal.existing_employee.employee_name || selected.name } return selected.name } function buildSelectionsFromMeta(meta: ChatMessageMeta, roles: StaffingRoleEntry[]): Record { const initial: Record = {} const persisted = meta.staffing_selections ?? {} for (const role of roles) { const roleId = String(role.role_id ?? '').trim() if (!roleId) continue initial[roleId] = normalizeSelection(persisted[roleId] ?? role.default_selection) } return initial } function buildRoleAgentsFromMeta(meta: ChatMessageMeta, roles: StaffingRoleEntry[]): Record { const persisted = meta.recruitment_role_agents ?? {} const initial: Record = {} for (const role of roles) { const roleId = String(role.role_id ?? '').trim() if (!roleId) continue initial[roleId] = persisted[roleId] ?? role.selected_agent ?? role.default_agent ?? DEFAULT_ROLE_AGENT } return initial } function hasSubmittedCheckpointMetadata(meta: ChatMessageMeta): boolean { return Boolean( String(meta.checkpoint_response_message_id ?? '').trim() || String(meta.checkpoint_responded_at ?? '').trim() || String(meta.checkpoint_reply_kind ?? '').trim() ) } export const RecruitmentPanel = React.memo(function RecruitmentPanel({ meta, onReply, responded, }: RecruitmentPanelProps) { const proposals = meta.proposals ?? [] const proposalByRole = useMemo(() => { const next: Record = {} for (const proposal of proposals) { const roleId = String(proposal.role_id ?? '').trim() if (roleId) next[roleId] = proposal } return next }, [proposals]) const roles = useMemo( () => (meta.staffing_roles?.length ? meta.staffing_roles : buildRolesFromProposals(proposals)), [meta.staffing_roles, proposals], ) const employees = meta.staffing_pool?.employees ?? [] const templates = meta.staffing_pool?.templates ?? [] const rationales = meta.recruitment_rationales ?? [] const optionsByRole = useMemo(() => { const next: Record = {} for (const role of roles) { const roleId = String(role.role_id ?? '').trim() if (roleId) next[roleId] = buildOptions(role, employees, templates) } return next }, [employees, roles, templates]) const [queries, setQueries] = useState>({}) const [feedback, setFeedback] = useState('') const [selections, setSelections] = useState>(() => buildSelectionsFromMeta(meta, roles)) const [roleAgents, setRoleAgents] = useState>(() => buildRoleAgentsFromMeta(meta, roles)) const [recruitmentAgent, setRecruitmentAgent] = useState(meta.recruitment_agent ?? DEFAULT_RECRUITMENT_AGENT) const isResponded = responded useEffect(() => { if (!isResponded || !hasSubmittedCheckpointMetadata(meta)) return setSelections(buildSelectionsFromMeta(meta, roles)) setRoleAgents(buildRoleAgentsFromMeta(meta, roles)) setRecruitmentAgent(meta.recruitment_agent ?? DEFAULT_RECRUITMENT_AGENT) }, [isResponded, meta, roles]) useEffect(() => { setRecruitmentAgent(meta.recruitment_agent ?? DEFAULT_RECRUITMENT_AGENT) }, [meta.recruitment_agent]) const buildReplyMetadata = useCallback((kind: NonNullable): CheckpointReplyMetadata => { const checkpointId = String(meta.checkpoint_id ?? '').trim() if (!checkpointId) { throw new Error('Recruitment checkpoint reply requires checkpoint_id metadata.') } const checkpointType = String(meta.checkpoint_type ?? '').trim() return { response_to_checkpoint_id: checkpointId, response_to_checkpoint_type: checkpointType || 'company_recruitment_confirmation', checkpoint_reply_kind: kind, staffing_selections: selections, recruitment_agent: recruitmentAgent, recruitment_role_agents: roles.reduce>((acc, role) => { const roleId = String(role.role_id ?? '').trim() if (!roleId) return acc acc[roleId] = roleAgents[roleId] ?? role.selected_agent ?? role.default_agent ?? DEFAULT_ROLE_AGENT return acc }, {}), } }, [meta.checkpoint_id, meta.checkpoint_type, recruitmentAgent, roleAgents, roles, selections]) const handleApprove = useCallback(() => { if (isResponded) return onReply('approve', buildReplyMetadata('approve')) }, [buildReplyMetadata, isResponded, onReply]) const handleFeedback = useCallback(() => { if (isResponded || !feedback.trim()) return onReply(feedback.trim(), buildReplyMetadata('feedback')) setFeedback('') }, [buildReplyMetadata, isResponded, feedback, onReply]) return (
Recruitment Review
{meta.company_profile || 'corporate'} {isResponded && Responded}
{meta.summary &&
{meta.summary}
}
{rationales.length > 0 && (
{rationales.map(item => (
{item.role_id} {item.role_label && item.role_label !== item.role_id && {item.role_label}} {item.selection_label && {item.selection_label}}
{item.rationale &&
{item.rationale}
}
))}
)}
{roles.map(role => { const roleId = String(role.role_id ?? '').trim() const options = optionsByRole[roleId] ?? [{ kind: 'fallback', id: '', name: 'Fallback role-only', subtitle: 'No employee override', category: 'fallback', searchText: 'fallback role only no employee override' }] const selected = optionForSelection(options, selections[roleId]) const proposal = proposalByRole[roleId] const query = queries[roleId] ?? '' const visibleOptions = options.filter(option => optionMatches(option, query)).slice(0, 8) return (
{roleId} {selected.kind}
{role.role_label && role.role_label !== roleId && (
{role.role_label}
)}
{selectedRecruitmentName(proposal, selected)}
{selected.category} {selected.subtitle}
setQueries(current => ({ ...current, [roleId]: event.target.value }))} placeholder="Search employees or templates..." disabled={isResponded} />
{visibleOptions.map(option => { const active = `${option.kind}:${option.id}` === selectionKey(selections[roleId]) return ( ) })}
) })}
{!isResponded && (