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:
parent
1f78b94243
commit
7253cbc89c
18 changed files with 513 additions and 247 deletions
|
|
@ -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);
|
||||
|
|
|
|||
20
src/app/api/tools/route.ts
Normal file
20
src/app/api/tools/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
166
src/components/MessageInputActions/ToolSelector.tsx
Normal file
166
src/components/MessageInputActions/ToolSelector.tsx
Normal 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;
|
||||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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 "Run Preview" 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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")',
|
||||
),
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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")',
|
||||
),
|
||||
}),
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
19
src/lib/types/api.ts
Normal 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
8
src/lib/types/cache.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Cache-related types
|
||||
export interface WidgetCache {
|
||||
[widgetId: string]: {
|
||||
content: string;
|
||||
lastFetched: Date;
|
||||
expiresAt: Date;
|
||||
};
|
||||
}
|
||||
27
src/lib/types/dashboard.ts
Normal file
27
src/lib/types/dashboard.ts
Normal 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
25
src/lib/types/widget.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue