From 2f6518207d976da4bb13687dd0e9a4b176c582b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Sun, 15 Feb 2026 17:57:32 +1100 Subject: [PATCH] sync: multiple features from private repo - quick-add-loading: fire-and-forget with loading placeholders, auto-open feed, SSE events, QuickAddInput redesign - database-table-pane: TablePane + DatabaseTableView, countNodes(), event_date sort - ui-polish-fixes: focus tab order, dim name editing, tab title sync, cost chip removal, custom sort drag-reorder, refresh button, dimension filter removal (~2,200 lines) - node-creation-quality: description service prompt rewrite, title sanitization, extraction tool AI prompt rewrites - feed-pane-ux: stripped kanban/grid/saved views, sort dropdown, AND dimension filtering, description preview Co-Authored-By: Claude Opus 4.6 --- README.md | 2 +- app/api/nodes/route.ts | 69 +- src/components/agents/QuickAddInput.tsx | 467 ++-- src/components/focus/FocusPanel.tsx | 36 +- src/components/layout/LeftToolbar.tsx | 14 +- src/components/layout/ThreePanelLayout.tsx | 150 +- src/components/nodes/FolderViewOverlay.tsx | 2230 +------------------ src/components/panes/TablePane.tsx | 39 + src/components/panes/ViewsPane.tsx | 9 +- src/components/panes/index.ts | 1 + src/components/panes/types.ts | 9 +- src/components/settings/LogsRow.tsx | 5 - src/components/views/DatabaseTableView.tsx | 629 ++++++ src/components/views/ViewsOverlay.tsx | 242 +- src/config/guides/system/creating-nodes.md | 18 +- src/services/agents/quickAdd.ts | 75 +- src/services/database/descriptionService.ts | 28 +- src/services/database/nodes.ts | 42 + src/services/events.ts | 2 + src/tools/database/createNode.ts | 4 +- src/tools/database/updateNode.ts | 6 +- src/tools/other/paperExtract.ts | 46 +- src/tools/other/websiteExtract.ts | 45 +- src/tools/other/youtubeExtract.ts | 67 +- src/types/database.ts | 2 +- 25 files changed, 1590 insertions(+), 2647 deletions(-) create mode 100644 src/components/panes/TablePane.tsx create mode 100644 src/components/views/DatabaseTableView.tsx diff --git a/README.md b/README.md index 3bedccf..b2cd69d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ``` -A local SQLite database with a UI for storing knowledge, and an MCP server so your AI tools can read/write to it. +**TL;DR:** Clone this repository and you'll have a local SQLite database on your computer. The database schema is structured so external AI agents can continuously read and write to it, building your knowledge graph externally. **Full documentation:** [ra-h.app/docs/open-source](https://ra-h.app/docs/open-source) diff --git a/app/api/nodes/route.ts b/app/api/nodes/route.ts index 7baf8c5..06d00cc 100644 --- a/app/api/nodes/route.ts +++ b/app/api/nodes/route.ts @@ -33,7 +33,7 @@ export async function GET(request: NextRequest) { // Handle sortBy parameter (sortBy=edges|updated|created) const sortByParam = searchParams.get('sortBy'); - if (sortByParam === 'edges' || sortByParam === 'updated' || sortByParam === 'created') { + if (sortByParam === 'edges' || sortByParam === 'updated' || sortByParam === 'created' || sortByParam === 'event_date') { filters.sortBy = sortByParam; } @@ -46,11 +46,13 @@ export async function GET(request: NextRequest) { } const nodes = await nodeService.getNodes(filters); - + const total = await nodeService.countNodes(filters); + return NextResponse.json({ success: true, data: nodes, - count: nodes.length + count: nodes.length, + total }); } catch (error) { console.error('Error fetching nodes:', error); @@ -62,10 +64,24 @@ export async function GET(request: NextRequest) { } } +// Weak-pattern regex for post-creation quality monitoring +const WEAK_PATTERNS = /\b(discusses|explores|examines|talks about|is about|delves into|This is a)\b/i; + +function sanitizeTitle(title: string): string { + let clean = title.trim(); + // Strip "Title: " prefix (extraction artifact) + if (clean.startsWith('Title: ')) clean = clean.slice(7); + // Strip trailing " / X" (Twitter artifact) + if (clean.endsWith(' / X')) clean = clean.slice(0, -4); + // Collapse whitespace + clean = clean.replace(/\s+/g, ' '); + return clean.slice(0, 160); +} + export async function POST(request: NextRequest) { try { const body = await request.json(); - + // Validate required fields if (!body.title) { return NextResponse.json({ @@ -74,7 +90,11 @@ export async function POST(request: NextRequest) { }, { status: 400 }); } + // Sanitize title (strip extraction artifacts) + body.title = sanitizeTitle(body.title); + const rawNotes = typeof body.notes === 'string' ? body.notes : null; + const eventDate = typeof body.event_date === 'string' ? body.event_date : null; // Process provided dimensions first (needed for description generation) const providedDimensions = Array.isArray(body.dimensions) ? body.dimensions : []; @@ -83,19 +103,33 @@ export async function POST(request: NextRequest) { .filter(Boolean) .slice(0, 8); - // Generate description with all available context - let nodeDescription: string | undefined; - try { - nodeDescription = await generateDescription({ - title: body.title, - notes: rawNotes || undefined, - link: body.link || undefined, - metadata: body.metadata, - dimensions: trimmedProvidedDimensions - }); - } catch (error) { - console.error('Error generating description:', error); - // Continue without description - dimension assignment will use content as fallback + // Use provided description if present, otherwise auto-generate + let nodeDescription: string | undefined = typeof body.description === 'string' && body.description.trim() + ? body.description.trim().slice(0, 280) + : undefined; + + if (!nodeDescription) { + try { + nodeDescription = await generateDescription({ + title: body.title, + notes: rawNotes || undefined, + link: body.link || undefined, + metadata: body.metadata, + dimensions: trimmedProvidedDimensions + }); + } catch (error) { + console.error('Error generating description:', error); + } + } + + // Final safety net — never store null/empty description + if (!nodeDescription || nodeDescription.trim().length === 0) { + nodeDescription = body.title.slice(0, 280); + } + + // Monitor description quality + if (nodeDescription && WEAK_PATTERNS.test(nodeDescription)) { + console.warn(`[DescriptionQuality] Weak description for node "${body.title}": "${nodeDescription}"`); } // Auto-assign locked dimensions + keyword dimensions for all new nodes @@ -129,6 +163,7 @@ export async function POST(request: NextRequest) { title: body.title, description: nodeDescription, notes: rawNotes ?? undefined, + event_date: eventDate ?? undefined, link: body.link, dimensions: finalDimensions, chunk: chunkToStore ?? undefined, diff --git a/src/components/agents/QuickAddInput.tsx b/src/components/agents/QuickAddInput.tsx index dae0e19..ee08c0f 100644 --- a/src/components/agents/QuickAddInput.tsx +++ b/src/components/agents/QuickAddInput.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; interface QuickAddSubmitPayload { @@ -11,11 +11,40 @@ interface QuickAddSubmitPayload { interface QuickAddInputProps { onSubmit: (payload: QuickAddSubmitPayload) => Promise; - // External control (optional - if provided, component is controlled) isOpen?: boolean; onClose?: () => void; } +type DetectedType = 'youtube' | 'website' | 'pdf-url' | 'note' | 'chat'; + +function detectType(raw: string): DetectedType { + 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'; +} + +const TYPE_LABELS: Record = { + youtube: 'YouTube', + website: 'Link', + 'pdf-url': 'PDF', + note: 'Note', + chat: 'Transcript', +}; + +const TYPE_COLORS: Record = { + youtube: '#ef4444', + website: '#3b82f6', + 'pdf-url': '#f59e0b', + note: '#22c55e', + chat: '#a78bfa', +}; + export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInputProps) { const [input, setInput] = useState(''); const [isPosting, setIsPosting] = useState(false); @@ -24,42 +53,32 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp const [uploadedFile, setUploadedFile] = useState(null); const [uploadError, setUploadError] = useState(null); - // Support both controlled (isOpen/onClose) and uncontrolled (internal state) modes const isControlled = isOpen !== undefined; const isExpanded = isControlled ? isOpen : isExpandedInternal; const setIsExpanded = isControlled ? (value: boolean) => { if (!value && onClose) onClose(); } : setIsExpandedInternal; + const detectedType = useMemo(() => detectType(input), [input]); + const showTypePill = input.trim().length > 0 && !uploadedFile; + const handleFileUpload = useCallback(async (file: File) => { setIsPosting(true); setUploadError(null); - try { const formData = new FormData(); formData.append('file', file); - const response = await fetch('/api/extract/pdf/upload', { method: 'POST', body: formData, }); - const result = await response.json(); - if (!response.ok || !result.success) { throw new Error(result.error || 'Upload failed'); } - - // Success - clear state setUploadedFile(null); setInput(''); setIsExpanded(false); - - // Show warning if present (large file) - if (result.warning) { - console.log('[QuickAddInput] Upload warning:', result.warning); - } - } catch (error) { console.error('[QuickAddInput] Upload error:', error); setUploadError(error instanceof Error ? error.message : 'Upload failed'); @@ -69,22 +88,14 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp }, []); const handleSubmit = async () => { - // If there's a file, upload it if (uploadedFile) { await handleFileUpload(uploadedFile); return; } - - // Otherwise, submit text as before if (!input.trim() || isPosting) return; - setIsPosting(true); try { - // Mode is auto-detected server-side via quickAdd.ts detectInputType() - await onSubmit({ - input: input.trim(), - mode: 'link', // Default; actual type is inferred server-side - }); + await onSubmit({ input: input.trim(), mode: 'link' }); setInput(''); setIsExpanded(false); } catch (error) { @@ -124,30 +135,18 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp e.stopPropagation(); setDragOver(false); setUploadError(null); - const file = e.dataTransfer?.files[0]; if (!file) return; - - // Check if it's a PDF if (file.type !== 'application/pdf' && !file.name.toLowerCase().endsWith('.pdf')) { setUploadError('Only PDF files are supported'); return; } - - // Check size (50MB limit) - const MAX_SIZE = 50 * 1024 * 1024; - if (file.size > MAX_SIZE) { - setUploadError(`File too large (${Math.round(file.size / 1024 / 1024)}MB). Maximum is 50MB.`); + if (file.size > 50 * 1024 * 1024) { + setUploadError(`File too large (${Math.round(file.size / 1024 / 1024)}MB). Max 50MB.`); return; } - setUploadedFile(file); - setInput(''); // Clear text input when file is dropped - }, []); - - const clearFile = useCallback(() => { - setUploadedFile(null); - setUploadError(null); + setInput(''); }, []); const hasContent = input.trim() || uploadedFile; @@ -159,12 +158,8 @@ export default function QuickAddInput({ onSubmit, isOpen, onClose }: QuickAddInp setUploadError(null); }; - // Collapsed state - only show button if NOT controlled externally if (!isExpanded) { - // In controlled mode, don't render anything when closed if (isControlled) return null; - - // Uncontrolled mode - show the "ADD STUFF" button return ( ); } - // Expanded state - the card content const modalContent = (
e.stopPropagation()} > - {/* Header */} -
- Add Stuff - -
- - {/* File preview (when a file is dropped) */} + {/* File preview */} {uploadedFile && ( -
- {/* PDF icon */} -
- - - - - - -
- - {/* File info */} -
-
- {uploadedFile.name} -
-
- {uploadedFile.size < 1024 * 1024 - ? `${Math.round(uploadedFile.size / 1024)} KB` - : `${(uploadedFile.size / 1024 / 1024).toFixed(1)} MB`} -
-
- - {/* Remove button */} -
)} - {/* Error message */} + {/* Error */} {uploadError && ( -
- {uploadError} -
+
{uploadError}
)} - {/* Input area - show if no file uploaded */} + {/* Input area */} {!uploadedFile && ( -
- {/* Drag overlay */} +
{dragOver && (
-
- - - - - Drop PDF here -
+ + + + + Drop PDF
)} -