fix(refactor): Cleanup components for improved readability and consistency
This commit is contained in:
parent
1228beb59a
commit
1b0c2c59b8
10 changed files with 590 additions and 350 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getWebContent, getWebContentLite } from '@/lib/utils/documents';
|
import { getWebContent, getWebContentLite } from '@/lib/utils/documents';
|
||||||
|
import { Document } from '@langchain/core/documents';
|
||||||
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
import { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||||
import { ChatOpenAI } from '@langchain/openai';
|
import { ChatOpenAI } from '@langchain/openai';
|
||||||
import { HumanMessage } from '@langchain/core/messages';
|
import { HumanMessage } from '@langchain/core/messages';
|
||||||
|
|
@ -10,6 +11,7 @@ import {
|
||||||
getCustomOpenaiModelName,
|
getCustomOpenaiModelName,
|
||||||
} from '@/lib/config';
|
} from '@/lib/config';
|
||||||
import { ChatOllama } from '@langchain/ollama';
|
import { ChatOllama } from '@langchain/ollama';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
interface Source {
|
interface Source {
|
||||||
url: string;
|
url: string;
|
||||||
|
|
@ -24,7 +26,9 @@ interface WidgetProcessRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to fetch content from a single source
|
// 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 {
|
try {
|
||||||
let document;
|
let document;
|
||||||
|
|
||||||
|
|
@ -33,13 +37,17 @@ async function fetchSourceContent(source: Source): Promise<{ content: string; er
|
||||||
document = await getWebContent(source.url);
|
document = await getWebContent(source.url);
|
||||||
} else {
|
} else {
|
||||||
// Use faster fetch for HTTP data/APIs
|
// 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) {
|
if (!document) {
|
||||||
return {
|
return {
|
||||||
content: '',
|
content: '',
|
||||||
error: `Failed to fetch content from ${source.url}`
|
error: `Failed to fetch content from ${source.url}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,20 +56,26 @@ async function fetchSourceContent(source: Source): Promise<{ content: string; er
|
||||||
console.error(`Error fetching content from ${source.url}:`, error);
|
console.error(`Error fetching content from ${source.url}:`, error);
|
||||||
return {
|
return {
|
||||||
content: '',
|
content: '',
|
||||||
error: `Error fetching ${source.url}: ${error instanceof Error ? error.message : 'Unknown error'}`
|
error: `Error fetching ${source.url}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Helper function to replace variables in prompt
|
// 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;
|
let processedPrompt = prompt;
|
||||||
|
|
||||||
// Replace source content variables
|
// Replace source content variables
|
||||||
sourceContents.forEach((content, index) => {
|
sourceContents.forEach((content, index) => {
|
||||||
const variable = `{{source_content_${index + 1}}}`;
|
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
|
// Replace location if provided
|
||||||
|
|
@ -73,7 +87,10 @@ function replacePromptVariables(prompt: string, sourceContents: string[], locati
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to get LLM instance based on provider and model
|
// 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 {
|
try {
|
||||||
const chatModelProviders = await getAvailableChatModelProviders();
|
const chatModelProviders = await getAvailableChatModelProviders();
|
||||||
|
|
||||||
|
|
@ -106,7 +123,11 @@ async function getLLMInstance(provider: string, model: string): Promise<BaseChat
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to process the prompt with LLM
|
// 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);
|
const llm = await getLLMInstance(provider, model);
|
||||||
|
|
||||||
if (!llm) {
|
if (!llm) {
|
||||||
|
|
@ -127,7 +148,7 @@ export async function POST(request: NextRequest) {
|
||||||
if (!body.sources || !body.prompt || !body.provider || !body.model) {
|
if (!body.sources || !body.prompt || !body.provider || !body.model) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Missing required fields: sources, prompt, provider, model' },
|
{ 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) {
|
if (!Array.isArray(body.sources) || body.sources.length === 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'At least one source URL is required' },
|
{ error: 'At least one source URL is required' },
|
||||||
{ status: 400 }
|
{ status: 400 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch content from all sources
|
// Fetch content from all sources
|
||||||
console.log(`Processing widget with ${body.sources.length} source(s)`);
|
console.log(`Processing widget with ${body.sources.length} source(s)`);
|
||||||
const sourceResults = await Promise.all(
|
const sourceResults = await Promise.all(
|
||||||
body.sources.map(source => fetchSourceContent(source))
|
body.sources.map((source) => fetchSourceContent(source)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for fetch errors
|
// Check for fetch errors
|
||||||
const fetchErrors = sourceResults
|
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);
|
.filter(Boolean);
|
||||||
|
|
||||||
if (fetchErrors.length > 0) {
|
if (fetchErrors.length > 0) {
|
||||||
|
|
@ -155,31 +178,36 @@ export async function POST(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract successful content
|
// Extract successful content
|
||||||
const sourceContents = sourceResults.map(result => result.content);
|
const sourceContents = sourceResults.map((result) => result.content);
|
||||||
|
|
||||||
// If all sources failed, return error
|
// If all sources failed, return error
|
||||||
if (sourceContents.every(content => !content)) {
|
if (sourceContents.every((content) => !content)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to fetch content from all sources' },
|
{ error: 'Failed to fetch content from all sources' },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace variables in prompt
|
// Replace variables in prompt
|
||||||
const processedPrompt = replacePromptVariables(body.prompt, sourceContents);
|
const processedPrompt = replacePromptVariables(body.prompt, sourceContents);
|
||||||
|
|
||||||
console.log('Processed prompt:', processedPrompt.substring(0, 200) + '...');
|
console.log('Processing prompt:', processedPrompt);
|
||||||
|
|
||||||
// Process with LLM
|
// Process with LLM
|
||||||
try {
|
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({
|
return NextResponse.json({
|
||||||
content: llmResponse,
|
content: llmResponse,
|
||||||
success: true,
|
success: true,
|
||||||
sourcesFetched: sourceContents.filter(content => content).length,
|
sourcesFetched: sourceContents.filter((content) => content).length,
|
||||||
totalSources: body.sources.length,
|
totalSources: body.sources.length,
|
||||||
warnings: fetchErrors.length > 0 ? fetchErrors : undefined
|
warnings: fetchErrors.length > 0 ? fetchErrors : undefined,
|
||||||
});
|
});
|
||||||
} catch (llmError) {
|
} catch (llmError) {
|
||||||
console.error('LLM processing failed:', llmError);
|
console.error('LLM processing failed:', llmError);
|
||||||
|
|
@ -193,24 +221,26 @@ export async function POST(request: NextRequest) {
|
||||||
${processedPrompt}
|
${processedPrompt}
|
||||||
|
|
||||||
## Sources Successfully Fetched
|
## 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')}` : ''}`;
|
${fetchErrors.length > 0 ? `## Source Errors\n${fetchErrors.join('\n')}` : ''}`;
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
content: diagnosticResponse,
|
content: diagnosticResponse,
|
||||||
success: false,
|
success: false,
|
||||||
error: llmError instanceof Error ? llmError.message : 'LLM processing failed',
|
error:
|
||||||
sourcesFetched: sourceContents.filter(content => content).length,
|
llmError instanceof Error
|
||||||
totalSources: body.sources.length
|
? llmError.message
|
||||||
|
: 'LLM processing failed',
|
||||||
|
sourcesFetched: sourceContents.filter((content) => content).length,
|
||||||
|
totalSources: body.sources.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error processing widget:', error);
|
console.error('Error processing widget:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Internal server error' },
|
{ error: 'Internal server error' },
|
||||||
{ status: 500 }
|
{ status: 500 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,28 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Plus, RefreshCw, Download, Upload, LayoutDashboard, Layers, List } from 'lucide-react';
|
import {
|
||||||
import { useState } from 'react';
|
Plus,
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
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 WidgetConfigModal from '@/components/dashboard/WidgetConfigModal';
|
||||||
import WidgetDisplay from '@/components/dashboard/WidgetDisplay';
|
import WidgetDisplay from '@/components/dashboard/WidgetDisplay';
|
||||||
import { useDashboard } from '@/lib/hooks/useDashboard';
|
import { useDashboard } from '@/lib/hooks/useDashboard';
|
||||||
import { Widget, WidgetConfig } from '@/lib/types';
|
import { Widget, WidgetConfig } from '@/lib/types';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const DashboardPage = () => {
|
const DashboardPage = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -24,7 +40,19 @@ const DashboardPage = () => {
|
||||||
} = useDashboard();
|
} = useDashboard();
|
||||||
|
|
||||||
const [showAddModal, setShowAddModal] = useState(false);
|
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);
|
setEditingWidget(null);
|
||||||
setShowAddModal(true);
|
setShowAddModal(true);
|
||||||
};
|
};
|
||||||
|
|
@ -60,18 +88,18 @@ const DashboardPage = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshAll = () => {
|
const handleRefreshAll = () => {
|
||||||
refreshAllWidgets();
|
refreshAllWidgets(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
try {
|
try {
|
||||||
const configJson = await exportDashboard();
|
const configJson = await exportDashboard();
|
||||||
await navigator.clipboard.writeText(configJson);
|
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');
|
console.log('Dashboard configuration copied to clipboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Export failed:', 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 {
|
try {
|
||||||
const configJson = await navigator.clipboard.readText();
|
const configJson = await navigator.clipboard.readText();
|
||||||
await importDashboard(configJson);
|
await importDashboard(configJson);
|
||||||
// TODO: Add toast notification for success
|
toast.success('Dashboard configuration imported successfully');
|
||||||
console.log('Dashboard configuration imported successfully');
|
console.log('Dashboard configuration imported successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Import failed:', error);
|
console.error('Import failed:', error);
|
||||||
// TODO: Add toast notification for error
|
toast.error('Failed to import dashboard configuration');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -98,13 +126,15 @@ const DashboardPage = () => {
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Welcome to your Dashboard</CardTitle>
|
<CardTitle>Welcome to your Dashboard</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Create your first widget to get started with personalized information
|
Create your first widget to get started with personalized
|
||||||
|
information
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<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>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
|
@ -186,7 +216,9 @@ const DashboardPage = () => {
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
<div className="text-center">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
) : widgets.length === 0 ? (
|
) : widgets.length === 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const extractThinkContent = (content: string): string | null => {
|
||||||
|
|
||||||
// Extract content between think tags and join if multiple
|
// Extract content between think tags and join if multiple
|
||||||
const extractedContent = matches
|
const extractedContent = matches
|
||||||
.map(match => match.replace(/<\/?think>/g, ''))
|
.map((match) => match.replace(/<\/?think>/g, ''))
|
||||||
.join('\n\n');
|
.join('\n\n');
|
||||||
|
|
||||||
// Return null if content is empty or only whitespace
|
// Return null if content is empty or only whitespace
|
||||||
|
|
@ -30,7 +30,7 @@ const removeThinkTags = (content: string): string => {
|
||||||
|
|
||||||
const ThinkTagProcessor = ({
|
const ThinkTagProcessor = ({
|
||||||
children,
|
children,
|
||||||
isOverlayMode = false
|
isOverlayMode = false,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
isOverlayMode?: boolean;
|
isOverlayMode?: boolean;
|
||||||
|
|
@ -111,7 +111,11 @@ interface MarkdownRendererProps {
|
||||||
thinkOverlay?: boolean;
|
thinkOverlay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownRenderer = ({ content, className, thinkOverlay = false }: MarkdownRendererProps) => {
|
const MarkdownRenderer = ({
|
||||||
|
content,
|
||||||
|
className,
|
||||||
|
thinkOverlay = false,
|
||||||
|
}: MarkdownRendererProps) => {
|
||||||
const [showThinkBox, setShowThinkBox] = useState(false);
|
const [showThinkBox, setShowThinkBox] = useState(false);
|
||||||
|
|
||||||
// Extract think content from the markdown
|
// Extract think content from the markdown
|
||||||
|
|
@ -166,8 +170,8 @@ const MarkdownRenderer = ({ content, className, thinkOverlay = false }: Markdown
|
||||||
'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 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-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',
|
'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',
|
'break-words text-black dark:text-white',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
options={markdownOverrides}
|
options={markdownOverrides}
|
||||||
>
|
>
|
||||||
|
|
@ -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"
|
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"
|
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>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -273,10 +273,7 @@ const MessageTabs = ({
|
||||||
{/* Answer Tab */}
|
{/* Answer Tab */}
|
||||||
{activeTab === 'text' && (
|
{activeTab === 'text' && (
|
||||||
<div className="flex flex-col space-y-4 animate-fadeIn">
|
<div className="flex flex-col space-y-4 animate-fadeIn">
|
||||||
<MarkdownRenderer
|
<MarkdownRenderer content={parsedMessage} className="px-4" />
|
||||||
content={parsedMessage}
|
|
||||||
className="px-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{loading && isLast ? null : (
|
{loading && isLast ? null : (
|
||||||
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white px-4 py-4">
|
<div className="flex flex-row items-center justify-between w-full text-black dark:text-white px-4 py-4">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
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 Link from 'next/link';
|
||||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
import { useSelectedLayoutSegments } from 'next/navigation';
|
||||||
import React, { useState, type ReactNode } from 'react';
|
import React, { useState, type ReactNode } from 'react';
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,8 @@ const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => {
|
||||||
|
|
||||||
// Use external expanded state if provided, otherwise use internal state
|
// Use external expanded state if provided, otherwise use internal state
|
||||||
const isExpanded = expanded !== undefined ? expanded : internalExpanded;
|
const isExpanded = expanded !== undefined ? expanded : internalExpanded;
|
||||||
const handleToggle = onToggle || (() => setInternalExpanded(!internalExpanded));
|
const handleToggle =
|
||||||
|
onToggle || (() => setInternalExpanded(!internalExpanded));
|
||||||
|
|
||||||
return (
|
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">
|
<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">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
'use client';
|
'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 { X, Plus, Trash2, Play, Save } from 'lucide-react';
|
||||||
import { Fragment, useState, useEffect } from 'react';
|
import { Fragment, useState, useEffect } from 'react';
|
||||||
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||||
|
|
@ -13,14 +19,22 @@ const replaceDateTimeVariables = (prompt: string): string => {
|
||||||
// Replace UTC datetime
|
// Replace UTC datetime
|
||||||
if (processedPrompt.includes('{{current_utc_datetime}}')) {
|
if (processedPrompt.includes('{{current_utc_datetime}}')) {
|
||||||
const utcDateTime = new Date().toISOString();
|
const utcDateTime = new Date().toISOString();
|
||||||
processedPrompt = processedPrompt.replace(/\{\{current_utc_datetime\}\}/g, utcDateTime);
|
processedPrompt = processedPrompt.replace(
|
||||||
|
/\{\{current_utc_datetime\}\}/g,
|
||||||
|
utcDateTime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace local datetime
|
// Replace local datetime
|
||||||
if (processedPrompt.includes('{{current_local_datetime}}')) {
|
if (processedPrompt.includes('{{current_local_datetime}}')) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString();
|
const localDateTime = new Date(
|
||||||
processedPrompt = processedPrompt.replace(/\{\{current_local_datetime\}\}/g, localDateTime);
|
now.getTime() - now.getTimezoneOffset() * 60000,
|
||||||
|
).toISOString();
|
||||||
|
processedPrompt = processedPrompt.replace(
|
||||||
|
/\{\{current_local_datetime\}\}/g,
|
||||||
|
localDateTime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return processedPrompt;
|
return processedPrompt;
|
||||||
|
|
@ -67,7 +81,10 @@ const WidgetConfigModal = ({
|
||||||
|
|
||||||
const [previewContent, setPreviewContent] = useState<string>('');
|
const [previewContent, setPreviewContent] = useState<string>('');
|
||||||
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
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
|
// Update config when editingWidget changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -106,7 +123,7 @@ const WidgetConfigModal = ({
|
||||||
// Update config when model selection changes
|
// Update config when model selection changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedModel) {
|
if (selectedModel) {
|
||||||
setConfig(prev => ({
|
setConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
provider: selectedModel.provider,
|
provider: selectedModel.provider,
|
||||||
model: selectedModel.model,
|
model: selectedModel.model,
|
||||||
|
|
@ -129,8 +146,13 @@ const WidgetConfigModal = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.sources.length === 0 || config.sources.every(s => !s.url.trim())) {
|
if (
|
||||||
setPreviewContent('Please add at least one source URL before running preview.');
|
config.sources.length === 0 ||
|
||||||
|
config.sources.every((s) => !s.url.trim())
|
||||||
|
) {
|
||||||
|
setPreviewContent(
|
||||||
|
'Please add at least one source URL before running preview.',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,7 +167,7 @@ const WidgetConfigModal = ({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
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,
|
prompt: processedPrompt,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
|
|
@ -157,36 +179,40 @@ const WidgetConfigModal = ({
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setPreviewContent(result.content);
|
setPreviewContent(result.content);
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
console.error('Preview error:', 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 {
|
} finally {
|
||||||
setIsPreviewLoading(false);
|
setIsPreviewLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSource = () => {
|
const addSource = () => {
|
||||||
setConfig(prev => ({
|
setConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
sources: [...prev.sources, { url: '', type: 'Web Page' }]
|
sources: [...prev.sources, { url: '', type: 'Web Page' }],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeSource = (index: number) => {
|
const removeSource = (index: number) => {
|
||||||
setConfig(prev => ({
|
setConfig((prev) => ({
|
||||||
...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) => {
|
const updateSource = (index: number, field: keyof Source, value: string) => {
|
||||||
setConfig(prev => ({
|
setConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
sources: prev.sources.map((source, i) =>
|
sources: prev.sources.map((source, i) =>
|
||||||
i === index ? { ...source, [field]: value } : source
|
i === index ? { ...source, [field]: value } : source,
|
||||||
)
|
),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -241,7 +267,12 @@ const WidgetConfigModal = ({
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={config.title}
|
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"
|
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..."
|
placeholder="Enter widget title..."
|
||||||
/>
|
/>
|
||||||
|
|
@ -258,13 +289,21 @@ const WidgetConfigModal = ({
|
||||||
<input
|
<input
|
||||||
type="url"
|
type="url"
|
||||||
value={source.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"
|
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"
|
placeholder="https://example.com"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
value={source.type}
|
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"
|
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>
|
<option value="Web Page">Web Page</option>
|
||||||
|
|
@ -297,7 +336,12 @@ const WidgetConfigModal = ({
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={config.prompt}
|
value={config.prompt}
|
||||||
onChange={(e) => setConfig(prev => ({ ...prev, prompt: e.target.value }))}
|
onChange={(e) =>
|
||||||
|
setConfig((prev) => ({
|
||||||
|
...prev,
|
||||||
|
prompt: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
rows={6}
|
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"
|
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..."
|
placeholder="Enter your prompt here..."
|
||||||
|
|
@ -310,13 +354,14 @@ const WidgetConfigModal = ({
|
||||||
Model & Provider
|
Model & Provider
|
||||||
</label>
|
</label>
|
||||||
<ModelSelector
|
<ModelSelector
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
setSelectedModel={setSelectedModel}
|
setSelectedModel={setSelectedModel}
|
||||||
truncateModelName={false}
|
truncateModelName={false}
|
||||||
showModelName={true}
|
showModelName={true}
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -330,12 +375,24 @@ const WidgetConfigModal = ({
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={config.refreshFrequency}
|
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"
|
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
|
<select
|
||||||
value={config.refreshUnit}
|
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"
|
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>
|
<option value="minutes">Minutes</option>
|
||||||
|
|
@ -361,10 +418,13 @@ const WidgetConfigModal = ({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 ? (
|
{previewContent ? (
|
||||||
<div className="prose prose-sm dark:prose-invert max-w-none">
|
<div className="prose prose-sm dark:prose-invert max-w-full">
|
||||||
<MarkdownRenderer thinkOverlay={true} content={previewContent} />
|
<MarkdownRenderer
|
||||||
|
thinkOverlay={true}
|
||||||
|
content={previewContent}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-black/50 dark:text-white/50 italic">
|
<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">
|
<div className="text-xs text-black/70 dark:text-white/70">
|
||||||
<h5 className="font-medium mb-2">Available Variables:</h5>
|
<h5 className="font-medium mb-2">Available Variables:</h5>
|
||||||
<div className="space-y-1">
|
<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>
|
||||||
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{current_local_datetime}}'}</code> - Current local date and time</div>
|
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
|
||||||
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{source_content_1}}'}</code> - Content from first source</div>
|
{'{{current_utc_datetime}}'}
|
||||||
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{source_content_2}}'}</code> - Content from second source</div>
|
</code>{' '}
|
||||||
<div><code className="bg-light-200 dark:bg-dark-200 px-1 rounded">{'{{location}}'}</code> - Your current location</div>
|
- 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
'use client';
|
'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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
@ -32,7 +40,12 @@ interface WidgetDisplayProps {
|
||||||
onRefresh: (widgetId: string) => void;
|
onRefresh: (widgetId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayProps) => {
|
const WidgetDisplay = ({
|
||||||
|
widget,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
onRefresh,
|
||||||
|
}: WidgetDisplayProps) => {
|
||||||
const [isFooterExpanded, setIsFooterExpanded] = useState(false);
|
const [isFooterExpanded, setIsFooterExpanded] = useState(false);
|
||||||
|
|
||||||
const formatLastUpdated = (date: Date | null) => {
|
const formatLastUpdated = (date: Date | null) => {
|
||||||
|
|
@ -95,14 +108,21 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
|
||||||
</div>
|
</div>
|
||||||
) : widget.error ? (
|
) : 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">
|
<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">
|
<div className="flex-1">
|
||||||
<p className="text-sm font-medium text-red-800 dark:text-red-300">Error Loading Content</p>
|
<p className="text-sm font-medium text-red-800 dark:text-red-300">
|
||||||
<p className="text-xs text-red-600 dark:text-red-400 mt-1">{widget.error}</p>
|
Error Loading Content
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-red-600 dark:text-red-400 mt-1">
|
||||||
|
{widget.error}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : widget.content ? (
|
) : 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} />
|
<MarkdownRenderer content={widget.content} thinkOverlay={true} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -134,10 +154,15 @@ const WidgetDisplay = ({ widget, onEdit, onDelete, onRefresh }: WidgetDisplayPro
|
||||||
{/* Sources */}
|
{/* Sources */}
|
||||||
{widget.sources.length > 0 && (
|
{widget.sources.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="space-y-1">
|
||||||
{widget.sources.map((source, index) => (
|
{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="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
|
||||||
<span className="text-gray-600 dark:text-gray-300 truncate">
|
<span className="text-gray-600 dark:text-gray-300 truncate">
|
||||||
{source.url}
|
{source.url}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
interface CardDescriptionProps
|
||||||
|
extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,13 +32,13 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
Card.displayName = 'Card';
|
Card.displayName = 'Card';
|
||||||
|
|
||||||
|
|
@ -50,7 +51,7 @@ const CardHeader = React.forwardRef<HTMLDivElement, CardHeaderProps>(
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
CardHeader.displayName = 'CardHeader';
|
CardHeader.displayName = 'CardHeader';
|
||||||
|
|
||||||
|
|
@ -60,27 +61,28 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, CardTitleProps>(
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-2xl font-semibold leading-none tracking-tight',
|
'text-2xl font-semibold leading-none tracking-tight',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</h3>
|
</h3>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
CardTitle.displayName = 'CardTitle';
|
CardTitle.displayName = 'CardTitle';
|
||||||
|
|
||||||
const CardDescription = React.forwardRef<HTMLParagraphElement, CardDescriptionProps>(
|
const CardDescription = React.forwardRef<
|
||||||
({ className, children, ...props }, ref) => (
|
HTMLParagraphElement,
|
||||||
<p
|
CardDescriptionProps
|
||||||
ref={ref}
|
>(({ className, children, ...props }, ref) => (
|
||||||
className={cn('text-sm text-muted-foreground', className)}
|
<p
|
||||||
{...props}
|
ref={ref}
|
||||||
>
|
className={cn('text-sm text-muted-foreground', className)}
|
||||||
{children}
|
{...props}
|
||||||
</p>
|
>
|
||||||
)
|
{children}
|
||||||
);
|
</p>
|
||||||
|
));
|
||||||
CardDescription.displayName = 'CardDescription';
|
CardDescription.displayName = 'CardDescription';
|
||||||
|
|
||||||
const CardContent = React.forwardRef<HTMLDivElement, CardContentProps>(
|
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}>
|
<div ref={ref} className={cn('p-6 pt-0', className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
CardContent.displayName = 'CardContent';
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
|
|
@ -101,8 +103,15 @@ const CardFooter = React.forwardRef<HTMLDivElement, CardFooterProps>(
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
CardFooter.displayName = 'CardFooter';
|
CardFooter.displayName = 'CardFooter';
|
||||||
|
|
||||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import {
|
||||||
DashboardState,
|
DashboardState,
|
||||||
DashboardConfig,
|
DashboardConfig,
|
||||||
DASHBOARD_STORAGE_KEYS,
|
DASHBOARD_STORAGE_KEYS,
|
||||||
WidgetCache
|
WidgetCache,
|
||||||
} from '@/lib/types';
|
} from '@/lib/types';
|
||||||
|
|
||||||
// Helper function to request location permission and get user's location
|
// Helper function to request location permission and get user's location
|
||||||
|
|
@ -31,7 +31,7 @@ const requestLocationPermission = async (): Promise<string | undefined> => {
|
||||||
enableHighAccuracy: true,
|
enableHighAccuracy: true,
|
||||||
timeout: 10000, // 10 seconds timeout
|
timeout: 10000, // 10 seconds timeout
|
||||||
maximumAge: 300000, // 5 minutes cache
|
maximumAge: 300000, // 5 minutes cache
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -47,14 +47,22 @@ const replaceDateTimeVariables = (prompt: string): string => {
|
||||||
// Replace UTC datetime
|
// Replace UTC datetime
|
||||||
if (processedPrompt.includes('{{current_utc_datetime}}')) {
|
if (processedPrompt.includes('{{current_utc_datetime}}')) {
|
||||||
const utcDateTime = new Date().toISOString();
|
const utcDateTime = new Date().toISOString();
|
||||||
processedPrompt = processedPrompt.replace(/\{\{current_utc_datetime\}\}/g, utcDateTime);
|
processedPrompt = processedPrompt.replace(
|
||||||
|
/\{\{current_utc_datetime\}\}/g,
|
||||||
|
utcDateTime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace local datetime
|
// Replace local datetime
|
||||||
if (processedPrompt.includes('{{current_local_datetime}}')) {
|
if (processedPrompt.includes('{{current_local_datetime}}')) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString();
|
const localDateTime = new Date(
|
||||||
processedPrompt = processedPrompt.replace(/\{\{current_local_datetime\}\}/g, localDateTime);
|
now.getTime() - now.getTimezoneOffset() * 60000,
|
||||||
|
).toISOString();
|
||||||
|
processedPrompt = processedPrompt.replace(
|
||||||
|
/\{\{current_local_datetime\}\}/g,
|
||||||
|
localDateTime,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return processedPrompt;
|
return processedPrompt;
|
||||||
|
|
@ -72,7 +80,7 @@ interface UseDashboardReturn {
|
||||||
updateWidget: (id: string, config: WidgetConfig) => void;
|
updateWidget: (id: string, config: WidgetConfig) => void;
|
||||||
deleteWidget: (id: string) => void;
|
deleteWidget: (id: string) => void;
|
||||||
refreshWidget: (id: string, forceRefresh?: boolean) => Promise<void>;
|
refreshWidget: (id: string, forceRefresh?: boolean) => Promise<void>;
|
||||||
refreshAllWidgets: () => Promise<void>;
|
refreshAllWidgets: (forceRefresh?: boolean) => Promise<void>;
|
||||||
|
|
||||||
// Storage management
|
// Storage management
|
||||||
exportDashboard: () => Promise<string>;
|
exportDashboard: () => Promise<string>;
|
||||||
|
|
@ -103,13 +111,19 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
// Save widgets to localStorage whenever they change (but not on initial load)
|
// Save widgets to localStorage whenever they change (but not on initial load)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!state.isLoading) {
|
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]);
|
}, [state.widgets, state.isLoading]);
|
||||||
|
|
||||||
// Save settings to localStorage whenever they change
|
// Save settings to localStorage whenever they change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(DASHBOARD_STORAGE_KEYS.SETTINGS, JSON.stringify(state.settings));
|
localStorage.setItem(
|
||||||
|
DASHBOARD_STORAGE_KEYS.SETTINGS,
|
||||||
|
JSON.stringify(state.settings),
|
||||||
|
);
|
||||||
}, [state.settings]);
|
}, [state.settings]);
|
||||||
|
|
||||||
const loadDashboardData = useCallback(() => {
|
const loadDashboardData = useCallback(() => {
|
||||||
|
|
@ -119,21 +133,25 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
const widgets: Widget[] = savedWidgets ? JSON.parse(savedWidgets) : [];
|
const widgets: Widget[] = savedWidgets ? JSON.parse(savedWidgets) : [];
|
||||||
|
|
||||||
// Convert date strings back to Date objects
|
// Convert date strings back to Date objects
|
||||||
widgets.forEach(widget => {
|
widgets.forEach((widget) => {
|
||||||
if (widget.lastUpdated) {
|
if (widget.lastUpdated) {
|
||||||
widget.lastUpdated = new Date(widget.lastUpdated);
|
widget.lastUpdated = new Date(widget.lastUpdated);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load settings
|
// Load settings
|
||||||
const savedSettings = localStorage.getItem(DASHBOARD_STORAGE_KEYS.SETTINGS);
|
const savedSettings = localStorage.getItem(
|
||||||
const settings = savedSettings ? JSON.parse(savedSettings) : {
|
DASHBOARD_STORAGE_KEYS.SETTINGS,
|
||||||
parallelLoading: true,
|
);
|
||||||
autoRefresh: false,
|
const settings = savedSettings
|
||||||
theme: 'auto',
|
? JSON.parse(savedSettings)
|
||||||
};
|
: {
|
||||||
|
parallelLoading: true,
|
||||||
|
autoRefresh: false,
|
||||||
|
theme: 'auto',
|
||||||
|
};
|
||||||
|
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
widgets,
|
widgets,
|
||||||
settings,
|
settings,
|
||||||
|
|
@ -141,7 +159,7 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
}));
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading dashboard data:', error);
|
console.error('Error loading dashboard data:', error);
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
error: 'Failed to load dashboard data',
|
error: 'Failed to load dashboard data',
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
@ -159,27 +177,27 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
widgets: [...prev.widgets, newWidget],
|
widgets: [...prev.widgets, newWidget],
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateWidget = useCallback((id: string, config: WidgetConfig) => {
|
const updateWidget = useCallback((id: string, config: WidgetConfig) => {
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
widgets: prev.widgets.map(widget =>
|
widgets: prev.widgets.map((widget) =>
|
||||||
widget.id === id
|
widget.id === id
|
||||||
? { ...widget, ...config, id } // Preserve the ID
|
? { ...widget, ...config, id } // Preserve the ID
|
||||||
: widget
|
: widget,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const deleteWidget = useCallback((id: string) => {
|
const deleteWidget = useCallback((id: string) => {
|
||||||
setState(prev => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
widgets: prev.widgets.filter(widget => widget.id !== id),
|
widgets: prev.widgets.filter((widget) => widget.id !== id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Also remove from cache
|
// Also remove from cache
|
||||||
|
|
@ -211,132 +229,149 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
|
|
||||||
const getCacheExpiryTime = (widget: Widget): Date => {
|
const getCacheExpiryTime = (widget: Widget): Date => {
|
||||||
const now = new 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);
|
return new Date(now.getTime() + refreshMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshWidget = useCallback(async (id: string, forceRefresh: boolean = false) => {
|
const refreshWidget = useCallback(
|
||||||
const widget = state.widgets.find(w => w.id === id);
|
async (id: string, forceRefresh: boolean = false) => {
|
||||||
if (!widget) return;
|
const widget = state.widgets.find((w) => w.id === id);
|
||||||
|
if (!widget) return;
|
||||||
|
|
||||||
// Check cache first (unless forcing refresh)
|
// Check cache first (unless forcing refresh)
|
||||||
if (!forceRefresh && isWidgetCacheValid(widget)) {
|
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
|
|
||||||
const cache = getWidgetCache();
|
const cache = getWidgetCache();
|
||||||
cache[id] = {
|
const cachedData = cache[widget.id];
|
||||||
content: result.content,
|
setState((prev) => ({
|
||||||
lastFetched: now,
|
|
||||||
expiresAt: getCacheExpiryTime(widget),
|
|
||||||
};
|
|
||||||
localStorage.setItem(DASHBOARD_STORAGE_KEYS.CACHE, JSON.stringify(cache));
|
|
||||||
} else {
|
|
||||||
setState(prev => ({
|
|
||||||
...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.id === id
|
||||||
? {
|
? {
|
||||||
...w,
|
...w,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: result.error || 'Failed to refresh widget',
|
error: 'Network error: Failed to refresh widget',
|
||||||
}
|
}
|
||||||
: w
|
: w,
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
setState(prev => ({
|
[state.widgets],
|
||||||
...prev,
|
);
|
||||||
widgets: prev.widgets.map(w =>
|
|
||||||
w.id === id
|
|
||||||
? {
|
|
||||||
...w,
|
|
||||||
isLoading: false,
|
|
||||||
error: 'Network error: Failed to refresh widget',
|
|
||||||
}
|
|
||||||
: w
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [state.widgets]);
|
|
||||||
|
|
||||||
const refreshAllWidgets = useCallback(async () => {
|
const refreshAllWidgets = useCallback(
|
||||||
const activeWidgets = state.widgets.filter(w => !w.isLoading);
|
async (forceRefresh = false) => {
|
||||||
|
const activeWidgets = state.widgets.filter((w) => !w.isLoading);
|
||||||
|
|
||||||
if (state.settings.parallelLoading) {
|
if (state.settings.parallelLoading) {
|
||||||
// Refresh all widgets in parallel (force refresh)
|
// Refresh all widgets in parallel (force refresh)
|
||||||
await Promise.all(activeWidgets.map(widget => refreshWidget(widget.id, true)));
|
await Promise.all(
|
||||||
} else {
|
activeWidgets.map((widget) => refreshWidget(widget.id, forceRefresh)),
|
||||||
// Refresh widgets sequentially (force refresh)
|
);
|
||||||
for (const widget of activeWidgets) {
|
} else {
|
||||||
await refreshWidget(widget.id, true);
|
// 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 exportDashboard = useCallback(async (): Promise<string> => {
|
||||||
const dashboardConfig: DashboardConfig = {
|
const dashboardConfig: DashboardConfig = {
|
||||||
|
|
@ -349,45 +384,57 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
return JSON.stringify(dashboardConfig, null, 2);
|
return JSON.stringify(dashboardConfig, null, 2);
|
||||||
}, [state.widgets, state.settings]);
|
}, [state.widgets, state.settings]);
|
||||||
|
|
||||||
const importDashboard = useCallback(async (configJson: string): Promise<void> => {
|
const importDashboard = useCallback(
|
||||||
try {
|
async (configJson: string): Promise<void> => {
|
||||||
const config: DashboardConfig = JSON.parse(configJson);
|
try {
|
||||||
|
const config: DashboardConfig = JSON.parse(configJson);
|
||||||
|
|
||||||
// Validate the config structure
|
// Validate the config structure
|
||||||
if (!config.widgets || !Array.isArray(config.widgets)) {
|
if (!config.widgets || !Array.isArray(config.widgets)) {
|
||||||
throw new Error('Invalid dashboard configuration: missing or invalid widgets array');
|
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(() => {
|
const clearCache = useCallback(() => {
|
||||||
localStorage.removeItem(DASHBOARD_STORAGE_KEYS.CACHE);
|
localStorage.removeItem(DASHBOARD_STORAGE_KEYS.CACHE);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const updateSettings = useCallback((newSettings: Partial<DashboardConfig['settings']>) => {
|
const updateSettings = useCallback(
|
||||||
setState(prev => ({
|
(newSettings: Partial<DashboardConfig['settings']>) => {
|
||||||
...prev,
|
setState((prev) => ({
|
||||||
settings: { ...prev.settings, ...newSettings },
|
...prev,
|
||||||
}));
|
settings: { ...prev.settings, ...newSettings },
|
||||||
}, []);
|
}));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue