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';
|
} from '@/lib/config';
|
||||||
import { ChatOllama } from '@langchain/ollama';
|
import { ChatOllama } from '@langchain/ollama';
|
||||||
import { createReactAgent } from '@langchain/langgraph/prebuilt';
|
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';
|
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
|
// Helper function to fetch content from a single source
|
||||||
async function fetchSourceContent(
|
async function fetchSourceContent(
|
||||||
source: Source,
|
source: Source,
|
||||||
|
|
@ -129,6 +119,7 @@ async function processWithLLM(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
provider: string,
|
provider: string,
|
||||||
model: string,
|
model: string,
|
||||||
|
tool_names?: string[],
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const llm = await getLLMInstance(provider, model);
|
const llm = await getLLMInstance(provider, model);
|
||||||
|
|
||||||
|
|
@ -136,10 +127,11 @@ async function processWithLLM(
|
||||||
throw new Error(`Invalid or unavailable model: ${provider}/${model}`);
|
throw new Error(`Invalid or unavailable model: ${provider}/${model}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tools = [
|
// Filter tools based on tool_names parameter
|
||||||
timezoneConverterTool,
|
const tools =
|
||||||
dateDifferenceTool,
|
tool_names && tool_names.length > 0
|
||||||
];
|
? allTools.filter((tool) => tool_names.includes(tool.name))
|
||||||
|
: [];
|
||||||
|
|
||||||
// Create the React agent with tools
|
// Create the React agent with tools
|
||||||
const agent = createReactAgent({
|
const agent = createReactAgent({
|
||||||
|
|
@ -148,14 +140,17 @@ async function processWithLLM(
|
||||||
});
|
});
|
||||||
|
|
||||||
// Invoke the agent with the prompt
|
// Invoke the agent with the prompt
|
||||||
const response = await agent.invoke({
|
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.` }),
|
messages: [
|
||||||
new HumanMessage({ content: prompt })
|
//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
|
},
|
||||||
});
|
{
|
||||||
|
recursionLimit: 15, // Limit recursion depth to prevent infinite loops
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Extract the final response content
|
// Extract the final response content
|
||||||
const lastMessage = response.messages[response.messages.length - 1];
|
const lastMessage = response.messages[response.messages.length - 1];
|
||||||
|
|
@ -200,7 +195,10 @@ export async function POST(request: NextRequest) {
|
||||||
sourceContents = sourceResults.map((result) => result.content);
|
sourceContents = sourceResults.map((result) => result.content);
|
||||||
sourcesFetched = sourceContents.filter((content) => content).length;
|
sourcesFetched = sourceContents.filter((content) => content).length;
|
||||||
// If all sources failed, return error
|
// 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(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to fetch content from all sources' },
|
{ error: 'Failed to fetch content from all sources' },
|
||||||
{ status: 500 },
|
{ status: 500 },
|
||||||
|
|
@ -218,6 +216,7 @@ export async function POST(request: NextRequest) {
|
||||||
processedPrompt,
|
processedPrompt,
|
||||||
body.provider,
|
body.provider,
|
||||||
body.model,
|
body.model,
|
||||||
|
body.tool_names,
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('LLM response:', llmResponse);
|
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 WidgetConfigModal from '@/components/dashboard/WidgetConfigModal';
|
||||||
import WidgetDisplay from '@/components/dashboard/WidgetDisplay';
|
import WidgetDisplay from '@/components/dashboard/WidgetDisplay';
|
||||||
import { useDashboard } from '@/lib/hooks/useDashboard';
|
import { useDashboard } from '@/lib/hooks/useDashboard';
|
||||||
import { Widget, WidgetConfig } from '@/lib/types';
|
import { Widget, WidgetConfig } from '@/lib/types/widget';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const DashboardPage = () => {
|
const DashboardPage = () => {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ import {
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
Eye,
|
||||||
|
EyeOff,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { cn } from '@/lib/utils';
|
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) => {
|
const toggleProviderExpansion = (providerId: string) => {
|
||||||
setExpandedProviders((prev) => {
|
setExpandedProviders((prev) => {
|
||||||
const newSet = new Set(prev);
|
const newSet = new Set(prev);
|
||||||
|
|
@ -1593,6 +1628,36 @@ export default function SettingsPage() {
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="p-3 bg-light-100 dark:bg-dark-100 border-t border-light-200 dark:border-dark-200">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
{modelEntries.map(([modelKey, model]) => (
|
{modelEntries.map(([modelKey, model]) => (
|
||||||
<div
|
<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 ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => {
|
||||||
|
const [internalExpanded, setInternalExpanded] = useState(false);
|
||||||
|
|
||||||
// Don't render anything if content is empty
|
// Don't render anything if content is empty
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [internalExpanded, setInternalExpanded] = useState(false);
|
|
||||||
|
|
||||||
// Use external expanded state if provided, otherwise use internal state
|
// Use external expanded state if provided, otherwise use internal state
|
||||||
const isExpanded = expanded !== undefined ? expanded : internalExpanded;
|
const isExpanded = expanded !== undefined ? expanded : internalExpanded;
|
||||||
const handleToggle =
|
const handleToggle =
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { X, Plus, Trash2, Play, Save } from 'lucide-react';
|
||||||
import { Fragment, useState, useEffect } from 'react';
|
import { Fragment, useState, useEffect } from 'react';
|
||||||
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||||
import ModelSelector from '@/components/MessageInputActions/ModelSelector';
|
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
|
// Helper function to replace date/time variables in prompts on the client side
|
||||||
const replaceDateTimeVariables = (prompt: string): string => {
|
const replaceDateTimeVariables = (prompt: string): string => {
|
||||||
|
|
@ -40,22 +42,6 @@ const replaceDateTimeVariables = (prompt: string): string => {
|
||||||
return processedPrompt;
|
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 {
|
interface WidgetConfigModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
|
@ -85,6 +71,7 @@ const WidgetConfigModal = ({
|
||||||
provider: string;
|
provider: string;
|
||||||
model: string;
|
model: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [selectedTools, setSelectedTools] = useState<string[]>([]);
|
||||||
|
|
||||||
// Update config when editingWidget changes
|
// Update config when editingWidget changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -102,6 +89,7 @@ const WidgetConfigModal = ({
|
||||||
provider: editingWidget.provider,
|
provider: editingWidget.provider,
|
||||||
model: editingWidget.model,
|
model: editingWidget.model,
|
||||||
});
|
});
|
||||||
|
setSelectedTools(editingWidget.tool_names || []);
|
||||||
} else {
|
} else {
|
||||||
// Reset to default values for new widget
|
// Reset to default values for new widget
|
||||||
setConfig({
|
setConfig({
|
||||||
|
|
@ -117,6 +105,7 @@ const WidgetConfigModal = ({
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
model: 'gpt-4',
|
model: 'gpt-4',
|
||||||
});
|
});
|
||||||
|
setSelectedTools([]);
|
||||||
}
|
}
|
||||||
}, [editingWidget]);
|
}, [editingWidget]);
|
||||||
|
|
||||||
|
|
@ -140,6 +129,7 @@ const WidgetConfigModal = ({
|
||||||
const filteredConfig = {
|
const filteredConfig = {
|
||||||
...config,
|
...config,
|
||||||
sources: config.sources.filter((s) => s.url.trim()),
|
sources: config.sources.filter((s) => s.url.trim()),
|
||||||
|
tool_names: selectedTools,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSave(filteredConfig);
|
onSave(filteredConfig);
|
||||||
|
|
@ -172,6 +162,7 @@ const WidgetConfigModal = ({
|
||||||
prompt: processedPrompt,
|
prompt: processedPrompt,
|
||||||
provider: config.provider,
|
provider: config.provider,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
|
tool_names: selectedTools,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -366,6 +357,21 @@ const WidgetConfigModal = ({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 */}
|
{/* Refresh Frequency */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-black dark:text-white mb-1">
|
<label className="block text-sm font-medium text-black dark:text-white mb-1">
|
||||||
|
|
@ -429,7 +435,7 @@ const WidgetConfigModal = ({
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-black/50 dark:text-white/50 italic">
|
<div className="text-sm text-black/50 dark:text-white/50 italic">
|
||||||
Click "Run Preview" to see how your widget will look
|
Click "Run Preview" to see how your widget will look
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -476,24 +482,6 @@ const WidgetConfigModal = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,28 +11,9 @@ import {
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
import MarkdownRenderer from '@/components/MarkdownRenderer';
|
||||||
|
import { Widget } from '@/lib/types/widget';
|
||||||
import { useState } from 'react';
|
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 {
|
interface WidgetDisplayProps {
|
||||||
widget: Widget;
|
widget: Widget;
|
||||||
onEdit: (widget: Widget) => void;
|
onEdit: (widget: Widget) => void;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { Widget, WidgetConfig } from '@/lib/types/widget';
|
||||||
import {
|
import {
|
||||||
Widget,
|
|
||||||
WidgetConfig,
|
|
||||||
DashboardState,
|
DashboardState,
|
||||||
DashboardConfig,
|
DashboardConfig,
|
||||||
DASHBOARD_STORAGE_KEYS,
|
DASHBOARD_STORAGE_KEYS,
|
||||||
WidgetCache,
|
} from '@/lib/types/dashboard';
|
||||||
} from '@/lib/types';
|
import { WidgetCache } from '@/lib/types/cache';
|
||||||
|
|
||||||
// Helper function to request location permission and get user's location
|
// Helper function to request location permission and get user's location
|
||||||
const requestLocationPermission = async (): Promise<string | undefined> => {
|
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(() => {
|
const loadDashboardData = useCallback(() => {
|
||||||
try {
|
try {
|
||||||
// Load widgets
|
// 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 addWidget = useCallback((config: WidgetConfig) => {
|
||||||
const newWidget: Widget = {
|
const newWidget: Widget = {
|
||||||
...config,
|
...config,
|
||||||
|
|
@ -215,7 +214,7 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isWidgetCacheValid = (widget: Widget): boolean => {
|
const isWidgetCacheValid = useCallback((widget: Widget): boolean => {
|
||||||
const cache = getWidgetCache();
|
const cache = getWidgetCache();
|
||||||
const cachedData = cache[widget.id];
|
const cachedData = cache[widget.id];
|
||||||
|
|
||||||
|
|
@ -225,15 +224,15 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
const expiresAt = new Date(cachedData.expiresAt);
|
const expiresAt = new Date(cachedData.expiresAt);
|
||||||
|
|
||||||
return now < expiresAt;
|
return now < expiresAt;
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const getCacheExpiryTime = (widget: Widget): Date => {
|
const getCacheExpiryTime = useCallback((widget: Widget): Date => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const refreshMs =
|
const refreshMs =
|
||||||
widget.refreshFrequency *
|
widget.refreshFrequency *
|
||||||
(widget.refreshUnit === 'hours' ? 3600000 : 60000);
|
(widget.refreshUnit === 'hours' ? 3600000 : 60000);
|
||||||
return new Date(now.getTime() + refreshMs);
|
return new Date(now.getTime() + refreshMs);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const refreshWidget = useCallback(
|
const refreshWidget = useCallback(
|
||||||
async (id: string, forceRefresh: boolean = false) => {
|
async (id: string, forceRefresh: boolean = false) => {
|
||||||
|
|
@ -287,6 +286,7 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
prompt: processedPrompt,
|
prompt: processedPrompt,
|
||||||
provider: widget.provider,
|
provider: widget.provider,
|
||||||
model: widget.model,
|
model: widget.model,
|
||||||
|
tool_names: widget.tool_names,
|
||||||
location,
|
location,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -351,7 +351,7 @@ export const useDashboard = (): UseDashboardReturn => {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[state.widgets],
|
[state.widgets, isWidgetCacheValid, getCacheExpiryTime],
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshAllWidgets = useCallback(
|
const refreshAllWidgets = useCallback(
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ import { parseDate, getDateParseErrorMessage } from '@/lib/utils/dates';
|
||||||
export const dateDifferenceTool = tool(
|
export const dateDifferenceTool = tool(
|
||||||
({ startDate, endDate }: { startDate: string; endDate: string }): string => {
|
({ startDate, endDate }: { startDate: string; endDate: string }): string => {
|
||||||
try {
|
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
|
// Parse the dates using the extracted utility function
|
||||||
const startDateTime = parseDate(startDate);
|
const startDateTime = parseDate(startDate);
|
||||||
|
|
@ -32,17 +34,40 @@ export const dateDifferenceTool = tool(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate differences in various units using Luxon's accurate methods
|
// Calculate differences in various units using Luxon's accurate methods
|
||||||
const diffMilliseconds = Math.abs(endDateTime.diff(startDateTime).toMillis());
|
const diffMilliseconds = Math.abs(
|
||||||
const diffSeconds = Math.abs(endDateTime.diff(startDateTime, 'seconds').seconds);
|
endDateTime.diff(startDateTime).toMillis(),
|
||||||
const diffMinutes = Math.abs(endDateTime.diff(startDateTime, 'minutes').minutes);
|
);
|
||||||
const diffHours = Math.abs(endDateTime.diff(startDateTime, 'hours').hours);
|
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 diffDays = Math.abs(endDateTime.diff(startDateTime, 'days').days);
|
||||||
const diffWeeks = Math.abs(endDateTime.diff(startDateTime, 'weeks').weeks);
|
const diffWeeks = Math.abs(
|
||||||
const diffMonths = Math.abs(endDateTime.diff(startDateTime, 'months').months);
|
endDateTime.diff(startDateTime, 'weeks').weeks,
|
||||||
const diffYears = Math.abs(endDateTime.diff(startDateTime, 'years').years);
|
);
|
||||||
|
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
|
// 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
|
// Determine which date is earlier
|
||||||
const isStartEarlier = startDateTime <= endDateTime;
|
const isStartEarlier = startDateTime <= endDateTime;
|
||||||
|
|
@ -93,7 +118,6 @@ Human-readable breakdown:`;
|
||||||
Direction: Start date is ${isStartEarlier ? 'earlier than' : 'later than'} the end date.`;
|
Direction: Start date is ${isStartEarlier ? 'earlier than' : 'later than'} the end date.`;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during date difference calculation:', error);
|
console.error('Error during date difference calculation:', error);
|
||||||
return `Error: ${error instanceof Error ? error.message : 'Unknown error occurred during date difference calculation'}`;
|
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',
|
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({
|
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")'),
|
startDate: z
|
||||||
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")'),
|
.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';
|
import { timezoneConverterTool } from './timezoneConverter';
|
||||||
export { dateDifferenceTool } from './dateDifference';
|
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
|
* Tool that converts a date from one timezone to another
|
||||||
*/
|
*/
|
||||||
export const timezoneConverterTool = tool(
|
export const timezoneConverterTool = tool(
|
||||||
({ dateString, toTimezone }: {
|
({
|
||||||
|
dateString,
|
||||||
|
toTimezone,
|
||||||
|
}: {
|
||||||
dateString: string;
|
dateString: string;
|
||||||
toTimezone: string;
|
toTimezone: string;
|
||||||
}): string => {
|
}): string => {
|
||||||
try {
|
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
|
// Parse the date string using the extracted utility function
|
||||||
const dateTime = parseDate(dateString);
|
const dateTime = parseDate(dateString);
|
||||||
|
|
@ -40,7 +45,6 @@ Target: ${targetISO} (${targetDateTime.zoneName})`;
|
||||||
|
|
||||||
console.log(output);
|
console.log(output);
|
||||||
return output;
|
return output;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during timezone conversion:', error);
|
console.error('Error during timezone conversion:', error);
|
||||||
return `Error: ${error instanceof Error ? error.message : 'Unknown error occurred during timezone conversion'}`;
|
return `Error: ${error instanceof Error ? error.message : 'Unknown error occurred during timezone conversion'}`;
|
||||||
|
|
@ -48,10 +52,19 @@ Target: ${targetISO} (${targetDateTime.zoneName})`;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'timezone_converter',
|
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({
|
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")'),
|
dateString: z
|
||||||
toTimezone: z.string().describe('Target timezone to convert to (e.g., "Asia/Tokyo", "America/Los_Angeles", "Europe/Paris")'),
|
.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(
|
export function getDateParseErrorMessage(
|
||||||
dateString: string,
|
dateString: string,
|
||||||
dateTime: DateTime,
|
dateTime: DateTime,
|
||||||
fieldName: string = 'date'
|
fieldName: string = 'date',
|
||||||
): string {
|
): 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}`;
|
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