From b6f21c210ee804782eba2e7c30c2ccdcbd95bffb Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Fri, 13 Feb 2026 05:07:46 +0000 Subject: Add unfold merged trace: convert to sequential node chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unfold takes a merged trace's messages, extracts the node order, and creates real edges chaining those nodes sequentially (A→B→C→D→E). The merged trace is deleted and replaced by a regular pass-through trace. - Add unfoldMergedTrace() to flowStore (creates edges, rewires downstream) - Add Unfold button (Layers icon) to Sidebar merged traces UI - Fix isMerged edge detection to use explicit flag instead of ID prefix - Fix LLMNode useUpdateNodeInternals deps for dynamic handle updates Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/Sidebar.tsx | 98 +++++- frontend/src/components/nodes/LLMNode.tsx | 77 +++-- frontend/src/store/flowStore.ts | 510 +++++++++++++++++++++--------- 3 files changed, 501 insertions(+), 184 deletions(-) (limited to 'frontend') diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 78d2475..65d5cd2 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -6,7 +6,7 @@ import type { NodeData, Trace, Message, MergedTrace, MergeStrategy } from '../st import type { Edge } from 'reactflow'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; -import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link } from 'lucide-react'; +import { Play, Settings, Info, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers } from 'lucide-react'; interface SidebarProps { isOpen: boolean; @@ -18,7 +18,7 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { const { nodes, edges, selectedNodeId, updateNodeData, getActiveContext, addNode, setSelectedNode, isTraceComplete, theme, - createMergedTrace, updateMergedTrace, deleteMergedTrace, computeMergedMessages, + createMergedTrace, updateMergedTrace, deleteMergedTrace, unfoldMergedTrace, computeMergedMessages, files, uploadFile, refreshFiles, addFileScope, removeFileScope, currentBlueprintPath, saveCurrentBlueprint } = useFlowStore(); @@ -183,6 +183,39 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { } }; + // Image helpers + const isImageFile = (mime: string) => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].includes(mime); + const getImageUrl = (fileId: string) => `${import.meta.env.VITE_BACKEND_URL || ''}/api/files/download?user=${encodeURIComponent(user?.username || 'test')}&file_id=${encodeURIComponent(fileId)}`; + + // Paste handler: upload pasted image and attach it + const handlePasteImage = async ( + e: React.ClipboardEvent, + addFile: (fileId: string) => void, + scopeFn?: () => string, + ) => { + const items = e.clipboardData?.items; + if (!items) return; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.type.startsWith('image/')) { + e.preventDefault(); + const blob = item.getAsFile(); + if (!blob) continue; + const file = new File([blob], `paste-${Date.now()}.${blob.type.split('/')[1] || 'png'}`, { type: blob.type }); + try { + const meta = await uploadFile(file, { provider: 'local' }); + addFile(meta.id); + if (scopeFn) { + try { await addFileScope(meta.id, scopeFn()); } catch {} + } + } catch (err) { + console.error('Paste upload failed:', err); + } + return; // only handle first image + } + } + }; + // Filter files for attach modal const filteredFilesToAttach = useMemo(() => { const q = attachSearch.trim().toLowerCase(); @@ -1036,6 +1069,10 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { }) }); + if (!response.ok) { + const errText = await response.text(); + throw new Error(errText || `HTTP ${response.status}`); + } if (!response.body) throw new Error('No response body'); const reader = response.body.getReader(); @@ -1671,7 +1708,7 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { {/* Alternating color indicator */}
{merged.colors.slice(0, 3).map((color, idx) => ( -
= ({ isOpen, onToggle, onInteract }) => {
)}
- +
Merged #{merged.id.slice(-6)} @@ -1715,6 +1752,19 @@ const Sidebar: React.FC = ({ isOpen, onToggle, onInteract }) => { + +