Skip to content
Open

test #21

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/env.example
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
188 changes: 188 additions & 0 deletions backend/src/index.js
Original file line number Diff line number Diff line change
@@ -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);
});
45 changes: 45 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
6 changes: 6 additions & 0 deletions frontend/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
37 changes: 37 additions & 0 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<Link to="/" className="flex items-center space-x-2 text-gray-900 hover:text-gray-700">
<FileText className="h-8 w-8 text-primary-600" />
<span className="text-xl font-bold">AI Meeting Digest</span>
</Link>
</div>
<nav className="flex space-x-8">
<Link
to="/"
className="flex items-center space-x-1 text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
<Home className="h-4 w-4" />
<span>Home</span>
</Link>
<Link
to="/history"
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
History
</Link>
</nav>
</div>
</div>
</header>
);
};

export default Header;
34 changes: 34 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
@@ -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;
}
16 changes: 16 additions & 0 deletions frontend/src/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
Loading