refactor: move edges from modal to dedicated tab in FocusPanel

- Add 'edges' to content tab bar (Notes | Desc | Edges | Source)
- Edges tab shows count badge, has Create/Cancel toggle for search
- Replace full-screen connections modal with inline tab content
- Search input appears conditionally via edgeSearchOpen state
- Connections button near title now switches to edges tab
- Remove ~400 lines of modal overlay code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
“BeeRad”
2026-02-15 09:22:04 +11:00
co-authored by Claude Opus 4.6
parent c23b743580
commit f7374590d9
+172 -547
View File
@@ -110,7 +110,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
const [edgesExpanded, setEdgesExpanded] = useState<{ [key: number]: boolean }>({}); const [edgesExpanded, setEdgesExpanded] = useState<{ [key: number]: boolean }>({});
// Connections section collapsed state (default closed) // Connections section collapsed state (default closed)
const [showConnectionsModal, setShowConnectionsModal] = useState(false); const [edgeSearchOpen, setEdgeSearchOpen] = useState(false);
// Title expanded state for click-to-expand full title // Title expanded state for click-to-expand full title
const [titleExpanded, setTitleExpanded] = useState<{ [key: number]: boolean }>({}); const [titleExpanded, setTitleExpanded] = useState<{ [key: number]: boolean }>({});
@@ -119,7 +119,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
const [regeneratingDescription, setRegeneratingDescription] = useState<number | null>(null); const [regeneratingDescription, setRegeneratingDescription] = useState<number | null>(null);
// Content tab state: 'notes', 'desc', or 'source' // Content tab state: 'notes', 'desc', or 'source'
const [activeContentTab, setActiveContentTab] = useState<'notes' | 'desc' | 'source'>('notes'); const [activeContentTab, setActiveContentTab] = useState<'notes' | 'desc' | 'edges' | 'source'>('notes');
// Desc (description) edit mode state // Desc (description) edit mode state
const [descEditMode, setDescEditMode] = useState(false); const [descEditMode, setDescEditMode] = useState(false);
@@ -1146,7 +1146,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
setAddingEdge(null); setAddingEdge(null);
setEdgeExplanation(''); setEdgeExplanation('');
setPendingEdgeTarget(null); setPendingEdgeTarget(null);
setShowConnectionsModal(false); setEdgeSearchOpen(false);
} catch (error) { } catch (error) {
console.error('Error creating edge:', error); console.error('Error creating edge:', error);
@@ -1184,7 +1184,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
setPendingEdgeTarget(null); setPendingEdgeTarget(null);
setNodeSearchQuery(''); setNodeSearchQuery('');
setNodeSearchSuggestions([]); setNodeSearchSuggestions([]);
setShowConnectionsModal(false); setEdgeSearchOpen(false);
} catch (error) { } catch (error) {
console.error('Error creating edge:', error); console.error('Error creating edge:', error);
@@ -1260,112 +1260,114 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}> <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
{/* Search Section */} {/* Search Section — only visible when toggled */}
<div style={{ marginBottom: '20px' }}> {edgeSearchOpen && (
<div style={{ <div style={{ marginBottom: '12px', position: 'relative' }}>
display: 'flex',
alignItems: 'center',
gap: '16px',
background: '#141414',
padding: '16px 20px',
borderRadius: '12px',
border: '1px solid #262626',
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.02)'
}}>
<svg width="20" height="20" viewBox="0 0 20 20" fill="#525252" style={{ flexShrink: 0 }}>
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
</svg>
<input <input
type="text" type="text"
autoFocus
value={nodeSearchQuery} value={nodeSearchQuery}
onChange={(e) => setNodeSearchQuery(e.target.value)} onChange={(e) => setNodeSearchQuery(e.target.value)}
onKeyDown={handleNodeSearchKeyDown} onKeyDown={(e) => {
placeholder="Connect to node..." if (e.key === 'Escape') {
e.preventDefault();
setEdgeSearchOpen(false);
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
} else {
handleNodeSearchKeyDown(e);
}
}}
placeholder="Search for a node to connect..."
style={{ style={{
flex: 1, width: '100%',
background: 'none', background: '#111',
border: 'none', border: '1px solid #2a2a2a',
outline: 'none', borderRadius: '6px',
color: '#fafafa', padding: '8px 12px',
fontSize: '16px', color: '#e5e5e5',
fontSize: '12px',
fontFamily: 'inherit', fontFamily: 'inherit',
fontWeight: 400 outline: 'none',
}} }}
/> />
{/* Search Suggestions */}
{nodeSearchSuggestions.length > 0 && (
<div style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
marginTop: '4px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '8px',
maxHeight: '200px',
overflowY: 'auto',
zIndex: 10,
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4)'
}}>
{nodeSearchSuggestions.map((suggestion, index) => (
<div
key={suggestion.id}
onClick={() => handleSelectNodeSuggestion(suggestion)}
style={{
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: '10px'
}}
onMouseEnter={(e) => {
if (index !== selectedSearchIndex) {
e.currentTarget.style.background = '#1a1a1a';
}
}}
onMouseLeave={(e) => {
if (index !== selectedSearchIndex) {
e.currentTarget.style.background = 'transparent';
}
}}
>
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '9px',
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
}}>
{suggestion.id}
</span>
<span style={{
fontSize: '13px',
color: '#e5e5e5',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1
}}>
{suggestion.title}
</span>
{index === selectedSearchIndex && (
<span style={{ color: '#525252', fontSize: '13px' }}></span>
)}
</div>
))}
</div>
)}
</div> </div>
)}
{/* Search Suggestions */}
{nodeSearchSuggestions.length > 0 && (
<div style={{
marginTop: '8px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '12px',
maxHeight: '240px',
overflowY: 'auto',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.02)'
}}>
{nodeSearchSuggestions.map((suggestion, index) => (
<div
key={suggestion.id}
onClick={() => handleSelectNodeSuggestion(suggestion)}
style={{
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: '10px'
}}
onMouseEnter={(e) => {
if (index !== selectedSearchIndex) {
e.currentTarget.style.background = '#1a1a1a';
}
}}
onMouseLeave={(e) => {
if (index !== selectedSearchIndex) {
e.currentTarget.style.background = 'transparent';
}
}}
>
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '9px',
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
}}>
{suggestion.id}
</span>
<span style={{
fontSize: '13px',
color: '#e5e5e5',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1
}}>
{suggestion.title}
</span>
{index === selectedSearchIndex && (
<span style={{ color: '#525252', fontSize: '13px' }}></span>
)}
</div>
))}
</div>
)}
</div>
{/* Existing Connections */} {/* Existing Connections */}
<div style={{ flex: 1, overflowY: 'auto' }}> <div style={{ flex: 1, overflowY: 'auto' }}>
@@ -1963,9 +1965,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</h1> </h1>
)} )}
{/* Connections Button */} {/* Connections Button — opens Edges tab */}
<button <button
onClick={() => setShowConnectionsModal(true)} onClick={() => setActiveContentTab('edges')}
style={{ style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -1973,21 +1975,26 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
padding: '4px 8px', padding: '4px 8px',
fontSize: '10px', fontSize: '10px',
fontWeight: 500, fontWeight: 500,
color: '#22c55e', color: activeContentTab === 'edges' ? '#22c55e' : '#666',
background: 'transparent', background: activeContentTab === 'edges' ? '#0f2417' : 'transparent',
border: '1px solid #166534', border: '1px solid',
borderColor: activeContentTab === 'edges' ? '#22c55e' : '#262626',
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.2s', transition: 'all 0.2s',
flexShrink: 0 flexShrink: 0
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = '#0f2417'; if (activeContentTab !== 'edges') {
e.currentTarget.style.borderColor = '#22c55e'; e.currentTarget.style.color = '#22c55e';
e.currentTarget.style.borderColor = '#166534';
}
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'transparent'; if (activeContentTab !== 'edges') {
e.currentTarget.style.borderColor = '#166534'; e.currentTarget.style.color = '#666';
e.currentTarget.style.borderColor = '#262626';
}
}} }}
title="Connections" title="Connections"
> >
@@ -2140,7 +2147,24 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
Desc Desc
</button> </button>
<button <button
onClick={() => { setActiveContentTab('source'); setDescEditMode(false); setNotesEditMode(false); }} onClick={() => { setActiveContentTab('edges'); setDescEditMode(false); setNotesEditMode(false); setSourceEditMode(false); }}
style={{
padding: '8px 16px',
fontSize: '11px',
fontWeight: activeContentTab === 'edges' ? 600 : 400,
color: activeContentTab === 'edges' ? '#e5e5e5' : '#666',
background: 'transparent',
border: 'none',
borderBottom: activeContentTab === 'edges' ? '2px solid #22c55e' : '2px solid transparent',
cursor: 'pointer',
transition: 'all 0.15s',
marginBottom: '-1px'
}}
>
Edges{activeTab && edgesData[activeTab]?.length ? ` (${edgesData[activeTab].length})` : ''}
</button>
<button
onClick={() => { setActiveContentTab('source'); setDescEditMode(false); setNotesEditMode(false); setEdgeSearchOpen(false); }}
style={{ style={{
padding: '8px 16px', padding: '8px 16px',
fontSize: '11px', fontSize: '11px',
@@ -2256,6 +2280,36 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
/> />
</div> </div>
)} )}
{/* Action button for Edges tab */}
{activeContentTab === 'edges' && (
<div style={{ display: 'flex', gap: '8px', marginBottom: '4px' }}>
<button
onClick={() => {
setEdgeSearchOpen(!edgeSearchOpen);
if (edgeSearchOpen) {
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
}
}}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '4px 8px',
fontSize: '10px',
color: edgeSearchOpen ? '#000' : '#22c55e',
background: edgeSearchOpen ? '#22c55e' : 'transparent',
border: '1px solid #166534',
borderRadius: '4px',
cursor: 'pointer',
fontWeight: 600,
}}
>
<Plus size={12} />
{edgeSearchOpen ? 'Cancel' : 'Create'}
</button>
</div>
)}
</div> </div>
{/* Desc Tab Content */} {/* Desc Tab Content */}
@@ -2724,6 +2778,13 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
</div> </div>
)} )}
{/* Edges Tab Content */}
{activeContentTab === 'edges' && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
{renderConnectionsBody()}
</div>
)}
{/* Source Tab Content */} {/* Source Tab Content */}
{activeContentTab === 'source' && ( {activeContentTab === 'source' && (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
@@ -2966,442 +3027,6 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
onConfirm={handleConfirmNodeDelete} onConfirm={handleConfirmNodeDelete}
onCancel={handleCancelNodeDelete} onCancel={handleCancelNodeDelete}
/> />
{showConnectionsModal && activeTab && (
<div
onClick={(e) => {
if (e.target === e.currentTarget) {
setShowConnectionsModal(false);
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
}
}}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0, 0, 0, 0.85)',
backdropFilter: 'blur(8px)',
display: 'flex',
justifyContent: 'center',
paddingTop: '10vh',
zIndex: 9999,
animation: 'backdropIn 200ms ease-out'
}}
>
<div
style={{
width: '100%',
maxWidth: '640px',
maxHeight: '80vh',
display: 'flex',
flexDirection: 'column',
animation: 'containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1)'
}}
>
{/* Search Input */}
<div style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '16px',
padding: '20px 24px',
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 24px 48px -12px rgba(0, 0, 0, 0.6)'
}}>
<svg width="22" height="22" viewBox="0 0 20 20" fill="#525252" style={{ flexShrink: 0 }}>
<path fillRule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clipRule="evenodd" />
</svg>
<input
autoFocus
type="text"
value={nodeSearchQuery}
onChange={(e) => setNodeSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setShowConnectionsModal(false);
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
} else {
handleNodeSearchKeyDown(e);
}
}}
placeholder="Search to add connection..."
style={{
flex: 1,
background: 'none',
border: 'none',
outline: 'none',
color: '#fafafa',
fontSize: '18px',
fontFamily: 'inherit',
fontWeight: 400
}}
/>
<kbd style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
padding: '4px 8px',
background: '#262626',
borderRadius: '6px',
fontSize: '11px',
fontFamily: 'inherit',
color: '#737373',
border: '1px solid #333'
}}>
esc
</kbd>
</div>
{/* Search Results */}
{nodeSearchSuggestions.length > 0 && (
<div style={{
marginTop: '8px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '16px',
overflow: 'hidden',
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 24px 48px -12px rgba(0, 0, 0, 0.6)',
animation: 'resultsIn 150ms ease-out',
maxHeight: '200px',
overflowY: 'auto'
}}>
{nodeSearchSuggestions.map((suggestion, index) => (
<div
key={suggestion.id}
onClick={() => {
handleSelectNodeSuggestion(suggestion);
setNodeSearchQuery('');
setNodeSearchSuggestions([]);
}}
style={{
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: '10px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#1a1a1a';
}}
onMouseLeave={(e) => {
if (index !== selectedSearchIndex) {
e.currentTarget.style.background = 'transparent';
}
}}
>
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '9px',
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
}}>
{suggestion.id}
</span>
<span style={{
fontSize: '13px',
color: '#e5e5e5',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
flex: 1
}}>
{suggestion.title}
</span>
{index === selectedSearchIndex && (
<span style={{ color: '#525252', fontSize: '13px' }}></span>
)}
</div>
))}
</div>
)}
{/* Empty search state */}
{nodeSearchQuery && nodeSearchSuggestions.length === 0 && (
<div style={{
marginTop: '8px',
padding: '24px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '16px',
color: '#525252',
fontSize: '14px',
textAlign: 'center'
}}>
No results for "{nodeSearchQuery}"
</div>
)}
{/* Existing Connections */}
{!nodeSearchQuery && (
<div style={{
marginTop: '16px',
background: '#141414',
border: '1px solid #262626',
borderRadius: '16px',
overflow: 'hidden',
boxShadow: '0 0 0 1px rgba(255, 255, 255, 0.04), 0 8px 24px -8px rgba(0, 0, 0, 0.4)',
flex: 1,
display: 'flex',
flexDirection: 'column',
minHeight: 0
}}>
<div style={{
padding: '12px 16px',
borderBottom: '1px solid #1f1f1f',
fontSize: '12px',
color: '#737373',
letterSpacing: '0.05em',
textTransform: 'uppercase',
fontWeight: 600
}}>
Existing Connections ({(edgesData[activeTab] || []).length})
</div>
<div style={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>
{loadingEdges.has(activeTab) ? (
<div style={{ padding: '24px', color: '#666', fontSize: '13px', textAlign: 'center' }}>
Loading...
</div>
) : (edgesData[activeTab] || []).length === 0 ? (
<div style={{ padding: '32px 24px', color: '#525252', fontSize: '14px', textAlign: 'center' }}>
No connections yet. Search above to add one.
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column' }}>
{(edgesData[activeTab] || []).map((connection) => (
<div
key={connection.id}
style={{
padding: '10px 16px',
borderBottom: '1px solid #1f1f1f',
display: 'flex',
flexDirection: 'column',
gap: '6px'
}}
>
{/* Connection header row */}
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
{/* Direction arrow: → if current node is FROM (outgoing), ← if current node is TO (incoming) */}
<span style={{
fontSize: '14px',
color: connection.edge.from_node_id === activeTab ? '#22c55e' : '#f59e0b',
fontWeight: 600,
width: '18px',
textAlign: 'center',
flexShrink: 0
}}>
{connection.edge.from_node_id === activeTab ? '→' : '←'}
</span>
<span style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '10px',
fontWeight: 600,
color: '#0a0a0a',
background: '#22c55e',
padding: '3px 6px',
borderRadius: '6px',
minWidth: '24px',
textAlign: 'center',
flexShrink: 0,
fontFamily: "'SF Mono', 'Fira Code', monospace"
}}>
{connection.connected_node.id}
</span>
<span style={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
{getNodeIcon(connection.connected_node, dimensionIcons, 12)}
</span>
<span
onClick={() => onNodeClick?.(connection.connected_node.id)}
style={{
flex: 1,
fontSize: '13px',
color: '#e5e5e5',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#22c55e'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#e5e5e5'; }}
>
{connection.connected_node.title}
</span>
<button
onClick={() => deleteEdge(connection.edge.id)}
disabled={deletingEdge === connection.edge.id}
style={{
padding: '6px',
background: 'transparent',
border: 'none',
color: '#525252',
cursor: deletingEdge === connection.edge.id ? 'not-allowed' : 'pointer',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.15s ease',
opacity: deletingEdge === connection.edge.id ? 0.5 : 1
}}
onMouseEnter={(e) => { e.currentTarget.style.color = '#ef4444'; e.currentTarget.style.background = '#1f1f1f'; }}
onMouseLeave={(e) => { e.currentTarget.style.color = '#525252'; e.currentTarget.style.background = 'transparent'; }}
>
<Trash2 size={14} />
</button>
</div>
{/* Description row */}
{edgeEditingId === connection.edge.id ? (
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
<input
value={edgeEditingValue}
onChange={(e) => setEdgeEditingValue(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveEdgeExplanation(connection.edge.id, connection.edge.context);
} else if (e.key === 'Escape') {
e.preventDefault();
cancelEditEdgeExplanation();
}
}}
autoFocus
placeholder="Add explanation..."
style={{
flex: 1,
fontSize: '11px',
color: '#e5e5e5',
background: '#0a0a0a',
border: '1px solid #333',
borderRadius: '6px',
padding: '6px 8px',
outline: 'none',
fontFamily: 'inherit'
}}
/>
<button
onClick={() => saveEdgeExplanation(connection.edge.id, connection.edge.context)}
style={{
padding: '5px 10px',
background: '#22c55e',
border: 'none',
borderRadius: '6px',
color: '#0a0a0a',
fontSize: '11px',
fontWeight: 600,
cursor: 'pointer'
}}
>
Save
</button>
<button
onClick={cancelEditEdgeExplanation}
style={{
padding: '5px 10px',
background: '#262626',
border: 'none',
borderRadius: '6px',
color: '#999',
fontSize: '11px',
fontWeight: 600,
cursor: 'pointer'
}}
>
Cancel
</button>
</div>
) : (
<div
onClick={() => startEditEdgeExplanation(connection.edge.id, connection.edge.context?.explanation as string | undefined)}
style={{
fontSize: '12px',
color: connection.edge.context?.explanation ? '#888' : '#525252',
cursor: 'pointer',
padding: '4px 0',
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'; }}
>
{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>
))}
</div>
)}
</div>
</div>
)}
</div>
<style jsx>{`
@keyframes backdropIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes containerIn {
from {
opacity: 0;
transform: scale(0.96) translateY(-8px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
@keyframes resultsIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`}</style>
</div>
)}
{/* Tab Context Menu */} {/* Tab Context Menu */}
{contextMenu && ( {contextMenu && (
<div <div