From 1b0c2c59b8481fe7b751082e1f722700401ac672 Mon Sep 17 00:00:00 2001 From: Willie Zutz Date: Sat, 19 Jul 2025 11:34:56 -0600 Subject: [PATCH] fix(refactor): Cleanup components for improved readability and consistency --- src/app/api/dashboard/process-widget/route.ts | 116 +++-- src/app/dashboard/page.tsx | 72 ++- src/components/MarkdownRenderer.tsx | 39 +- src/components/MessageTabs.tsx | 5 +- src/components/Sidebar.tsx | 9 +- src/components/ThinkBox.tsx | 5 +- .../dashboard/WidgetConfigModal.tsx | 175 ++++++-- src/components/dashboard/WidgetDisplay.tsx | 57 ++- src/components/ui/card.tsx | 49 ++- src/lib/hooks/useDashboard.ts | 413 ++++++++++-------- 10 files changed, 590 insertions(+), 350 deletions(-) diff --git a/src/app/api/dashboard/process-widget/route.ts b/src/app/api/dashboard/process-widget/route.ts index 5f42831..952629a 100644 --- a/src/app/api/dashboard/process-widget/route.ts +++ b/src/app/api/dashboard/process-widget/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import { getWebContent, getWebContentLite } from '@/lib/utils/documents'; +import { Document } from '@langchain/core/documents'; import { BaseChatModel } from '@langchain/core/language_models/chat_models'; import { ChatOpenAI } from '@langchain/openai'; import { HumanMessage } from '@langchain/core/messages'; @@ -10,6 +11,7 @@ import { getCustomOpenaiModelName, } from '@/lib/config'; import { ChatOllama } from '@langchain/ollama'; +import axios from 'axios'; interface Source { url: string; @@ -24,56 +26,71 @@ interface WidgetProcessRequest { } // Helper function to fetch content from a single source -async function fetchSourceContent(source: Source): Promise<{ content: string; error?: string }> { +async function fetchSourceContent( + source: Source, +): Promise<{ content: string; error?: string }> { try { let document; - + if (source.type === 'Web Page') { // Use headless browser for complex web pages document = await getWebContent(source.url); } else { // Use faster fetch for HTTP data/APIs - document = await getWebContentLite(source.url); + const response = await axios.get(source.url, { transformResponse: [] }); + document = new Document({ + pageContent: response.data || '', + metadata: { source: source.url }, + }); } if (!document) { - return { - content: '', - error: `Failed to fetch content from ${source.url}` + return { + content: '', + error: `Failed to fetch content from ${source.url}`, }; } return { content: document.pageContent }; } catch (error) { console.error(`Error fetching content from ${source.url}:`, error); - return { - content: '', - error: `Error fetching ${source.url}: ${error instanceof Error ? error.message : 'Unknown error'}` + return { + content: '', + error: `Error fetching ${source.url}: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } } - // Helper function to replace variables in prompt -function replacePromptVariables(prompt: string, sourceContents: string[], location?: string): string { +function replacePromptVariables( + prompt: string, + sourceContents: string[], + location?: string, +): string { let processedPrompt = prompt; - + // Replace source content variables sourceContents.forEach((content, index) => { const variable = `{{source_content_${index + 1}}}`; - processedPrompt = processedPrompt.replace(new RegExp(variable, 'g'), content); + processedPrompt = processedPrompt.replace( + new RegExp(variable, 'g'), + content, + ); }); - + // Replace location if provided if (location) { processedPrompt = processedPrompt.replace(/\{\{location\}\}/g, location); } - + return processedPrompt; } // Helper function to get LLM instance based on provider and model -async function getLLMInstance(provider: string, model: string): Promise { +async function getLLMInstance( + provider: string, + model: string, +): Promise { try { const chatModelProviders = await getAvailableChatModelProviders(); @@ -89,12 +106,12 @@ async function getLLMInstance(provider: string, model: string): Promise { +async function processWithLLM( + prompt: string, + provider: string, + model: string, +): Promise { const llm = await getLLMInstance(provider, model); - + if (!llm) { throw new Error(`Invalid or unavailable model: ${provider}/${model}`); } const message = new HumanMessage({ content: prompt }); const response = await llm.invoke([message]); - + return response.content as string; } export async function POST(request: NextRequest) { try { const body: WidgetProcessRequest = await request.json(); - + // Validate required fields if (!body.sources || !body.prompt || !body.provider || !body.model) { return NextResponse.json( { error: 'Missing required fields: sources, prompt, provider, model' }, - { status: 400 } + { status: 400 }, ); } @@ -135,19 +156,21 @@ export async function POST(request: NextRequest) { if (!Array.isArray(body.sources) || body.sources.length === 0) { return NextResponse.json( { error: 'At least one source URL is required' }, - { status: 400 } + { status: 400 }, ); } // Fetch content from all sources console.log(`Processing widget with ${body.sources.length} source(s)`); const sourceResults = await Promise.all( - body.sources.map(source => fetchSourceContent(source)) + body.sources.map((source) => fetchSourceContent(source)), ); // Check for fetch errors const fetchErrors = sourceResults - .map((result, index) => result.error ? `Source ${index + 1}: ${result.error}` : null) + .map((result, index) => + result.error ? `Source ${index + 1}: ${result.error}` : null, + ) .filter(Boolean); if (fetchErrors.length > 0) { @@ -155,35 +178,40 @@ export async function POST(request: NextRequest) { } // Extract successful content - const sourceContents = sourceResults.map(result => result.content); - + const sourceContents = sourceResults.map((result) => result.content); + // If all sources failed, return error - if (sourceContents.every(content => !content)) { + if (sourceContents.every((content) => !content)) { return NextResponse.json( { error: 'Failed to fetch content from all sources' }, - { status: 500 } + { status: 500 }, ); } // Replace variables in prompt const processedPrompt = replacePromptVariables(body.prompt, sourceContents); - - console.log('Processed prompt:', processedPrompt.substring(0, 200) + '...'); + + console.log('Processing prompt:', processedPrompt); // Process with LLM try { - const llmResponse = await processWithLLM(processedPrompt, body.provider, body.model); - + const llmResponse = await processWithLLM( + processedPrompt, + body.provider, + body.model, + ); + + console.log('LLM response:', llmResponse); return NextResponse.json({ content: llmResponse, success: true, - sourcesFetched: sourceContents.filter(content => content).length, + sourcesFetched: sourceContents.filter((content) => content).length, totalSources: body.sources.length, - warnings: fetchErrors.length > 0 ? fetchErrors : undefined + warnings: fetchErrors.length > 0 ? fetchErrors : undefined, }); } catch (llmError) { console.error('LLM processing failed:', llmError); - + // Return diagnostic information if LLM fails const diagnosticResponse = `# Widget Processing - LLM Error @@ -193,24 +221,26 @@ export async function POST(request: NextRequest) { ${processedPrompt} ## Sources Successfully Fetched -${sourceContents.filter(content => content).length} of ${body.sources.length} sources +${sourceContents.filter((content) => content).length} of ${body.sources.length} sources ${fetchErrors.length > 0 ? `## Source Errors\n${fetchErrors.join('\n')}` : ''}`; return NextResponse.json({ content: diagnosticResponse, success: false, - error: llmError instanceof Error ? llmError.message : 'LLM processing failed', - sourcesFetched: sourceContents.filter(content => content).length, - totalSources: body.sources.length + error: + llmError instanceof Error + ? llmError.message + : 'LLM processing failed', + sourcesFetched: sourceContents.filter((content) => content).length, + totalSources: body.sources.length, }); } - } catch (error) { console.error('Error processing widget:', error); return NextResponse.json( { error: 'Internal server error' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index cabac17..657f059 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,12 +1,28 @@ 'use client'; -import { Plus, RefreshCw, Download, Upload, LayoutDashboard, Layers, List } from 'lucide-react'; -import { useState } from 'react'; -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Plus, + RefreshCw, + Download, + Upload, + LayoutDashboard, + Layers, + List, +} from 'lucide-react'; +import { useState, useEffect, useRef } from 'react'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; import WidgetConfigModal from '@/components/dashboard/WidgetConfigModal'; import WidgetDisplay from '@/components/dashboard/WidgetDisplay'; import { useDashboard } from '@/lib/hooks/useDashboard'; import { Widget, WidgetConfig } from '@/lib/types'; +import { toast } from 'sonner'; const DashboardPage = () => { const { @@ -24,7 +40,19 @@ const DashboardPage = () => { } = useDashboard(); const [showAddModal, setShowAddModal] = useState(false); - const [editingWidget, setEditingWidget] = useState(null); const handleAddWidget = () => { + const [editingWidget, setEditingWidget] = useState(null); + const hasAutoRefreshed = useRef(false); + + // Auto-refresh stale widgets when dashboard loads (only once) + useEffect(() => { + if (!isLoading && widgets.length > 0 && !hasAutoRefreshed.current) { + hasAutoRefreshed.current = true; + + refreshAllWidgets(); + } + }, [isLoading, widgets, refreshAllWidgets]); + + const handleAddWidget = () => { setEditingWidget(null); setShowAddModal(true); }; @@ -60,18 +88,18 @@ const DashboardPage = () => { }; const handleRefreshAll = () => { - refreshAllWidgets(); + refreshAllWidgets(true); }; const handleExport = async () => { try { const configJson = await exportDashboard(); await navigator.clipboard.writeText(configJson); - // TODO: Add toast notification for success + toast.success('Dashboard configuration copied to clipboard'); console.log('Dashboard configuration copied to clipboard'); } catch (error) { console.error('Export failed:', error); - // TODO: Add toast notification for error + toast.error('Failed to copy dashboard configuration'); } }; @@ -79,11 +107,11 @@ const DashboardPage = () => { try { const configJson = await navigator.clipboard.readText(); await importDashboard(configJson); - // TODO: Add toast notification for success + toast.success('Dashboard configuration imported successfully'); console.log('Dashboard configuration imported successfully'); } catch (error) { console.error('Import failed:', error); - // TODO: Add toast notification for error + toast.error('Failed to import dashboard configuration'); } }; @@ -98,18 +126,20 @@ const DashboardPage = () => { Welcome to your Dashboard - Create your first widget to get started with personalized information + Create your first widget to get started with personalized + information - +

- Widgets let you fetch content from any URL and process it with AI to show exactly what you need. + Widgets let you fetch content from any URL and process it with AI to + show exactly what you need.

- + - - + - + - + - + )} diff --git a/src/components/MessageTabs.tsx b/src/components/MessageTabs.tsx index c7a7028..56e70f7 100644 --- a/src/components/MessageTabs.tsx +++ b/src/components/MessageTabs.tsx @@ -273,10 +273,7 @@ const MessageTabs = ({ {/* Answer Tab */} {activeTab === 'text' && (
- + {loading && isLast ? null : (
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 9d3044e..cb93e4a 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,7 +1,14 @@ 'use client'; import { cn } from '@/lib/utils'; -import { BookOpenText, Home, Search, SquarePen, Settings, LayoutDashboard } from 'lucide-react'; +import { + BookOpenText, + Home, + Search, + SquarePen, + Settings, + LayoutDashboard, +} from 'lucide-react'; import Link from 'next/link'; import { useSelectedLayoutSegments } from 'next/navigation'; import React, { useState, type ReactNode } from 'react'; diff --git a/src/components/ThinkBox.tsx b/src/components/ThinkBox.tsx index defb5f1..0125a4e 100644 --- a/src/components/ThinkBox.tsx +++ b/src/components/ThinkBox.tsx @@ -17,10 +17,11 @@ const ThinkBox = ({ content, expanded, onToggle }: ThinkBoxProps) => { } const [internalExpanded, setInternalExpanded] = useState(false); - + // Use external expanded state if provided, otherwise use internal state const isExpanded = expanded !== undefined ? expanded : internalExpanded; - const handleToggle = onToggle || (() => setInternalExpanded(!internalExpanded)); + const handleToggle = + onToggle || (() => setInternalExpanded(!internalExpanded)); return (
diff --git a/src/components/dashboard/WidgetConfigModal.tsx b/src/components/dashboard/WidgetConfigModal.tsx index ae55f28..148e14d 100644 --- a/src/components/dashboard/WidgetConfigModal.tsx +++ b/src/components/dashboard/WidgetConfigModal.tsx @@ -1,6 +1,12 @@ 'use client'; -import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'; +import { + Dialog, + DialogPanel, + DialogTitle, + Transition, + TransitionChild, +} from '@headlessui/react'; import { X, Plus, Trash2, Play, Save } from 'lucide-react'; import { Fragment, useState, useEffect } from 'react'; import MarkdownRenderer from '@/components/MarkdownRenderer'; @@ -9,20 +15,28 @@ import ModelSelector from '@/components/MessageInputActions/ModelSelector'; // Helper function to replace date/time variables in prompts on the client side const replaceDateTimeVariables = (prompt: string): string => { let processedPrompt = prompt; - + // Replace UTC datetime if (processedPrompt.includes('{{current_utc_datetime}}')) { const utcDateTime = new Date().toISOString(); - processedPrompt = processedPrompt.replace(/\{\{current_utc_datetime\}\}/g, utcDateTime); + processedPrompt = processedPrompt.replace( + /\{\{current_utc_datetime\}\}/g, + utcDateTime, + ); } - + // Replace local datetime if (processedPrompt.includes('{{current_local_datetime}}')) { const now = new Date(); - const localDateTime = new Date(now.getTime() - now.getTimezoneOffset() * 60000).toISOString(); - processedPrompt = processedPrompt.replace(/\{\{current_local_datetime\}\}/g, localDateTime); + const localDateTime = new Date( + now.getTime() - now.getTimezoneOffset() * 60000, + ).toISOString(); + processedPrompt = processedPrompt.replace( + /\{\{current_local_datetime\}\}/g, + localDateTime, + ); } - + return processedPrompt; }; @@ -67,7 +81,10 @@ const WidgetConfigModal = ({ const [previewContent, setPreviewContent] = useState(''); const [isPreviewLoading, setIsPreviewLoading] = useState(false); - const [selectedModel, setSelectedModel] = useState<{ provider: string; model: string } | null>(null); + const [selectedModel, setSelectedModel] = useState<{ + provider: string; + model: string; + } | null>(null); // Update config when editingWidget changes useEffect(() => { @@ -106,7 +123,7 @@ const WidgetConfigModal = ({ // Update config when model selection changes useEffect(() => { if (selectedModel) { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, provider: selectedModel.provider, model: selectedModel.model, @@ -118,7 +135,7 @@ const WidgetConfigModal = ({ if (!config.title.trim() || !config.prompt.trim()) { return; // TODO: Add proper validation feedback } - + onSave(config); onClose(); }; @@ -129,8 +146,13 @@ const WidgetConfigModal = ({ return; } - if (config.sources.length === 0 || config.sources.every(s => !s.url.trim())) { - setPreviewContent('Please add at least one source URL before running preview.'); + if ( + config.sources.length === 0 || + config.sources.every((s) => !s.url.trim()) + ) { + setPreviewContent( + 'Please add at least one source URL before running preview.', + ); return; } @@ -145,7 +167,7 @@ const WidgetConfigModal = ({ 'Content-Type': 'application/json', }, body: JSON.stringify({ - sources: config.sources.filter(s => s.url.trim()), // Only send sources with URLs + sources: config.sources.filter((s) => s.url.trim()), // Only send sources with URLs prompt: processedPrompt, provider: config.provider, model: config.model, @@ -153,40 +175,44 @@ const WidgetConfigModal = ({ }); const result = await response.json(); - + if (result.success) { setPreviewContent(result.content); } else { - setPreviewContent(`**Preview Error:** ${result.error || 'Unknown error occurred'}\n\n${result.content || ''}`); + setPreviewContent( + `**Preview Error:** ${result.error || 'Unknown error occurred'}\n\n${result.content || ''}`, + ); } } catch (error) { console.error('Preview error:', error); - setPreviewContent(`**Network Error:** Failed to connect to the preview service.\n\n${error instanceof Error ? error.message : 'Unknown error'}`); + setPreviewContent( + `**Network Error:** Failed to connect to the preview service.\n\n${error instanceof Error ? error.message : 'Unknown error'}`, + ); } finally { setIsPreviewLoading(false); } }; const addSource = () => { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, - sources: [...prev.sources, { url: '', type: 'Web Page' }] + sources: [...prev.sources, { url: '', type: 'Web Page' }], })); }; const removeSource = (index: number) => { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, - sources: prev.sources.filter((_, i) => i !== index) + sources: prev.sources.filter((_, i) => i !== index), })); }; const updateSource = (index: number, field: keyof Source, value: string) => { - setConfig(prev => ({ + setConfig((prev) => ({ ...prev, - sources: prev.sources.map((source, i) => - i === index ? { ...source, [field]: value } : source - ) + sources: prev.sources.map((source, i) => + i === index ? { ...source, [field]: value } : source, + ), })); }; @@ -241,7 +267,12 @@ const WidgetConfigModal = ({ setConfig(prev => ({ ...prev, title: e.target.value }))} + onChange={(e) => + setConfig((prev) => ({ + ...prev, + title: e.target.value, + })) + } className="w-full px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="Enter widget title..." /> @@ -258,13 +289,21 @@ const WidgetConfigModal = ({ updateSource(index, 'url', e.target.value)} + onChange={(e) => + updateSource(index, 'url', e.target.value) + } className="flex-1 px-3 py-2 border border-light-200 dark:border-dark-200 rounded-md bg-light-primary dark:bg-dark-primary text-black dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="https://example.com" />