diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ed804a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+.env.local
+.DS_Store
+node_modules/
+dist/
+package-lock.json
+CLAUDE.md
\ No newline at end of file
diff --git a/claudash/.env.example b/claudash/.env.example
new file mode 100644
index 0000000..b8ecd60
--- /dev/null
+++ b/claudash/.env.example
@@ -0,0 +1,24 @@
+# REQUIRED: Anthropic API Key
+REACT_APP_ANTHROPIC_API_KEY=sk-ant-your-key-here
+
+# Data sources (comma-separated URLs)
+# Supports: .json (auto-parsed), .rss/.xml (RSS feeds), plain URLs (scraped)
+REACT_APP_DATA_SOURCES=https://www.reddit.com/r/technology.json,https://hacker-news.firebaseio.com/v0/topstories.json,https://www.reddit.com/r/worldnews.json
+
+# Location for weather (supports "city, state, country" or zip code)
+REACT_APP_LOCATION=harrisburg, pa, usa
+
+# Optional: Custom CORS proxy (default: allorigins.win)
+REACT_APP_CORS_PROXY=https://api.allorigins.win/raw?url=
+
+# Optional: Refresh interval in minutes (default: 15)
+REACT_APP_REFRESH_INTERVAL=15
+
+# Optional: Days of history to keep (default: 7)
+REACT_APP_HISTORY_DAYS=7
+
+# Optional: Claude model (default: claude-sonnet-4-5-20250929)
+REACT_APP_CLAUDE_MODEL=claude-3-5-sonnet-20241022
+
+# Optional: Debug mode (shows raw data, enables dummy data)
+REACT_APP_DEBUG_MODE=false
\ No newline at end of file
diff --git a/claudash/.gitignore b/claudash/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/claudash/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/claudash/README.md b/claudash/README.md
new file mode 100644
index 0000000..18bc70e
--- /dev/null
+++ b/claudash/README.md
@@ -0,0 +1,16 @@
+# React + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
diff --git a/claudash/eslint.config.js b/claudash/eslint.config.js
new file mode 100644
index 0000000..4fa125d
--- /dev/null
+++ b/claudash/eslint.config.js
@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{js,jsx}'],
+ extends: [
+ js.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ parserOptions: {
+ ecmaVersion: 'latest',
+ ecmaFeatures: { jsx: true },
+ sourceType: 'module',
+ },
+ },
+ rules: {
+ 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
+ },
+ },
+])
diff --git a/claudash/index.html b/claudash/index.html
new file mode 100644
index 0000000..0654a82
--- /dev/null
+++ b/claudash/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ claudash
+
+
+
+
+
+
diff --git a/claudash/package.json b/claudash/package.json
new file mode 100644
index 0000000..dfda0cc
--- /dev/null
+++ b/claudash/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "claudash",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "proxy": "node proxy-server.js",
+ "start": "npm run proxy & npm run dev",
+ "build": "vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.14.0",
+ "@emotion/styled": "^11.14.1",
+ "@mui/icons-material": "^7.3.7",
+ "@mui/material": "^7.3.7",
+ "@mui/x-charts": "^8.27.0",
+ "cors": "^2.8.6",
+ "express": "^5.2.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/react": "^19.2.7",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "vite": "^4.5.3"
+ }
+}
diff --git a/claudash/proxy-server.js b/claudash/proxy-server.js
new file mode 100644
index 0000000..72abfe4
--- /dev/null
+++ b/claudash/proxy-server.js
@@ -0,0 +1,59 @@
+import express from 'express';
+import cors from 'cors';
+
+const app = express();
+const PORT = 3001;
+
+app.use(cors());
+app.use(express.json());
+
+// Proxy endpoint for Claude API
+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: {
+ 'Content-Type': 'application/json',
+ 'x-api-key': apiKey,
+ 'anthropic-version': '2023-06-01'
+ },
+ 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);
+ res.json(data);
+ } catch (error) {
+ console.error('Proxy error:', error);
+ res.status(500).json({ error: error.message });
+ }
+});
+
+app.listen(PORT, () => {
+ console.log(`Proxy server running on http://localhost:${PORT}`);
+ console.log('This proxy allows the frontend to communicate with the Anthropic API');
+});
\ No newline at end of file
diff --git a/claudash/public/vite.svg b/claudash/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/claudash/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/claudash/src/App.css b/claudash/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/claudash/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.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);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/claudash/src/App.jsx b/claudash/src/App.jsx
new file mode 100644
index 0000000..c418a28
--- /dev/null
+++ b/claudash/src/App.jsx
@@ -0,0 +1,23 @@
+import { ThemeProvider, CssBaseline } from '@mui/material';
+import { useState } from 'react';
+import Dashboard from './components/Dashboard';
+import DebugPanel from './components/DebugPanel';
+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)}
+ isDark={isDark}
+ />
+ {debugMode && }
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/claudash/src/assets/react.svg b/claudash/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/claudash/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/claudash/src/components/ChatInterface.jsx b/claudash/src/components/ChatInterface.jsx
new file mode 100644
index 0000000..79e4b2c
--- /dev/null
+++ b/claudash/src/components/ChatInterface.jsx
@@ -0,0 +1,144 @@
+import { useState, useRef, useEffect } from 'react';
+import { Box, TextField, IconButton, Paper, Typography } from '@mui/material';
+import { Send, ExpandLess, ExpandMore } from '@mui/icons-material';
+import { sendChatMessage } from '../services/claudeService';
+
+function ChatInterface() {
+ const [messages, setMessages] = useState([]);
+ const [input, setInput] = useState('');
+ 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.'
+ }]);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ return (
+
+ {/* Expand/Collapse Button */}
+
+ setIsExpanded(!isExpanded)}
+ size="small"
+ >
+ {isExpanded ? : }
+
+
+ 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}
+
+
+ ))}
+
+
+ )}
+
+ {/* Input area - always visible */}
+
+ setInput(e.target.value)}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ }}
+ placeholder="Ask Claude anything..."
+ disabled={loading}
+ size="small"
+ />
+
+
+
+
+
+ );
+}
+
+export default ChatInterface;
\ No newline at end of file
diff --git a/claudash/src/components/Dashboard.jsx b/claudash/src/components/Dashboard.jsx
new file mode 100644
index 0000000..b5b3a40
--- /dev/null
+++ b/claudash/src/components/Dashboard.jsx
@@ -0,0 +1,173 @@
+import { useState, useEffect } from 'react';
+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';
+import { saveBriefing, getPreviousBriefing, hasTodaysBriefing, getTodaysBriefing } from '../services/storageService';
+import BriefingWidget from './widgets/BriefingWidget';
+import WeatherWidget from './widgets/WeatherWidget';
+import NewsWidget from './widgets/NewsWidget';
+import ChatInterface from './ChatInterface';
+
+function Dashboard({ themeToggle, isDark }) {
+ const [loading, setLoading] = useState(true);
+ const [briefing, setBriefing] = useState('');
+ const [sourceData, setSourceData] = useState({});
+ const [error, setError] = useState(null);
+ const [refreshing, setRefreshing] = useState(false);
+
+ 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 || {});
+ setLoading(false);
+ return;
+ }
+
+ console.log('Fetching fresh data...');
+
+ // Fetch all configured sources
+ const data = await fetchAllSources();
+ console.log('Fetched sources:', Object.keys(data.sources || {}));
+ setSourceData(data.sources || {});
+
+ // 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);
+
+ console.log('Calling generateBriefing...');
+ // Generate today's briefing with Claude
+ const newBriefing = await generateBriefing(data, previousBriefing);
+ console.log('Briefing generated, length:', newBriefing?.length);
+ setBriefing(newBriefing);
+
+ // Save to localStorage
+ const today = new Date().toISOString().split('T')[0];
+ console.log('Saving briefing for:', today);
+ saveBriefing(today, newBriefing, data.sources);
+
+ } 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);
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Top toolbar */}
+
+ {
+ localStorage.clear();
+ window.location.reload();
+ }}
+ title="Clear all cache and reload"
+ color="error"
+ >
+ 🗑️
+
+
+
+
+
+ {isDark ? : }
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* 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 */}
+
+
+ );
+}
+
+export default Dashboard;
\ No newline at end of file
diff --git a/claudash/src/components/DebugPanel.jsx b/claudash/src/components/DebugPanel.jsx
new file mode 100644
index 0000000..5549954
--- /dev/null
+++ b/claudash/src/components/DebugPanel.jsx
@@ -0,0 +1,81 @@
+import { useState } from 'react';
+import { Button, Box, Paper, Typography } from '@mui/material';
+import { Download, Upload } from '@mui/icons-material';
+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' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `claudash-debug-${new Date().toISOString()}.json`;
+ 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 {
+ const data = JSON.parse(e.target.result);
+ importDummyData(data);
+ setDummyMode(true);
+ window.location.reload();
+ } catch (error) {
+ console.error('Error importing data:', error);
+ alert('Failed to import data. Please check the file format.');
+ }
+ };
+ reader.readAsText(file);
+ };
+
+ return (
+
+ Debug Tools
+
+ }
+ onClick={handleExport}
+ sx={{ mb: 1 }}
+ variant="outlined"
+ >
+ Export Data
+
+
+ }
+ variant="outlined"
+ >
+ Import Dummy Data
+
+
+
+ {dummyMode && (
+
+ Using dummy data
+
+ )}
+
+ );
+}
+
+export default DebugPanel;
\ No newline at end of file
diff --git a/claudash/src/components/widgets/BriefingWidget.jsx b/claudash/src/components/widgets/BriefingWidget.jsx
new file mode 100644
index 0000000..95b83b6
--- /dev/null
+++ b/claudash/src/components/widgets/BriefingWidget.jsx
@@ -0,0 +1,36 @@
+import { Typography, Skeleton, Box } from '@mui/material';
+import GenericWidget from './GenericWidget';
+
+function BriefingWidget({ briefing, sx }) {
+ return (
+
+
+ {briefing ? (
+
+ {briefing}
+
+ ) : (
+ <>
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
+
+export default BriefingWidget;
\ No newline at end of file
diff --git a/claudash/src/components/widgets/GenericWidget.jsx b/claudash/src/components/widgets/GenericWidget.jsx
new file mode 100644
index 0000000..85a408c
--- /dev/null
+++ b/claudash/src/components/widgets/GenericWidget.jsx
@@ -0,0 +1,22 @@
+import { Paper, Typography, Box } from '@mui/material';
+
+function GenericWidget({ title, data, children, sx, ...props }) {
+ return (
+
+ {title}
+
+ {children || {JSON.stringify(data)}}
+
+
+ );
+}
+
+export default GenericWidget;
\ No newline at end of file
diff --git a/claudash/src/components/widgets/NewsWidget.jsx b/claudash/src/components/widgets/NewsWidget.jsx
new file mode 100644
index 0000000..969abb5
--- /dev/null
+++ b/claudash/src/components/widgets/NewsWidget.jsx
@@ -0,0 +1,157 @@
+import { Typography, 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 (
+
+
+ {data?.message || 'News data unavailable'}
+
+
+ );
+ }
+
+ // Handle Reddit data
+ if (data.type === 'reddit' && data.posts) {
+ const subreddit = data.url?.match(/\/r\/([^\/\.]+)/)?.[1] || 'Reddit';
+
+ return (
+
+
+ {data.posts.slice(0, 5).map((post, index) => (
+
+
+
+
+ {post.title}
+
+
+
+
+ }
+ secondaryTypographyProps={{ component: 'div' }}
+ secondary={
+
+ }
+ label={post.score}
+ size="small"
+ variant="outlined"
+ />
+ }
+ label={post.comments}
+ size="small"
+ variant="outlined"
+ />
+
+ by {post.author}
+
+
+ }
+ />
+
+ ))}
+
+
+ );
+ }
+
+ // Handle RSS feed data
+ if (data.type === 'rss' && data.items) {
+ return (
+
+
+ {data.items.slice(0, 5).map((item, index) => (
+
+
+
+ {item.title}
+
+
+
+ }
+ secondaryTypographyProps={{ component: 'div' }}
+ secondary={
+
+
+ {item.pubDate && new Date(item.pubDate).toLocaleDateString()}
+
+ {item.description && (
+
+ {item.description.replace(/<[^>]*>?/gm, '')}
+
+ )}
+
+ }
+ />
+
+ ))}
+
+
+ );
+ }
+
+ // Handle Hacker News data
+ if (data.type === 'hackernews' && data.storyIds) {
+ return (
+
+
+ {data.storyIds.length} top stories loaded
+
+
+ (Story details will load when API is configured)
+
+
+ );
+ }
+
+ // Default fallback for unknown data types
+ return (
+
+
+ Data format not recognized
+
+
+ {JSON.stringify(data).substring(0, 100)}...
+
+
+ );
+}
+
+export default NewsWidget;
\ No newline at end of file
diff --git a/claudash/src/components/widgets/WeatherWidget.jsx b/claudash/src/components/widgets/WeatherWidget.jsx
new file mode 100644
index 0000000..8a05594
--- /dev/null
+++ b/claudash/src/components/widgets/WeatherWidget.jsx
@@ -0,0 +1,92 @@
+import { Typography, Box, Chip, Stack } from '@mui/material';
+import { Cloud, WbSunny, Thunderstorm, AcUnit } from '@mui/icons-material';
+import GenericWidget from './GenericWidget';
+
+function WeatherWidget({ data, sx }) {
+ const getWeatherIcon = (description) => {
+ const desc = description?.toLowerCase() || '';
+ if (desc.includes('sun') || desc.includes('clear')) return ;
+ if (desc.includes('cloud') || desc.includes('overcast')) return ;
+ if (desc.includes('rain') || desc.includes('storm')) return ;
+ if (desc.includes('snow') || desc.includes('ice')) return ;
+ return ;
+ };
+
+ if (!data || data.error) {
+ return (
+
+
+ {data?.message || 'Weather data unavailable'}
+
+
+ );
+ }
+
+ const current = data.current || {};
+ const location = data.nearestArea?.areaName?.[0]?.value || data.location || 'Unknown';
+ const temp = current.temp_F || current.temp_C || '?';
+ const tempUnit = current.temp_F ? '°F' : '°C';
+ const description = current.weatherDesc?.[0]?.value || 'No description';
+ const humidity = current.humidity || '?';
+ const windSpeed = current.windspeedMiles || current.windspeedKmph || '?';
+ const windUnit = current.windspeedMiles ? 'mph' : 'km/h';
+
+ return (
+
+
+
+
+ {getWeatherIcon(description)}
+
+
+
+ {temp}
+
+
+ {tempUnit}
+
+
+
+
+
+ {description}
+
+
+
+
+
+
+
+ {data.forecast && data.forecast.length > 0 && (
+
+
+ Forecast
+
+
+ {data.forecast.slice(0, 3).map((day, index) => (
+
+
+ Day {index + 1}
+
+
+ {day.maxtempF || day.maxtempC}° / {day.mintempF || day.mintempC}°
+
+
+ ))}
+
+
+ )}
+
+
+ );
+}
+
+export default WeatherWidget;
\ No newline at end of file
diff --git a/claudash/src/index.css b/claudash/src/index.css
new file mode 100644
index 0000000..08a3ac9
--- /dev/null
+++ b/claudash/src/index.css
@@ -0,0 +1,68 @@
+: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;
+ }
+}
diff --git a/claudash/src/main.jsx b/claudash/src/main.jsx
new file mode 100644
index 0000000..6161d18
--- /dev/null
+++ b/claudash/src/main.jsx
@@ -0,0 +1,9 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
\ No newline at end of file
diff --git a/claudash/src/services/claudeService.js b/claudash/src/services/claudeService.js
new file mode 100644
index 0000000..4c3d7c6
--- /dev/null
+++ b/claudash/src/services/claudeService.js
@@ -0,0 +1,237 @@
+import {
+ ANTHROPIC_API_URL,
+ DEFAULT_CLAUDE_MODEL,
+ MAX_TOKENS
+} from '../utils/constants';
+
+/**
+ * Make a request to Claude API
+ * @param {string} prompt - The prompt to send
+ * @param {Array} messages - Optional message history
+ * @returns {Promise} Claude's response
+ */
+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,
+ model: model,
+ maxTokens: MAX_TOKENS,
+ messages: [
+ ...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',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ 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;
+ } catch (error) {
+ console.error('Error calling Claude:', error);
+ throw error;
+ }
+}
+
+/**
+ * Build briefing prompt with memory
+ * @param {Object} currentData - Current source data
+ * @param {Object} previousBriefing - Previous briefing object
+ * @returns {string} The prompt
+ */
+function buildBriefingPrompt(currentData, previousBriefing) {
+ let prompt = `You are creating a personalized daily briefing. Be conversational and highlight what's interesting.
+
+CURRENT DATA:
+`;
+
+ // Add all current source data
+ Object.entries(currentData.sources || currentData).forEach(([source, data]) => {
+ if (data && !data.error) {
+ prompt += `\n${source.toUpperCase()}:\n${JSON.stringify(data, null, 2)}\n`;
+ }
+ });
+
+ // Add historical context if available
+ if (previousBriefing && previousBriefing.briefing) {
+ prompt += `\n\nYESTERDAY'S BRIEFING:\n${previousBriefing.briefing}\n`;
+ prompt += `\nHighlight what's changed, any follow-up stories, or interesting trends.`;
+ }
+
+ prompt += `\n\nCreate a friendly briefing (300-400 words) that:
+- Starts with a warm greeting appropriate for the time of day
+- Highlights the most interesting items from the data
+- Notes anything that's changed since yesterday (if applicable)
+- Has personality and isn't just a dry list
+- Includes relevant weather information if available
+- Ends with something engaging or thought-provoking
+
+Focus on the most newsworthy and interesting items. Don't try to mention everything.`;
+
+ return prompt;
+}
+
+/**
+ * Generate daily briefing
+ * @param {Object} currentData - Current data from all sources
+ * @param {Object} previousBriefing - Previous briefing for comparison
+ * @returns {Promise} Generated briefing text
+ */
+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;
+ } catch (error) {
+ console.error('Error generating briefing:', error);
+
+ // Return a fallback briefing
+ return `Good morning! I encountered an issue generating your personalized briefing today.
+
+ Error: ${error.message}
+
+ Please check your API key configuration and try again.`;
+ }
+}
+
+/**
+ * Send a chat message to Claude
+ * @param {string} message - User's message
+ * @param {Array} conversationHistory - Previous messages
+ * @returns {Promise} Claude's response
+ */
+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,
+ 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;
+ } catch (error) {
+ console.error('Error in chat:', error);
+ return `I apologize, but I encountered an error: ${error.message}. Please try again.`;
+ }
+}
+
+/**
+ * Extract meaningful content from HTML using Claude
+ * @param {string} html - The HTML content
+ * @param {string} url - The source URL
+ * @returns {Promise