339 lines
No EOL
11 KiB
JavaScript
339 lines
No EOL
11 KiB
JavaScript
import {
|
|
ANTHROPIC_API_URL,
|
|
DEFAULT_CLAUDE_MODEL,
|
|
MAX_TOKENS
|
|
} from '../utils/constants';
|
|
import { condenseSourcesForClaude } from './sourceParser';
|
|
|
|
/**
|
|
* Make a request to Claude API
|
|
* @param {string} prompt - The prompt to send
|
|
* @param {Array} messages - Optional message history
|
|
* @returns {Promise<string>} Claude's response
|
|
*/
|
|
async function callClaude(prompt, messages = []) {
|
|
const apiKey = import.meta.env.REACT_APP_ANTHROPIC_API_KEY;
|
|
|
|
console.log('=== callClaude function ===');
|
|
console.log('API Key configured:', !!apiKey);
|
|
console.log('API Key starts with:', apiKey?.substring(0, 15) + '...');
|
|
|
|
if (!apiKey || apiKey === 'sk-ant-your-key-here') {
|
|
console.error('Anthropic API key not configured');
|
|
return 'Please configure your Anthropic API key in .env.local';
|
|
}
|
|
|
|
const model = import.meta.env.REACT_APP_CLAUDE_MODEL || DEFAULT_CLAUDE_MODEL;
|
|
console.log('Using model:', model);
|
|
console.log('Max tokens:', MAX_TOKENS);
|
|
console.log('Prompt length:', prompt.length);
|
|
console.log('Previous messages:', messages.length);
|
|
|
|
try {
|
|
const requestBody = {
|
|
apiKey: apiKey,
|
|
model: model,
|
|
maxTokens: MAX_TOKENS,
|
|
messages: [
|
|
...messages,
|
|
{ role: 'user', content: prompt }
|
|
]
|
|
};
|
|
|
|
console.log('Sending request to proxy...');
|
|
console.log('Request messages:', requestBody.messages.map(m => ({ role: m.role, contentLength: m.content.length })));
|
|
|
|
// Use local proxy server instead of direct API call
|
|
const response = await fetch('http://localhost:3001/api/claude', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
console.log('Proxy response status:', response.status);
|
|
|
|
if (!response.ok) {
|
|
const error = await response.text();
|
|
console.error('Proxy error response:', error);
|
|
throw new Error(`Claude API error: ${response.status} - ${error}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Response data structure:', Object.keys(data));
|
|
console.log('Response content:', data.content?.[0]?.text?.substring(0, 200) + '...');
|
|
console.log('Usage data:', data.usage);
|
|
|
|
return {
|
|
text: data.content[0].text,
|
|
usage: data.usage // { input_tokens, output_tokens }
|
|
};
|
|
} catch (error) {
|
|
console.error('Error calling Claude:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build briefing prompt with memory
|
|
* @param {Object} currentData - Current source data
|
|
* @param {Object} previousBriefing - Previous briefing object (condensed summary)
|
|
* @returns {string} The prompt
|
|
*/
|
|
function buildBriefingPrompt(currentData, previousBriefing) {
|
|
const customInstructions = import.meta.env.REACT_APP_CLAUDE_INSTRUCTIONS;
|
|
|
|
let prompt = '';
|
|
|
|
// Prepend custom instructions if provided
|
|
if (customInstructions && customInstructions.trim()) {
|
|
prompt += `CUSTOM INSTRUCTIONS: ${customInstructions}\n\n`;
|
|
}
|
|
|
|
prompt += `You are creating a personalized daily briefing. Be conversational and highlight what's interesting.
|
|
|
|
CURRENT DATA:
|
|
`;
|
|
|
|
// CONDENSE DATA BEFORE SENDING TO CLAUDE
|
|
const condensedData = condenseSourcesForClaude(currentData.sources || currentData);
|
|
console.log('Original data size:', JSON.stringify(currentData).length);
|
|
console.log('Condensed data size:', JSON.stringify(condensedData).length);
|
|
console.log('Size reduction:', Math.round((1 - JSON.stringify(condensedData).length / JSON.stringify(currentData).length) * 100) + '%');
|
|
|
|
// Add all condensed source data
|
|
Object.entries(condensedData).forEach(([source, data]) => {
|
|
if (data && !data.error) {
|
|
prompt += `\n${source.toUpperCase()}:\n${JSON.stringify(data, null, 2)}\n`;
|
|
}
|
|
});
|
|
|
|
// Add historical context if available (now using condensed summary)
|
|
if (previousBriefing && previousBriefing.condensedSummary) {
|
|
prompt += `\n\nPREVIOUS BRIEFING SUMMARY:\n${previousBriefing.condensedSummary}\n`;
|
|
prompt += `\nHighlight what's changed, any follow-up stories, or interesting trends.`;
|
|
}
|
|
|
|
prompt += `\n\nCreate a friendly briefing (300-400 words) that:
|
|
- Starts with a warm greeting appropriate for the time of day
|
|
- Highlights the most interesting items from the data
|
|
- Notes anything that's changed since yesterday (if applicable)
|
|
- Has personality and isn't just a dry list
|
|
- Includes relevant weather information if available
|
|
- Ends with something engaging or thought-provoking
|
|
|
|
Focus on the most newsworthy and interesting items. Don't try to mention everything.
|
|
|
|
IMPORTANT: Provide your response as valid JSON with exactly two fields:
|
|
{
|
|
"fullBriefing": "Your complete 300-400 word friendly briefing text here",
|
|
"condensedSummary": "A 50-75 word ultra-condensed summary of the key topics and main stories for historical context"
|
|
}
|
|
|
|
The JSON must be properly formatted with escaped quotes where necessary.`;
|
|
|
|
return prompt;
|
|
}
|
|
|
|
/**
|
|
* Generate a condensed summary of a briefing for historical storage
|
|
* @param {string} fullBriefing - The full briefing text
|
|
* @returns {Promise<string>} Condensed summary (50-75 words)
|
|
*/
|
|
async function generateCondensedSummary(fullBriefing) {
|
|
const prompt = `Create an ultra-condensed summary of this briefing in 50-75 words.
|
|
Focus only on the key topics and main stories mentioned.
|
|
This will be used as historical context for tomorrow's briefing generation.
|
|
|
|
FULL BRIEFING:
|
|
${fullBriefing}
|
|
|
|
Provide ONLY the condensed summary, no preamble or explanation.`;
|
|
|
|
try {
|
|
const result = await callClaude(prompt);
|
|
return result.text;
|
|
} catch (error) {
|
|
console.error('Error generating condensed summary:', error);
|
|
// Fallback to first 300 chars of original
|
|
return fullBriefing.substring(0, 300) + '...';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate daily briefing with condensed summary
|
|
* @param {Object} currentData - Current data from all sources
|
|
* @param {Object} previousBriefing - Previous briefing for comparison
|
|
* @returns {Promise<Object>} Object containing full briefing and condensed summary
|
|
*/
|
|
export async function generateBriefing(currentData, previousBriefing) {
|
|
if (!currentData || Object.keys(currentData.sources || currentData).length === 0) {
|
|
return {
|
|
text: 'No data available to generate briefing. Please check your data sources configuration.',
|
|
condensedSummary: 'No data available.'
|
|
};
|
|
}
|
|
|
|
try {
|
|
const prompt = buildBriefingPrompt(currentData, previousBriefing);
|
|
const result = await callClaude(prompt);
|
|
|
|
// Parse JSON response that now includes both fullBriefing and condensedSummary
|
|
try {
|
|
// Strip markdown code blocks if present
|
|
let jsonText = result.text;
|
|
if (jsonText.includes('```json')) {
|
|
jsonText = jsonText.replace(/```json\s*/g, '').replace(/```\s*/g, '');
|
|
} else if (jsonText.includes('```')) {
|
|
jsonText = jsonText.replace(/```\s*/g, '');
|
|
}
|
|
jsonText = jsonText.trim();
|
|
|
|
const parsed = JSON.parse(jsonText);
|
|
|
|
// Validate the JSON structure
|
|
if (!parsed.fullBriefing || !parsed.condensedSummary) {
|
|
throw new Error('Invalid response structure from Claude');
|
|
}
|
|
|
|
console.log('Successfully parsed briefing with condensed summary');
|
|
console.log('Condensed summary:', parsed.condensedSummary.substring(0, 100) + '...');
|
|
|
|
return {
|
|
text: parsed.fullBriefing,
|
|
condensedSummary: parsed.condensedSummary,
|
|
usage: result.usage
|
|
};
|
|
} catch (parseError) {
|
|
console.error('Failed to parse JSON response, attempting fallback:', parseError);
|
|
|
|
// Fallback: if JSON parsing fails, use the raw text and generate summary separately
|
|
// This maintains backward compatibility but should rarely happen
|
|
console.warn('Using fallback: generating condensed summary separately');
|
|
const condensedSummary = await generateCondensedSummary(result.text);
|
|
|
|
return {
|
|
text: result.text,
|
|
condensedSummary: condensedSummary,
|
|
usage: result.usage
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('Error generating briefing:', error);
|
|
|
|
// Return a fallback briefing
|
|
const fallbackText = `Good morning! I encountered an issue generating your personalized briefing today.
|
|
|
|
Error: ${error.message}
|
|
|
|
Please check your API key configuration and try again.`;
|
|
|
|
return {
|
|
text: fallbackText,
|
|
condensedSummary: `Error: ${error.message}`
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send a chat message to Claude
|
|
* @param {string} message - User's message
|
|
* @param {Array} conversationHistory - Previous messages
|
|
* @returns {Promise<string>} Claude's response
|
|
*/
|
|
export async function sendChatMessage(message, conversationHistory = []) {
|
|
if (!message || !message.trim()) {
|
|
throw new Error('Message cannot be empty');
|
|
}
|
|
|
|
// Build context from conversation history
|
|
const messages = conversationHistory.map(msg => ({
|
|
role: msg.role === 'user' ? 'user' : 'assistant',
|
|
content: msg.content
|
|
}));
|
|
|
|
// Add context about the dashboard
|
|
const contextPrompt = `You are a helpful assistant embedded in a personal dashboard application.
|
|
You have access to the user's daily briefing data and can help answer questions about news, weather,
|
|
and other information sources configured in their dashboard. Be conversational and helpful.
|
|
|
|
User's message: ${message}`;
|
|
|
|
try {
|
|
const result = await callClaude(contextPrompt, messages);
|
|
return result.text; // For chat, just return text for now
|
|
} catch (error) {
|
|
console.error('Error in chat:', error);
|
|
return `I apologize, but I encountered an error: ${error.message}. Please try again.`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract meaningful content from HTML using Claude
|
|
* @param {string} html - The HTML content
|
|
* @param {string} url - The source URL
|
|
* @returns {Promise<Object>} Extracted content
|
|
*/
|
|
export async function extractContentFromHTML(html, url) {
|
|
const prompt = `Extract the main content from this HTML page.
|
|
Focus on the primary article, news content, or main information.
|
|
Ignore navigation, ads, and other peripheral content.
|
|
|
|
URL: ${url}
|
|
|
|
HTML (truncated to first 10000 chars):
|
|
${html.substring(0, 10000)}
|
|
|
|
Please provide a JSON response with:
|
|
- title: The main title
|
|
- summary: A brief summary (2-3 sentences)
|
|
- mainContent: The main text content (up to 500 words)
|
|
- metadata: Any relevant metadata (author, date, etc.)
|
|
|
|
Respond with valid JSON only.`;
|
|
|
|
try {
|
|
const response = await callClaude(prompt);
|
|
// Try to parse as JSON
|
|
try {
|
|
return JSON.parse(response);
|
|
} catch {
|
|
// If not valid JSON, return structured response
|
|
return {
|
|
title: 'Content from ' + url,
|
|
summary: response.substring(0, 200),
|
|
mainContent: response,
|
|
metadata: { url, extractedAt: Date.now() }
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('Error extracting HTML content:', error);
|
|
return {
|
|
error: true,
|
|
message: error.message,
|
|
url: url
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a summary of specific data
|
|
* @param {Object} data - Data to summarize
|
|
* @param {string} dataType - Type of data (reddit, news, etc.)
|
|
* @returns {Promise<string>} Summary
|
|
*/
|
|
export async function summarizeData(data, dataType) {
|
|
const prompt = `Summarize this ${dataType} data in 2-3 sentences, highlighting the most interesting or important items:
|
|
|
|
${JSON.stringify(data, null, 2)}
|
|
|
|
Be concise and focus on what would be most relevant to someone checking their daily news.`;
|
|
|
|
try {
|
|
return await callClaude(prompt);
|
|
} catch (error) {
|
|
console.error(`Error summarizing ${dataType}:`, error);
|
|
return `Unable to summarize ${dataType} data.`;
|
|
}
|
|
} |