Pulse/frontend-modern/src/components/AI/AIStatusIndicator.tsx
rcourtman 420e4960dc feat(ui): add learning status hook and enhance AI indicator visibility
New useLearningStatus hook:
- Polls /api/ai/intelligence/learning every 60 seconds
- Provides resourceCount(), metricCount(), learningState()
- Convenience accessors: isActive(), isLearning(), isWaiting()

Enhanced AIStatusIndicator:
- Now shows when ANY baselines exist (not just when Patrol enabled)
- Tooltip shows 'X resources baselined' for transparency
- Healthy state  45 resources baselined'shows '
- Works even without Pro license since baselines are FREE

This makes the AI presence visible from the moment Pulse starts
learning, providing immediate value feedback to all users.
2025-12-21 11:45:55 +00:00

197 lines
7.5 KiB
TypeScript

/**
* AIStatusIndicator - Subtle header component showing AI patrol health and anomalies
*
* Design: Minimal presence when healthy, highlighted when issues or anomalies detected.
* Clicking navigates to the Alerts page where AI Insights are displayed.
*/
import { createResource, Show, createMemo, onCleanup } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { getPatrolStatus, type PatrolStatus } from '../../api/patrol';
import { useAllAnomalies } from '@/hooks/useAnomalies';
import { useLearningStatus } from '@/hooks/useLearningStatus';
import './AIStatusIndicator.css';
export function AIStatusIndicator() {
const navigate = useNavigate();
// Poll patrol status every 30 seconds
const [status, { refetch }] = createResource<PatrolStatus>(
async () => {
try {
return await getPatrolStatus();
} catch {
return null as unknown as PatrolStatus;
}
},
{ initialValue: undefined }
);
// Get anomaly data (also polls every 30 seconds via the hook)
const anomalyData = useAllAnomalies();
// Get learning status (polls every 60 seconds)
const learningStatus = useLearningStatus();
// Refetch patrol status every 30 seconds with proper cleanup
const intervalId = setInterval(() => refetch(), 30000);
onCleanup(() => clearInterval(intervalId));
// Count anomalies by severity
const anomalyCounts = createMemo(() => {
const anomalies = anomalyData.anomalies();
const counts = { critical: 0, high: 0, medium: 0, low: 0 };
for (const a of anomalies) {
counts[a.severity]++;
}
return counts;
});
const totalAnomalies = createMemo(() => anomalyData.count());
const hasIssues = createMemo(() => {
const s = status();
if (!s) return false;
return s.summary.critical > 0 || s.summary.warning > 0;
});
const hasWatch = createMemo(() => {
const s = status();
if (!s) return false;
return s.summary.watch > 0 && !hasIssues();
});
const hasAnomalies = createMemo(() => {
const counts = anomalyCounts();
return counts.critical > 0 || counts.high > 0;
});
const hasMildAnomalies = createMemo(() => {
const counts = anomalyCounts();
return !hasAnomalies() && (counts.medium > 0 || counts.low > 0);
});
const totalFindings = createMemo(() => {
const s = status();
if (!s) return 0;
return s.summary.critical + s.summary.warning + s.summary.watch;
});
const tooltipText = createMemo(() => {
const parts: string[] = [];
// Patrol status
const s = status();
if (s?.enabled && s?.running) {
if (s.summary.critical > 0) parts.push(`${s.summary.critical} critical findings`);
if (s.summary.warning > 0) parts.push(`${s.summary.warning} warnings`);
if (s.summary.watch > 0) parts.push(`${s.summary.watch} watching`);
}
// Anomaly status
const counts = anomalyCounts();
const anomalyTotal = totalAnomalies();
if (anomalyTotal > 0) {
const anomalyParts: string[] = [];
if (counts.critical > 0) anomalyParts.push(`${counts.critical} critical`);
if (counts.high > 0) anomalyParts.push(`${counts.high} high`);
if (counts.medium > 0) anomalyParts.push(`${counts.medium} medium`);
if (counts.low > 0) anomalyParts.push(`${counts.low} low`);
parts.push(`Anomalies: ${anomalyParts.join(', ')}`);
}
// Learning status - show when healthy to indicate AI is working
const resourceCount = learningStatus.resourceCount();
if (parts.length === 0 && resourceCount > 0) {
// Show learning progress when healthy
return `AI: All healthy • ${resourceCount} resources baselined`;
}
if (parts.length === 0) {
if (!s?.enabled) {
// Show baseline info even when patrol disabled
if (resourceCount > 0) {
return `AI Learning: ${resourceCount} resources baselined`;
}
return 'AI Baseline Learning active';
}
if (s?.license_required) {
if (s.license_status === 'active') {
return 'AI Patrol is not included in this license tier';
}
if (s.license_status === 'expired') {
return 'AI Patrol license expired';
}
return 'AI Patrol requires Pulse Pro';
}
return 'AI: All systems healthy';
}
return `AI Intelligence: ${parts.join(' | ')}`;
});
const statusClass = createMemo(() => {
if (hasIssues() || hasAnomalies()) return 'ai-status--issues';
if (hasWatch() || hasMildAnomalies()) return 'ai-status--watch';
return 'ai-status--healthy';
});
const handleClick = () => {
// Navigate to Alerts page with AI Insights subtab selected
navigate('/alerts?subtab=ai-insights');
};
// Combined total for badge
const badgeCount = createMemo(() => totalFindings() + totalAnomalies());
// Show indicator if patrol is enabled, we have anomalies, OR we have learned baselines
// This makes the AI presence visible even before Patrol is configured
const showIndicator = createMemo(() => {
const s = status();
const hasBaselines = learningStatus.resourceCount() > 0;
return s?.enabled || totalAnomalies() > 0 || hasBaselines;
});
return (
<Show when={showIndicator()}>
<button
class={`ai-status-indicator ${statusClass()}`}
onClick={handleClick}
title={tooltipText()}
>
<span class="ai-status-icon">
<Show when={hasIssues() || hasAnomalies()} fallback={
<Show when={hasWatch() || hasMildAnomalies()} fallback={
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
<path d="M9 12l2 2 4-4" />
</svg>
}>
{/* Watch icon - eye */}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
<circle cx="12" cy="12" r="3" />
</svg>
</Show>
}>
{/* Issues icon - alert */}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
</Show>
</span>
<Show when={badgeCount() > 0}>
<span class="ai-status-count">{badgeCount()}</span>
</Show>
</button>
</Show>
);
}
export default AIStatusIndicator;