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.
This commit is contained in:
Willie Zutz 2025-07-23 00:08:00 -06:00
parent 1f78b94243
commit 7253cbc89c
18 changed files with 513 additions and 247 deletions

View file

@ -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<string> {
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);

View file

@ -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 },
);
}
}

View file

@ -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 = () => {

View file

@ -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<string, any>,
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 && (
<div className="p-3 bg-light-100 dark:bg-dark-100 border-t border-light-200 dark:border-dark-200">
<div className="flex justify-end mb-3 space-x-2">
<button
onClick={(e) => {
e.stopPropagation();
handleProviderVisibilityToggle(
models,
true,
);
}}
className="px-3 py-1.5 text-xs rounded-md bg-green-100 hover:bg-green-200 dark:bg-green-900/30 dark:hover:bg-green-900/50 text-green-700 dark:text-green-400 flex items-center gap-1.5 transition-colors"
title="Show all models in this provider"
>
<Eye size={14} />
Show All
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleProviderVisibilityToggle(
models,
false,
);
}}
className="px-3 py-1.5 text-xs rounded-md bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-700 dark:text-red-400 flex items-center gap-1.5 transition-colors"
title="Hide all models in this provider"
>
<EyeOff size={14} />
Hide All
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{modelEntries.map(([modelKey, model]) => (
<div

View file

@ -0,0 +1,166 @@
import {
Wrench,
ChevronDown,
CheckSquare,
Square,
Loader2,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverButton,
PopoverPanel,
Transition,
} from '@headlessui/react';
import { Fragment, useEffect, useState } from 'react';
interface Tool {
name: string;
description: string;
}
interface ToolSelectorProps {
selectedToolNames: string[];
onSelectedToolNamesChange: (names: string[]) => void;
}
const ToolSelector = ({
selectedToolNames,
onSelectedToolNamesChange,
}: ToolSelectorProps) => {
const [availableTools, setAvailableTools] = useState<Tool[]>([]);
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 (
<Popover className="relative">
{({ open }) => (
<>
<PopoverButton
className={cn(
'flex items-center gap-1 rounded-lg text-sm transition-colors duration-150 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500',
selectedCount > 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"
>
<Wrench size={18} />
{selectedCount > 0 ? <span> {selectedCount} </span> : null}
<ChevronDown size={16} className="opacity-60" />
</PopoverButton>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<PopoverPanel className="absolute z-20 w-72 transform bottom-full mb-2">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black/5 dark:ring-white/5 bg-white dark:bg-dark-secondary">
<div className="px-4 py-3 border-b border-light-200 dark:border-dark-200">
<h3 className="text-sm font-medium text-black/90 dark:text-white/90">
Select Tools
</h3>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
Choose tools to assist the AI.
</p>
</div>
{isLoading ? (
<div className="px-4 py-3">
<Loader2 className="animate-spin text-black/70 dark:text-white/70" />
</div>
) : (
<div className="max-h-60 overflow-y-auto p-1.5 space-y-0.5">
{availableTools.length === 0 && (
<p className="text-xs text-black/50 dark:text-white/50 px-2.5 py-2 text-center">
No tools available.
</p>
)}
{availableTools.map((tool) => (
<div
key={tool.name}
onClick={() => 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) ? (
<CheckSquare
size={18}
className="text-[#24A0ED] flex-shrink-0 mt-0.5"
/>
) : (
<Square
size={18}
className="text-black/40 dark:text-white/40 flex-shrink-0 mt-0.5"
/>
)}
<div className="flex-1 min-w-0">
<span
className="text-sm font-medium text-black/80 dark:text-white/80 block truncate"
title={tool.name}
>
{tool.name.replace(/_/g, ' ')}
</span>
<p className="text-xs text-black/60 dark:text-white/60 mt-0.5">
{tool.description}
</p>
</div>
</div>
))}
</div>
)}
</div>
</PopoverPanel>
</Transition>
</>
)}
</Popover>
);
};
export default ToolSelector;

View file

@ -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 =

View file

@ -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<string[]>([]);
// 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 = ({
</p>
</div>
{/* Tool Selection */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-2">
Available Tools
</label>
<ToolSelector
selectedToolNames={selectedTools}
onSelectedToolNamesChange={setSelectedTools}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Select tools to assist the AI in processing your widget.
Your model must support tool calling.
</p>
</div>
{/* Refresh Frequency */}
<div>
<label className="block text-sm font-medium text-black dark:text-white mb-1">
@ -429,7 +435,7 @@ const WidgetConfigModal = ({
</div>
) : (
<div className="text-sm text-black/50 dark:text-white/50 italic">
Click "Run Preview" to see how your widget will look
Click &quot;Run Preview&quot; to see how your widget will look
</div>
)}
</div>
@ -476,24 +482,6 @@ const WidgetConfigModal = ({
</div>
</div>
</div>
<div className="text-xs text-black/70 dark:text-white/70">
<h5 className="font-medium mb-2">Available Tools (Your model must support tool calling):</h5>
<div className="space-y-1">
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'date_difference'}
</code>{' '}
- Get the difference between two dates (Works best with <a className='text-blue-500' href="https://en.wikipedia.org/wiki/ISO_8601" target="_blank" rel="noopener noreferrer">ISO 8601</a> formatted dates)
</div>
<div>
<code className="bg-light-200 dark:bg-dark-200 px-1 rounded">
{'timezone_converter'}
</code>{' '}
- Convert a date from one timezone to another (Works best with <a className='text-blue-500' href="https://en.wikipedia.org/wiki/ISO_8601" target="_blank" rel="noopener noreferrer">ISO 8601</a> formatted dates)
- Expects target timezone in the <a className='text-blue-500' href="https://nodatime.org/TimeZones" target="_blank" rel="noopener noreferrer">IANA</a> format (e.g., 'America/New_York', 'Europe/London', etc.)
</div>
</div>
</div>
</div>
</div>

View file

@ -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;

View file

@ -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<string | undefined> => {
@ -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(

View file

@ -9,7 +9,9 @@ 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);
@ -32,17 +34,40 @@ export const dateDifferenceTool = tool(
}
// 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;
@ -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")',
),
}),
}
},
);

View file

@ -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];

View file

@ -7,12 +7,17 @@ import { parseDate, getDateParseErrorMessage } from '@/lib/utils/dates';
* Tool that converts a date from one timezone to another
*/
export const timezoneConverterTool = tool(
({ dateString, toTimezone }: {
({
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);
@ -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")',
),
}),
}
},
);

View file

@ -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;
};
}

19
src/lib/types/api.ts Normal file
View file

@ -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;
}

8
src/lib/types/cache.ts Normal file
View file

@ -0,0 +1,8 @@
// Cache-related types
export interface WidgetCache {
[widgetId: string]: {
content: string;
lastFetched: Date;
expiresAt: Date;
};
}

View file

@ -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;

25
src/lib/types/widget.ts Normal file
View file

@ -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;
}

View file

@ -46,7 +46,7 @@ export function parseDate(dateString: string): DateTime {
export function getDateParseErrorMessage(
dateString: string,
dateTime: DateTime,
fieldName: string = 'date'
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}`;
}