From e15f223ed841a32db5a014f68c34a9b73f36b0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CBeeRad=E2=80=9D?= Date: Thu, 25 Dec 2025 19:36:34 +1100 Subject: [PATCH] sync: Drag-to-chat + views system from private repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added drag-to-chat: drag any node into chat input to insert [NODE:id:"title"] - Added application/x-rah-node MIME type to all drag sources - New views folder: GridView, KanbanView, ListView, ViewFilters, ViewPanel - UI improvements: green pill node IDs, grid card fixes, dimension folder redesign - Drop handler in TerminalInput with visual feedback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/components/agents/TerminalInput.tsx | 97 +++- src/components/focus/FocusPanel.tsx | 36 +- src/components/nodes/FolderViewOverlay.tsx | 578 ++++++++++++--------- src/components/nodes/NodesPanel.tsx | 10 +- src/components/views/GridView.tsx | 159 ++++++ src/components/views/KanbanView.tsx | 536 +++++++++++++++++++ src/components/views/ListView.tsx | 184 +++++++ src/components/views/ViewFilters.tsx | 242 +++++++++ src/components/views/ViewPanel.tsx | 286 ++++++++++ 9 files changed, 1860 insertions(+), 268 deletions(-) create mode 100644 src/components/views/GridView.tsx create mode 100644 src/components/views/KanbanView.tsx create mode 100644 src/components/views/ListView.tsx create mode 100644 src/components/views/ViewFilters.tsx create mode 100644 src/components/views/ViewPanel.tsx diff --git a/src/components/agents/TerminalInput.tsx b/src/components/agents/TerminalInput.tsx index 241be05..2b013e4 100644 --- a/src/components/agents/TerminalInput.tsx +++ b/src/components/agents/TerminalInput.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, type DragEvent } from 'react'; import { Mic, MicOff } from 'lucide-react'; interface TerminalInputProps { @@ -34,6 +34,7 @@ export default function TerminalInput({ const [prompts, setPrompts] = useState>([]); const [showSlashMenu, setShowSlashMenu] = useState(false); const [activeIndex, setActiveIndex] = useState(0); + const [isDragOver, setIsDragOver] = useState(false); useEffect(() => { const load = async () => { @@ -151,7 +152,90 @@ export default function TerminalInput({ const amplitudeBars = Array.from({ length: 8 }); - + // Handle node drag over chat input + const handleDragOver = (e: DragEvent) => { + // Check if it's a node being dragged (either custom MIME or text/plain fallback) + if (e.dataTransfer.types.includes('application/x-rah-node') || + e.dataTransfer.types.includes('application/node-info') || + e.dataTransfer.types.includes('text/plain')) { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + setIsDragOver(true); + } + }; + + const handleDragLeave = (e: DragEvent) => { + // Only reset if actually leaving the textarea (not entering a child) + if (e.currentTarget === e.target) { + setIsDragOver(false); + } + }; + + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + + // Try application/x-rah-node first (structured data with id + title) + let nodeData = e.dataTransfer.getData('application/x-rah-node'); + if (nodeData) { + try { + const { id, title } = JSON.parse(nodeData); + const token = `[NODE:${id}:"${title}"]`; + insertAtCursor(token); + return; + } catch (err) { + console.error('Failed to parse x-rah-node data:', err); + } + } + + // Fallback: try application/node-info (from NodesPanel) + nodeData = e.dataTransfer.getData('application/node-info'); + if (nodeData) { + try { + const { id, title } = JSON.parse(nodeData); + const token = `[NODE:${id}:"${title || 'Untitled'}"]`; + insertAtCursor(token); + return; + } catch (err) { + console.error('Failed to parse node-info data:', err); + } + } + + // Last resort: use text/plain (might already be formatted as [NODE:id:"title"]) + const plainText = e.dataTransfer.getData('text/plain'); + if (plainText && plainText.startsWith('[NODE:')) { + insertAtCursor(plainText); + } + }; + + const insertAtCursor = (text: string) => { + const textarea = textareaRef.current; + if (!textarea) { + setInput(prev => prev + text + ' '); + return; + } + + const start = textarea.selectionStart || 0; + const end = textarea.selectionEnd || 0; + const before = input.slice(0, start); + const after = input.slice(end); + + // Add space before if there's text and it doesn't end with space + const needsSpaceBefore = before.length > 0 && !before.endsWith(' ') && !before.endsWith('\n'); + // Add space after + const newText = (needsSpaceBefore ? ' ' : '') + text + ' '; + + setInput(before + newText + after); + + // Set cursor position after the inserted text + setTimeout(() => { + const newPos = start + newText.length; + textarea.setSelectionRange(newPos, newPos); + textarea.focus(); + }, 0); + }; + + return ( <>