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" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<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>
</head>
<body>

View file

@ -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;
}

View file

@ -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 (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<CssBaseline />
<Dashboard
themeToggle={() => setIsDark(!isDark)}
isDark={isDark}
/>
{debugMode && <DebugPanel />}
<div className={isDark ? 'app-dark' : 'app-light'}>
<Dashboard
themeToggle={() => setIsDark(!isDark)}
isDark={isDark}
/>
{debugMode && <DebugPanel />}
</div>
</ThemeProvider>
);
}
export default App;
export default App;

View file

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

View file

@ -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 (
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
<CircularProgress />
<p className="loading-status">{loadingStatus}</p>
</Box>
);
}
if (loading) {
return (
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
<CircularProgress />
<Typography variant="body1" color="text.secondary">
{loadingStatus}
</Typography>
</Box>
);
}
<Box sx={{ minHeight: '100vh', pb: 10 }}>
{/* Top toolbar */}
<Box sx={{
position: 'fixed',
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>
return (
<Box sx={{ minHeight: '100vh', pb: 10 }}>
{/* Top toolbar */}
<Box sx={{
position: 'fixed',
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 }}>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Container maxWidth="xl" sx={{ pt: 4 }}>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{tokenUsage && (
<div className="token-usage-box">
<p className="token-usage-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>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 && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'background.paper', borderRadius: 1, border: '1px solid', borderColor: 'divider' }}>
<Typography variant="body2" color="text.secondary">
<strong>API Usage:</strong> {tokenUsage.input_tokens?.toLocaleString()} input + {tokenUsage.output_tokens?.toLocaleString()} output = {(tokenUsage.input_tokens + tokenUsage.output_tokens).toLocaleString()} total tokens
{' • '}
<strong>Cost:</strong> ${((tokenUsage.input_tokens * 0.003 / 1000) + (tokenUsage.output_tokens * 0.015 / 1000)).toFixed(4)}
{' '}
<Typography component="span" variant="caption" color="text.disabled">
(Claude Sonnet 4 pricing: $0.003/1K input, $0.015/1K output)
</Typography>
</Typography>
</Box>
)}
{cacheInfo && (
<div className="cache-info-box">
<p className="cache-info-text">
<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
{' '}
<span className="cache-info-caption">
(Click refresh to generate a new briefing)
</span>
</p>
</div>
)}
{cacheInfo && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'success.main', color: 'success.contrastText', borderRadius: 1, border: '1px solid', borderColor: 'success.dark' }}>
<Typography variant="body2">
<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
{' '}
<Typography component="span" variant="caption" sx={{ opacity: 0.9 }}>
(Click refresh to generate a new briefing)
</Typography>
</Typography>
</Box>
)}
<Box className="widget-grid">
<Box className="widget-row">
<BriefingWidget briefing={briefing} />
<WeatherWidget
data={sourceData.weather}
/>
</Box>
<Box className="widget-row condensed">
{/* Render NewsWidget for each news source */}
{Object.entries(sourceData).map(([key, data]) => {
if (key === 'weather') return null;
return (
<NewsWidget
key={key}
data={data}
/>
);
})}
</Box>
</Box>
</Container>
{/* Widget Grid - easily rearrangeable */}
<Box sx={{
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' }}
/>
);
})}
{/* Fixed chat at bottom */}
<ChatInterface />
</Box>
</Container>
{/* Fixed chat at bottom */}
<ChatInterface />
</Box>
);
);
}
export default Dashboard;

View file

