fix(refactor): Cleanup components for improved readability and consistency

This commit is contained in:
Willie Zutz 2025-07-19 11:34:56 -06:00
parent 1228beb59a
commit 1b0c2c59b8
10 changed files with 590 additions and 350 deletions

View file

@ -1,5 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { getWebContent, getWebContentLite } from '@/lib/utils/documents';
import { Document } from '@langchain/core/documents';
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
import { ChatOpenAI } from '@langchain/openai';
import { HumanMessage } from '@langchain/core/messages';
@ -10,6 +11,7 @@ import {
getCustomOpenaiModelName,
} from '@/lib/config';
import { ChatOllama } from '@langchain/ollama';
import axios from 'axios';
interface Source {
url: string;
@ -24,56 +26,71 @@ interface WidgetProcessRequest {
}
// Helper function to fetch content from a single source
async function fetchSourceContent(source: Source): Promise<{ content: string; error?: string }> {
async function fetchSourceContent(
source: Source,
): Promise<{ content: string; error?: string }> {
try {
let document;
if (source.type === 'Web Page') {
// Use headless browser for complex web pages
document = await getWebContent(source.url);
} else {
// Use faster fetch for HTTP data/APIs
document = await getWebContentLite(source.url);
const response = await axios.get(source.url, { transformResponse: [] });
document = new Document({
pageContent: response.data || '',
metadata: { source: source.url },
});
}
if (!document) {
return {
content: '',
error: `Failed to fetch content from ${source.url}`
return {
content: '',
error: `Failed to fetch content from ${source.url}`,
};
}
return { content: document.pageContent };
} catch (error) {
console.error(`Error fetching content from ${source.url}:`, error);
return {
content: '',
error: `Error fetching ${source.url}: ${error instanceof Error ? error.message : 'Unknown error'}`
return {
content: '',
error: `Error fetching ${source.url}: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
// Helper function to replace variables in prompt
function replacePromptVariables(prompt: string, sourceContents: string[], location?: string): string {
function replacePromptVariables(
prompt: string,
sourceContents: string[],
location?: string,
): string {
let processedPrompt = prompt;
// Replace source content variables
sourceContents.forEach((content, index) => {
const variable = `{{source_content_${index + 1}}}`;
processedPrompt = processedPrompt.replace(new RegExp(variable, 'g'), content);
processedPrompt = processedPrompt.replace(
new RegExp(variable, 'g'),
content,
);
});
// Replace location if provided
if (location) {
processedPrompt = processedPrompt.replace(/\{\{location\}\}/g, location);
}
return processedPrompt;
}
// Helper function to get LLM instance based on provider and model
async function getLLMInstance(provider: string, model: string): Promise<BaseChatModel | null> {
async function getLLMInstance(
provider: string,
model: string,
): Promise<BaseChatModel | null> {
try {
const chatModelProviders = await getAvailableChatModelProviders();
@ -89,12 +106,12 @@ async function getLLMInstance(provider: string, model: string): Promise<BaseChat
if (chatModelProviders[provider] && chatModelProviders[provider][model]) {
const llm = chatModelProviders[provider][model].model as BaseChatModel;
// Special handling for Ollama models
if (llm instanceof ChatOllama && provider === 'ollama') {
llm.numCtx = 2048; // Default context window
}
return llm;
}
@ -106,28 +123,32 @@ async function getLLMInstance(provider: string, model: string): Promise<BaseChat
}
// Helper function to process the prompt with LLM
async function processWithLLM(prompt: string, provider: string, model: string): Promise<string> {
async function processWithLLM(
prompt: string,
provider: string,
model: string,
): Promise<string> {
const llm = await getLLMInstance(provider, model);
if (!llm) {
throw new Error(`Invalid or unavailable model: ${provider}/${model}`);
}
const message = new HumanMessage({ content: prompt });
const response = await llm.invoke([message]);
return response.content as string;
}
export async function POST(request: NextRequest) {
try {
const body: WidgetProcessRequest = await request.json();
// Validate required fields
if (!body.sources || !body.prompt || !body.provider || !body.model) {
return NextResponse.json(
{ error: 'Missing required fields: sources, prompt, provider, model' },
{ status: 400 }
{ status: 400 },
);
}
@ -135,19 +156,21 @@ export async function POST(request: NextRequest) {
if (!Array.isArray(body.sources) || body.sources.length === 0) {
return NextResponse.json(
{ error: 'At least one source URL is required' },
{ status: 400 }
{ status: 400 },
);
}
// Fetch content from all sources
console.log(`Processing widget with ${body.sources.length} source(s)`);
const sourceResults = await Promise.all(
body.sources.map(source => fetchSourceContent(source))
body.sources.map((source) => fetchSourceContent(source)),
);
// Check for fetch errors
const fetchErrors = sourceResults
.map((result, index) => result.error ? `Source ${index + 1}: ${result.error}` : null)
.map((result, index) =>
result.error ? `Source ${index + 1}: ${result.error}` : null,
)
.filter(Boolean);
if (fetchErrors.length > 0) {
@ -155,35 +178,40 @@ export async function POST(request: NextRequest) {
}
// Extract successful content
const sourceContents = sourceResults.map(result => result.content);
const sourceContents = sourceResults.map((result) => result.content);
// If all sources failed, return error
if (sourceContents.every(content => !content)) {
if (sourceContents.every((content) => !content)) {
return NextResponse.json(
{ error: 'Failed to fetch content from all sources' },
{ status: 500 }
{ status: 500 },
);
}
// Replace variables in prompt
const processedPrompt = replacePromptVariables(body.prompt, sourceContents);
console.log('Processed prompt:', processedPrompt.substring(0, 200) + '...');
console.log('Processing prompt:', processedPrompt);
// Process with LLM
try {
const llmResponse = await processWithLLM(processedPrompt, body.provider, body.model);
const llmResponse = await processWithLLM(
processedPrompt,
body.provider,
body.model,
);
console.log('LLM response:', llmResponse);
return NextResponse.json({
content: llmResponse,
success: true,
sourcesFetched: sourceContents.filter(content => content).length,
sourcesFetched: sourceContents.filter((content) => content).length,
totalSources: body.sources.length,
warnings: fetchErrors.length > 0 ? fetchErrors : undefined
warnings: fetchErrors.length > 0 ? fetchErrors : undefined,
});
} catch (llmError) {
console.error('LLM processing failed:', llmError);
// Return diagnostic information if LLM fails
const diagnosticResponse = `# Widget Processing - LLM Error
@ -193,24 +221,26 @@ export async function POST(request: NextRequest) {
${processedPrompt}
## Sources Successfully Fetched
${sourceContents.filter(content => content).length} of ${body.sources.length} sources
${sourceContents.filter((content) => content).length} of ${body.sources.length} sources
${fetchErrors.length > 0 ? `## Source Errors\n${fetchErrors.join('\n')}` : ''}`;
return NextResponse.json({
content: diagnosticResponse,
success: false,
error: llmError instanceof Error ? llmError.message : 'LLM processing failed',
sourcesFetched: sourceContents.filter(content => content).length,
totalSources: body.sources.length
error:
llmError instanceof Error
? llmError.message
: 'LLM processing failed',
sourcesFetched: sourceContents.filter((content) => content).length,
totalSources: body.sources.length,
});
}
} catch (error) {
console.error('Error processing widget:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
{ status: 500 },
);
}
}

View file

@ -1,12 +1,28 @@
'use client';
import { Plus, RefreshCw, Download, Upload, LayoutDashboard, Layers, List } from 'lucide-react';
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import {
Plus,
RefreshCw,
Download,
Upload,
LayoutDashboard,
Layers,
List,
} from 'lucide-react';
import { useState, useEffect, useRef } from 'react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import WidgetConfigModal from '@/components/dashboard/WidgetConfigModal';
import WidgetDisplay from '@/components/dashboard/WidgetDisplay';
import { useDashboard } from '@/lib/hooks/useDashboard';
import { Widget, WidgetConfig } from '@/lib/types';
import { toast } from 'sonner';
const DashboardPage = () => {
const {
@ -24,7 +40,19 @@ const DashboardPage = () => {
} = useDashboard();
const [showAddModal, setShowAddModal] = useState(false);
const [editingWidget, setEditingWidget] = useState<Widget | null>(null); const handleAddWidget = () => {
const [editingWidget, setEditingWidget] = useState<Widget | null>(null);
const hasAutoRefreshed = useRef(false);
// Auto-refresh stale widgets when dashboard loads (only once)
useEffect(() => {
if (!isLoading && widgets.length > 0 && !hasAutoRefreshed.current) {
hasAutoRefreshed.current = true;
refreshAllWidgets();
}
}, [isLoading, widgets, refreshAllWidgets]);
const handleAddWidget = () => {
setEditingWidget(null);
setShowAddModal(true);
};
@ -60,18 +88,18 @@ const DashboardPage = () => {
};
const handleRefreshAll = () => {
refreshAllWidgets();
refreshAllWidgets(true);
};
const handleExport = async () => {
try {
const configJson = await exportDashboard();
await navigator.clipboard.writeText(configJson);
// TODO: Add toast notification for success
toast.success('Dashboard configuration copied to clipboard');
console.log('Dashboard configuration copied to clipboard');
} catch (error) {
console.error('Export failed:', error);
// TODO: Add toast notification for error
toast.error('Failed to copy dashboard configuration');
}
};
@ -79,11 +107,11 @@ const DashboardPage = () => {
try {
const configJson = await navigator.clipboard.readText();
await importDashboard(configJson);
// TODO: Add toast notification for success
toast.success('Dashboard configuration imported successfully');
console.log('Dashboard configuration imported successfully');
} catch (error) {
console.error('Import failed:', error);
// TODO: Add toast notification for error
toast.error('Failed to import dashboard configuration');
}
};
@ -98,18 +126,20 @@ const DashboardPage = () => {
<CardHeader>
<CardTitle>Welcome to your Dashboard</CardTitle>
<CardDescription>
Create your first widget to get started with personalized information
Create your first widget to get started with personalized
information
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
Widgets let you fetch content from any URL and process it with AI to show exactly what you need.
Widgets let you fetch content from any URL and process it with AI to
show exactly what you need.
</p>
</CardContent>
<CardFooter className="justify-center">
<button
<button
onClick={handleAddWidget}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition duration-200 flex items-center space-x-2"
>
@ -130,7 +160,7 @@ const DashboardPage = () => {
<LayoutDashboard />
<h1 className="text-3xl font-medium p-2">Dashboard</h1>
</div>
<div className="flex items-center space-x-2">
<button
onClick={handleRefreshAll}
@ -139,7 +169,7 @@ const DashboardPage = () => {
>
<RefreshCw size={18} className="text-black dark:text-white" />
</button>
<button
onClick={handleToggleProcessingMode}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
@ -151,7 +181,7 @@ const DashboardPage = () => {
<List size={18} className="text-black dark:text-white" />
)}
</button>
<button
onClick={handleExport}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
@ -159,7 +189,7 @@ const DashboardPage = () => {
>
<Download size={18} className="text-black dark:text-white" />
</button>
<button
onClick={handleImport}
className="p-2 hover:bg-light-secondary dark:hover:bg-dark-secondary rounded-lg transition duration-200"
@ -167,7 +197,7 @@ const DashboardPage = () => {
>
<Upload size={18} className="text-black dark:text-white" />
</button>
<button
onClick={handleAddWidget}
className="p-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition duration-200"
@ -186,7 +216,9 @@ const DashboardPage = () => {
<div className="flex items-center justify-center py-12">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">Loading dashboard...</p>
<p className="text-gray-500 dark:text-gray-400">
Loading dashboard...
</p>
</div>
</div>
) : widgets.length === 0 ? (

View file

@ -14,12 +14,12 @@ const extractThinkContent = (content: string): string | null => {
const thinkRegex = /<think>([\s\S]*?)<\/think>/g;
const matches = content.match(thinkRegex);
if (!matches) return null;
// Extract content between think tags and join if multiple
const extractedContent = matches
.map(match => match.replace(/<\/?think>/g, ''))
.map((match) => match.replace(/<\/?think>/g, ''))
.join('\n\n');
// Return null if content is empty or only whitespace
return extractedContent.trim().length === 0 ? null : extractedContent;
};
@ -28,10 +28,10 @@ const removeThinkTags = (content: string): string => {
return content.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
};
const ThinkTagProcessor = ({
children,
isOverlayMode = false
}: {
const ThinkTagProcessor = ({
children,
isOverlayMode = false,
}: {
children: React.ReactNode;
isOverlayMode?: boolean;
}) => {
@ -111,9 +111,13 @@ interface MarkdownRendererProps {
thinkOverlay?: boolean;
}
const MarkdownRenderer = ({ content, className, thinkOverlay = false }: MarkdownRendererProps) => {
const MarkdownRenderer = ({
content,
className,
thinkOverlay = false,
}: MarkdownRendererProps) => {
const [showThinkBox, setShowThinkBox] = useState(false);
// Extract think content from the markdown
const thinkContent = thinkOverlay ? extractThinkContent(content) : null;
const contentWithoutThink = thinkOverlay ? removeThinkTags(content) : content;
@ -153,27 +157,27 @@ const MarkdownRenderer = ({ content, className, thinkOverlay = false }: Markdown
{/* Think box when expanded - shows above markdown */}
{thinkOverlay && thinkContent && showThinkBox && (
<div className="mb-4">
<ThinkBox
content={thinkContent}
<ThinkBox
content={thinkContent}
expanded={true}
onToggle={() => setShowThinkBox(false)}
/>
</div>
)}
<Markdown
className={cn(
'prose prose-h1:mb-3 prose-h2:mb-2 prose-h2:mt-6 prose-h2:font-[800] prose-h3:mt-4 prose-h3:mb-1.5 prose-h3:font-[600] prose-invert prose-p:leading-relaxed prose-pre:p-0 font-[400]',
'prose-code:bg-transparent prose-code:p-0 prose-code:text-inherit prose-code:font-normal prose-code:before:content-none prose-code:after:content-none',
'prose-pre:bg-transparent prose-pre:border-0 prose-pre:m-0 prose-pre:p-0',
'max-w-none break-words text-black dark:text-white',
className
'break-words text-black dark:text-white',
className,
)}
options={markdownOverrides}
>
{thinkOverlay ? contentWithoutThink : content}
</Markdown>
{/* Overlay icon when think box is collapsed */}
{thinkOverlay && thinkContent && !showThinkBox && (
<button
@ -181,7 +185,10 @@ const MarkdownRenderer = ({ content, className, thinkOverlay = false }: Markdown
className="absolute top-2 right-2 p-2 rounded-lg bg-black/20 dark:bg-white/20 backdrop-blur-sm opacity-30 hover:opacity-100 transition-opacity duration-200 group"
title="Show thinking process"
>
<Brain size={16} className="text-gray-700 dark:text-gray-300 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
<Brain
size={16}
className="text-gray-700 dark:text-gray-300 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors"
/>
</button>
)}
</div>

View file

@ -273,10 +273,7 @@ const MessageTabs = ({
{/* Answer Tab */}
{activeTab === 'text' && (
<div className="flex flex-col space-y-4 animate-fadeIn">
<MarkdownRenderer
content={parsedMessage}
className="px-4"
/>
<MarkdownRenderer content={parsedMessage} className="px-4" />
{loading && isLast ? null : (
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white px-4 py-4">

View file

@ -1,7 +1,14 @@
'use client';
import { cn } from '@/lib/utils';
import { BookOpenText, Home, Search, SquarePen, Settings, LayoutDashboard } from 'lucide-react';
import {
BookOpenText,
Home,
Search,
SquarePen,
Settings,
LayoutDashboard,
} from 'lucide-react';
import Link from 'next/link';
import { useSelectedLayoutSegments } from 'next/navigation';
import React, { useState, type ReactNode } from 'react';

View file

@ -17,10 +17,11 @@ const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => {
}
const [internalExpanded, setInternalExpanded] = useState(false);
// Use external expanded state if provided, otherwise use internal state
const isExpanded = expanded !== undefined ? expanded : internalExpanded;
const handleToggle = onToggle || (() => setInternalExpanded(!internalExpanded));
const handleToggle =
onToggle || (() => setInternalExpanded(!internalExpanded));
return (
<div className="my-4 bg-light-secondary/50 dark:bg-dark-secondary/50 rounded-xl border border-light-200 dark:border-dark-200 overflow-hidden">

View file

@ -1,6 +1,12 @@
'use client';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import {
Dialog,
DialogPanel,
DialogTitle,
Transition,
TransitionChild,
} from '@headlessui/react';
import { X, Plus, Trash2, Play, Save } from 'lucide-react';
import { Fragment, useState, useEffect } from 'react';
import MarkdownRenderer from '@/components/MarkdownRenderer';
@ -9,20 +15,28 @@ import ModelSelector from '@/components/MessageInputActions/ModelSelector';
// Helper function to replace date/time variables in prompts on the client side
const replaceDateTimeVariables = (prompt: string): string => {
let processedPrompt = prompt;
// Replace UTC datetime
if (processedPrompt.includes('{{current_utc_datetime}}')) {
const utcDateTime = new Date().toISOString();
processedPrompt = processedPrompt.replace(/\{\{current_utc_datetime\}\}/g, utcDateTime);
processedPrompt = processedPrompt.replace(
/\{\{current_utc_datetime\}\}/g,
utcDateTime,
);
}
// Replace local datetime
if (processedPrompt.includes('{{current_local_datetime}}')) {
const now = new Date();
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString();
processedPrompt = processedPrompt.replace(/\{\{current_local_datetime\}\}/g, localDateTime);
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
).toISOString();
processedPrompt = processedPrompt.replace(
/\{\{current_local_datetime\}\}/g,
localDateTime,
);
}
return processedPrompt;
};
@ -67,7 +81,10 @@ const WidgetConfigModal = ({
const [previewContent, setPreviewContent] = useState<string>('');
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState<{ provider: string; model: string } | null>(null);
const [selectedModel, setSelectedModel] = useState<{
provider: string;
model: string;
} | null>(null);
// Update config when editingWidget changes
useEffect(() => {
@ -106,7 +123,7 @@ const WidgetConfigModal = ({
// Update config when model selection changes
useEffect(() => {
if (selectedModel) {
setConfig(prev => ({
setConfig((prev) => ({
...prev,
provider: selectedModel.provider,
model: selectedModel.model,
@ -118,7 +135,7 @@ const WidgetConfigModal = ({
if (!config.title.trim() || !config.prompt.trim()) {
return; // TODO: Add proper validation feedback
}
onSave(config);
onClose();
};
@ -129,8 +146,13 @@ const WidgetConfigModal = ({
return;
}
if (config.sources.length === 0 || config.sources.every(s => !s.url.trim())) {
setPreviewContent('Please add at least one source URL before running preview.');
if (
config.sources.length === 0 ||
config.sources.every((s) => !s.url.trim())
) {
setPreviewContent(
'Please add at least one source URL before running preview.',
);
return;
}
@ -145,7 +167,7 @@ const WidgetConfigModal = ({
'Content-Type': 'application/json',
},
body: JSON.stringify({
sources: config.sources.filter(s => s.url.trim()), // Only send sources with URLs
sources: config.sources.filter((s) => s.url.trim()), // Only send sources with URLs
prompt: processedPrompt,
provider: config.provider,
model: config.model,
@ -153,40 +175,44 @@ const WidgetConfigModal = ({
});
const result = await response.json();
if (result.success) {
setPreviewContent(result.content);
} else {
setPreviewContent(`**Preview Error:** ${result.error || 'Unknown error occurred'}\n\n${result.content || ''}`);
setPreviewContent(
`**Preview Error:** ${result.error || 'Unknown error occurred'}\n\n${result.content || ''}`,
);
}
} catch (error) {
console.error('Preview error:', error);
setPreviewContent(`**Network Error:** Failed to connect to the preview service.\n\n${error instanceof Error ? error.message : 'Unknown error'}`);
setPreviewContent(
`**Network Error:** Failed to connect to the preview service.\n\n${error instanceof Error ? error.message : 'Unknown error'}`,
);
} finally {
setIsPreviewLoading(false);
}
};
const addSource = () => {
setConfig(prev => ({
setConfig((prev) => ({
...prev,
sources: [...prev.sources, { url: '', type: 'Web Page' }]
sources: [...prev.sources, { url: '', type: 'Web Page' }],
}));
};
const removeSource = (index: number) => {
setConfig(prev => ({
setConfig((prev) => ({
...prev,
sources: prev.sources.filter((_, i) => i !== index)
sources: prev.sources.filter((_, i) => i !== index),
}));
};
const updateSource = (index: number, field: keyof Source, value: string) => {
setConfig(prev => ({
setConfig((prev) => ({
...prev,
sources: prev.sources.map((source, i) =>
i === index ? { ...source, [field]: value } : source
)
sources: prev.sources.map((source, i) =>
i === index ? { ...source, [field]: value } : source,
),
}));
};
@ -241,7 +267,12 @@ const WidgetConfigModal = ({
<input
type="text"
value={config.title}
onChange={(e) => setConfig(prev => ({ ...prev, title: e.target.value }))}
onChange={(e) =>
setConfig((prev) => ({
...prev,
title: e.target.value,
}))
}
className="w-full px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter widget title..."
/>
@ -258,13 +289,21 @@ const WidgetConfigModal = ({
<input
type="url"
value={source.url}
onChange={(e) => updateSource(index, 'url', e.target.value)}
onChange={(e) =>
updateSource(index, 'url', e.target.value)
}
className="flex-1 px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="https://example.com"
/>
<select
value={source.type}
onChange={(e) => updateSource(index, 'type', e.target.value as Source['type'])}
onChange={(e) =>
updateSource(
index,
'type',
e.target.value as Source['type'],
)
}
className="px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="Web Page">Web Page</option>
@ -297,7 +336,12 @@ const WidgetConfigModal = ({
</label>
<textarea
value={config.prompt}
onChange={(e) => setConfig(prev => ({ ...prev, prompt: e.target.value }))}
onChange={(e) =>
setConfig((prev) => ({
...prev,
prompt: e.target.value,
}))
}
rows={6}
className="w-full px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter your prompt here..."
@ -310,13 +354,14 @@ const WidgetConfigModal = ({
Model & Provider
</label>
<ModelSelector
selectedModel={selectedModel}
setSelectedModel={setSelectedModel}
truncateModelName={false}
showModelName={true}
/>
selectedModel={selectedModel}
setSelectedModel={setSelectedModel}
truncateModelName={false}
showModelName={true}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Select the AI model and provider to process your widget content
Select the AI model and provider to process your widget
content
</p>
</div>
@ -330,12 +375,24 @@ const WidgetConfigModal = ({
type="number"
min="1"
value={config.refreshFrequency}
onChange={(e) => setConfig(prev => ({ ...prev, refreshFrequency: parseInt(e.target.value) || 1 }))}
onChange={(e) =>
setConfig((prev) => ({
...prev,
refreshFrequency: parseInt(e.target.value) || 1,
}))
}
className="flex-1 px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<select
value={config.refreshUnit}
onChange={(e) => setConfig(prev => ({ ...prev, refreshUnit: e.target.value as 'minutes' | 'hours' }))}
onChange={(e) =>
setConfig((prev) => ({
...prev,
refreshUnit: e.target.value as
| 'minutes'
| 'hours',
}))
}
className="px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="minutes">Minutes</option>
@ -360,11 +417,14 @@ const WidgetConfigModal = ({
{isPreviewLoading ? 'Loading...' : 'Run Preview'}
</button>
</div>
<div className="h-80 p-4 border border-light-200 dark:border-dark-200 rounded-md bg-light-secondary dark:bg-dark-secondary overflow-y-auto">
<div className="h-80 p-4 border border-light-200 dark:border-dark-200 rounded-md bg-light-secondary dark:bg-dark-secondary overflow-y-auto max-w-full">
{previewContent ? (
<div className="prose prose-sm dark:prose-invert max-w-none">
<MarkdownRenderer thinkOverlay={true} content={previewContent} />
<div className="prose prose-sm dark:prose-invert max-w-full">
<MarkdownRenderer
thinkOverlay={true}
content={previewContent}
/>
</div>
) : (
<div className="text-sm text-black/50 dark:text-white/50 italic">
@ -377,11 +437,36 @@ const WidgetConfigModal = ({
<div className="text-xs text-black/70 dark:text-white/70">
<h5 className="font-medium mb-2">Available Variables:</h5>
<div className="space-y-1">
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{current_utc_datetime}}'}</code> - Current UTC date and time</div>
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{current_local_datetime}}'}</code> - Current local date and time</div>
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{source_content_1}}'}</code> - Content from first source</div>
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{source_content_2}}'}</code> - Content from second source</div>
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{location}}'}</code> - Your current location</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'{{current_utc_datetime}}'}
</code>{' '}
- Current UTC date and time
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'{{current_local_datetime}}'}
</code>{' '}
- Current local date and time
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'{{source_content_1}}'}
</code>{' '}
- Content from first source
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'{{source_content_2}}'}
</code>{' '}
- Content from second source
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'{{location}}'}
</code>{' '}
- Your current location
</div>
</div>
</div>
</div>

View file

@ -1,6 +1,14 @@
'use client';
import { RefreshCw, Edit, Trash2, Clock, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
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';
@ -32,12 +40,17 @@ interface WidgetDisplayProps {
onRefresh: (widgetId: string) => void;
}
const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayProps) => {
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));
@ -61,16 +74,16 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
<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
<span
className="text-xs text-gray-500 dark:text-gray-400"
title={getRefreshFrequencyText()}
>
{formatLastUpdated(widget.lastUpdated)}
</span>
{/* Refresh button */}
<button
onClick={() => onRefresh(widget.id)}
@ -78,9 +91,9 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
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' : ''}`}
<RefreshCw
size={16}
className={`text-gray-600 dark:text-gray-400 ${widget.isLoading ? 'animate-spin' : ''}`}
/>
</button>
</div>
@ -95,14 +108,21 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
</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" />
<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>
<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">
<div className="prose prose-sm dark:prose-invert">
<MarkdownRenderer content={widget.content} thinkOverlay={true} />
</div>
) : (
@ -134,10 +154,15 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
{/* Sources */}
{widget.sources.length > 0 && (
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Sources:</p>
<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">
<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}
@ -160,7 +185,7 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
<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"

View file

@ -21,7 +21,8 @@ interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
children: React.ReactNode;
}
interface CardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
interface CardDescriptionProps
extends React.HTMLAttributes<HTMLParagraphElement> {
children: React.ReactNode;
}
@ -31,13 +32,13 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
ref={ref}
className={cn(
'rounded-lg border bg-card text-card-foreground shadow-sm',
className
className,
)}
{...props}
>
{children}
</div>
)
),
);
Card.displayName = 'Card';
@ -50,7 +51,7 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
>
{children}
</div>
)
),
);
CardHeader.displayName = 'CardHeader';
@ -60,27 +61,28 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, CardTitleProps>(
ref={ref}
className={cn(
'text-2xl font-semibold leading-none tracking-tight',
className
className,
)}
{...props}
>
{children}
</h3>
)
),
);
CardTitle.displayName = 'CardTitle';
const CardDescription = React.forwardRef<HTMLParagraphElement, CardDescriptionProps>(
({ className, children, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
>
{children}
</p>
)
);
const CardDescription = React.forwardRef<
HTMLParagraphElement,
CardDescriptionProps
>(({ className, children, ...props }, ref) => (
<p
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
>
{children}
</p>
));
CardDescription.displayName = 'CardDescription';
const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
@ -88,7 +90,7 @@ const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
<div ref={ref} className={cn('p-6 pt-0', className)} {...props}>
{children}
</div>
)
),
);
CardContent.displayName = 'CardContent';
@ -101,8 +103,15 @@ const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
>
{children}
</div>
)
),
);
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};

