styles restructuring

This commit is contained in:
Drew Peifer 2026-02-13 23:49:18 -05:00
parent 062dea1ba6
commit d3eeaa7f40
12 changed files with 638 additions and 428 deletions

View file

@ -4,6 +4,9 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Chivo:ital,wght@0,100..900;1,100..900&family=Russo+One&family=Tektur:wght@400..900&display=swap" rel="stylesheet">
<title>claudash</title> <title>claudash</title>
</head> </head>
<body> <body>

View file

@ -1,42 +1,376 @@
#root { /* CSS Reset - replaces MUI CssBaseline */
max-width: 1280px; *,
margin: 0 auto; *::before,
padding: 2rem; *::after {
text-align: center; box-sizing: border-box;
} }
.logo { * {
height: 6em; margin: 0;
padding: 1.5em; padding: 0;
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);
} }
@keyframes logo-spin { html {
from { -webkit-font-smoothing: antialiased;
transform: rotate(0deg); -moz-osx-font-smoothing: grayscale;
} text-rendering: optimizeLegibility;
to {
transform: rotate(360deg);
}
} }
@media (prefers-reduced-motion: no-preference) { body {
a:nth-of-type(2) .logo { margin: 0;
animation: logo-spin infinite 20s linear; min-height: 100vh;
} line-height: 1.5;
font-synthesis: none;
font-weight: 400;
font-family: "Chivo", sans-serif !important;
} }
.card { h1, h2, h3, h4, h5, h6 {
padding: 2em; 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 { .app-light {
color: #888; 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;
} }

View file

@ -1,8 +1,9 @@
import { ThemeProvider, CssBaseline } from '@mui/material'; import { ThemeProvider } from '@mui/material';
import { useState } from 'react'; import { useState } from 'react';
import Dashboard from './components/Dashboard'; import Dashboard from './components/Dashboard';
import DebugPanel from './components/DebugPanel'; import DebugPanel from './components/DebugPanel';
import { lightTheme, darkTheme } from './theme'; import { lightTheme, darkTheme } from './theme';
import './App.css';
function App() { function App() {
const [isDark, setIsDark] = useState(true); const [isDark, setIsDark] = useState(true);
@ -10,14 +11,15 @@ function App() {
return ( return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}> <ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<CssBaseline /> <div className={isDark ? 'app-dark' : 'app-light'}>
<Dashboard <Dashboard
themeToggle={() => setIsDark(!isDark)} themeToggle={() => setIsDark(!isDark)}
isDark={isDark} isDark={isDark}
/> />
{debugMode && <DebugPanel />} {debugMode && <DebugPanel />}
</div>
</ThemeProvider> </ThemeProvider>
); );
} }
export default App; export default App;

View file

@ -1,5 +1,5 @@
import { useState, useRef, useEffect } from 'react'; 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 { Send, ExpandLess, ExpandMore } from '@mui/icons-material';
import { sendChatMessage } from '../services/claudeService'; import { sendChatMessage } from '../services/claudeService';
@ -66,18 +66,16 @@ function ChatInterface() {
> >
{isExpanded ? <ExpandMore /> : <ExpandLess />} {isExpanded ? <ExpandMore /> : <ExpandLess />}
</IconButton> </IconButton>
<Typography variant="body2" sx={{ ml: 1 }}> <span className="chat-header">Chat with Claude</span>
Chat with Claude
</Typography>
</Box> </Box>
{/* Messages area - only visible when expanded */} {/* Messages area - only visible when expanded */}
{isExpanded && ( {isExpanded && (
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}> <Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
{messages.length === 0 && ( {messages.length === 0 && (
<Typography color="text.secondary" variant="body2"> <p className="chat-placeholder">
Ask me anything about your briefing or the news... Ask me anything about your briefing or the news...
</Typography> </p>
)} )}
{messages.map((msg, idx) => ( {messages.map((msg, idx) => (
<Box <Box
@ -87,19 +85,9 @@ function ChatInterface() {
textAlign: msg.role === 'user' ? 'right' : 'left' textAlign: msg.role === 'user' ? 'right' : 'left'
}} }}
> >
<Typography <div className={msg.role === 'user' ? 'chat-message-user' : 'chat-message-assistant'}>
variant="body2"
sx={{
display: 'inline-block',
bgcolor: msg.role === 'user' ? 'primary.main' : 'grey.800',
color: 'white',
p: 1.5,
borderRadius: 2,
maxWidth: '70%'
}}
>
{msg.content} {msg.content}
</Typography> </div>
</Box> </Box>
))} ))}
<div ref={messagesEndRef} /> <div ref={messagesEndRef} />

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; 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 { Brightness4, Brightness7, Refresh } from '@mui/icons-material';
import { fetchAllSources } from '../services/dataFetcher'; import { fetchAllSources } from '../services/dataFetcher';
import { generateBriefing } from '../services/claudeService'; import { generateBriefing } from '../services/claudeService';
@ -10,241 +10,233 @@ import NewsWidget from './widgets/NewsWidget';
import ChatInterface from './ChatInterface'; import ChatInterface from './ChatInterface';
function Dashboard({ themeToggle, isDark }) { function Dashboard({ themeToggle, isDark }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [loadingStatus, setLoadingStatus] = useState('Initializing...'); const [loadingStatus, setLoadingStatus] = useState('Initializing...');
const [briefing, setBriefing] = useState(''); const [briefing, setBriefing] = useState('');
const [sourceData, setSourceData] = useState({}); const [sourceData, setSourceData] = useState({});
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [tokenUsage, setTokenUsage] = useState(null); const [tokenUsage, setTokenUsage] = useState(null);
const [cacheInfo, setCacheInfo] = useState(null); const [cacheInfo, setCacheInfo] = useState(null);
useEffect(() => { useEffect(() => {
loadDashboard(); 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...');
async function loadDashboard(forceRefresh = false) {
try { try {
// Generate today's briefing with Claude console.log('=== loadDashboard called ===');
const result = await generateBriefing(data, previousBriefing); console.log('Force refresh:', forceRefresh);
console.log('Briefing generated'); setError(null);
// Handle result object with text and usage // Check if we already have today's briefing (unless forcing refresh)
if (result && typeof result === 'object' && result.text) { if (!forceRefresh && hasTodaysBriefing()) {
setBriefing(result.text); console.log('Using cached briefing from today');
setTokenUsage(result.usage); const todaysBriefing = getTodaysBriefing();
setCacheInfo(null); // Clear cache info when generating fresh briefing setBriefing(todaysBriefing.briefing);
console.log('Token usage:', result.usage); setSourceData(todaysBriefing.sources || {});
} else if (typeof result === 'string') {
// Fallback for string responses
setBriefing(result);
setCacheInfo(null);
}
// Save to localStorage // Calculate cache age
const today = new Date().toISOString().split('T')[0]; const cacheAge = Date.now() - todaysBriefing.timestamp;
console.log('Saving briefing for:', today); const cacheAgeMinutes = Math.floor(cacheAge / (1000 * 60));
const briefingText = result?.text || result;
saveBriefing(today, briefingText, data.sources); // Set cache info to display message
} catch (briefingError) { setCacheInfo({
console.error('Failed to generate briefing:', briefingError); age: cacheAgeMinutes,
setBriefing(`Unable to generate AI briefing: ${briefingError.message}\n\nYou can still view the data widgets below.`); 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 = () => { const handleRefresh = () => {
setRefreshing(true); setRefreshing(true);
loadDashboard(true); loadDashboard(true);
}; };
if (loading) {
return (
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
<CircularProgress />
<p className="loading-status">{loadingStatus}</p>
</Box>
);
}
if (loading) {
return ( return (
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}> <Box sx={{ minHeight: '100vh', pb: 10 }}>
<CircularProgress /> {/* Top toolbar */}
<Typography variant="body1" color="text.secondary"> <Box sx={{
{loadingStatus} position: 'fixed',
</Typography> top: 0,
</Box> right: 0,
); p: 2,
} display: 'flex',
gap: 1,
zIndex: 100
}}>
<IconButton
onClick={() => {
localStorage.clear();
window.location.reload();
}}
title="Clear all cache and reload"
color="error"
>
🗑
</IconButton>
<IconButton
onClick={handleRefresh}
disabled={refreshing}
title="Refresh data"
>
<Refresh />
</IconButton>
<IconButton
onClick={themeToggle}
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Box>
return ( <Container maxWidth="xl" sx={{ pt: 4 }}>
<Box sx={{ minHeight: '100vh', pb: 10 }}> {error && (
{/* Top toolbar */} <Alert severity="error" sx={{ mb: 2 }}>
<Box sx={{ {error}
position: 'fixed', </Alert>
top: 0, )}
right: 0,
p: 2,
display: 'flex',
gap: 1,
zIndex: 100
}}>
<IconButton
onClick={() => {
localStorage.clear();
window.location.reload();
}}
title="Clear all cache and reload"
color="error"
>
🗑
</IconButton>
<IconButton
onClick={handleRefresh}
disabled={refreshing}
title="Refresh data"
>
<Refresh />
</IconButton>
<IconButton
onClick={themeToggle}
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
{isDark ? <Brightness7 /> : <Brightness4 />}
</IconButton>
</Box>
<Container maxWidth="xl" sx={{ pt: 4 }}> {tokenUsage && (
{error && ( <div className="token-usage-box">
<Alert severity="error" sx={{ mb: 2 }}> <p className="token-usage-text">
{error} <strong>API Usage:</strong> {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens
</Alert> {' • '}
)} <strong>Cost:</strong> ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)}
{' '}
<span className="token-usage-caption">
(Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output)
</span>
</p>
</div>
)}
{tokenUsage && ( {cacheInfo && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}> <div className="cache-info-box">
<Typography variant="body2" color="text.secondary"> <p className="cache-info-text">
<strong>API Usage:</strong> {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens <strong>💾 Using cached results</strong> from {cacheInfo.age < 1 ? 'less than a minute' : cacheInfo.age === 1 ? '1 minute' : `${cacheInfo.age} minutes`} ago no API costs incurred
{' • '} {' '}
<strong>Cost:</strong> ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)} <span className="cache-info-caption">
{' '} (Click refresh to generate a new briefing)
<Typography component="span" variant="caption" color="text.disabled"> </span>
(Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output) </p>
</Typography> </div>
</Typography> )}
</Box>
)}
{cacheInfo && ( <Box className="widget-grid">
<Box sx={{ mb: 2, p: 2, bgcolor: 'success.main', color: 'success.contrastText', borderRadius: 1, border: '1px solid', borderColor: 'success.dark' }}> <Box className="widget-row">
<Typography variant="body2"> <BriefingWidget briefing={briefing} />
<strong>💾 Using cached results</strong> from {cacheInfo.age < 1 ? 'less than a minute' : cacheInfo.age === 1 ? '1 minute' : `${cacheInfo.age} minutes`} ago no API costs incurred <WeatherWidget
{' '} data={sourceData.weather}
<Typography component="span" variant="caption" sx={{ opacity: 0.9 }}> />
(Click refresh to generate a new briefing) </Box>
</Typography> <Box className="widget-row condensed">
</Typography> {/* Render NewsWidget for each news source */}
</Box> {Object.entries(sourceData).map(([key, data]) => {
)} if (key === 'weather') return null;
return (
<NewsWidget
key={key}
data={data}
/>
);
})}
</Box>
</Box>
</Container>
{/* Widget Grid - easily rearrangeable */} {/* Fixed chat at bottom */}
<Box sx={{ <ChatInterface />
display: 'flex',
flexWrap: 'wrap',
gap: 2,
mb: 4
}}>
<BriefingWidget briefing={briefing} sx={{ flex: '1 1 100%' }} />
<WeatherWidget
data={sourceData.weather}
sx={{ flex: '1 1 300px' }}
/>
{/* Render NewsWidget for each news source */}
{Object.entries(sourceData).map(([key, data]) => {
if (key === 'weather') return null;
return (
<NewsWidget
key={key}
data={data}
sx={{ flex: '1 1 400px' }}
/>
);
})}
</Box> </Box>
</Container> );
{/* Fixed chat at bottom */}
<ChatInterface />
</Box>
);
} }
export default Dashboard; export default Dashboard;

View file

@ -1,5 +1,5 @@
import { useState } from 'react'; 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 { Download, Upload } from '@mui/icons-material';
import { exportAllData, importDummyData } from '../services/storageService'; import { exportAllData, importDummyData } from '../services/storageService';
@ -38,7 +38,7 @@ function DebugPanel() {
return ( return (
<Paper sx={{ position: 'fixed', bottom: 80, right: 16, p: 2, width: 250, zIndex: 999 }}> <Paper sx={{ position: 'fixed', bottom: 80, right: 16, p: 2, width: 250, zIndex: 999 }}>
<Typography variant="h6" gutterBottom>Debug Tools</Typography> <h3 className="debug-title">Debug Tools</h3>
<Button <Button
fullWidth fullWidth
@ -66,13 +66,7 @@ function DebugPanel() {
</Button> </Button>
{dummyMode && ( {dummyMode && (
<Typography <span className="debug-warning">Using dummy data</span>
variant="caption"
color="warning.main"
sx={{ mt: 1, display: 'block' }}
>
Using dummy data
</Typography>
)} )}
</Paper> </Paper>
); );

View file

@ -1,4 +1,4 @@
import { Typography, Skeleton, Box } from '@mui/material'; import { Skeleton, Box } from '@mui/material';
import GenericWidget from './GenericWidget'; import GenericWidget from './GenericWidget';
function BriefingWidget({ briefing, sx }) { function BriefingWidget({ briefing, sx }) {
@ -9,16 +9,7 @@ function BriefingWidget({ briefing, sx }) {
> >
<Box sx={{ mt: 1 }}> <Box sx={{ mt: 1 }}>
{briefing ? ( {briefing ? (
<Typography <p className="briefing-text">{briefing}</p>
variant="body1"
sx={{
whiteSpace: 'pre-line',
lineHeight: 1.7,
fontSize: '1.05rem'
}}
>
{briefing}
</Typography>
) : ( ) : (
<> <>
<Skeleton variant="text" sx={{ fontSize: '1rem' }} /> <Skeleton variant="text" sx={{ fontSize: '1rem' }} />
@ -33,4 +24,4 @@ function BriefingWidget({ briefing, sx }) {
); );
} }
export default BriefingWidget; export default BriefingWidget;

View file

@ -1,4 +1,4 @@
import { Paper, Typography, Box } from '@mui/material'; import { Paper, Box } from '@mui/material';
function GenericWidget({ title, data, children, sx, ...props }) { function GenericWidget({ title, data, children, sx, ...props }) {
return ( return (
@ -11,12 +11,12 @@ function GenericWidget({ title, data, children, sx, ...props }) {
}} }}
{...props} {...props}
> >
<Typography variant="h6" gutterBottom>{title}</Typography> <h2 className="widget-title">{title}</h2>
<Box> <Box>
{children || <Typography>{JSON.stringify(data)}</Typography>} {children || <p>{JSON.stringify(data)}</p>}
</Box> </Box>
</Paper> </Paper>
); );
} }
export default GenericWidget; export default GenericWidget;

View file

@ -1,14 +1,14 @@
import { Typography, Box, Link, Chip, List, ListItem, ListItemText } from '@mui/material'; import { Box, Link, Chip, List, ListItem, ListItemText } from '@mui/material';
import { OpenInNew, Comment, TrendingUp } from '@mui/icons-material'; import { OpenInNew, Comment, TrendingUp } from '@mui/icons-material';
import GenericWidget from './GenericWidget'; import GenericWidget from './GenericWidget';
function NewsWidget({ data, sx }) { function NewsWidget({ data, sx }) {
if (!data || data.error) { if (!data || data.error) {
return ( return (
<GenericWidget title="News" sx={{ opacity: 0.5, ...sx }}> <GenericWidget className="news-widget" title="News" sx={{ opacity: 0.5, ...sx }}>
<Typography color="text.secondary"> <p className="widget-error">
{data?.message || 'News data unavailable'} {data?.message || 'News data unavailable'}
</Typography> </p>
</GenericWidget> </GenericWidget>
); );
} }
@ -18,7 +18,7 @@ function NewsWidget({ data, sx }) {
const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit'; const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit';
return ( return (
<GenericWidget title={`r/${subreddit}`} sx={{ ...sx }}> <GenericWidget className="news-widget" title={`r/${subreddit}`} sx={{ ...sx }}>
<List dense> <List dense>
{data.posts.slice(0, 5).map((post, index) => ( {data.posts.slice(0, 5).map((post, index) => (
<ListItem key={index} disablePadding sx={{ mb: 1 }}> <ListItem key={index} disablePadding sx={{ mb: 1 }}>
@ -55,10 +55,10 @@ function NewsWidget({ data, sx }) {
'&:hover': { textDecoration: 'underline' } '&:hover': { textDecoration: 'underline' }
}} }}
> >
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <div className="news-title">
{post.title} {post.title}
<OpenInNew sx={{ fontSize: 14 }} /> <OpenInNew sx={{ fontSize: 14 }} />
</Typography> </div>
</Link> </Link>
</Box> </Box>
} }
@ -77,9 +77,7 @@ function NewsWidget({ data, sx }) {
size="small" size="small"
variant="outlined" variant="outlined"
/> />
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, alignSelf: 'center' }}> <span className="news-author">by {post.author}</span>
by {post.author}
</Typography>
</Box> </Box>
} }
/> />
@ -130,34 +128,23 @@ function NewsWidget({ data, sx }) {
'&:hover': { textDecoration: 'underline' } '&:hover': { textDecoration: 'underline' }
}} }}
> >
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> <div className="news-title">
{item.title} {item.title}
<OpenInNew sx={{ fontSize: 14 }} /> <OpenInNew sx={{ fontSize: 14 }} />
</Typography> </div>
</Link> </Link>
</Box> </Box>
} }
secondaryTypographyProps={{ component: 'div' }} secondaryTypographyProps={{ component: 'div' }}
secondary={ secondary={
<Box> <Box>
<Typography variant="caption" color="text.secondary"> <div className="news-date">
{item.pubDate && new Date(item.pubDate).toLocaleDateString()} {item.pubDate && new Date(item.pubDate).toLocaleDateString()}
</Typography> </div>
{item.description && ( {item.description && (
<Typography <div className="news-description">
variant="caption"
color="text.secondary"
sx={{
display: '-webkit-box',
mt: 0.5,
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{item.description.replace(/<[^>]*>?/gm, '')} {item.description.replace(/<[^>]*>?/gm, '')}
</Typography> </div>
)} )}
</Box> </Box>
} }
@ -173,12 +160,12 @@ function NewsWidget({ data, sx }) {
if (data.type === 'hackernews' && data.storyIds) { if (data.type === 'hackernews' && data.storyIds) {
return ( return (
<GenericWidget title="Hacker News" sx={{ ...sx }}> <GenericWidget title="Hacker News" sx={{ ...sx }}>
<Typography variant="body2" color="text.secondary"> <p className="widget-info">
{data.storyIds.length} top stories loaded {data.storyIds.length} top stories loaded
</Typography> </p>
<Typography variant="caption" color="text.secondary"> <p className="widget-caption">
(Story details will load when API is configured) (Story details will load when API is configured)
</Typography> </p>
</GenericWidget> </GenericWidget>
); );
} }
@ -186,12 +173,12 @@ function NewsWidget({ data, sx }) {
// Default fallback for unknown data types // Default fallback for unknown data types
return ( return (
<GenericWidget title="News" sx={{ ...sx }}> <GenericWidget title="News" sx={{ ...sx }}>
<Typography variant="body2" color="text.secondary"> <p className="widget-info">
Data format not recognized Data format not recognized
</Typography> </p>
<Typography variant="caption" color="text.secondary"> <p className="widget-caption">
{JSON.stringify(data).substring(0, 100)}... {JSON.stringify(data).substring(0, 100)}...
</Typography> </p>
</GenericWidget> </GenericWidget>
); );
} }

View file

@ -1,4 +1,4 @@
import { Typography, Box, Chip, Stack } from '@mui/material'; import { Box, Chip, Stack } from '@mui/material';
import { Cloud, WbSunny, Thunderstorm, AcUnit } from '@mui/icons-material'; import { Cloud, WbSunny, Thunderstorm, AcUnit } from '@mui/icons-material';
import GenericWidget from './GenericWidget'; import GenericWidget from './GenericWidget';
@ -15,9 +15,9 @@ function WeatherWidget({ data, sx }) {
if (!data || data.error) { if (!data || data.error) {
return ( return (
<GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}> <GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}>
<Typography color="text.secondary"> <p className="widget-error">
{data?.message || 'Weather data unavailable'} {data?.message || 'Weather data unavailable'}
</Typography> </p>
</GenericWidget> </GenericWidget>
); );
} }
@ -39,18 +39,12 @@ function WeatherWidget({ data, sx }) {
{getWeatherIcon(description)} {getWeatherIcon(description)}
</Box> </Box>
<Box> <Box>
<Typography variant="h3" component="span"> <span className="weather-temp">{temp}</span>
{temp} <span className="weather-unit">{tempUnit}</span>
</Typography>
<Typography variant="h5" component="span" color="text.secondary">
{tempUnit}
</Typography>
</Box> </Box>
</Box> </Box>
<Typography variant="body1" sx={{ mb: 2 }}> <p className="weather-description">{description}</p>
{description}
</Typography>
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1}>
<Chip <Chip
@ -67,18 +61,14 @@ function WeatherWidget({ data, sx }) {
{data.forecast && data.forecast.length > 0 && ( {data.forecast && data.forecast.length > 0 && (
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom> <div className="forecast-label">Forecast</div>
Forecast
</Typography>
<Stack direction="row" spacing={1}> <Stack direction="row" spacing={1}>
{data.forecast.slice(0, 3).map((day, index) => ( {data.forecast.slice(0, 3).map((day, index) => (
<Box key={index} sx={{ textAlign: 'center' }}> <Box key={index} sx={{ textAlign: 'center' }}>
<Typography variant="caption" display="block"> <div className="forecast-day">Day {index + 1}</div>
Day {index + 1} <div className="forecast-temp">
</Typography>
<Typography variant="body2">
{day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}° {day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
</Typography> </div>
</Box> </Box>
))} ))}
</Stack> </Stack>

View file

@ -1,68 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
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;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View file

@ -1,9 +1,6 @@
import { createTheme } from '@mui/material/styles'; import { createTheme } from '@mui/material/styles';
const sharedTheme = { const sharedTheme = {
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
},
shape: { shape: {
borderRadius: 8, borderRadius: 8,
}, },