sync: UI polish - welcome screen, connections modal, search modals
- Redesigned welcome screen with centered Start button and CAPTURE quick add - Improved QuickAddInput with elegant mode tabs and circular submit button - Restored existing connections view in connections modal with edit/delete - Spotlight-style search modals across all search experiences - Three-panel color system and neutral design updates - Fixed textarea resize jump and node ID badge styling
This commit is contained in:
@@ -130,3 +130,65 @@ html, body {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal/Popup animations */
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes modalFadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal utility classes */
|
||||
.modal-backdrop {
|
||||
animation: modalFadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
animation: modalSlideUp 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Text truncation utilities */
|
||||
.text-truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-truncate-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text-truncate-3 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -111,6 +111,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleCreated = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ delegation: AgentDelegation }>).detail;
|
||||
@@ -187,151 +188,132 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#0a0a0a', position: 'relative' }}>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent', position: 'relative' }}>
|
||||
{/* Mode Header */}
|
||||
{mode === 'quickadd' ? (
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '24px',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Collapse button - top right */}
|
||||
{onCollapse && (
|
||||
<button
|
||||
onClick={onCollapse}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
left: '12px',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
title="Collapse chat panel (⌘\)"
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = '#666';
|
||||
}}
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
{/* Top spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Session Section */}
|
||||
<div style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
padding: '20px',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
maxWidth: '600px'
|
||||
}}>
|
||||
<pre style={{
|
||||
color: '#22c55e',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, Courier, monospace",
|
||||
lineHeight: '1.1',
|
||||
margin: '0 0 20px 0',
|
||||
letterSpacing: '0',
|
||||
fontWeight: 700,
|
||||
textShadow: '0 0 10px rgba(139, 212, 80, 0.4)'
|
||||
}}>{`
|
||||
██████╗ █████╗ ██╗ ██╗
|
||||
██╔══██╗██╔══██╗ ██║ ██║
|
||||
██████╔╝███████║█████╗███████║
|
||||
██╔══██╗██╔══██║╚════╝██╔══██║
|
||||
██║ ██║██║ ██║ ██║ ██║
|
||||
╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝
|
||||
`}</pre>
|
||||
<button
|
||||
onClick={() => {
|
||||
setMode('session');
|
||||
setRahMode('easy'); // Always start new session in easy mode
|
||||
}}
|
||||
style={{
|
||||
width: 'auto',
|
||||
padding: '14px 32px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#0a0a0a',
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
letterSpacing: '0.02em'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#2dd46d';
|
||||
e.currentTarget.style.boxShadow = '0 4px 16px rgba(34, 197, 94, 0.4)';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = '#22c55e';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
}}
|
||||
>
|
||||
Start Session
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Bottom spacer */}
|
||||
<div style={{ flex: 1 }} />
|
||||
|
||||
{/* Divider */}
|
||||
{/* Top Bar - just collapse button */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
width: '100%',
|
||||
maxWidth: '600px',
|
||||
marginBottom: '12px'
|
||||
justifyContent: 'flex-start',
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<div style={{ flex: 1, height: '1px', background: '#1a1a1a' }} />
|
||||
<span style={{
|
||||
color: '#6b6b6b',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
padding: '0 12px'
|
||||
}}>or</span>
|
||||
<div style={{ flex: 1, height: '1px', background: '#1a1a1a' }} />
|
||||
{onCollapse && (
|
||||
<button
|
||||
onClick={onCollapse}
|
||||
style={{
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
title="Collapse chat panel (⌘\)"
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = '#666';
|
||||
}}
|
||||
>
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Add Section */}
|
||||
{/* Center Section - Start button centered */}
|
||||
<div style={{
|
||||
background: 'transparent',
|
||||
padding: '0',
|
||||
width: '100%',
|
||||
maxWidth: '600px',
|
||||
marginBottom: '12px'
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setMode('session');
|
||||
setRahMode('easy');
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '14px',
|
||||
padding: '0',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
const text = e.currentTarget.querySelector('.start-text') as HTMLElement;
|
||||
const icon = e.currentTarget.querySelector('.start-icon') as HTMLElement;
|
||||
if (text) text.style.color = '#22c55e';
|
||||
if (icon) {
|
||||
icon.style.transform = 'translateY(-3px)';
|
||||
icon.style.boxShadow = '0 8px 20px rgba(34, 197, 94, 0.3), 0 0 0 4px rgba(34, 197, 94, 0.1)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
const text = e.currentTarget.querySelector('.start-text') as HTMLElement;
|
||||
const icon = e.currentTarget.querySelector('.start-icon') as HTMLElement;
|
||||
if (text) text.style.color = '#737373';
|
||||
if (icon) {
|
||||
icon.style.transform = 'translateY(0)';
|
||||
icon.style.boxShadow = '0 0 0 0 rgba(34, 197, 94, 0)';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="start-text"
|
||||
style={{
|
||||
color: '#737373',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase',
|
||||
transition: 'color 0.2s ease'
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</span>
|
||||
<div
|
||||
className="start-icon"
|
||||
style={{
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
borderRadius: '50%',
|
||||
background: '#22c55e',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
boxShadow: '0 0 0 0 rgba(34, 197, 94, 0)'
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#0a0a0a" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 19V5"/>
|
||||
<path d="M5 12l7-7 7 7"/>
|
||||
</svg>
|
||||
</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}
|
||||
@@ -375,7 +357,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
<Minimize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
{/* Quick Add button - positioned at far right */}
|
||||
{/* Capture button - positioned at far right */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
@@ -387,29 +369,31 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
zIndex: 20
|
||||
}}>
|
||||
<button
|
||||
onClick={() => {
|
||||
window.dispatchEvent(new CustomEvent('rah:switch-quickadd'));
|
||||
}}
|
||||
onClick={() => setMode('quickadd')}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '12px',
|
||||
cursor: 'pointer',
|
||||
padding: '4px 8px',
|
||||
transition: 'color 0.2s',
|
||||
fontFamily: 'inherit',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '6px 12px',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '6px',
|
||||
color: '#e5e5e5',
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
fontFamily: 'inherit',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = '#d0d0d0';
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = '#fff';
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
@@ -419,14 +403,14 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
borderRadius: '50%',
|
||||
background: '#fff',
|
||||
background: '#22c55e',
|
||||
color: '#0a0a0a',
|
||||
fontSize: '12px',
|
||||
lineHeight: 1,
|
||||
fontWeight: 300,
|
||||
fontWeight: 600,
|
||||
flexShrink: 0
|
||||
}}>+</span>
|
||||
Quick Add
|
||||
Capture
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -565,7 +549,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px 0 12px;
|
||||
background: #0a0a0a;
|
||||
background: #101010;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
@@ -577,7 +561,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
}
|
||||
|
||||
.agent-tabs::-webkit-scrollbar-track {
|
||||
background: #0d0d0d;
|
||||
background: #131313;
|
||||
}
|
||||
|
||||
.agent-tabs::-webkit-scrollbar-thumb {
|
||||
@@ -598,7 +582,7 @@ export default function AgentsPanel({ openTabsData, activeTabId, onNodeClick, on
|
||||
}
|
||||
|
||||
.agent-tab-wrapper.active {
|
||||
background: #141414;
|
||||
background: #181818;
|
||||
}
|
||||
|
||||
.agent-tab {
|
||||
|
||||
@@ -25,9 +25,9 @@ const MODE_CONFIG: Array<{
|
||||
{
|
||||
key: 'link',
|
||||
label: 'Link',
|
||||
hint: 'Drop URLs for auto YouTube/PDF/Web extraction',
|
||||
hint: 'Drop URLs for auto extraction',
|
||||
icon: (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<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>
|
||||
@@ -36,26 +36,23 @@ const MODE_CONFIG: Array<{
|
||||
{
|
||||
key: 'note',
|
||||
label: 'Note',
|
||||
hint: 'Quick jot — no extraction, no summary',
|
||||
hint: 'Quick note, no processing',
|
||||
icon: (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<path d="M12 20h9" strokeLinecap="round" />
|
||||
<path d="M12 4h9" strokeLinecap="round" />
|
||||
<path d="M4 4h1v16H4z" />
|
||||
<path d="M7 9h7" strokeLinecap="round" />
|
||||
<path d="M7 13h5" strokeLinecap="round" />
|
||||
<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 messy transcripts — store raw text + summary',
|
||||
hint: 'Paste conversations',
|
||||
icon: (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
|
||||
<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" />
|
||||
<path d="M7 8h10" strokeLinecap="round" />
|
||||
<path d="M7 12h6" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -67,18 +64,13 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [manualMode, setManualMode] = useState<QuickAddIntent | null>(null);
|
||||
const [autoMode, setAutoMode] = useState<QuickAddIntent>('link');
|
||||
const [autoReason, setAutoReason] = useState<string | null>(null);
|
||||
|
||||
const effectiveMode: QuickAddIntent = manualMode ?? autoMode;
|
||||
|
||||
const currentPlaceholder = useMemo(() => {
|
||||
if (effectiveMode === 'note') {
|
||||
return 'Write a quick note — no extraction, just append to your graph';
|
||||
}
|
||||
if (effectiveMode === 'chat') {
|
||||
return 'Paste any conversation (ChatGPT, Claude, etc.) and we will store raw text + summary';
|
||||
}
|
||||
return "Drop links, URL's, ideas or notes to add new nodes";
|
||||
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;
|
||||
@@ -89,26 +81,15 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
|
||||
|
||||
const inferChatIntent = (value: string) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
setAutoMode('link');
|
||||
setAutoReason(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (manualMode) return;
|
||||
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) ||
|
||||
/\b\d{1,2}:\d{2}\b/.test(trimmed);
|
||||
/You said:|ChatGPT said:|Claude said:|Assistant:|User:/i.test(trimmed);
|
||||
|
||||
if (looksLikeTranscript && trimmed.length > 280) {
|
||||
setAutoMode('chat');
|
||||
setAutoReason('Detected chat transcript');
|
||||
} else {
|
||||
setAutoMode('link');
|
||||
setAutoReason(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -120,7 +101,6 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
|
||||
const handleModeClick = (mode: QuickAddIntent) => {
|
||||
setManualMode(mode);
|
||||
setAutoMode(mode);
|
||||
setAutoReason(null);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
@@ -132,7 +112,7 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
|
||||
setInput('');
|
||||
setManualMode(null);
|
||||
setAutoMode('link');
|
||||
setAutoReason(null);
|
||||
setIsExpanded(false);
|
||||
} catch (error) {
|
||||
console.error('[QuickAddInput] Submit error:', error);
|
||||
} finally {
|
||||
@@ -140,23 +120,155 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const isLinkMode = effectiveMode === 'link';
|
||||
const shouldSubmit = isLinkMode
|
||||
? (e.key === 'Enter' && !e.shiftKey)
|
||||
: (e.key === 'Enter' && (e.metaKey || e.ctrlKey));
|
||||
|
||||
if (shouldSubmit) {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
setIsExpanded(false);
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const renderInputField = () => {
|
||||
if (effectiveMode === 'link') {
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
// Collapsed state - visible trigger
|
||||
if (!isExpanded) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
style={{
|
||||
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',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.borderColor = '#444';
|
||||
e.currentTarget.style.color = '#aaa';
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = '#333';
|
||||
e.currentTarget.style.color = '#888';
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
borderRadius: '50%',
|
||||
background: '#22c55e',
|
||||
color: '#0a0a0a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px',
|
||||
fontWeight: 700
|
||||
}}>+</span>
|
||||
CAPTURE
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// Expanded state
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
width: '100%',
|
||||
background: 'transparent',
|
||||
padding: '0',
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsExpanded(false);
|
||||
setInput('');
|
||||
}}
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
padding: '8px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'color 0.15s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#999'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input area - consistent height */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
@@ -164,269 +276,116 @@ export default function QuickAddInput({ activeDelegations, onSubmit }: QuickAddI
|
||||
disabled={isPosting || isSoftLimited}
|
||||
autoFocus
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
width: '100%',
|
||||
minHeight: '80px',
|
||||
maxHeight: '200px',
|
||||
padding: '12px 14px',
|
||||
background: '#0a0a0a',
|
||||
border: '2px solid #22c55e',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '8px',
|
||||
color: '#e5e5e5',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit',
|
||||
outline: 'none',
|
||||
transition: 'all 0.15s ease',
|
||||
boxShadow: '0 0 0 3px rgba(34, 197, 94, 0.15)'
|
||||
resize: 'none',
|
||||
transition: 'border-color 0.15s ease'
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.25)';
|
||||
e.currentTarget.style.borderColor = '#333';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.15)';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
|
||||
return (
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={currentPlaceholder}
|
||||
disabled={isPosting || isSoftLimited}
|
||||
autoFocus
|
||||
rows={effectiveMode === 'chat' ? 6 : 4}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '12px 16px',
|
||||
background: '#0a0a0a',
|
||||
border: '2px solid #22c55e',
|
||||
borderRadius: '6px',
|
||||
color: '#e5e5e5',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
outline: 'none',
|
||||
transition: 'all 0.15s ease',
|
||||
resize: 'vertical',
|
||||
minHeight: effectiveMode === 'chat' ? '150px' : '110px',
|
||||
boxShadow: '0 0 0 3px rgba(34, 197, 94, 0.15)'
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.25)';
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 3px rgba(34, 197, 94, 0.15)';
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}>
|
||||
{!isExpanded ? (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '16px'
|
||||
}}>
|
||||
<div style={{
|
||||
color: '#6b6b6b',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
Quickly add stuff
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsExpanded(true)}
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
{/* Footer */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<span style={{ fontSize: '11px', color: '#525252' }}>
|
||||
⌘↵ to submit · esc to close
|
||||
</span>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!input.trim() || isPosting || isSoftLimited}
|
||||
style={{
|
||||
width: '36px',
|
||||
height: '36px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0',
|
||||
background: input.trim() && !isPosting ? '#22c55e' : '#262626',
|
||||
border: 'none',
|
||||
borderRadius: '50%',
|
||||
cursor: input.trim() && !isPosting ? 'pointer' : 'not-allowed',
|
||||
transition: 'all 0.2s ease',
|
||||
boxShadow: input.trim() && !isPosting ? '0 0 0 0 rgba(34, 197, 94, 0)' : 'none'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (input.trim() && !isPosting) {
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(34, 197, 94, 0.3)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 0 0 0 rgba(34, 197, 94, 0)';
|
||||
}}
|
||||
>
|
||||
{isPosting ? (
|
||||
<span style={{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
border: '2px solid #0a0a0a',
|
||||
borderTopColor: 'transparent',
|
||||
borderRadius: '50%',
|
||||
color: '#0a0a0a',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 150ms ease',
|
||||
fontSize: '28px',
|
||||
fontWeight: 300,
|
||||
lineHeight: 1
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1.05)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 16px rgba(34, 197, 94, 0.4)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.transform = 'scale(1)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}}
|
||||
title="Add new content"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', gap: '8px', justifyContent: 'center' }}>
|
||||
{MODE_CONFIG.map((mode) => {
|
||||
const isActive = effectiveMode === mode.key && (manualMode === mode.key || (!manualMode && autoMode === mode.key));
|
||||
return (
|
||||
<button
|
||||
key={mode.key}
|
||||
type="button"
|
||||
onClick={() => handleModeClick(mode.key)}
|
||||
title={mode.hint}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '6px 12px',
|
||||
borderRadius: '999px',
|
||||
border: `1px solid ${isActive ? '#22c55e' : '#1f1f1f'}`,
|
||||
background: isActive ? 'rgba(34, 197, 94, 0.15)' : 'transparent',
|
||||
color: isActive ? '#22c55e' : '#9c9c9c',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{mode.icon}</span>
|
||||
{mode.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{autoReason && !manualMode && (
|
||||
<div style={{
|
||||
textAlign: 'center',
|
||||
color: '#9c9c9c',
|
||||
fontSize: '11px',
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
Auto-detected chat transcript
|
||||
</div>
|
||||
animation: 'spin 0.8s linear infinite'
|
||||
}} />
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={input.trim() ? '#0a0a0a' : '#525252'} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 19V5"/>
|
||||
<path d="M5 12l7-7 7 7"/>
|
||||
</svg>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: '8px', alignItems: 'stretch' }}>
|
||||
{renderInputField()}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!input.trim() || isPosting || isSoftLimited}
|
||||
aria-label={isPosting ? 'Adding' : 'Add'}
|
||||
title={isPosting ? 'Adding…' : 'Add (Enter)'}
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: input.trim() && !isPosting && !isSoftLimited ? '#22c55e' : '#22c55e',
|
||||
border: `2px solid #22c55e`,
|
||||
borderRadius: '50%',
|
||||
color: input.trim() && !isPosting && !isSoftLimited ? '#0a0a0a' : '#0a0a0a',
|
||||
cursor: input.trim() && !isPosting && !isSoftLimited ? 'pointer' : 'not-allowed',
|
||||
transition: 'all 150ms ease',
|
||||
opacity: input.trim() && !isPosting && !isSoftLimited ? 1 : 0.7,
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (input.trim() && !isPosting && !isSoftLimited) {
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(34, 197, 94, 0.4)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (input.trim() && !isPosting && !isSoftLimited) {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isPosting ? (
|
||||
<span style={{ fontSize: '12px' }}>•••</span>
|
||||
) : (
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 4l-6.5 6.5 1.42 1.42L11 8.84V20h2V8.84l4.08 3.08 1.42-1.42L12 4z"/>
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Active processing indicator */}
|
||||
{activeCount > 0 && (
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
padding: '10px 12px',
|
||||
background: '#0a1a0a',
|
||||
border: '1px solid #1a3a1a',
|
||||
borderRadius: '6px',
|
||||
borderRadius: '8px',
|
||||
color: '#22c55e',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span style={{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: '#22c55e',
|
||||
animation: 'pulse 2s ease-in-out infinite'
|
||||
}} />
|
||||
<span>
|
||||
{activeCount === 1
|
||||
? 'Adding 1 node to your database...'
|
||||
: `Adding ${activeCount} nodes to your database...`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSoftLimited && (
|
||||
<div style={{
|
||||
padding: '10px 14px',
|
||||
background: '#1a0a00',
|
||||
border: '1px solid #3a2a1a',
|
||||
borderRadius: '6px',
|
||||
color: '#ff9b5c',
|
||||
fontSize: '11px',
|
||||
fontFamily: "'JetBrains Mono', ui-monospace",
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span>⚠</span>
|
||||
<span>Finish one of the 5 active Quick Adds before adding more.</span>
|
||||
Adding {activeCount} node{activeCount > 1 ? 's' : ''}...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -49,15 +49,32 @@ export default function TerminalInput({
|
||||
load();
|
||||
}, [helperId]);
|
||||
|
||||
// Auto-resize textarea
|
||||
// Auto-resize textarea - only resize when needed to avoid jumps
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.style.height = 'auto';
|
||||
const scrollHeight = textareaRef.current.scrollHeight;
|
||||
const lineHeight = 20; // Approximate line height
|
||||
const newRows = Math.min(Math.max(1, Math.floor(scrollHeight / lineHeight)), 5);
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
const lineHeight = 24; // line-height * font-size approximately
|
||||
const minHeight = 32;
|
||||
const maxHeight = 120;
|
||||
|
||||
// Check if content overflows current height (need to grow)
|
||||
const needsGrow = textarea.scrollHeight > textarea.clientHeight;
|
||||
|
||||
// Check if we cleared content significantly (need to shrink)
|
||||
const lineCount = (input.match(/\n/g) || []).length + 1;
|
||||
const estimatedHeight = Math.max(lineCount * lineHeight, minHeight);
|
||||
const needsShrink = !input.trim() || (textarea.clientHeight > estimatedHeight + lineHeight);
|
||||
|
||||
if (needsGrow || needsShrink) {
|
||||
// Only recalculate when necessary
|
||||
textarea.style.height = 'auto';
|
||||
const scrollHeight = textarea.scrollHeight;
|
||||
const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);
|
||||
textarea.style.height = `${newHeight}px`;
|
||||
|
||||
const newRows = Math.min(Math.max(1, Math.floor(newHeight / lineHeight)), 5);
|
||||
setRows(newRows);
|
||||
textareaRef.current.style.height = `${scrollHeight}px`;
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
|
||||
@@ -23,13 +23,14 @@ export default function ConfirmDialog({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
background: 'rgba(0, 0, 0, 0.85)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -39,30 +40,34 @@ export default function ConfirmDialog({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
style={{
|
||||
width: '380px',
|
||||
maxWidth: '100%',
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
background: '#121212',
|
||||
border: '1px solid #2a2a2a',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
color: '#f8fafc',
|
||||
color: '#e5e5e5',
|
||||
marginBottom: '12px',
|
||||
letterSpacing: '0.02em'
|
||||
letterSpacing: '0.01em',
|
||||
fontFamily: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: '#94a3b8',
|
||||
color: '#a8a8a8',
|
||||
marginBottom: '24px',
|
||||
lineHeight: 1.6
|
||||
lineHeight: 1.6,
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word'
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
|
||||
@@ -60,13 +60,14 @@ export default function InputDialog({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
background: 'rgba(0, 0, 0, 0.85)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -81,30 +82,34 @@ export default function InputDialog({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
style={{
|
||||
width: '380px',
|
||||
maxWidth: '100%',
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
background: '#121212',
|
||||
border: '1px solid #2a2a2a',
|
||||
borderRadius: '8px',
|
||||
padding: '24px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
color: '#f8fafc',
|
||||
color: '#e5e5e5',
|
||||
marginBottom: '12px',
|
||||
letterSpacing: '0.02em'
|
||||
letterSpacing: '0.01em',
|
||||
fontFamily: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
||||
}}>
|
||||
{title}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: '#94a3b8',
|
||||
color: '#a8a8a8',
|
||||
marginBottom: '16px',
|
||||
lineHeight: 1.6
|
||||
lineHeight: 1.6,
|
||||
wordWrap: 'break-word',
|
||||
overflowWrap: 'break-word'
|
||||
}}>
|
||||
{message}
|
||||
</div>
|
||||
@@ -118,14 +123,15 @@ export default function InputDialog({
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '10px 12px',
|
||||
background: '#0f0f0f',
|
||||
background: '#0a0a0a',
|
||||
border: '1px solid #2a2a2a',
|
||||
borderRadius: '8px',
|
||||
color: '#f8fafc',
|
||||
borderRadius: '6px',
|
||||
color: '#e5e5e5',
|
||||
fontSize: '13px',
|
||||
marginBottom: '24px',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s'
|
||||
transition: 'border-color 0.2s',
|
||||
fontFamily: "'Geist', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#3a3a3a';
|
||||
|
||||
+480
-160
@@ -734,13 +734,14 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #1f1f1f'
|
||||
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="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<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
|
||||
@@ -748,15 +749,16 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
value={nodeSearchQuery}
|
||||
onChange={(e) => setNodeSearchQuery(e.target.value)}
|
||||
onKeyDown={handleNodeSearchKeyDown}
|
||||
placeholder="Search node to connect..."
|
||||
placeholder="Connect to node..."
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#f8fafc',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
color: '#fafafa',
|
||||
fontSize: '16px',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 400
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -765,26 +767,30 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
{nodeSearchSuggestions.length > 0 && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
background: '#0a0a0a',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '8px',
|
||||
maxHeight: '200px',
|
||||
overflowY: 'auto'
|
||||
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: '12px 16px',
|
||||
padding: '14px 16px',
|
||||
cursor: 'pointer',
|
||||
borderBottom: index < nodeSearchSuggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
|
||||
background: index === selectedSearchIndex ? '#1f1f1f' : 'transparent',
|
||||
transition: 'background 0.1s'
|
||||
background: index === selectedSearchIndex ? '#1a1a1a' : 'transparent',
|
||||
transition: 'background 100ms ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (index !== selectedSearchIndex) {
|
||||
e.currentTarget.style.background = '#151515';
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
@@ -793,30 +799,35 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '4px' }}>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
color: '#000',
|
||||
background: '#22c55e',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '20px',
|
||||
textAlign: 'center',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
{suggestion.id}
|
||||
</span>
|
||||
<span style={{ fontSize: '13px', color: '#f8fafc' }}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
</div>
|
||||
{suggestion.dimensions && suggestion.dimensions.length > 0 && (
|
||||
<div style={{ fontSize: '11px', color: '#94a3b8' }}>
|
||||
{suggestion.dimensions.slice(0, 3).join(', ')}
|
||||
{suggestion.dimensions.length > 3 && ` +${suggestion.dimensions.length - 3} more`}
|
||||
</div>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#0a0a0a',
|
||||
background: '#22c55e',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '28px',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'SF Mono', 'Fira Code', monospace"
|
||||
}}>
|
||||
#{suggestion.id}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '15px',
|
||||
color: '#e5e5e5',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1
|
||||
}}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
{index === selectedSearchIndex && (
|
||||
<span style={{ color: '#525252', fontSize: '13px' }}>↵</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -826,7 +837,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
|
||||
{/* Existing Connections */}
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ fontSize: '12px', color: '#94a3b8', marginBottom: '12px', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
|
||||
<div style={{ fontSize: '12px', color: '#737373', marginBottom: '12px', letterSpacing: '0.02em' }}>
|
||||
Existing Connections
|
||||
</div>
|
||||
{loadingEdges.has(activeTab) ? (
|
||||
@@ -877,7 +888,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
flex: 1,
|
||||
fontSize: '11px',
|
||||
color: '#ddd',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
padding: '4px',
|
||||
@@ -1038,12 +1049,12 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#000' }}>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent' }}>
|
||||
{/* Tab Bar */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
borderBottom: '1px solid #333',
|
||||
background: '#0a0a0a',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
background: '#0f0f0f',
|
||||
flexShrink: 0,
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden'
|
||||
@@ -1068,9 +1079,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderRight: '1px solid #333',
|
||||
background: isActive ? '#000' : '#0a0a0a',
|
||||
borderBottom: isActive ? '2px solid #22c55e' : 'none',
|
||||
borderRight: '1px solid #1a1a1a',
|
||||
background: isActive ? '#121212' : '#0f0f0f',
|
||||
borderBottom: isActive ? '2px solid #666' : 'none',
|
||||
paddingBottom: isActive ? '0' : '2px',
|
||||
minWidth: '120px',
|
||||
maxWidth: '200px'
|
||||
@@ -1315,7 +1326,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
style={{
|
||||
color: '#3b82f6',
|
||||
fontSize: '11px',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '6px',
|
||||
padding: '4px 6px',
|
||||
@@ -1364,7 +1375,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Connections Button */}
|
||||
{/* Connections Button - Green CTA */}
|
||||
<button
|
||||
onClick={() => setShowConnectionsModal(true)}
|
||||
style={{
|
||||
@@ -1374,9 +1385,9 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
padding: '6px 10px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
color: '#94a3b8',
|
||||
color: '#22c55e',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1f1f1f',
|
||||
border: '1px solid #166534',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
textTransform: 'uppercase',
|
||||
@@ -1385,17 +1396,15 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#0f0f0f';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
e.currentTarget.style.color = '#cbd5f5';
|
||||
e.currentTarget.style.background = '#0f2417';
|
||||
e.currentTarget.style.borderColor = '#22c55e';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
e.currentTarget.style.borderColor = '#166534';
|
||||
}}
|
||||
>
|
||||
<Link size={12} />
|
||||
<Link size={14} />
|
||||
{activeTab && edgesData[activeTab] && edgesData[activeTab].length > 0 && (
|
||||
<span style={{
|
||||
fontSize: '10px',
|
||||
@@ -1412,6 +1421,42 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
onClick={() => confirmDeleteNode(activeTab)}
|
||||
disabled={deletingNode === activeTab}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '6px',
|
||||
color: deletingNode === activeTab ? '#994444' : '#525252',
|
||||
background: 'transparent',
|
||||
border: '1px solid #262626',
|
||||
borderRadius: '6px',
|
||||
cursor: deletingNode === activeTab ? 'wait' : 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#dc2626';
|
||||
e.currentTarget.style.borderColor = '#dc2626';
|
||||
e.currentTarget.style.background = 'rgba(220, 38, 38, 0.1)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#525252';
|
||||
e.currentTarget.style.borderColor = '#262626';
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
}
|
||||
}}
|
||||
title="Delete node"
|
||||
>
|
||||
{deletingNode === activeTab ? '...' : <Trash2 size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Title Row with ID and Trash */}
|
||||
@@ -1443,7 +1488,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
fontSize: '20px',
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
padding: '4px 8px',
|
||||
@@ -1474,7 +1519,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
padding: '4px 8px',
|
||||
margin: '0',
|
||||
borderRadius: '0',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: '1px solid transparent',
|
||||
transition: 'border-color 0.2s',
|
||||
flex: 1,
|
||||
@@ -1500,36 +1545,6 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{/* Trash Icon */}
|
||||
<button
|
||||
onClick={() => confirmDeleteNode(activeTab)}
|
||||
disabled={deletingNode === activeTab}
|
||||
style={{
|
||||
color: deletingNode === activeTab ? '#994444' : '#dc2626',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: deletingNode === activeTab ? 'not-allowed' : 'pointer',
|
||||
padding: '4px',
|
||||
transition: 'color 0.2s',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#b91c1c';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (deletingNode !== activeTab) {
|
||||
e.currentTarget.style.color = '#dc2626';
|
||||
}
|
||||
}}
|
||||
title="Delete node"
|
||||
>
|
||||
{deletingNode === activeTab ? '...' : <Trash2 size={14} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1634,7 +1649,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
color: '#e5e5e5',
|
||||
fontSize: '16px', /* Increased from 15px for better readability */
|
||||
lineHeight: '1.7', /* Increased from 1.6 for more comfortable reading */
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderRadius: '0',
|
||||
padding: '12px', /* Increased from 8px to 12px */
|
||||
@@ -1680,7 +1695,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
margin: '0',
|
||||
borderRadius: '0',
|
||||
transition: 'all 0.2s',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
whiteSpace: 'pre-wrap',
|
||||
minHeight: '200px',
|
||||
@@ -1708,7 +1723,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#000',
|
||||
background: 'transparent',
|
||||
flex: 1
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
@@ -1773,7 +1788,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
color: '#e5e5e5',
|
||||
fontSize: '11px',
|
||||
lineHeight: '1.4',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
padding: '8px',
|
||||
@@ -1794,7 +1809,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
lineHeight: '1.4',
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: '1px solid #1a1a1a',
|
||||
borderRadius: '0',
|
||||
transition: 'all 0.2s',
|
||||
@@ -1815,7 +1830,7 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
fontStyle: 'italic',
|
||||
cursor: 'pointer',
|
||||
padding: '8px',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: '1px dashed #1a1a1a',
|
||||
borderRadius: '0',
|
||||
transition: 'all 0.2s'
|
||||
@@ -1854,93 +1869,398 @@ export default function FocusPanel({ openTabs, activeTab, onTabSelect, onNodeCli
|
||||
onConfirm={handleConfirmNodeDelete}
|
||||
onCancel={handleCancelNodeDelete}
|
||||
/>
|
||||
{showConnectionsModal && (
|
||||
{showConnectionsModal && activeTab && (
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
setShowConnectionsModal(false);
|
||||
setNodeSearchQuery('');
|
||||
setNodeSearchSuggestions([]);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.85)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 20,
|
||||
padding: '20px'
|
||||
paddingTop: '10vh',
|
||||
zIndex: 9999,
|
||||
animation: 'backdropIn 200ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '520px',
|
||||
maxHeight: '85vh',
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
width: '100%',
|
||||
maxWidth: '640px',
|
||||
maxHeight: '80vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)'
|
||||
animation: 'containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1)'
|
||||
}}
|
||||
>
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '16px 20px',
|
||||
borderBottom: '1px solid #1f1f1f'
|
||||
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)'
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.02em',
|
||||
color: '#f8fafc',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<Link size={16} />
|
||||
Connections
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowConnectionsModal(false)}
|
||||
<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={{
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#94a3b8',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s'
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fafafa',
|
||||
fontSize: '18px',
|
||||
fontFamily: 'inherit',
|
||||
fontWeight: 400
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#0f0f0f';
|
||||
e.currentTarget.style.borderColor = '#2a2a2a';
|
||||
e.currentTarget.style.color = '#cbd5f5';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.borderColor = '#1f1f1f';
|
||||
e.currentTarget.style.color = '#94a3b8';
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ padding: '20px', overflowY: 'auto', flex: 1 }}>
|
||||
{renderConnectionsBody()}
|
||||
/>
|
||||
<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: '14px 20px',
|
||||
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'
|
||||
}}
|
||||
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: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#0a0a0a',
|
||||
background: '#22c55e',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '28px',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'SF Mono', 'Fira Code', monospace"
|
||||
}}>
|
||||
#{suggestion.id}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
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: '16px 20px',
|
||||
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: '14px 20px',
|
||||
borderBottom: '1px solid #1f1f1f',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
}}
|
||||
>
|
||||
{/* Connection header row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
color: '#0a0a0a',
|
||||
background: '#22c55e',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '28px',
|
||||
textAlign: 'center',
|
||||
flexShrink: 0,
|
||||
fontFamily: "'SF Mono', 'Fira Code', monospace"
|
||||
}}>
|
||||
#{connection.connected_node.id}
|
||||
</span>
|
||||
<span
|
||||
onClick={() => onNodeClick?.(connection.connected_node.id)}
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: '14px',
|
||||
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: '8px', 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: '12px',
|
||||
color: '#e5e5e5',
|
||||
background: '#0a0a0a',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '6px',
|
||||
padding: '8px 10px',
|
||||
outline: 'none',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => saveEdgeExplanation(connection.edge.id, connection.edge.context)}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
background: '#22c55e',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
color: '#0a0a0a',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEditEdgeExplanation}
|
||||
style={{
|
||||
padding: '6px 12px',
|
||||
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'
|
||||
}}
|
||||
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...'}
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -176,134 +176,51 @@ export default function DimensionSearchModal({
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
animation: 'fadeIn 150ms ease-out'
|
||||
}}
|
||||
className="search-backdrop"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Search dimensions"
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
width: '90%',
|
||||
maxWidth: '500px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)',
|
||||
animation: 'slideIn 150ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div ref={modalRef} className="search-container">
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: (suggestions.length > 0 || canCreateNew) ? '1px solid #1f1f1f' : 'none'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f'
|
||||
}}>
|
||||
{/* Search Icon */}
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<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>
|
||||
<div className="search-input-wrapper">
|
||||
<svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search or create dimension..."
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search or create dimension..."
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
padding: '0 4px',
|
||||
lineHeight: 1,
|
||||
transition: 'color 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
|
||||
aria-label="Close search"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="search-shortcut">
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{/* Results */}
|
||||
{suggestions.length > 0 && (
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
<div className="search-results">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion.dimension}
|
||||
onClick={() => handleSelectDimension(suggestion.dimension)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '13px',
|
||||
background: index === selectedIndex ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: index < suggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
|
||||
color: '#e5e5e5',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{
|
||||
color: suggestion.isPriority ? '#22c55e' : '#e5e5e5'
|
||||
}}>
|
||||
{suggestion.dimension}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ color: '#666', fontSize: '11px' }}>
|
||||
{suggestion.count}
|
||||
<span className={`result-name ${suggestion.isPriority ? 'priority' : ''}`}>
|
||||
{suggestion.dimension}
|
||||
</span>
|
||||
<span className="result-count">{suggestion.count}</span>
|
||||
{index === selectedIndex && (
|
||||
<span className="result-hint">↵</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -311,57 +228,225 @@ export default function DimensionSearchModal({
|
||||
|
||||
{/* Create New Option */}
|
||||
{canCreateNew && (
|
||||
<button
|
||||
onClick={() => handleSelectDimension(searchQuery.trim())}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
fontSize: '13px',
|
||||
background: selectedIndex === suggestions.length ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderTop: suggestions.length > 0 ? '1px solid #1f1f1f' : 'none',
|
||||
color: '#22c55e',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
onMouseEnter={() => setSelectedIndex(suggestions.length)}
|
||||
>
|
||||
<span style={{ fontSize: '16px', fontWeight: 300 }}>+</span>
|
||||
Create "{searchQuery.trim()}"
|
||||
</button>
|
||||
<div className="search-create">
|
||||
<button
|
||||
onClick={() => handleSelectDimension(searchQuery.trim())}
|
||||
onMouseEnter={() => setSelectedIndex(suggestions.length)}
|
||||
className={`create-button ${selectedIndex === suggestions.length ? 'selected' : ''}`}
|
||||
>
|
||||
<span className="create-icon">+</span>
|
||||
Create "{searchQuery.trim()}"
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyboard Hint */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #1f1f1f',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '16px',
|
||||
fontSize: '11px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<span>↑↓ Navigate</span>
|
||||
<span>↵ Select</span>
|
||||
<span>Esc Close</span>
|
||||
</div>
|
||||
{/* Empty state */}
|
||||
{!searchQuery && suggestions.length === 0 && (
|
||||
<div className="search-empty">
|
||||
Start typing to search dimensions
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
.search-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 9999;
|
||||
animation: backdropIn 200ms ease-out;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 70vh;
|
||||
animation: containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #525252;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fafafa;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.search-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-shortcut kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
background: #262626;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
color: #737373;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
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;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #1f1f1f;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.result-name {
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.result-name.priority {
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.result-count {
|
||||
color: #525252;
|
||||
font-size: 12px;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
}
|
||||
|
||||
.result-hint {
|
||||
color: #525252;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-create {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.create-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
color: #22c55e;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.create-button:hover,
|
||||
.create-button.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.create-icon {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
margin-top: 8px;
|
||||
padding: 32px 24px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
color: #525252;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@keyframes containerIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
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;
|
||||
|
||||
@@ -160,182 +160,232 @@ export default function EdgeSearchModal({
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
animation: 'fadeIn 150ms ease-out'
|
||||
}}
|
||||
className="search-backdrop"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Search nodes to connect"
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{
|
||||
background: '#1a1a1a',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '8px',
|
||||
width: '90%',
|
||||
maxWidth: '600px',
|
||||
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.6)',
|
||||
animation: 'slideIn 150ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div ref={modalRef} className="search-container">
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: suggestions.length > 0 ? '1px solid #2a2a2a' : 'none'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #333'
|
||||
}}>
|
||||
{/* Search Icon */}
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<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>
|
||||
<div className="search-input-wrapper">
|
||||
<svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search node to connect..."
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Connect to node..."
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
padding: '0 4px',
|
||||
lineHeight: 1,
|
||||
transition: 'color 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
|
||||
aria-label="Close search"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<div className="search-shortcut">
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{/* Results */}
|
||||
{suggestions.length > 0 && (
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{suggestions.map((suggestion, index) => {
|
||||
const primaryDimension = suggestion.dimensions && suggestion.dimensions.length > 0
|
||||
? suggestion.dimensions[0]
|
||||
: '';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
fontSize: '13px',
|
||||
background: index === selectedIndex ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: index < suggestions.length - 1 ? '1px solid #2a2a2a' : 'none',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
>
|
||||
{primaryDimension && (
|
||||
<span style={{
|
||||
color: '#666',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
minWidth: '60px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
{primaryDimension}
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#e5e5e5'
|
||||
}}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="search-results">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
|
||||
>
|
||||
<span className="result-id">#{suggestion.id}</span>
|
||||
<span className="result-title">{suggestion.title}</span>
|
||||
{index === selectedIndex && (
|
||||
<span className="result-hint">↵</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyboard Hint */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #2a2a2a',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '16px',
|
||||
fontSize: '11px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<span>↑↓ Navigate</span>
|
||||
<span>↵ Select</span>
|
||||
<span>Esc Close</span>
|
||||
</div>
|
||||
{/* Empty state */}
|
||||
{searchQuery && suggestions.length === 0 && (
|
||||
<div className="search-empty">
|
||||
No results for "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
.search-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 9999;
|
||||
animation: backdropIn 200ms ease-out;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 70vh;
|
||||
animation: containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #525252;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fafafa;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.search-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-shortcut kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
background: #262626;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
color: #737373;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
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;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #1f1f1f;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.result-id {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: #0a0a0a;
|
||||
background: #22c55e;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
min-width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-hint {
|
||||
color: #525252;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
margin-top: 8px;
|
||||
padding: 32px 24px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
color: #525252;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@keyframes containerIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
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;
|
||||
|
||||
@@ -403,7 +403,7 @@ export default function ThreePanelLayout() {
|
||||
width: '40px',
|
||||
flexShrink: 0,
|
||||
borderRight: '1px solid #2a2a2a',
|
||||
background: '#0f0f0f',
|
||||
background: '#0a0a0a',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
@@ -446,7 +446,7 @@ export default function ThreePanelLayout() {
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#0f0f0f',
|
||||
background: '#0a0a0a',
|
||||
position: 'relative',
|
||||
padding: '4px'
|
||||
}}
|
||||
@@ -486,18 +486,63 @@ export default function ThreePanelLayout() {
|
||||
style={{
|
||||
width: chatCollapsed
|
||||
? (nodesCollapsed
|
||||
? `calc(100% - 48px - 40px)`
|
||||
: `calc(${effectiveMiddleWidth}% - 48px)`)
|
||||
? `calc(100% - 40px)`
|
||||
: `calc(100% - ${effectiveLeftWidth}% - 3px)`)
|
||||
: `${effectiveMiddleWidth}%`,
|
||||
flexShrink: 0,
|
||||
borderRight: chatCollapsed ? 'none' : '1px solid #1a1a1a',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
background: '#0f0f0f',
|
||||
padding: '4px'
|
||||
background: '#0a0a0a',
|
||||
padding: '8px',
|
||||
paddingRight: chatCollapsed ? '8px' : '4px'
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
background: '#141414',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #1f1f1f',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: chatCollapsed ? '20px' : '0',
|
||||
position: 'relative'
|
||||
}}>
|
||||
{/* Expand chat button - shown when collapsed */}
|
||||
{chatCollapsed && (
|
||||
<button
|
||||
onClick={() => setChatCollapsed(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
right: '12px',
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
zIndex: 10
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = '#666';
|
||||
}}
|
||||
title="Expand Chat (⌘\)"
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
<div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
|
||||
<div style={{
|
||||
height: '100%',
|
||||
@@ -523,6 +568,7 @@ export default function ThreePanelLayout() {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Resize Handle */}
|
||||
@@ -541,47 +587,7 @@ export default function ThreePanelLayout() {
|
||||
)}
|
||||
|
||||
{/* Right Panel - Agents (collapsible) */}
|
||||
{chatCollapsed ? (
|
||||
// Collapsed rail
|
||||
<div style={{
|
||||
width: '48px',
|
||||
flexShrink: 0,
|
||||
borderLeft: '1px solid #2a2a2a',
|
||||
background: '#0f0f0f',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
paddingTop: '12px'
|
||||
}}>
|
||||
<button
|
||||
onClick={() => setChatCollapsed(false)}
|
||||
style={{
|
||||
width: '28px',
|
||||
height: '28px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f',
|
||||
background: 'transparent',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease'
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = '#1a1a1a';
|
||||
e.currentTarget.style.color = '#999';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = '#666';
|
||||
}}
|
||||
title="Expand Chat (⌘\)"
|
||||
>
|
||||
<Maximize2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
{!chatCollapsed && (
|
||||
<div
|
||||
style={{
|
||||
width: `${effectiveRightWidth}%`,
|
||||
@@ -591,15 +597,26 @@ export default function ThreePanelLayout() {
|
||||
flexDirection: 'column',
|
||||
background: '#0a0a0a',
|
||||
position: 'relative',
|
||||
padding: '4px'
|
||||
padding: '8px',
|
||||
paddingLeft: '4px'
|
||||
}}
|
||||
>
|
||||
<AgentsPanel
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeNodeId}
|
||||
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
|
||||
onCollapse={() => setChatCollapsed(true)}
|
||||
/>
|
||||
<div style={{
|
||||
flex: 1,
|
||||
background: '#141414',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid #1f1f1f',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
<AgentsPanel
|
||||
openTabsData={openTabsData}
|
||||
activeTabId={activeNodeId}
|
||||
onNodeClick={(nodeId) => handleNodeSelect(nodeId, false)}
|
||||
onCollapse={() => setChatCollapsed(true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -433,12 +433,12 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
}}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px', /* Increased from 10px to 12px */
|
||||
margin: '2px 0', /* Added vertical margin for better spacing */
|
||||
padding: '8px 16px',
|
||||
margin: '0',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
gap: '8px',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit',
|
||||
background: selectedNodes.has(node.id) ? '#1a1a1a' : 'transparent',
|
||||
@@ -460,7 +460,24 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Node ID Badge */}
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#22c55e',
|
||||
color: '#0a0a0a',
|
||||
fontSize: '9px',
|
||||
fontWeight: 600,
|
||||
padding: '1px 5px',
|
||||
borderRadius: '3px',
|
||||
flexShrink: 0,
|
||||
fontFamily: 'monospace',
|
||||
lineHeight: 1,
|
||||
height: '16px'
|
||||
}}>
|
||||
#{node.id}
|
||||
</span>
|
||||
|
||||
{/* Title */}
|
||||
<span style={{
|
||||
@@ -531,7 +548,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: '#000' }}>
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: 'transparent' }}>
|
||||
{/* Search Modal */}
|
||||
<SearchModal
|
||||
isOpen={showSearchModal}
|
||||
@@ -571,7 +588,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
borderBottom: '1px solid #1a1a1a',
|
||||
background: '#0a0a0a',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: creating ? 'not-allowed' : 'pointer',
|
||||
textAlign: 'left',
|
||||
@@ -662,7 +679,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
borderRadius: '4px', /* Added border radius */
|
||||
background: '#0f0f0f', /* Slightly different background */
|
||||
background: 'transparent', /* Transparent to show panel color */
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
@@ -696,11 +713,10 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 18px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: dimensionsSectionCollapsed ? '#94a3b8' : '#f8fafc',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.18em',
|
||||
letterSpacing: '0.01em',
|
||||
borderBottom: '1px solid #151515',
|
||||
borderLeft: '3px solid transparent',
|
||||
background: dimensionsSectionCollapsed ? '#050505' : '#0e1811',
|
||||
@@ -715,7 +731,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Layers size={14} color={dimensionsSectionCollapsed ? '#94a3b8' : '#f8fafc'} />
|
||||
<span>Dimensions ({lockedDimensions.length})</span>
|
||||
<span>Dimensions</span>
|
||||
</div>
|
||||
{dimensionsSectionCollapsed ? (
|
||||
<ChevronRight size={14} strokeWidth={2.5} color="#94a3b8" />
|
||||
@@ -763,10 +779,7 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
) : (
|
||||
<Folder size={16} color="#64748b" />
|
||||
)}
|
||||
<span style={{
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.08em'
|
||||
}}>
|
||||
<span>
|
||||
{lockedDim.dimension}
|
||||
</span>
|
||||
</div>
|
||||
@@ -813,11 +826,10 @@ export default function NodesPanel({ selectedNodes, onNodeSelect, onNodeCreated,
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 18px',
|
||||
fontSize: '11px',
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
fontWeight: 600,
|
||||
color: allNodesSectionCollapsed ? '#94a3b8' : '#f8fafc',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.18em',
|
||||
letterSpacing: '0.01em',
|
||||
borderBottom: '1px solid #151515',
|
||||
borderLeft: '3px solid transparent',
|
||||
background: allNodesSectionCollapsed ? '#050505' : '#0e1811',
|
||||
|
||||
@@ -154,207 +154,242 @@ export default function SearchModal({ isOpen, onClose, onNodeSelect, existingFil
|
||||
|
||||
const modalContent = (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0, 0, 0, 0.8)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
animation: 'fadeIn 150ms ease-out'
|
||||
}}
|
||||
className="search-backdrop"
|
||||
onClick={handleBackdropClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Search nodes"
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
style={{
|
||||
background: '#050505',
|
||||
border: '1px solid #1f1f1f',
|
||||
borderRadius: '12px',
|
||||
width: '90%',
|
||||
maxWidth: '600px',
|
||||
boxShadow: '0 25px 65px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(255, 255, 255, 0.05)',
|
||||
animation: 'slideIn 150ms ease-out'
|
||||
}}
|
||||
>
|
||||
<div ref={modalRef} className="search-container">
|
||||
{/* Search Input */}
|
||||
<div style={{
|
||||
padding: '16px',
|
||||
borderBottom: suggestions.length > 0 ? '1px solid #1f1f1f' : 'none'
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
background: '#0a0a0a',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #1f1f1f'
|
||||
}}>
|
||||
{/* Search Icon */}
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill="#666">
|
||||
<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>
|
||||
<div className="search-input-wrapper">
|
||||
<svg className="search-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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>
|
||||
|
||||
{/* Selected Filters */}
|
||||
{existingFilters.map((filter, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={filter.value}
|
||||
color={'#1a1a4d'}
|
||||
maxWidth={120}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Input */}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={existingFilters.length === 0 ? "Search nodes..." : ""}
|
||||
style={{
|
||||
flex: 1,
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
color: '#fff',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
{/* Selected Filters */}
|
||||
{existingFilters.map((filter, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={filter.value}
|
||||
color={'#1a1a4d'}
|
||||
maxWidth={120}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#666',
|
||||
cursor: 'pointer',
|
||||
fontSize: '20px',
|
||||
padding: '0 4px',
|
||||
lineHeight: 1,
|
||||
transition: 'color 0.2s'
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#fff'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = '#666'; }}
|
||||
aria-label="Close search"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={existingFilters.length === 0 ? "Search nodes..." : ""}
|
||||
className="search-input"
|
||||
/>
|
||||
|
||||
<div className="search-shortcut">
|
||||
<kbd>esc</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Suggestions */}
|
||||
{/* Results */}
|
||||
{suggestions.length > 0 && (
|
||||
<div style={{
|
||||
maxHeight: '300px',
|
||||
overflowY: 'auto'
|
||||
}}>
|
||||
{suggestions.map((suggestion, index) => {
|
||||
const primaryDimension = suggestion.dimensions && suggestion.dimensions.length > 0
|
||||
? suggestion.dimensions[0]
|
||||
: '';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 16px',
|
||||
textAlign: 'left',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
fontSize: '13px',
|
||||
background: index === selectedIndex ? '#252525' : 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: index < suggestions.length - 1 ? '1px solid #1f1f1f' : 'none',
|
||||
color: '#ccc',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
fontFamily: 'inherit'
|
||||
}}
|
||||
>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
color: '#000',
|
||||
background: '#22c55e',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '6px',
|
||||
minWidth: '20px',
|
||||
textAlign: 'center',
|
||||
letterSpacing: '0.05em',
|
||||
flexShrink: 0
|
||||
}}>
|
||||
{suggestion.id}
|
||||
</span>
|
||||
{primaryDimension && (
|
||||
<span style={{
|
||||
color: '#666',
|
||||
fontSize: '10px',
|
||||
fontWeight: 600,
|
||||
minWidth: '60px',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em'
|
||||
}}>
|
||||
{primaryDimension}
|
||||
</span>
|
||||
)}
|
||||
<span style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#e5e5e5'
|
||||
}}>
|
||||
{suggestion.title}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="search-results">
|
||||
{suggestions.map((suggestion, index) => (
|
||||
<button
|
||||
key={suggestion.id}
|
||||
onClick={() => handleSelectSuggestion(suggestion)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
className={`search-result-item ${index === selectedIndex ? 'selected' : ''}`}
|
||||
>
|
||||
<span className="result-id">#{suggestion.id}</span>
|
||||
<span className="result-title">{suggestion.title}</span>
|
||||
{index === selectedIndex && (
|
||||
<span className="result-hint">↵</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Keyboard Hint */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderTop: '1px solid #1f1f1f',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '16px',
|
||||
fontSize: '11px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<span>↑↓ Navigate</span>
|
||||
<span>↵ Select</span>
|
||||
<span>Esc Close</span>
|
||||
</div>
|
||||
{/* Empty state */}
|
||||
{searchQuery && suggestions.length === 0 && (
|
||||
<div className="search-empty">
|
||||
No results for "{searchQuery}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes fadeIn {
|
||||
.search-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 15vh;
|
||||
z-index: 9999;
|
||||
animation: backdropIn 200ms ease-out;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
max-height: 70vh;
|
||||
animation: containerIn 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 24px 48px -12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #525252;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fafafa;
|
||||
font-size: 18px;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #525252;
|
||||
}
|
||||
|
||||
.search-shortcut {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-shortcut kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px 8px;
|
||||
background: #262626;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
color: #737373;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.search-results {
|
||||
margin-top: 8px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
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;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 20px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #1f1f1f;
|
||||
cursor: pointer;
|
||||
transition: background 100ms ease;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.search-result-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.search-result-item:hover,
|
||||
.search-result-item.selected {
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.result-id {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-family: 'SF Mono', 'Fira Code', monospace;
|
||||
color: #0a0a0a;
|
||||
background: #22c55e;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
min-width: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
flex: 1;
|
||||
color: #e5e5e5;
|
||||
font-size: 15px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.result-hint {
|
||||
color: #525252;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.search-empty {
|
||||
margin-top: 8px;
|
||||
padding: 32px 24px;
|
||||
background: #141414;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 16px;
|
||||
color: #525252;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes backdropIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
@keyframes containerIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user