View file

@ -1,11 +1,11 @@
import { useState, useEffect, useCallback } from 'react';
import {
Widget,
WidgetConfig,
DashboardState,
import {
Widget,
WidgetConfig,
DashboardState,
DashboardConfig,
DASHBOARD_STORAGE_KEYS,
WidgetCache
WidgetCache,
} from '@/lib/types';
// Helper function to request location permission and get user's location
@ -31,7 +31,7 @@ const requestLocationPermission = async (): Promise<string | undefined> => {
enableHighAccuracy: true,
timeout: 10000, // 10 seconds timeout
maximumAge: 300000, // 5 minutes cache
}
},
);
});
} catch (error) {
@ -43,20 +43,28 @@ const requestLocationPermission = async (): Promise<string | undefined> => {
// Helper function to replace date/time variables in prompts on the client side
const replaceDateTimeVariables = (prompt: string): string => {
let processedPrompt = prompt;
// Replace UTC datetime
if (processedPrompt.includes('{{current_utc_datetime}}')) {
const utcDateTime = new Date().toISOString();
processedPrompt = processedPrompt.replace(/\{\{current_utc_datetime\}\}/g, utcDateTime);
processedPrompt = processedPrompt.replace(
/\{\{current_utc_datetime\}\}/g,
utcDateTime,
);
}
// Replace local datetime
if (processedPrompt.includes('{{current_local_datetime}}')) {
const now = new Date();
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString();
processedPrompt = processedPrompt.replace(/\{\{current_local_datetime\}\}/g, localDateTime);
const localDateTime = new Date(
now.getTime() - now.getTimezoneOffset() * 60000,
).toISOString();
processedPrompt = processedPrompt.replace(
/\{\{current_local_datetime\}\}/g,
localDateTime,
);
}
return processedPrompt;
};
@ -66,19 +74,19 @@ interface UseDashboardReturn {
isLoading: boolean;
error: string | null;
settings: DashboardConfig['settings'];
// Widget management
addWidget: (config: WidgetConfig) => void;
updateWidget: (id: string, config: WidgetConfig) => void;
deleteWidget: (id: string) => void;
refreshWidget: (id: string, forceRefresh?: boolean) => Promise<void>;
refreshAllWidgets: () => Promise<void>;
refreshAllWidgets: (forceRefresh?: boolean) => Promise<void>;
// Storage management
exportDashboard: () => Promise<string>;
importDashboard: (configJson: string) => Promise<void>;
clearCache: () => void;
// Settings
updateSettings: (newSettings: Partial<DashboardConfig['settings']>) => void;
}
@ -103,13 +111,19 @@ export const useDashboard = (): UseDashboardReturn => {
// Save widgets to localStorage whenever they change (but not on initial load)
useEffect(() => {
if (!state.isLoading) {
localStorage.setItem(DASHBOARD_STORAGE_KEYS.WIDGETS, JSON.stringify(state.widgets));
localStorage.setItem(
DASHBOARD_STORAGE_KEYS.WIDGETS,
JSON.stringify(state.widgets),
);
}
}, [state.widgets, state.isLoading]);
// Save settings to localStorage whenever they change
useEffect(() => {
localStorage.setItem(DASHBOARD_STORAGE_KEYS.SETTINGS, JSON.stringify(state.settings));
localStorage.setItem(
DASHBOARD_STORAGE_KEYS.SETTINGS,
JSON.stringify(state.settings),
);
}, [state.settings]);
const loadDashboardData = useCallback(() => {
@ -117,23 +131,27 @@ export const useDashboard = (): UseDashboardReturn => {
// Load widgets
const savedWidgets = localStorage.getItem(DASHBOARD_STORAGE_KEYS.WIDGETS);
const widgets: Widget[] = savedWidgets ? JSON.parse(savedWidgets) : [];
// Convert date strings back to Date objects
widgets.forEach(widget => {
widgets.forEach((widget) => {
if (widget.lastUpdated) {
widget.lastUpdated = new Date(widget.lastUpdated);
}
});
// Load settings
const savedSettings = localStorage.getItem(DASHBOARD_STORAGE_KEYS.SETTINGS);
const settings = savedSettings ? JSON.parse(savedSettings) : {
parallelLoading: true,
autoRefresh: false,
theme: 'auto',
};
const savedSettings = localStorage.getItem(
DASHBOARD_STORAGE_KEYS.SETTINGS,
);
const settings = savedSettings
? JSON.parse(savedSettings)
: {
parallelLoading: true,
autoRefresh: false,
theme: 'auto',
};
setState(prev => ({
setState((prev) => ({
...prev,
widgets,
settings,
@ -141,7 +159,7 @@ export const useDashboard = (): UseDashboardReturn => {
}));
} catch (error) {
console.error('Error loading dashboard data:', error);
setState(prev => ({
setState((prev) => ({
...prev,
error: 'Failed to load dashboard data',
isLoading: false,
@ -159,27 +177,27 @@ export const useDashboard = (): UseDashboardReturn => {
error: null,
};
setState(prev => ({
setState((prev) => ({
...prev,
widgets: [...prev.widgets, newWidget],
}));
}, []);
const updateWidget = useCallback((id: string, config: WidgetConfig) => {
setState(prev => ({
setState((prev) => ({
...prev,
widgets: prev.widgets.map(widget =>
widgets: prev.widgets.map((widget) =>
widget.id === id
? { ...widget, ...config, id } // Preserve the ID
: widget
: widget,
),
}));
}, []);
const deleteWidget = useCallback((id: string) => {
setState(prev => ({
setState((prev) => ({
...prev,
widgets: prev.widgets.filter(widget => widget.id !== id),
widgets: prev.widgets.filter((widget) => widget.id !== id),
}));
// Also remove from cache
@ -200,143 +218,160 @@ export const useDashboard = (): UseDashboardReturn => {
const isWidgetCacheValid = (widget: Widget): boolean => {
const cache = getWidgetCache();
const cachedData = cache[widget.id];
if (!cachedData) return false;
const now = new Date();
const expiresAt = new Date(cachedData.expiresAt);
return now < expiresAt;
};
const getCacheExpiryTime = (widget: Widget): Date => {
const now = new Date();
const refreshMs = widget.refreshFrequency * (widget.refreshUnit === 'hours' ? 3600000 : 60000);
const refreshMs =
widget.refreshFrequency *
(widget.refreshUnit === 'hours' ? 3600000 : 60000);
return new Date(now.getTime() + refreshMs);
};
const refreshWidget = useCallback(async (id: string, forceRefresh: boolean = false) => {
const widget = state.widgets.find(w => w.id === id);
if (!widget) return;
const refreshWidget = useCallback(
async (id: string, forceRefresh: boolean = false) => {
const widget = state.widgets.find((w) => w.id === id);
if (!widget) return;
// Check cache first (unless forcing refresh)
if (!forceRefresh && isWidgetCacheValid(widget)) {
const cache = getWidgetCache();
const cachedData = cache[widget.id];
setState(prev => ({
...prev,
widgets: prev.widgets.map(w =>
w.id === id
? { ...w, content: cachedData.content, lastUpdated: new Date(cachedData.lastFetched) }
: w
),
}));
return;
}
// Set loading state
setState(prev => ({
...prev,
widgets: prev.widgets.map(w =>
w.id === id ? { ...w, isLoading: true, error: null } : w
),
}));
try {
// Check if prompt uses location variable and request permission if needed
let location: string | undefined;
if (widget.prompt.includes('{{location}}')) {
location = await requestLocationPermission();
}
// Replace date/time variables on the client side
const processedPrompt = replaceDateTimeVariables(widget.prompt);
const response = await fetch('/api/dashboard/process-widget', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sources: widget.sources,
prompt: processedPrompt,
provider: widget.provider,
model: widget.model,
location,
}),
});
const result = await response.json();
const now = new Date();
if (result.success) {
// Update widget
setState(prev => ({
...prev,
widgets: prev.widgets.map(w =>
w.id === id
? {
...w,
isLoading: false,
content: result.content,
lastUpdated: now,
error: null,
}
: w
),
}));
// Cache the result
// Check cache first (unless forcing refresh)
if (!forceRefresh && isWidgetCacheValid(widget)) {
const cache = getWidgetCache();
cache[id] = {
content: result.content,
lastFetched: now,
expiresAt: getCacheExpiryTime(widget),
};
localStorage.setItem(DASHBOARD_STORAGE_KEYS.CACHE, JSON.stringify(cache));
} else {
setState(prev => ({
const cachedData = cache[widget.id];
setState((prev) => ({
...prev,
widgets: prev.widgets.map(w =>
widgets: prev.widgets.map((w) =>
w.id === id
? {
...w,
content: cachedData.content,
lastUpdated: new Date(cachedData.lastFetched),
}
: w,
),
}));
return;
}
// Set loading state
setState((prev) => ({
...prev,
widgets: prev.widgets.map((w) =>
w.id === id ? { ...w, isLoading: true, error: null } : w,
),
}));
try {
// Check if prompt uses location variable and request permission if needed
let location: string | undefined;
if (widget.prompt.includes('{{location}}')) {
location = await requestLocationPermission();
}
// Replace date/time variables on the client side
const processedPrompt = replaceDateTimeVariables(widget.prompt);
const response = await fetch('/api/dashboard/process-widget', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sources: widget.sources,
prompt: processedPrompt,
provider: widget.provider,
model: widget.model,
location,
}),
});
const result = await response.json();
const now = new Date();
if (result.success) {
// Update widget
setState((prev) => ({
...prev,
widgets: prev.widgets.map((w) =>
w.id === id
? {
...w,
isLoading: false,
content: result.content,
lastUpdated: now,
error: null,
}
: w,
),
}));
// Cache the result
const cache = getWidgetCache();
cache[id] = {
content: result.content,
lastFetched: now,
expiresAt: getCacheExpiryTime(widget),
};
localStorage.setItem(
DASHBOARD_STORAGE_KEYS.CACHE,
JSON.stringify(cache),
);
} else {
setState((prev) => ({
...prev,
widgets: prev.widgets.map((w) =>
w.id === id
? {
...w,
isLoading: false,
error: result.error || 'Failed to refresh widget',
}
: w,
),
}));
}
} catch (error) {
setState((prev) => ({
...prev,
widgets: prev.widgets.map((w) =>
w.id === id
? {
...w,
isLoading: false,
error: result.error || 'Failed to refresh widget',
error: 'Network error: Failed to refresh widget',
}
: w
: w,
),
}));
}
} catch (error) {
setState(prev => ({
...prev,
widgets: prev.widgets.map(w =>
w.id === id
? {
...w,
isLoading: false,
error: 'Network error: Failed to refresh widget',
}
: w
),
}));
}
}, [state.widgets]);
},
[state.widgets],
);
const refreshAllWidgets = useCallback(async () => {
const activeWidgets = state.widgets.filter(w => !w.isLoading);
if (state.settings.parallelLoading) {
// Refresh all widgets in parallel (force refresh)
await Promise.all(activeWidgets.map(widget => refreshWidget(widget.id, true)));
} else {
// Refresh widgets sequentially (force refresh)
for (const widget of activeWidgets) {
await refreshWidget(widget.id, true);
const refreshAllWidgets = useCallback(
async (forceRefresh = false) => {
const activeWidgets = state.widgets.filter((w) => !w.isLoading);
if (state.settings.parallelLoading) {
// Refresh all widgets in parallel (force refresh)
await Promise.all(
activeWidgets.map((widget) => refreshWidget(widget.id, forceRefresh)),
);
} else {
// Refresh widgets sequentially (force refresh)
for (const widget of activeWidgets) {
await refreshWidget(widget.id, forceRefresh);
}
}
}
}, [state.widgets, state.settings.parallelLoading, refreshWidget]);
},
[state.widgets, state.settings.parallelLoading, refreshWidget],
);
const exportDashboard = useCallback(async (): Promise<string> => {
const dashboardConfig: DashboardConfig = {
@ -349,45 +384,57 @@ export const useDashboard = (): UseDashboardReturn => {
return JSON.stringify(dashboardConfig, null, 2);
}, [state.widgets, state.settings]);
const importDashboard = useCallback(async (configJson: string): Promise<void> => {
try {
const config: DashboardConfig = JSON.parse(configJson);
// Validate the config structure
if (!config.widgets || !Array.isArray(config.widgets)) {
throw new Error('Invalid dashboard configuration: missing or invalid widgets array');
const importDashboard = useCallback(
async (configJson: string): Promise<void> => {
try {
const config: DashboardConfig = JSON.parse(configJson);
// Validate the config structure
if (!config.widgets || !Array.isArray(config.widgets)) {
throw new Error(
'Invalid dashboard configuration: missing or invalid widgets array',
);
}
// Process widgets and ensure they have valid IDs
const processedWidgets: Widget[] = config.widgets.map((widget) => ({
...widget,
id:
widget.id ||
Date.now().toString() + Math.random().toString(36).substr(2, 9),
lastUpdated: widget.lastUpdated ? new Date(widget.lastUpdated) : null,
isLoading: false,
content: widget.content || null,
error: null,
}));
setState((prev) => ({
...prev,
widgets: processedWidgets,
settings: { ...prev.settings, ...config.settings },
}));
} catch (error) {
throw new Error(
`Failed to import dashboard: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
);
}
// Process widgets and ensure they have valid IDs
const processedWidgets: Widget[] = config.widgets.map(widget => ({
...widget,
id: widget.id || Date.now().toString() + Math.random().toString(36).substr(2, 9),
lastUpdated: widget.lastUpdated ? new Date(widget.lastUpdated) : null,
isLoading: false,
content: widget.content || null,
error: null,
}));
setState(prev => ({
...prev,
widgets: processedWidgets,
settings: { ...prev.settings, ...config.settings },
}));
} catch (error) {
throw new Error(`Failed to import dashboard: ${error instanceof Error ? error.message : 'Invalid JSON'}`);
}
}, []);
},
[],
);
const clearCache = useCallback(() => {
localStorage.removeItem(DASHBOARD_STORAGE_KEYS.CACHE);
}, []);
const updateSettings = useCallback((newSettings: Partial<DashboardConfig['settings']>) => {
setState(prev => ({
...prev,
settings: { ...prev.settings, ...newSettings },
}));
}, []);
const updateSettings = useCallback(
(newSettings: Partial<DashboardConfig['settings']>) => {
setState((prev) => ({
...prev,
settings: { ...prev.settings, ...newSettings },
}));
},
[],
);
return {
// State
@ -395,19 +442,19 @@ export const useDashboard = (): UseDashboardReturn => {
isLoading: state.isLoading,
error: state.error,
settings: state.settings,
// Widget management
addWidget,
updateWidget,
deleteWidget,
refreshWidget,
refreshAllWidgets,
// Storage management
exportDashboard,
importDashboard,
clearCache,
// Settings
updateSettings,
};