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' && ( -
-