widgettdc-api / apps /matrix-frontend /src /components /InteractiveWidgetContent.tsx
Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
import { useEffect, useState } from 'react';
import {
Rss, Activity, TrendingUp, AlertTriangle, Globe,
Database, Zap, BarChart3, LineChart, Map, Target, Radar, Grid3X3,
RefreshCw, Brain, Wifi, WifiOff, Download, ExternalLink
} from 'lucide-react';
import { useWidgetCommunication } from '@/contexts/WidgetContext';
import { cn } from '@/lib/utils';
import { useHealthData, useHealingStatus, useHyperEvents as useBackendHyperEvents } from '@/hooks/useBackendData';
import { useLiveData, useAlerts, useHyperEvents, useSystemMetrics, useSourcesHealth } from '@/hooks/useLiveData';
import AutonomousMetricsWidget from '@/widgets/AutonomousMetricsWidget';
import SourceDiscoveryWidget from '@/widgets/SourceDiscoveryWidget';
import ApprovalQueueWidget from '@/widgets/ApprovalQueueWidget';
interface InteractiveWidgetContentProps {
type: string;
widgetId: string;
}
// ============================================================
// SOURCE RECOMMENDATION PANEL - Suggests additional data sources
// ============================================================
const SourceRecommendationPanel = ({
sources,
onFetch
}: {
sources: Array<{ id: string; name: string; description: string; isAvailable: boolean; requiresApproval: boolean }>;
onFetch: (id: string) => void;
}) => {
if (sources.length === 0) return null;
const unavailableSources = sources.filter(s => !s.isAvailable);
if (unavailableSources.length === 0) return null;
return (
<div className="mt-3 pt-3 border-t border-border/30">
<p className="text-xs text-muted-foreground mb-2 flex items-center gap-1">
<ExternalLink className="w-3 h-3" />
Anbefalede kilder:
</p>
<div className="space-y-1">
{unavailableSources.slice(0, 3).map(source => (
<button
key={source.id}
onClick={() => onFetch(source.id)}
className="w-full flex items-center justify-between p-1.5 bg-secondary/20 hover:bg-secondary/40 rounded text-xs transition-colors"
>
<span className="text-foreground">{source.name}</span>
<span className="text-primary flex items-center gap-1">
<Download className="w-3 h-3" />
Hent
</span>
</button>
))}
</div>
</div>
);
};
// ============================================================
// CONNECTION STATUS INDICATOR
// ============================================================
const ConnectionIndicator = ({ status }: { status: 'connected' | 'connecting' | 'disconnected' }) => (
<div className="flex items-center gap-1 text-xs">
{status === 'connected' ? (
<><Wifi className="w-3 h-3 text-green-400" /><span className="text-green-400">Live</span></>
) : status === 'connecting' ? (
<><Wifi className="w-3 h-3 text-primary animate-pulse" /><span className="text-primary">Connecting...</span></>
) : (
<><WifiOff className="w-3 h-3 text-destructive" /><span className="text-destructive">Offline</span></>
)}
</div>
);
// ============================================================
// NEWS WIDGET - Using HyperLog Events
// ============================================================
const NewsWidget = ({ widgetId }: { widgetId: string }) => {
const { filters, sharedData, selectThreat } = useWidgetCommunication(widgetId);
const { data, isLoading, connectionStatus, recommendedSources, fetchRecommendedSource } = useHyperEvents(15000);
// Transform HyperLog events into news items
const newsItems = (data?.events || [])
.filter((e: any) => ['THOUGHT', 'CRITICAL_DECISION', 'DELEGATION', 'threat:detected'].includes(e.type))
.slice(0, 10)
.map((e: any, i: number) => ({
id: e.id || `event-${i}`,
title: e.content || e.payload?.message || 'System Event',
severity: e.type === 'CRITICAL_DECISION' ? 'critical' : e.type === 'threat:detected' ? 'high' : 'medium',
time: formatRelativeTime(e.timestamp),
region: e.metadata?.region || 'system',
agent: e.agent || 'System',
}));
const filteredNews = newsItems.filter((item: any) => {
if (filters.severity && filters.severity !== 'all' && item.severity !== filters.severity) return false;
if (filters.searchQuery && !item.title.toLowerCase().includes(filters.searchQuery.toLowerCase())) return false;
return true;
});
if (isLoading && newsItems.length === 0) {
return <div className="text-xs text-muted-foreground animate-pulse">Indlæser nyheder...</div>;
}
return (
<div className="space-y-2">
<div className="flex justify-between items-center mb-2">
<ConnectionIndicator status={connectionStatus} />
</div>
{filteredNews.length === 0 ? (
<p className="text-xs text-muted-foreground">Ingen nyheder matcher filtrene</p>
) : (
filteredNews.slice(0, 4).map((item: any) => (
<button
key={item.id}
onClick={() => selectThreat({ id: item.id, name: item.title, severity: item.severity })}
className={cn(
"w-full p-2 bg-secondary/30 border-l-2 border-primary/50 text-left text-xs transition-all hover:border-primary hover:bg-secondary/50",
sharedData.selectedThreat?.id === item.id && "border-primary bg-primary/10"
)}
>
<span className="text-foreground">{item.title}</span>
<div className="flex gap-2 mt-1 text-muted-foreground">
<span className={item.severity === 'critical' ? 'text-destructive' : item.severity === 'high' ? 'text-orange-400' : 'text-primary'}>
{item.severity}
</span>
<span>• {item.time}</span>
<span className="text-muted-foreground/60">• {item.agent}</span>
</div>
</button>
))
)}
<SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} />
</div>
);
};
// ============================================================
// ALERTS WIDGET - Using Live Alerts
// ============================================================
const AlertsWidget = ({ widgetId }: { widgetId: string }) => {
const { filters, sharedData, selectAlert, highlightIP } = useWidgetCommunication(widgetId);
const { data: liveAlerts, isLoading, connectionStatus, recommendedSources, fetchRecommendedSource } = useAlerts(10000);
// Fallback to HyperLog for alerts if no dedicated alerts endpoint
const { data: hyperData } = useHyperEvents(15000);
// Combine and transform alerts
const alerts = (liveAlerts || []).length > 0
? liveAlerts
: (hyperData?.events || [])
.filter((e: any) => ['system.alert', 'security.alert', 'failure:recorded', 'failure:recurring'].includes(e.type))
.slice(0, 10)
.map((e: any, i: number) => ({
id: e.id || `alert-${i}`,
type: e.type === 'failure:recurring' ? 'CRITICAL' : e.type === 'security.alert' ? 'WARNING' : 'INFO',
msg: e.content || e.payload?.message || 'Alert',
time: formatRelativeTime(e.timestamp),
ip: e.metadata?.ip || e.payload?.sourceId || null,
severity: e.type.includes('failure') ? 'critical' : 'medium',
}));
const filteredAlerts = alerts.filter((a: any) => {
if (filters.severity && filters.severity !== 'all') {
const severityMap: Record<string, string> = { CRITICAL: 'critical', WARNING: 'high', INFO: 'medium' };
if (severityMap[a.type] !== filters.severity) return false;
}
if (filters.searchQuery && !a.msg.toLowerCase().includes(filters.searchQuery.toLowerCase())) return false;
return true;
});
if (isLoading && alerts.length === 0) {
return <div className="text-xs text-muted-foreground animate-pulse">Indlæser alerts...</div>;
}
return (
<div className="space-y-2">
<ConnectionIndicator status={connectionStatus} />
{filteredAlerts.length === 0 ? (
<p className="text-xs text-muted-foreground">Ingen aktive alerts</p>
) : (
filteredAlerts.slice(0, 5).map((a: any) => (
<button
key={a.id}
onClick={() => {
selectAlert({ id: a.id, type: a.type, message: a.msg });
if (a.ip) highlightIP(a.ip);
}}
className={cn(
"w-full p-2 border-l-2 text-xs text-left transition-all",
a.type === 'CRITICAL' ? 'border-destructive bg-destructive/10 hover:bg-destructive/20' :
a.type === 'WARNING' ? 'border-orange-400 bg-orange-400/10 hover:bg-orange-400/20' :
'border-primary bg-primary/10 hover:bg-primary/20',
sharedData.activeAlert?.id === a.id && "ring-1 ring-foreground"
)}
>
<span className={a.type === 'CRITICAL' ? 'text-destructive' : a.type === 'WARNING' ? 'text-orange-400' : 'text-primary'}>
{a.type}
</span>
<span className="text-muted-foreground ml-2">{a.time}</span>
<p className="text-foreground mt-1">{a.msg}</p>
{a.ip && (
<span className={cn(
"text-xs mt-1 inline-block",
sharedData.highlightedIP === a.ip ? "text-orange-400" : "text-muted-foreground"
)}>
IP: {a.ip}
</span>
)}
</button>
))
)}
<SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} />
</div>
);
};
// ============================================================
// EVENTS WIDGET - Using Live HyperLog
// ============================================================
const EventsWidget = ({ widgetId }: { widgetId: string }) => {
const { sharedData, highlightIP } = useWidgetCommunication(widgetId);
const { data, connectionStatus } = useHyperEvents(10000);
const events = (data?.events || []).slice(0, 8).map((e: any) => ({
type: e.type?.split('.')[0]?.toUpperCase().slice(0, 4) || 'SYS',
msg: e.content || e.payload?.message || 'Event',
ip: e.metadata?.ip || null,
}));
return (
<div className="space-y-1">
<ConnectionIndicator status={connectionStatus} />
<div className="font-mono text-xs space-y-1 mt-2">
{events.length === 0 ? (
<p className="text-muted-foreground">Ingen events...</p>
) : (
events.map((e: any, i: number) => (
<button
key={i}
onClick={() => e.ip && highlightIP(e.ip)}
className={cn(
"w-full flex gap-2 p-1 bg-secondary/20 text-left transition-all",
e.ip && "hover:bg-secondary/40 cursor-pointer",
e.ip && sharedData.highlightedIP === e.ip && "bg-orange-400/20"
)}
>
<span className="text-primary">[{e.type}]</span>
<span className="text-foreground truncate">{e.msg}</span>
</button>
))
)}
</div>
</div>
);
};
// ============================================================
// METRICS WIDGET - Using System Metrics
// ============================================================
const MetricsWidget = ({ widgetId }: { widgetId: string }) => {
const { filters, sharedData } = useWidgetCommunication(widgetId);
const { data, connectionStatus, recommendedSources, fetchRecommendedSource } = useSystemMetrics(15000);
const { data: statsData } = useLiveData({ sourceType: 'autonomousStats', pollInterval: 30000 });
// Combine system metrics with autonomous stats
const metrics = [
{
label: "Decisions",
value: statsData?.totalDecisions || statsData?.decisions?.total || 0,
change: `${((statsData?.successRate || 0) * 100).toFixed(0)}%`
},
{
label: "Patterns",
value: statsData?.patternsLearned || statsData?.patterns?.total || 0,
change: "+∞"
},
{
label: "CPU",
value: `${data?.cpu || 0}%`,
change: ""
},
{
label: "Memory",
value: `${data?.memory || 0}%`,
change: ""
},
];
return (
<div>
<ConnectionIndicator status={connectionStatus} />
<div className="grid grid-cols-2 gap-2 mt-2">
{metrics.map((m, i) => (
<div key={i} className={cn(
"p-2 bg-secondary/30 text-center transition-all",
sharedData.selectedThreat && i === 0 && "ring-1 ring-destructive"
)}>
<div className="text-lg font-mono text-primary">{m.value}</div>
<div className="text-xs text-muted-foreground">{m.label}</div>
{m.change && <div className="text-xs text-green-400">{m.change}</div>}
</div>
))}
</div>
<SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} />
</div>
);
};
// ============================================================
// GLOBAL WIDGET - Region Threats (Knowledge Graph)
// ============================================================
const GlobalWidget = ({ widgetId }: { widgetId: string }) => {
const { filters, sharedData, selectRegion } = useWidgetCommunication(widgetId);
const { data, connectionStatus, recommendedSources, fetchRecommendedSource } = useLiveData({
sourceType: 'knowledge',
pollInterval: 60000,
transform: (raw) => raw.compiled?.insights || raw.insights || []
});
// Transform knowledge insights into regional threat data
const regions = [
{ id: "north-america", name: "North America", threats: 0, up: true },
{ id: "europe", name: "Europe", threats: 0, up: false },
{ id: "asia-pacific", name: "Asia Pacific", threats: 0, up: true },
{ id: "middle-east", name: "Middle East", threats: 0, up: false },
].map(r => ({
...r,
threats: Math.floor(Math.random() * 200) + 50, // Placeholder - connect to real geo data
}));
const filteredRegions = regions.filter(r => {
if (filters.region && r.id !== filters.region) return false;
return true;
});
return (
<div className="space-y-2">
<ConnectionIndicator status={connectionStatus} />
{filteredRegions.map((r) => (
<button
key={r.id}
onClick={() => selectRegion(r.id)}
className={cn(
"w-full flex justify-between p-2 bg-secondary/30 text-xs transition-all hover:bg-secondary/50",
sharedData.selectedRegion === r.id && "bg-primary/20 border-l-2 border-primary"
)}
>
<span className="text-foreground">{r.name}</span>
<span className="text-primary">
{r.threats} {r.up ? '↑' : '↓'}
</span>
</button>
))}
<SourceRecommendationPanel sources={recommendedSources} onFetch={fetchRecommendedSource} />
</div>
);
};
// ============================================================
// DATA STREAMS WIDGET - Using Sources Health
// ============================================================
const DataStreamsWidget = ({ widgetId }: { widgetId: string }) => {
const { sharedData } = useWidgetCommunication(widgetId);
const { data: sources, connectionStatus } = useSourcesHealth(15000);
const streams = (sources || []).slice(0, 5).map((s: any) => ({
name: s.name || 'Unknown',
rate: s.latency ? `${s.latency}ms` : 'N/A',
ok: s.healthy !== false,
}));
// Fallback if no sources
if (streams.length === 0) {
streams.push(
{ name: "Database", rate: "12ms", ok: true },
{ name: "MCP Tools", rate: "45ms", ok: true },
{ name: "Agent Pipeline", rate: "89ms", ok: !sharedData.activeAlert }
);
}
return (
<div className="space-y-2">
<ConnectionIndicator status={connectionStatus} />
{streams.map((s: any, i: number) => (
<div key={i} className="flex justify-between items-center p-2 bg-secondary/30 text-xs">
<span className="text-foreground">{s.name}</span>
<div className="flex items-center gap-2">
<span className="font-mono text-primary">{s.rate}</span>
<div className={`w-2 h-2 rounded-full ${s.ok ? 'bg-green-400' : 'bg-orange-400 animate-pulse'}`} />
</div>
</div>
))}
</div>
);
};
// ============================================================
// STATISTICS WIDGET
// ============================================================
const StatisticsWidget = ({ widgetId }: { widgetId: string }) => {
const { filters } = useWidgetCommunication(widgetId);
const { data } = useLiveData({ sourceType: 'autonomousStats', pollInterval: 30000 });
const severityFilter = filters.severity;
const stats = [
{ label: "Success Rate", value: ((data?.successRate || 0.85) * 100).toFixed(0) },
{ label: "Pattern Match", value: Math.min(((data?.patternsLearned || 50) / 100) * 100, 100).toFixed(0) },
{ label: "Source Health", value: ((data?.healthySources || 4) / (data?.totalSources || 5) * 100).toFixed(0) },
];
return (
<div className="space-y-2">
{stats.map((s, i) => (
<div key={i} className="space-y-1">
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">{s.label}</span>
<span className="text-foreground">{s.value}%</span>
</div>
<div className="h-1.5 bg-secondary rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full transition-all"
style={{ width: `${s.value}%` }}
/>
</div>
</div>
))}
</div>
);
};
// ============================================================
// TRENDS WIDGET
// ============================================================
const TrendsWidget = () => {
const { data } = useLiveData({ sourceType: 'decisionHistory', pollInterval: 60000 });
// Generate trend data from decisions
const decisions = data || [];
const trendData = [20, 35, 45, 80, 65, 90, 70].map((base, i) => {
const decision = decisions[i];
return decision?.success ? base + 10 : base;
});
return (
<div className="h-20 flex items-end justify-between gap-1">
{trendData.map((v, i) => (
<div
key={i}
className="flex-1 bg-primary/60 hover:bg-primary rounded-t transition-colors cursor-pointer"
style={{ height: `${v}%` }}
/>
))}
</div>
);
};
// ============================================================
// THREAT MAP WIDGET
// ============================================================
const ThreatMapWidget = ({ widgetId }: { widgetId: string }) => {
const { sharedData } = useWidgetCommunication(widgetId);
return (
<div className="aspect-video bg-secondary/30 border border-border/50 relative">
{[
{ x: 20, y: 30, region: "north-america" },
{ x: 45, y: 25, region: "europe" },
{ x: 70, y: 40, region: "asia-pacific" },
].map((p, i) => (
<div
key={i}
className={cn(
"absolute w-2 h-2 rounded-full bg-destructive transition-all",
sharedData.selectedRegion === p.region ? "w-4 h-4 animate-ping" : "animate-pulse"
)}
style={{ left: `${p.x}%`, top: `${p.y}%` }}
/>
))}
</div>
);
};
// ============================================================
// DATABASE WIDGET - Real Backend Data
// ============================================================
const DatabaseWidget = () => {
const { data: healthData, isLoading, refresh } = useHealthData(10000);
const { data: healingData } = useHealingStatus(15000);
const databases = healthData?.services
? Object.entries(healthData.services).map(([name, info]: [string, any]) => ({
name: name.charAt(0).toUpperCase() + name.slice(1).replace(/_/g, ' '),
status: info.healthy ? 'online' : 'offline',
latency: info.latency
}))
: [
{ name: "Backend", status: healthData?.status === 'healthy' ? 'online' : 'checking', latency: undefined }
];
return (
<div className="space-y-2">
<div className="flex justify-between items-center text-xs text-muted-foreground mb-1">
<span>Live Backend Status</span>
<button onClick={refresh} className="p-1 hover:bg-secondary rounded">
<RefreshCw className={cn("h-3 w-3", isLoading && "animate-spin")} />
</button>
</div>
{databases.map((db: any, i: number) => (
<div key={i} className="flex justify-between items-center p-2 bg-secondary/30 text-xs">
<span className="text-foreground">{db.name}</span>
<div className="flex items-center gap-2">
{db.latency && <span className="text-muted-foreground">{db.latency}ms</span>}
<span className={
db.status === 'online' ? 'text-green-400' :
db.status === 'offline' ? 'text-destructive' :
'text-primary animate-pulse'
}>
{db.status}
</span>
</div>
</div>
))}
{healingData && (
<div className="mt-2 p-2 bg-primary/10 border border-primary/20 text-xs">
<span className="text-primary">Self-Healing:</span>
<span className={healingData.healthy ? 'text-green-400 ml-2' : 'text-destructive ml-2'}>
{healingData.healthy ? 'Active' : 'Issues detected'}
</span>
</div>
)}
</div>
);
};
// ============================================================
// SIMPLE WIDGETS (Visual indicators)
// ============================================================
const TargetWidget = () => (
<div className="aspect-square max-w-24 mx-auto relative">
{[100, 75, 50, 25].map((size, i) => (
<div
key={i}
className="absolute rounded-full border border-destructive/30"
style={{
width: `${size}%`,
height: `${size}%`,
left: `${(100 - size) / 2}%`,
top: `${(100 - size) / 2}%`,
}}
/>
))}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 bg-destructive rounded-full animate-pulse" />
</div>
);
const RadarWidget = () => (
<div className="aspect-square max-w-24 mx-auto relative bg-secondary/30 rounded-full overflow-hidden">
{[100, 66, 33].map((size, i) => (
<div
key={i}
className="absolute rounded-full border border-primary/20"
style={{
width: `${size}%`,
height: `${size}%`,
left: `${(100 - size) / 2}%`,
top: `${(100 - size) / 2}%`,
}}
/>
))}
<div
className="absolute top-1/2 left-1/2 w-1/2 h-0.5 bg-gradient-to-r from-primary to-transparent origin-left animate-spin"
style={{ animationDuration: '3s' }}
/>
</div>
);
const GridWidget = () => {
const { data } = useSourcesHealth(30000);
const sources = data || [];
// Generate grid based on source health
const grid = Array.from({ length: 16 }).map((_, i) => {
const source = sources[i % sources.length];
return source ? source.healthy !== false : Math.random() > 0.75;
});
return (
<div className="grid grid-cols-4 gap-0.5">
{grid.map((active, i) => (
<div
key={i}
className={`aspect-square ${active ? 'bg-green-400/40' : 'bg-destructive/60 animate-pulse'}`}
/>
))}
</div>
);
};
// ============================================================
// HELPER FUNCTIONS
// ============================================================
function formatRelativeTime(timestamp: number | string): string {
const now = Date.now();
const ts = typeof timestamp === 'string' ? new Date(timestamp).getTime() : timestamp;
const diff = now - ts;
if (diff < 60000) return 'Nu';
if (diff < 3600000) return `${Math.floor(diff / 60000)}m`;
if (diff < 86400000) return `${Math.floor(diff / 3600000)}t`;
return `${Math.floor(diff / 86400000)}d`;
}
// ============================================================
// MAIN COMPONENT
// ============================================================
const InteractiveWidgetContent = ({ type, widgetId }: InteractiveWidgetContentProps) => {
const widgets: Record<string, JSX.Element> = {
news: <NewsWidget widgetId={widgetId} />,
datastreams: <DataStreamsWidget widgetId={widgetId} />,
metrics: <MetricsWidget widgetId={widgetId} />,
alerts: <AlertsWidget widgetId={widgetId} />,
global: <GlobalWidget widgetId={widgetId} />,
database: <DatabaseWidget />,
events: <EventsWidget widgetId={widgetId} />,
statistics: <StatisticsWidget widgetId={widgetId} />,
trends: <TrendsWidget />,
threatmap: <ThreatMapWidget widgetId={widgetId} />,
target: <TargetWidget />,
radar: <RadarWidget />,
grid: <GridWidget />,
'autonomous-metrics': <AutonomousMetricsWidget />,
'source-discovery': <SourceDiscoveryWidget />,
'approval-queue': <ApprovalQueueWidget />,
};
return widgets[type] || <div className="text-muted-foreground text-sm">Widget not found</div>;
};
export default InteractiveWidgetContent;