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>
150 lines
3.4 KiB
JavaScript
150 lines
3.4 KiB
JavaScript
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",
|
|
});
|
|
}
|