web / frontend /src /components /modals /SourcesModal.tsx
Chandima Prabhath
feat: Add chat components and modals for enhanced user interaction
1904e4c
raw
history blame
12.7 kB
import { useState, useEffect } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Calendar as CalendarComponent } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { DateRange } from "@/types";
import { RetrievedSource } from "@/services/apiService";
import { storage, STORAGE_KEYS } from "@/lib/storage";
import {
Search,
ArrowUpDown,
Calendar,
FileText,
ChevronDown,
ChevronUp,
ExternalLink
} from "lucide-react";
interface SourcesModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export const SourcesModal = ({ open, onOpenChange }: SourcesModalProps) => {
const [searchTerm, setSearchTerm] = useState("");
const [selectedSource, setSelectedSource] = useState<string>("");
const [selectedCategory, setSelectedCategory] = useState<string>("");
const [date, setDate] = useState<DateRange>({
from: undefined,
to: undefined,
});
const [sortBy, setSortBy] = useState<string>("date-desc");
const [sources, setSources] = useState<RetrievedSource[]>([]);
const [sourceTypes, setSourceTypes] = useState<string[]>([]);
const [categories, setCategories] = useState<string[]>([]);
// Load sources from storage
useEffect(() => {
if (open) {
const storedSources = storage.get<RetrievedSource[]>(STORAGE_KEYS.SOURCES) || [];
setSources(storedSources);
// Extract unique source types and categories
const types = Array.from(new Set(storedSources.map(s => s.metadata?.source).filter(Boolean)));
setSourceTypes(types as string[]);
const cats = Array.from(new Set(storedSources.map(s => {
// For this example, we'll use the first word of the content as a mock category
const firstWord = s.content_snippet.split(' ')[0];
return firstWord.length > 3 ? firstWord : "General";
})));
setCategories(cats);
}
}, [open]);
// Filter sources based on search term, source, category, and date
const filteredSources = sources.filter(source => {
const matchesSearch = !searchTerm ||
source.content_snippet.toLowerCase().includes(searchTerm.toLowerCase()) ||
(source.metadata?.source || "").toLowerCase().includes(searchTerm.toLowerCase());
const matchesSource = !selectedSource || selectedSource === "All" || source.metadata?.source === selectedSource;
// Mock category matching based on first word of content
const sourceCategory = source.content_snippet.split(' ')[0].length > 3 ?
source.content_snippet.split(' ')[0] : "General";
const matchesCategory = !selectedCategory || selectedCategory === "All" || sourceCategory === selectedCategory;
let matchesDate = true;
if (date.from && source.metadata?.ruling_date) {
matchesDate = matchesDate && new Date(source.metadata.ruling_date) >= date.from;
}
if (date.to && source.metadata?.ruling_date) {
matchesDate = matchesDate && new Date(source.metadata.ruling_date) <= date.to;
}
return matchesSearch && matchesSource && matchesCategory && matchesDate;
});
// Sort sources
const sortedSources = [...filteredSources].sort((a, b) => {
if (sortBy === "date-desc") {
return new Date(b.metadata?.ruling_date || "").getTime() -
new Date(a.metadata?.ruling_date || "").getTime();
} else if (sortBy === "date-asc") {
return new Date(a.metadata?.ruling_date || "").getTime() -
new Date(b.metadata?.ruling_date || "").getTime();
} else if (sortBy === "relevance-desc") {
return b.content_snippet.length - a.content_snippet.length;
}
return 0;
});
const resetFilters = () => {
setSearchTerm("");
setSelectedSource("");
setSelectedCategory("");
setDate({ from: undefined, to: undefined });
};
const [expandedSources, setExpandedSources] = useState<Record<number, boolean>>({});
const toggleSource = (index: number) => {
setExpandedSources(prev => ({
...prev,
[index]: !prev[index]
}));
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-full max-w-4xl">
<DialogHeader>
<DialogTitle className="flex items-center text-xl">
<FileText className="mr-2 h-5 w-5" />
Knowledge Sources
</DialogTitle>
</DialogHeader>
<div>
<div className="flex flex-col md:flex-row md:items-center justify-between mb-2">
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2">
<Select value={sortBy} onValueChange={setSortBy}>
<SelectTrigger className="w-[180px]">
<div className="flex items-center">
<ArrowUpDown className="mr-2 h-3.5 w-3.5 text-muted-foreground" />
<span>Sort By</span>
</div>
</SelectTrigger>
<SelectContent>
<SelectItem value="date-desc">Date (Newest)</SelectItem>
<SelectItem value="date-asc">Date (Oldest)</SelectItem>
<SelectItem value="relevance-desc">Relevance</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="bg-accent/30 rounded-lg p-1 mb-2">
<div className="flex flex-col md:flex-row space-y-1 md:space-y-0 md:space-x-2">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
type="text"
placeholder="Search sources..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Select value={selectedSource} onValueChange={setSelectedSource}>
<SelectTrigger className="w-full md:w-[180px]">
<span className="truncate">
{selectedSource || "All Sources"}
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Sources</SelectItem>
{sourceTypes.map(type => (
<SelectItem key={type} value={type}>{type}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
<SelectTrigger className="w-full md:w-[180px]">
<span className="truncate">
{selectedCategory || "All Categories"}
</span>
</SelectTrigger>
<SelectContent>
<SelectItem value="All">All Categories</SelectItem>
{categories.map(category => (
<SelectItem key={category} value={category}>{category}</SelectItem>
))}
</SelectContent>
</Select>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-full md:w-[180px] justify-start text-left">
<Calendar className="mr-2 h-4 w-4" />
<span>
{date.from || date.to ? (
<>
{date.from ? format(date.from, "LLL dd, y") : "From"} - {" "}
{date.to ? format(date.to, "LLL dd, y") : "To"}
</>
) : (
"Date Range"
)}
</span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<CalendarComponent
mode="range"
selected={date}
onSelect={(value: DateRange | undefined) => {
if (value) setDate(value);
}}
className="p-3"
/>
</PopoverContent>
</Popover>
<Button
variant="outline"
className="md:w-auto"
onClick={resetFilters}
>
Reset
</Button>
</div>
</div>
<div className="max-h-[45dvh] md:max-h-[60vh] overflow-y-auto space-y-4">
{sortedSources.length > 0 ? (
sortedSources.map((source, index) => (
<div key={index} className="border rounded-lg overflow-hidden transition-all duration-300">
<div className="p-4 bg-card">
<div className="flex items-start justify-between">
<div>
<h3 className="font-medium">
{source.metadata?.source || "Source"}
</h3>
<div className="flex items-center mt-1 text-sm text-muted-foreground">
<FileText className="h-3.5 w-3.5 mr-1.5" />
<span>{source.metadata?.source || "Unknown Source"}</span>
{source.metadata?.ruling_date && (
<>
<span className="mx-1.5"></span>
<Calendar className="h-3.5 w-3.5 mr-1.5" />
<span>{new Date(source.metadata.ruling_date).toLocaleDateString()}</span>
</>
)}
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => toggleSource(index)}
className="p-0 h-8 w-8 hover:bg-accent/10"
>
{expandedSources[index] ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</div>
<div className={expandedSources[index] ? "" : "max-h-10 md:max-h-16 overflow-hidden relative"}>
<p className="text-sm text-muted-foreground mt-2">
{source.content_snippet}
</p>
{!expandedSources[index] && (
<div className="absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-t from-card to-transparent"></div>
)}
</div>
{expandedSources[index] && (
<div className="mt-4 text-sm flex justify-end">
<Button variant="link" size="sm" className="h-8 p-0 text-primary">
<ExternalLink className="h-3 w-3 mr-1" />
View source
</Button>
</div>
)}
</div>
</div>
))
) : (
<div className="text-center p-8">
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-muted/30 flex items-center justify-center">
<FileText className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium mb-2">No sources found</h3>
<p className="text-muted-foreground">
{sources.length > 0
? "No sources match your current search criteria. Try adjusting your filters."
: "Chat with Insight AI to get information with source citations."}
</p>
<Button
variant="outline"
className="mt-4"
onClick={resetFilters}
>
Reset Filters
</Button>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};