diff --git a/app/api/extract/pdf/upload/route.ts b/app/api/extract/pdf/upload/route.ts index 53d8d78..75ef942 100644 --- a/app/api/extract/pdf/upload/route.ts +++ b/app/api/extract/pdf/upload/route.ts @@ -57,9 +57,8 @@ export async function POST(request: NextRequest) { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, - type: 'pdf', - // Keep content readable in UI (not the full extracted text) - content: `Imported PDF: ${file.name} (${extraction.metadata.pages} pages, ${Math.round(extraction.metadata.text_length / 1000)}k characters)`, + // Give the description generator and UI a readable summary plus a short excerpt. + notes: `Imported PDF document: ${file.name} (${extraction.metadata.pages} pages, ${Math.round(extraction.metadata.text_length / 1000)}k characters).\n\nPreview:\n${extraction.chunk.slice(0, 1200)}`, // Full extracted text goes in chunk for universal chunking/embedding chunk: extraction.chunk, metadata: { diff --git a/app/api/nodes/[id]/route.ts b/app/api/nodes/[id]/route.ts index b79a709..003680b 100644 --- a/app/api/nodes/[id]/route.ts +++ b/app/api/nodes/[id]/route.ts @@ -75,10 +75,9 @@ export async function PUT( if (typeof body.description === 'string') { const descriptionError = validateExplicitDescription(body.description); if (descriptionError) { - return NextResponse.json({ - success: false, - error: descriptionError - }, { status: 400 }); + console.warn( + `[DescriptionQuality] User-updated description failed validation for node ${nodeId}: ${descriptionError}` + ); } } diff --git a/app/api/nodes/route.ts b/app/api/nodes/route.ts index 33658df..5e9aff6 100644 --- a/app/api/nodes/route.ts +++ b/app/api/nodes/route.ts @@ -93,6 +93,7 @@ export async function POST(request: NextRequest) { body.title = sanitizeTitle(body.title); const rawNotes = typeof body.notes === 'string' ? body.notes : null; + const rawChunk = typeof body.chunk === 'string' ? body.chunk : null; const eventDate = typeof body.event_date === 'string' ? body.event_date : null; // Process provided dimensions first (needed for description generation) @@ -107,7 +108,7 @@ export async function POST(request: NextRequest) { try { nodeDescription = await generateDescription({ title: body.title, - notes: rawNotes || undefined, + notes: rawNotes || rawChunk?.slice(0, 2000) || undefined, link: body.link || undefined, metadata: body.metadata, dimensions: trimmedProvidedDimensions @@ -122,14 +123,22 @@ export async function POST(request: NextRequest) { nodeDescription = body.title.slice(0, 280); } - const finalDescription = nodeDescription ?? body.title.slice(0, 280); + let finalDescription = nodeDescription ?? body.title.slice(0, 280); const descriptionError = validateExplicitDescription(finalDescription); if (descriptionError) { - return NextResponse.json({ - success: false, - error: descriptionError - }, { status: 400 }); + const isUserSuppliedDescription = typeof body.description === 'string' && body.description.trim().length > 0; + if (isUserSuppliedDescription) { + return NextResponse.json({ + success: false, + error: descriptionError + }, { status: 400 }); + } + + console.warn( + `[DescriptionQuality] Auto-generated description failed validation for "${body.title}": ${descriptionError}. Falling back to title.` + ); + finalDescription = body.title.slice(0, 280); } // Monitor description quality @@ -139,7 +148,6 @@ export async function POST(request: NextRequest) { // Use only provided dimensions (no auto-assignment) const finalDimensions = trimmedProvidedDimensions; - const rawChunk = typeof body.chunk === 'string' ? body.chunk : null; let chunkToStore = rawChunk; let chunkStatus: Node['chunk_status']; diff --git a/app/api/quick-add/route.ts b/app/api/quick-add/route.ts index b5b78a4..885eb8d 100644 --- a/app/api/quick-add/route.ts +++ b/app/api/quick-add/route.ts @@ -14,18 +14,18 @@ export async function POST(request: NextRequest) { } const normalizedMode: QuickAddMode | undefined = - mode === 'link' || mode === 'note' || mode === 'chat' ? mode : undefined; + mode === 'link' || mode === 'text' ? mode : undefined; const normalizedDescription: string | undefined = typeof description === 'string' && description.trim() ? description.trim() : undefined; - const delegation = await enqueueQuickAdd({ + const result = await enqueueQuickAdd({ rawInput: input.trim(), mode: normalizedMode, description: normalizedDescription, }); - return NextResponse.json({ success: true, delegation }); + return NextResponse.json({ success: true, result }); } catch (error) { console.error('[Quick Add API] Error:', error); const message = error instanceof Error ? error.message : 'Unknown error'; diff --git a/apps/mcp-server-standalone/index.js b/apps/mcp-server-standalone/index.js index 04a05c7..1e38f40 100644 --- a/apps/mcp-server-standalone/index.js +++ b/apps/mcp-server-standalone/index.js @@ -197,9 +197,14 @@ function validateExplicitDescription(description) { return 'Description must be explicit and substantial (at least 24 characters).'; } const weakPatterns = /\b(discusses|explores|examines|talks about|is about|delves into)\b/i; + const explicitEntityPatterns = /\b(article|artifact|book|brief|claim|company|concept|conversation|dataset|decision|dimension|document|episode|essay|event|guide|idea|insight|interview|lesson|link|node|note|paper|person|plan|placeholder|podcast|post|presentation|project|question|record|research|resource|skill|source|status|summary|talk|target|test node|thread|tool|transcript|tweet|update|video|website|workflow)\b/i; + const uncertaintyPatterns = /\b(likely|probably|possibly|appears to be|seems to be|unclear|uncertain)\b/i; if (weakPatterns.test(text)) { return 'Description is too vague. State exactly what this is and why it matters.'; } + if (!explicitEntityPatterns.test(text) && !uncertaintyPatterns.test(text)) { + return 'Description must explicitly identify what this thing is, or state uncertainty explicitly.'; + } return null; } diff --git a/apps/mcp-server-standalone/services/sqlite-client.js b/apps/mcp-server-standalone/services/sqlite-client.js index 9a1ffb3..b4cf84a 100644 --- a/apps/mcp-server-standalone/services/sqlite-client.js +++ b/apps/mcp-server-standalone/services/sqlite-client.js @@ -70,6 +70,7 @@ function initDatabase() { source TEXT, created_at TEXT, context TEXT, + explanation TEXT, FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_node_id) REFERENCES nodes(id) ON DELETE CASCADE ); @@ -112,6 +113,18 @@ function initDatabase() { db.pragma('cache_size = 5000'); db.pragma('busy_timeout = 5000'); + const edgeCols = db.prepare('PRAGMA table_info(edges)').all().map(c => c.name); + if (!edgeCols.includes('explanation')) { + db.exec('ALTER TABLE edges ADD COLUMN explanation TEXT;'); + try { + db.exec(` + UPDATE edges SET explanation = json_extract(context, '$.explanation') + WHERE explanation IS NULL AND json_extract(context, '$.explanation') IS NOT NULL; + `); + } catch {} + console.error('[RA-H] Migrated edges: added explanation column'); + } + return db; } diff --git a/src/components/agents/QuickAddInput.tsx b/src/components/agents/QuickAddInput.tsx index ee08c0f..d878495 100644 --- a/src/components/agents/QuickAddInput.tsx +++ b/src/components/agents/QuickAddInput.tsx @@ -1,11 +1,11 @@ "use client"; -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useMemo, useRef } from 'react'; import { createPortal } from 'react-dom'; interface QuickAddSubmitPayload { input: string; - mode: 'link' | 'note' | 'chat'; + mode: 'link' | 'text'; description?: string; } @@ -15,34 +15,28 @@ interface QuickAddInputProps { onClose?: () => void; } -type DetectedType = 'youtube' | 'website' | 'pdf-url' | 'note' | 'chat'; +type SourceType = 'link' | 'text' | 'upload'; -function detectType(raw: string): DetectedType { +function detectSourceType(raw: string): Exclude { const trimmed = raw.trim(); - if (!trimmed) return 'note'; - if (/youtu(\.be|be\.com)/i.test(trimmed)) return 'youtube'; - if (/\.pdf($|\?)/i.test(trimmed) || /arxiv\.org\//i.test(trimmed)) return 'pdf-url'; - if (/^https?:\/\//i.test(trimmed)) return 'website'; - const newlines = (trimmed.match(/\n/g)?.length ?? 0); - if (newlines >= 3 && trimmed.length > 300) return 'chat'; - if (/You said:|ChatGPT said:|Claude said:/i.test(trimmed)) return 'chat'; - return 'note'; + if (!trimmed) return 'text'; + const isSingleLine = !trimmed.includes('\n'); + if (isSingleLine) { + if (/^https?:\/\//i.test(trimmed)) return 'link'; + } + return 'text'; } -const TYPE_LABELS: Record = { - youtube: 'YouTube', - website: 'Link', - 'pdf-url': 'PDF', - note: 'Note', - chat: 'Transcript', +const SOURCE_LABELS: Record = { + link: 'Link', + text: 'Text', + upload: 'Upload', }; -const TYPE_COLORS: Record = { - youtube: '#ef4444', - website: '#3b82f6', - 'pdf-url': '#f59e0b', - note: '#22c55e', - chat: '#a78bfa', +const SOURCE_COLORS: Record = { + link: '#3b82f6', + text: '#22c55e', + upload: '#f59e0b', }; export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInputProps) { @@ -52,6 +46,8 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp const [dragOver, setDragOver] = useState(false); const [uploadedFile, setUploadedFile] = useState(null); const [uploadError, setUploadError] = useState(null); + const [sourceOverride, setSourceOverride] = useState | null>(null); + const fileInputRef = useRef(null); const isControlled = isOpen !== undefined; const isExpanded = isControlled ? isOpen : isExpandedInternal; @@ -59,8 +55,8 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp ? (value: boolean) => { if (!value && onClose) onClose(); } : setIsExpandedInternal; - const detectedType = useMemo(() => detectType(input), [input]); - const showTypePill = input.trim().length > 0 && !uploadedFile; + const detectedSource = useMemo(() => detectSourceType(input), [input]); + const effectiveSource: SourceType = uploadedFile ? 'upload' : (sourceOverride ?? detectedSource); const handleFileUpload = useCallback(async (file: File) => { setIsPosting(true); @@ -78,6 +74,7 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp } setUploadedFile(null); setInput(''); + setSourceOverride(null); setIsExpanded(false); } catch (error) { console.error('[QuickAddInput] Upload error:', error); @@ -95,8 +92,12 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp if (!input.trim() || isPosting) return; setIsPosting(true); try { - await onSubmit({ input: input.trim(), mode: 'link' }); + await onSubmit({ + input: input.trim(), + mode: effectiveSource === 'link' ? 'link' : 'text', + }); setInput(''); + setSourceOverride(null); setIsExpanded(false); } catch (error) { console.error('[QuickAddInput] Submit error:', error); @@ -115,6 +116,7 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp setInput(''); setUploadedFile(null); setUploadError(null); + setSourceOverride(null); } }; @@ -147,6 +149,7 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp } setUploadedFile(file); setInput(''); + setSourceOverride(null); }, []); const hasContent = input.trim() || uploadedFile; @@ -156,6 +159,37 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp setInput(''); setUploadedFile(null); setUploadError(null); + setSourceOverride(null); + }; + + const handleSourceSelect = (source: Exclude) => { + setSourceOverride(source); + setUploadedFile(null); + setUploadError(null); + }; + + const handleUploadClick = () => { + setUploadError(null); + fileInputRef.current?.click(); + }; + + const handleFileInputChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { + setUploadError('Only PDF files are supported'); + event.target.value = ''; + return; + } + if (file.size > 50 * 1024 * 1024) { + setUploadError(`File too large (${Math.round(file.size / 1024 / 1024)}MB). Max 50MB.`); + event.target.value = ''; + return; + } + setUploadedFile(file); + setInput(''); + setSourceOverride(null); + event.target.value = ''; }; if (!isExpanded) { @@ -210,7 +244,14 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp onDrop={handleDrop} onClick={(e) => e.stopPropagation()} > - {/* File preview */} + + {uploadedFile && (
@@ -231,12 +272,10 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp
)} - {/* Error */} {uploadError && (
{uploadError}
)} - {/* Input area */} {!uploadedFile && (
{dragOver && ( @@ -250,9 +289,18 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp )}