mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 18:28:15 +00:00
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:
160
src/pages/rules-settings.tsx
Normal file
160
src/pages/rules-settings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user