baveshraam's picture
FIX: SurrealDB 2.0 migration syntax and Frontend/CORS link
f871fed
'use client';
/**
* Add Monitor Dialog Component
*
* Dialog for selecting sources to monitor for updates.
*/
import { useState, useEffect } from 'react';
import { Search, Globe, FileText, Check, Clock, Zap } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useCreateMonitor, useMonitors } from '@/lib/hooks/use-monitoring';
import { Checkbox } from '@/components/ui/checkbox';
interface AddMonitorDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
interface SourceAsset {
file_path?: string;
url?: string;
}
interface SourceItem {
id: string;
title: string;
source_type?: string;
asset_type?: string;
asset?: SourceAsset;
url?: string;
}
export function AddMonitorDialog({ open, onOpenChange }: AddMonitorDialogProps) {
const [sources, setSources] = useState<SourceItem[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [selectedSources, setSelectedSources] = useState<Set<string>>(new Set());
const [frequency, setFrequency] = useState<'hourly' | 'daily' | 'weekly'>('daily');
const createMonitor = useCreateMonitor();
const { data: existingMonitors } = useMonitors();
// Fetch available sources
useEffect(() => {
if (open) {
setLoading(true);
fetch('/api/sources?limit=100')
.then(res => res.json())
.then(data => {
setSources(data || []);
setLoading(false);
})
.catch(err => {
console.error('Failed to fetch sources:', err);
setLoading(false);
});
}
}, [open]);
// Filter sources that can be monitored (web sources) and not already monitored
const monitoredSourceIds = new Set(existingMonitors?.map(m => m.source_id) || []);
const availableSources = sources.filter(source => {
// Only web-based sources can be monitored (has a URL)
const sourceUrl = source.asset?.url || source.url;
const isWebSource = source.source_type === 'url' ||
source.asset_type === 'web' ||
(sourceUrl && sourceUrl.length > 0);
// Check if already being monitored
const isAlreadyMonitored = monitoredSourceIds.has(source.id) ||
monitoredSourceIds.has(`source:${source.id.split(':')[1]}`);
// Apply search filter
const matchesSearch = !searchQuery ||
source.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
sourceUrl?.toLowerCase().includes(searchQuery.toLowerCase());
return isWebSource && !isAlreadyMonitored && matchesSearch;
});
const handleToggleSource = (sourceId: string) => {
const newSelected = new Set(selectedSources);
if (newSelected.has(sourceId)) {
newSelected.delete(sourceId);
} else {
newSelected.add(sourceId);
}
setSelectedSources(newSelected);
};
const handleSelectAll = () => {
if (selectedSources.size === availableSources.length) {
setSelectedSources(new Set());
} else {
setSelectedSources(new Set(availableSources.map(s => s.id)));
}
};
const handleAddMonitors = async () => {
const sourceIds = Array.from(selectedSources);
for (const sourceId of sourceIds) {
await createMonitor.mutateAsync({
source_id: sourceId,
check_frequency: frequency,
enabled: true,
});
}
setSelectedSources(new Set());
onOpenChange(false);
};
const getSourceIcon = (source: SourceItem) => {
const hasUrl = source.source_type === 'url' || source.asset?.url || source.url;
if (hasUrl) {
return <Globe className="h-4 w-4 text-blue-500" />;
}
return <FileText className="h-4 w-4 text-muted-foreground" />;
};
const getSourceUrl = (source: SourceItem) => {
return source.asset?.url || source.url || '';
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Zap className="h-5 w-5 text-primary" />
Add Source Monitors
</DialogTitle>
<DialogDescription>
Select web sources to monitor for changes. You'll be notified when content updates.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Search and Frequency */}
<div className="flex gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search sources..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<Select value={frequency} onValueChange={(v) => setFrequency(v as any)}>
<SelectTrigger className="w-40">
<Clock className="h-4 w-4 mr-2" />
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hourly">Every hour</SelectItem>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
</SelectContent>
</Select>
</div>
{/* Source List */}
<ScrollArea className="h-[300px] border rounded-md">
{loading ? (
<div className="p-8 text-center text-muted-foreground">
<div className="animate-pulse">Loading sources...</div>
</div>
) : availableSources.length === 0 ? (
<div className="p-8 text-center text-muted-foreground">
<Globe className="h-12 w-12 mx-auto mb-4 opacity-20" />
<p className="font-medium">No web sources available</p>
<p className="text-sm mt-1">
{sources.length === 0
? 'Add some web sources to your notebooks first'
: 'All web sources are already being monitored'}
</p>
</div>
) : (
<div className="p-2">
{/* Select All Header */}
<div className="flex items-center gap-3 p-2 border-b mb-2">
<Checkbox
checked={selectedSources.size === availableSources.length && availableSources.length > 0}
onCheckedChange={handleSelectAll}
/>
<span className="text-sm font-medium">
Select All ({availableSources.length} sources)
</span>
</div>
{/* Source Items */}
{availableSources.map((source) => {
const sourceUrl = getSourceUrl(source);
return (
<div
key={source.id}
className={`flex items-center gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
selectedSources.has(source.id)
? 'bg-primary/10 border border-primary/30'
: 'hover:bg-muted'
}`}
onClick={() => handleToggleSource(source.id)}
>
<Checkbox
checked={selectedSources.has(source.id)}
onCheckedChange={() => handleToggleSource(source.id)}
/>
<div className="flex-shrink-0">
{getSourceIcon(source)}
</div>
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{source.title || 'Untitled Source'}</p>
{sourceUrl && (
<p className="text-xs text-muted-foreground truncate">{sourceUrl}</p>
)}
</div>
{selectedSources.has(source.id) && (
<Check className="h-4 w-4 text-primary flex-shrink-0" />
)}
</div>
);
})}
</div>
)}
</ScrollArea>
{/* Footer */}
<div className="flex items-center justify-between pt-4 border-t">
<div className="text-sm text-muted-foreground">
{selectedSources.size > 0 ? (
<span>{selectedSources.size} source{selectedSources.size > 1 ? 's' : ''} selected</span>
) : (
<span>Select sources to monitor</span>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button
onClick={handleAddMonitors}
disabled={selectedSources.size === 0 || createMonitor.isPending}
>
{createMonitor.isPending ? (
<>Adding...</>
) : (
<>
<Zap className="h-4 w-4 mr-2" />
Add {selectedSources.size > 0 ? selectedSources.size : ''} Monitor{selectedSources.size > 1 ? 's' : ''}
</>
)}
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}