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
+13 -14
View File
@@ -202,11 +202,11 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
height: '100%',
position: 'relative'
}}>
{/* Top Bar - just collapse button */}
{/* Top Bar - collapse button + Add Stuff */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
justifyContent: 'space-between',
marginBottom: '24px'
}}>
{onCollapse && (
@@ -238,6 +238,13 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
<Minimize2 size={14} />
</button>
)}
{/* Add Stuff - top right */}
<div style={{ maxWidth: '200px' }}>
<QuickAddInput
activeDelegations={orderedDelegations}
onSubmit={handleQuickAddSubmit}
/>
</div>
</div>
{/* Center Section - Start button centered */}
@@ -280,7 +287,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
}
}}
>
<span
<span
className="start-text"
style={{
color: '#737373',
@@ -293,7 +300,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
>
Start
</span>
<div
<div
className="start-icon"
style={{
width: '44px',
@@ -314,14 +321,6 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
</div>
</button>
</div>
{/* Quick Add - at bottom, full width with padding */}
<div style={{ width: '100%', padding: '0 16px 16px 16px' }}>
<QuickAddInput
activeDelegations={orderedDelegations}
onSubmit={handleQuickAddSubmit}
/>
</div>
</div>
) : null}
@@ -399,7 +398,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
e.currentTarget.style.borderColor = '#1f1f1f';
}}
>
<span style={{
<span style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
@@ -413,7 +412,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, activeDimension
fontWeight: 600,
flexShrink: 0
}}>+</span>
Capture
Add Stuff
</button>
</div>
+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%',
+339 -42
View File
@@ -68,6 +68,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
const [nodeSearchSuggestions, setNodeSearchSuggestions] = useState<NodeSearchResult[]>([]);
const [selectedSearchIndex, setSelectedSearchIndex] = useState(0);
const [edgeExplanation, setEdgeExplanation] = useState('');
const [pendingEdgeTarget, setPendingEdgeTarget] = useState<{ id: number; title: string } | null>(null);
const [deletingEdge, setDeletingEdge] = useState<number | null>(null);
const [edgeEditingId, setEdgeEditingId] = useState<number | null>(null);
const [edgeEditingValue, setEdgeEditingValue] = useState<string>('');
@@ -296,7 +297,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
from_node_id: nodeId,
to_node_id: toId,
source: 'user',
context: { explanation: 'Referenced via @ mention', created_via: 'at_mention' }
explanation: 'Referenced via @ mention'
})
});
}));
@@ -345,7 +346,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
from_node_id: nodeId,
to_node_id: toId,
source: 'user',
context: { explanation: 'Referenced via @ mention', created_via: 'at_mention' }
explanation: 'Referenced via @ mention'
})
});
}));
@@ -540,7 +541,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
from_node_id: sourceNodeId,
to_node_id: nodeId,
source: 'user',
context: { explanation: 'Referenced via @ mention', created_via: 'at_mention' }
explanation: 'Referenced via @ mention'
})
});
} catch (e) {
@@ -630,7 +631,8 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
// Edge management functions (following same patterns as node functions)
const handleEdgeNodeSelect = (targetNodeId: number, _targetNodeTitle?: string) => {
createEdgeWithExplanation(targetNodeId, '');
setPendingEdgeTarget({ id: targetNodeId, title: _targetNodeTitle || `Node ${targetNodeId}` });
setEdgeExplanation('');
};
// Handle node search keyboard navigation
@@ -648,13 +650,18 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
};
const handleSelectNodeSuggestion = (suggestion: NodeSearchResult) => {
createEdgeWithExplanation(suggestion.id, '');
setNodeSearchQuery('');
setPendingEdgeTarget({ id: suggestion.id, title: suggestion.title });
setEdgeExplanation('');
setNodeSearchSuggestions([]);
};
const createEdgeWithExplanation = async (targetNodeId: number, explanation: string) => {
if (activeNodeId === null) return;
const trimmed = (explanation || '').trim();
if (!trimmed) {
alert('Please add a short explanation for why this connection exists.');
return;
}
try {
const response = await fetch('/api/edges', {
method: 'POST',
@@ -665,10 +672,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
from_node_id: activeNodeId,
to_node_id: targetNodeId,
source: 'user',
context: {
explanation: explanation,
created_via: 'focus_panel'
}
explanation: trimmed
}),
});
@@ -682,6 +686,10 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
// Reset state
setAddingEdge(null);
setEdgeExplanation('');
setPendingEdgeTarget(null);
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
setShowConnectionsModal(false);
} catch (error) {
console.error('Error creating edge:', error);
@@ -807,14 +815,14 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
key={suggestion.id}
onClick={() => handleSelectNodeSuggestion(suggestion)}
style={{
padding: '14px 16px',
padding: '10px 14px',
cursor: 'pointer',
borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
background: index === selectedSearchIndex ? '#1a1a1a' : 'transparent',
transition: 'background 100ms ease',
display: 'flex',
alignItems: 'center',
gap: '12px'
gap: '10px'
}}
onMouseEnter={(e) => {
if (index !== selectedSearchIndex) {
@@ -831,13 +839,13 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontSize: '9px',
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '4px 8px',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '28px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
@@ -845,7 +853,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
{suggestion.id}
</span>
<span style={{
fontSize: '15px',
fontSize: '13px',
color: '#e5e5e5',
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -861,6 +869,123 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
))}
</div>
)}
{/* Explanation (required) */}
{pendingEdgeTarget && (
<div style={{
marginTop: '10px',
background: '#0f0f0f',
border: '1px solid #262626',
borderRadius: '12px',
padding: '12px',
display: 'flex',
flexDirection: 'column',
gap: '10px'
}}>
<div style={{ color: '#e5e5e5', fontSize: '13px', fontWeight: 500 }}>
Connecting to: <span style={{ color: '#a3e635' }}>{pendingEdgeTarget.title}</span>
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{[
{ label: 'Made by', value: 'Created by ' },
{ label: 'Part of', value: 'Part of ' },
{ label: 'Came from', value: 'Came from ' },
{ label: 'Related', value: 'Related to ' },
].map((chip) => (
<button
key={chip.label}
type="button"
onClick={() => {
setEdgeExplanation((prev) => {
const trimmed = (prev || '').trim();
return trimmed.length > 0 ? prev : chip.value;
});
}}
style={{
padding: '6px 10px',
fontSize: '12px',
borderRadius: '999px',
border: '1px solid #262626',
background: '#141414',
color: '#e5e5e5',
cursor: 'pointer',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = '#1a1a1a'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#141414'; }}
title={`Prefill: ${chip.value.trim()}`}
>
{chip.label}
</button>
))}
</div>
<textarea
value={edgeExplanation}
onChange={(e) => setEdgeExplanation(e.target.value)}
placeholder="Why does this connect? (e.g., 'Author of this book', 'Inspired this insight')"
rows={2}
style={{
width: '100%',
resize: 'vertical',
background: '#141414',
border: '1px solid #1f1f1f',
color: '#fafafa',
borderRadius: '10px',
padding: '10px',
fontSize: '13px',
outline: 'none',
fontFamily: 'inherit',
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
createEdgeWithExplanation(pendingEdgeTarget.id, edgeExplanation);
}
if (e.key === 'Escape') {
e.preventDefault();
setPendingEdgeTarget(null);
setEdgeExplanation('');
}
}}
/>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button
onClick={() => {
setPendingEdgeTarget(null);
setEdgeExplanation('');
}}
style={{
padding: '8px 10px',
background: 'transparent',
border: '1px solid #262626',
color: '#a3a3a3',
borderRadius: '10px',
cursor: 'pointer',
fontSize: '12px'
}}
>
Cancel
</button>
<button
onClick={() => createEdgeWithExplanation(pendingEdgeTarget.id, edgeExplanation)}
style={{
padding: '8px 10px',
background: '#22c55e',
border: '1px solid #16a34a',
color: '#0a0a0a',
borderRadius: '10px',
cursor: 'pointer',
fontSize: '12px',
fontWeight: 600
}}
>
Create connection
</button>
</div>
<div style={{ color: '#737373', fontSize: '11px' }}>
Tip: press <span style={{ fontFamily: 'monospace' }}></span>+<span style={{ fontFamily: 'monospace' }}>Enter</span> to create.
</div>
</div>
)}
</div>
{/* Existing Connections */}
@@ -877,11 +1002,11 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
}
const visible = edgesExpanded[activeTab] ? list : list.slice(0, 5);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
{visible.map((connection) => (
<div key={connection.id} style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>
<div key={connection.id} style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '12px' }}>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{
display: 'inline-block',
fontSize: '10px',
@@ -896,7 +1021,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
}}>
{connection.connected_node.id}
</span>
<span style={{ color: '#f8fafc', fontSize: '14px', fontWeight: 500 }}>{connection.connected_node.title}</span>
<span style={{ color: '#f8fafc', fontSize: '13px', fontWeight: 500 }}>{connection.connected_node.title}</span>
</div>
{edgeEditingId === connection.edge.id ? (
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
@@ -958,8 +1083,33 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</div>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{typeof connection.edge.context?.type === 'string' && (
<span style={{
fontSize: '10px',
color: '#a3a3a3',
background: '#1f1f1f',
border: '1px solid #262626',
padding: '2px 6px',
borderRadius: '999px',
textTransform: 'none'
}}>
{String(connection.edge.context.type).replace(/_/g, ' ')}
</span>
)}
{connection.edge.context?.explanation ? (
<span style={{ color: '#94a3b8', fontSize: '12px' }}> {connection.edge.context.explanation}</span>
<span
style={{
color: '#94a3b8',
fontSize: '12px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: 0,
}}
title={String(connection.edge.context.explanation)}
>
{connection.edge.context.explanation}
</span>
) : (
<span style={{ color: '#64748b', fontSize: '12px', fontStyle: 'italic' }}>No explanation</span>
)}
@@ -986,7 +1136,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
disabled={deletingEdge === connection.edge.id}
style={{
color: deletingEdge === connection.edge.id ? '#64748b' : '#94a3b8',
fontSize: '14px',
fontSize: '12px',
background: 'transparent',
border: 'none',
cursor: deletingEdge === connection.edge.id ? 'not-allowed' : 'pointer',
@@ -2127,14 +2277,14 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
setNodeSearchSuggestions([]);
}}
style={{
padding: '14px 20px',
padding: '10px 16px',
cursor: 'pointer',
borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
background: index === selectedSearchIndex ? '#1a1a1a' : 'transparent',
transition: 'background 100ms ease',
display: 'flex',
alignItems: 'center',
gap: '14px'
gap: '10px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#1a1a1a';
@@ -2149,13 +2299,13 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontSize: '9px',
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '4px 8px',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '28px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
@@ -2163,7 +2313,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
{suggestion.id}
</span>
<span style={{
fontSize: '14px',
fontSize: '13px',
color: '#e5e5e5',
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -2196,6 +2346,126 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</div>
)}
{/* Explanation prompt (required) */}
{pendingEdgeTarget && (
<div style={{
marginTop: '10px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '16px',
padding: '16px 18px',
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 24px 48px -12px rgba(0, 0, 0, 0.6)',
display: 'flex',
flexDirection: 'column',
gap: '12px'
}}>
<div style={{ color: '#e5e5e5', fontSize: '13px', fontWeight: 600 }}>
Create connection to: <span style={{ color: '#a3e635' }}>{pendingEdgeTarget.title}</span>
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{[
{ label: 'Made by', value: 'Created by ' },
{ label: 'Part of', value: 'Part of ' },
{ label: 'Came from', value: 'Came from ' },
{ label: 'Related', value: 'Related to ' },
].map((chip) => (
<button
key={chip.label}
type="button"
onClick={() => {
setEdgeExplanation((prev) => {
const trimmed = (prev || '').trim();
return trimmed.length > 0 ? prev : chip.value;
});
}}
style={{
padding: '6px 10px',
fontSize: '12px',
borderRadius: '999px',
border: '1px solid #262626',
background: '#0f0f0f',
color: '#e5e5e5',
cursor: 'pointer',
}}
onMouseEnter={(e) => { e.currentTarget.style.background = '#1a1a1a'; }}
onMouseLeave={(e) => { e.currentTarget.style.background = '#0f0f0f'; }}
title={`Prefill: ${chip.value.trim()}`}
>
{chip.label}
</button>
))}
</div>
<textarea
value={edgeExplanation}
onChange={(e) => setEdgeExplanation(e.target.value)}
placeholder="Why does this connect? (e.g., 'Author of this book', 'Inspired this insight')"
rows={2}
style={{
width: '100%',
resize: 'vertical',
background: '#0f0f0f',
border: '1px solid #333',
color: '#fafafa',
borderRadius: '12px',
padding: '10px 12px',
fontSize: '13px',
outline: 'none',
fontFamily: 'inherit'
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
createEdgeWithExplanation(pendingEdgeTarget.id, edgeExplanation);
}
if (e.key === 'Escape') {
e.preventDefault();
setPendingEdgeTarget(null);
setEdgeExplanation('');
}
}}
autoFocus
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}>
<button
onClick={() => {
setPendingEdgeTarget(null);
setEdgeExplanation('');
}}
style={{
padding: '8px 12px',
background: '#262626',
border: 'none',
borderRadius: '10px',
color: '#a3a3a3',
fontSize: '12px',
fontWeight: 600,
cursor: 'pointer'
}}
>
Cancel
</button>
<button
onClick={() => createEdgeWithExplanation(pendingEdgeTarget.id, edgeExplanation)}
style={{
padding: '8px 12px',
background: '#22c55e',
border: '1px solid #16a34a',
borderRadius: '10px',
color: '#0a0a0a',
fontSize: '12px',
fontWeight: 700,
cursor: 'pointer'
}}
>
Create
</button>
</div>
<div style={{ color: '#737373', fontSize: '11px' }}>
Tip: press <span style={{ fontFamily: 'monospace' }}></span>+<span style={{ fontFamily: 'monospace' }}>Enter</span> to create.
</div>
</div>
)}
{/* Existing Connections */}
{!nodeSearchQuery && (
<div style={{
@@ -2211,7 +2481,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
minHeight: 0
}}>
<div style={{
padding: '16px 20px',
padding: '12px 16px',
borderBottom: '1px solid #1f1f1f',
fontSize: '12px',
color: '#737373',
@@ -2236,15 +2506,15 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
<div
key={connection.id}
style={{
padding: '14px 20px',
padding: '10px 16px',
borderBottom: '1px solid #1f1f1f',
display: 'flex',
flexDirection: 'column',
gap: '8px'
gap: '6px'
}}
>
{/* Connection header row */}
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
<span style={{
display: 'inline-flex',
alignItems: 'center',
@@ -2253,9 +2523,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '4px 8px',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '28px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
@@ -2266,7 +2536,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
onClick={() => onNodeClick?.(connection.connected_node.id)}
style={{
flex: 1,
fontSize: '14px',
fontSize: '13px',
color: '#e5e5e5',
cursor: 'pointer',
overflow: 'hidden',
@@ -2302,7 +2572,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</div>
{/* Description row */}
{edgeEditingId === connection.edge.id ? (
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
<input
value={edgeEditingValue}
onChange={(e) => setEdgeEditingValue(e.target.value)}
@@ -2319,12 +2589,12 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
placeholder="Add explanation..."
style={{
flex: 1,
fontSize: '12px',
fontSize: '11px',
color: '#e5e5e5',
background: '#0a0a0a',
border: '1px solid #333',
borderRadius: '6px',
padding: '8px 10px',
padding: '6px 8px',
outline: 'none',
fontFamily: 'inherit'
}}
@@ -2332,7 +2602,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
<button
onClick={() => saveEdgeExplanation(connection.edge.id, connection.edge.context)}
style={{
padding: '6px 12px',
padding: '5px 10px',
background: '#22c55e',
border: 'none',
borderRadius: '6px',
@@ -2347,7 +2617,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
<button
onClick={cancelEditEdgeExplanation}
style={{
padding: '6px 12px',
padding: '5px 10px',
background: '#262626',
border: 'none',
borderRadius: '6px',
@@ -2368,12 +2638,39 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
color: connection.edge.context?.explanation ? '#888' : '#525252',
cursor: 'pointer',
padding: '4px 0',
fontStyle: connection.edge.context?.explanation ? 'normal' : 'italic'
fontStyle: connection.edge.context?.explanation ? 'normal' : 'italic',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#aaa'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = connection.edge.context?.explanation ? '#888' : '#525252'; }}
>
{(connection.edge.context?.explanation as string) || 'Click to add explanation...'}
{typeof connection.edge.context?.type === 'string' && (
<span style={{
fontSize: '10px',
color: '#a3a3a3',
background: '#0f0f0f',
border: '1px solid #262626',
padding: '2px 6px',
borderRadius: '999px',
flexShrink: 0
}}>
{String(connection.edge.context.type).replace(/_/g, ' ')}
</span>
)}
<span
style={{
flex: 1,
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
title={String(connection.edge.context?.explanation || '')}
>
{(connection.edge.context?.explanation as string) || 'Click to add explanation...'}
</span>
</div>
)}
</div>