{"repo": "amitkumarankushcom-commits/roottolearn", "files": {"backend/routes/summaries.js": {"13e112381a2f952251f28fdf64f56f9cdc3d35f1": {"t": 1785745531.6163068, "n": 14139, "h": "cca3f0b7ebb888d7", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst multer = require('multer');\nconst path = require('path');\nconst fs = require('fs');\nconst OpenAI = require('openai');\nconst Anthropic = require('@anthropic-ai/sdk');\nconst supabase = require('../config/supabase');\n\n// ── AI clients\nconst openaiClient = process.env.OPENAI_API_KEY\n ? new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n : null;\n\nconst deepseekClient = process.env.DEEPSEEK_API_KEY\n ? new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com' })\n : null;\n\nconst anthropicClient = process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY !== 'dummy'\n ? new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })\n : null;\n\n// Groq client for free Whisper transcription (OpenAI-compatible API)\nconst groqClient = process.env.GROQ_API_KEY\n ? new OpenAI({ apiKey: process.env.GROQ_API_KEY, baseURL: 'https://api.groq.com/openai/v1' })\n : null;\n\nconst upload = multer({\n dest: path.join(__dirname, '..', 'tmp'),\n limits: { fileSize: 100 * 1024 * 1024 },\n fileFilter: (req, file, cb) => {\n const allowed = ['.mp3', '.wav', '.m4a', '.ogg', '.webm', '.mp4', '.mpeg', '.mpga'];\n const ext = path.extname(file.originalname).toLowerCase();\n if (allowed.includes(ext)) cb(null, true);\n else cb(new Error('Unsupported audio format. Use MP3, WAV, M4A, or OGG.'));\n }\n});\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── AI Summarization using selected model\nasync function aiSummarize(text, style, occupation, language, model) {\n const styleGuide = {\n simple: 'Write a clear, concise summary with sections: Overview, Main Points (as bullet list), Conclusion.',\n detailed: 'Write a thorough detailed summary with sections: Overview, Detailed Analysis (multiple paragraphs), Summary & Takeaways.',\n bullets: 'Write the entire summary as organized bullet points with sections: Overview, Key Points, Conclusion.'\n };\n\n const prompt = `You are an expert summarizer for ${occupation || 'general'} audience.\nSummarize the following text in ${language || 'English'}.\n${styleGuide[style] || styleGuide.simple}\nUse markdown formatting with ## headings.\nMark important terms with **bold**.\nAdd a > blockquote callout for the most critical takeaway.\nAt the end, add:\n---JSON---\n{\"highlights\":[{\"icon\":\"📌\",\"val\":\"\",\"label\":\"Highlight\"},{\"icon\":\"📊\",\"val\":\"
\",\"label\":\"Fact\"},{\"icon\":\"⚡\",\"val\":\"\",\"label\":\"Next Step\"}],\"takeaways\":[\"\",\"\",\"\"]}\n---END---\n\nText to summarize:\n${text.slice(0, 60000)}`;\n\n // Try selected model, fall back to others if unavailable\n const modelOrder = model === 'gpt'\n ? ['gpt', 'deepseek', 'claude']\n : model === 'deepseek'\n ? ['deepseek', 'gpt', 'claude']\n : ['claude', 'deepseek', 'gpt'];\n\n for (const m of modelOrder) {\n try {\n if (m === 'claude' && anthropicClient) {\n const resp = await anthropicClient.messages.create({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.content[0].text;\n }\n if (m === 'gpt' && openaiClient) {\n const resp = await openaiClient.chat.completions.create({\n model: 'gpt-4o-mini',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.choices[0].message.content;\n }\n if (m === 'deepseek' && deepseekClient) {\n const resp = await deepseekClient.chat.completions.create({\n model: 'deepseek-chat',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.choices[0].message.content;\n }\n } catch (err) {\n console.warn(`[AI SUMMARIZE] ${m} failed:`, err.message);\n continue;\n }\n }\n\n // All models failed — fall back to local text splitter\n console.warn('[AI SUMMARIZE] All models failed, using local fallback');\n return null;\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n // ── Enforce free plan limit (3 summaries per month)\n const userPlan = req.user.plan || 'free';\n if (userPlan === 'free') {\n const startOfMonth = new Date();\n startOfMonth.setDate(1);\n startOfMonth.setHours(0, 0, 0, 0);\n\n const { count, error: countErr } = await supabase\n .from('summaries')\n .select('id', { count: 'exact', head: true })\n .eq('user_id', req.user.id)\n .gte('created_at', startOfMonth.toISOString());\n\n if (!countErr && count >= 3) {\n return res.status(403).json({ error: 'Free plan limit reached (3 summaries/month). Upgrade to Pro for unlimited access.', upgradeRequired: true });\n }\n }\n\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n\n if (!text || !text.trim()) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n // Try AI summarization first, fall back to local text splitter\n let summaryText = await aiSummarize(text, style, occupation, language, model || 'claude');\n if (!summaryText) {\n summaryText = buildSummaryFromText(text, style, occupation);\n }\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Could not generate summary' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries/transcribe (Audio → Text via Whisper)\nrouter.post('/transcribe', authenticateToken, upload.single('audio'), async (req, res) => {\n if (!req.file) {\n return res.status(400).json({ error: 'No audio file uploaded' });\n }\n\n const filePath = req.file.path;\n\n try {\n if (!openaiClient && !groqClient) {\n return res.status(503).json({ error: 'Audio transcription is not configured. Set OPENAI_API_KEY or GROQ_API_KEY in environment.' });\n }\n\n // Rename temp file to include original extension (Whisper needs it)\n const ext = path.extname(req.file.originalname).toLowerCase() || '.mp3';\n const namedPath = filePath + ext;\n fs.renameSync(filePath, namedPath);\n\n // Try transcription providers in order: Groq (free) → OpenAI\n let text = '';\n let lastError = null;\n\n const providers = [\n { name: 'Groq', client: groqClient, model: 'whisper-large-v3-turbo' },\n { name: 'OpenAI', client: openaiClient, model: 'whisper-1' },\n ];\n\n for (const provider of providers) {\n if (!provider.client) continue;\n try {\n console.log(`[TRANSCRIBE] Trying ${provider.name} Whisper...`);\n const transcription = await provider.client.audio.transcriptions.create({\n file: fs.createReadStream(namedPath),\n model: provider.model,\n response_format: 'text',\n });\n text = typeof transcription === 'string' ? transcription : transcription.text || '';\n if (text.trim()) {\n console.log(`[TRANSCRIBE] ${provider.name} succeeded (${text.length} chars)`);\n break;\n }\n } catch (err) {\n console.warn(`[TRANSCRIBE] ${provider.name} failed:`, err.message);\n lastError = err;\n }\n }\n\n // Clean up temp file\n fs.unlink(namedPath, () => {});\n\n if (!text.trim()) {\n if (lastError && lastError.status === 401) {\n return res.status(503).json({ error: 'API key invalid. Set a valid GROQ_API_KEY (free at console.groq.com) or OPENAI_API_KEY.' });\n }\n return res.status(422).json({ error: 'Could not transcribe audio. The file may be empty or inaudible.' });\n }\n\n res.json({ success: true, text });\n\n } catch (error) {\n // Clean up temp file on error\n try { fs.unlinkSync(filePath); } catch {}\n try { fs.unlinkSync(filePath + path.extname(req.file.originalname)); } catch {}\n\n console.error('[TRANSCRIBE ERROR]', error);\n res.status(500).json({ error: 'Transcription failed: ' + (error.message || 'Unknown error') });\n }\n});\n\nmodule.exports = router;\n"}, "9f5e6accf8c9c059af39824457a498f45897c860": {"t": 1785753925.9182675, "n": 13202, "h": "d3d4ee5dc49d993a", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst multer = require('multer');\nconst path = require('path');\nconst fs = require('fs');\nconst OpenAI = require('openai');\nconst Anthropic = require('@anthropic-ai/sdk');\nconst supabase = require('../config/supabase');\n\n// ── AI clients\nconst openaiClient = process.env.OPENAI_API_KEY\n ? new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n : null;\n\nconst deepseekClient = process.env.DEEPSEEK_API_KEY\n ? new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com' })\n : null;\n\nconst anthropicClient = process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY !== 'dummy'\n ? new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })\n : null;\nconst upload = multer({\n dest: path.join(__dirname, '..', 'tmp'),\n limits: { fileSize: 100 * 1024 * 1024 },\n fileFilter: (req, file, cb) => {\n const allowed = ['.mp3', '.wav', '.m4a', '.ogg', '.webm', '.mp4', '.mpeg', '.mpga'];\n const ext = path.extname(file.originalname).toLowerCase();\n if (allowed.includes(ext)) cb(null, true);\n else cb(new Error('Unsupported audio format. Use MP3, WAV, M4A, or OGG.'));\n }\n});\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── AI Summarization using selected model\nasync function aiSummarize(text, style, occupation, language, model) {\n const styleGuide = {\n simple: 'Write a clear, concise summary with sections: Overview, Main Points (as bullet list), Conclusion.',\n detailed: 'Write a thorough detailed summary with sections: Overview, Detailed Analysis (multiple paragraphs), Summary & Takeaways.',\n bullets: 'Write the entire summary as organized bullet points with sections: Overview, Key Points, Conclusion.'\n };\n\n const prompt = `You are an expert summarizer for ${occupation || 'general'} audience.\nSummarize the following text in ${language || 'English'}.\n${styleGuide[style] || styleGuide.simple}\nUse markdown formatting with ## headings.\nMark important terms with **bold**.\nAdd a > blockquote callout for the most critical takeaway.\nAt the end, add:\n---JSON---\n{\"highlights\":[{\"icon\":\"📌\",\"val\":\"\",\"label\":\"Highlight\"},{\"icon\":\"📊\",\"val\":\"
\",\"label\":\"Fact\"},{\"icon\":\"⚡\",\"val\":\"\",\"label\":\"Next Step\"}],\"takeaways\":[\"\",\"\",\"\"]}\n---END---\n\nText to summarize:\n${text.slice(0, 60000)}`;\n\n // Try selected model, fall back to others if unavailable\n const modelOrder = model === 'gpt'\n ? ['gpt', 'deepseek', 'claude']\n : model === 'deepseek'\n ? ['deepseek', 'gpt', 'claude']\n : ['claude', 'deepseek', 'gpt'];\n\n for (const m of modelOrder) {\n try {\n if (m === 'claude' && anthropicClient) {\n const resp = await anthropicClient.messages.create({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.content[0].text;\n }\n if (m === 'gpt' && openaiClient) {\n const resp = await openaiClient.chat.completions.create({\n model: 'gpt-4o-mini',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.choices[0].message.content;\n }\n if (m === 'deepseek' && deepseekClient) {\n const resp = await deepseekClient.chat.completions.create({\n model: 'deepseek-chat',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.choices[0].message.content;\n }\n } catch (err) {\n console.warn(`[AI SUMMARIZE] ${m} failed:`, err.message);\n continue;\n }\n }\n\n // All models failed — fall back to local text splitter\n console.warn('[AI SUMMARIZE] All models failed, using local fallback');\n return null;\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n // ── Enforce free plan limit (3 summaries per month)\n const userPlan = req.user.plan || 'free';\n if (userPlan === 'free') {\n const startOfMonth = new Date();\n startOfMonth.setDate(1);\n startOfMonth.setHours(0, 0, 0, 0);\n\n const { count, error: countErr } = await supabase\n .from('summaries')\n .select('id', { count: 'exact', head: true })\n .eq('user_id', req.user.id)\n .gte('created_at', startOfMonth.toISOString());\n\n if (!countErr && count >= 3) {\n return res.status(403).json({ error: 'Free plan limit reached (3 summaries/month). Upgrade to Pro for unlimited access.', upgradeRequired: true });\n }\n }\n\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n\n if (!text || !text.trim()) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n // Try AI summarization first, fall back to local text splitter\n let summaryText = await aiSummarize(text, style, occupation, language, model || 'claude');\n if (!summaryText) {\n summaryText = buildSummaryFromText(text, style, occupation);\n }\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Could not generate summary' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries/transcribe (Audio → Text via Whisper)\nrouter.post('/transcribe', authenticateToken, upload.single('audio'), async (req, res) => {\n if (!req.file) {\n return res.status(400).json({ error: 'No audio file uploaded' });\n }\n\n const filePath = req.file.path;\n\n try {\n if (!openaiClient) {\n return res.status(503).json({ error: 'Audio transcription is not configured. Set OPENAI_API_KEY in environment.' });\n }\n\n // Rename temp file to include original extension (Whisper needs it)\n const ext = path.extname(req.file.originalname).toLowerCase() || '.mp3';\n const namedPath = filePath + ext;\n fs.renameSync(filePath, namedPath);\n\n const transcription = await openaiClient.audio.transcriptions.create({\n file: fs.createReadStream(namedPath),\n model: 'whisper-1',\n response_format: 'text',\n });\n\n // Clean up temp file\n fs.unlink(namedPath, () => {});\n\n const text = typeof transcription === 'string' ? transcription : transcription.text || '';\n if (!text.trim()) {\n return res.status(422).json({ error: 'Could not transcribe audio. The file may be empty or inaudible.' });\n }\n\n res.json({ success: true, text });\n\n } catch (error) {\n // Clean up temp file on error\n try { fs.unlinkSync(filePath); } catch {}\n try { fs.unlinkSync(filePath + path.extname(req.file.originalname)); } catch {}\n\n console.error('[TRANSCRIBE ERROR]', error);\n\n if (error.status === 401) {\n return res.status(503).json({ error: 'Audio/video transcription requires a valid OpenAI API key (Whisper). This is separate from the AI model selection. Please set a valid OPENAI_API_KEY or use a PDF/text file instead.' });\n }\n res.status(500).json({ error: 'Transcription failed: ' + (error.message || 'Unknown error') });\n }\n});\n\nmodule.exports = router;\n"}, "584f90d7450c37ba365ba9da8073a2c60ecf2943": {"t": 1785753927.3575766, "n": 13067, "h": "75cc4aaa71c7d39f", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst multer = require('multer');\nconst path = require('path');\nconst fs = require('fs');\nconst OpenAI = require('openai');\nconst Anthropic = require('@anthropic-ai/sdk');\nconst supabase = require('../config/supabase');\n\n// ── AI clients\nconst openaiClient = process.env.OPENAI_API_KEY\n ? new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n : null;\n\nconst deepseekClient = process.env.DEEPSEEK_API_KEY\n ? new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com' })\n : null;\n\nconst anthropicClient = process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY !== 'dummy'\n ? new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })\n : null;\nconst upload = multer({\n dest: path.join(__dirname, '..', 'tmp'),\n limits: { fileSize: 100 * 1024 * 1024 },\n fileFilter: (req, file, cb) => {\n const allowed = ['.mp3', '.wav', '.m4a', '.ogg', '.webm', '.mp4', '.mpeg', '.mpga'];\n const ext = path.extname(file.originalname).toLowerCase();\n if (allowed.includes(ext)) cb(null, true);\n else cb(new Error('Unsupported audio format. Use MP3, WAV, M4A, or OGG.'));\n }\n});\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── AI Summarization using selected model\nasync function aiSummarize(text, style, occupation, language, model) {\n const styleGuide = {\n simple: 'Write a clear, concise summary with sections: Overview, Main Points (as bullet list), Conclusion.',\n detailed: 'Write a thorough detailed summary with sections: Overview, Detailed Analysis (multiple paragraphs), Summary & Takeaways.',\n bullets: 'Write the entire summary as organized bullet points with sections: Overview, Key Points, Conclusion.'\n };\n\n const prompt = `You are an expert summarizer for ${occupation || 'general'} audience.\nSummarize the following text in ${language || 'English'}.\n${styleGuide[style] || styleGuide.simple}\nUse markdown formatting with ## headings.\nMark important terms with **bold**.\nAdd a > blockquote callout for the most critical takeaway.\nAt the end, add:\n---JSON---\n{\"highlights\":[{\"icon\":\"📌\",\"val\":\"\",\"label\":\"Highlight\"},{\"icon\":\"📊\",\"val\":\"
\",\"label\":\"Fact\"},{\"icon\":\"⚡\",\"val\":\"\",\"label\":\"Next Step\"}],\"takeaways\":[\"\",\"\",\"\"]}\n---END---\n\nText to summarize:\n${text.slice(0, 60000)}`;\n\n // Try selected model, fall back to others if unavailable\n const modelOrder = model === 'gpt'\n ? ['gpt', 'deepseek', 'claude']\n : model === 'deepseek'\n ? ['deepseek', 'gpt', 'claude']\n : ['claude', 'deepseek', 'gpt'];\n\n for (const m of modelOrder) {\n try {\n if (m === 'claude' && anthropicClient) {\n const resp = await anthropicClient.messages.create({\n model: 'claude-sonnet-4-20250514',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.content[0].text;\n }\n if (m === 'gpt' && openaiClient) {\n const resp = await openaiClient.chat.completions.create({\n model: 'gpt-4o-mini',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.choices[0].message.content;\n }\n if (m === 'deepseek' && deepseekClient) {\n const resp = await deepseekClient.chat.completions.create({\n model: 'deepseek-chat',\n max_tokens: 4096,\n messages: [{ role: 'user', content: prompt }]\n });\n return resp.choices[0].message.content;\n }\n } catch (err) {\n console.warn(`[AI SUMMARIZE] ${m} failed:`, err.message);\n continue;\n }\n }\n\n // All models failed — fall back to local text splitter\n console.warn('[AI SUMMARIZE] All models failed, using local fallback');\n return null;\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n // ── Enforce free plan limit (3 summaries per month)\n const userPlan = req.user.plan || 'free';\n if (userPlan === 'free') {\n const startOfMonth = new Date();\n startOfMonth.setDate(1);\n startOfMonth.setHours(0, 0, 0, 0);\n\n const { count, error: countErr } = await supabase\n .from('summaries')\n .select('id', { count: 'exact', head: true })\n .eq('user_id', req.user.id)\n .gte('created_at', startOfMonth.toISOString());\n\n if (!countErr && count >= 3) {\n return res.status(403).json({ error: 'Free plan limit reached (3 summaries/month). Upgrade to Pro for unlimited access.', upgradeRequired: true });\n }\n }\n\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n\n if (!text || !text.trim()) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n // Try AI summarization first, fall back to local text splitter\n let summaryText = await aiSummarize(text, style, occupation, language, model || 'claude');\n if (!summaryText) {\n summaryText = buildSummaryFromText(text, style, occupation);\n }\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Could not generate summary' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries/transcribe (Audio → Text via Whisper)\nrouter.post('/transcribe', authenticateToken, upload.single('audio'), async (req, res) => {\n if (!req.file) {\n return res.status(400).json({ error: 'No audio file uploaded' });\n }\n\n const filePath = req.file.path;\n\n try {\n if (!openaiClient) {\n return res.status(503).json({ error: 'Audio transcription is not configured. Set OPENAI_API_KEY in environment.' });\n }\n\n // Rename temp file to include original extension (Whisper needs it)\n const ext = path.extname(req.file.originalname).toLowerCase() || '.mp3';\n const namedPath = filePath + ext;\n fs.renameSync(filePath, namedPath);\n\n const transcription = await openaiClient.audio.transcriptions.create({\n file: fs.createReadStream(namedPath),\n model: 'whisper-1',\n response_format: 'text',\n });\n\n // Clean up temp file\n fs.unlink(namedPath, () => {});\n\n const text = typeof transcription === 'string' ? transcription : transcription.text || '';\n if (!text.trim()) {\n return res.status(422).json({ error: 'Could not transcribe audio. The file may be empty or inaudible.' });\n }\n\n res.json({ success: true, text });\n\n } catch (error) {\n // Clean up temp file on error\n try { fs.unlinkSync(filePath); } catch {}\n try { fs.unlinkSync(filePath + path.extname(req.file.originalname)); } catch {}\n\n console.error('[TRANSCRIBE ERROR]', error);\n\n if (error.status === 401) {\n return res.status(503).json({ error: 'Invalid OpenAI API key. Check OPENAI_API_KEY.' });\n }\n res.status(500).json({ error: 'Transcription failed: ' + (error.message || 'Unknown error') });\n }\n});\n\nmodule.exports = router;\n"}, "bfcc921234a65ceb7496fc315f1ef7f38c5d3375": {"t": 1785753928.7699668, "n": 9882, "h": "fe4993372c383f90", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst multer = require('multer');\nconst path = require('path');\nconst fs = require('fs');\nconst OpenAI = require('openai');\nconst supabase = require('../config/supabase');\n\n// ── Multer config for audio uploads (store in /tmp, max 25MB)\nconst upload = multer({\n dest: path.join(__dirname, '..', 'tmp'),\n limits: { fileSize: 100 * 1024 * 1024 },\n fileFilter: (req, file, cb) => {\n const allowed = ['.mp3', '.wav', '.m4a', '.ogg', '.webm', '.mp4', '.mpeg', '.mpga'];\n const ext = path.extname(file.originalname).toLowerCase();\n if (allowed.includes(ext)) cb(null, true);\n else cb(new Error('Unsupported audio format. Use MP3, WAV, M4A, or OGG.'));\n }\n});\n\n// ── OpenAI client (for Whisper transcription)\nconst openai = process.env.OPENAI_API_KEY\n ? new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n : null;\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n // ── Enforce free plan limit (3 summaries per month)\n const userPlan = req.user.plan || 'free';\n if (userPlan === 'free') {\n const startOfMonth = new Date();\n startOfMonth.setDate(1);\n startOfMonth.setHours(0, 0, 0, 0);\n\n const { count, error: countErr } = await supabase\n .from('summaries')\n .select('id', { count: 'exact', head: true })\n .eq('user_id', req.user.id)\n .gte('created_at', startOfMonth.toISOString());\n\n if (!countErr && count >= 3) {\n return res.status(403).json({ error: 'Free plan limit reached (3 summaries/month). Upgrade to Pro for unlimited access.', upgradeRequired: true });\n }\n }\n\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n const summaryText = buildSummaryFromText(text, style, occupation);\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries/transcribe (Audio → Text via Whisper)\nrouter.post('/transcribe', authenticateToken, upload.single('audio'), async (req, res) => {\n if (!req.file) {\n return res.status(400).json({ error: 'No audio file uploaded' });\n }\n\n const filePath = req.file.path;\n\n try {\n if (!openai) {\n return res.status(503).json({ error: 'Audio transcription is not configured. Set OPENAI_API_KEY in environment.' });\n }\n\n // Rename temp file to include original extension (Whisper needs it)\n const ext = path.extname(req.file.originalname).toLowerCase() || '.mp3';\n const namedPath = filePath + ext;\n fs.renameSync(filePath, namedPath);\n\n const transcription = await openai.audio.transcriptions.create({\n file: fs.createReadStream(namedPath),\n model: 'whisper-1',\n response_format: 'text',\n });\n\n // Clean up temp file\n fs.unlink(namedPath, () => {});\n\n const text = typeof transcription === 'string' ? transcription : transcription.text || '';\n if (!text.trim()) {\n return res.status(422).json({ error: 'Could not transcribe audio. The file may be empty or inaudible.' });\n }\n\n res.json({ success: true, text });\n\n } catch (error) {\n // Clean up temp file on error\n try { fs.unlinkSync(filePath); } catch {}\n try { fs.unlinkSync(filePath + path.extname(req.file.originalname)); } catch {}\n\n console.error('[TRANSCRIBE ERROR]', error);\n\n if (error.status === 401) {\n return res.status(503).json({ error: 'Invalid OpenAI API key. Check OPENAI_API_KEY.' });\n }\n res.status(500).json({ error: 'Transcription failed: ' + (error.message || 'Unknown error') });\n }\n});\n\nmodule.exports = router;\n"}, "117c10c77381acdd2c3ffc32eefa4d6d429ad87d": {"t": 1785753930.9575222, "n": 9881, "h": "dcbda06467dca86a", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst multer = require('multer');\nconst path = require('path');\nconst fs = require('fs');\nconst OpenAI = require('openai');\nconst supabase = require('../config/supabase');\n\n// ── Multer config for audio uploads (store in /tmp, max 25MB)\nconst upload = multer({\n dest: path.join(__dirname, '..', 'tmp'),\n limits: { fileSize: 25 * 1024 * 1024 },\n fileFilter: (req, file, cb) => {\n const allowed = ['.mp3', '.wav', '.m4a', '.ogg', '.webm', '.mp4', '.mpeg', '.mpga'];\n const ext = path.extname(file.originalname).toLowerCase();\n if (allowed.includes(ext)) cb(null, true);\n else cb(new Error('Unsupported audio format. Use MP3, WAV, M4A, or OGG.'));\n }\n});\n\n// ── OpenAI client (for Whisper transcription)\nconst openai = process.env.OPENAI_API_KEY\n ? new OpenAI({ apiKey: process.env.OPENAI_API_KEY })\n : null;\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n // ── Enforce free plan limit (3 summaries per month)\n const userPlan = req.user.plan || 'free';\n if (userPlan === 'free') {\n const startOfMonth = new Date();\n startOfMonth.setDate(1);\n startOfMonth.setHours(0, 0, 0, 0);\n\n const { count, error: countErr } = await supabase\n .from('summaries')\n .select('id', { count: 'exact', head: true })\n .eq('user_id', req.user.id)\n .gte('created_at', startOfMonth.toISOString());\n\n if (!countErr && count >= 3) {\n return res.status(403).json({ error: 'Free plan limit reached (3 summaries/month). Upgrade to Pro for unlimited access.', upgradeRequired: true });\n }\n }\n\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n const summaryText = buildSummaryFromText(text, style, occupation);\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries/transcribe (Audio → Text via Whisper)\nrouter.post('/transcribe', authenticateToken, upload.single('audio'), async (req, res) => {\n if (!req.file) {\n return res.status(400).json({ error: 'No audio file uploaded' });\n }\n\n const filePath = req.file.path;\n\n try {\n if (!openai) {\n return res.status(503).json({ error: 'Audio transcription is not configured. Set OPENAI_API_KEY in environment.' });\n }\n\n // Rename temp file to include original extension (Whisper needs it)\n const ext = path.extname(req.file.originalname).toLowerCase() || '.mp3';\n const namedPath = filePath + ext;\n fs.renameSync(filePath, namedPath);\n\n const transcription = await openai.audio.transcriptions.create({\n file: fs.createReadStream(namedPath),\n model: 'whisper-1',\n response_format: 'text',\n });\n\n // Clean up temp file\n fs.unlink(namedPath, () => {});\n\n const text = typeof transcription === 'string' ? transcription : transcription.text || '';\n if (!text.trim()) {\n return res.status(422).json({ error: 'Could not transcribe audio. The file may be empty or inaudible.' });\n }\n\n res.json({ success: true, text });\n\n } catch (error) {\n // Clean up temp file on error\n try { fs.unlinkSync(filePath); } catch {}\n try { fs.unlinkSync(filePath + path.extname(req.file.originalname)); } catch {}\n\n console.error('[TRANSCRIBE ERROR]', error);\n\n if (error.status === 401) {\n return res.status(503).json({ error: 'Invalid OpenAI API key. Check OPENAI_API_KEY.' });\n }\n res.status(500).json({ error: 'Transcription failed: ' + (error.message || 'Unknown error') });\n }\n});\n\nmodule.exports = router;\n"}, "e35b017aa6d20ab9b85d13a960ff527900f8f3ff": {"t": 1785753933.2090695, "n": 7436, "h": "7ac68ee4e635d76e", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst supabase = require('../config/supabase');\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n // ── Enforce free plan limit (3 summaries per month)\n const userPlan = req.user.plan || 'free';\n if (userPlan === 'free') {\n const startOfMonth = new Date();\n startOfMonth.setDate(1);\n startOfMonth.setHours(0, 0, 0, 0);\n\n const { count, error: countErr } = await supabase\n .from('summaries')\n .select('id', { count: 'exact', head: true })\n .eq('user_id', req.user.id)\n .gte('created_at', startOfMonth.toISOString());\n\n if (!countErr && count >= 3) {\n return res.status(403).json({ error: 'Free plan limit reached (3 summaries/month). Upgrade to Pro for unlimited access.', upgradeRequired: true });\n }\n }\n\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n const summaryText = buildSummaryFromText(text, style, occupation);\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\nmodule.exports = router;\n"}, "63f1c865033a46c9f5c77518c6eab8f8be94a31b": {"t": 1785753934.9597495, "n": 6751, "h": "322890b99fa3c86e", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst supabase = require('../config/supabase');\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple', occupation = 'general') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (sentences.length === 0) return cleaned;\n\n // Split into logical groups (intro, body, conclusion)\n const total = sentences.length;\n const introEnd = Math.max(1, Math.ceil(total * 0.2));\n const bodyEnd = Math.max(introEnd + 1, Math.ceil(total * 0.75));\n\n const intro = sentences.slice(0, introEnd);\n const body = sentences.slice(introEnd, bodyEnd);\n const conclusion = sentences.slice(bodyEnd);\n\n const parts = [];\n\n if (style === 'bullets') {\n parts.push('## Overview');\n intro.forEach(s => parts.push(`- ${s}`));\n if (body.length) {\n parts.push('');\n parts.push('## Key Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n conclusion.forEach(s => parts.push(`- ${s}`));\n }\n } else if (style === 'detailed') {\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Detailed Analysis');\n // Group body sentences into paragraphs of 3\n for (let i = 0; i < body.length; i += 3) {\n parts.push(body.slice(i, i + 3).join(' '));\n }\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Summary & Takeaways');\n parts.push(conclusion.join(' '));\n }\n } else {\n // simple\n parts.push('## Overview');\n parts.push(intro.join(' '));\n if (body.length) {\n parts.push('');\n parts.push('## Main Points');\n body.forEach(s => parts.push(`- ${s}`));\n }\n if (conclusion.length) {\n parts.push('');\n parts.push('## Conclusion');\n parts.push(conclusion.join(' '));\n }\n }\n\n return parts.join('\\n');\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n const summaryText = buildSummaryFromText(text, style, occupation);\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\nmodule.exports = router;\n"}, "616a430b60c8a24e83e0513d2a3254a8c7a6259c": {"t": 1785753936.164765, "n": 5239, "h": "a534b4aff6391760", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst supabase = require('../config/supabase');\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (style === 'bullets') {\n return sentences.slice(0, 6).map(s => `- ${s}`).join('\\n');\n }\n if (style === 'detailed') {\n return sentences.slice(0, 10).join(' ');\n }\n return sentences.slice(0, 4).join(' ');\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n const { title, text, language, occupation, style, model } = req.body;\n\n const fileName = title || `Summary ${new Date().toLocaleString()}`;\n const summaryText = buildSummaryFromText(text, style);\n\n if (!summaryText) {\n return res.status(400).json({ error: 'Text content is required' });\n }\n\n const wordCount = summaryText.trim().split(/\\s+/).length;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n file_name: fileName,\n file_type: 'text',\n file_size_kb: Math.round(new Blob([text || '']).size / 1024) || 0,\n language: language || 'english-simple',\n occupation: occupation || 'general',\n style: style || 'simple',\n word_count: wordCount,\n summary_text: summaryText,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { file_name, summary_text } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const updates = {};\n if (file_name) updates.file_name = file_name;\n if (summary_text) updates.summary_text = summary_text;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update(updates)\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\nmodule.exports = router;\n"}, "f64da5bdf1e2131313e713ceac46e4904b20e2ff": {"t": 1785753939.0990374, "n": 4905, "h": "b79db232161c636e", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst supabase = require('../config/supabase');\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\nfunction buildSummaryFromText(text, style = 'simple') {\n const cleaned = String(text || '').replace(/\\s+/g, ' ').trim();\n if (!cleaned) return '';\n\n const sentences = cleaned.split(/(?<=[.!?])\\s+/).filter(Boolean);\n if (style === 'bullets') {\n return sentences.slice(0, 6).map(s => `- ${s}`).join('\\n');\n }\n if (style === 'detailed') {\n return sentences.slice(0, 10).join(' ');\n }\n return sentences.slice(0, 4).join(' ');\n}\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n const { title, content, text, courseId, style } = req.body;\n\n const finalTitle = title || `Summary ${new Date().toLocaleString()}`;\n const finalContent = content || buildSummaryFromText(text, style);\n\n if (!finalTitle || !finalContent) {\n return res.status(400).json({ error: 'Title and content required' });\n }\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n title: finalTitle,\n content: finalContent,\n course_id: courseId || null,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { title, content } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update({\n title,\n content,\n updated_at: new Date().toISOString()\n })\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\nmodule.exports = router;\n"}, "8370feb95699a530a6be9bc09a1308d40cec2c21": {"t": 1785753940.7750916, "n": 4271, "h": "b842a17c864037c4", "c": "// ============================================================\n// routes/summaries.js - Learning Summaries Routes\n// ============================================================\n\nconst express = require('express');\nconst router = express.Router();\nconst jwt = require('jsonwebtoken');\nconst supabase = require('../config/supabase');\n\n// ── Middleware: Check JWT token\nconst authenticateToken = (req, res, next) => {\n const authHeader = req.headers['authorization'];\n const token = authHeader && authHeader.split(' ')[1];\n\n if (!token) {\n return res.status(401).json({ error: 'No token provided' });\n }\n\n jwt.verify(token, process.env.JWT_SECRET, (err, user) => {\n if (err) {\n return res.status(403).json({ error: 'Invalid token' });\n }\n req.user = user;\n next();\n });\n};\n\n// ── GET /api/summaries\nrouter.get('/', authenticateToken, async (req, res) => {\n try {\n const { data: summaries, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('user_id', req.user.id)\n .order('created_at', { ascending: false });\n\n if (error) throw error;\n\n res.json({ success: true, summaries: summaries || [] });\n\n } catch (error) {\n console.error('[GET SUMMARIES ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── GET /api/summaries/:id\nrouter.get('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .select('*')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (error || !summary) {\n return res.status(404).json({ error: 'Summary not found' });\n }\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[GET SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── POST /api/summaries\nrouter.post('/', authenticateToken, async (req, res) => {\n try {\n const { title, content, courseId } = req.body;\n\n if (!title || !content) {\n return res.status(400).json({ error: 'Title and content required' });\n }\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .insert([{\n user_id: req.user.id,\n title,\n content,\n course_id: courseId || null,\n created_at: new Date().toISOString()\n }])\n .select()\n .single();\n\n if (error) throw error;\n\n res.status(201).json({ success: true, summary });\n\n } catch (error) {\n console.error('[CREATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── PUT /api/summaries/:id\nrouter.put('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n const { title, content } = req.body;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { data: summary, error } = await supabase\n .from('summaries')\n .update({\n title,\n content,\n updated_at: new Date().toISOString()\n })\n .eq('id', id)\n .select()\n .single();\n\n if (error) throw error;\n\n res.json({ success: true, summary });\n\n } catch (error) {\n console.error('[UPDATE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\n// ── DELETE /api/summaries/:id\nrouter.delete('/:id', authenticateToken, async (req, res) => {\n try {\n const { id } = req.params;\n\n // Verify ownership\n const { data: existing } = await supabase\n .from('summaries')\n .select('id')\n .eq('id', id)\n .eq('user_id', req.user.id)\n .single();\n\n if (!existing) {\n return res.status(403).json({ error: 'Unauthorized' });\n }\n\n const { error } = await supabase\n .from('summaries')\n .delete()\n .eq('id', id);\n\n if (error) throw error;\n\n res.json({ success: true, message: 'Summary deleted' });\n\n } catch (error) {\n console.error('[DELETE SUMMARY ERROR]', error);\n res.status(500).json({ error: error.message });\n }\n});\n\nmodule.exports = router;\n"}, "651f569394abb69af97ab5a35e17f0bb4999ec4e": {"t": 1785753944.714404, "n": 8378, "h": "30e0738bd15d484a", "c": "// backend/routes/summaries.js — AI summarization endpoint\nconst router = require('express').Router();\nconst multer = require('multer');\nconst Anthropic = require('@anthropic-ai/sdk');\nconst OpenAI = require('openai');\nconst { requireUser, requirePlan } = require('../middleware/auth');\nconst db = require('../config/db');\n\nconst upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 100 * 1024 * 1024 } });\nconst claude = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });\nconst gpt = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\nconst deepseek = new OpenAI({ apiKey: process.env.DEEPSEEK_API_KEY, baseURL: 'https://api.deepseek.com' });\n\nconst LANG_MAP = {\n 'english-simple': 'simple, clear English with short sentences',\n 'hinglish': 'Hinglish — natural Hindi+English mix (e.g. \"Revenue badh gayi\", \"Growth ho rahi hai\")',\n 'hindi': 'pure Hindi in Devanagari script',\n 'spanish':'Spanish','french':'French','german':'German','arabic':'Arabic (العربية)',\n 'japanese':'Japanese (日本語)','korean':'Korean (한국어)','chinese':'Simplified Chinese',\n 'portuguese':'Brazilian Portuguese','italian':'Italian','russian':'Russian',\n 'bengali':'Bengali (বাংলা)','urdu':'Urdu (اردو)','tamil':'Tamil (தமிழ்)',\n 'telugu':'Telugu (తెలుగు)','marathi':'Marathi (मराठी)','dutch':'Dutch','swedish':'Swedish',\n};\n\nconst OCC_FOCUS = {\n student: 'definitions, concepts, exam key points, formulas, examples',\n business: 'revenue, KPIs, growth rates, profit margins, action items, deadlines',\n medical: 'diagnoses, symptoms, treatments, medications, dosages, prognosis',\n legal: 'clauses, obligations, deadlines, parties, penalties, jurisdiction',\n tech: 'APIs, specs, architecture, requirements, frameworks, performance',\n research: 'methodology, sample size, findings, statistics, conclusions',\n teacher: 'learning objectives, key concepts, examples, assessments',\n general: 'main ideas, important facts, conclusions',\n};\n\n// POST /api/summaries — create summary\nrouter.post('/', requireUser, upload.single('file'), async (req, res) => {\n try {\n const { language='english-simple', occupation='general', style='simple', text, model='gpt' } = req.body;\n\n // Check free quota\n if (req.user.plan === 'free') {\n const [[u]] = await db.execute('SELECT docs_this_month FROM users WHERE id=?', [req.user.sub]);\n if (u.docs_this_month >= 3) {\n return res.status(403).json({ error:'Free plan limit reached (3/month). Please upgrade.', upgradeRequired:true });\n }\n }\n\n let content = '';\n let fileName = 'pasted-text', fileType = 'text', fileSize = 0;\n\n if (req.file) {\n fileName = req.file.originalname;\n fileSize = Math.round(req.file.size / 1024);\n const ext = fileName.split('.').pop().toLowerCase();\n fileType = ['mp3','wav','m4a','ogg'].includes(ext) ? 'audio' : ['ppt','pptx'].includes(ext) ? 'ppt' : 'pdf';\n content = `[File: ${fileName}]\\n${req.file.buffer.toString('utf-8').substring(0,6000)}`;\n } else if (text) {\n content = text;\n } else {\n return res.status(400).json({ error:'Provide a file or text.' });\n }\n\n const styleInstr = {\n simple: 'Write a concise summary in 3-4 readable paragraphs.',\n detailed: 'Write a comprehensive summary in 6-8 paragraphs.',\n bullets: 'Write summary using ## headings and • bullet points.',\n };\n\n const prompt = `You are an expert summarizer for a ${occupation} professional. Summarize in ${LANG_MAP[language]||'simple English'}.\n\nOCCUPATION FOCUS — highlight: ${OCC_FOCUS[occupation]||OCC_FOCUS.general}\n${styleInstr[style]}\n\nAfter summary add exactly:\n---JSON---\n{\"highlights\":[{\"icon\":\"📊\",\"val\":\"VALUE\",\"label\":\"LABEL\"},{\"icon\":\"📅\",\"val\":\"VALUE\",\"label\":\"LABEL\"},{\"icon\":\"⚡\",\"val\":\"VALUE\",\"label\":\"LABEL\"}],\"takeaways\":[\"point1\",\"point2\",\"point3\"]}\n---END---\n\n${content.substring(0,7000)}`;\n\n // Stream response\n res.setHeader('Content-Type','text/plain; charset=utf-8');\n res.setHeader('X-Accel-Buffering','no');\n\n let full = '';\n\n try {\n console.log('[Summarize] Using model:', model, '| API Key set:', !!process.env.OPENAI_API_KEY);\n if (model === 'gpt') {\n console.log('[GPT] Calling OpenAI API...');\n const stream = await gpt.chat.completions.create({ model:'gpt-4o-mini', max_tokens:1200, messages:[{role:'user',content:prompt}], stream:true });\n console.log('[GPT] Stream started');\n for await (const chunk of stream) {\n const text = chunk.choices[0]?.delta?.content || '';\n if (text) { full += text; res.write(text); }\n }\n console.log('[GPT] Stream completed');\n } else if (model === 'deepseek') {\n console.log('[DeepSeek] Calling API...');\n const stream = await deepseek.chat.completions.create({ model:'deepseek-chat', max_tokens:1200, messages:[{role:'user',content:prompt}], stream:true });\n for await (const chunk of stream) {\n const text = chunk.choices[0]?.delta?.content || '';\n if (text) { full += text; res.write(text); }\n }\n console.log('[DeepSeek] Stream completed');\n } else {\n // default: claude\n console.log('[Claude] Calling API...');\n const stream = claude.messages.stream({ model:'claude-sonnet-4-6', max_tokens:1200, messages:[{role:'user',content:prompt}] });\n for await (const chunk of stream) {\n if (chunk.type==='content_block_delta' && chunk.delta?.text) {\n full += chunk.delta.text; res.write(chunk.delta.text);\n }\n }\n console.log('[Claude] Stream completed');\n }\n } catch (aiError) {\n console.error('[AI API Error]', {\n status: aiError.status,\n message: aiError.message,\n code: aiError.code,\n type: aiError.type,\n error: aiError.error,\n full: aiError.toString()\n });\n // Fallback to GPT if Claude fails\n if (model !== 'gpt') {\n console.log('[Fallback] Model', model, 'failed, trying GPT...');\n try {\n const stream = await gpt.chat.completions.create({ model:'gpt-4o-mini', max_tokens:1200, messages:[{role:'user',content:prompt}], stream:true });\n for await (const chunk of stream) {\n const text = chunk.choices[0]?.delta?.content || '';\n if (text) { full += text; res.write(text); }\n }\n console.log('[GPT Fallback] Succeeded');\n } catch (gptError) {\n console.error('[GPT Fallback Error]', gptError.status, gptError.message);\n throw gptError;\n }\n } else {\n throw aiError;\n }\n }\n\n res.end();\n\n // Save to DB (async, non-blocking)\n const words = full.replace(/---JSON---[\\s\\S]*?---END---/,'').trim().split(/\\s+/).length;\n db.execute(\n `INSERT INTO summaries (user_id,file_name,file_type,file_size_kb,language,occupation,style,word_count,summary_text) VALUES (?,?,?,?,?,?,?,?,?)`,\n [req.user.sub, fileName, fileType, fileSize, language, occupation, style, words, full.substring(0,60000)]\n ).then(() => {\n db.execute('UPDATE users SET docs_this_month=docs_this_month+1, total_docs=total_docs+1 WHERE id=?', [req.user.sub]);\n }).catch(console.error);\n\n } catch (e) {\n console.error('[Summarize] Outer catch', {\n message: e.message,\n status: e.status,\n code: e.code,\n stack: e.stack\n });\n if (!res.headersSent) {\n if (e.status === 401) {\n return res.status(401).json({ error:'API key invalid or expired.' });\n } else if (e.status === 429) {\n return res.status(429).json({ error:'Rate limit exceeded. Try again in 1 minute.' });\n } else if (e.code === 'ERR_MODULE_NOT_FOUND') {\n return res.status(500).json({ error:'Missing AI SDK. Check server config.' });\n } else {\n return res.status(500).json({ error:'Summarization failed: ' + (e.message || 'Unknown error') });\n }\n }\n }\n});\n\n// GET /api/summaries — list (admin or own)\nrouter.get('/', requireUser, async (req, res) => {\n try {\n const [rows] = await db.execute(\n 'SELECT id,file_name,language,occupation,style,word_count,created_at FROM summaries WHERE user_id=? ORDER BY created_at DESC LIMIT 50',\n [req.user.sub]\n );\n res.json({ summaries: rows });\n } catch { res.status(500).json({ error:'Fetch failed.' }); }\n});\n\nmodule.exports = router;\n"}}}}