widgettdc-api / apps /matrix-frontend /src /widgets /SourceDiscoveryWidget.tsx
Kraft102's picture
fix: sql.js Docker/Alpine compatibility layer for PatternMemory and FailureMemory
5a81b95
/**
* SourceDiscoveryWidget - Displays orphan sources and allows widget generation
*
* This widget enables the reverse flow: Source → Widget
* Shows data sources that don't have matching widgets and offers to generate them.
*/
import React, { useState } from 'react';
import {
Database, Zap, Globe, AlertTriangle, Plus, RefreshCw,
Check, Loader2, ChevronRight, Sparkles, Link2
} from 'lucide-react';
import { useSourceDiscovery, useGeneratedWidgets } from '@/services/SourceWidgetDiscovery';
import { cn } from '@/lib/utils';
// Icon mapping for source types
const sourceTypeIcons: Record<string, React.ReactNode> = {
database: <Database className="w-4 h-4" />,
'mcp-tool': <Zap className="w-4 h-4" />,
file: <Database className="w-4 h-4" />,
'email-adapter': <AlertTriangle className="w-4 h-4" />,
external: <Globe className="w-4 h-4" />,
osint: <Globe className="w-4 h-4" />,
threatIntel: <AlertTriangle className="w-4 h-4" />,
};
export default function SourceDiscoveryWidget() {
const {
sources,
orphanSources,
suggestedWidgets,
isLoading,
refresh,
generateWidget
} = useSourceDiscovery();
const { generatedWidgets } = useGeneratedWidgets();
const [generatingFor, setGeneratingFor] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'orphans' | 'all' | 'generated'>('orphans');
const handleGenerateWidget = async (sourceId: string, suggestedName: string) => {
setGeneratingFor(sourceId);
try {
await generateWidget(sourceId, suggestedName);
} finally {
setGeneratingFor(null);
}
};
return (
<div className="h-full flex flex-col bg-card/50 backdrop-blur-sm border border-border/50 rounded-lg overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border/30 bg-gradient-to-r from-primary/10 to-transparent">
<div className="flex items-center gap-2">
<Link2 className="w-5 h-5 text-primary" />
<span className="font-display text-sm uppercase tracking-wider text-primary">
Source Discovery
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{sources.length} kilder • {orphanSources.length} uden widget
</span>
<button
onClick={refresh}
className="p-1.5 hover:bg-secondary/50 rounded transition-colors"
disabled={isLoading}
>
<RefreshCw className={cn("w-4 h-4 text-primary", isLoading && "animate-spin")} />
</button>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-border/30">
{[
{ id: 'orphans', label: 'Orphan Kilder', count: orphanSources.length },
{ id: 'all', label: 'Alle Kilder', count: sources.length },
{ id: 'generated', label: 'Genererede', count: generatedWidgets.length },
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as any)}
className={cn(
"flex-1 px-4 py-2 text-xs font-medium transition-colors",
activeTab === tab.id
? "text-primary border-b-2 border-primary bg-primary/5"
: "text-muted-foreground hover:text-foreground"
)}
>
{tab.label}
{tab.count > 0 && (
<span className="ml-1.5 px-1.5 py-0.5 bg-primary/20 text-primary rounded text-xs">
{tab.count}
</span>
)}
</button>
))}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4 space-y-3">
{isLoading ? (
<div className="flex items-center justify-center h-32">
<Loader2 className="w-6 h-6 text-primary animate-spin" />
</div>
) : activeTab === 'orphans' ? (
orphanSources.length === 0 ? (
<div className="text-center py-8">
<Check className="w-8 h-8 text-green-400 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">Alle kilder har widgets!</p>
</div>
) : (
orphanSources.map(source => {
const suggested = suggestedWidgets.find(s => s.forSource === source.id);
return (
<div
key={source.id}
className="p-3 bg-secondary/30 border border-border/30 rounded-lg hover:border-primary/30 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-2">
<div className="p-2 bg-primary/10 rounded">
{sourceTypeIcons[source.type] || <Database className="w-4 h-4" />}
</div>
<div>
<p className="text-sm font-medium text-foreground">{source.name}</p>
<p className="text-xs text-muted-foreground">{source.type}</p>
</div>
</div>
<button
onClick={() => handleGenerateWidget(source.id, suggested?.suggestedName || `${source.name} Widget`)}
disabled={generatingFor === source.id}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-medium transition-all",
"bg-primary/20 text-primary hover:bg-primary/30",
generatingFor === source.id && "opacity-50 cursor-not-allowed"
)}
>
{generatingFor === source.id ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Sparkles className="w-3 h-3" />
)}
Generer Widget
</button>
</div>
{/* Capabilities */}
<div className="mt-2 flex flex-wrap gap-1">
{source.capabilities.slice(0, 4).map((cap, i) => (
<span key={i} className="px-1.5 py-0.5 bg-secondary/50 text-muted-foreground text-xs rounded">
{cap}
</span>
))}
{source.capabilities.length > 4 && (
<span className="px-1.5 py-0.5 text-muted-foreground text-xs">
+{source.capabilities.length - 4} flere
</span>
)}
</div>
{/* Suggested widget info */}
{suggested && (
<div className="mt-2 flex items-center gap-2 text-xs text-primary/70">
<ChevronRight className="w-3 h-3" />
Foreslået: <span className="font-medium">{suggested.suggestedName}</span>
</div>
)}
</div>
);
})
)
) : activeTab === 'all' ? (
sources.map(source => (
<div
key={source.id}
className="p-3 bg-secondary/20 border border-border/20 rounded-lg"
>
<div className="flex items-center gap-2">
<div className="p-1.5 bg-secondary/50 rounded">
{sourceTypeIcons[source.type] || <Database className="w-3 h-3" />}
</div>
<div className="flex-1">
<p className="text-sm text-foreground">{source.name}</p>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{source.type}</span>
<span></span>
<span>{source.estimatedLatency}ms</span>
{source.recommendedWidgets.length > 0 && (
<>
<span></span>
<span className="text-primary">{source.recommendedWidgets[0]}</span>
</>
)}
</div>
</div>
{source.hasWidget ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<span className="text-xs text-orange-400">Ingen widget</span>
)}
</div>
</div>
))
) : (
/* Generated widgets tab */
generatedWidgets.length === 0 ? (
<div className="text-center py-8">
<Plus className="w-8 h-8 text-muted-foreground mx-auto mb-2" />
<p className="text-sm text-muted-foreground">Ingen genererede widgets endnu</p>
</div>
) : (
generatedWidgets.map((widget, i) => (
<div
key={i}
className="p-3 bg-gradient-to-r from-primary/10 to-transparent border border-primary/20 rounded-lg"
>
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4 text-primary" />
<div>
<p className="text-sm font-medium text-foreground">{widget.name}</p>
<p className="text-xs text-muted-foreground">
For: {widget.dataSource} • Template: {widget.template}
</p>
</div>
</div>
<div className="mt-2 flex flex-wrap gap-1">
{widget.capabilities.slice(0, 3).map((cap, j) => (
<span key={j} className="px-1.5 py-0.5 bg-primary/10 text-primary text-xs rounded">
{cap}
</span>
))}
</div>
</div>
))
)
)}
</div>
{/* Footer */}
<div className="p-3 border-t border-border/30 bg-secondary/20">
<p className="text-xs text-muted-foreground text-center">
Autonomt system lærer fra kilde↔widget koblinger
</p>
</div>
</div>
);
}