diff --git a/backend/controllers/chatMessage.controller.js b/backend/controllers/chatMessage.controller.js index 971a14c..75ee452 100644 --- a/backend/controllers/chatMessage.controller.js +++ b/backend/controllers/chatMessage.controller.js @@ -28,16 +28,10 @@ if (MEM0_ENABLED) { } } -// Daily token budget tracked per user per UTC day. -const dailyBudgetKey = (userId) => `tokenBudget:${userId}:${new Date().toISOString().slice(0, 10)}`; - -const startOfUtcDay = () => { - const now = new Date(); - return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); -}; - -const secondsUntilUtcMidnight = () => - Math.ceil((startOfUtcDay().getTime() + 86400000 - Date.now()) / 1000); +// SSE helper — writes a properly-framed SSE event +function writeSSE(res, event, data) { + res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); +} const getAvailableModels = asyncHandler(async (req, res) => { const apikeys = await prisma.apiKey.findMany({ @@ -210,11 +204,18 @@ if (!chat.chatSources[0].isVectorLess) { limit: 20, with_payload: true, }); - - const [denseResults, keywordResults] = await Promise.all([denseTask, keywordTask]); - - if (denseResults?.points?.length) { - allDensePoints.push(...denseResults.points); + treeindex.loadData(docTree.sourceData); + treeindex.loadTree(docTree.treeData); + + relevantNodeIds = await treeindex.retrieveRelevantNodes(userPrompt); + if(relevantNodeIds.length == 0) { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache, no-transform"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("X-Accel-Buffering", "no"); + writeSSE(res, "error", { message: "No relevant sources found for this query" }); + res.end(); + return; } if (keywordResults?.points?.length) { @@ -305,7 +306,7 @@ if (!chat.chatSources[0].isVectorLess) { res.setHeader("Cache-Control", "no-cache, no-transform"); res.setHeader("Connection", "keep-alive"); res.setHeader("X-Accel-Buffering", "no"); - + let llmResponse = ""; let inputTokens = 0; let outputTokens = 0; @@ -317,17 +318,19 @@ if (!chat.chatSources[0].isVectorLess) { if (chunk.usage) { inputTokens = chunk.usage.prompt_tokens; outputTokens = chunk.usage.completion_tokens; + writeSSE(res, "usage", { inputTokens, outputTokens }); } if (content) { llmResponse += content; - res.write(content); + writeSSE(res, "chunk", { content }); } } - } catch (error) { - res.write(`\n\ndata: {"error": "Stream ended with error: ${error.message.replace(/\n/g, ' ')}"}\n\n`); - } finally { - res.end(); - } + writeSSE(res, "done", {}); + } catch (error) { + writeSSE(res, "error", { message: error.message || "Stream ended with error" }); + } finally { + res.end(); + } if (llmResponse.trim()) { if (MEM0_ENABLED && memory) { diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index 2ea20b5..a9c379d 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -361,101 +361,82 @@ const resetPassword = asyncHandler(async (req, res) => { }); const deleteMyData = asyncHandler(async (req, res) => { - const userId = req.user.id; // set by your auth middleware - const { confirm } = req.query; - - // Acceptance criteria: require explicit confirmation flag - if (confirm !== "true") { - return res.status(400).json({ - success: false, - message: "Pass confirm=true as a query param to confirm deletion.", + const userId = req.user.id; // set by your auth middleware + const { confirm } = req.query; + + // Acceptance criteria: require explicit confirmation flag + if (confirm !== "true") { + return res.status(400).json({ + success: false, + message: 'Pass confirm=true as a query param to confirm deletion.', + }); + } + + try { + await prisma.$transaction(async (tx) => { + // 1. Delete ChatMessageSources (deepest dependency first) + await tx.chatMessageSource.deleteMany({ + where: { chatMessage: { chat: { userId } } }, + }); + + // 2. Delete ChatMessages + await tx.chatMessage.deleteMany({ + where: { chat: { userId } }, + }); + + // 3. Delete UsageEvents (onDelete: SetNull means we must handle these) + await tx.usageEvent.deleteMany({ where: { userId } }); + + // 4. Delete ApiKeys + await tx.apiKey.deleteMany({ where: { userId } }); + + // 5. Delete ChatSources that belong ONLY to this user's chats + // Safe detach: only remove if no other user's chat references them + const userChatIds = ( + await tx.chat.findMany({ + where: { userId }, + select: { id: true }, + }) + ).map((c) => c.id); + + // Find ChatSource IDs used by this user's chats + const userChatSources = await tx.chatSource.findMany({ + where: { chatId: { in: userChatIds } }, + select: { id: true, docsUrl: true }, + }); + + // Only delete ChatSources not referenced by any other chat + for (const cs of userChatSources) { + const otherChatCount = await tx.chat.count({ + where: { + chatSources: { some: { docsUrl: cs.docsUrl } }, + userId: { not: userId }, + }, }); - } - - try { - await prisma.$transaction(async (tx) => { - // 1. Delete ChatMessageSources (deepest dependency first) - await tx.chatMessageSource.deleteMany({ - where: { chatMessage: { chat: { userId } } }, - }); - - // 2. Delete ChatMessages - await tx.chatMessage.deleteMany({ - where: { chat: { userId } }, - }); - - // 3. Delete UsageEvents (onDelete: SetNull means we must handle these) - await tx.usageEvents.deleteMany({ where: { userId } }); - - // 4. Delete ApiKeys - await tx.apiKey.deleteMany({ where: { userId } }); - - // 5. Delete ChatSources that belong ONLY to this user's chats - // Safe detach: only remove if no other user's chat references them - const userChatIds = ( - await tx.chat.findMany({ - where: { userId }, - select: { id: true }, - }) - ).map((c) => c.id); - - // Find ChatSources used by this user's chats - const userChatSources = await tx.chatSource.findMany({ - where: { - chats: { - some: { - id: { - in: userChatIds, - }, - }, - }, - }, - select: { - id: true, - documentationUrl: true, - lastIndexedAt: true, - }, - }); - - // Only delete ChatSources not referenced by any other user's chats - for (const cs of userChatSources) { - const otherChatCount = await tx.chat.count({ - where: { - userId: { not: userId }, - chatSources: { - some: { - documentationUrl: cs.documentationUrl, - }, - }, - }, - }); - - if (otherChatCount === 0) { - await tx.chatSource.delete({ - where: { id: cs.id }, - }); - } - } + if (otherChatCount === 0) { + await tx.chatSource.delete({ where: { id: cs.id } }); + } + } - // 6. Delete Chats - await tx.chat.deleteMany({ where: { userId } }); - }); + // 6. Delete Chats + await tx.chat.deleteMany({ where: { userId } }); + }); - return res.status(200).json({ - success: true, - message: "All your data has been deleted.", - }); - } catch (error) { - // Idempotent: if already deleted, return success - if (error.code === "P2025") { - return res.status(200).json({ - success: true, - message: "Data already deleted or not found.", - }); - } - console.error("deleteMyData error:", error); - return res.status(500).json({ success: false, message: "Server error." }); + return res.status(200).json({ + success: true, + message: 'All your data has been deleted.', + }); + } catch (error) { + // Idempotent: if already deleted, return success + if (error.code === 'P2025') { + return res.status(200).json({ + success: true, + message: 'Data already deleted or not found.', + }); } + console.error('deleteMyData error:', error); + return res.status(500).json({ success: false, message: 'Server error.' }); + } }); export { diff --git a/src/lib/api.ts b/src/lib/api.ts index c9835a3..3dcabd3 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -388,13 +388,15 @@ export const getMessageSources = (messageId: string) => }), ); + export const sendMessageStream = async (payload: { userPrompt: string; model: string; provider: string; chatId: string; onChunk?: (chunk: string) => void; - signal?: AbortSignal; + onError?: (message: string) => void; + signal?: AbortSignal; }) => { const token = getAccessToken(); const headers = new Headers({ "Content-Type": "application/json" }); @@ -406,8 +408,13 @@ export const sendMessageStream = async (payload: { method: "POST", headers, credentials: "include", - body: JSON.stringify(payload), - signal: payload.signal, + body: JSON.stringify({ + userPrompt: payload.userPrompt, + model: payload.model, + provider: payload.provider, + chatId: payload.chatId, + }), + signal: payload.signal, }); if (!response.ok || !response.body) { @@ -418,25 +425,54 @@ export const sendMessageStream = async (payload: { throw new Error(payload?.message || "Unable to send message"); } + // AFTER: const reader = response.body.getReader(); const decoder = new TextDecoder(); let text = ""; - + let buffer = ""; + while (true) { const { done, value } = await reader.read(); if (done) break; - const chunk = decoder.decode(value, { stream: true }); - if (!chunk) continue; - text += chunk; - payload.onChunk?.(chunk); - } - - const tail = decoder.decode(); - if (tail) { - text += tail; - payload.onChunk?.(tail); + buffer += decoder.decode(value, { stream: true }); + + // SSE frames are separated by double newlines + const frames = buffer.split("\n\n"); + buffer = frames.pop() ?? ""; // last partial frame stays in buffer + + for (const frame of frames) { + if (!frame.trim()) continue; + + let eventName = "message"; + let dataLine = ""; + + for (const line of frame.split("\n")) { + if (line.startsWith("event: ")) eventName = line.slice(7).trim(); + else if (line.startsWith("data: ")) dataLine = line.slice(6).trim(); + } + + if (!dataLine) continue; + + let parsed: Record; + try { + parsed = JSON.parse(dataLine); + } catch { + continue; + } + + if (eventName === "chunk" && typeof parsed.content === "string") { + text += parsed.content; + payload.onChunk?.(parsed.content); + } else if (eventName === "error") { + const msg = typeof parsed.message === "string" ? parsed.message : "Stream error"; + payload.onError?.(msg); + payload.onChunk?.(msg); // surface it in the UI + + } + // "usage" and "done" events — no UI action needed, silently consumed + } } - + invalidateChatMessages(payload.chatId); return text; }; @@ -755,17 +791,3 @@ export const forkSharedChat = (shareToken: string) => export const deleteMyData = () => apiRequest<{ message: string }>("/user/delete-my-data?confirm=true", { method: "DELETE" }); - -export const adminImpersonate = (userId: string) => - apiRequest<{ - accessToken: string; - user: { - id: string; - fullname?: string | null; - username?: string | null; - email?: string | null; - }; - }>(`/admin/impersonate/${userId}`, { method: "POST" }); - -export const adminStopImpersonation = () => - apiRequest("/admin/stop-impersonation", { method: "POST" }); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 6518755..96899e7 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,6 +1,9 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Sidebar } from "../components/Sidebar"; + + + import { User, Mail, LogOut, Save, Key, CheckCircle2, Trash2, Pencil, X } from "lucide-react"; import { logoutUser, forceSignOut } from "../lib/auth"; import { getUserProfile, deleteMyData } from "../lib/api";