@ -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 (
<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
fullWidth
@ -66,13 +66,7 @@ function DebugPanel() {
</Button>
{dummyMode && (
<Typography
variant="caption"
color="warning.main"
sx={{ mt: 1, display: 'block' }}
>
Using dummy data
</Typography>
<span className="debug-warning">Using dummy data</span>
)}
</Paper>
);

View file

@ -1,4 +1,4 @@
import { Typography, Skeleton, Box } from '@mui/material';
import { Skeleton, Box } from '@mui/material';
import GenericWidget from './GenericWidget';
function BriefingWidget({ briefing, sx }) {
@ -9,16 +9,7 @@ function BriefingWidget({ briefing, sx }) {
>
<Box sx={{ mt: 1 }}>
{briefing ? (
<Typography
variant="body1"
sx={{
whiteSpace: 'pre-line',
lineHeight: 1.7,
fontSize: '1.05rem'
}}
>
{briefing}
</Typography>
<p className="briefing-text">{briefing}</p>
) : (
<>
<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 }) {
return (
@ -11,12 +11,12 @@ function GenericWidget({ title, data, children, sx, ...props }) {
}}
{...props}
>
<Typography variant="h6" gutterBottom>{title}</Typography>
<h2 className="widget-title">{title}</h2>
<Box>
{children || <Typography>{JSON.stringify(data)}</Typography>}
{children || <p>{JSON.stringify(data)}</p>}
</Box>
</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 GenericWidget from './GenericWidget';
function NewsWidget({ data, sx }) {
if (!data || data.error) {
return (
<GenericWidget title="News" sx={{ opacity: 0.5, ...sx }}>
<Typography color="text.secondary">
<GenericWidget className="news-widget" title="News" sx={{ opacity: 0.5, ...sx }}>
<p className="widget-error">
{data?.message || 'News data unavailable'}
</Typography>
</p>
</GenericWidget>
);
}
@ -18,7 +18,7 @@ function NewsWidget({ data, sx }) {
const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit';
return (
<GenericWidget title={`r/${subreddit}`} sx={{ ...sx }}>
<GenericWidget className="news-widget" title={`r/${subreddit}`} sx={{ ...sx }}>
<List dense>
{data.posts.slice(0, 5).map((post, index) => (
<ListItem key={index} disablePadding sx={{ mb: 1 }}>
@ -55,10 +55,10 @@ function NewsWidget({ data, sx }) {
'&:hover': { textDecoration: 'underline' }
}}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<div className="news-title">
{post.title}
<OpenInNew sx={{ fontSize: 14 }} />
</Typography>
</div>
</Link>
</Box>
}
@ -77,9 +77,7 @@ function NewsWidget({ data, sx }) {
size="small"
variant="outlined"
/>
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, alignSelf: 'center' }}>
by {post.author}
</Typography>
<span className="news-author">by {post.author}</span>
</Box>
}
/>
@ -130,34 +128,23 @@ function NewsWidget({ data, sx }) {
'&:hover': { textDecoration: 'underline' }
}}
>
<Typography variant="body2" sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<div className="news-title">
{item.title}
<OpenInNew sx={{ fontSize: 14 }} />
</Typography>
</div>
</Link>
</Box>
}
secondaryTypographyProps={{ component: 'div' }}
secondary={
<Box>
<Typography variant="caption" color="text.secondary">
<div className="news-date">
{item.pubDate && new Date(item.pubDate).toLocaleDateString()}
</Typography>
</div>
{item.description && (
<Typography
variant="caption"
color="text.secondary"
sx={{
display: '-webkit-box',
mt: 0.5,
overflow: 'hidden',
textOverflow: 'ellipsis',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
<div className="news-description">
{item.description.replace(/<[^>]*>?/gm, '')}
</Typography>
</div>
)}
</Box>
}
@ -173,12 +160,12 @@ function NewsWidget({ data, sx }) {
if (data.type === 'hackernews' && data.storyIds) {
return (
<GenericWidget title="Hacker News" sx={{ ...sx }}>
<Typography variant="body2" color="text.secondary">
<p className="widget-info">
{data.storyIds.length} top stories loaded
</Typography>
<Typography variant="caption" color="text.secondary">
</p>
<p className="widget-caption">
(Story details will load when API is configured)
</Typography>
</p>
</GenericWidget>
);
}
@ -186,12 +173,12 @@ function NewsWidget({ data, sx }) {
// Default fallback for unknown data types
return (
<GenericWidget title="News" sx={{ ...sx }}>
<Typography variant="body2" color="text.secondary">
<p className="widget-info">
Data format not recognized
</Typography>
<Typography variant="caption" color="text.secondary">
</p>
<p className="widget-caption">
{JSON.stringify(data).substring(0, 100)}...
</Typography>
</p>
</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 GenericWidget from './GenericWidget';
@ -15,9 +15,9 @@ function WeatherWidget({ data, sx }) {
if (!data || data.error) {
return (
<GenericWidget title="Weather" sx={{ opacity: 0.5, ...sx }}>
<Typography color="text.secondary">
<p className="widget-error">
{data?.message || 'Weather data unavailable'}
</Typography>
</p>
</GenericWidget>
);
}
@ -39,18 +39,12 @@ function WeatherWidget({ data, sx }) {
{getWeatherIcon(description)}
</Box>
<Box>
<Typography variant="h3" component="span">
{temp}
</Typography>
<Typography variant="h5" component="span" color="text.secondary">
{tempUnit}
</Typography>
<span className="weather-temp">{temp}</span>
<span className="weather-unit">{tempUnit}</span>
</Box>
</Box>
<Typography variant="body1" sx={{ mb: 2 }}>
{description}
</Typography>
<p className="weather-description">{description}</p>
<Stack direction="row" spacing={1}>
<Chip
@ -67,18 +61,14 @@ function WeatherWidget({ data, sx }) {
{data.forecast && data.forecast.length > 0 && (
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
Forecast
</Typography>
<div className="forecast-label">Forecast</div>
<Stack direction="row" spacing={1}>
{data.forecast.slice(0, 3).map((day, index) => (
<Box key={index} sx={{ textAlign: 'center' }}>
<Typography variant="caption" display="block">
Day {index + 1}
</Typography>
<Typography variant="body2">
<div className="forecast-day">Day {index + 1}</div>
<div className="forecast-temp">
{day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
</Typography>
</div>
</Box>
))}
</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';
const sharedTheme = {
typography: {
fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
},
shape: {
borderRadius: 8,
},