added token usage and cost to UI, cleanup

This commit is contained in:
Drew Peifer 2026-02-12 03:26:10 -05:00
parent a85e96aad8
commit bffb11e5b0
15 changed files with 285 additions and 254 deletions

View file

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

View file

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

View file

@ -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"
> >

View file

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

View file

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

View file

@ -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'

View file

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

View file

@ -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',

View file

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

View file

@ -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) {

View file

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

View file

@ -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',

View file

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

View file

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

View file

@ -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: {