diff --git a/backend/env.example b/backend/env.example new file mode 100644 index 0000000..23a74fd --- /dev/null +++ b/backend/env.example @@ -0,0 +1,9 @@ +# Google Gemini API Key +GEMINI_API_KEY=your_gemini_api_key_here + +# Server Configuration +PORT=3001 +FRONTEND_URL=http://localhost:3000 + +# Database (SQLite file will be created automatically) +# No additional configuration needed for SQLite \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..5fb912c --- /dev/null +++ b/backend/package.json @@ -0,0 +1,28 @@ +{ + "name": "ai-meeting-digest-backend", + "version": "1.0.0", + "description": "Backend for AI Meeting Digest application", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "nodemon src/index.js", + "test": "jest" + }, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "sqlite3": "^5.1.6", + "uuid": "^9.0.1", + "@google/generative-ai": "^0.2.1", + "dotenv": "^16.3.1", + "helmet": "^7.1.0", + "express-rate-limit": "^7.1.5" + }, + "devDependencies": { + "nodemon": "^3.0.2", + "jest": "^29.7.0" + }, + "keywords": ["ai", "meeting", "digest", "api"], + "author": "Candidate", + "license": "MIT" +} \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..98df420 --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,188 @@ +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const rateLimit = require('express-rate-limit'); +const { GoogleGenerativeAI } = require('@google/generative-ai'); +const sqlite3 = require('sqlite3').verbose(); +const { v4: uuidv4 } = require('uuid'); +require('dotenv').config(); + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Initialize Google AI +const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY); + +// Database setup +const db = new sqlite3.Database('./digests.db'); + +// Create table if not exists +db.serialize(() => { + db.run(`CREATE TABLE IF NOT EXISTS digests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + public_id TEXT UNIQUE NOT NULL, + transcript TEXT NOT NULL, + summary TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + )`); +}); + +// Middleware +app.use(helmet()); +app.use(cors({ + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true +})); + +// Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100 // limit each IP to 100 requests per windowMs +}); +app.use(limiter); + +app.use(express.json({ limit: '10mb' })); + +// AI prompt for generating digest +const generateDigestPrompt = (transcript) => ` +Please analyze the following meeting transcript and provide a structured summary in the following JSON format: + +{ + "overview": "A brief, one-paragraph overview of the meeting", + "keyDecisions": ["Decision 1", "Decision 2", "Decision 3"], + "actionItems": [ + { + "task": "Action item description", + "assignee": "Person responsible" + } + ] +} + +Meeting Transcript: +${transcript} + +Please ensure the response is valid JSON and includes all three sections: overview, keyDecisions (as an array), and actionItems (as an array of objects with task and assignee properties). +`; + +// Routes +app.post('/api/digests', async (req, res) => { + try { + const { transcript } = req.body; + + if (!transcript || transcript.trim().length === 0) { + return res.status(400).json({ error: 'Transcript is required' }); + } + + if (transcript.length > 50000) { + return res.status(400).json({ error: 'Transcript too long (max 50,000 characters)' }); + } + + // Generate AI summary + const model = genAI.getGenerativeModel({ model: "gemini-pro" }); + const result = await model.generateContent(generateDigestPrompt(transcript)); + const response = await result.response; + const summaryText = response.text(); + + // Parse JSON response + let summary; + try { + summary = JSON.parse(summaryText); + } catch (error) { + // Fallback if AI doesn't return valid JSON + summary = { + overview: summaryText, + keyDecisions: [], + actionItems: [] + }; + } + + // Generate public ID + const publicId = uuidv4(); + + // Save to database + const stmt = db.prepare(` + INSERT INTO digests (public_id, transcript, summary) + VALUES (?, ?, ?) + `); + + stmt.run(publicId, transcript, JSON.stringify(summary), function(err) { + if (err) { + console.error('Database error:', err); + return res.status(500).json({ error: 'Failed to save digest' }); + } + + res.json({ + id: this.lastID, + publicId, + summary, + createdAt: new Date().toISOString() + }); + }); + + stmt.finalize(); + } catch (error) { + console.error('Error generating digest:', error); + res.status(500).json({ error: 'Failed to generate digest' }); + } +}); + +// Get all digests +app.get('/api/digests', (req, res) => { + db.all(` + SELECT id, public_id as publicId, summary, created_at as createdAt + FROM digests + ORDER BY created_at DESC + `, [], (err, rows) => { + if (err) { + console.error('Database error:', err); + return res.status(500).json({ error: 'Failed to fetch digests' }); + } + + const digests = rows.map(row => ({ + ...row, + summary: JSON.parse(row.summary) + })); + + res.json(digests); + }); +}); + +// Get single digest by public ID +app.get('/api/digests/:publicId', (req, res) => { + const { publicId } = req.params; + + db.get(` + SELECT id, public_id as publicId, transcript, summary, created_at as createdAt + FROM digests + WHERE public_id = ? + `, [publicId], (err, row) => { + if (err) { + console.error('Database error:', err); + return res.status(500).json({ error: 'Failed to fetch digest' }); + } + + if (!row) { + return res.status(404).json({ error: 'Digest not found' }); + } + + res.json({ + ...row, + summary: JSON.parse(row.summary) + }); + }); +}); + +// Health check +app.get('/api/health', (req, res) => { + res.json({ status: 'OK', timestamp: new Date().toISOString() }); +}); + +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); + +// Graceful shutdown +process.on('SIGINT', () => { + db.close(); + process.exit(0); +}); \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..580b335 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,45 @@ +{ + "name": "ai-meeting-digest-frontend", + "version": "1.0.0", + "private": true, + "dependencies": { + "@types/node": "^20.10.0", + "@types/react": "^18.2.45", + "@types/react-dom": "^18.2.18", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.1", + "react-scripts": "5.0.1", + "typescript": "^5.3.3", + "axios": "^1.6.2", + "lucide-react": "^0.294.0", + "tailwindcss": "^3.3.6", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "proxy": "http://localhost:3001" +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..0cc9a9d --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} \ No newline at end of file diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..82eec7a --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { FileText, Home } from 'lucide-react'; + +const Header: React.FC = () => { + return ( + + + + + + + AI Meeting Digest + + + + + + Home + + + History + + + + + + ); +}; + +export default Header; \ No newline at end of file diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..2aa5278 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,34 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #a8a8a8; +} \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..0db5c8c --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import './index.css'; +import App from './App'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement +); +root.render( + + + + + +); \ No newline at end of file diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..a579c4f --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,37 @@ +import axios from 'axios'; +import { Digest, DigestDetail } from '../types'; + +const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api'; + +const api = axios.create({ + baseURL: API_BASE_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +export const digestService = { + // Create a new digest + async createDigest(transcript: string): Promise { + const response = await api.post('/digests', { transcript }); + return response.data; + }, + + // Get all digests + async getDigests(): Promise { + const response = await api.get('/digests'); + return response.data; + }, + + // Get a single digest by public ID + async getDigest(publicId: string): Promise { + const response = await api.get(`/digests/${publicId}`); + return response.data; + }, + + // Health check + async healthCheck(): Promise<{ status: string; timestamp: string }> { + const response = await api.get('/health'); + return response.data; + }, +}; \ No newline at end of file diff --git a/frontend/src/types.ts b/frontend/src/types.ts new file mode 100644 index 0000000..2424585 --- /dev/null +++ b/frontend/src/types.ts @@ -0,0 +1,22 @@ +export interface Digest { + id: number; + publicId: string; + summary: { + overview: string; + keyDecisions: string[]; + actionItems: { + task: string; + assignee: string; + }[]; + }; + createdAt: string; +} + +export interface DigestDetail extends Digest { + transcript: string; +} + +export interface ApiResponse { + data?: T; + error?: string; +} \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..04952b2 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,19 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#eff6ff', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + } + } + }, + }, + plugins: [], +} \ No newline at end of file