import React, { useCallback, useEffect, useMemo, useState } from 'react' import type { CheckpointReplyMetadata, ChatMessageMeta, 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 StaffingSelectionPanelProps { 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 templateScore = (template: StaffingTemplateOption): number => { const templateText = `${template.template_id ?? ''} ${template.template_name ?? ''} ${template.category ?? ''} ${(template.domains ?? []).join(' ')} ${(template.tags ?? []).join(' ')}`.toLowerCase() const categoryTerm = String(template.category ?? '').toLowerCase() let score = 0 for (const token of roleText.split(/[^a-z0-9]+/).filter(token => token.length >= 3)) { if (templateText.includes(token) || (categoryTerm && token.includes(categoryTerm))) score += 1 } if (templateText.includes(roleId.replace(/_/g, '-')) || templateText.includes(roleId.replace(/_/g, ' '))) score += 2 return score } 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 return { kind: 'template', id, name, subtitle, category, searchText: `${id} ${name} ${category} ${(template.domains ?? []).join(' ')} ${(template.tags ?? []).join(' ')}`.toLowerCase(), rank: templateScore(template), } }).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 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.staffing_action ?? '').trim() ) } export const StaffingSelectionPanel = React.memo(function StaffingSelectionPanel({ meta, onReply, responded, }: StaffingSelectionPanelProps) { const roles = useMemo(() => meta.staffing_roles ?? [], [meta.staffing_roles]) const employees = meta.staffing_pool?.employees ?? [] const templates = meta.staffing_pool?.templates ?? [] 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 [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 const recommendAutoRecruit = meta.recommended_action === 'auto_recruit' && templates.length > 0 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((action: 'manual_approve' | 'auto_recruit'): CheckpointReplyMetadata => { const checkpointId = String(meta.checkpoint_id ?? '').trim() if (!checkpointId) { throw new Error('Staffing checkpoint reply requires checkpoint_id metadata.') } return { response_to_checkpoint_id: checkpointId, response_to_checkpoint_type: 'company_staffing_selection', staffing_action: action, 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, recruitmentAgent, roleAgents, roles, selections]) const handleApprove = useCallback(() => { if (isResponded) return onReply('approve', buildReplyMetadata('manual_approve')) }, [buildReplyMetadata, isResponded, onReply]) const handleAutoRecruit = useCallback(() => { if (isResponded) return onReply('auto recruit', buildReplyMetadata('auto_recruit')) }, [buildReplyMetadata, isResponded, onReply]) return (
Manual Staffing
{meta.company_profile || 'corporate'} {recommendAutoRecruit && !isResponded && Recruit recommended} {isResponded && Responded}
{meta.summary &&
{meta.summary}
}
{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 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}
)}
{selected.name}
{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 && (
{recommendAutoRecruit ? ( <> ) : ( <> {templates.length > 0 && } )}
)}
) })