sync: Idea Genealogy + Quick Capture Overhaul

From private repo:

Idea Genealogy:
- Required edge explanations across UI/tools/API/MCP
- Inferred edge schema (category/type/confidence)
- Added part_of relationship type
- Connection UI chips (Made by / Part of / Came from / Related)

Quick Capture Overhaul:
- Jina.ai fallback for JS-rendered sites (Twitter, SPAs)
- Simplified UI: removed mode buttons, auto-detect input type
- Renamed to "Add Stuff", moved to top-right
- Auto-edge creation from description entities
- Increased content summary length (500→1500 chars)

Description Generation Fix:
- Simplified prompt to "what is this"
- Added link/dimensions context
- Better creator attribution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-01-14 17:08:26 +11:00
co-authored by Claude Opus 4.5
parent f7b8b2058c
commit 2cbc950fd4
21 changed files with 1296 additions and 389 deletions
+81 -217
View File
@@ -1,14 +1,11 @@
"use client";
import { useMemo, useState } from 'react';
import type { ReactNode } from 'react';
import { useState } from 'react';
import type { AgentDelegation } from '@/services/agents/delegation';
type QuickAddIntent = 'link' | 'note' | 'chat';
interface QuickAddSubmitPayload {
input: string;
mode: QuickAddIntent;
mode: 'link' | 'note' | 'chat';
description?: string;
}
@@ -17,63 +14,10 @@ interface QuickAddInputProps {
onSubmit: (payload: QuickAddSubmitPayload) => Promise<void>;
}
const MODE_CONFIG: Array<{
key: QuickAddIntent;
label: string;
hint: string;
icon: ReactNode;
}> = [
{
key: 'link',
label: 'Link',
hint: 'Drop URLs for auto extraction',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M10 13a5 5 0 0 0 7.54.35l2.12-2.12a5 5 0 1 0-7.07-7.07l-1.29 1.29" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14 11a5 5 0 0 0-7.54-.35l-2.12 2.12a5 5 0 1 0 7.07 7.07l1.29-1.29" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
},
{
key: 'note',
label: 'Note',
hint: 'Quick note, no processing',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" strokeLinecap="round" strokeLinejoin="round" />
<path d="M14 2v6h6" strokeLinecap="round" strokeLinejoin="round" />
<path d="M16 13H8" strokeLinecap="round" />
<path d="M16 17H8" strokeLinecap="round" />
</svg>
)
},
{
key: 'chat',
label: 'Chat',
hint: 'Paste conversations',
icon: (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 15a2 2 0 0 1-2 2H9l-4 4V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2z" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
}
];
export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddInputProps) {
const [input, setInput] = useState('');
const [description, setDescription] = useState('');
const [isPosting, setIsPosting] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [manualMode, setManualMode] = useState<QuickAddIntent | null>(null);
const [autoMode, setAutoMode] = useState<QuickAddIntent>('link');
const effectiveMode: QuickAddIntent = manualMode ?? autoMode;
const currentPlaceholder = useMemo(() => {
if (effectiveMode === 'note') return 'Write a quick note...';
if (effectiveMode === 'chat') return 'Paste conversation...';
return 'Paste a link or write something...';
}, [effectiveMode]);
const maxConcurrent = 5;
const activeCount = activeDelegations.filter(
@@ -81,44 +25,17 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
).length;
const isSoftLimited = activeCount >= maxConcurrent;
const inferChatIntent = (value: string) => {
const trimmed = value.trim();
if (!trimmed || manualMode) return;
const newlineCount = (trimmed.match(/\n/g)?.length ?? 0);
const looksLikeTranscript =
newlineCount >= 2 ||
/You said:|ChatGPT said:|Claude said:|Assistant:|User:/i.test(trimmed);
if (looksLikeTranscript && trimmed.length > 280) {
setAutoMode('chat');
}
};
const handleInputChange = (value: string) => {
setInput(value);
inferChatIntent(value);
};
const handleModeClick = (mode: QuickAddIntent) => {
setManualMode(mode);
setAutoMode(mode);
};
const handleSubmit = async () => {
if (!input.trim() || isPosting || isSoftLimited) return;
setIsPosting(true);
try {
// Mode is auto-detected server-side via quickAdd.ts detectInputType()
await onSubmit({
input: input.trim(),
mode: effectiveMode,
description: description.trim() || undefined
mode: 'link', // Default; actual type is inferred server-side
});
setInput('');
setDescription('');
setManualMode(null);
setAutoMode('link');
setIsExpanded(false);
} catch (error) {
console.error('[QuickAddInput] Submit error:', error);
@@ -138,7 +55,7 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
}
};
// Collapsed state - visible trigger
// Collapsed state - prominent "ADD STUFF" button
if (!isExpanded) {
return (
<button
@@ -147,114 +64,105 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '12px',
width: '100%',
padding: '16px 24px',
background: 'transparent',
border: '1px solid #333',
borderRadius: '12px',
color: '#888',
fontSize: '12px',
gap: '8px',
padding: '10px 16px',
background: 'rgba(34, 197, 94, 0.1)',
border: '1px solid rgba(34, 197, 94, 0.3)',
borderRadius: '8px',
color: '#22c55e',
fontSize: '11px',
fontWeight: 600,
letterSpacing: '0.1em',
letterSpacing: '0.08em',
textTransform: 'uppercase',
cursor: 'pointer',
transition: 'all 0.2s ease'
transition: 'all 0.2s ease',
whiteSpace: 'nowrap',
boxShadow: '0 0 12px rgba(34, 197, 94, 0.15)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#444';
e.currentTarget.style.color = '#aaa';
e.currentTarget.style.background = '#1a1a1a';
e.currentTarget.style.background = 'rgba(34, 197, 94, 0.2)';
e.currentTarget.style.borderColor = '#22c55e';
e.currentTarget.style.boxShadow = '0 0 20px rgba(34, 197, 94, 0.25)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#333';
e.currentTarget.style.color = '#888';
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.background = 'rgba(34, 197, 94, 0.1)';
e.currentTarget.style.borderColor = 'rgba(34, 197, 94, 0.3)';
e.currentTarget.style.boxShadow = '0 0 12px rgba(34, 197, 94, 0.15)';
}}
>
CAPTURE
<span style={{
width: '22px',
height: '22px',
width: '18px',
height: '18px',
borderRadius: '50%',
background: '#22c55e',
color: '#0a0a0a',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '14px',
fontSize: '12px',
fontWeight: 700
}}>+</span>
Add Stuff
</button>
);
}
// Expanded state
// Expanded state - full width overlay
return (
<div style={{
position: 'absolute',
top: '60px',
left: '20px',
right: '20px',
display: 'flex',
flexDirection: 'column',
gap: '12px',
width: '100%',
background: 'transparent',
padding: '0',
background: '#0a0a0a',
padding: '16px',
borderRadius: '12px',
border: '1px solid #2a2a2a',
zIndex: 100,
animation: 'fadeIn 150ms ease-out'
}}>
{/* Mode tabs */}
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
{MODE_CONFIG.map((mode) => {
const isActive = effectiveMode === mode.key;
return (
<button
key={mode.key}
type="button"
onClick={() => handleModeClick(mode.key)}
title={mode.hint}
style={{
display: 'flex',
alignItems: 'center',
gap: '6px',
padding: '8px 14px',
borderRadius: '8px',
border: isActive ? '1px solid #333' : '1px solid transparent',
background: isActive ? '#1a1a1a' : 'transparent',
color: isActive ? '#fff' : '#666',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.15s ease',
textTransform: 'uppercase',
letterSpacing: '0.05em'
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.color = '#999';
e.currentTarget.style.background = '#1a1a1a';
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.color = '#666';
e.currentTarget.style.background = 'transparent';
}
}}
>
<span style={{ display: 'flex', opacity: isActive ? 1 : 0.7 }}>{mode.icon}</span>
{mode.label}
</button>
);
})}
{/* Header */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}>
<span style={{
color: '#22c55e',
fontSize: '11px',
fontWeight: 600,
letterSpacing: '0.1em',
textTransform: 'uppercase',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{
width: '18px',
height: '18px',
borderRadius: '50%',
background: '#22c55e',
color: '#0a0a0a',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: 700
}}>+</span>
Add Stuff
</span>
{/* Close button */}
<button
onClick={() => {
setIsExpanded(false);
setInput('');
setDescription('');
}}
style={{
marginLeft: 'auto',
padding: '8px',
padding: '6px',
background: 'transparent',
border: 'none',
color: '#666',
@@ -274,28 +182,29 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
</button>
</div>
{/* Input area - consistent height */}
{/* Input area - expanded */}
<div style={{ position: 'relative' }}>
<textarea
value={input}
onChange={(e) => handleInputChange(e.target.value)}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={currentPlaceholder}
placeholder="Paste a URL, note, or transcript — RA-H will figure out what it is."
disabled={isPosting || isSoftLimited}
autoFocus
style={{
width: '100%',
minHeight: '80px',
maxHeight: '200px',
padding: '12px 14px',
minHeight: '140px',
maxHeight: '300px',
padding: '14px 16px',
background: '#0a0a0a',
border: '1px solid #1f1f1f',
borderRadius: '8px',
borderRadius: '10px',
color: '#e5e5e5',
fontSize: '14px',
fontFamily: 'inherit',
outline: 'none',
resize: 'none',
lineHeight: '1.6',
transition: 'border-color 0.15s ease'
}}
onFocus={(e) => {
@@ -307,51 +216,6 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
/>
</div>
{/* Description field - optional */}
<div style={{ position: 'relative' }}>
<textarea
value={description}
onChange={(e) => {
if (e.target.value.length <= 280) {
setDescription(e.target.value);
}
}}
onKeyDown={handleKeyDown}
placeholder="This is a... (optional)"
disabled={isPosting || isSoftLimited}
style={{
width: '100%',
minHeight: '50px',
maxHeight: '100px',
padding: '10px 14px',
background: '#0a0a0a',
border: '1px solid #1f1f1f',
borderRadius: '8px',
color: '#a5a5a5',
fontSize: '13px',
fontFamily: 'inherit',
outline: 'none',
resize: 'none',
transition: 'border-color 0.15s ease'
}}
onFocus={(e) => {
e.currentTarget.style.borderColor = '#333';
}}
onBlur={(e) => {
e.currentTarget.style.borderColor = '#1f1f1f';
}}
/>
<span style={{
position: 'absolute',
bottom: '8px',
right: '10px',
fontSize: '10px',
color: description.length >= 260 ? '#f59e0b' : '#525252'
}}>
{description.length}/280
</span>
</div>
{/* Footer */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ fontSize: '11px', color: '#525252' }}>
@@ -361,8 +225,8 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
onClick={handleSubmit}
disabled={!input.trim() || isPosting || isSoftLimited}
style={{
width: '36px',
height: '36px',
width: '38px',
height: '38px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -386,9 +250,9 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
}}
>
{isPosting ? (
<span style={{
width: '14px',
height: '14px',
<span style={{
width: '14px',
height: '14px',
border: '2px solid #0a0a0a',
borderTopColor: 'transparent',
borderRadius: '50%',