Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useCallback, useEffect, useRef, lazy, Suspense, Component, type ReactNode } from 'react';
import { LayoutProvider } from './contexts/LayoutContext';
import { LayoutProvider, useLayout } from './contexts/LayoutContext';
import { EmployeeProvider } from './contexts/EmployeeContext';
import {
ConversationProvider,
Expand All @@ -17,6 +17,7 @@ import { TestDataImporter } from './components/dev/TestDataImporter';
import { OnboardingProvider, OnboardingFlow, useOnboarding } from './components/onboarding';
import { useEmployees } from './contexts/EmployeeContext';
import { useNetwork, useCommandPalette } from './hooks';
import { RECRUITING_ENABLED } from './lib/featureFlags';

const EmployeeEdit = lazy(() =>
import('./components/employees/EmployeeEdit').then((module) => ({ default: module.EmployeeEdit }))
Expand All @@ -31,6 +32,9 @@ const CommandPalette = lazy(() => import('./components/CommandPalette'));
const UpgradePrompt = lazy(() =>
import('./components/trial/UpgradePrompt').then((module) => ({ default: module.UpgradePrompt }))
);
const RecruitingView = lazy(() =>
import('./components/recruiting').then((module) => ({ default: module.RecruitingView }))
);
Comment on lines +35 to +37

// Error Boundary to catch React render errors.
// Positioned at the outermost App root so first-launch failures (onboarding
Expand Down Expand Up @@ -311,6 +315,12 @@ function MainAppContent() {
const [isTestDataModalOpen, setIsTestDataModalOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const chatInputRef = useRef<ChatInputHandle>(null);
const { sidebarTab } = useLayout();

// Recruit module (FHR-70) takes over the main content area when its tab is
// active. Guarded by RECRUITING_ENABLED so an off flag can never strand the
// user on an empty view (the tab can't be selected when the flag is off).
const showRecruiting = RECRUITING_ENABLED && sidebarTab === 'recruiting';

// Command palette hook (uses useLayout internally)
const { isOpen: isPaletteOpen, close: closePalette } = useCommandPalette({
Expand All @@ -337,7 +347,13 @@ function MainAppContent() {
contextPanel={<EmployeeDetail />}
onSettingsClick={() => setIsSettingsOpen(true)}
>
<ChatArea chatInputRef={chatInputRef} />
{showRecruiting ? (
<Suspense fallback={null}>
<RecruitingView />
</Suspense>
) : (
<ChatArea chatInputRef={chatInputRef} />
)}
</AppShell>
<EmployeeEditModal />
<ImportWizardModal />
Expand Down
15 changes: 15 additions & 0 deletions src/components/conversations/TabSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Allows switching between Conversations and Employees views in sidebar

import type { SidebarTab } from '../../contexts/LayoutContext';
import { RECRUITING_ENABLED } from '../../lib/featureFlags';

interface TabSwitcherProps {
value: SidebarTab;
Expand Down Expand Up @@ -30,6 +31,20 @@ export function TabSwitcher({ value, onChange }: TabSwitcherProps) {
},
];

// Recruit module tab (FHR-70) — only shown when the feature flag is on, so
// production (flag off) keeps exactly the Chats / People switcher it had.
if (RECRUITING_ENABLED) {
tabs.push({
key: 'recruiting',
label: 'Recruit',
icon: (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
),
});
}

return (
<div className="flex gap-1 p-1 bg-stone-200/50 rounded-lg">
{tabs.map((tab) => (
Expand Down
9 changes: 4 additions & 5 deletions src/components/layout/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,11 +279,10 @@ export function AppShell({ children, contextPanel, onSettingsClick }: AppShellPr

{/* Tab Content */}
<div className="flex-1 overflow-hidden">
{sidebarTab === 'conversations' ? (
<ConversationSidebar />
) : (
<EmployeePanel />
)}
{sidebarTab === 'conversations' && <ConversationSidebar />}
{sidebarTab === 'employees' && <EmployeePanel />}
{/* recruiting: sidebar intentionally empty in the S0.1 skeleton —
the empty Recruiting view renders in the main content area. */}
</div>
</div>
</aside>
Expand Down
33 changes: 33 additions & 0 deletions src/components/recruiting/RecruitingView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// People Partner — Recruit module (talent sourcing)
//
// S0.1 skeleton (FHR-70): an empty Recruiting view, rendered in the main
// content area when the `recruiting` tab is active and gated behind
// RECRUITING_ENABLED. This is the first visible seam for the module — S0.2
// stands up the Rust `recruiting` module + first command, and S0.3 round-trips
// a live Exa search into this view. Intentionally empty until then.

export function RecruitingView() {
return (
<div className="h-full flex flex-col items-center justify-center text-center px-6">
<div className="w-14 h-14 rounded-full bg-primary-100 flex items-center justify-center mb-4">
<svg
className="w-7 h-7 text-primary-600"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={1.5}
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
</div>
<h2 className="text-lg font-display font-semibold text-stone-800 mb-1">Recruit</h2>
<p className="text-sm text-stone-500 max-w-sm">
Context-aware talent sourcing, seeded by the employee data you already
have. This module is under construction — nothing to show here yet.
</p>
</div>
);
}

export default RecruitingView;
1 change: 1 addition & 0 deletions src/components/recruiting/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RecruitingView } from './RecruitingView';
2 changes: 1 addition & 1 deletion src/contexts/LayoutContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';

export type SidebarTab = 'conversations' | 'employees';
export type SidebarTab = 'conversations' | 'employees' | 'recruiting';

interface LayoutState {
sidebarOpen: boolean;
Expand Down
21 changes: 21 additions & 0 deletions src/lib/featureFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Feature flags — compile-time gates for in-development modules.
//
// These are plain constants, deliberately NOT settings-backed: a flag here
// hides an unfinished module from production users while it's being built, and
// is flipped by editing this file + rebuilding. There's nothing to toggle at
// runtime yet — the gated views are empty skeletons. When a module graduates
// to real, user-facing functionality, migrate its flag to a settings-table
// toggle (mirroring the Signals / Fairness "Intelligence Features" pattern in
// SettingsPanel) so users/support can enable it without a rebuild.
//
// Typed `: boolean` (not the inferred literal `false`) on purpose — it stops
// TypeScript from narrowing reads to dead branches, so `if (RECRUITING_ENABLED)`
// and `RECRUITING_ENABLED ? … : …` type-check cleanly with the flag either way.

/**
* Recruit module (talent sourcing) — FHR-70 → FHR-91.
* OFF in production until the module is built out. Flip to `true` locally to
* develop S0.2 (Rust `recruiting` module) and S0.3 (live Exa round-trip)
* against the UI seam established in S0.1.
*/
export const RECRUITING_ENABLED: boolean = false;