feat: rules engine — priority config UI + worklist scoring

- Rules engine spec v2 (priority vs automation rules distinction)
- Priority Rules settings page with weight sliders, SLA config, campaign/source weights
- Collapsible config sections with dynamic headers
- Live worklist preview panel with client-side scoring
- AI assistant panel (collapsible) with rules-engine-specific system prompt
- Worklist panel: score display with SLA status dots, sort by score
- Scoring library (scoring.ts) for client-side preview computation
- Sidebar: Rules Engine nav item under Configuration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 16:51:29 +05:30
parent 462601d0dc
commit b90740e009
14 changed files with 1680 additions and 514 deletions

View File

@@ -0,0 +1,160 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Tabs, TabList, Tab } from '@/components/application/tabs/tabs';
import { Button } from '@/components/base/buttons/button';
import { PriorityConfigPanel } from '@/components/rules/priority-config-panel';
import { CampaignWeightsPanel } from '@/components/rules/campaign-weights-panel';
import { SourceWeightsPanel } from '@/components/rules/source-weights-panel';
import { WorklistPreview } from '@/components/rules/worklist-preview';
import { RulesAiAssistant } from '@/components/rules/rules-ai-assistant';
import { DEFAULT_PRIORITY_CONFIG } from '@/lib/scoring';
import type { PriorityConfig } from '@/lib/scoring';
const API_BASE = import.meta.env.VITE_SIDECAR_URL ?? 'http://localhost:4100';
const getToken = () => localStorage.getItem('helix_access_token');
export const RulesSettingsPage = () => {
const token = getToken();
const [config, setConfig] = useState<PriorityConfig>(DEFAULT_PRIORITY_CONFIG);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [dirty, setDirty] = useState(false);
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const load = async () => {
try {
const res = await fetch(`${API_BASE}/api/rules/priority-config`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (res.ok) {
const data = await res.json();
setConfig(data);
}
} catch {
// Fallback to defaults
} finally {
setLoading(false);
}
};
load();
}, [token]);
const saveConfig = useCallback(async (newConfig: PriorityConfig) => {
try {
setSaving(true);
await fetch(`${API_BASE}/api/rules/priority-config`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify(newConfig),
});
setDirty(false);
} catch {
// Silent fail
} finally {
setSaving(false);
}
}, [token]);
const handleConfigChange = (newConfig: PriorityConfig) => {
setConfig(newConfig);
setDirty(true);
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(() => saveConfig(newConfig), 1000);
};
const applyTemplate = async () => {
try {
const res = await fetch(`${API_BASE}/api/rules/templates/hospital-starter/apply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (res.ok) {
const configRes = await fetch(`${API_BASE}/api/rules/priority-config`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
if (configRes.ok) {
setConfig(await configRes.json());
setDirty(false);
}
}
} catch {
// Silent fail
}
};
if (loading) {
return (
<div className="flex h-full items-center justify-center">
<div className="text-sm text-tertiary">Loading rules configuration...</div>
</div>
);
}
return (
<div className="flex h-full flex-col overflow-hidden">
{/* Header */}
<div className="shrink-0 flex items-center justify-between border-b border-secondary px-6 py-4">
<div>
<h1 className="text-lg font-bold text-primary">Rules Engine</h1>
<p className="text-sm text-tertiary">Configure how leads are prioritized and routed in the worklist</p>
</div>
<div className="flex items-center gap-3">
{dirty && <span className="text-xs text-warning-primary">{saving ? 'Saving...' : 'Unsaved changes'}</span>}
<Button size="sm" color="secondary" onClick={applyTemplate}>
Apply Starter Template
</Button>
</div>
</div>
{/* Tabs + Content — fills remaining height */}
<div className="flex flex-1 flex-col min-h-0">
<Tabs aria-label="Rules configuration" className="flex flex-1 flex-col min-h-0">
<div className="shrink-0 border-b border-secondary px-6 pt-2">
<TabList items={[{ id: 'priority', label: 'Priority Rules' }, { id: 'automations', label: 'Automations' }]} type="underline" size="sm">
{(item) => <Tab key={item.id} id={item.id} label={item.label} />}
</TabList>
</div>
<Tabs.Panel id="priority" className="flex flex-1 min-h-0">
<div className="flex flex-1 min-h-0">
{/* Left — config panels, scrollable */}
<div className="flex-1 overflow-y-auto p-6 space-y-8">
<PriorityConfigPanel config={config} onChange={handleConfigChange} />
<CampaignWeightsPanel config={config} onChange={handleConfigChange} />
<SourceWeightsPanel config={config} onChange={handleConfigChange} />
</div>
{/* Right — preview + collapsible AI */}
<div className="w-[400px] shrink-0 border-l border-secondary flex flex-col min-h-0 bg-secondary">
{/* Preview — takes available space */}
<div className="flex-1 overflow-y-auto p-4 min-h-0">
<WorklistPreview config={config} />
</div>
{/* AI Assistant — collapsible at bottom */}
<RulesAiAssistant config={config} />
</div>
</div>
</Tabs.Panel>
<Tabs.Panel id="automations" className="flex flex-1 min-h-0">
<div className="flex items-center justify-center flex-1 p-12">
<div className="text-center">
<h3 className="text-md font-semibold text-primary mb-2">Automation Rules</h3>
<p className="text-sm text-tertiary max-w-md">
Configure rules that automatically assign leads, escalate SLA breaches, and manage lead lifecycle.
This feature is coming soon.
</p>
</div>
</div>
</Tabs.Panel>
</Tabs>
</div>
</div>
);
};