Perplexica/src/components/dashboard/WidgetDisplay.tsx
Willie Zutz 1228beb59a feat(dashboard): Implement Widget Configuration and Display Components
- Added WidgetConfigModal for creating and editing widgets with fields for title, sources, prompt, provider, model, and refresh frequency.
- Integrated MarkdownRenderer for displaying widget content previews.
- Created WidgetDisplay component to show widget details, including loading states, error handling, and source information.
- Developed a reusable Card component structure for consistent UI presentation.
- Introduced useDashboard hook for managing widget state, including adding, updating, deleting, and refreshing widgets.
- Implemented local storage management for dashboard state and settings.
- Added types for widgets, dashboard configuration, and API requests/responses to improve type safety and clarity.
2025-07-19 08:23:06 -06:00

179 lines
6.5 KiB
TypeScript

'use client';
import { RefreshCw, Edit, Trash2, Clock, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import MarkdownRenderer from '@/components/MarkdownRenderer';
import { useState } from 'react';
interface Source {
url: string;
type: 'Web Page' | 'HTTP Data';
}
interface Widget {
id: string;
title: string;
sources: Source[];
prompt: string;
provider: string;
model: string;
refreshFrequency: number;
refreshUnit: 'minutes' | 'hours';
lastUpdated: Date | null;
isLoading: boolean;
content: string | null;
error: string | null;
}
interface WidgetDisplayProps {
widget: Widget;
onEdit: (widget: Widget) => void;
onDelete: (widgetId: string) => void;
onRefresh: (widgetId: string) => void;
}
const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayProps) => {
const [isFooterExpanded, setIsFooterExpanded] = useState(false);
const formatLastUpdated = (date: Date | null) => {
if (!date) return 'Never';
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
return `${diffDays}d ago`;
};
const getRefreshFrequencyText = () => {
return `Every ${widget.refreshFrequency} ${widget.refreshUnit}`;
};
return (
<Card className="flex flex-col h-fit">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-medium truncate">
{widget.title}
</CardTitle>
<div className="flex items-center space-x-2">
{/* Last updated date with refresh frequency tooltip */}
<span
className="text-xs text-gray-500 dark:text-gray-400"
title={getRefreshFrequencyText()}
>
{formatLastUpdated(widget.lastUpdated)}
</span>
{/* Refresh button */}
<button
onClick={() => onRefresh(widget.id)}
disabled={widget.isLoading}
className="p-1.5 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded transition-colors disabled:opacity-50"
title="Refresh Widget"
>
<RefreshCw
size={16}
className={`text-gray-600 dark:text-gray-400 ${widget.isLoading ? 'animate-spin' : ''}`}
/>
</button>
</div>
</div>
</CardHeader>
<CardContent className="flex-1">
{widget.isLoading ? (
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<RefreshCw size={20} className="animate-spin mr-2" />
<span>Loading content...</span>
</div>
) : widget.error ? (
<div className="flex items-start space-x-2 p-3 bg-red-50 dark:bg-red-900/20 rounded border border-red-200 dark:border-red-800">
<AlertCircle size={16} className="text-red-500 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-red-800 dark:text-red-300">Error Loading Content</p>
<p className="text-xs text-red-600 dark:text-red-400 mt-1">{widget.error}</p>
</div>
</div>
) : widget.content ? (
<div className="prose prose-sm dark:prose-invert max-w-none">
<MarkdownRenderer content={widget.content} thinkOverlay={true} />
</div>
) : (
<div className="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
<div className="text-center">
<p className="text-sm">No content yet</p>
<p className="text-xs mt-1">Click refresh to load content</p>
</div>
</div>
)}
</CardContent>
{/* Collapsible footer with sources and actions */}
<div className="bg-light-secondary/30 dark:bg-dark-secondary/30">
<button
onClick={() => setIsFooterExpanded(!isFooterExpanded)}
className="w-full px-4 py-2 flex items-center space-x-2 text-xs text-gray-500 dark:text-gray-400 hover:bg-light-secondary dark:hover:bg-dark-secondary transition-colors"
>
{isFooterExpanded ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)}
<span>Sources & Actions</span>
</button>
{isFooterExpanded && (
<div className="px-4 pb-4 space-y-3">
{/* Sources */}
{widget.sources.length > 0 && (
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Sources:</p>
<div className="space-y-1">
{widget.sources.map((source, index) => (
<div key={index} className="flex items-center space-x-2 text-xs">
<span className="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
<span className="text-gray-600 dark:text-gray-300 truncate">
{source.url}
</span>
<span className="text-gray-400 dark:text-gray-500">
({source.type})
</span>
</div>
))}
</div>
</div>
)}
{/* Action buttons */}
<div className="flex items-center space-x-2 pt-2">
<button
onClick={() => onEdit(widget)}
className="flex items-center space-x-1 px-2 py-1 text-xs text-gray-600 dark:text-gray-400 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded transition-colors"
>
<Edit size={12} />
<span>Edit</span>
</button>
<button
onClick={() => onDelete(widget.id)}
className="flex items-center space-x-1 px-2 py-1 text-xs text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
>
<Trash2 size={12} />
<span>Delete</span>
</button>
</div>
</div>
)}
</div>
</Card>
);
};
export default WidgetDisplay;