From 918d9d82ea37272fc1927c7e50b93a6e1962a386 Mon Sep 17 00:00:00 2001 From: xudongzhangzhang Date: Sat, 16 Aug 2025 15:51:10 +0800 Subject: [PATCH] zhangxudong design a AI meeting system, include front and backend --- CANDIDATE_README.md | 97 ++++++++++++++++++++++++++ README.md | 1 + frontend/package.json | 21 ++++++ frontend/postcss.config.js | 6 ++ frontend/src/App.jsx | 23 +++++++ frontend/src/index.css | 3 + frontend/src/main.jsx | 13 ++++ frontend/src/pages/HistoryPage.jsx | 54 +++++++++++++++ frontend/src/pages/HomePage.jsx | 106 +++++++++++++++++++++++++++++ frontend/src/pages/SummaryPage.jsx | 53 +++++++++++++++ frontend/tailwind.config.js | 7 ++ 11 files changed, 384 insertions(+) create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/App.jsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.jsx create mode 100644 frontend/src/pages/HistoryPage.jsx create mode 100644 frontend/src/pages/HomePage.jsx create mode 100644 frontend/src/pages/SummaryPage.jsx create mode 100644 frontend/tailwind.config.js diff --git a/CANDIDATE_README.md b/CANDIDATE_README.md index e69de29..07de75c 100644 --- a/CANDIDATE_README.md +++ b/CANDIDATE_README.md @@ -0,0 +1,97 @@ +### 1. Technology Choices + +* **Frontend:** `[React + Vite + Tailwind CSS + React Router]` +* **Backend:** `[FastAPI (Python 3.10+) + Uvicorn + SQLAlchemy]` +* **Database:** `[SQLite (dev) via SQLAlchemy ORM]` +* **AI Service:** `[Google Gemini API (streaming)]` + +Briefly explain why you chose this stack. +* React + Vite + Tailwind: fast dev experience (HMR), minimal styling overhead, clean responsive UI; React Router keeps routes simple (/, /history, /digest/:publicId). + +* FastAPI: type-hinted, high performance, great auto docs (/docs), straightforward Server-Sent Events (SSE) via StreamingResponse. + +* SQLite: zero-config and file-based, ideal for a take-home; easily swappable to Postgres/MySQL through SQLAlchemy. + +* Gemini: strong long-text summarization and streaming support; generous free tier; can be swapped out if needed. +### 2. How to Run the Project + +Provide clear, step-by-step instructions for how to get your project running locally. + +Frontend and backend run as separate processes. Default backend: http://localhost:8000. Default frontend: http://localhost:5173. +#### 2.1 Backend (FastAPI) +##### 1. Install dependencies +```bash +cd backend +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt + +``` +##### 2. Configure environment +```bash +export GEMINI_API_KEY="" +``` +#### 3. Start the server +```bash +uvicorn main:app --reload --port 8000 +``` +#### 4. Open API docs +Swagger UI: http://localhost:8000/docs +#### 5. Key endpoints +* POST /summaries — create a job from transcript; returns { id, public_id } +* GET /summaries/{id}/stream — SSE stream of the AI summary; persists on completion +* GET /summaries — list past digests with overview snippets and timestamps +* GET /summaries/{public_id} — fetch a single digest by shareable public_id + +#### 2.2 Frontend (React + Vite) +##### 1. Install dependencies +```bash +cd frontend +npm install +``` +##### 2. Configure environment +* Create frontend/.env if backend URL differs: +```bash +VITE_API_BASE_URL=http://localhost:8000 +``` +#### 3. Start the server +```bash +npm run dev +``` +#### 4. Use the app +Swagger UI: http://localhost:8000/docs +#### 5. Key endpoints +* Home: paste transcript → click Generate Summary → watch streaming output (Overview / Key Decisions / Action Items). On finish, a share link appears. +* History: see previously generated digests (timestamp + overview snippet); copy share links. +* Share page: /digest/:publicId shows a single digest (viewable by anyone with the link). + +### 3. Design Decisions & Trade-offs + +Explain any significant architectural or design decisions you made. What were the trade-offs? If you implemented the challenge features, describe your approach. What would you do differently if you had more time? + +#### 3.1 Two-step flow (create + stream) +* Create the record via POST /summaries (persist transcript, return id and public_id), then stream via GET /summaries/{id}/stream. +* Pros: clear lifecycle; better UX/error isolation; easy to persist final summary after streaming. +* Cons: one extra request—acceptable for clarity and robustness. + +#### 3.2 SSE vs WebSockets +* Chose SSE for simplicity: native EventSource in browsers; minimal server code. +* Trade-off: unidirectional (server → client). If future features need duplex or collaborative editing, switch to WebSockets. +#### 3.3 Output shaping +* Prompt enforces three sections: Overview, Key Decisions, Action Items. +* Initially streams raw text for immediacy; persists the full text at the end. +* If more time: move to structured JSON (response schema / function calling) and store fields separately for search/analytics. +#### 3.4 IDs and share links +* Internal numeric id + external public_id (UUID) for shareable URLs. +* Pros: non-guessable public links; simple read-only sharing. +* Cons: demo omits auth. In production, add auth/ACL for sensitive transcripts. +#### 3.5 Extensibility and deployment +* DB swap to Postgres by changing the SQLAlchemy URL. +* For multi-instance streaming, consider sticky sessions or a shared pub/sub (e.g., Redis). +* Production: reverse proxy with TLS, observability (structured logs, tracing), and stricter CORS. + +### 4. AI Usage Log + +Describe how you used AI programming assistants during this project. Be specific! +SSE boilerplate and edge cases +Used an AI assistant to scaffold a minimal FastAPI StreamingResponse endpoint, then refined to include a final [DONE], error propagation, and DB persistence after streaming. \ No newline at end of file diff --git a/README.md b/README.md index a9f54df..7287719 100644 --- a/README.md +++ b/README.md @@ -133,3 +133,4 @@ Explain any significant architectural or design decisions you made. What were th ### 4. AI Usage Log Describe how you used AI programming assistants during this project. Be specific! + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..f3877f5 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "ai-meeting-digest", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.15.0", + "tailwindcss": "^3.3.0", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.21" + }, + "devDependencies": { + "vite": "^4.3.9" + } +} diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..5cbc2c7 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..d6b0c3d --- /dev/null +++ b/frontend/src/App.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { Routes, Route, Link } from 'react-router-dom'; +import HomePage from './pages/HomePage.jsx'; +import HistoryPage from './pages/HistoryPage.jsx'; +import SummaryPage from './pages/SummaryPage.jsx'; + +export default function App() { + return ( +
+ {/* Simple navigation links */} + + + + } /> + } /> + } /> + +
+ ); +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..9c682c1 --- /dev/null +++ b/frontend/src/main.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App.jsx'; +import './index.css'; /* Import Tailwind CSS styles */ + +ReactDOM.createRoot(document.getElementById('root')).render( + + + + + +); diff --git a/frontend/src/pages/HistoryPage.jsx b/frontend/src/pages/HistoryPage.jsx new file mode 100644 index 0000000..21573dd --- /dev/null +++ b/frontend/src/pages/HistoryPage.jsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; +const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; + +// The history list displays each summary with its creation timestamp and an overview snippet. +// Each entry has a "View" link (navigating to the summary’s detail page) and a "Copy Link" button to copy the share URL. + +export default function HistoryPage() { + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Fetch all past summaries on mount + fetch(`${API_BASE}/summaries`) + .then(res => res.json()) + .then(data => { + setHistory(data); + }) + .catch(err => console.error("Failed to load history:", err)) + .finally(() => setLoading(false)); + }, []); + + const handleCopy = (publicId) => { + const url = `${window.location.origin}/digest/${publicId}`; + navigator.clipboard.writeText(url); + alert("Copied share link for summary ID " + publicId); + }; + + return ( +
+

Summary History

+ {loading ? ( +

Loading summaries...

+ ) : history.length === 0 ? ( +

No summaries found.

+ ) : ( +
    + {history.map(item => ( +
  • +
    + [{new Date(item.created_at).toLocaleString()}] + {item.overview_snippet} +
    +
    + View + +
    +
  • + ))} +
+ )} +
+ ); +} diff --git a/frontend/src/pages/HomePage.jsx b/frontend/src/pages/HomePage.jsx new file mode 100644 index 0000000..e8b2bec --- /dev/null +++ b/frontend/src/pages/HomePage.jsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; + +const API_BASE = import.meta.env.VITE_API_BASE_URL || "http://localhost:8000"; + +export default function HomePage() { + const [transcript, setTranscript] = useState(""); + const [summary, setSummary] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [shareLink, setShareLink] = useState(""); + + const handleGenerate = async () => { + if (!transcript.trim()) return; + setSummary(""); + setShareLink(""); + setIsLoading(true); + + try { + // 1. Send transcript to backend to create a summary record + const res = await fetch(`${API_BASE}/summaries`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ transcript }) + }); + const data = await res.json(); + const { id, public_id } = data; + if (!id) throw new Error("Failed to create summary task"); + + // 2. Open SSE connection for streaming summary + const evtSource = new EventSource(`${API_BASE}/summaries/${id}/stream`); + evtSource.onmessage = (event) => { + if (event.data === "[DONE]") { + // Streaming finished + evtSource.close(); + setIsLoading(false); + // Prepare shareable link (frontend route) + if (public_id) { + const url = `${window.location.origin}/digest/${public_id}`; + setShareLink(url); + } + } else { + // Append incoming chunk to summary text + setSummary(prev => prev + event.data); + } + }; + evtSource.onerror = (err) => { + console.error("SSE error:", err); + evtSource.close(); + setIsLoading(false); + }; + } catch (err) { + console.error("Error generating summary:", err); + alert("Failed to generate summary. Please try again."); + setIsLoading(false); + } + }; + + const handleCopyLink = () => { + if (shareLink) { + navigator.clipboard.writeText(shareLink); + alert("Share link copied to clipboard!"); + } + }; + + return ( +
+

AI Meeting Digest

+