From 7253cbc89c6cb55b2aecfcaf5c4094feb65a82e2 Mon Sep 17 00:00:00 2001 From: Willie Zutz Date: Wed, 23 Jul 2025 00:08:00 -0600 Subject: [PATCH] feat(dashboard): refactor widget processing to support dynamic tool selection - Updated the widget processing API to accept tool names as an optional parameter. - Consolidated tool imports and created an `allTools` array for easier management. - Added a new ToolSelector component for selecting tools in the widget configuration modal. - Enhanced date difference and timezone conversion tools with improved descriptions and error handling. - Refactored types for widgets and dashboard to streamline the codebase and improve type safety. - Removed deprecated types and organized type definitions into separate files for better maintainability. --- src/app/api/dashboard/process-widget/route.ts | 51 +++--- src/app/api/tools/route.ts | 20 +++ src/app/dashboard/page.tsx | 2 +- src/app/settings/page.tsx | 65 +++++++ .../MessageInputActions/ToolSelector.tsx | 166 ++++++++++++++++++ src/components/ThinkBox.tsx | 4 +- .../dashboard/WidgetConfigModal.tsx | 58 +++--- src/components/dashboard/WidgetDisplay.tsx | 21 +-- src/lib/hooks/useDashboard.ts | 64 +++---- src/lib/tools/dateDifference.ts | 81 ++++++--- src/lib/tools/index.ts | 9 +- src/lib/tools/timezoneConverter.ts | 37 ++-- src/lib/types.ts | 83 --------- src/lib/types/api.ts | 19 ++ src/lib/types/cache.ts | 8 + src/lib/types/dashboard.ts | 27 +++ src/lib/types/widget.ts | 25 +++ src/lib/utils/dates.ts | 20 +-- 18 files changed, 513 insertions(+), 247 deletions(-) create mode 100644 src/app/api/tools/route.ts create mode 100644 src/components/MessageInputActions/ToolSelector.tsx delete mode 100644 src/lib/types.ts create mode 100644 src/lib/types/api.ts create mode 100644 src/lib/types/cache.ts create mode 100644 src/lib/types/dashboard.ts create mode 100644 src/lib/types/widget.ts diff --git a/src/app/api/dashboard/process-widget/route.ts b/src/app/api/dashboard/process-widget/route.ts index a92bb34..39cb86c 100644 --- a/src/app/api/dashboard/process-widget/route.ts +++ b/src/app/api/dashboard/process-widget/route.ts @@ -12,21 +12,11 @@ import { } from '@/lib/config'; import { ChatOllama } from '@langchain/ollama'; import { createReactAgent } from '@langchain/langgraph/prebuilt'; -import { timezoneConverterTool, dateDifferenceTool } from '@/lib/tools'; +import { allTools } from '@/lib/tools'; +import { Source } from '@/lib/types/widget'; +import { WidgetProcessRequest } from '@/lib/types/api'; import axios from 'axios'; -interface Source { - url: string; - type: 'Web Page' | 'HTTP Data'; -} - -interface WidgetProcessRequest { - sources: Source[]; - prompt: string; - provider: string; - model: string; -} - // Helper function to fetch content from a single source async function fetchSourceContent( source: Source, @@ -129,6 +119,7 @@ async function processWithLLM( prompt: string, provider: string, model: string, + tool_names?: string[], ): Promise { const llm = await getLLMInstance(provider, model); @@ -136,10 +127,11 @@ async function processWithLLM( throw new Error(`Invalid or unavailable model: ${provider}/${model}`); } - const tools = [ - timezoneConverterTool, - dateDifferenceTool, - ]; + // Filter tools based on tool_names parameter + const tools = + tool_names && tool_names.length > 0 + ? allTools.filter((tool) => tool_names.includes(tool.name)) + : []; // Create the React agent with tools const agent = createReactAgent({ @@ -148,14 +140,17 @@ async function processWithLLM( }); // Invoke the agent with the prompt - const response = await agent.invoke({ - messages: [ - //new SystemMessage({ content: `You have the following tools available: ${tools.map(tool => tool.name).join(', ')} use them as necessary to complete the task.` }), - new HumanMessage({ content: prompt }) - ], - },{ - recursionLimit: 15, // Limit recursion depth to prevent infinite loops - }); + const response = await agent.invoke( + { + messages: [ + //new SystemMessage({ content: `You have the following tools available: ${tools.map(tool => tool.name).join(', ')} use them as necessary to complete the task.` }), + new HumanMessage({ content: prompt }), + ], + }, + { + recursionLimit: 15, // Limit recursion depth to prevent infinite loops + }, + ); // Extract the final response content const lastMessage = response.messages[response.messages.length - 1]; @@ -200,7 +195,10 @@ export async function POST(request: NextRequest) { sourceContents = sourceResults.map((result) => result.content); sourcesFetched = sourceContents.filter((content) => content).length; // If all sources failed, return error - if (sourceContents.length > 0 && sourceContents.every((content) => !content)) { + if ( + sourceContents.length > 0 && + sourceContents.every((content) => !content) + ) { return NextResponse.json( { error: 'Failed to fetch content from all sources' }, { status: 500 }, @@ -218,6 +216,7 @@ export async function POST(request: NextRequest) { processedPrompt, body.provider, body.model, + body.tool_names, ); console.log('LLM response:', llmResponse); diff --git a/src/app/api/tools/route.ts b/src/app/api/tools/route.ts new file mode 100644 index 0000000..6a6f794 --- /dev/null +++ b/src/app/api/tools/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server'; +import { allTools } from '@/lib/tools'; + +export async function GET() { + try { + // Map over all tools to extract name and description + const toolsList = allTools.map((tool) => ({ + name: tool.name, + description: tool.description, + })); + + return NextResponse.json(toolsList); + } catch (error) { + console.error('Error fetching tools:', error); + return NextResponse.json( + { error: 'Failed to fetch available tools' }, + { status: 500 }, + ); + } +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 9910292..8537418 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -21,7 +21,7 @@ import { 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 { Widget, WidgetConfig } from '@/lib/types/widget'; import { toast } from 'sonner'; const DashboardPage = () => { diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 9dddaf0..a8d3c1e 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -13,6 +13,8 @@ import { RotateCcw, ChevronDown, ChevronRight, + Eye, + EyeOff, } from 'lucide-react'; import { useEffect, useState, useRef } from 'react'; import { cn } from '@/lib/utils'; @@ -604,6 +606,39 @@ export default function SettingsPage() { } }; + const handleProviderVisibilityToggle = async ( + providerModels: Record, + showAll: boolean, + ) => { + const modelKeys = Object.keys(providerModels); + let updatedHiddenModels: string[]; + + if (showAll) { + // Show all models in this provider, remove all from hidden list + updatedHiddenModels = hiddenModels.filter( + (modelKey) => !modelKeys.includes(modelKey), + ); + } else { + // Hide all models in this provider, add all to hidden list + const modelsToHide = modelKeys.filter( + (modelKey) => !hiddenModels.includes(modelKey), + ); + updatedHiddenModels = [...hiddenModels, ...modelsToHide]; + } + + // Update local state immediately + setHiddenModels(updatedHiddenModels); + + // Persist changes to backend + try { + await saveConfig('hiddenModels', updatedHiddenModels); + } catch (error) { + console.error('Failed to save hidden models:', error); + // Revert local state on error + setHiddenModels(hiddenModels); + } + }; + const toggleProviderExpansion = (providerId: string) => { setExpandedProviders((prev) => { const newSet = new Set(prev); @@ -1593,6 +1628,36 @@ export default function SettingsPage() { {isExpanded && (
+
+ + +
{modelEntries.map(([modelKey, model]) => (
void; +} + +const ToolSelector = ({ + selectedToolNames, + onSelectedToolNamesChange, +}: ToolSelectorProps) => { + const [availableTools, setAvailableTools] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const fetchTools = async () => { + try { + setIsLoading(true); + const response = await fetch('/api/tools'); + if (response.ok) { + const tools = await response.json(); + setAvailableTools(tools); + + // Check if any currently selected tool names are not in the API response + const availableToolNames = tools.map((tool: Tool) => tool.name); + const validSelectedNames = selectedToolNames.filter((name) => + availableToolNames.includes(name), + ); + + // If some selected names are no longer available, update the selection + if (validSelectedNames.length !== selectedToolNames.length) { + onSelectedToolNamesChange(validSelectedNames); + } + } else { + console.error('Failed to load tools.'); + } + } catch (error) { + console.error('Error loading tools.'); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + // Only fetch tools once when the component mounts + fetchTools(); + }, [selectedToolNames, onSelectedToolNamesChange]); + + const handleToggleTool = (toolName: string) => { + const newSelectedNames = selectedToolNames.includes(toolName) + ? selectedToolNames.filter((name) => name !== toolName) + : [...selectedToolNames, toolName]; + onSelectedToolNamesChange(newSelectedNames); + }; + + const selectedCount = selectedToolNames.length; + + return ( + + {({ open }) => ( + <> + 0 + ? 'text-[#24A0ED] hover:text-blue-200' + : 'text-black/60 hover:text-black/30 dark:text-white/60 dark:hover:*:text-white/30', + )} + title="Select Tools" + > + + {selectedCount > 0 ? {selectedCount} : null} + + + + +
+
+

+ Select Tools +

+

+ Choose tools to assist the AI. +

+
+ {isLoading ? ( +
+ +
+ ) : ( +
+ {availableTools.length === 0 && ( +

+ No tools available. +

+ )} + + {availableTools.map((tool) => ( +
handleToggleTool(tool.name)} + className="flex items-start gap-2.5 p-2.5 rounded-md hover:bg-light-100 dark:hover:bg-dark-100 cursor-pointer" + > + {selectedToolNames.includes(tool.name) ? ( + + ) : ( + + )} +
+ + {tool.name.replace(/_/g, ' ')} + +

+ {tool.description} +

+
+
+ ))} +
+ )} +
+
+
+ + )} +
+ ); +}; + +export default ToolSelector; diff --git a/src/components/ThinkBox.tsx b/src/components/ThinkBox.tsx index c54ea4e..b694459 100644 --- a/src/components/ThinkBox.tsx +++ b/src/components/ThinkBox.tsx @@ -11,13 +11,13 @@ interface ThinkBoxProps { } const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => { + const [internalExpanded, setInternalExpanded] = useState(false); + // Don't render anything if content is empty if (!content) { return null; } - const [internalExpanded, setInternalExpanded] = useState(false); - // Use external expanded state if provided, otherwise use internal state const isExpanded = expanded !== undefined ? expanded : internalExpanded; const handleToggle = diff --git a/src/components/dashboard/WidgetConfigModal.tsx b/src/components/dashboard/WidgetConfigModal.tsx index 9651e23..1f47ddf 100644 --- a/src/components/dashboard/WidgetConfigModal.tsx +++ b/src/components/dashboard/WidgetConfigModal.tsx @@ -11,6 +11,8 @@ import { X, Plus, Trash2, Play, Save } from 'lucide-react'; import { Fragment, useState, useEffect } from 'react'; import MarkdownRenderer from '@/components/MarkdownRenderer'; import ModelSelector from '@/components/MessageInputActions/ModelSelector'; +import ToolSelector from '@/components/MessageInputActions/ToolSelector'; +import { WidgetConfig, Source } from '@/lib/types/widget'; // Helper function to replace date/time variables in prompts on the client side const replaceDateTimeVariables = (prompt: string): string => { @@ -40,22 +42,6 @@ const replaceDateTimeVariables = (prompt: string): string => { return processedPrompt; }; -interface Source { - url: string; - type: 'Web Page' | 'HTTP Data'; -} - -interface WidgetConfig { - id?: string; - title: string; - sources: Source[]; - prompt: string; - provider: string; - model: string; - refreshFrequency: number; - refreshUnit: 'minutes' | 'hours'; -} - interface WidgetConfigModalProps { isOpen: boolean; onClose: () => void; @@ -85,6 +71,7 @@ const WidgetConfigModal = ({ provider: string; model: string; } | null>(null); + const [selectedTools, setSelectedTools] = useState([]); // Update config when editingWidget changes useEffect(() => { @@ -102,6 +89,7 @@ const WidgetConfigModal = ({ provider: editingWidget.provider, model: editingWidget.model, }); + setSelectedTools(editingWidget.tool_names || []); } else { // Reset to default values for new widget setConfig({ @@ -117,6 +105,7 @@ const WidgetConfigModal = ({ provider: 'openai', model: 'gpt-4', }); + setSelectedTools([]); } }, [editingWidget]); @@ -140,6 +129,7 @@ const WidgetConfigModal = ({ const filteredConfig = { ...config, sources: config.sources.filter((s) => s.url.trim()), + tool_names: selectedTools, }; onSave(filteredConfig); @@ -172,6 +162,7 @@ const WidgetConfigModal = ({ prompt: processedPrompt, provider: config.provider, model: config.model, + tool_names: selectedTools, }), }); @@ -366,6 +357,21 @@ const WidgetConfigModal = ({

+ {/* Tool Selection */} +
+ + +

+ Select tools to assist the AI in processing your widget. + Your model must support tool calling. +

+
+ {/* Refresh Frequency */}
) : (
- Click "Run Preview" to see how your widget will look + Click "Run Preview" to see how your widget will look
)}
@@ -476,24 +482,6 @@ const WidgetConfigModal = ({
-
-
Available Tools (Your model must support tool calling):
-
-
- - {'date_difference'} - {' '} - - Get the difference between two dates (Works best with ISO 8601 formatted dates) -
-
- - {'timezone_converter'} - {' '} - - Convert a date from one timezone to another (Works best with ISO 8601 formatted dates) - - Expects target timezone in the IANA format (e.g., 'America/New_York', 'Europe/London', etc.) -
-
-
diff --git a/src/components/dashboard/WidgetDisplay.tsx b/src/components/dashboard/WidgetDisplay.tsx index e794dcf..c4a916d 100644 --- a/src/components/dashboard/WidgetDisplay.tsx +++ b/src/components/dashboard/WidgetDisplay.tsx @@ -11,28 +11,9 @@ import { } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import MarkdownRenderer from '@/components/MarkdownRenderer'; +import { Widget } from '@/lib/types/widget'; import { useState } from 'react'; -interface Source { - url: string; - type: 'Web Page' | 'HTTP Data'; -} - -interface Widget { - id: string; - title: string; - sources: Source[]; - prompt: string; - provider: string; - model: string; - refreshFrequency: number; - refreshUnit: 'minutes' | 'hours'; - lastUpdated: Date | null; - isLoading: boolean; - content: string | null; - error: string | null; -} - interface WidgetDisplayProps { widget: Widget; onEdit: (widget: Widget) => void; diff --git a/src/lib/hooks/useDashboard.ts b/src/lib/hooks/useDashboard.ts index a0ca31f..1a093f2 100644 --- a/src/lib/hooks/useDashboard.ts +++ b/src/lib/hooks/useDashboard.ts @@ -1,12 +1,11 @@ import { useState, useEffect, useCallback } from 'react'; +import { Widget, WidgetConfig } from '@/lib/types/widget'; import { - Widget, - WidgetConfig, DashboardState, DashboardConfig, DASHBOARD_STORAGE_KEYS, - WidgetCache, -} from '@/lib/types'; +} from '@/lib/types/dashboard'; +import { WidgetCache } from '@/lib/types/cache'; // Helper function to request location permission and get user's location const requestLocationPermission = async (): Promise => { @@ -103,29 +102,6 @@ export const useDashboard = (): UseDashboardReturn => { }, }); - // Load dashboard data from localStorage on mount - useEffect(() => { - loadDashboardData(); - }, []); - - // 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), - ); - } - }, [state.widgets, state.isLoading]); - - // Save settings to localStorage whenever they change - useEffect(() => { - localStorage.setItem( - DASHBOARD_STORAGE_KEYS.SETTINGS, - JSON.stringify(state.settings), - ); - }, [state.settings]); - const loadDashboardData = useCallback(() => { try { // Load widgets @@ -167,6 +143,29 @@ export const useDashboard = (): UseDashboardReturn => { } }, []); + // Load dashboard data from localStorage on mount + useEffect(() => { + loadDashboardData(); + }, [loadDashboardData]); + + // 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), + ); + } + }, [state.widgets, state.isLoading]); + + // Save settings to localStorage whenever they change + useEffect(() => { + localStorage.setItem( + DASHBOARD_STORAGE_KEYS.SETTINGS, + JSON.stringify(state.settings), + ); + }, [state.settings]); + const addWidget = useCallback((config: WidgetConfig) => { const newWidget: Widget = { ...config, @@ -215,7 +214,7 @@ export const useDashboard = (): UseDashboardReturn => { } }; - const isWidgetCacheValid = (widget: Widget): boolean => { + const isWidgetCacheValid = useCallback((widget: Widget): boolean => { const cache = getWidgetCache(); const cachedData = cache[widget.id]; @@ -225,15 +224,15 @@ export const useDashboard = (): UseDashboardReturn => { const expiresAt = new Date(cachedData.expiresAt); return now < expiresAt; - }; + }, []); - const getCacheExpiryTime = (widget: Widget): Date => { + const getCacheExpiryTime = useCallback((widget: Widget): Date => { const now = new Date(); const refreshMs = widget.refreshFrequency * (widget.refreshUnit === 'hours' ? 3600000 : 60000); return new Date(now.getTime() + refreshMs); - }; + }, []); const refreshWidget = useCallback( async (id: string, forceRefresh: boolean = false) => { @@ -287,6 +286,7 @@ export const useDashboard = (): UseDashboardReturn => { prompt: processedPrompt, provider: widget.provider, model: widget.model, + tool_names: widget.tool_names, location, }), }); @@ -351,7 +351,7 @@ export const useDashboard = (): UseDashboardReturn => { })); } }, - [state.widgets], + [state.widgets, isWidgetCacheValid, getCacheExpiryTime], ); const refreshAllWidgets = useCallback( diff --git a/src/lib/tools/dateDifference.ts b/src/lib/tools/dateDifference.ts index 7a1bc9f..cd75446 100644 --- a/src/lib/tools/dateDifference.ts +++ b/src/lib/tools/dateDifference.ts @@ -9,51 +9,76 @@ import { parseDate, getDateParseErrorMessage } from '@/lib/utils/dates'; export const dateDifferenceTool = tool( ({ startDate, endDate }: { startDate: string; endDate: string }): string => { try { - console.log(`Calculating difference between "${startDate}" and "${endDate}"`); - + console.log( + `Calculating difference between "${startDate}" and "${endDate}"`, + ); + // Parse the dates using the extracted utility function const startDateTime = parseDate(startDate); const endDateTime = parseDate(endDate); - + // Check if dates are valid if (!startDateTime.isValid) { return getDateParseErrorMessage(startDate, startDateTime, 'start date'); } - + if (!endDateTime.isValid) { return getDateParseErrorMessage(endDate, endDateTime, 'end date'); } - + // Create an interval between the two dates for accurate calculations const interval = Interval.fromDateTimes(startDateTime, endDateTime); - + if (!interval.isValid) { return `Error: Invalid interval between dates. Reason: ${interval.invalidReason}`; } - + // Calculate differences in various units using Luxon's accurate methods - const diffMilliseconds = Math.abs(endDateTime.diff(startDateTime).toMillis()); - const diffSeconds = Math.abs(endDateTime.diff(startDateTime, 'seconds').seconds); - const diffMinutes = Math.abs(endDateTime.diff(startDateTime, 'minutes').minutes); - const diffHours = Math.abs(endDateTime.diff(startDateTime, 'hours').hours); + const diffMilliseconds = Math.abs( + endDateTime.diff(startDateTime).toMillis(), + ); + const diffSeconds = Math.abs( + endDateTime.diff(startDateTime, 'seconds').seconds, + ); + const diffMinutes = Math.abs( + endDateTime.diff(startDateTime, 'minutes').minutes, + ); + const diffHours = Math.abs( + endDateTime.diff(startDateTime, 'hours').hours, + ); const diffDays = Math.abs(endDateTime.diff(startDateTime, 'days').days); - const diffWeeks = Math.abs(endDateTime.diff(startDateTime, 'weeks').weeks); - const diffMonths = Math.abs(endDateTime.diff(startDateTime, 'months').months); - const diffYears = Math.abs(endDateTime.diff(startDateTime, 'years').years); - + const diffWeeks = Math.abs( + endDateTime.diff(startDateTime, 'weeks').weeks, + ); + const diffMonths = Math.abs( + endDateTime.diff(startDateTime, 'months').months, + ); + const diffYears = Math.abs( + endDateTime.diff(startDateTime, 'years').years, + ); + // Get multi-unit breakdown for more human-readable output - const multiUnitDiff = endDateTime.diff(startDateTime, ['years', 'months', 'days', 'hours', 'minutes', 'seconds']).toObject(); - + const multiUnitDiff = endDateTime + .diff(startDateTime, [ + 'years', + 'months', + 'days', + 'hours', + 'minutes', + 'seconds', + ]) + .toObject(); + // Determine which date is earlier const isStartEarlier = startDateTime <= endDateTime; const earlierDate = isStartEarlier ? startDateTime : endDateTime; const laterDate = isStartEarlier ? endDateTime : startDateTime; - + // Format the dates for display with ISO format const formatDate = (dt: DateTime) => { return `${dt.toLocaleString(DateTime.DATETIME_FULL)} (${dt.toISO()})`; }; - + let result = `Date difference calculation: From: ${formatDate(earlierDate)} To: ${formatDate(laterDate)} @@ -93,7 +118,6 @@ Human-readable breakdown:`; Direction: Start date is ${isStartEarlier ? 'earlier than' : 'later than'} the end date.`; return result; - } catch (error) { console.error('Error during date difference calculation:', error); return `Error: ${error instanceof Error ? error.message : 'Unknown error occurred during date difference calculation'}`; @@ -101,10 +125,19 @@ Direction: Start date is ${isStartEarlier ? 'earlier than' : 'later than'} the e }, { name: 'date_difference', - description: 'Calculate the time difference between two dates. Returns a detailed breakdown of years, months, days, hours, etc. If no timezone is specified, dates will be treated as local to the server time.', + description: + 'Get the difference between two dates (Works best with ISO 8601 formatted dates)', schema: z.object({ - startDate: z.string().describe('The start date (e.g., "2024-01-15", "Jan 15, 2024", "2024-01-15 14:30:00Z", "2024-01-15T14:30:00-05:00")'), - endDate: z.string().describe('The end date (e.g., "2024-12-25", "Dec 25, 2024", "2024-12-25 18:00:00Z", "2024-12-25T18:00:00-05:00")'), + startDate: z + .string() + .describe( + 'The start date (e.g., "2024-01-15", "Jan 15, 2024", "2024-01-15 14:30:00Z", "2024-01-15T14:30:00-05:00")', + ), + endDate: z + .string() + .describe( + 'The end date (e.g., "2024-12-25", "Dec 25, 2024", "2024-12-25 18:00:00Z", "2024-12-25T18:00:00-05:00")', + ), }), - } + }, ); diff --git a/src/lib/tools/index.ts b/src/lib/tools/index.ts index 91c1b83..65217f3 100644 --- a/src/lib/tools/index.ts +++ b/src/lib/tools/index.ts @@ -1,2 +1,7 @@ -export { timezoneConverterTool } from './timezoneConverter'; -export { dateDifferenceTool } from './dateDifference'; +import { timezoneConverterTool } from './timezoneConverter'; +import { dateDifferenceTool } from './dateDifference'; + +export { timezoneConverterTool, dateDifferenceTool }; + +// Array containing all available tools +export const allTools = [timezoneConverterTool, dateDifferenceTool]; diff --git a/src/lib/tools/timezoneConverter.ts b/src/lib/tools/timezoneConverter.ts index c8482d9..cb9969c 100644 --- a/src/lib/tools/timezoneConverter.ts +++ b/src/lib/tools/timezoneConverter.ts @@ -7,16 +7,21 @@ import { parseDate, getDateParseErrorMessage } from '@/lib/utils/dates'; * Tool that converts a date from one timezone to another */ export const timezoneConverterTool = tool( - ({ dateString, toTimezone }: { - dateString: string; - toTimezone: string; + ({ + dateString, + toTimezone, + }: { + dateString: string; + toTimezone: string; }): string => { try { - console.log(`Converting date "${dateString}" to timezone "${toTimezone}"`); - + console.log( + `Converting date "${dateString}" to timezone "${toTimezone}"`, + ); + // Parse the date string using the extracted utility function const dateTime = parseDate(dateString); - + // Check if the parsed date is valid if (!dateTime.isValid) { return getDateParseErrorMessage(dateString, dateTime); @@ -24,7 +29,7 @@ export const timezoneConverterTool = tool( // Convert to target timezone const targetDateTime = dateTime.setZone(toTimezone); - + // Check if the target timezone is valid if (!targetDateTime.isValid) { return `Error: Invalid timezone "${toTimezone}". Please use valid timezone identifiers like "America/New_York", "Europe/London", "Asia/Tokyo", etc. Reason: ${targetDateTime.invalidReason}`; @@ -40,7 +45,6 @@ Target: ${targetISO} (${targetDateTime.zoneName})`; console.log(output); return output; - } catch (error) { console.error('Error during timezone conversion:', error); return `Error: ${error instanceof Error ? error.message : 'Unknown error occurred during timezone conversion'}`; @@ -48,10 +52,19 @@ Target: ${targetISO} (${targetDateTime.zoneName})`; }, { name: 'timezone_converter', - description: 'Convert a date from one timezone to another timezone. Supports standard timezone identifiers.', + description: + 'Convert a date from one timezone to another (Works best with ISO 8601 formatted dates) - Expects target timezone in the IANA format (e.g., "America/New_York", "Europe/London", etc.)', schema: z.object({ - dateString: z.string().describe('The date string to convert. This must include the timezone offset or Z for UTC (e.g., "2023-10-01T00:00:00Z" or "2025-08-10T00:00:00-06:00")'), - toTimezone: z.string().describe('Target timezone to convert to (e.g., "Asia/Tokyo", "America/Los_Angeles", "Europe/Paris")'), + dateString: z + .string() + .describe( + 'The date string to convert. This must include the timezone offset or Z for UTC (e.g., "2023-10-01T00:00:00Z" or "2025-08-10T00:00:00-06:00")', + ), + toTimezone: z + .string() + .describe( + 'Target timezone to convert to (e.g., "Asia/Tokyo", "America/Los_Angeles", "Europe/Paris")', + ), }), - } + }, ); diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index 7291180..0000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Dashboard-related TypeScript type definitions - -export interface Source { - url: string; - type: 'Web Page' | 'HTTP Data'; -} - -export interface Widget { - id: string; - title: string; - sources: Source[]; - prompt: string; - provider: string; - model: string; - refreshFrequency: number; - refreshUnit: 'minutes' | 'hours'; - lastUpdated: Date | null; - isLoading: boolean; - content: string | null; - error: string | null; -} - -export interface WidgetConfig { - id?: string; - title: string; - sources: Source[]; - prompt: string; - provider: string; - model: string; - refreshFrequency: number; - refreshUnit: 'minutes' | 'hours'; -} - -export interface DashboardConfig { - widgets: Widget[]; - settings: { - parallelLoading: boolean; - autoRefresh: boolean; - theme: 'auto' | 'light' | 'dark'; - }; - lastExport?: Date; - version: string; -} - -export interface DashboardState { - widgets: Widget[]; - isLoading: boolean; - error: string | null; - settings: DashboardConfig['settings']; -} - -// Widget processing API types -export interface WidgetProcessRequest { - sources: Source[]; - prompt: string; - provider: string; - model: string; -} - -export interface WidgetProcessResponse { - content: string; - success: boolean; - sourcesFetched?: number; - totalSources?: number; - warnings?: string[]; - error?: string; -} - -// Local storage keys -export const DASHBOARD_STORAGE_KEYS = { - WIDGETS: 'perplexica_dashboard_widgets', - SETTINGS: 'perplexica_dashboard_settings', - CACHE: 'perplexica_dashboard_cache', -} as const; - -// Cache types -export interface WidgetCache { - [widgetId: string]: { - content: string; - lastFetched: Date; - expiresAt: Date; - }; -} diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts new file mode 100644 index 0000000..fbc45c0 --- /dev/null +++ b/src/lib/types/api.ts @@ -0,0 +1,19 @@ +// API request/response types +import { Source } from './widget'; + +export interface WidgetProcessRequest { + sources: Source[]; + prompt: string; + provider: string; + model: string; + tool_names?: string[]; +} + +export interface WidgetProcessResponse { + content: string; + success: boolean; + sourcesFetched?: number; + totalSources?: number; + warnings?: string[]; + error?: string; +} diff --git a/src/lib/types/cache.ts b/src/lib/types/cache.ts new file mode 100644 index 0000000..c1406f9 --- /dev/null +++ b/src/lib/types/cache.ts @@ -0,0 +1,8 @@ +// Cache-related types +export interface WidgetCache { + [widgetId: string]: { + content: string; + lastFetched: Date; + expiresAt: Date; + }; +} diff --git a/src/lib/types/dashboard.ts b/src/lib/types/dashboard.ts new file mode 100644 index 0000000..2849412 --- /dev/null +++ b/src/lib/types/dashboard.ts @@ -0,0 +1,27 @@ +// Dashboard configuration and state types +import { Widget } from './widget'; + +export interface DashboardConfig { + widgets: Widget[]; + settings: { + parallelLoading: boolean; + autoRefresh: boolean; + theme: 'auto' | 'light' | 'dark'; + }; + lastExport?: Date; + version: string; +} + +export interface DashboardState { + widgets: Widget[]; + isLoading: boolean; + error: string | null; + settings: DashboardConfig['settings']; +} + +// Local storage keys +export const DASHBOARD_STORAGE_KEYS = { + WIDGETS: 'perplexica_dashboard_widgets', + SETTINGS: 'perplexica_dashboard_settings', + CACHE: 'perplexica_dashboard_cache', +} as const; diff --git a/src/lib/types/widget.ts b/src/lib/types/widget.ts new file mode 100644 index 0000000..1605df4 --- /dev/null +++ b/src/lib/types/widget.ts @@ -0,0 +1,25 @@ +// Core domain types for widgets +export interface Source { + url: string; + type: 'Web Page' | 'HTTP Data'; +} + +export interface WidgetConfig { + id?: string; + title: string; + sources: Source[]; + prompt: string; + provider: string; + model: string; + refreshFrequency: number; + refreshUnit: 'minutes' | 'hours'; + tool_names?: string[]; +} + +export interface Widget extends WidgetConfig { + id: string; + lastUpdated: Date | null; + isLoading: boolean; + content: string | null; + error: string | null; +} diff --git a/src/lib/utils/dates.ts b/src/lib/utils/dates.ts index 983f767..a509d9b 100644 --- a/src/lib/utils/dates.ts +++ b/src/lib/utils/dates.ts @@ -3,27 +3,27 @@ import { DateTime } from 'luxon'; /** * Parses a date string using multiple Luxon formats with fallback to JavaScript Date parsing. * Preserves timezone information when available using setZone: true. - * + * * @param dateString - The date string to parse * @returns A parsed DateTime object */ export function parseDate(dateString: string): DateTime { // Try to parse as ISO format first (most common) let dateTime = DateTime.fromISO(dateString, { setZone: true }); - + // If ISO parsing fails, try other common formats if (!dateTime.isValid) { dateTime = DateTime.fromRFC2822(dateString, { setZone: true }); } - + if (!dateTime.isValid) { dateTime = DateTime.fromHTTP(dateString, { setZone: true }); } - + if (!dateTime.isValid) { dateTime = DateTime.fromSQL(dateString, { setZone: true }); } - + // If all parsing attempts fail, try JavaScript Date parsing as fallback if (!dateTime.isValid) { const jsDate = new Date(dateString); @@ -31,22 +31,22 @@ export function parseDate(dateString: string): DateTime { dateTime = DateTime.fromJSDate(jsDate); } } - + return dateTime; } /** * Generates a standardized error message for date parsing failures. - * + * * @param dateString - The original date string that failed to parse * @param dateTime - The invalid DateTime object * @param fieldName - Optional field name for more specific error messages (e.g., "start date", "end date") * @returns A formatted error message */ export function getDateParseErrorMessage( - dateString: string, - dateTime: DateTime, - fieldName: string = 'date' + dateString: string, + dateTime: DateTime, + fieldName: string = 'date', ): string { return `Error: Unable to parse ${fieldName} "${dateString}". Please provide a valid date format (ISO 8601, RFC 2822, SQL, or common date formats). Reason: ${dateTime.invalidReason}`; }