From 905d43e9b8bbf6f5a7c3136bca589af20dbe72d7 Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Wed, 15 Apr 2026 16:58:18 +0530 Subject: [PATCH 1/4] feat: implement memory management system with conflict detection and resolution services --- docker-compose.cloud.yml | 26 ++++ server/package-lock.json | 107 +++++-------- server/package.json | 1 + server/src/middleware/rateLimit.ts | 13 ++ server/src/repositories/memory.repository.ts | 14 +- server/src/routes/memorie.route.ts | 11 +- .../services/conflict-detection.service.ts | 26 ++-- server/src/services/memory.service.ts | 140 ++++++++++-------- 8 files changed, 193 insertions(+), 145 deletions(-) diff --git a/docker-compose.cloud.yml b/docker-compose.cloud.yml index f90c127..05a91d8 100644 --- a/docker-compose.cloud.yml +++ b/docker-compose.cloud.yml @@ -16,6 +16,13 @@ services: - "3000:3000" networks: - neuramemory-network + deploy: + resources: + limits: + cpus: '1.0' + memory: 2G + reservations: + memory: 512M postgres: image: postgres:16-alpine @@ -38,6 +45,11 @@ services: timeout: 5s retries: 5 start_period: 10s + deploy: + resources: + limits: + cpus: '0.3' + memory: 512M qdrant: image: qdrant/qdrant:latest @@ -50,6 +62,13 @@ services: - qdrant_data:/qdrant/storage networks: - neuramemory-network + deploy: + resources: + limits: + cpus: '0.75' + memory: 2560M + reservations: + memory: 512M crawl4ai: image: unclecode/crawl4ai:latest @@ -58,6 +77,13 @@ services: shm_size: '1gb' networks: - neuramemory-network + deploy: + resources: + limits: + cpus: '1.0' + memory: 2G + reservations: + memory: 1G volumes: postgres_data: diff --git a/server/package-lock.json b/server/package-lock.json index c78074d..e4a395e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,6 +24,7 @@ "mammoth": "^1.12.0", "multer": "^2.1.1", "openai": "^6.27.0", + "p-limit": "^7.3.0", "pdfjs-dist": "^4.6.82", "pg": "^8.20.0", "swagger-ui-express": "^5.0.1", @@ -924,9 +925,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -947,9 +945,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -970,9 +965,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -993,9 +985,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1016,9 +1005,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1249,9 +1235,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1268,9 +1251,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1287,9 +1267,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1306,9 +1283,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1325,9 +1299,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1344,9 +1315,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1575,9 +1543,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1594,9 +1559,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1613,9 +1575,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1632,9 +1591,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4324,9 +4280,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4347,9 +4300,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4370,9 +4320,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4393,9 +4340,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4872,16 +4816,15 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", + "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.2.1" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4903,6 +4846,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", @@ -6286,13 +6258,12 @@ } }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/server/package.json b/server/package.json index 6330aab..a0bacf3 100644 --- a/server/package.json +++ b/server/package.json @@ -58,6 +58,7 @@ "mammoth": "^1.12.0", "multer": "^2.1.1", "openai": "^6.27.0", + "p-limit": "^7.3.0", "pdfjs-dist": "^4.6.82", "pg": "^8.20.0", "swagger-ui-express": "^5.0.1", diff --git a/server/src/middleware/rateLimit.ts b/server/src/middleware/rateLimit.ts index 33f41ee..b0ce423 100644 --- a/server/src/middleware/rateLimit.ts +++ b/server/src/middleware/rateLimit.ts @@ -61,3 +61,16 @@ export const registerRateLimiter = rateLimit({ : 'Too many registration attempts. Please try again in 1 hour.', ), }); + +/** + * Limit memory processing requests (PDF, URL, Text extraction). + * Higher resource cost per request requires stricter limiting. + */ +export const memoryRateLimiter = rateLimit({ + ...baseOptions, + windowMs: 15 * 60 * 1000, // 15 minutes + max: isDevelopmentLike ? 10_000 : 30, // 30 requests per 15 minutes in prod + message: rateLimitResponse( + 'Too many document processing requests. Please try again in 15 minutes.', + ), +}); diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index a454392..d1cdfc2 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -347,24 +347,28 @@ export async function deleteMemoriesByIds(ids: string[]): Promise { } /** - * Patch specific payload fields on an existing memory point without + * Patch specific payload fields on one or more existing memory points without * overwriting the rest of the payload (used to set `conflicted` / * `conflictGroupId` for the `flag` resolution strategy). * * Requirements: 7.6 */ -export async function updateMemoryPayloadFields( - pointId: string, +export async function updatePayloadFields( + pointIds: string[], fields: Partial, ): Promise { + if (pointIds.length === 0) return; + await ensureCollection(); const client = getQdrantClient(); await client.setPayload(COLLECTION_NAME, { payload: fields as Record, - points: [pointId], + points: pointIds, wait: true, }); - console.log(`[MemoryRepo] Updated payload fields on memory point ${pointId}.`); + console.log( + `[MemoryRepo] Updated payload fields on ${pointIds.length} memory point(s).`, + ); } diff --git a/server/src/routes/memorie.route.ts b/server/src/routes/memorie.route.ts index 29dde99..9ec232a 100644 --- a/server/src/routes/memorie.route.ts +++ b/server/src/routes/memorie.route.ts @@ -22,6 +22,11 @@ import { deleteMemoryById, updateMemoryById, } from '../controllers/memories/memorie.controller.js'; +import { + loginRateLimiter, + registerRateLimiter, + memoryRateLimiter, +} from '../middleware/rateLimit.js'; import { documentUpload } from '../middleware/upload.js'; import { requireAuth } from '../middleware/auth/requireAuth.js'; @@ -35,13 +40,13 @@ router.use(requireAuth); // --------------------------------------------------------------------------- /** Plain text → extract → embed → store */ -router.post('/text', createFromText); +router.post('/text', memoryRateLimiter, createFromText); /** URL / link → fetch content → extract → embed → store */ -router.post('/link', createFromLink); +router.post('/link', memoryRateLimiter, createFromLink); /** Document upload → parse → extract → embed → store */ -router.post('/document', documentUpload.single('file'), createFromDocument); +router.post('/document', memoryRateLimiter, documentUpload.single('file'), createFromDocument); // --------------------------------------------------------------------------- // Read / Delete diff --git a/server/src/services/conflict-detection.service.ts b/server/src/services/conflict-detection.service.ts index 1a89d2c..d966db7 100644 --- a/server/src/services/conflict-detection.service.ts +++ b/server/src/services/conflict-detection.service.ts @@ -330,6 +330,8 @@ export async function filterRetrieved( // Pre-store Conflict Orchestration // --------------------------------------------------------------------------- +import pLimit from 'p-limit'; + /** * Checks whether an incoming memory conflicts with existing candidates before * it is stored. Applies the configured resolution strategy when conflicts are @@ -355,15 +357,21 @@ export async function checkBeforeStore( return { action: 'store', pointsToDelete: [], pointToStore: incoming }; } - // Step 2: Check each similar candidate for contradiction via LLM - const confirmedConflicts: Array<{ candidate: ScoredMemory; analysis: ConflictAnalysis }> = []; - - for (const candidate of similar) { - const analysis = await detectConflict(incoming.text, candidate.payload.text); - if (analysis.isConflict && analysis.confidence >= CONFLICT_CONFIDENCE_THRESHOLD) { - confirmedConflicts.push({ candidate, analysis }); - } - } + // Step 2: Check similar candidates for contradiction via LLM in parallel (Capped for Free Tier) + const limit = pLimit(5); + const analyses = await Promise.all( + similar.map((candidate) => + limit(async () => { + const analysis = await detectConflict(incoming.text, candidate.payload.text); + return { candidate, analysis }; + }) + ), + ); + + const confirmedConflicts = analyses.filter( + ({ analysis }) => + analysis.isConflict && analysis.confidence >= CONFLICT_CONFIDENCE_THRESHOLD, + ); // Step 3: No true contradictions — check for near-duplicates if (confirmedConflicts.length === 0) { diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 69ac7f4..8109870 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -18,7 +18,7 @@ import { updateMemoryPoint, searchMemoriesScored, deleteMemoriesByIds, - updateMemoryPayloadFields, + updatePayloadFields, } from '../repositories/memory.repository.js'; import { checkBeforeStore } from '../services/conflict-detection.service.js'; import { env } from '../config/env.js'; @@ -96,100 +96,120 @@ async function processText( } satisfies StoredMemoryPayload, })); - let memoriesStoredCount = 0; +import pLimit from 'p-limit'; - for (const point of points) { - const incomingMemory: IncomingMemory = { - text: point.payload.text, - vector: point.vector, - kind: point.payload.kind, - importance: point.payload.importance, - source: point.payload.source, - createdAt: point.payload.createdAt, - }; + const limit = pLimit(5); + const results = await Promise.all( + points.map((point) => + limit(async () => { + const incomingMemory: IncomingMemory = { + text: point.payload.text, + vector: point.vector, + kind: point.payload.kind, + importance: point.payload.importance, + source: point.payload.source, + createdAt: point.payload.createdAt, + }; - let candidates: ScoredMemory[] = []; - try { - candidates = await searchMemoriesScored(point.vector, userId, 10); - } catch (err) { - console.warn( - '[processText] Qdrant search failed, skipping conflict detection for this point:', - err instanceof Error ? err.message : err, - ); - // Fail-open: upsert normally - await upsertMemories([point]); - memoriesStoredCount++; - continue; - } + try { + const candidates = await searchMemoriesScored(point.vector, userId, 10); + const resolution = await checkBeforeStore( + incomingMemory, + candidates, + env.CONFLICT_STRATEGY, + ); + return { point, candidates, resolution }; + } catch (err) { + console.warn( + '[processText] Point processing failed, falling back to simple store:', + err instanceof Error ? err.message : err, + ); + return { + point, + candidates: [], + resolution: { action: 'store', pointsToDelete: [], pointToStore: incomingMemory } as const, + }; + } + }) + ), + ); - const resolution = await checkBeforeStore(incomingMemory, candidates, env.CONFLICT_STRATEGY); + const toUpsert: Array<{ vector: number[]; payload: StoredMemoryPayload }> = []; + const toDelete = new Set(); + const toUpdatePayloads: Array<{ ids: string[]; fields: Partial }> = []; + for (const { point, candidates, resolution } of results) { switch (resolution.action) { case 'store': { - await upsertMemories([point]); - memoriesStoredCount++; + toUpsert.push(point); break; } case 'replace': case 'merge': { - // Ownership check before delete + // Ownership check before adding to delete set for (const id of resolution.pointsToDelete) { const existing = candidates.find((c) => c.id === id); if (existing && existing.payload.userId === userId) { - await deleteMemoriesByIds([id]); - } else { - console.warn( - `[processText] Skipping delete of point ${id}: ownership mismatch or not found`, - ); + toDelete.add(id); } } if (resolution.pointToStore) { - await upsertMemories([ - { - vector: resolution.pointToStore.vector, - payload: { ...resolution.pointToStore, userId } as StoredMemoryPayload, - }, - ]); - memoriesStoredCount++; + toUpsert.push({ + vector: resolution.pointToStore.vector, + payload: { ...resolution.pointToStore, userId } as StoredMemoryPayload, + }); } break; } case 'flag': { if (resolution.pointToStore) { - await upsertMemories([ - { - vector: resolution.pointToStore.vector, - payload: { ...resolution.pointToStore, userId } as StoredMemoryPayload, - }, - ]); - memoriesStoredCount++; + toUpsert.push({ + vector: resolution.pointToStore.vector, + payload: { ...resolution.pointToStore, userId } as StoredMemoryPayload, + }); } - // Mark existing conflicting points if (resolution.conflictGroupId) { - for (const candidate of candidates) { - if ( - candidate.score >= env.SIMILARITY_THRESHOLD && - candidate.payload.userId === userId - ) { - await updateMemoryPayloadFields(candidate.id, { + const idsToFlag = candidates + .filter( + (c) => + c.score >= env.SIMILARITY_THRESHOLD && + c.payload.userId === userId, + ) + .map((c) => c.id); + + if (idsToFlag.length > 0) { + toUpdatePayloads.push({ + ids: idsToFlag, + fields: { conflicted: true, conflictGroupId: resolution.conflictGroupId, - }); - } + }, + }); } } break; } - case 'skip': { - // Near-duplicate: do not store + case 'skip': break; - } } } + // Execute batch operations + if (toDelete.size > 0) { + await deleteMemoriesByIds(Array.from(toDelete)); + } + if (toUpsert.length > 0) { + await upsertMemories(toUpsert); + } + for (const update of toUpdatePayloads) { + await updatePayloadFields(update.ids, update.fields); + } + + const memoriesStoredCount = toUpsert.length; + return { success: true, - message: `Successfully stored ${memoriesStoredCount} memor${memoriesStoredCount === 1 ? 'y' : 'ies'}.`, + message: `Successfully processed document. Stored ${memoriesStoredCount} memory point(s).`, data: { memoriesStored: memoriesStoredCount, semantic: extracted.semantic, From ed019a5c46bcfdd13b28a9a44f1eaeca7cf89324 Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Wed, 15 Apr 2026 17:39:30 +0530 Subject: [PATCH 2/4] feat: implement core memory ingestion, storage, and retrieval system with multi-format content extraction --- client/src/components/ChatToggleButton.tsx | 32 +- client/src/components/Login.tsx | 2 + client/src/components/MainArea.tsx | 308 ++---------------- client/src/components/ManageMemories.tsx | 147 +-------- client/src/components/MemoryCard.tsx | 111 +++++++ client/src/components/MemoryGrid.tsx | 88 +++++ client/src/components/Navbar.tsx | 6 + client/src/components/Signup.tsx | 3 + client/src/components/SystemStatus.tsx | 99 ++++++ .../ingestion/DocumentIngestion.tsx | 150 +++++++++ .../components/ingestion/LinkIngestion.tsx | 78 +++++ .../src/components/ingestion/SubmitButton.tsx | 47 +++ .../components/ingestion/TextIngestion.tsx | 58 ++++ server/src/config/env.ts | 36 +- server/src/index.ts | 134 +++----- server/src/lib/postgres.ts | 15 +- server/src/middleware/rateLimit.ts | 16 +- server/src/repositories/user.repository.ts | 40 ++- server/src/routes/chat.route.ts | 12 +- server/src/routes/health.route.ts | 28 +- server/src/routes/mcp.route.ts | 182 +---------- server/src/routes/memorie.route.ts | 2 - server/src/services/chat.service.ts | 15 +- server/src/services/mcp.service.ts | 151 +++++++++ server/src/services/memory.service.ts | 9 +- server/src/utils/content-extractors.ts | 53 ++- 26 files changed, 1042 insertions(+), 780 deletions(-) create mode 100644 client/src/components/MemoryCard.tsx create mode 100644 client/src/components/MemoryGrid.tsx create mode 100644 client/src/components/SystemStatus.tsx create mode 100644 client/src/components/ingestion/DocumentIngestion.tsx create mode 100644 client/src/components/ingestion/LinkIngestion.tsx create mode 100644 client/src/components/ingestion/SubmitButton.tsx create mode 100644 client/src/components/ingestion/TextIngestion.tsx create mode 100644 server/src/services/mcp.service.ts diff --git a/client/src/components/ChatToggleButton.tsx b/client/src/components/ChatToggleButton.tsx index 5eb0cc8..bc4ebe1 100644 --- a/client/src/components/ChatToggleButton.tsx +++ b/client/src/components/ChatToggleButton.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef } from 'react'; interface ChatToggleButtonProps { isOpen: boolean; @@ -9,7 +9,20 @@ const STORAGE_KEY = 'neura-chat-btn-pos'; function ChatToggleButton({ isOpen, onToggle }: ChatToggleButtonProps) { const [position, setPosition] = useState<{ x: number; y: number } | null>( - null, + () => { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + const parsed = JSON.parse(saved); + if (typeof parsed.x === 'number' && typeof parsed.y === 'number') { + return parsed; + } + } + } catch { + // ignore parse errors + } + return null; + }, ); // Drag state stored in refs to avoid re-renders during drag @@ -21,21 +34,6 @@ function ChatToggleButton({ isOpen, onToggle }: ChatToggleButtonProps) { currentY: number; }>({ startX: 0, startY: 0, isDragging: false, currentX: 0, currentY: 0 }); - // Restore position from localStorage on mount - useEffect(() => { - try { - const saved = localStorage.getItem(STORAGE_KEY); - if (saved) { - const parsed = JSON.parse(saved); - if (typeof parsed.x === 'number' && typeof parsed.y === 'number') { - setPosition(parsed); - } - } - } catch { - // ignore parse errors - } - }, []); - const handlePointerDown = (e: React.PointerEvent) => { e.currentTarget.setPointerCapture(e.pointerId); dragRef.current = { diff --git a/client/src/components/Login.tsx b/client/src/components/Login.tsx index 6746ab3..d047d1b 100644 --- a/client/src/components/Login.tsx +++ b/client/src/components/Login.tsx @@ -58,6 +58,7 @@ const Login = () => { type="email" className="bg-neutral-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:ring-2 focus:ring-blue-500 outline-none transition" required + autoComplete="email" autoFocus /> @@ -75,6 +76,7 @@ const Login = () => { type="password" className="bg-neutral-800 border border-gray-700 rounded-lg px-4 py-3 text-white focus:ring-2 focus:ring-blue-500 outline-none transition" required + autoComplete="current-password" /> diff --git a/client/src/components/MainArea.tsx b/client/src/components/MainArea.tsx index e1072f3..b8caef7 100644 --- a/client/src/components/MainArea.tsx +++ b/client/src/components/MainArea.tsx @@ -1,7 +1,8 @@ -import { useState, useRef } from 'react'; +import React, { useState } from 'react'; import { useNavigate } from 'react-router'; -import { AxiosError } from 'axios'; -import { api } from '../lib/api'; +import { TextIngestion } from './ingestion/TextIngestion'; +import { LinkIngestion } from './ingestion/LinkIngestion'; +import { DocumentIngestion } from './ingestion/DocumentIngestion'; // ── Types ────────────────────────────────────────────────────────────────── type Tab = 'text' | 'link' | 'document'; @@ -69,105 +70,20 @@ const TABS: { id: Tab; label: string; icon: React.ReactNode }[] = [ }, ]; -const ACCEPTED_TYPES = '.pdf,.docx,.txt,.md,.csv'; - -// ── Helpers ──────────────────────────────────────────────────────────────── -// No manual auth headers needed; axios will attach the neura_token cookie automatically. - // ── Component ────────────────────────────────────────────────────────────── const MainArea = () => { const navigate = useNavigate(); const [activeTab, setActiveTab] = useState('text'); - const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); - const [dragOver, setDragOver] = useState(false); - const [selectedFile, setSelectedFile] = useState(null); - const fileInputRef = useRef(null); const resetFeedback = () => { setError(''); setSuccess(''); }; - // ── Submit handlers ────────────────────────────────────────────────────── - - const handleTextSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const text = (new FormData(e.currentTarget).get('text') as string).trim(); - if (!text) return; - - resetFeedback(); - setLoading(true); - try { - await api.post('/api/v1/memories/text', { text }); - setSuccess('Memory saved from text!'); - (e.target as HTMLFormElement).reset(); - } catch (err) { - setError( - err instanceof AxiosError - ? (err.response?.data?.message ?? 'Failed to save text.') - : 'Unexpected error.', - ); - } finally { - setLoading(false); - } - }; - - const handleLinkSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const url = (new FormData(e.currentTarget).get('url') as string).trim(); - if (!url) return; - - resetFeedback(); - setLoading(true); - try { - await api.post('/api/v1/memories/link', { url }); - setSuccess('Memory saved from link!'); - (e.target as HTMLFormElement).reset(); - } catch (err) { - setError( - err instanceof AxiosError - ? (err.response?.data?.message ?? 'Failed to process link.') - : 'Unexpected error.', - ); - } finally { - setLoading(false); - } - }; - - const handleDocumentSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!selectedFile) { - setError('Please select a file to upload.'); - return; - } - - resetFeedback(); - setLoading(true); - const form = new FormData(); - form.append('file', selectedFile); - - try { - await api.post('/api/v1/memories/document', form); - setSuccess(`Memory saved from "${selectedFile.name}"!`); - setSelectedFile(null); - if (fileInputRef.current) fileInputRef.current.value = ''; - } catch (err) { - setError( - err instanceof AxiosError - ? (err.response?.data?.message ?? 'Failed to upload document.') - : 'Unexpected error.', - ); - } finally { - setLoading(false); - } - }; - - const onFileChange = (file: File | null) => { - setSelectedFile(file); - resetFeedback(); - }; + const handleSuccess = (msg: string) => setSuccess(msg); + const handleError = (msg: string) => setError(msg); return (
@@ -194,7 +110,6 @@ const MainArea = () => { onClick={() => { setActiveTab(tab.id); resetFeedback(); - setSelectedFile(null); }} className={`flex-1 flex items-center justify-center gap-2 py-3.5 text-sm font-semibold transition-colors duration-150 focus:outline-none cursor-pointer ${ @@ -211,166 +126,33 @@ const MainArea = () => { {/* Panel body */}
- {/* ── TEXT TAB ── */} {activeTab === 'text' && ( -
-