added token usage and cost to UI, cleanup
This commit is contained in:
parent
a85e96aad8
commit
bffb11e5b0
15 changed files with 285 additions and 254 deletions
|
|
@ -11,21 +11,21 @@ app.use(express.json());
|
||||||
app.post('/api/claude', async (req, res) => {
|
app.post('/api/claude', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { messages, apiKey, model, maxTokens } = req.body;
|
const { messages, apiKey, model, maxTokens } = req.body;
|
||||||
|
|
||||||
console.log('=== Proxy received request ===');
|
console.log('=== Proxy received request ===');
|
||||||
console.log('Model:', model);
|
console.log('Model:', model);
|
||||||
console.log('Max tokens:', maxTokens);
|
console.log('Max tokens:', maxTokens);
|
||||||
console.log('Messages count:', messages?.length);
|
console.log('Messages count:', messages?.length);
|
||||||
console.log('API Key present:', !!apiKey);
|
console.log('API Key present:', !!apiKey);
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
model: model,
|
model: model,
|
||||||
max_tokens: maxTokens,
|
max_tokens: maxTokens,
|
||||||
messages: messages
|
messages: messages
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Request body:', JSON.stringify(requestBody, null, 2));
|
console.log('Request body:', JSON.stringify(requestBody, null, 2));
|
||||||
|
|
||||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -35,17 +35,18 @@ app.post('/api/claude', async (req, res) => {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody)
|
body: JSON.stringify(requestBody)
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Claude API response status:', response.status);
|
console.log('Claude API response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
console.error('Claude API error:', error);
|
console.error('Claude API error:', error);
|
||||||
return res.status(response.status).json({ error });
|
return res.status(response.status).json({ error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Claude API success, response content length:', data.content?.[0]?.text?.length);
|
console.log('Claude API success, response content length:', data.content?.[0]?.text?.length);
|
||||||
|
console.log('Token usage:', data.usage);
|
||||||
res.json(data);
|
res.json(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Proxy error:', error);
|
console.error('Proxy error:', error);
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ import { lightTheme, darkTheme } from './theme';
|
||||||
function App() {
|
function App() {
|
||||||
const [isDark, setIsDark] = useState(true);
|
const [isDark, setIsDark] = useState(true);
|
||||||
const debugMode = import.meta.env.REACT_APP_DEBUG_MODE === 'true';
|
const debugMode = import.meta.env.REACT_APP_DEBUG_MODE === 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
|
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Dashboard
|
<Dashboard
|
||||||
themeToggle={() => setIsDark(!isDark)}
|
themeToggle={() => setIsDark(!isDark)}
|
||||||
isDark={isDark}
|
isDark={isDark}
|
||||||
/>
|
/>
|
||||||
{debugMode && <DebugPanel />}
|
{debugMode && <DebugPanel />}
|
||||||
|
|
|
||||||
|
|
@ -9,42 +9,42 @@ function ChatInterface() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!input.trim() || loading) return;
|
if (!input.trim() || loading) return;
|
||||||
|
|
||||||
const userMessage = { role: 'user', content: input };
|
const userMessage = { role: 'user', content: input };
|
||||||
setMessages(prev => [...prev, userMessage]);
|
setMessages(prev => [...prev, userMessage]);
|
||||||
setInput('');
|
setInput('');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setIsExpanded(true); // Expand chat when sending a message
|
setIsExpanded(true); // Expand chat when sending a message
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await sendChatMessage(input, messages);
|
const response = await sendChatMessage(input, messages);
|
||||||
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
setMessages(prev => [...prev, { role: 'assistant', content: response }]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Chat error:', error);
|
console.error('Chat error:', error);
|
||||||
setMessages(prev => [...prev, {
|
setMessages(prev => [...prev, {
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: 'Sorry, I encountered an error. Please try again.'
|
content: 'Sorry, I encountered an error. Please try again.'
|
||||||
}]);
|
}]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={8}
|
elevation={8}
|
||||||
sx={{
|
sx={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: isExpanded ? '40vh' : '64px',
|
height: isExpanded ? '40vh' : '64px',
|
||||||
transition: 'height 0.3s ease',
|
transition: 'height 0.3s ease',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -53,14 +53,14 @@ function ChatInterface() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Expand/Collapse Button */}
|
{/* Expand/Collapse Button */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
p: 1,
|
p: 1,
|
||||||
borderBottom: 1,
|
borderBottom: 1,
|
||||||
borderColor: 'divider'
|
borderColor: 'divider'
|
||||||
}}>
|
}}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => setIsExpanded(!isExpanded)}
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>
|
||||||
|
|
@ -70,7 +70,7 @@ function ChatInterface() {
|
||||||
Chat with Claude
|
Chat with Claude
|
||||||
</Typography>
|
</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 }}>
|
||||||
|
|
@ -80,16 +80,16 @@ function ChatInterface() {
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{messages.map((msg, idx) => (
|
{messages.map((msg, idx) => (
|
||||||
<Box
|
<Box
|
||||||
key={idx}
|
key={idx}
|
||||||
sx={{
|
sx={{
|
||||||
mb: 2,
|
mb: 2,
|
||||||
textAlign: msg.role === 'user' ? 'right' : 'left'
|
textAlign: msg.role === 'user' ? 'right' : 'left'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
sx={{
|
sx={{
|
||||||
display: 'inline-block',
|
display: 'inline-block',
|
||||||
bgcolor: msg.role === 'user' ? 'primary.main' : 'grey.800',
|
bgcolor: msg.role === 'user' ? 'primary.main' : 'grey.800',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
|
|
@ -105,13 +105,13 @@ function ChatInterface() {
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Input area - always visible */}
|
{/* Input area - always visible */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
p: 2,
|
p: 2,
|
||||||
borderTop: isExpanded ? 1 : 0,
|
borderTop: isExpanded ? 1 : 0,
|
||||||
borderColor: 'divider',
|
borderColor: 'divider',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
backgroundColor: 'background.paper'
|
backgroundColor: 'background.paper'
|
||||||
}}>
|
}}>
|
||||||
|
|
@ -129,8 +129,8 @@ function ChatInterface() {
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={loading || !input.trim()}
|
disabled={loading || !input.trim()}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,18 @@ function Dashboard({ themeToggle, isDark }) {
|
||||||
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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDashboard();
|
loadDashboard();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function loadDashboard(forceRefresh = false) {
|
async function loadDashboard(forceRefresh = false) {
|
||||||
try {
|
try {
|
||||||
console.log('=== loadDashboard called ===');
|
console.log('=== loadDashboard called ===');
|
||||||
console.log('Force refresh:', forceRefresh);
|
console.log('Force refresh:', forceRefresh);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
// Check if we already have today's briefing (unless forcing refresh)
|
// Check if we already have today's briefing (unless forcing refresh)
|
||||||
if (!forceRefresh && hasTodaysBriefing()) {
|
if (!forceRefresh && hasTodaysBriefing()) {
|
||||||
console.log('Using cached briefing from today');
|
console.log('Using cached briefing from today');
|
||||||
|
|
@ -36,53 +37,63 @@ function Dashboard({ themeToggle, isDark }) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Fetching fresh data...');
|
console.log('Fetching fresh data...');
|
||||||
setLoadingStatus('Fetching weather and news sources...');
|
setLoadingStatus('Fetching weather and news sources...');
|
||||||
|
|
||||||
// Fetch all configured sources
|
// Fetch all configured sources
|
||||||
const data = await fetchAllSources();
|
const data = await fetchAllSources();
|
||||||
console.log('Fetched sources:', Object.keys(data.sources || {}));
|
console.log('Fetched sources:', Object.keys(data.sources || {}));
|
||||||
console.log('Failed sources:', data.failed);
|
console.log('Failed sources:', data.failed);
|
||||||
setSourceData(data.sources || {});
|
setSourceData(data.sources || {});
|
||||||
|
|
||||||
// If all sources failed, show error but continue
|
// If all sources failed, show error but continue
|
||||||
if (data.failed > 0 && data.successful === 0) {
|
if (data.failed > 0 && data.successful === 0) {
|
||||||
setError('All data sources failed to load. Please check your internet connection and try again.');
|
setError('All data sources failed to load. Please check your internet connection and try again.');
|
||||||
} else if (data.failed > 0) {
|
} else if (data.failed > 0) {
|
||||||
setError(`${data.failed} data source(s) failed to load. Showing available data.`);
|
setError(`${data.failed} data source(s) failed to load. Showing available data.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if API key is configured
|
// Check if API key is configured
|
||||||
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
||||||
console.log('API key configured:', !!apiKey);
|
console.log('API key configured:', !!apiKey);
|
||||||
|
|
||||||
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
||||||
console.warn('API key not configured properly');
|
console.warn('API key not configured properly');
|
||||||
setBriefing('Please add your Anthropic API key to .env.local to generate briefings.');
|
setBriefing('Please add your Anthropic API key to .env.local to generate briefings.');
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get yesterday's briefing for comparison
|
// Get yesterday's briefing for comparison
|
||||||
const previousBriefing = getPreviousBriefing(1);
|
const previousBriefing = getPreviousBriefing(1);
|
||||||
console.log('Previous briefing exists:', !!previousBriefing);
|
console.log('Previous briefing exists:', !!previousBriefing);
|
||||||
|
|
||||||
// Only generate briefing if we have some data
|
// Only generate briefing if we have some data
|
||||||
if (Object.keys(data.sources || {}).length > 0) {
|
if (Object.keys(data.sources || {}).length > 0) {
|
||||||
console.log('Calling generateBriefing...');
|
console.log('Calling generateBriefing...');
|
||||||
setLoadingStatus('Generating your personalized briefing with AI...');
|
setLoadingStatus('Generating your personalized briefing with AI...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate today's briefing with Claude
|
// Generate today's briefing with Claude
|
||||||
const newBriefing = await generateBriefing(data, previousBriefing);
|
const result = await generateBriefing(data, previousBriefing);
|
||||||
console.log('Briefing generated, length:', newBriefing?.length);
|
console.log('Briefing generated');
|
||||||
setBriefing(newBriefing);
|
|
||||||
|
// Handle result object with text and usage
|
||||||
|
if (result && typeof result === 'object' && result.text) {
|
||||||
|
setBriefing(result.text);
|
||||||
|
setTokenUsage(result.usage);
|
||||||
|
console.log('Token usage:', result.usage);
|
||||||
|
} else if (typeof result === 'string') {
|
||||||
|
// Fallback for string responses
|
||||||
|
setBriefing(result);
|
||||||
|
}
|
||||||
|
|
||||||
// Save to localStorage
|
// Save to localStorage
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
console.log('Saving briefing for:', today);
|
console.log('Saving briefing for:', today);
|
||||||
saveBriefing(today, newBriefing, data.sources);
|
const briefingText = result?.text || result;
|
||||||
|
saveBriefing(today, briefingText, data.sources);
|
||||||
} catch (briefingError) {
|
} catch (briefingError) {
|
||||||
console.error('Failed to generate briefing:', 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.`);
|
setBriefing(`Unable to generate AI briefing: ${briefingError.message}\n\nYou can still view the data widgets below.`);
|
||||||
|
|
@ -90,7 +101,7 @@ function Dashboard({ themeToggle, isDark }) {
|
||||||
} else {
|
} else {
|
||||||
setBriefing('No data sources loaded successfully. Please check your configuration and try again.');
|
setBriefing('No data sources loaded successfully. Please check your configuration and try again.');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Dashboard load error:', error);
|
console.error('Dashboard load error:', error);
|
||||||
console.error('Error stack:', error.stack);
|
console.error('Error stack:', error.stack);
|
||||||
|
|
@ -100,12 +111,12 @@ function Dashboard({ themeToggle, isDark }) {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
loadDashboard(true);
|
loadDashboard(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
|
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center" minHeight="100vh" gap={2}>
|
||||||
|
|
@ -116,20 +127,20 @@ function Dashboard({ themeToggle, isDark }) {
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ minHeight: '100vh', pb: 10 }}>
|
<Box sx={{ minHeight: '100vh', pb: 10 }}>
|
||||||
{/* Top toolbar */}
|
{/* Top toolbar */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
p: 2,
|
p: 2,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
gap: 1,
|
gap: 1,
|
||||||
zIndex: 100
|
zIndex: 100
|
||||||
}}>
|
}}>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|
@ -139,56 +150,70 @@ function Dashboard({ themeToggle, isDark }) {
|
||||||
>
|
>
|
||||||
🗑️
|
🗑️
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={refreshing}
|
disabled={refreshing}
|
||||||
title="Refresh data"
|
title="Refresh data"
|
||||||
>
|
>
|
||||||
<Refresh />
|
<Refresh />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={themeToggle}
|
onClick={themeToggle}
|
||||||
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
>
|
>
|
||||||
{isDark ? <Brightness7 /> : <Brightness4 />}
|
{isDark ? <Brightness7 /> : <Brightness4 />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Container maxWidth="xl" sx={{ pt: 4 }}>
|
<Container maxWidth="xl" sx={{ pt: 4 }}>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="error" sx={{ mb: 2 }}>
|
<Alert severity="error" sx={{ mb: 2 }}>
|
||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Widget Grid - easily rearrangeable */}
|
{/* Widget Grid - easily rearrangeable */}
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
mb: 4
|
mb: 4
|
||||||
}}>
|
}}>
|
||||||
<BriefingWidget briefing={briefing} sx={{ flex: '1 1 100%' }} />
|
<BriefingWidget briefing={briefing} sx={{ flex: '1 1 100%' }} />
|
||||||
|
|
||||||
<WeatherWidget
|
<WeatherWidget
|
||||||
data={sourceData.weather}
|
data={sourceData.weather}
|
||||||
sx={{ flex: '1 1 300px' }}
|
sx={{ flex: '1 1 300px' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Render NewsWidget for each news source */}
|
{/* Render NewsWidget for each news source */}
|
||||||
{Object.entries(sourceData).map(([key, data]) => {
|
{Object.entries(sourceData).map(([key, data]) => {
|
||||||
if (key === 'weather') return null;
|
if (key === 'weather') return null;
|
||||||
return (
|
return (
|
||||||
<NewsWidget
|
<NewsWidget
|
||||||
key={key}
|
key={key}
|
||||||
data={data}
|
data={data}
|
||||||
sx={{ flex: '1 1 400px' }}
|
sx={{ flex: '1 1 400px' }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
{/* Fixed chat at bottom */}
|
{/* Fixed chat at bottom */}
|
||||||
<ChatInterface />
|
<ChatInterface />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { exportAllData, importDummyData } from '../services/storageService';
|
||||||
|
|
||||||
function DebugPanel() {
|
function DebugPanel() {
|
||||||
const [dummyMode, setDummyMode] = useState(false);
|
const [dummyMode, setDummyMode] = useState(false);
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
const data = exportAllData();
|
const data = exportAllData();
|
||||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
|
@ -16,11 +16,11 @@ function DebugPanel() {
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImport = (event) => {
|
const handleImport = (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -35,40 +35,40 @@ function DebugPanel() {
|
||||||
};
|
};
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
<Typography variant="h6" gutterBottom>Debug Tools</Typography>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
startIcon={<Download />}
|
startIcon={<Download />}
|
||||||
onClick={handleExport}
|
onClick={handleExport}
|
||||||
sx={{ mb: 1 }}
|
sx={{ mb: 1 }}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
>
|
>
|
||||||
Export Data
|
Export Data
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
component="label"
|
component="label"
|
||||||
startIcon={<Upload />}
|
startIcon={<Upload />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
>
|
>
|
||||||
Import Dummy Data
|
Import Dummy Data
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
hidden
|
hidden
|
||||||
accept=".json"
|
accept=".json"
|
||||||
onChange={handleImport}
|
onChange={handleImport}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{dummyMode && (
|
{dummyMode && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="warning.main"
|
color="warning.main"
|
||||||
sx={{ mt: 1, display: 'block' }}
|
sx={{ mt: 1, display: 'block' }}
|
||||||
>
|
>
|
||||||
Using dummy data
|
Using dummy data
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ import GenericWidget from './GenericWidget';
|
||||||
|
|
||||||
function BriefingWidget({ briefing, sx }) {
|
function BriefingWidget({ briefing, sx }) {
|
||||||
return (
|
return (
|
||||||
<GenericWidget
|
<GenericWidget
|
||||||
title="Your Daily Briefing"
|
title="Your Daily Briefing"
|
||||||
sx={{ ...sx }}
|
sx={{ ...sx }}
|
||||||
>
|
>
|
||||||
<Box sx={{ mt: 1 }}>
|
<Box sx={{ mt: 1 }}>
|
||||||
{briefing ? (
|
{briefing ? (
|
||||||
<Typography
|
<Typography
|
||||||
variant="body1"
|
variant="body1"
|
||||||
sx={{
|
sx={{
|
||||||
whiteSpace: 'pre-line',
|
whiteSpace: 'pre-line',
|
||||||
lineHeight: 1.7,
|
lineHeight: 1.7,
|
||||||
fontSize: '1.05rem'
|
fontSize: '1.05rem'
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { Paper, Typography, Box } from '@mui/material';
|
||||||
|
|
||||||
function GenericWidget({ title, data, children, sx, ...props }) {
|
function GenericWidget({ title, data, children, sx, ...props }) {
|
||||||
return (
|
return (
|
||||||
<Paper
|
<Paper
|
||||||
elevation={2}
|
elevation={2}
|
||||||
sx={{
|
sx={{
|
||||||
p: 2,
|
p: 2,
|
||||||
minWidth: 300,
|
minWidth: 300,
|
||||||
...sx
|
...sx
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<Typography variant="h6" gutterBottom>{title}</Typography>
|
<Typography variant="h6" gutterBottom>{title}</Typography>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ function NewsWidget({ data, sx }) {
|
||||||
// Handle Reddit data
|
// Handle Reddit data
|
||||||
if (data.type === 'reddit' && data.posts) {
|
if (data.type === 'reddit' && data.posts) {
|
||||||
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 title={`r/${subreddit}`} sx={{ ...sx }}>
|
||||||
<List dense>
|
<List dense>
|
||||||
|
|
@ -49,7 +49,7 @@ function NewsWidget({ data, sx }) {
|
||||||
href={post.url}
|
href={post.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
sx={{
|
sx={{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'text.primary',
|
color: 'text.primary',
|
||||||
'&:hover': { textDecoration: 'underline' }
|
'&:hover': { textDecoration: 'underline' }
|
||||||
|
|
@ -65,17 +65,17 @@ function NewsWidget({ data, sx }) {
|
||||||
secondaryTypographyProps={{ component: 'div' }}
|
secondaryTypographyProps={{ component: 'div' }}
|
||||||
secondary={
|
secondary={
|
||||||
<Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
|
<Box sx={{ display: 'flex', gap: 1, mt: 0.5 }}>
|
||||||
<Chip
|
<Chip
|
||||||
icon={<TrendingUp />}
|
icon={<TrendingUp />}
|
||||||
label={post.score}
|
label={post.score}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
<Chip
|
<Chip
|
||||||
icon={<Comment />}
|
icon={<Comment />}
|
||||||
label={post.comments}
|
label={post.comments}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, alignSelf: 'center' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ ml: 1, alignSelf: 'center' }}>
|
||||||
by {post.author}
|
by {post.author}
|
||||||
|
|
@ -124,7 +124,7 @@ function NewsWidget({ data, sx }) {
|
||||||
href={item.link}
|
href={item.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
sx={{
|
sx={{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
color: 'text.primary',
|
color: 'text.primary',
|
||||||
'&:hover': { textDecoration: 'underline' }
|
'&:hover': { textDecoration: 'underline' }
|
||||||
|
|
@ -144,10 +144,10 @@ function NewsWidget({ data, sx }) {
|
||||||
{item.pubDate && new Date(item.pubDate).toLocaleDateString()}
|
{item.pubDate && new Date(item.pubDate).toLocaleDateString()}
|
||||||
</Typography>
|
</Typography>
|
||||||
{item.description && (
|
{item.description && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
sx={{
|
sx={{
|
||||||
display: '-webkit-box',
|
display: '-webkit-box',
|
||||||
mt: 0.5,
|
mt: 0.5,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
|
|
||||||
|
|
@ -47,21 +47,21 @@ function WeatherWidget({ data, sx }) {
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography variant="body1" sx={{ mb: 2 }}>
|
<Typography variant="body1" sx={{ mb: 2 }}>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1}>
|
<Stack direction="row" spacing={1}>
|
||||||
<Chip
|
<Chip
|
||||||
label={`Humidity: ${humidity}%`}
|
label={`Humidity: ${humidity}%`}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
<Chip
|
<Chip
|
||||||
label={`Wind: ${windSpeed} ${windUnit}`}
|
label={`Wind: ${windSpeed} ${windUnit}`}
|
||||||
size="small"
|
size="small"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import {
|
import {
|
||||||
ANTHROPIC_API_URL,
|
ANTHROPIC_API_URL,
|
||||||
DEFAULT_CLAUDE_MODEL,
|
DEFAULT_CLAUDE_MODEL,
|
||||||
MAX_TOKENS
|
MAX_TOKENS
|
||||||
} from '../utils/constants';
|
} from '../utils/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -12,22 +12,22 @@ import {
|
||||||
*/
|
*/
|
||||||
async function callClaude(prompt, messages = []) {
|
async function callClaude(prompt, messages = []) {
|
||||||
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
||||||
|
|
||||||
console.log('=== callClaude function ===');
|
console.log('=== callClaude function ===');
|
||||||
console.log('API Key configured:', !!apiKey);
|
console.log('API Key configured:', !!apiKey);
|
||||||
console.log('API Key starts with:', apiKey?.substring(0, 15) + '...');
|
console.log('API Key starts with:', apiKey?.substring(0, 15) + '...');
|
||||||
|
|
||||||
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
||||||
console.error('Anthropic API key not configured');
|
console.error('Anthropic API key not configured');
|
||||||
return 'Please configure your Anthropic API key in .env.local';
|
return 'Please configure your Anthropic API key in .env.local';
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = import.meta.env.REACT_APP_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL;
|
const model = import.meta.env.REACT_APP_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL;
|
||||||
console.log('Using model:', model);
|
console.log('Using model:', model);
|
||||||
console.log('Max tokens:', MAX_TOKENS);
|
console.log('Max tokens:', MAX_TOKENS);
|
||||||
console.log('Prompt length:', prompt.length);
|
console.log('Prompt length:', prompt.length);
|
||||||
console.log('Previous messages:', messages.length);
|
console.log('Previous messages:', messages.length);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
|
@ -38,10 +38,10 @@ async function callClaude(prompt, messages = []) {
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('Sending request to proxy...');
|
console.log('Sending request to proxy...');
|
||||||
console.log('Request messages:', requestBody.messages.map(m => ({ role: m.role, contentLength: m.content.length })));
|
console.log('Request messages:', requestBody.messages.map(m => ({ role: m.role, contentLength: m.content.length })));
|
||||||
|
|
||||||
// Use local proxy server instead of direct API call
|
// Use local proxy server instead of direct API call
|
||||||
const response = await fetch('http://localhost:3001/api/claude', {
|
const response = await fetch('http://localhost:3001/api/claude', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -50,19 +50,24 @@ async function callClaude(prompt, messages = []) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody)
|
body: JSON.stringify(requestBody)
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('Proxy response status:', response.status);
|
console.log('Proxy response status:', response.status);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const error = await response.text();
|
const error = await response.text();
|
||||||
console.error('Proxy error response:', error);
|
console.error('Proxy error response:', error);
|
||||||
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
console.log('Response data structure:', Object.keys(data));
|
console.log('Response data structure:', Object.keys(data));
|
||||||
console.log('Response content:', data.content?.[0]?.text?.substring(0, 200) + '...');
|
console.log('Response content:', data.content?.[0]?.text?.substring(0, 200) + '...');
|
||||||
return data.content[0].text;
|
console.log('Usage data:', data.usage);
|
||||||
|
|
||||||
|
return {
|
||||||
|
text: data.content[0].text,
|
||||||
|
usage: data.usage // { input_tokens, output_tokens }
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error calling Claude:', error);
|
console.error('Error calling Claude:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -77,14 +82,14 @@ async function callClaude(prompt, messages = []) {
|
||||||
*/
|
*/
|
||||||
function buildBriefingPrompt(currentData, previousBriefing) {
|
function buildBriefingPrompt(currentData, previousBriefing) {
|
||||||
const customInstructions = import.meta.env.REACT_APP_CLAUDE_INSTRUCTIONS;
|
const customInstructions = import.meta.env.REACT_APP_CLAUDE_INSTRUCTIONS;
|
||||||
|
|
||||||
let prompt = '';
|
let prompt = '';
|
||||||
|
|
||||||
// Prepend custom instructions if provided
|
// Prepend custom instructions if provided
|
||||||
if (customInstructions && customInstructions.trim()) {
|
if (customInstructions && customInstructions.trim()) {
|
||||||
prompt += `CUSTOM INSTRUCTIONS: ${customInstructions}\n\n`;
|
prompt += `CUSTOM INSTRUCTIONS: ${customInstructions}\n\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt += `You are creating a personalized daily briefing. Be conversational and highlight what's interesting.
|
prompt += `You are creating a personalized daily briefing. Be conversational and highlight what's interesting.
|
||||||
|
|
||||||
CURRENT DATA:
|
CURRENT DATA:
|
||||||
|
|
@ -126,19 +131,19 @@ export async function generateBriefing(currentData, previousBriefing) {
|
||||||
if (!currentData || Object.keys(currentData.sources || currentData).length === 0) {
|
if (!currentData || Object.keys(currentData.sources || currentData).length === 0) {
|
||||||
return 'No data available to generate briefing. Please check your data sources configuration.';
|
return 'No data available to generate briefing. Please check your data sources configuration.';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prompt = buildBriefingPrompt(currentData, previousBriefing);
|
const prompt = buildBriefingPrompt(currentData, previousBriefing);
|
||||||
const briefing = await callClaude(prompt);
|
const result = await callClaude(prompt);
|
||||||
return briefing;
|
return result; // Returns { text, usage }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error generating briefing:', error);
|
console.error('Error generating briefing:', error);
|
||||||
|
|
||||||
// Return a fallback briefing
|
// Return a fallback briefing
|
||||||
return `Good morning! I encountered an issue generating your personalized briefing today.
|
return `Good morning! I encountered an issue generating your personalized briefing today.
|
||||||
|
|
||||||
Error: ${error.message}
|
Error: ${error.message}
|
||||||
|
|
||||||
Please check your API key configuration and try again.`;
|
Please check your API key configuration and try again.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -153,23 +158,23 @@ export async function sendChatMessage(message, conversationHistory = []) {
|
||||||
if (!message || !message.trim()) {
|
if (!message || !message.trim()) {
|
||||||
throw new Error('Message cannot be empty');
|
throw new Error('Message cannot be empty');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build context from conversation history
|
// Build context from conversation history
|
||||||
const messages = conversationHistory.map(msg => ({
|
const messages = conversationHistory.map(msg => ({
|
||||||
role: msg.role === 'user' ? 'user' : 'assistant',
|
role: msg.role === 'user' ? 'user' : 'assistant',
|
||||||
content: msg.content
|
content: msg.content
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add context about the dashboard
|
// Add context about the dashboard
|
||||||
const contextPrompt = `You are a helpful assistant embedded in a personal dashboard application.
|
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,
|
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.
|
and other information sources configured in their dashboard. Be conversational and helpful.
|
||||||
|
|
||||||
User's message: ${message}`;
|
User's message: ${message}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await callClaude(contextPrompt, messages);
|
const result = await callClaude(contextPrompt, messages);
|
||||||
return response;
|
return result.text; // For chat, just return text for now
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in chat:', error);
|
console.error('Error in chat:', error);
|
||||||
return `I apologize, but I encountered an error: ${error.message}. Please try again.`;
|
return `I apologize, but I encountered an error: ${error.message}. Please try again.`;
|
||||||
|
|
@ -183,23 +188,23 @@ export async function sendChatMessage(message, conversationHistory = []) {
|
||||||
* @returns {Promise<Object>} Extracted content
|
* @returns {Promise<Object>} Extracted content
|
||||||
*/
|
*/
|
||||||
export async function extractContentFromHTML(html, url) {
|
export async function extractContentFromHTML(html, url) {
|
||||||
const prompt = `Extract the main content from this HTML page.
|
const prompt = `Extract the main content from this HTML page.
|
||||||
Focus on the primary article, news content, or main information.
|
Focus on the primary article, news content, or main information.
|
||||||
Ignore navigation, ads, and other peripheral content.
|
Ignore navigation, ads, and other peripheral content.
|
||||||
|
|
||||||
URL: ${url}
|
URL: ${url}
|
||||||
|
|
||||||
HTML (truncated to first 10000 chars):
|
HTML (truncated to first 10000 chars):
|
||||||
${html.substring(0, 10000)}
|
${html.substring(0, 10000)}
|
||||||
|
|
||||||
Please provide a JSON response with:
|
Please provide a JSON response with:
|
||||||
- title: The main title
|
- title: The main title
|
||||||
- summary: A brief summary (2-3 sentences)
|
- summary: A brief summary (2-3 sentences)
|
||||||
- mainContent: The main text content (up to 500 words)
|
- mainContent: The main text content (up to 500 words)
|
||||||
- metadata: Any relevant metadata (author, date, etc.)
|
- metadata: Any relevant metadata (author, date, etc.)
|
||||||
|
|
||||||
Respond with valid JSON only.`;
|
Respond with valid JSON only.`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await callClaude(prompt);
|
const response = await callClaude(prompt);
|
||||||
// Try to parse as JSON
|
// Try to parse as JSON
|
||||||
|
|
@ -236,7 +241,7 @@ export async function summarizeData(data, dataType) {
|
||||||
${JSON.stringify(data, null, 2)}
|
${JSON.stringify(data, null, 2)}
|
||||||
|
|
||||||
Be concise and focus on what would be most relevant to someone checking their daily news.`;
|
Be concise and focus on what would be most relevant to someone checking their daily news.`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await callClaude(prompt);
|
return await callClaude(prompt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { parseSource, standardizeSourceData } from './sourceParser';
|
import { parseSource, standardizeSourceData } from './sourceParser';
|
||||||
import { getCachedData, cacheSourceData } from './storageService';
|
import { getCachedData, cacheSourceData } from './storageService';
|
||||||
import { fetchWithCors } from '../utils/corsProxy';
|
import { fetchWithCors } from '../utils/corsProxy';
|
||||||
import {
|
import {
|
||||||
DEFAULT_DATA_SOURCES,
|
DEFAULT_DATA_SOURCES,
|
||||||
WEATHER_API_BASE,
|
WEATHER_API_BASE,
|
||||||
DEFAULT_REFRESH_INTERVAL
|
DEFAULT_REFRESH_INTERVAL
|
||||||
} from '../utils/constants';
|
} from '../utils/constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,23 +33,23 @@ function getCacheKey(url) {
|
||||||
*/
|
*/
|
||||||
export async function fetchSingleSource(url) {
|
export async function fetchSingleSource(url) {
|
||||||
const cacheKey = getCacheKey(url);
|
const cacheKey = getCacheKey(url);
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cached = getCachedData(cacheKey);
|
const cached = getCachedData(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log(`Using cached data for ${url}`);
|
console.log(`Using cached data for ${url}`);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching fresh data from ${url}`);
|
console.log(`Fetching fresh data from ${url}`);
|
||||||
const data = await parseSource(url);
|
const data = await parseSource(url);
|
||||||
const standardized = standardizeSourceData(data, url);
|
const standardized = standardizeSourceData(data, url);
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL;
|
const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL;
|
||||||
cacheSourceData(cacheKey, standardized, ttl);
|
cacheSourceData(cacheKey, standardized, ttl);
|
||||||
|
|
||||||
return standardized;
|
return standardized;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching ${url}:`, error);
|
console.error(`Error fetching ${url}:`, error);
|
||||||
|
|
@ -73,24 +73,24 @@ export async function fetchWeather(location) {
|
||||||
console.warn('No location configured for weather');
|
console.warn('No location configured for weather');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cacheKey = `weather_${location.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
const cacheKey = `weather_${location.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cached = getCachedData(cacheKey);
|
const cached = getCachedData(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
console.log(`Using cached weather data for ${location}`);
|
console.log(`Using cached weather data for ${location}`);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Format location for wttr.in
|
// Format location for wttr.in
|
||||||
const formattedLocation = encodeURIComponent(location);
|
const formattedLocation = encodeURIComponent(location);
|
||||||
const weatherUrl = `${WEATHER_API_BASE}/${formattedLocation}?format=j1`;
|
const weatherUrl = `${WEATHER_API_BASE}/${formattedLocation}?format=j1`;
|
||||||
|
|
||||||
console.log(`Fetching weather for ${location}`);
|
console.log(`Fetching weather for ${location}`);
|
||||||
const data = await fetchWithCors(weatherUrl);
|
const data = await fetchWithCors(weatherUrl);
|
||||||
|
|
||||||
// Standardize weather data
|
// Standardize weather data
|
||||||
const weatherData = {
|
const weatherData = {
|
||||||
type: 'weather',
|
type: 'weather',
|
||||||
|
|
@ -100,11 +100,11 @@ export async function fetchWeather(location) {
|
||||||
nearestArea: data.nearest_area?.[0] || {},
|
nearestArea: data.nearest_area?.[0] || {},
|
||||||
fetchedAt: Date.now()
|
fetchedAt: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL;
|
const ttl = parseInt(import.meta.env.REACT_APP_REFRESH_INTERVAL) || DEFAULT_REFRESH_INTERVAL;
|
||||||
cacheSourceData(cacheKey, weatherData, ttl);
|
cacheSourceData(cacheKey, weatherData, ttl);
|
||||||
|
|
||||||
return weatherData;
|
return weatherData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching weather for ${location}:`, error);
|
console.error(`Error fetching weather for ${location}:`, error);
|
||||||
|
|
@ -120,22 +120,22 @@ export async function fetchAllSources() {
|
||||||
// Get configured sources from environment or use defaults
|
// Get configured sources from environment or use defaults
|
||||||
const sourcesEnv = import.meta.env.REACT_APP_DATA_SOURCES;
|
const sourcesEnv = import.meta.env.REACT_APP_DATA_SOURCES;
|
||||||
const sources = sourcesEnv ? parseEnvArray(sourcesEnv) : DEFAULT_DATA_SOURCES;
|
const sources = sourcesEnv ? parseEnvArray(sourcesEnv) : DEFAULT_DATA_SOURCES;
|
||||||
|
|
||||||
// Get location for weather
|
// Get location for weather
|
||||||
const location = import.meta.env.REACT_APP_LOCATION;
|
const location = import.meta.env.REACT_APP_LOCATION;
|
||||||
|
|
||||||
console.log('Fetching from sources:', sources);
|
console.log('Fetching from sources:', sources);
|
||||||
console.log('Weather location:', location);
|
console.log('Weather location:', location);
|
||||||
|
|
||||||
// Create fetch promises
|
// Create fetch promises
|
||||||
const promises = sources.map(url =>
|
const promises = sources.map(url =>
|
||||||
fetchSingleSource(url).catch(error => ({
|
fetchSingleSource(url).catch(error => ({
|
||||||
error: true,
|
error: true,
|
||||||
url: url,
|
url: url,
|
||||||
message: error.message
|
message: error.message
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add weather fetch if location is configured
|
// Add weather fetch if location is configured
|
||||||
if (location) {
|
if (location) {
|
||||||
promises.push(
|
promises.push(
|
||||||
|
|
@ -146,14 +146,14 @@ export async function fetchAllSources() {
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all in parallel
|
// Fetch all in parallel
|
||||||
const results = await Promise.allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
// Process results
|
// Process results
|
||||||
const sourceData = {};
|
const sourceData = {};
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
|
||||||
results.forEach((result, index) => {
|
results.forEach((result, index) => {
|
||||||
if (result.status === 'fulfilled') {
|
if (result.status === 'fulfilled') {
|
||||||
const data = result.value;
|
const data = result.value;
|
||||||
|
|
@ -193,12 +193,12 @@ export async function fetchAllSources() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log any errors
|
// Log any errors
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
console.warn('Some sources failed to fetch:', errors);
|
console.warn('Some sources failed to fetch:', errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sources: sourceData,
|
sources: sourceData,
|
||||||
errors: errors,
|
errors: errors,
|
||||||
|
|
@ -233,7 +233,7 @@ export async function fetchHackerNewsStories(storyIds, limit = 10) {
|
||||||
const idsToFetch = storyIds.slice(0, limit);
|
const idsToFetch = storyIds.slice(0, limit);
|
||||||
const promises = idsToFetch.map(id => fetchHackerNewsStory(id));
|
const promises = idsToFetch.map(id => fetchHackerNewsStory(id));
|
||||||
const results = await Promise.allSettled(promises);
|
const results = await Promise.allSettled(promises);
|
||||||
|
|
||||||
return results
|
return results
|
||||||
.filter(r => r.status === 'fulfilled' && r.value)
|
.filter(r => r.status === 'fulfilled' && r.value)
|
||||||
.map(r => r.value);
|
.map(r => r.value);
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@ import { RSS_TO_JSON_API } from '../utils/constants';
|
||||||
*/
|
*/
|
||||||
export function detectSourceType(url) {
|
export function detectSourceType(url) {
|
||||||
const urlLower = url.toLowerCase();
|
const urlLower = url.toLowerCase();
|
||||||
|
|
||||||
if (urlLower.endsWith('.json')) return 'json';
|
if (urlLower.endsWith('.json')) return 'json';
|
||||||
if (urlLower.endsWith('.rss') ||
|
if (urlLower.endsWith('.rss') ||
|
||||||
urlLower.endsWith('.xml') ||
|
urlLower.endsWith('.xml') ||
|
||||||
urlLower.includes('/feed') ||
|
urlLower.includes('/feed') ||
|
||||||
urlLower.includes('rss')) return 'rss';
|
urlLower.includes('rss')) return 'rss';
|
||||||
|
|
||||||
return 'html'; // Default to HTML scraping
|
return 'html'; // Default to HTML scraping
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,18 +44,18 @@ export async function parseRSS(url) {
|
||||||
// Use RSS to JSON service
|
// Use RSS to JSON service
|
||||||
const rssUrl = `${RSS_TO_JSON_API}?rss_url=${encodeURIComponent(url)}`;
|
const rssUrl = `${RSS_TO_JSON_API}?rss_url=${encodeURIComponent(url)}`;
|
||||||
const response = await fetch(rssUrl);
|
const response = await fetch(rssUrl);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`RSS parse failed: ${response.status}`);
|
throw new Error(`RSS parse failed: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Check if RSS2JSON returned an error
|
// Check if RSS2JSON returned an error
|
||||||
if (data.status !== 'ok') {
|
if (data.status !== 'ok') {
|
||||||
throw new Error(data.message || 'RSS feed parsing failed');
|
throw new Error(data.message || 'RSS feed parsing failed');
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error parsing RSS from ${url}:`, error);
|
console.error(`Error parsing RSS from ${url}:`, error);
|
||||||
|
|
@ -72,7 +72,7 @@ export async function parseRSS(url) {
|
||||||
export async function parseHTML(url, corsProxy) {
|
export async function parseHTML(url, corsProxy) {
|
||||||
try {
|
try {
|
||||||
const html = await fetchWithCors(url);
|
const html = await fetchWithCors(url);
|
||||||
|
|
||||||
// Return structured data for Claude to process
|
// Return structured data for Claude to process
|
||||||
return {
|
return {
|
||||||
url: url,
|
url: url,
|
||||||
|
|
@ -94,17 +94,17 @@ export async function parseHTML(url, corsProxy) {
|
||||||
*/
|
*/
|
||||||
export async function parseSource(url, type = null) {
|
export async function parseSource(url, type = null) {
|
||||||
const sourceType = type || detectSourceType(url);
|
const sourceType = type || detectSourceType(url);
|
||||||
|
|
||||||
switch(sourceType) {
|
switch(sourceType) {
|
||||||
case 'json':
|
case 'json':
|
||||||
return await parseJSON(url);
|
return await parseJSON(url);
|
||||||
|
|
||||||
case 'rss':
|
case 'rss':
|
||||||
return await parseRSS(url);
|
return await parseRSS(url);
|
||||||
|
|
||||||
case 'html':
|
case 'html':
|
||||||
return await parseHTML(url);
|
return await parseHTML(url);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown source type: ${sourceType}`);
|
throw new Error(`Unknown source type: ${sourceType}`);
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ export function standardizeSourceData(data, sourceUrl) {
|
||||||
before: data.data.before
|
before: data.data.before
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceUrl.includes('hacker-news') && Array.isArray(data)) {
|
if (sourceUrl.includes('hacker-news') && Array.isArray(data)) {
|
||||||
// Hacker News top stories (just IDs)
|
// Hacker News top stories (just IDs)
|
||||||
return {
|
return {
|
||||||
|
|
@ -163,7 +163,7 @@ export function standardizeSourceData(data, sourceUrl) {
|
||||||
fetchedAt: Date.now()
|
fetchedAt: Date.now()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.items && data.feed) {
|
if (data.items && data.feed) {
|
||||||
// RSS feed format from rss2json
|
// RSS feed format from rss2json
|
||||||
return {
|
return {
|
||||||
|
|
@ -181,7 +181,7 @@ export function standardizeSourceData(data, sourceUrl) {
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default format for unknown sources
|
// Default format for unknown sources
|
||||||
return {
|
return {
|
||||||
type: 'unknown',
|
type: 'unknown',
|
||||||
|
|
|
||||||
|
|
@ -34,15 +34,15 @@ function saveStorage(storage) {
|
||||||
*/
|
*/
|
||||||
export function saveBriefing(date, briefingText, sourceData) {
|
export function saveBriefing(date, briefingText, sourceData) {
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
|
|
||||||
storage.briefings[date] = {
|
storage.briefings[date] = {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
briefing: briefingText,
|
briefing: briefingText,
|
||||||
sources: sourceData
|
sources: sourceData
|
||||||
};
|
};
|
||||||
|
|
||||||
saveStorage(storage);
|
saveStorage(storage);
|
||||||
|
|
||||||
// Clean up old data after saving
|
// Clean up old data after saving
|
||||||
cleanupOldData();
|
cleanupOldData();
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +57,7 @@ export function getPreviousBriefing(daysAgo = 1) {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
date.setDate(date.getDate() - daysAgo);
|
date.setDate(date.getDate() - daysAgo);
|
||||||
const dateKey = date.toISOString().split('T')[0];
|
const dateKey = date.toISOString().split('T')[0];
|
||||||
|
|
||||||
return storage.briefings[dateKey] || null;
|
return storage.briefings[dateKey] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,13 +69,13 @@ export function getPreviousBriefing(daysAgo = 1) {
|
||||||
*/
|
*/
|
||||||
export function cacheSourceData(sourceKey, data, ttlMinutes = CACHE_TTL) {
|
export function cacheSourceData(sourceKey, data, ttlMinutes = CACHE_TTL) {
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
|
|
||||||
storage.cache[sourceKey] = {
|
storage.cache[sourceKey] = {
|
||||||
data: data,
|
data: data,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
ttl: ttlMinutes * 60 * 1000 // Convert to milliseconds
|
ttl: ttlMinutes * 60 * 1000 // Convert to milliseconds
|
||||||
};
|
};
|
||||||
|
|
||||||
saveStorage(storage);
|
saveStorage(storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,21 +87,21 @@ export function cacheSourceData(sourceKey, data, ttlMinutes = CACHE_TTL) {
|
||||||
export function getCachedData(sourceKey) {
|
export function getCachedData(sourceKey) {
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
const cached = storage.cache[sourceKey];
|
const cached = storage.cache[sourceKey];
|
||||||
|
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const age = now - cached.timestamp;
|
const age = now - cached.timestamp;
|
||||||
|
|
||||||
if (age > cached.ttl) {
|
if (age > cached.ttl) {
|
||||||
// Cache expired
|
// Cache expired
|
||||||
delete storage.cache[sourceKey];
|
delete storage.cache[sourceKey];
|
||||||
saveStorage(storage);
|
saveStorage(storage);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cached.data;
|
return cached.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,10 +112,10 @@ export function getCachedData(sourceKey) {
|
||||||
export function cleanupOldData(daysToKeep = null) {
|
export function cleanupOldData(daysToKeep = null) {
|
||||||
const storage = getStorage();
|
const storage = getStorage();
|
||||||
const days = daysToKeep || parseInt(import.meta.env.REACT_APP_HISTORY_DAYS) || DEFAULT_HISTORY_DAYS;
|
const days = daysToKeep || parseInt(import.meta.env.REACT_APP_HISTORY_DAYS) || DEFAULT_HISTORY_DAYS;
|
||||||
|
|
||||||
const cutoffDate = new Date();
|
const cutoffDate = new Date();
|
||||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||||
|
|
||||||
// Clean old briefings
|
// Clean old briefings
|
||||||
if (storage.briefings) {
|
if (storage.briefings) {
|
||||||
Object.keys(storage.briefings).forEach(date => {
|
Object.keys(storage.briefings).forEach(date => {
|
||||||
|
|
@ -124,7 +124,7 @@ export function cleanupOldData(daysToKeep = null) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean expired cache entries
|
// Clean expired cache entries
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (storage.cache) {
|
if (storage.cache) {
|
||||||
|
|
@ -135,7 +135,7 @@ export function cleanupOldData(daysToKeep = null) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStorage(storage);
|
saveStorage(storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { DEFAULT_CORS_PROXY } from './constants';
|
||||||
*/
|
*/
|
||||||
export async function fetchWithCors(url, options = {}) {
|
export async function fetchWithCors(url, options = {}) {
|
||||||
const proxy = import.meta.env.REACT_APP_CORS_PROXY || DEFAULT_CORS_PROXY;
|
const proxy = import.meta.env.REACT_APP_CORS_PROXY || DEFAULT_CORS_PROXY;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try direct fetch first
|
// Try direct fetch first
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
|
|
@ -19,11 +19,11 @@ export async function fetchWithCors(url, options = {}) {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
if (contentType && contentType.includes('application/json')) {
|
if (contentType && contentType.includes('application/json')) {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
|
@ -33,15 +33,15 @@ export async function fetchWithCors(url, options = {}) {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If CORS fails, try with proxy
|
// If CORS fails, try with proxy
|
||||||
console.warn(`CORS failed for ${url}, trying proxy...`, error.message);
|
console.warn(`CORS failed for ${url}, trying proxy...`, error.message);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const proxiedUrl = `${proxy}${encodeURIComponent(url)}`;
|
const proxiedUrl = `${proxy}${encodeURIComponent(url)}`;
|
||||||
const response = await fetch(proxiedUrl, options);
|
const response = await fetch(proxiedUrl, options);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type');
|
const contentType = response.headers.get('content-type');
|
||||||
if (contentType && contentType.includes('application/json')) {
|
if (contentType && contentType.includes('application/json')) {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
|
@ -82,7 +82,7 @@ export async function getAccessibleUrl(url) {
|
||||||
if (canAccess) {
|
if (canAccess) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const proxy = import.meta.env.REACT_APP_CORS_PROXY || DEFAULT_CORS_PROXY;
|
const proxy = import.meta.env.REACT_APP_CORS_PROXY || DEFAULT_CORS_PROXY;
|
||||||
return `${proxy}${encodeURIComponent(url)}`;
|
return `${proxy}${encodeURIComponent(url)}`;
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react'
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
// Load env file based on `mode` in the current working directory.
|
// Load env file based on `mode` in the current working directory.
|
||||||
const env = loadEnv(mode, process.cwd(), '')
|
const env = loadEnv(mode, process.cwd(), '')
|
||||||
|
|
||||||
// Expose REACT_APP_ prefixed variables to import.meta.env
|
// Expose REACT_APP_ prefixed variables to import.meta.env
|
||||||
const processEnvValues = {
|
const processEnvValues = {
|
||||||
'process.env': Object.entries(env).reduce(
|
'process.env': Object.entries(env).reduce(
|
||||||
|
|
@ -18,7 +18,7 @@ export default defineConfig(({ mode }) => {
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
define: {
|
define: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue