feat: Meeting Assistant MVP - Complete implementation
Enterprise Meeting Knowledge Management System with: Backend (FastAPI): - Authentication proxy with JWT (pj-auth-api integration) - MySQL database with 4 tables (users, meetings, conclusions, actions) - Meeting CRUD with system code generation (C-YYYYMMDD-XX, A-YYYYMMDD-XX) - Dify LLM integration for AI summarization - Excel export with openpyxl - 20 unit tests (all passing) Client (Electron): - Login page with company auth - Meeting list with create/delete - Meeting detail with real-time transcription - Editable transcript textarea (single block, easy editing) - AI summarization with conclusions/action items - 5-second segment recording (efficient for long meetings) Sidecar (Python): - faster-whisper medium model with int8 quantization - ONNX Runtime VAD (lightweight, ~20MB vs PyTorch ~2GB) - Chinese punctuation processing - OpenCC for Traditional Chinese conversion - Anti-hallucination parameters - Auto-cleanup of temp audio files OpenSpec: - add-meeting-assistant-mvp (47 tasks, archived) - add-realtime-transcription (29 tasks, archived) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
149
client/src/services/api.js
Normal file
149
client/src/services/api.js
Normal file
@@ -0,0 +1,149 @@
|
||||
const API_BASE_URL = "http://localhost:8000/api";
|
||||
|
||||
let authToken = null;
|
||||
let tokenRefreshTimer = null;
|
||||
|
||||
export function setToken(token) {
|
||||
authToken = token;
|
||||
localStorage.setItem("authToken", token);
|
||||
scheduleTokenRefresh();
|
||||
}
|
||||
|
||||
export function getToken() {
|
||||
if (!authToken) {
|
||||
authToken = localStorage.getItem("authToken");
|
||||
}
|
||||
return authToken;
|
||||
}
|
||||
|
||||
export function clearToken() {
|
||||
authToken = null;
|
||||
localStorage.removeItem("authToken");
|
||||
if (tokenRefreshTimer) {
|
||||
clearTimeout(tokenRefreshTimer);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleTokenRefresh() {
|
||||
// Refresh token 5 minutes before expiry (assuming 24h token)
|
||||
const refreshIn = 23 * 60 * 60 * 1000; // 23 hours
|
||||
if (tokenRefreshTimer) {
|
||||
clearTimeout(tokenRefreshTimer);
|
||||
}
|
||||
tokenRefreshTimer = setTimeout(async () => {
|
||||
try {
|
||||
// Re-login would require stored credentials
|
||||
// For now, just notify user to re-login
|
||||
console.warn("Token expiring soon, please re-login");
|
||||
} catch (error) {
|
||||
console.error("Token refresh failed:", error);
|
||||
}
|
||||
}, refreshIn);
|
||||
}
|
||||
|
||||
async function request(endpoint, options = {}) {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
const error = await response.json();
|
||||
if (error.detail?.error === "token_expired") {
|
||||
clearToken();
|
||||
window.electronAPI.navigate("login");
|
||||
throw new Error("Session expired, please login again");
|
||||
}
|
||||
throw new Error(error.detail || "Unauthorized");
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.detail || `HTTP error ${response.status}`);
|
||||
}
|
||||
|
||||
// Handle blob responses for export
|
||||
if (options.responseType === "blob") {
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Auth API
|
||||
export async function login(email, password) {
|
||||
const data = await request("/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
setToken(data.token);
|
||||
localStorage.setItem("userEmail", data.email);
|
||||
localStorage.setItem("userRole", data.role);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getMe() {
|
||||
return request("/me");
|
||||
}
|
||||
|
||||
// Meetings API
|
||||
export async function getMeetings() {
|
||||
return request("/meetings");
|
||||
}
|
||||
|
||||
export async function getMeeting(id) {
|
||||
return request(`/meetings/${id}`);
|
||||
}
|
||||
|
||||
export async function createMeeting(meeting) {
|
||||
return request("/meetings", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(meeting),
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateMeeting(id, meeting) {
|
||||
return request(`/meetings/${id}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(meeting),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteMeeting(id) {
|
||||
return request(`/meetings/${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateActionItem(meetingId, actionId, data) {
|
||||
return request(`/meetings/${meetingId}/actions/${actionId}`, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// AI API
|
||||
export async function summarizeTranscript(transcript) {
|
||||
return request("/ai/summarize", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ transcript }),
|
||||
});
|
||||
}
|
||||
|
||||
// Export API
|
||||
export async function exportMeeting(id) {
|
||||
return request(`/meetings/${id}/export`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user