diff --git a/claudash/index.html b/claudash/index.html index 0654a82..65e1862 100644 --- a/claudash/index.html +++ b/claudash/index.html @@ -4,6 +4,9 @@ + + + claudash diff --git a/claudash/src/App.css b/claudash/src/App.css index b9d355d..af301c7 100644 --- a/claudash/src/App.css +++ b/claudash/src/App.css @@ -1,42 +1,376 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; +/* CSS Reset - replaces MUI CssBaseline */ +*, +*::before, +*::after { + box-sizing: border-box; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); +* { + margin: 0; + padding: 0; } -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } +html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } +body { + margin: 0; + min-height: 100vh; + line-height: 1.5; + font-synthesis: none; + font-weight: 400; + font-family: "Chivo", sans-serif !important; } -.card { - padding: 2em; +h1, h2, h3, h4, h5, h6 { + font-family: "Russo One", sans-serif !important; + font-size:1.2rem; +} +/* App theme containers */ +.app-dark { + min-height: 100vh; + background-color: #0a1929; + color: rgba(255, 255, 255, 0.87); } -.read-the-docs { - color: #888; +.app-light { + min-height: 100vh; + background-color: #ffffff; + color: rgba(0, 0, 0, 0.87); +} +.widget-grid { + display:flex; + flex-direction: column; + gap: 20px; + justify-content: space-evenly; +} +.widget-row { + display:flex; + justify-content: flex-start; + flex-direction: row; + gap: 10px; +} +.widget-row.condensed { + flex-wrap: wrap; + justify-content: space-between; +} +.widget-row.condensed .news-widget{ + flex-grow: 2; +} + +a { + font-weight: 500; + color: #90caf9; + text-decoration: inherit; +} + +a:hover { + color: #64b5f6; +} + +.app-light a { + color: #1976d2; +} + +.app-light a:hover { + color: #1565c0; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} + +button:hover { + border-color: #646cff; +} + +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +.app-light button { + background-color: #f5f5f5; + color: rgba(0, 0, 0, 0.87); +} + +/* Input/TextField resets - form elements need explicit inheritance */ +input, textarea { + font-family: inherit; +} + +/* Chat Interface Styles */ +.chat-header { + margin-left: 8px; + font-size: 0.875rem; +} + +.chat-placeholder { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + margin: 0; +} + +.app-light .chat-placeholder { + color: rgba(0, 0, 0, 0.6); +} + +.chat-message-user { + display: inline-block; + padding: 12px; + border-radius: 8px; + max-width: 70%; + background-color: #1976d2; + color: white; + font-size: 0.875rem; +} + +.chat-message-assistant { + display: inline-block; + padding: 12px; + border-radius: 8px; + max-width: 70%; + background-color: #424242; + color: white; + font-size: 0.875rem; +} + +.app-light .chat-message-assistant { + background-color: #e0e0e0; + color: #000000; +} + +/* Dashboard Styles */ +.loading-status { + color: rgba(255, 255, 255, 0.6); + font-size: 1rem; + margin: 0; +} + +.app-light .loading-status { + color: rgba(0, 0, 0, 0.6); +} + +.token-usage-box { + margin-bottom: 16px; + padding: 16px; + background-color: #1e2a35; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.app-light .token-usage-box { + background-color: #ffffff; + border: 1px solid rgba(0, 0, 0, 0.12); +} + +.token-usage-text { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + margin: 0; +} + +.app-light .token-usage-text { + color: rgba(0, 0, 0, 0.6); +} + +.token-usage-caption { + color: rgba(255, 255, 255, 0.38); + font-size: 0.75rem; +} + +.app-light .token-usage-caption { + color: rgba(0, 0, 0, 0.38); +} + +.cache-info-box { + margin-bottom: 16px; + padding: 16px; + background-color: #2e7d32; + color: white; + border-radius: 4px; + border: 1px solid #1b5e20; +} + +.app-light .cache-info-box { + background-color: #4caf50; + color: white; + border: 1px solid #388e3c; +} + +.cache-info-text { + font-size: 0.875rem; + margin: 0; +} + +.cache-info-caption { + font-size: 0.75rem; + opacity: 0.9; +} + +/* Widget Styles */ +.widget-title { + font-size: 2rem; + font-weight: 500; + margin: 0 0 8px 0; + line-height: 1.6; +} + +.widget-error { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + margin: 0; +} + +.app-light .widget-error { + color: rgba(0, 0, 0, 0.6); +} + +.widget-info { + color: rgba(255, 255, 255, 0.6); + font-size: 0.875rem; + margin: 0 0 8px 0; +} + +.app-light .widget-info { + color: rgba(0, 0, 0, 0.6); +} + +.widget-caption { + color: rgba(255, 255, 255, 0.6); + font-size: 0.75rem; + margin: 0; +} + +.app-light .widget-caption { + color: rgba(0, 0, 0, 0.6); +} + +/* Briefing Widget Styles */ +.briefing-text { + white-space: pre-line; + line-height: 1.7; + font-size: 1.05rem; + margin: 8px 0 0 0; +} + +/* Weather Widget Styles */ +.weather-temp { + font-size: 3rem; + font-weight: 400; + line-height: 1; +} + +.weather-unit { + font-size: 1.5rem; + color: rgba(255, 255, 255, 0.6); + margin-left: 4px; +} + +.app-light .weather-unit { + color: rgba(0, 0, 0, 0.6); +} + +.weather-description { + font-size: 1rem; + margin: 0 0 16px 0; +} + +.forecast-label { + font-size: 0.875rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 8px; +} + +.app-light .forecast-label { + color: rgba(0, 0, 0, 0.6); +} + +.forecast-day { + font-size: 0.75rem; + display: block; + margin-bottom: 4px; +} + +.forecast-temp { + font-size: 0.875rem; +} + +/* News Widget Styles */ +.news-title { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.875rem; +} + +.news-author { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.6); + margin-left: 8px; + align-self: center; +} + +.app-light .news-author { + color: rgba(0, 0, 0, 0.6); +} + +.news-date { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 4px; +} + +.app-light .news-date { + color: rgba(0, 0, 0, 0.6); +} + +.news-description { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.6); + display: -webkit-box; + margin-top: 4px; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.app-light .news-description { + color: rgba(0, 0, 0, 0.6); +} + +/* Debug Panel Styles */ +.debug-title { + font-size: 1.25rem; + font-weight: 500; + margin: 0 0 16px 0; +} + +.debug-warning { + color: #ed6c02; + font-size: 0.75rem; + margin-top: 8px; + display: block; +} + +.app-light .debug-warning { + color: #e65100; } diff --git a/claudash/src/App.jsx b/claudash/src/App.jsx index 69c78ec..4f70eb5 100644 --- a/claudash/src/App.jsx +++ b/claudash/src/App.jsx @@ -1,8 +1,9 @@ -import { ThemeProvider, CssBaseline } from '@mui/material'; +import { ThemeProvider } from '@mui/material'; import { useState } from 'react'; import Dashboard from './components/Dashboard'; import DebugPanel from './components/DebugPanel'; import { lightTheme, darkTheme } from './theme'; +import './App.css'; function App() { const [isDark, setIsDark] = useState(true); @@ -10,14 +11,15 @@ function App() { return ( - - setIsDark(!isDark)} - isDark={isDark} - /> - {debugMode && } +
+ setIsDark(!isDark)} + isDark={isDark} + /> + {debugMode && } +
); } -export default App; \ No newline at end of file +export default App; diff --git a/claudash/src/components/ChatInterface.jsx b/claudash/src/components/ChatInterface.jsx index fb708bf..ce5dc6f 100644 --- a/claudash/src/components/ChatInterface.jsx +++ b/claudash/src/components/ChatInterface.jsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect } from 'react'; -import { Box, TextField, IconButton, Paper, Typography } from '@mui/material'; +import { Box, TextField, IconButton, Paper } from '@mui/material'; import { Send, ExpandLess, ExpandMore } from '@mui/icons-material'; import { sendChatMessage } from '../services/claudeService'; @@ -66,18 +66,16 @@ function ChatInterface() { > {isExpanded ? : } - - Chat with Claude - + Chat with Claude {/* Messages area - only visible when expanded */} {isExpanded && ( {messages.length === 0 && ( - +

Ask me anything about your briefing or the news... - +

)} {messages.map((msg, idx) => ( - +
{msg.content} - +
))}
diff --git a/claudash/src/components/Dashboard.jsx b/claudash/src/components/Dashboard.jsx index 3e0ba02..aa00cf4 100644 --- a/claudash/src/components/Dashboard.jsx +++ b/claudash/src/components/Dashboard.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Box, Container, CircularProgress, IconButton, Alert, Typography } from '@mui/material'; +import { Box, Container, CircularProgress, IconButton, Alert } from '@mui/material'; import { Brightness4, Brightness7, Refresh } from '@mui/icons-material'; import { fetchAllSources } from '../services/dataFetcher'; import { generateBriefing } from '../services/claudeService'; @@ -10,241 +10,233 @@ import NewsWidget from './widgets/NewsWidget'; import ChatInterface from './ChatInterface'; function Dashboard({ themeToggle, isDark }) { - const [loading, setLoading] = useState(true); - const [loadingStatus, setLoadingStatus] = useState('Initializing...'); - const [briefing, setBriefing] = useState(''); - const [sourceData, setSourceData] = useState({}); - const [error, setError] = useState(null); - const [refreshing, setRefreshing] = useState(false); - const [tokenUsage, setTokenUsage] = useState(null); - const [cacheInfo, setCacheInfo] = useState(null); + const [loading, setLoading] = useState(true); + const [loadingStatus, setLoadingStatus] = useState('Initializing...'); + const [briefing, setBriefing] = useState(''); + const [sourceData, setSourceData] = useState({}); + const [error, setError] = useState(null); + const [refreshing, setRefreshing] = useState(false); + const [tokenUsage, setTokenUsage] = useState(null); + const [cacheInfo, setCacheInfo] = 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'); - const todaysBriefing = getTodaysBriefing(); - setBriefing(todaysBriefing.briefing); - setSourceData(todaysBriefing.sources || {}); - - // Calculate cache age - const cacheAge = Date.now() - todaysBriefing.timestamp; - const cacheAgeMinutes = Math.floor(cacheAge / (1000 * 60)); - - // Set cache info to display message - setCacheInfo({ - age: cacheAgeMinutes, - timestamp: todaysBriefing.timestamp - }); - setTokenUsage(null); // Clear token usage when using cache - - 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...'); + useEffect(() => { + loadDashboard(); + }, []); + async function loadDashboard(forceRefresh = false) { try { - // Generate today's briefing with Claude - const result = await generateBriefing(data, previousBriefing); - console.log('Briefing generated'); + console.log('=== loadDashboard called ==='); + console.log('Force refresh:', forceRefresh); + setError(null); - // Handle result object with text and usage - if (result && typeof result === 'object' && result.text) { - setBriefing(result.text); - setTokenUsage(result.usage); - setCacheInfo(null); // Clear cache info when generating fresh briefing - console.log('Token usage:', result.usage); - } else if (typeof result === 'string') { - // Fallback for string responses - setBriefing(result); - setCacheInfo(null); - } + // Check if we already have today's briefing (unless forcing refresh) + if (!forceRefresh && hasTodaysBriefing()) { + console.log('Using cached briefing from today'); + const todaysBriefing = getTodaysBriefing(); + setBriefing(todaysBriefing.briefing); + setSourceData(todaysBriefing.sources || {}); - // Save to localStorage - const today = new Date().toISOString().split('T')[0]; - console.log('Saving briefing for:', today); - 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.`); + // Calculate cache age + const cacheAge = Date.now() - todaysBriefing.timestamp; + const cacheAgeMinutes = Math.floor(cacheAge / (1000 * 60)); + + // Set cache info to display message + setCacheInfo({ + age: cacheAgeMinutes, + timestamp: todaysBriefing.timestamp + }); + setTokenUsage(null); // Clear token usage when using cache + + 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 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); + setCacheInfo(null); // Clear cache info when generating fresh briefing + console.log('Token usage:', result.usage); + } else if (typeof result === 'string') { + // Fallback for string responses + setBriefing(result); + setCacheInfo(null); + } + + // Save to localStorage + const today = new Date().toISOString().split('T')[0]; + console.log('Saving briefing for:', today); + 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.`); + } + } 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); + setError(`Failed to load dashboard: ${error.message}`); + } finally { + setLoading(false); + setRefreshing(false); } - } 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); - setError(`Failed to load dashboard: ${error.message}`); - } finally { - setLoading(false); - setRefreshing(false); } - } - const handleRefresh = () => { - setRefreshing(true); - loadDashboard(true); - }; + const handleRefresh = () => { + setRefreshing(true); + loadDashboard(true); + }; + + if (loading) { + return ( + + +

{loadingStatus}

+
+ ); + } - if (loading) { return ( - - - - {loadingStatus} - - - ); - } + + {/* Top toolbar */} + + { + localStorage.clear(); + window.location.reload(); + }} + title="Clear all cache and reload" + color="error" + > + 🗑️ + + + + + + {isDark ? : } + + - return ( - - {/* Top toolbar */} - - { - localStorage.clear(); - window.location.reload(); - }} - title="Clear all cache and reload" - color="error" - > - 🗑️ - - - - - - {isDark ? : } - - + + {error && ( + + {error} + + )} - - {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) + +

+
+ )} - {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) - - - - )} + {cacheInfo && ( +
+

+ 💾 Using cached results from {cacheInfo.age < 1 ? 'less than a minute' : cacheInfo.age === 1 ? '1 minute' : `${cacheInfo.age} minutes`} ago — no API costs incurred + {' '} + + (Click refresh to generate a new briefing) + +

+
+ )} - {cacheInfo && ( - - - 💾 Using cached results from {cacheInfo.age < 1 ? 'less than a minute' : cacheInfo.age === 1 ? '1 minute' : `${cacheInfo.age} minutes`} ago — no API costs incurred - {' '} - - (Click refresh to generate a new briefing) - - - - )} + + + + + + + {/* Render NewsWidget for each news source */} + {Object.entries(sourceData).map(([key, data]) => { + if (key === 'weather') return null; + return ( + + ); + })} + + +
- {/* 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 */} + -
- - {/* Fixed chat at bottom */} - -
- ); + ); } export default Dashboard; \ No newline at end of file diff --git a/claudash/src/components/DebugPanel.jsx b/claudash/src/components/DebugPanel.jsx index b36ae75..10b5672 100644 --- a/claudash/src/components/DebugPanel.jsx +++ b/claudash/src/components/DebugPanel.jsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { Button, Box, Paper, Typography } from '@mui/material'; +import { Button, Box, Paper } from '@mui/material'; import { Download, Upload } from '@mui/icons-material'; import { exportAllData, importDummyData } from '../services/storageService'; @@ -38,7 +38,7 @@ function DebugPanel() { return ( - Debug Tools +

Debug Tools