From bffb11e5b0de7cde62e2e12a05f8a451527acb6e Mon Sep 17 00:00:00 2001 From: Drew Peifer Date: Thu, 12 Feb 2026 03:26:10 -0500 Subject: [PATCH] added token usage and cost to UI, cleanup --- claudash/proxy-server.js | 15 +-- claudash/src/App.jsx | 6 +- claudash/src/components/ChatInterface.jsx | 76 ++++++------ claudash/src/components/Dashboard.jsx | 117 +++++++++++------- claudash/src/components/DebugPanel.jsx | 40 +++--- .../src/components/widgets/BriefingWidget.jsx | 10 +- .../src/components/widgets/GenericWidget.jsx | 12 +- .../src/components/widgets/NewsWidget.jsx | 34 ++--- .../src/components/widgets/WeatherWidget.jsx | 18 +-- claudash/src/services/claudeService.js | 83 +++++++------ claudash/src/services/dataFetcher.js | 50 ++++---- claudash/src/services/sourceParser.js | 32 ++--- claudash/src/services/storageService.js | 28 ++--- claudash/src/utils/corsProxy.js | 14 +-- claudash/vite.config.js | 4 +- 15 files changed, 285 insertions(+), 254 deletions(-) diff --git a/claudash/proxy-server.js b/claudash/proxy-server.js index 72abfe4..08d51df 100644 --- a/claudash/proxy-server.js +++ b/claudash/proxy-server.js @@ -11,21 +11,21 @@ app.use(express.json()); app.post('/api/claude', async (req, res) => { try { const { messages, apiKey, model, maxTokens } = req.body; - + console.log('=== Proxy received request ==='); console.log('Model:', model); console.log('Max tokens:', maxTokens); console.log('Messages count:', messages?.length); console.log('API Key present:', !!apiKey); - + const requestBody = { model: model, max_tokens: maxTokens, messages: messages }; - + console.log('Request body:', JSON.stringify(requestBody, null, 2)); - + const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { @@ -35,17 +35,18 @@ app.post('/api/claude', async (req, res) => { }, body: JSON.stringify(requestBody) }); - + console.log('Claude API response status:', response.status); - + if (!response.ok) { const error = await response.text(); console.error('Claude API error:', error); return res.status(response.status).json({ error }); } - + const data = await response.json(); console.log('Claude API success, response content length:', data.content?.[0]?.text?.length); + console.log('Token usage:', data.usage); res.json(data); } catch (error) { console.error('Proxy error:', error); diff --git a/claudash/src/App.jsx b/claudash/src/App.jsx index c418a28..69c78ec 100644 --- a/claudash/src/App.jsx +++ b/claudash/src/App.jsx @@ -7,12 +7,12 @@ import { lightTheme, darkTheme } from './theme'; function App() { const [isDark, setIsDark] = useState(true); const debugMode = import.meta.env.REACT_APP_DEBUG_MODE === 'true'; - + return ( - setIsDark(!isDark)} + setIsDark(!isDark)} isDark={isDark} /> {debugMode && } diff --git a/claudash/src/components/ChatInterface.jsx b/claudash/src/components/ChatInterface.jsx index 79e4b2c..fb708bf 100644 --- a/claudash/src/components/ChatInterface.jsx +++ b/claudash/src/components/ChatInterface.jsx @@ -9,42 +9,42 @@ function ChatInterface() { const [loading, setLoading] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const messagesEndRef = useRef(null); - + const handleSend = async () => { if (!input.trim() || loading) return; - + const userMessage = { role: 'user', content: input }; setMessages(prev => [...prev, userMessage]); setInput(''); setLoading(true); setIsExpanded(true); // Expand chat when sending a message - + try { const response = await sendChatMessage(input, messages); setMessages(prev => [...prev, { role: 'assistant', content: response }]); } catch (error) { console.error('Chat error:', error); - setMessages(prev => [...prev, { - role: 'assistant', - content: 'Sorry, I encountered an error. Please try again.' + setMessages(prev => [...prev, { + role: 'assistant', + content: 'Sorry, I encountered an error. Please try again.' }]); } finally { setLoading(false); } }; - + useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]); - + return ( - {/* Expand/Collapse Button */} - - setIsExpanded(!isExpanded)} size="small" > @@ -70,7 +70,7 @@ function ChatInterface() { Chat with Claude - + {/* Messages area - only visible when expanded */} {isExpanded && ( @@ -80,16 +80,16 @@ function ChatInterface() { )} {messages.map((msg, idx) => ( - - )} - + {/* Input area - always visible */} - @@ -129,8 +129,8 @@ function ChatInterface() { disabled={loading} size="small" /> - diff --git a/claudash/src/components/Dashboard.jsx b/claudash/src/components/Dashboard.jsx index 737e0b7..dd69fb0 100644 --- a/claudash/src/components/Dashboard.jsx +++ b/claudash/src/components/Dashboard.jsx @@ -16,17 +16,18 @@ function Dashboard({ themeToggle, isDark }) { const [sourceData, setSourceData] = useState({}); const [error, setError] = useState(null); const [refreshing, setRefreshing] = useState(false); - + const [tokenUsage, setTokenUsage] = useState(null); + useEffect(() => { loadDashboard(); }, []); - + async function loadDashboard(forceRefresh = false) { try { console.log('=== loadDashboard called ==='); console.log('Force refresh:', forceRefresh); setError(null); - + // Check if we already have today's briefing (unless forcing refresh) if (!forceRefresh && hasTodaysBriefing()) { console.log('Using cached briefing from today'); @@ -36,53 +37,63 @@ function Dashboard({ themeToggle, isDark }) { setLoading(false); return; } - + console.log('Fetching fresh data...'); setLoadingStatus('Fetching weather and news sources...'); - + // Fetch all configured sources const data = await fetchAllSources(); console.log('Fetched sources:', Object.keys(data.sources || {})); console.log('Failed sources:', data.failed); setSourceData(data.sources || {}); - + // If all sources failed, show error but continue if (data.failed > 0 && data.successful === 0) { setError('All data sources failed to load. Please check your internet connection and try again.'); } else if (data.failed > 0) { setError(`${data.failed} data source(s) failed to load. Showing available data.`); } - + // Check if API key is configured const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY; console.log('API key configured:', !!apiKey); - + if (!apiKey || apiKey === 'sk-ant-your-key-here') { console.warn('API key not configured properly'); setBriefing('Please add your Anthropic API key to .env.local to generate briefings.'); setLoading(false); return; } - + // Get yesterday's briefing for comparison const previousBriefing = getPreviousBriefing(1); console.log('Previous briefing exists:', !!previousBriefing); - + // Only generate briefing if we have some data if (Object.keys(data.sources || {}).length > 0) { console.log('Calling generateBriefing...'); setLoadingStatus('Generating your personalized briefing with AI...'); - + try { // Generate today's briefing with Claude - const newBriefing = await generateBriefing(data, previousBriefing); - console.log('Briefing generated, length:', newBriefing?.length); - setBriefing(newBriefing); - + const result = await generateBriefing(data, previousBriefing); + console.log('Briefing generated'); + + // Handle result object with text and usage + if (result && typeof result === 'object' && result.text) { + setBriefing(result.text); + setTokenUsage(result.usage); + console.log('Token usage:', result.usage); + } else if (typeof result === 'string') { + // Fallback for string responses + setBriefing(result); + } + // Save to localStorage const today = new Date().toISOString().split('T')[0]; console.log('Saving briefing for:', today); - saveBriefing(today, newBriefing, data.sources); + const briefingText = result?.text || result; + saveBriefing(today, briefingText, data.sources); } catch (briefingError) { console.error('Failed to generate briefing:', briefingError); setBriefing(`Unable to generate AI briefing: ${briefingError.message}\n\nYou can still view the data widgets below.`); @@ -90,7 +101,7 @@ function Dashboard({ themeToggle, isDark }) { } else { setBriefing('No data sources loaded successfully. Please check your configuration and try again.'); } - + } catch (error) { console.error('Dashboard load error:', error); console.error('Error stack:', error.stack); @@ -100,12 +111,12 @@ function Dashboard({ themeToggle, isDark }) { setRefreshing(false); } } - + const handleRefresh = () => { setRefreshing(true); loadDashboard(true); }; - + if (loading) { return ( @@ -116,20 +127,20 @@ function Dashboard({ themeToggle, isDark }) { ); } - + return ( {/* Top toolbar */} - - { localStorage.clear(); window.location.reload(); @@ -139,56 +150,70 @@ function Dashboard({ themeToggle, isDark }) { > 🗑️ - - {isDark ? : } - + {error && ( {error} )} - + + {tokenUsage && ( + + + API Usage: {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens + {' • '} + Cost: ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)} + {' '} + + (Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output) + + + + )} + {/* Widget Grid - easily rearrangeable */} - - - - + {/* Render NewsWidget for each news source */} {Object.entries(sourceData).map(([key, data]) => { if (key === 'weather') return null; return ( - ); })} - + {/* Fixed chat at bottom */} diff --git a/claudash/src/components/DebugPanel.jsx b/claudash/src/components/DebugPanel.jsx index 5549954..b36ae75 100644 --- a/claudash/src/components/DebugPanel.jsx +++ b/claudash/src/components/DebugPanel.jsx @@ -5,7 +5,7 @@ import { exportAllData, importDummyData } from '../services/storageService'; function DebugPanel() { const [dummyMode, setDummyMode] = useState(false); - + const handleExport = () => { const data = exportAllData(); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); @@ -16,11 +16,11 @@ function DebugPanel() { a.click(); URL.revokeObjectURL(url); }; - + const handleImport = (event) => { const file = event.target.files[0]; if (!file) return; - + const reader = new FileReader(); reader.onload = (e) => { try { @@ -35,40 +35,40 @@ function DebugPanel() { }; reader.readAsText(file); }; - + return ( Debug Tools - - - - - + {dummyMode && ( - Using dummy data diff --git a/claudash/src/components/widgets/BriefingWidget.jsx b/claudash/src/components/widgets/BriefingWidget.jsx index 95b83b6..7a9f058 100644 --- a/claudash/src/components/widgets/BriefingWidget.jsx +++ b/claudash/src/components/widgets/BriefingWidget.jsx @@ -3,15 +3,15 @@ import GenericWidget from './GenericWidget'; function BriefingWidget({ briefing, sx }) { return ( - {briefing ? ( - {title} diff --git a/claudash/src/components/widgets/NewsWidget.jsx b/claudash/src/components/widgets/NewsWidget.jsx index b03891a..8d715d7 100644 --- a/claudash/src/components/widgets/NewsWidget.jsx +++ b/claudash/src/components/widgets/NewsWidget.jsx @@ -16,7 +16,7 @@ function NewsWidget({ data, sx }) { // Handle Reddit data if (data.type === 'reddit' && data.posts) { const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit'; - + return ( @@ -49,7 +49,7 @@ function NewsWidget({ data, sx }) { href={post.url} target="_blank" rel="noopener noreferrer" - sx={{ + sx={{ textDecoration: 'none', color: 'text.primary', '&:hover': { textDecoration: 'underline' } @@ -65,17 +65,17 @@ function NewsWidget({ data, sx }) { secondaryTypographyProps={{ component: 'div' }} secondary={ - } - label={post.score} - size="small" - variant="outlined" + } + label={post.score} + size="small" + variant="outlined" /> - } - label={post.comments} - size="small" - variant="outlined" + } + label={post.comments} + size="small" + variant="outlined" /> by {post.author} @@ -124,7 +124,7 @@ function NewsWidget({ data, sx }) { href={item.link} target="_blank" rel="noopener noreferrer" - sx={{ + sx={{ textDecoration: 'none', color: 'text.primary', '&:hover': { textDecoration: 'underline' } @@ -144,10 +144,10 @@ function NewsWidget({ data, sx }) { {item.pubDate && new Date(item.pubDate).toLocaleDateString()} {item.description && ( - - + {description} - - diff --git a/claudash/src/services/claudeService.js b/claudash/src/services/claudeService.js index d6a6044..07b3e69 100644 --- a/claudash/src/services/claudeService.js +++ b/claudash/src/services/claudeService.js @@ -1,7 +1,7 @@ -import { - ANTHROPIC_API_URL, - DEFAULT_CLAUDE_MODEL, - MAX_TOKENS +import { + ANTHROPIC_API_URL, + DEFAULT_CLAUDE_MODEL, + MAX_TOKENS } from '../utils/constants'; /** @@ -12,22 +12,22 @@ import { */ async function callClaude(prompt, messages = []) { const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY; - + console.log('=== callClaude function ==='); console.log('API Key configured:', !!apiKey); console.log('API Key starts with:', apiKey?.substring(0, 15) + '...'); - + if (!apiKey || apiKey === 'sk-ant-your-key-here') { console.error('Anthropic API key not configured'); return 'Please configure your Anthropic API key in .env.local'; } - + const model = import.meta.env.REACT_APP_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL; console.log('Using model:', model); console.log('Max tokens:', MAX_TOKENS); console.log('Prompt length:', prompt.length); console.log('Previous messages:', messages.length); - + try { const requestBody = { apiKey: apiKey, @@ -38,10 +38,10 @@ async function callClaude(prompt, messages = []) { { role: 'user', content: prompt } ] }; - + console.log('Sending request to proxy...'); console.log('Request messages:', requestBody.messages.map(m => ({ role: m.role, contentLength: m.content.length }))); - + // Use local proxy server instead of direct API call const response = await fetch('http://localhost:3001/api/claude', { method: 'POST', @@ -50,19 +50,24 @@ async function callClaude(prompt, messages = []) { }, body: JSON.stringify(requestBody) }); - + console.log('Proxy response status:', response.status); - + if (!response.ok) { const error = await response.text(); console.error('Proxy error response:', error); throw new Error(`Claude API error: ${response.status} - ${error}`); } - + const data = await response.json(); console.log('Response data structure:', Object.keys(data)); console.log('Response content:', data.content?.[0]?.text?.substring(0, 200) + '...'); - return data.content[0].text; + console.log('Usage data:', data.usage); + + return { + text: data.content[0].text, + usage: data.usage // { input_tokens, output_tokens } + }; } catch (error) { console.error('Error calling Claude:', error); throw error; @@ -77,14 +82,14 @@ async function callClaude(prompt, messages = []) { */ function buildBriefingPrompt(currentData, previousBriefing) { const customInstructions = import.meta.env.REACT_APP_CLAUDE_INSTRUCTIONS; - + let prompt = ''; - + // Prepend custom instructions if provided if (customInstructions && customInstructions.trim()) { prompt += `CUSTOM INSTRUCTIONS: ${customInstructions}\n\n`; } - + prompt += `You are creating a personalized daily briefing. Be conversational and highlight what's interesting. CURRENT DATA: @@ -126,19 +131,19 @@ export async function generateBriefing(currentData, previousBriefing) { if (!currentData || Object.keys(currentData.sources || currentData).length === 0) { return 'No data available to generate briefing. Please check your data sources configuration.'; } - + try { const prompt = buildBriefingPrompt(currentData, previousBriefing); - const briefing = await callClaude(prompt); - return briefing; + const result = await callClaude(prompt); + return result; // Returns { text, usage } } catch (error) { console.error('Error generating briefing:', error); - + // Return a fallback briefing - return `Good morning! I encountered an issue generating your personalized briefing today. - + return `Good morning! I encountered an issue generating your personalized briefing today. + Error: ${error.message} - + Please check your API key configuration and try again.`; } } @@ -153,23 +158,23 @@ export async function sendChatMessage(message, conversationHistory = []) { if (!message || !message.trim()) { throw new Error('Message cannot be empty'); } - + // Build context from conversation history const messages = conversationHistory.map(msg => ({ role: msg.role === 'user' ? 'user' : 'assistant', content: msg.content })); - + // Add context about the dashboard - const contextPrompt = `You are a helpful assistant embedded in a personal dashboard application. - You have access to the user's daily briefing data and can help answer questions about news, weather, + const contextPrompt = `You are a helpful assistant embedded in a personal dashboard application. + You have access to the user's daily briefing data and can help answer questions about news, weather, and other information sources configured in their dashboard. Be conversational and helpful. - + User's message: ${message}`; - + try { - const response = await callClaude(contextPrompt, messages); - return response; + const result = await callClaude(contextPrompt, messages); + return result.text; // For chat, just return text for now } catch (error) { console.error('Error in chat:', error); return `I apologize, but I encountered an error: ${error.message}. Please try again.`; @@ -183,23 +188,23 @@ export async function sendChatMessage(message, conversationHistory = []) { * @returns {Promise} Extracted content */ export async function extractContentFromHTML(html, url) { - const prompt = `Extract the main content from this HTML page. + const prompt = `Extract the main content from this HTML page. Focus on the primary article, news content, or main information. Ignore navigation, ads, and other peripheral content. - + URL: ${url} - + HTML (truncated to first 10000 chars): ${html.substring(0, 10000)} - + Please provide a JSON response with: - title: The main title - summary: A brief summary (2-3 sentences) - mainContent: The main text content (up to 500 words) - metadata: Any relevant metadata (author, date, etc.) - + Respond with valid JSON only.`; - + try { const response = await callClaude(prompt); // Try to parse as JSON @@ -236,7 +241,7 @@ export async function summarizeData(data, dataType) { ${JSON.stringify(data, null, 2)} Be concise and focus on what would be most relevant to someone checking their daily news.`; - + try { return await callClaude(prompt); } catch (error) { diff --git a/claudash/src/services/dataFetcher.js b/claudash/src/services/dataFetcher.js index 9d886be..af35cdd 100644 --- a/claudash/src/services/dataFetcher.js +++ b/claudash/src/services/dataFetcher.js @@ -1,10 +1,10 @@ import { parseSource, standardizeSourceData } from './sourceParser'; import { getCachedData, cacheSourceData } from './storageService'; import { fetchWithCors } from '../utils/corsProxy'; -import { - DEFAULT_DATA_SOURCES, +import { + DEFAULT_DATA_SOURCES, WEATHER_API_BASE, - DEFAULT_REFRESH_INTERVAL + DEFAULT_REFRESH_INTERVAL } from '../utils/constants'; /** @@ -33,23 +33,23 @@ function getCacheKey(url) { */ export async function fetchSingleSource(url) { const cacheKey = getCacheKey(url); - + // Check cache first const cached = getCachedData(cacheKey); if (cached) { console.log(`Using cached data for ${url}`); return cached; } - + try { console.log(`Fetching fresh data from ${url}`); const data = await parseSource(url); const standardized = standardizeSourceData(data, url); - + // Cache the result const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL; cacheSourceData(cacheKey, standardized, ttl); - + return standardized; } catch (error) { console.error(`Error fetching ${url}:`, error); @@ -73,24 +73,24 @@ export async function fetchWeather(location) { console.warn('No location configured for weather'); return null; } - + const cacheKey = `weather_${location.replace(/[^a-zA-Z0-9]/g, '_')}`; - + // Check cache first const cached = getCachedData(cacheKey); if (cached) { console.log(`Using cached weather data for ${location}`); return cached; } - + try { // Format location for wttr.in const formattedLocation = encodeURIComponent(location); const weatherUrl = `${WEATHER_API_BASE}/${formattedLocation}?format=j1`; - + console.log(`Fetching weather for ${location}`); const data = await fetchWithCors(weatherUrl); - + // Standardize weather data const weatherData = { type: 'weather', @@ -100,11 +100,11 @@ export async function fetchWeather(location) { nearestArea: data.nearest_area?.[0] || {}, fetchedAt: Date.now() }; - + // Cache the result const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL; cacheSourceData(cacheKey, weatherData, ttl); - + return weatherData; } catch (error) { console.error(`Error fetching weather for ${location}:`, error); @@ -120,22 +120,22 @@ export async function fetchAllSources() { // Get configured sources from environment or use defaults const sourcesEnv = import.meta.env.REACT_APP_DATA_SOURCES; const sources = sourcesEnv ? parseEnvArray(sourcesEnv) : DEFAULT_DATA_SOURCES; - + // Get location for weather const location = import.meta.env.REACT_APP_LOCATION; - + console.log('Fetching from sources:', sources); console.log('Weather location:', location); - + // Create fetch promises - const promises = sources.map(url => + const promises = sources.map(url => fetchSingleSource(url).catch(error => ({ error: true, url: url, message: error.message })) ); - + // Add weather fetch if location is configured if (location) { promises.push( @@ -146,14 +146,14 @@ export async function fetchAllSources() { })) ); } - + // Fetch all in parallel const results = await Promise.allSettled(promises); - + // Process results const sourceData = {}; const errors = []; - + results.forEach((result, index) => { if (result.status === 'fulfilled') { const data = result.value; @@ -193,12 +193,12 @@ export async function fetchAllSources() { }); } }); - + // Log any errors if (errors.length > 0) { console.warn('Some sources failed to fetch:', errors); } - + return { sources: sourceData, errors: errors, @@ -233,7 +233,7 @@ export async function fetchHackerNewsStories(storyIds, limit = 10) { const idsToFetch = storyIds.slice(0, limit); const promises = idsToFetch.map(id => fetchHackerNewsStory(id)); const results = await Promise.allSettled(promises); - + return results .filter(r => r.status === 'fulfilled' && r.value) .map(r => r.value); diff --git a/claudash/src/services/sourceParser.js b/claudash/src/services/sourceParser.js index f9d943d..fe34959 100644 --- a/claudash/src/services/sourceParser.js +++ b/claudash/src/services/sourceParser.js @@ -8,13 +8,13 @@ import { RSS_TO_JSON_API } from '../utils/constants'; */ export function detectSourceType(url) { const urlLower = url.toLowerCase(); - + if (urlLower.endsWith('.json')) return 'json'; - if (urlLower.endsWith('.rss') || - urlLower.endsWith('.xml') || + if (urlLower.endsWith('.rss') || + urlLower.endsWith('.xml') || urlLower.includes('/feed') || urlLower.includes('rss')) return 'rss'; - + return 'html'; // Default to HTML scraping } @@ -44,18 +44,18 @@ export async function parseRSS(url) { // Use RSS to JSON service const rssUrl = `${RSS_TO_JSON_API}?rss_url=${encodeURIComponent(url)}`; const response = await fetch(rssUrl); - + if (!response.ok) { throw new Error(`RSS parse failed: ${response.status}`); } - + const data = await response.json(); - + // Check if RSS2JSON returned an error if (data.status !== 'ok') { throw new Error(data.message || 'RSS feed parsing failed'); } - + return data; } catch (error) { console.error(`Error parsing RSS from ${url}:`, error); @@ -72,7 +72,7 @@ export async function parseRSS(url) { export async function parseHTML(url, corsProxy) { try { const html = await fetchWithCors(url); - + // Return structured data for Claude to process return { url: url, @@ -94,17 +94,17 @@ export async function parseHTML(url, corsProxy) { */ export async function parseSource(url, type = null) { const sourceType = type || detectSourceType(url); - + switch(sourceType) { case 'json': return await parseJSON(url); - + case 'rss': return await parseRSS(url); - + case 'html': return await parseHTML(url); - + default: throw new Error(`Unknown source type: ${sourceType}`); } @@ -153,7 +153,7 @@ export function standardizeSourceData(data, sourceUrl) { before: data.data.before }; } - + if (sourceUrl.includes('hacker-news') && Array.isArray(data)) { // Hacker News top stories (just IDs) return { @@ -163,7 +163,7 @@ export function standardizeSourceData(data, sourceUrl) { fetchedAt: Date.now() }; } - + if (data.items && data.feed) { // RSS feed format from rss2json return { @@ -181,7 +181,7 @@ export function standardizeSourceData(data, sourceUrl) { })) }; } - + // Default format for unknown sources return { type: 'unknown', diff --git a/claudash/src/services/storageService.js b/claudash/src/services/storageService.js index a2f58cb..a4e6327 100644 --- a/claudash/src/services/storageService.js +++ b/claudash/src/services/storageService.js @@ -34,15 +34,15 @@ function saveStorage(storage) { */ export function saveBriefing(date, briefingText, sourceData) { const storage = getStorage(); - + storage.briefings[date] = { timestamp: Date.now(), briefing: briefingText, sources: sourceData }; - + saveStorage(storage); - + // Clean up old data after saving cleanupOldData(); } @@ -57,7 +57,7 @@ export function getPreviousBriefing(daysAgo = 1) { const date = new Date(); date.setDate(date.getDate() - daysAgo); const dateKey = date.toISOString().split('T')[0]; - + return storage.briefings[dateKey] || null; } @@ -69,13 +69,13 @@ export function getPreviousBriefing(daysAgo = 1) { */ export function cacheSourceData(sourceKey, data, ttlMinutes = CACHE_TTL) { const storage = getStorage(); - + storage.cache[sourceKey] = { data: data, timestamp: Date.now(), ttl: ttlMinutes * 60 * 1000 // Convert to milliseconds }; - + saveStorage(storage); } @@ -87,21 +87,21 @@ export function cacheSourceData(sourceKey, data, ttlMinutes = CACHE_TTL) { export function getCachedData(sourceKey) { const storage = getStorage(); const cached = storage.cache[sourceKey]; - + if (!cached) { return null; } - + const now = Date.now(); const age = now - cached.timestamp; - + if (age > cached.ttl) { // Cache expired delete storage.cache[sourceKey]; saveStorage(storage); return null; } - + return cached.data; } @@ -112,10 +112,10 @@ export function getCachedData(sourceKey) { export function cleanupOldData(daysToKeep = null) { const storage = getStorage(); const days = daysToKeep || parseInt(import.meta.env.REACT_APP_HISTORY_DAYS) || DEFAULT_HISTORY_DAYS; - + const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - days); - + // Clean old briefings if (storage.briefings) { Object.keys(storage.briefings).forEach(date => { @@ -124,7 +124,7 @@ export function cleanupOldData(daysToKeep = null) { } }); } - + // Clean expired cache entries const now = Date.now(); if (storage.cache) { @@ -135,7 +135,7 @@ export function cleanupOldData(daysToKeep = null) { } }); } - + saveStorage(storage); } diff --git a/claudash/src/utils/corsProxy.js b/claudash/src/utils/corsProxy.js index 3399f09..46f8ef5 100644 --- a/claudash/src/utils/corsProxy.js +++ b/claudash/src/utils/corsProxy.js @@ -8,7 +8,7 @@ import { DEFAULT_CORS_PROXY } from './constants'; */ export async function fetchWithCors(url, options = {}) { const proxy = import.meta.env.REACT_APP_CORS_PROXY || DEFAULT_CORS_PROXY; - + try { // Try direct fetch first const response = await fetch(url, { @@ -19,11 +19,11 @@ export async function fetchWithCors(url, options = {}) { 'Accept': 'application/json', } }); - + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - + const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); @@ -33,15 +33,15 @@ export async function fetchWithCors(url, options = {}) { } catch (error) { // If CORS fails, try with proxy console.warn(`CORS failed for ${url}, trying proxy...`, error.message); - + try { const proxiedUrl = `${proxy}${encodeURIComponent(url)}`; const response = await fetch(proxiedUrl, options); - + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - + const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); @@ -82,7 +82,7 @@ export async function getAccessibleUrl(url) { if (canAccess) { return url; } - + const proxy = import.meta.env.REACT_APP_CORS_PROXY || DEFAULT_CORS_PROXY; return `${proxy}${encodeURIComponent(url)}`; } \ No newline at end of file diff --git a/claudash/vite.config.js b/claudash/vite.config.js index 60f7c4b..ca52ecc 100644 --- a/claudash/vite.config.js +++ b/claudash/vite.config.js @@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react' export default defineConfig(({ mode }) => { // Load env file based on `mode` in the current working directory. const env = loadEnv(mode, process.cwd(), '') - + // Expose REACT_APP_ prefixed variables to import.meta.env const processEnvValues = { 'process.env': Object.entries(env).reduce( @@ -18,7 +18,7 @@ export default defineConfig(({ mode }) => { {}, ) } - + return { plugins: [react()], define: {