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:
co-authored by
Claude Opus 4.5
parent
f7b8b2058c
commit
2cbc950fd4
@@ -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>
|
||||
|
||||
|
||||
@@ -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%',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user