import React, { useState, useEffect, useRef, useMemo } from 'react';
import { useReactFlow } from 'reactflow';
import useFlowStore from '../store/flowStore';
import { useAuthStore } from '../store/authStore';
import type { NodeData, Trace, Message, MergedTrace, MergeStrategy, CouncilData, CouncilMemberConfig } from '../store/flowStore';
import type { Edge } from 'reactflow';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import 'katex/dist/katex.min.css';

// Preprocess LaTeX delimiters: convert \(...\) → $...$ and \[...\] → $$...$$
const preprocessLaTeX = (content: string): string => {
  return content
    .replace(/\\\[([\s\S]*?)\\\]/g, (_, math) => `$$${math}$$`)
    .replace(/\\\(([\s\S]*?)\\\)/g, (_, math) => `$${math}$`);
};

import { Play, Settings, Info, ChevronLeft, ChevronRight, ChevronDown, Maximize2, Edit3, X, Check, FileText, MessageCircle, Send, GripVertical, GitMerge, Trash2, AlertCircle, Loader2, Navigation, Upload, Search, Link, Layers, Eye, EyeOff, Copy, ClipboardCheck, Users } from 'lucide-react';

interface SidebarProps {
  isOpen: boolean;
  onToggle: () => void;
  onInteract?: () => void;
}

const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
  const { 
    nodes, edges, selectedNodeId, updateNodeData, getActiveContext, addNode, setSelectedNode, 
    isTraceComplete, theme,
    createMergedTrace, updateMergedTrace, deleteMergedTrace, unfoldMergedTrace, computeMergedMessages,
    files, uploadFile, refreshFiles, addFileScope, removeFileScope, currentBlueprintPath,
    saveCurrentBlueprint
  } = useFlowStore();
  const { getAuthHeader, user } = useAuthStore();
  const { setCenter, getViewport } = useReactFlow();
  const isDark = theme === 'dark';
  
  // Premium models and authorized users
  const PREMIUM_USERS = ['test', 'blackhao'];
  const canUsePremiumModels = user?.username ? PREMIUM_USERS.includes(user.username) : false;
  // Premium models: gpt-5-pro, gpt-5.2-pro, o3
  const [activeTab, setActiveTab] = useState<'interact' | 'settings' | 'debug'>('interact');
  const [streamBuffer, setStreamBuffer] = useState('');
  const [streamingNodeId, setStreamingNodeId] = useState<string | null>(null); // Track which node is streaming
  
  // Attachments state
  const [showAttachModal, setShowAttachModal] = useState(false);
  const [attachSearch, setAttachSearch] = useState('');
  const settingsUploadRef = useRef<HTMLInputElement>(null);
  const [settingsUploading, setSettingsUploading] = useState(false);
  
  // Response Modal & Edit states
  const [isModalOpen, setIsModalOpen] = useState(false);
  const [isEditing, setIsEditing] = useState(false);
  const [editedResponse, setEditedResponse] = useState('');
  const [rawTextMode, setRawTextMode] = useState(false);
  const [copiedResponse, setCopiedResponse] = useState(false);
  
  // Summary states
  const [showSummaryModal, setShowSummaryModal] = useState(false);
  const [summaryModel, setSummaryModel] = useState('gpt-5-nano');
  const [isSummarizing, setIsSummarizing] = useState(false);
  
  // Quick Chat states
  const [quickChatOpen, setQuickChatOpen] = useState(false);
  const [quickChatTrace, setQuickChatTrace] = useState<Trace | null>(null);
  const [quickChatLastNodeId, setQuickChatLastNodeId] = useState<string | null>(null); // Track the last node in the chat chain
  const [quickChatMessages, setQuickChatMessages] = useState<Message[]>([]);
  const [quickChatInput, setQuickChatInput] = useState('');
  const [quickChatModel, setQuickChatModel] = useState('gpt-5.1');
  const [quickChatLoading, setQuickChatLoading] = useState(false);
  const [quickChatTemp, setQuickChatTemp] = useState(0.7);
  const [quickChatEffort, setQuickChatEffort] = useState<'low' | 'medium' | 'high'>('medium');
  const [quickChatNeedsDuplicate, setQuickChatNeedsDuplicate] = useState(false);
  const [quickChatWebSearch, setQuickChatWebSearch] = useState(true);
  const [quickChatAttachedFiles, setQuickChatAttachedFiles] = useState<string[]>([]); // File IDs for current message
  const [quickChatSentFiles, setQuickChatSentFiles] = useState<{msgId: string, fileIds: string[]}[]>([]); // Files sent with messages
  const [showQuickChatAttachModal, setShowQuickChatAttachModal] = useState(false);
  const [quickChatAttachSearch, setQuickChatAttachSearch] = useState('');
  const [quickChatUploading, setQuickChatUploading] = useState(false); // Upload loading state
  const quickChatEndRef = useRef<HTMLDivElement>(null);
  const quickChatInputRef = useRef<HTMLTextAreaElement>(null);
  const quickChatUploadRef = useRef<HTMLInputElement>(null);
  
  // Merge Trace states
  const [showMergeModal, setShowMergeModal] = useState(false);
  const [mergeSelectedIds, setMergeSelectedIds] = useState<string[]>([]);
  const [mergeStrategy, setMergeStrategy] = useState<MergeStrategy>('query_time');
  const [mergeDraggedId, setMergeDraggedId] = useState<string | null>(null);
  const [mergeOrder, setMergeOrder] = useState<string[]>([]);
  const [showMergePreview, setShowMergePreview] = useState(false);
  const [isSummarizingMerge, setIsSummarizingMerge] = useState(false);

  // Council mode states
  const [councilStage, setCouncilStage] = useState<string>('');
  const [councilStreamBuffer, setCouncilStreamBuffer] = useState('');
  const [openCtxDropdown, setOpenCtxDropdown] = useState<string | null>(null);
  const [councilTab, setCouncilTab] = useState<'final' | 'responses' | 'rankings'>('final');
  const [councilResponseTab, setCouncilResponseTab] = useState(0);
  const [councilRankingTab, setCouncilRankingTab] = useState<'aggregate' | number>('aggregate');

  // Quick Chat council mode states
  const [quickChatCouncilMode, setQuickChatCouncilMode] = useState(false);
  const [quickChatCouncilModels, setQuickChatCouncilModels] = useState<CouncilMemberConfig[]>([]);
  const [quickChatChairmanModel, setQuickChatChairmanModel] = useState<CouncilMemberConfig | null>(null);
  const [quickChatCouncilStage, setQuickChatCouncilStage] = useState('');
  const [quickChatCouncilData, setQuickChatCouncilData] = useState<CouncilData | null>(null);
  const [quickChatCouncilConfigOpen, setQuickChatCouncilConfigOpen] = useState(false);

  const selectedNode = nodes.find((n) => n.id === selectedNodeId);

  // Reset stream buffer and modal states when node changes
  useEffect(() => {
    setStreamBuffer('');
    setIsModalOpen(false);
    setIsEditing(false);
    setShowMergeModal(false);
    setMergeSelectedIds([]);
    setShowMergePreview(false);
  }, [selectedNodeId]);
  
  // Default select first trace when node changes and no trace is selected
  useEffect(() => {
    if (selectedNode && 
        selectedNode.data.traces && 
        selectedNode.data.traces.length > 0 &&
        (!selectedNode.data.activeTraceIds || selectedNode.data.activeTraceIds.length === 0)) {
      updateNodeData(selectedNode.id, {
        activeTraceIds: [selectedNode.data.traces[0].id]
      });
    }
  }, [selectedNodeId, selectedNode?.data.traces?.length]);
  
  // Sync editedResponse when entering edit mode
  useEffect(() => {
    if (isEditing && selectedNode) {
      setEditedResponse(selectedNode.data.response || '');
    }
  }, [isEditing, selectedNode?.data.response]);

  // Scroll to bottom when quick chat messages change
  useEffect(() => {
    if (quickChatEndRef.current) {
      quickChatEndRef.current.scrollIntoView({ behavior: 'smooth' });
    }
  }, [quickChatMessages]);

  // Attachment helpers
  const handleAttach = async (fileId: string) => {
    if (!selectedNode) return;
    const current = selectedNode.data.attachedFileIds || [];
    if (!current.includes(fileId)) {
      updateNodeData(selectedNode.id, {
        attachedFileIds: [...current, fileId]
      });
      // Add scope to file for filtering
      const projectPath = currentBlueprintPath || 'untitled';
      const scope = `${projectPath}/${selectedNode.id}`;
      try {
        await addFileScope(fileId, scope);
      } catch (e) {
        console.error('Failed to add file scope:', e);
      }
      // Auto-save blueprint to persist attached files
      if (currentBlueprintPath) {
        saveCurrentBlueprint(currentBlueprintPath, getViewport()).catch(console.error);
      }
    }
    setShowAttachModal(false);
  };

  const handleDetach = async (fileId: string) => {
    if (!selectedNode) return;
    const current = selectedNode.data.attachedFileIds || [];
    updateNodeData(selectedNode.id, {
      attachedFileIds: current.filter(id => id !== fileId)
    });
    // Remove scope from file
    const projectPath = currentBlueprintPath || 'untitled';
    const scope = `${projectPath}/${selectedNode.id}`;
    try {
      await removeFileScope(fileId, scope);
    } catch (e) {
      console.error('Failed to remove file scope:', e);
    }
    // Auto-save blueprint to persist detached files
    if (currentBlueprintPath) {
      saveCurrentBlueprint(currentBlueprintPath, getViewport()).catch(console.error);
    }
  };

  const handleUploadAndAttach = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files || e.target.files.length === 0 || !selectedNode) return;
    const file = e.target.files[0];
    setSettingsUploading(true);
    try {
      const meta = await uploadFile(file, { provider: 'local' });
      handleAttach(meta.id);
    } catch (err) {
      alert(`Upload failed: ${(err as Error).message}`);
    } finally {
      e.target.value = '';
      setSettingsUploading(false);
    }
  };

  // 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<HTMLTextAreaElement>,
    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();
    if (!q) return files;
    return files.filter(f => f.name.toLowerCase().includes(q));
  }, [files, attachSearch]);

  // Filter files for Quick Chat attach modal
  const filteredQuickChatFiles = useMemo(() => {
    const q = quickChatAttachSearch.trim().toLowerCase();
    if (!q) return files;
    return files.filter(f => f.name.toLowerCase().includes(q));
  }, [files, quickChatAttachSearch]);

  if (!isOpen) {
    return (
      <div className={`border-l h-screen flex flex-col items-center py-4 w-12 z-10 transition-all duration-300 ${
        isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-200 bg-white'
      }`}>
        <button 
          onClick={onToggle}
          className={`p-2 rounded mb-4 ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-100'}`}
          title="Expand"
        >
          <ChevronLeft size={20} className={isDark ? 'text-gray-400' : 'text-gray-500'} />
        </button>
        {selectedNode && (
           <div className={`writing-vertical text-xs font-bold uppercase tracking-widest mt-4 ${isDark ? 'text-gray-400' : 'text-gray-500'}`} style={{ writingMode: 'vertical-rl' }}>
             {selectedNode.data.label}
           </div>
        )}
      </div>
    );
  }

  if (!selectedNode) {
    return (
      <div className={`w-96 border-l h-screen flex flex-col shadow-xl z-10 transition-all duration-300 ${
        isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-200 bg-white'
      }`}>
        <div className={`p-3 border-b flex justify-between items-center ${
          isDark ? 'border-gray-700 bg-gray-900' : 'border-gray-200 bg-gray-50'
        }`}>
           <span className={`text-sm font-medium ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Details</span>
           <button onClick={onToggle} className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`}>
             <ChevronRight size={16} className={isDark ? 'text-gray-400' : 'text-gray-500'} />
           </button>
        </div>
        <div className={`flex-1 p-4 text-center flex flex-col justify-center ${
          isDark ? 'bg-gray-900 text-gray-400' : 'bg-gray-50 text-gray-500'
        }`}>
        <p>Select a node to edit</p>
        </div>
      </div>
    );
  }

  const handleRun = async () => {
    if (!selectedNode) return;
    
    // Check if upstream is complete before running
    const tracesCheck = checkActiveTracesComplete();
    if (!tracesCheck.complete) {
      console.warn('Cannot run: upstream context is incomplete');
      return;
    }
    
    // Capture the node ID at the start of the request
    const runningNodeId = selectedNode.id;
    const runningPrompt = selectedNode.data.userPrompt;
    
    // Record query sent timestamp
    const querySentAt = Date.now();
    updateNodeData(runningNodeId, { status: 'loading', response: '', querySentAt });
    setStreamBuffer('');
    setStreamingNodeId(runningNodeId);

    // Use getActiveContext which respects the user's selected traces
    const context = getActiveContext(runningNodeId);
    
    // Calculate scopes: all nodes in the current trace path
    const projectPath = currentBlueprintPath || 'untitled';
    
    // Compute all upstream node IDs by traversing edges backward
    const traceNodeIds = new Set<string>();
    traceNodeIds.add(runningNodeId);
    
    const visited = new Set<string>();
    const queue = [runningNodeId];
    
    while (queue.length > 0) {
      const currentNodeId = queue.shift()!;
      if (visited.has(currentNodeId)) continue;
      visited.add(currentNodeId);
      
      // Find all incoming edges to this node
      const incomingEdges = edges.filter(e => e.target === currentNodeId);
      for (const edge of incomingEdges) {
        const sourceNodeId = edge.source;
        if (!visited.has(sourceNodeId)) {
          traceNodeIds.add(sourceNodeId);
          queue.push(sourceNodeId);
        }
      }
    }
    
    // Build scopes for all nodes in the trace path
    const scopes = Array.from(traceNodeIds).map(nodeId => `${projectPath}/${nodeId}`);
    console.log('[file_search] trace scopes:', scopes);

    // If no prompt but has files, use a default prompt
    const attachedFiles = selectedNode.data.attachedFileIds || [];
    const effectivePrompt = runningPrompt?.trim() 
      ? runningPrompt 
      : attachedFiles.length > 0 
        ? 'Please analyze the attached files.'
        : '';

    try {
      const response = await fetch(`/api/run_node_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
        body: JSON.stringify({
          node_id: runningNodeId,
          incoming_contexts: [{ messages: context }],
          user_prompt: effectivePrompt,
          attached_file_ids: attachedFiles,
          scopes,
          merge_strategy: selectedNode.data.mergeStrategy || 'smart',
          config: {
            provider: selectedNode.data.model.includes('claude') ? 'claude' : (selectedNode.data.model.includes('gpt') || selectedNode.data.model === 'o3') ? 'openai' : 'google',
            model_name: selectedNode.data.model,
            temperature: selectedNode.data.temperature,
            system_prompt: selectedNode.data.systemPrompt,
            api_key: selectedNode.data.apiKey,
            enable_google_search: selectedNode.data.enableGoogleSearch !== false,
            reasoning_effort: selectedNode.data.reasoningEffort || 'medium',
          }
        })
      });

      if (!response.body) return;
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let fullResponse = '';

      while (true) {
        const { value, done } = await reader.read();
        if (done) {
          console.log('[stream] done, fullResponse length:', fullResponse.length);
          break;
        }
        const chunk = decoder.decode(value);
        console.log('[stream] received chunk:', chunk.substring(0, 50));
        fullResponse += chunk;
        // Only update stream buffer, the display logic will check streamingNodeId
        setStreamBuffer(prev => prev + chunk);
      }
      
      // Update final state using captured nodeId
      const newUserMsg = { 
        id: `msg_${Date.now()}_u`,
        role: 'user', 
        content: runningPrompt 
      };
      const newAssistantMsg = { 
        id: `msg_${Date.now()}_a`,
        role: 'assistant', 
        content: fullResponse 
      };
      
      const responseReceivedAt = Date.now();
      
      updateNodeData(runningNodeId, { 
        status: 'success', 
        response: fullResponse,
        responseReceivedAt,
        messages: [...context, newUserMsg, newAssistantMsg] as any 
      });
      
      // Auto-generate title
      generateTitle(runningNodeId, runningPrompt, fullResponse);

    } catch (error) {
      console.error(error);
      updateNodeData(runningNodeId, { status: 'error' });
    } finally {
      setStreamingNodeId(prev => prev === runningNodeId ? null : prev);
    }
  };

  // Council mode: 3-stage LLM council execution
  const handleRunCouncil = async () => {
    if (!selectedNode) return;
    const councilModels: CouncilMemberConfig[] = selectedNode.data.councilModels || [];
    const chairmanConfig: CouncilMemberConfig = selectedNode.data.chairmanModel || councilModels[0];
    if (councilModels.length < 2) return;

    const tracesCheck = checkActiveTracesComplete();
    if (!tracesCheck.complete) return;

    const runningNodeId = selectedNode.id;
    const runningPrompt = selectedNode.data.userPrompt;
    const querySentAt = Date.now();

    updateNodeData(runningNodeId, {
      status: 'loading',
      response: '',
      querySentAt,
      councilData: { stage1: null, stage2: null, stage3: null },
    });
    setStreamBuffer('');
    setCouncilStreamBuffer('');
    setCouncilStage('Starting council...');
    setStreamingNodeId(runningNodeId);

    const context = getActiveContext(runningNodeId);
    const projectPath = currentBlueprintPath || 'untitled';
    const traceNodeIds = new Set<string>();
    traceNodeIds.add(runningNodeId);
    const visited = new Set<string>();
    const queue = [runningNodeId];
    while (queue.length > 0) {
      const currentNodeId = queue.shift()!;
      if (visited.has(currentNodeId)) continue;
      visited.add(currentNodeId);
      const incomingEdges = edges.filter(e => e.target === currentNodeId);
      for (const edge of incomingEdges) {
        if (!visited.has(edge.source)) {
          traceNodeIds.add(edge.source);
          queue.push(edge.source);
        }
      }
    }
    const scopes = Array.from(traceNodeIds).map(nodeId => `${projectPath}/${nodeId}`);

    const attachedFiles = selectedNode.data.attachedFileIds || [];
    const effectivePrompt = runningPrompt?.trim()
      ? runningPrompt
      : attachedFiles.length > 0
        ? 'Please analyze the attached files.'
        : '';

    // Resolve context messages for a specific trace ID
    const resolveTraceContext = (traceId: string): Message[] => {
      const node = nodes.find(n => n.id === runningNodeId);
      if (!node) return [];
      // Search incoming traces
      let trace: Trace | undefined = (node.data.traces || []).find((t: Trace) => t.id === traceId);
      // Then outgoing traces
      if (!trace) trace = (node.data.outgoingTraces || []).find((t: Trace) => t.id === traceId);
      if (trace) {
        const nodePrefix = `${runningNodeId}-`;
        const isOriginated = trace.id === `trace-${runningNodeId}` ||
          trace.id.startsWith('fork-') ||
          (trace.id.startsWith('prepend-') && trace.id.includes(`-from-${runningNodeId}`));
        return isOriginated ? trace.messages.filter(m => !m.id?.startsWith(nodePrefix)) : [...trace.messages];
      }
      // Check merged traces
      const merged = (node.data.mergedTraces || []).find((m: MergedTrace) => m.id === traceId);
      if (merged) return [...merged.messages];
      return [];
    };

    try {
      const response = await fetch(`/api/run_council_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
        body: JSON.stringify({
          node_id: runningNodeId,
          incoming_contexts: [{ messages: context }],
          user_prompt: effectivePrompt,
          council_models: councilModels.map((cfg) => {
            const base: Record<string, any> = {
              model_name: cfg.model,
              temperature: cfg.temperature ?? null,
              reasoning_effort: cfg.reasoningEffort ?? null,
              enable_google_search: cfg.enableWebSearch ?? null,
            };
            if (cfg.traceId) {
              base.incoming_contexts = [{ messages: resolveTraceContext(cfg.traceId) }];
            }
            return base;
          }),
          chairman_model: {
            model_name: chairmanConfig.model,
            temperature: chairmanConfig.temperature ?? null,
            reasoning_effort: chairmanConfig.reasoningEffort ?? null,
            enable_google_search: chairmanConfig.enableWebSearch ?? null,
          },
          system_prompt: selectedNode.data.systemPrompt || null,
          temperature: selectedNode.data.temperature,
          reasoning_effort: selectedNode.data.reasoningEffort || 'medium',
          enable_google_search: selectedNode.data.enableGoogleSearch !== false,
          merge_strategy: selectedNode.data.mergeStrategy || 'smart',
          attached_file_ids: attachedFiles,
          scopes,
        }),
      });

      if (!response.body) return;
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let sseBuffer = '';
      let stage1Results: Array<{ model: string; response: string }> = [];
      let stage2Data: any = null;
      let stage3Full = '';
      let stage3Model = '';

      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        sseBuffer += decoder.decode(value, { stream: true });

        // Parse SSE events (data: {...}\n\n)
        const parts = sseBuffer.split('\n\n');
        sseBuffer = parts.pop() || '';

        for (const part of parts) {
          const line = part.trim();
          if (!line.startsWith('data: ')) continue;
          let evt: any;
          try {
            evt = JSON.parse(line.slice(6));
          } catch { continue; }

          switch (evt.type) {
            case 'stage1_start':
              setCouncilStage('Stage 1: Collecting responses...');
              break;
            case 'stage1_model_complete':
              stage1Results = [...stage1Results, evt.data];
              setCouncilStage(`Stage 1: ${stage1Results.length}/${councilModels.length} models done`);
              updateNodeData(runningNodeId, {
                councilData: { stage1: [...stage1Results], stage2: null, stage3: null },
              });
              break;
            case 'stage1_complete':
              stage1Results = evt.data;
              updateNodeData(runningNodeId, {
                councilData: { stage1: stage1Results, stage2: null, stage3: null },
              });
              break;
            case 'stage2_start':
              setCouncilStage('Stage 2: Peer ranking...');
              break;
            case 'stage2_complete':
              stage2Data = evt.data;
              updateNodeData(runningNodeId, {
                councilData: { stage1: stage1Results, stage2: stage2Data, stage3: null },
              });
              break;
            case 'stage3_start':
              setCouncilStage('Stage 3: Chairman synthesizing...');
              setCouncilStreamBuffer('');
              break;
            case 'stage3_chunk':
              stage3Full += evt.data.chunk;
              setCouncilStreamBuffer(stage3Full);
              setStreamBuffer(stage3Full);
              break;
            case 'stage3_complete':
              stage3Model = evt.data.model;
              stage3Full = evt.data.response;
              break;
            case 'complete': {
              const responseReceivedAt = Date.now();
              const councilData: CouncilData = {
                stage1: stage1Results,
                stage2: stage2Data,
                stage3: { model: stage3Model, response: stage3Full },
              };
              const newUserMsg = { id: `msg_${Date.now()}_u`, role: 'user', content: runningPrompt };
              const newAssistantMsg = { id: `msg_${Date.now()}_a`, role: 'assistant', content: stage3Full };
              updateNodeData(runningNodeId, {
                status: 'success',
                response: stage3Full,
                responseReceivedAt,
                councilData,
                messages: [...context, newUserMsg, newAssistantMsg] as any,
              });
              setCouncilStage('');
              generateTitle(runningNodeId, runningPrompt, stage3Full);
              break;
            }
            case 'error':
              updateNodeData(runningNodeId, { status: 'error' });
              setCouncilStage('');
              break;
          }
        }
      }
    } catch (error) {
      console.error(error);
      updateNodeData(runningNodeId, { status: 'error' });
      setCouncilStage('');
    } finally {
      setStreamingNodeId(prev => prev === runningNodeId ? null : prev);
    }
  };

  const handleChange = (field: keyof NodeData, value: any) => {
    updateNodeData(selectedNode.id, { [field]: value });
  };
  
  const handleSaveEdit = () => {
    if (!selectedNode) return;
    updateNodeData(selectedNode.id, { response: editedResponse });
    setIsEditing(false);
  };
  
  const handleCancelEdit = () => {
    setIsEditing(false);
    setEditedResponse(selectedNode?.data.response || '');
  };
  
  // Summarize response
  const handleSummarize = async () => {
    if (!selectedNode?.data.response) return;
    
    setIsSummarizing(true);
    setShowSummaryModal(false);
    
    try {
      const res = await fetch(`/api/summarize?user=${encodeURIComponent(user?.username || 'test')}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
        body: JSON.stringify({
          content: selectedNode.data.response,
          model: summaryModel
        })
      });
      
      if (res.ok) {
        const data = await res.json();
        if (data.summary) {
          // Replace response with summary
          updateNodeData(selectedNode.id, { response: data.summary });
        }
      }
    } catch (error) {
      console.error('Summarization failed:', error);
    } finally {
      setIsSummarizing(false);
    }
  };
  
  // Auto-generate title using gpt-5-nano
  const generateTitle = async (nodeId: string, userPrompt: string, response: string) => {
    try {
      const res = await fetch(`/api/generate_title?user=${encodeURIComponent(user?.username || 'test')}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
        body: JSON.stringify({ user_prompt: userPrompt, response })
      });
      
      if (res.ok) {
        const data = await res.json();
        if (data.title) {
          updateNodeData(nodeId, { label: data.title });
        }
      }
    } catch (error) {
      console.error('Failed to generate title:', error);
      // Silently fail - keep the original title
    }
  };

  // Open merge modal
  const openMergeModal = () => {
    if (!selectedNode?.data.traces) return;
    const traceIds = selectedNode.data.traces.map((t: Trace) => t.id);
    setMergeOrder(traceIds);
    setMergeSelectedIds([]);
    setShowMergePreview(false);
    setShowMergeModal(true);
  };

  // Drag-and-drop handlers for merge modal
  const handleMergeDragStart = (e: React.DragEvent, traceId: string) => {
    setMergeDraggedId(traceId);
    e.dataTransfer.effectAllowed = 'move';
  };

  const handleMergeDragOver = (e: React.DragEvent, overTraceId: string) => {
    e.preventDefault();
    if (!mergeDraggedId || mergeDraggedId === overTraceId) return;
    
    const newOrder = [...mergeOrder];
    const draggedIndex = newOrder.indexOf(mergeDraggedId);
    const overIndex = newOrder.indexOf(overTraceId);
    
    if (draggedIndex !== -1 && overIndex !== -1) {
      newOrder.splice(draggedIndex, 1);
      newOrder.splice(overIndex, 0, mergeDraggedId);
      setMergeOrder(newOrder);
    }
  };

  const handleMergeDragEnd = () => {
    setMergeDraggedId(null);
  };

  // Toggle trace selection in merge modal
  const toggleMergeSelection = (traceId: string) => {
    setMergeSelectedIds(prev => {
      if (prev.includes(traceId)) {
        return prev.filter(id => id !== traceId);
      } else {
        return [...prev, traceId];
      }
    });
  };

  // Create merged trace
  const handleCreateMergedTrace = async () => {
    if (!selectedNode || mergeSelectedIds.length < 2) return;
    
    // Get the ordered trace IDs based on mergeOrder
    const orderedSelectedIds = mergeOrder.filter(id => mergeSelectedIds.includes(id));
    
    if (mergeStrategy === 'summary') {
      setIsSummarizingMerge(true);
      try {
        const messages = computeMergedMessages(selectedNode.id, orderedSelectedIds, 'trace_order');
        const content = messages.map(m => `${m.role}: ${m.content}`).join('\n\n');
        
        const res = await fetch(`/api/summarize?user=${encodeURIComponent(user?.username || 'test')}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
          body: JSON.stringify({
            content,
            model_name: 'gpt-5-nano',
            api_key: selectedNode.data.apiKey
          })
        });
        
        if (res.ok) {
          const data = await res.json();
          const mergedId = createMergedTrace(selectedNode.id, orderedSelectedIds, 'summary');
          if (mergedId && data.summary) {
            updateMergedTrace(selectedNode.id, mergedId, { summarizedContent: data.summary });
          }
        }
      } catch (error) {
        console.error('Failed to summarize for merge:', error);
      } finally {
        setIsSummarizingMerge(false);
      }
    } else {
      createMergedTrace(selectedNode.id, orderedSelectedIds, mergeStrategy);
    }
    
    // Close modal and reset
    setShowMergeModal(false);
    setMergeSelectedIds([]);
    setShowMergePreview(false);
  };

  // Get preview of merged messages
  const getMergePreview = () => {
    if (!selectedNode || mergeSelectedIds.length < 2) return [];
    const orderedSelectedIds = mergeOrder.filter(id => mergeSelectedIds.includes(id));
    return computeMergedMessages(selectedNode.id, orderedSelectedIds, mergeStrategy);
  };

  // Check if a trace has downstream nodes from the current selected node
  const traceHasDownstream = (_trace: Trace): boolean => {
    if (!selectedNode) return false;
    
    // Find edges going out from selectedNode that are part of this trace
    const outgoingEdge = edges.find(e => 
      e.source === selectedNode.id && 
      e.sourceHandle?.startsWith('trace-')
    );
    
    return !!outgoingEdge;
  };
  
  // Quick Chat functions
  const openQuickChat = (trace: Trace | null, isNewTrace: boolean = false) => {
    if (!selectedNode) return;
    onInteract?.(); // Close context menu when opening quick chat
    
    // Check if current node has a "sent" query (has response) or just unsent draft
    const hasResponse = !!selectedNode.data.response;
    const hasDraftPrompt = !!selectedNode.data.userPrompt && !hasResponse;
    
    // Helper to extract node ID from message ID (format: nodeId-u or nodeId-a)
    const getNodeIdFromMsgId = (msgId: string): string | null => {
      if (!msgId) return null;
      const parts = msgId.split('-');
      if (parts.length >= 2) {
        // Remove last part (-u or -a) and rejoin
        return parts.slice(0, -1).join('-');
      }
      return null;
    };
    
    // Helper to build sentFiles from messages
    const buildSentFilesFromMessages = (messages: Message[]): {msgId: string, fileIds: string[]}[] => {
      const sentFiles: {msgId: string, fileIds: string[]}[] = [];
      for (const msg of messages) {
        if (msg.role === 'user' && msg.id) {
          const nodeId = getNodeIdFromMsgId(msg.id);
          if (nodeId) {
            const node = nodes.find(n => n.id === nodeId);
            if (node && node.data.attachedFileIds && node.data.attachedFileIds.length > 0) {
              sentFiles.push({ msgId: msg.id, fileIds: node.data.attachedFileIds });
            }
          }
        }
      }
      return sentFiles;
    };
    
    if (isNewTrace || !trace) {
      // Start a new trace from current node
      const initialMessages: Message[] = [];
      // Only include user prompt as message if it was actually sent (has response)
      if (selectedNode.data.userPrompt && hasResponse) {
        initialMessages.push({ id: `${selectedNode.id}-u`, role: 'user', content: selectedNode.data.userPrompt });
      }
      if (selectedNode.data.response) {
        initialMessages.push({ id: `${selectedNode.id}-a`, role: 'assistant', content: selectedNode.data.response });
      }
      
      setQuickChatTrace({
        id: `new-trace-${selectedNode.id}`,
        sourceNodeId: selectedNode.id,
        color: '#888',
        messages: initialMessages
      });
      setQuickChatMessages(initialMessages);
      setQuickChatSentFiles(buildSentFilesFromMessages(initialMessages));
      setQuickChatNeedsDuplicate(false);
      setQuickChatLastNodeId(selectedNode.id);
    } else {
      // Use existing trace context
      const hasDownstream = traceHasDownstream(trace);
      setQuickChatNeedsDuplicate(hasDownstream);
      
      // Build full message history
      const fullMessages: Message[] = [...trace.messages];
      // Only include current node's content if it was sent
      if (selectedNode.data.userPrompt && hasResponse) {
        fullMessages.push({ id: `${selectedNode.id}-u`, role: 'user', content: selectedNode.data.userPrompt });
      }
      if (selectedNode.data.response) {
        fullMessages.push({ id: `${selectedNode.id}-a`, role: 'assistant', content: selectedNode.data.response });
      }
      
      setQuickChatTrace({
        ...trace,
        sourceNodeId: selectedNode.id,
        messages: fullMessages
      });
      setQuickChatMessages(fullMessages);
      setQuickChatSentFiles(buildSentFilesFromMessages(fullMessages));
      
      // Always set last node ID to current selected node
      // handleQuickChatSend will decide whether to overwrite (if empty) or create new node (if has response)
      setQuickChatLastNodeId(selectedNode.id);
    }

    // Copy council settings from node if active
    if (selectedNode.data.councilMode && selectedNode.data.councilModels && selectedNode.data.councilModels.length >= 2) {
      setQuickChatCouncilMode(true);
      setQuickChatCouncilModels([...selectedNode.data.councilModels]);
      setQuickChatChairmanModel(selectedNode.data.chairmanModel || selectedNode.data.councilModels[0]);
      setQuickChatCouncilConfigOpen(false);
    } else {
      setQuickChatCouncilMode(false);
    }
    setQuickChatCouncilData(null);
    setQuickChatCouncilStage('');

    setQuickChatOpen(true);
    // If there's an unsent draft, put it in the input box
    setQuickChatInput(hasDraftPrompt ? selectedNode.data.userPrompt : '');
  };

  const closeQuickChat = () => {
    setQuickChatOpen(false);
    setQuickChatTrace(null);
    setQuickChatMessages([]);
    setQuickChatAttachedFiles([]);
    setQuickChatSentFiles([]);
    setQuickChatCouncilMode(false);
    setQuickChatCouncilData(null);
    setQuickChatCouncilStage('');
  };

  // Quick Chat file attachment helpers
  const getQuickChatScope = () => {
    const projectPath = currentBlueprintPath || 'untitled';
    return `${projectPath}/quick_chat_temp`;
  };

  const handleQuickChatAttach = async (fileId: string) => {
    if (!quickChatAttachedFiles.includes(fileId)) {
      setQuickChatAttachedFiles(prev => [...prev, fileId]);
      // Add scope to file for filtering
      try {
        await addFileScope(fileId, getQuickChatScope());
      } catch (e) {
        console.error('Failed to add file scope:', e);
      }
    }
    setShowQuickChatAttachModal(false);
  };

  const handleQuickChatDetach = async (fileId: string) => {
    setQuickChatAttachedFiles(prev => prev.filter(id => id !== fileId));
    // Remove scope from file
    try {
      await removeFileScope(fileId, getQuickChatScope());
    } catch (e) {
      console.error('Failed to remove file scope:', e);
    }
  };

  const handleQuickChatUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (!e.target.files || e.target.files.length === 0) return;
    const file = e.target.files[0];
    setQuickChatUploading(true);
    try {
      const meta = await uploadFile(file, { provider: 'local' });
      setQuickChatAttachedFiles(prev => [...prev, meta.id]);
      // Add scope to file for filtering
      try {
        await addFileScope(meta.id, getQuickChatScope());
      } catch (e) {
        console.error('Failed to add file scope:', e);
      }
    } catch (err) {
      alert(`Upload failed: ${(err as Error).message}`);
    } finally {
      e.target.value = '';
      setQuickChatUploading(false);
    }
  };
  
  // Open Quick Chat for a merged trace
  const openMergedQuickChat = (merged: MergedTrace) => {
    if (!selectedNode) return;
    onInteract?.();
    
    // Check if current node has a "sent" query (has response) or just unsent draft
    const hasResponse = !!selectedNode.data.response;
    const hasDraftPrompt = !!selectedNode.data.userPrompt && !hasResponse;
    
    // Helper to extract node ID from message ID (format: nodeId-u or nodeId-a)
    const getNodeIdFromMsgId = (msgId: string): string | null => {
      if (!msgId) return null;
      const parts = msgId.split('-');
      if (parts.length >= 2) {
        return parts.slice(0, -1).join('-');
      }
      return null;
    };
    
    // Build messages from merged trace
    const fullMessages: Message[] = [...merged.messages];
    // Only include current node's content if it was sent
    if (selectedNode.data.userPrompt && hasResponse) {
      fullMessages.push({ id: `${selectedNode.id}-u`, role: 'user', content: selectedNode.data.userPrompt });
    }
    if (selectedNode.data.response) {
      fullMessages.push({ id: `${selectedNode.id}-a`, role: 'assistant', content: selectedNode.data.response });
    }
    
    // Build sentFiles from messages
    const sentFiles: {msgId: string, fileIds: string[]}[] = [];
    for (const msg of fullMessages) {
      if (msg.role === 'user' && msg.id) {
        const nodeId = getNodeIdFromMsgId(msg.id);
        if (nodeId) {
          const node = nodes.find(n => n.id === nodeId);
          if (node && node.data.attachedFileIds && node.data.attachedFileIds.length > 0) {
            sentFiles.push({ msgId: msg.id, fileIds: node.data.attachedFileIds });
          }
        }
      }
    }
    
    // Create a pseudo-trace for the merged context
    setQuickChatTrace({
      id: merged.id,
      sourceNodeId: selectedNode.id,
      color: merged.colors[0] || '#888',
      messages: fullMessages
    });
    setQuickChatMessages(fullMessages);
    setQuickChatSentFiles(sentFiles);
    setQuickChatNeedsDuplicate(false); // Merged traces don't duplicate
    
    setQuickChatOpen(true);
    // If there's an unsent draft, put it in the input box
    setQuickChatInput(hasDraftPrompt ? selectedNode.data.userPrompt : '');
  };
  
  // Check if a trace is complete (all upstream nodes have Q&A)
  const canQuickChat = (trace: Trace): boolean => {
    return isTraceComplete(trace);
  };
  
  // Helper: Check if all upstream nodes have complete Q&A by traversing edges
  const checkUpstreamNodesComplete = (nodeId: string, visited: Set<string> = new Set()): boolean => {
    if (visited.has(nodeId)) return true; // Avoid cycles
    visited.add(nodeId);
    
    const node = nodes.find(n => n.id === nodeId);
    if (!node) return true;
    
    // Find all incoming edges to this node
    const incomingEdges = edges.filter(e => e.target === nodeId);
    
    for (const edge of incomingEdges) {
      const sourceNode = nodes.find(n => n.id === edge.source);
      if (!sourceNode) continue;
      
      // Check if source node is disabled - skip disabled nodes
      if (sourceNode.data.disabled) continue;
      
      // Check if source node has complete Q&A
      if (!sourceNode.data.userPrompt || !sourceNode.data.response) {
        return false; // Found an incomplete upstream node
      }
      
      // Recursively check further upstream
      if (!checkUpstreamNodesComplete(edge.source, visited)) {
        return false;
      }
    }
    
    return true;
  };

  // Helper: find incoming edge for a given trace ID (with fallbacks)
  const findIncomingEdgeForTrace = (nodeId: string, traceId: string): Edge | null => {
    // 1) exact match by sourceHandle
    let edge = edges.find(e => e.target === nodeId && e.sourceHandle === `trace-${traceId}`);
    if (edge) return edge;
    // 2) fallback: any incoming edge whose source has this trace in outgoingTraces
    edge = edges.find(e => {
      if (e.target !== nodeId) return false;
      const src = nodes.find(n => n.id === e.source);
      return src?.data.outgoingTraces?.some((t: Trace) => t.id === traceId);
    });
    return edge || null;
  };

  // Helper: get source trace IDs for a merged trace on a given node (supports propagated merged traces)
  const getMergedSourceIds = (nodeId: string, traceId: string): string[] => {
    const node = nodes.find(n => n.id === nodeId);
    if (!node) return [];
    const mergedLocal = node.data.mergedTraces?.find((m: MergedTrace) => m.id === traceId);
    if (mergedLocal) return mergedLocal.sourceTraceIds || [];
    const incomingMatch = node.data.traces?.find((t: Trace) => t.id === traceId);
    if (incomingMatch?.isMerged && incomingMatch.sourceTraceIds) return incomingMatch.sourceTraceIds;
    const outgoingMatch = node.data.outgoingTraces?.find((t: Trace) => t.id === traceId);
    if (outgoingMatch?.isMerged && outgoingMatch.sourceTraceIds) return outgoingMatch.sourceTraceIds;
    return [];
  };

  // Recursive: Check if specific trace path upstream has complete nodes (supports multi-level merged)
  const checkTracePathComplete = (
    nodeId: string,
    traceId: string,
    visited: Set<string> = new Set()
  ): boolean => {
    const visitKey = `${nodeId}-${traceId}`;
    if (visited.has(visitKey)) return true;
    visited.add(visitKey);

    // Determine if this node is the merge owner or just receiving a propagated merged trace
    const localMerge = nodes.find(n => n.id === nodeId)?.data.mergedTraces?.some(m => m.id === traceId);
    const localParents = getMergedSourceIds(nodeId, traceId);

    const incomingEdge = findIncomingEdgeForTrace(nodeId, traceId);
    if (!incomingEdge) {
      // If no incoming edge and this node owns the merge, check parents from here
      if (localMerge && localParents.length > 0) {
        for (const pid of localParents) {
          if (!checkTracePathComplete(nodeId, pid, visited)) return false;
        }
        return true;
      }
      return true; // head
    }

    const sourceNode = nodes.find(n => n.id === incomingEdge.source);
    if (!sourceNode || sourceNode.data.disabled) return true;

    // If merged at sourceNode (or propagated merged), recurse into each parent from the merge owner
    const parentIds = localMerge ? localParents : getMergedSourceIds(sourceNode.id, traceId);
    if (parentIds.length > 0) {
      const mergeOwnerId = localMerge ? nodeId : sourceNode.id;
      for (const pid of parentIds) {
        if (!checkTracePathComplete(mergeOwnerId, pid, visited)) return false;
      }
      return true;
    }

    // Regular trace: check node content then continue upstream
    if (!sourceNode.data.userPrompt || !sourceNode.data.response) return false;
    return checkTracePathComplete(sourceNode.id, traceId, visited);
  };

  // Recursive: Find the first empty node on a specific trace path (supports multi-level merged)
  const findEmptyNodeOnTrace = (
    nodeId: string,
    traceId: string,
    visited: Set<string> = new Set()
  ): string | null => {
    const visitKey = `${nodeId}-${traceId}`;
    if (visited.has(visitKey)) return null;
    visited.add(visitKey);

    // Determine if this node owns the merge or just receives propagated merged trace
    const localMerge = nodes.find(n => n.id === nodeId)?.data.mergedTraces?.some(m => m.id === traceId);
    const localParents = getMergedSourceIds(nodeId, traceId);

    const incomingEdge = findIncomingEdgeForTrace(nodeId, traceId);
    if (!incomingEdge) {
      if (localMerge && localParents.length > 0) {
        for (const pid of localParents) {
          const upstreamEmpty = findEmptyNodeOnTrace(nodeId, pid, visited);
          if (upstreamEmpty) return upstreamEmpty;
        }
      }
      return null;
    }

    const sourceNode = nodes.find(n => n.id === incomingEdge.source);
    if (!sourceNode || sourceNode.data.disabled) return null;

    const parentIds = localMerge ? localParents : getMergedSourceIds(sourceNode.id, traceId);
    if (parentIds.length > 0) {
      const mergeOwnerId = localMerge ? nodeId : sourceNode.id;
      for (const pid of parentIds) {
        const upstreamEmpty = findEmptyNodeOnTrace(mergeOwnerId, pid, visited);
        if (upstreamEmpty) return upstreamEmpty;
      }
    }

    if (!sourceNode.data.userPrompt || !sourceNode.data.response) {
      return sourceNode.id;
    }
    return findEmptyNodeOnTrace(sourceNode.id, traceId, visited);
  };

  // Check if all active traces are complete (for main Run Node button)
  const checkActiveTracesComplete = (): { complete: boolean; incompleteTraceId?: string } => {
    if (!selectedNode) return { complete: true };
    
    const activeTraceIds = selectedNode.data.activeTraceIds || [];
    if (activeTraceIds.length === 0) return { complete: true };
    
    // Check upstream nodes ONLY for active traces (supports merged trace recursion)
    for (const traceId of activeTraceIds) {
      if (!checkTracePathComplete(selectedNode.id, traceId)) {
        return { complete: false, incompleteTraceId: 'upstream' };
      }
    }
    
    // Check incoming traces content (message integrity)
    const incomingTraces = selectedNode.data.traces || [];
    for (const traceId of activeTraceIds) {
      const trace = incomingTraces.find((t: Trace) => t.id === traceId);
      if (trace && !isTraceComplete(trace)) {
        return { complete: false, incompleteTraceId: traceId };
      }
    }
    
    // Check merged traces content (including propagated merged traces)
    for (const traceId of activeTraceIds) {
      const sourceIds = getMergedSourceIds(selectedNode.id, traceId);
      if (sourceIds.length > 0) {
        for (const sourceId of sourceIds) {
          const sourceTrace = incomingTraces.find((t: Trace) => t.id === sourceId);
          if (sourceTrace && !isTraceComplete(sourceTrace)) {
            return { complete: false, incompleteTraceId: sourceId };
          }
        }
      }
    }
    
    return { complete: true };
  };

  // Navigate to an empty upstream node on the active traces
  const navigateToEmptyNode = () => {
    if (!selectedNode) return;
    const activeTraceIds = selectedNode.data.activeTraceIds || [];
    
    for (const traceId of activeTraceIds) {
      const emptyNodeId = findEmptyNodeOnTrace(selectedNode.id, traceId);
      if (emptyNodeId) {
        const emptyNode = nodes.find(n => n.id === emptyNodeId);
        if (emptyNode) {
          setCenter(emptyNode.position.x + 100, emptyNode.position.y + 50, { zoom: 1.2, duration: 500 });
          setSelectedNode(emptyNodeId);
          return; // Found one, navigate and stop
        }
      }
    }
  };
  
  const activeTracesCheck = selectedNode ? checkActiveTracesComplete() : { complete: true };

  const handleQuickChatSend = async () => {
    // Allow send if there's text OR attached files
    const hasContent = quickChatInput.trim() || quickChatAttachedFiles.length > 0;
    if (!hasContent || !quickChatTrace || quickChatLoading || !selectedNode) return;

    const userInput = quickChatInput;
    const attachedFilesCopy = [...quickChatAttachedFiles];
    const msgId = `qc_${Date.now()}_u`;
    
    const userMessage: Message = {
      id: msgId,
      role: 'user',
      content: userInput || '[Files attached]'
    };

    // Track sent files for display
    if (attachedFilesCopy.length > 0) {
      setQuickChatSentFiles(prev => [...prev, { msgId, fileIds: attachedFilesCopy }]);
    }

    // Add user message to display
    const messagesBeforeSend = [...quickChatMessages];
    setQuickChatMessages(prev => [...prev, userMessage]);
    setQuickChatInput('');
    setQuickChatAttachedFiles([]); // Clear attached files after send
    setQuickChatLoading(true);

    // Store model at send time to avoid issues with model switching during streaming
    const modelAtSend = quickChatModel;
    const tempAtSend = quickChatTemp;
    const effortAtSend = quickChatEffort;
    const webSearchAtSend = quickChatWebSearch;
    const reasoningModels = ['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'];

    // Snapshot council config at send time
    const isCouncilSend = quickChatCouncilMode && quickChatCouncilModels.length >= 2;
    const councilModelsAtSend = isCouncilSend ? [...quickChatCouncilModels] : [];
    const chairmanAtSend = isCouncilSend ? (quickChatChairmanModel || quickChatCouncilModels[0]) : null;

    try {
      // Build scopes for file search (Quick Chat uses a temp scope)
      const projectPath = currentBlueprintPath || 'untitled';
      const scopes = [`${projectPath}/quick_chat_temp`];

      let fullResponse = '';

      if (isCouncilSend && chairmanAtSend) {
        // ========== COUNCIL MODE ==========
        setQuickChatCouncilStage('Starting council...');
        setQuickChatCouncilData({ stage1: null, stage2: null, stage3: null });

        const response = await fetch(`/api/run_council_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
          body: JSON.stringify({
            node_id: 'quick_chat_temp',
            incoming_contexts: [{ messages: messagesBeforeSend }],
            user_prompt: userInput || 'Please analyze the attached files.',
            council_models: councilModelsAtSend.map(cfg => ({
              model_name: cfg.model,
              temperature: cfg.temperature ?? null,
              reasoning_effort: cfg.reasoningEffort ?? null,
              enable_google_search: cfg.enableWebSearch ?? null,
            })),
            chairman_model: {
              model_name: chairmanAtSend.model,
              temperature: chairmanAtSend.temperature ?? null,
              reasoning_effort: chairmanAtSend.reasoningEffort ?? null,
              enable_google_search: chairmanAtSend.enableWebSearch ?? null,
            },
            system_prompt: selectedNode.data.systemPrompt || null,
            temperature: selectedNode.data.temperature,
            reasoning_effort: selectedNode.data.reasoningEffort || 'medium',
            enable_google_search: selectedNode.data.enableGoogleSearch !== false,
            merge_strategy: 'smart',
            attached_file_ids: attachedFilesCopy,
            scopes,
          }),
        });

        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();
        const decoder = new TextDecoder();
        let sseBuffer = '';
        let stage1Results: Array<{ model: string; response: string }> = [];
        let stage2Data: any = null;
        let stage3Full = '';
        let stage3Model = '';

        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          sseBuffer += decoder.decode(value, { stream: true });
          const parts = sseBuffer.split('\n\n');
          sseBuffer = parts.pop() || '';

          for (const part of parts) {
            const line = part.trim();
            if (!line.startsWith('data: ')) continue;
            let evt: any;
            try { evt = JSON.parse(line.slice(6)); } catch { continue; }

            switch (evt.type) {
              case 'stage1_start':
                setQuickChatCouncilStage('Stage 1: Collecting responses...');
                break;
              case 'stage1_model_complete':
                stage1Results = [...stage1Results, evt.data];
                setQuickChatCouncilStage(`Stage 1: ${stage1Results.length}/${councilModelsAtSend.length} models done`);
                setQuickChatCouncilData({ stage1: [...stage1Results], stage2: null, stage3: null });
                break;
              case 'stage1_complete':
                stage1Results = evt.data;
                setQuickChatCouncilData({ stage1: stage1Results, stage2: null, stage3: null });
                break;
              case 'stage2_start':
                setQuickChatCouncilStage('Stage 2: Peer ranking...');
                break;
              case 'stage2_complete':
                stage2Data = evt.data;
                setQuickChatCouncilData({ stage1: stage1Results, stage2: stage2Data, stage3: null });
                break;
              case 'stage3_start':
                setQuickChatCouncilStage('Stage 3: Chairman synthesizing...');
                break;
              case 'stage3_chunk':
                stage3Full += evt.data.chunk;
                // Stream chairman response into chat
                setQuickChatMessages(prev => {
                  const newMsgs = [...prev];
                  const lastMsg = newMsgs[newMsgs.length - 1];
                  if (lastMsg?.role === 'assistant') {
                    return [...newMsgs.slice(0, -1), { ...lastMsg, content: stage3Full }];
                  } else {
                    return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: stage3Full }];
                  }
                });
                break;
              case 'stage3_complete':
                stage3Model = evt.data.model;
                stage3Full = evt.data.response;
                break;
              case 'complete': {
                const finalData: CouncilData = {
                  stage1: stage1Results,
                  stage2: stage2Data,
                  stage3: { model: stage3Model, response: stage3Full },
                };
                setQuickChatCouncilData(finalData);
                setQuickChatCouncilStage('');
                // Ensure final message is set
                setQuickChatMessages(prev => {
                  const newMsgs = [...prev];
                  const lastMsg = newMsgs[newMsgs.length - 1];
                  if (lastMsg?.role === 'assistant') {
                    return [...newMsgs.slice(0, -1), { ...lastMsg, content: stage3Full }];
                  } else {
                    return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: stage3Full }];
                  }
                });
                break;
              }
              case 'error':
                setQuickChatCouncilStage('');
                throw new Error(evt.data?.message || 'Council error');
            }
          }
        }
        fullResponse = stage3Full;
      } else {
        // ========== SINGLE MODEL MODE ==========
        // Determine provider
        const isClaude = modelAtSend.includes('claude');
        const isOpenAI = modelAtSend.includes('gpt') || modelAtSend === 'o3';
        const provider = isClaude ? 'claude' : isOpenAI ? 'openai' : 'google';
        const isReasoning = reasoningModels.includes(modelAtSend);

        const response = await fetch(`/api/run_node_stream?user=${encodeURIComponent(user?.username || 'test')}`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json', ...getAuthHeader() },
          body: JSON.stringify({
            node_id: 'quick_chat_temp',
            incoming_contexts: [{ messages: messagesBeforeSend }],
            user_prompt: userInput || 'Please analyze the attached files.',
            attached_file_ids: attachedFilesCopy,
            scopes,
            merge_strategy: 'smart',
            config: {
              provider,
              model_name: modelAtSend,
              temperature: isReasoning ? 1 : tempAtSend,
              enable_google_search: webSearchAtSend,
              reasoning_effort: effortAtSend,
            }
          })
        });

        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();
        const decoder = new TextDecoder();

        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          const chunk = decoder.decode(value);
          fullResponse += chunk;

          setQuickChatMessages(prev => {
            const newMsgs = [...prev];
            const lastMsg = newMsgs[newMsgs.length - 1];
            if (lastMsg?.role === 'assistant') {
              return [...newMsgs.slice(0, -1), { ...lastMsg, content: fullResponse }];
            } else {
              return [...newMsgs, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }];
            }
          });
        }
      }

      // Determine whether to overwrite current node or create new one
      // Use quickChatLastNodeId as the "current" node in the chat flow to ensure continuity
      // If not set, fallback to quickChatTrace.sourceNodeId (initial state)
      const fromNodeId = quickChatLastNodeId || quickChatTrace.sourceNodeId;
      const fromNode = nodes.find(n => n.id === fromNodeId);
      const fromNodeHasResponse = fromNode?.data.response && fromNode.data.response.trim() !== '';
      
      if (!fromNodeHasResponse && fromNode) {
        // Overwrite the source node (it's empty)
        const nodeUpdate: any = {
          userPrompt: userInput,
          response: fullResponse,
          model: isCouncilSend ? chairmanAtSend!.model : modelAtSend,
          temperature: isCouncilSend ? selectedNode.data.temperature : (reasoningModels.includes(modelAtSend) ? 1 : tempAtSend),
          reasoningEffort: isCouncilSend ? (selectedNode.data.reasoningEffort || 'medium') : effortAtSend,
          enableGoogleSearch: isCouncilSend ? (selectedNode.data.enableGoogleSearch !== false) : webSearchAtSend,
          attachedFileIds: attachedFilesCopy,
          status: 'success',
          querySentAt: Date.now(),
          responseReceivedAt: Date.now(),
        };
        if (isCouncilSend) {
          nodeUpdate.councilMode = true;
          nodeUpdate.councilModels = councilModelsAtSend;
          nodeUpdate.chairmanModel = chairmanAtSend;
          nodeUpdate.councilData = quickChatCouncilData;
        }
        updateNodeData(fromNodeId, nodeUpdate);
        
        // Update trace to reflect current node now has content
        setQuickChatTrace(prev => prev ? {
          ...prev,
          messages: [...messagesBeforeSend, userMessage, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }]
        } : null);
        
        // Update last node ID
        setQuickChatLastNodeId(fromNodeId);
        
        // Generate title
        generateTitle(fromNodeId, userInput, fullResponse);
      } else {
        // Create new node (source node has response, continue the chain)
        const newNodeId = `node_${Date.now()}`;
        const sourceNode = fromNode || selectedNode;
        const newPos = {
          x: sourceNode.position.x + 300,
          y: sourceNode.position.y
        };
        
        const newNodeData: any = {
            label: isCouncilSend ? 'Council Chat' : 'Quick Chat',
            model: isCouncilSend ? chairmanAtSend!.model : modelAtSend,
            temperature: isCouncilSend ? selectedNode.data.temperature : (reasoningModels.includes(modelAtSend) ? 1 : tempAtSend),
            systemPrompt: '',
            userPrompt: userInput,
            mergeStrategy: 'smart' as const,
            reasoningEffort: isCouncilSend ? (selectedNode.data.reasoningEffort || 'medium') : effortAtSend,
            enableGoogleSearch: isCouncilSend ? (selectedNode.data.enableGoogleSearch !== false) : webSearchAtSend,
            traces: [],
            outgoingTraces: [],
            forkedTraces: [],
            mergedTraces: [],
            activeTraceIds: [],
            attachedFileIds: attachedFilesCopy,
            response: fullResponse,
            status: 'success' as const,
            inputs: 1,
            querySentAt: Date.now(),
            responseReceivedAt: Date.now(),
        };
        if (isCouncilSend) {
          newNodeData.councilMode = true;
          newNodeData.councilModels = councilModelsAtSend;
          newNodeData.chairmanModel = chairmanAtSend;
          newNodeData.councilData = quickChatCouncilData;
        }
        const newNode = {
          id: newNodeId,
          type: 'llmNode',
          position: newPos,
          data: {
            ...newNodeData,
          }
        };

        addNode(newNode);

        // Connect to the source node
        setTimeout(() => {
          const store = useFlowStore.getState();
          const currentEdges = store.edges;
          const sourceNodeData = store.nodes.find(n => n.id === fromNodeId);
          
          // Find the right trace handle to use
          let sourceHandle = 'new-trace';
          
          // Get the base trace ID (e.g., 'trace-A' from 'trace-A_B_C' or 'new-trace-A' or 'merged-xxx')
          const currentTraceId = quickChatTrace?.id || '';
          const isNewTrace = currentTraceId.startsWith('new-trace-');
          const isMergedTrace = currentTraceId.startsWith('merged-');
          
          if (isMergedTrace) {
            // For merged trace: find the merged trace handle on the source node
            // The trace ID may have evolved (e.g., 'merged-xxx' -> 'merged-xxx_nodeA' -> 'merged-xxx_nodeA_nodeB')
            // We need to find the version that ends with the current source node ID
            
            // First try: exact match with evolved ID (merged-xxx_sourceNodeId)
            const evolvedMergedId = `${currentTraceId}_${fromNodeId}`;
            let mergedOutgoing = sourceNodeData?.data.outgoingTraces?.find(
              t => t.id === evolvedMergedId
            );
            
            // Second try: find trace that starts with merged ID and ends with this node
            if (!mergedOutgoing) {
              mergedOutgoing = sourceNodeData?.data.outgoingTraces?.find(
                t => t.id.startsWith(currentTraceId) && t.id.endsWith(`_${fromNodeId}`)
              );
            }
            
            // Third try: find any trace that contains the merged ID
            if (!mergedOutgoing) {
              mergedOutgoing = sourceNodeData?.data.outgoingTraces?.find(
                t => t.id.startsWith(currentTraceId) || t.id === currentTraceId
              );
            }
            
            // Fourth try: find any merged trace
            if (!mergedOutgoing) {
              mergedOutgoing = sourceNodeData?.data.outgoingTraces?.find(
                t => t.id.startsWith('merged-')
              );
            }
            
            if (mergedOutgoing) {
              sourceHandle = `trace-${mergedOutgoing.id}`;
            } else {
              // Last resort: use the merged trace ID directly
              sourceHandle = `trace-${currentTraceId}`;
            }
          } else if (isNewTrace) {
            // For "Start New Trace": create a fresh independent trace from the original node
            // First, check if this is the original starting node or a continuation node
            const originalStartNodeId = currentTraceId.replace('new-trace-', '');
            const isOriginalNode = fromNodeId === originalStartNodeId;
            
            if (isOriginalNode) {
              // This is the first round - starting from original node
              const hasOutgoingEdges = currentEdges.some(e => e.source === fromNodeId);
              if (hasOutgoingEdges) {
                // Original node already has downstream - create a new fork
                sourceHandle = 'new-trace';
              } else {
                // No downstream yet - use self trace
                const selfTrace = sourceNodeData?.data.outgoingTraces?.find(
                  t => t.id === `trace-${fromNodeId}`
                );
                if (selfTrace) {
                  sourceHandle = `trace-${selfTrace.id}`;
                }
              }
            } else {
              // This is a continuation - find the trace ID (should be preserved now)
              // Look for a trace that was created from the original node's self trace
              const matchingTrace = sourceNodeData?.data.outgoingTraces?.find(t => {
                return t.id.includes(originalStartNodeId);
              });
              
              if (matchingTrace) {
                sourceHandle = `trace-${matchingTrace.id}`;
              } else {
                // Fallback 1: Check INCOMING traces (Connect to Continue Handle)
                const incoming = sourceNodeData?.data.traces?.find(t => 
                  t.id.includes(originalStartNodeId)
                );
                
                if (incoming) {
                  // ID is preserved, so handle ID is just trace-{id}
                  sourceHandle = `trace-${incoming.id}`;
                } else {
                  // Fallback 2: find any trace that ends with fromNodeId (unlikely if ID preserved)
                  const anyMatch = sourceNodeData?.data.outgoingTraces?.find(
                    t => t.id === `trace-${fromNodeId}`
                  );
                  if (anyMatch) {
                    sourceHandle = `trace-${anyMatch.id}`;
                  }
                }
              }
            }
          } else {
            // For existing trace: ID is preserved
            const baseTraceId = currentTraceId.replace(/^trace-/, '');
            
            // 1. Try OUTGOING traces first (if already connected downstream)
            const matchingOutgoing = sourceNodeData?.data.outgoingTraces?.find(t => {
              const traceBase = t.id.replace(/^trace-/, '');
              return traceBase === baseTraceId; // Exact match now
            });
            
            if (matchingOutgoing) {
              sourceHandle = `trace-${matchingOutgoing.id}`;
            } else {
              // 2. Try INCOMING traces (Connect to Continue Handle)
              const matchingIncoming = sourceNodeData?.data.traces?.find(t => {
                 const tId = t.id.replace(/^trace-/, '');
                 return tId === baseTraceId; // Exact match now
              });
              
              if (matchingIncoming) {
                 // ID is preserved
                 sourceHandle = `trace-${matchingIncoming.id}`;
              }
            }
          }
          
          // If this is the first message and we need to duplicate (has downstream),
          // onConnect will automatically handle the trace duplication
          // because the sourceHandle already has an outgoing edge
          
          store.onConnect({
            source: fromNodeId,
            sourceHandle,
            target: newNodeId,
            targetHandle: 'input-0'
          });
          
          // After first duplication, subsequent messages continue on the new trace
          // Reset the duplicate flag since we're now on the new branch
          setQuickChatNeedsDuplicate(false);
          
          // Update trace for continued chat - use newNodeId as the new source
          // Find the actual trace ID on the new node to ensure continuity
          const newNode = store.nodes.find(n => n.id === newNodeId);
          const currentId = quickChatTrace?.id || '';
          const isMerged = currentId.startsWith('merged-');
          const isCurrentNewTrace = currentId.startsWith('new-trace-');
          
          let nextTraceId = currentId;
          
          if (newNode && newNode.data.outgoingTraces) {
            // Find the trace that continues the current conversation
            // Now trace IDs don't evolve, so it should be simpler
            
            if (isMerged) {
               // Merged traces might still need evolution or logic check
               // For now assuming linear extension keeps same ID if we changed flowStore
               // But merged trace logic in flowStore might still append ID?
               // Let's check if evolved version exists
               const evolved = newNode.data.outgoingTraces.find(t => 
                 t.id === `${currentId}_${newNodeId}`
               );
               if (evolved) nextTraceId = evolved.id;
               else nextTraceId = currentId; // Try keeping same ID
            } else if (isCurrentNewTrace) {
               // For new trace, check if we have an outgoing trace with the start node ID
               const startNodeId = currentId.replace('new-trace-', '');
               const match = newNode.data.outgoingTraces.find(t => 
                 t.id.includes(startNodeId)
               );
               if (match) nextTraceId = match.id;
            } else {
               // Regular trace: ID should be preserved
               nextTraceId = currentId;
            }
          }
          
          setQuickChatTrace(prev => prev ? {
            ...prev,
            id: nextTraceId,
            sourceNodeId: newNodeId,
            messages: [...messagesBeforeSend, userMessage, { id: `qc_${Date.now()}_a`, role: 'assistant', content: fullResponse }]
          } : null);
          
          // Update last node ID to the new node
          setQuickChatLastNodeId(newNodeId);
          
          // Generate title
          generateTitle(newNodeId, userInput, fullResponse);
        }, 100);
      }

    } catch (error) {
      console.error('Quick chat error:', error);
      setQuickChatMessages(prev => [...prev, {
        id: `qc_err_${Date.now()}`,
        role: 'assistant',
        content: `Error: ${error}`
      }]);
      setQuickChatCouncilStage('');
    } finally {
      setQuickChatLoading(false);
      setQuickChatCouncilStage('');
      // Refocus the input after sending
      setTimeout(() => {
        quickChatInputRef.current?.focus();
      }, 50);
    }
  };

  return (
    <div 
      className={`w-96 border-l h-screen flex flex-col shadow-xl z-10 transition-all duration-300 ${
        isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-200 bg-white'
      }`}
      onClick={onInteract}
    >
      {/* Header */}
      <div className={`p-4 border-b flex flex-col gap-2 ${
        isDark ? 'border-gray-700 bg-gray-900' : 'border-gray-200 bg-gray-50'
      }`}>
        <div className="flex justify-between items-center">
           <input 
             type="text" 
             value={selectedNode.data.label}
             onChange={(e) => handleChange('label', e.target.value)}
             className={`font-bold text-lg bg-transparent border-none focus:ring-0 focus:outline-none w-full ${
               isDark ? 'text-gray-200' : 'text-gray-900'
             }`}
           />
           <button onClick={onToggle} className={`p-1 rounded shrink-0 ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`}>
             <ChevronRight size={16} className={isDark ? 'text-gray-400' : 'text-gray-500'} />
           </button>
        </div>
        <div className="flex items-center justify-between mt-1">
          <div className={`text-xs px-2 py-1 rounded uppercase ${
            isDark ? 'bg-blue-900 text-blue-300' : 'bg-blue-100 text-blue-700'
          }`}>
          {selectedNode.data.status}
          </div>
          <div className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
             ID: {selectedNode.id}
          </div>
        </div>
      </div>

      {/* Tabs */}
      <div className={`flex border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
        <button 
          onClick={() => setActiveTab('interact')}
          className={`flex-1 p-3 text-sm flex justify-center items-center gap-2 ${activeTab === 'interact' ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-600 hover:bg-gray-50'}`}
        >
          <Play size={16} /> Interact
        </button>
        <button 
          onClick={() => setActiveTab('settings')}
          className={`flex-1 p-3 text-sm flex justify-center items-center gap-2 ${activeTab === 'settings' ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-600 hover:bg-gray-50'}`}
        >
          <Settings size={16} /> Settings
        </button>
        <button 
          onClick={() => setActiveTab('debug')}
          className={`flex-1 p-3 text-sm flex justify-center items-center gap-2 ${activeTab === 'debug' ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-600 hover:bg-gray-50'}`}
        >
          <Info size={16} /> Debug
        </button>
      </div>

      {/* Content */}
      <div className="flex-1 overflow-y-auto p-4">
        {activeTab === 'interact' && (
          <div className="space-y-4">
            <div>
              <div className="flex items-center justify-between mb-1">
                <label className="block text-sm font-medium text-gray-700">Model</label>
                <button
                  onClick={() => handleChange('councilMode', !selectedNode.data.councilMode)}
                  className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded-full transition-colors ${
                    selectedNode.data.councilMode
                      ? 'bg-amber-100 text-amber-700 border border-amber-300 dark:bg-amber-900/50 dark:text-amber-300 dark:border-amber-700'
                      : isDark ? 'bg-gray-700 text-gray-400 hover:bg-gray-600' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'
                  }`}
                >
                  <Users size={11} />
                  Council {selectedNode.data.councilMode ? 'ON' : 'OFF'}
                </button>
              </div>

              {!selectedNode.data.councilMode ? (
                /* Single model selector */
                <select
                  value={selectedNode.data.model}
                  onChange={(e) => {
                    const newModel = e.target.value;
                    const reasoningModels = [
                      'gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano',
                      'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest',
                      'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'
                    ];
                    if (reasoningModels.includes(newModel)) {
                      handleChange('temperature', 1);
                    }
                    handleChange('model', newModel);
                  }}
                  className="w-full border border-gray-300 rounded-md p-2 text-sm"
                >
                  <optgroup label="Claude">
                    <option value="claude-sonnet-4-5">claude-sonnet-4.5</option>
                    <option value="claude-opus-4">claude-opus-4</option>
                    <option value="claude-opus-4-5">claude-opus-4.5</option>
                    <option value="claude-opus-4-6">claude-opus-4.6</option>
                  </optgroup>
                  <optgroup label="Gemini">
                    <option value="gemini-2.5-flash">gemini-2.5-flash</option>
                    <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
                    <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
                  </optgroup>
                  <optgroup label="OpenAI (Standard)">
                    <option value="gpt-4.1">gpt-4.1</option>
                    <option value="gpt-4o">gpt-4o</option>
                  </optgroup>
                  <optgroup label="OpenAI (Reasoning)">
                    <option value="gpt-5">gpt-5</option>
                    <option value="gpt-5-chat-latest">gpt-5-chat-latest</option>
                    <option value="gpt-5-mini">gpt-5-mini</option>
                    <option value="gpt-5-nano">gpt-5-nano</option>
                    <option value="gpt-5-pro" disabled={!canUsePremiumModels}>gpt-5-pro {!canUsePremiumModels && '🔒'}</option>
                    <option value="gpt-5.1">gpt-5.1</option>
                    <option value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>
                    <option value="gpt-5.2">gpt-5.2</option>
                    <option value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
                    <option value="gpt-5.2-pro" disabled={!canUsePremiumModels}>gpt-5.2-pro {!canUsePremiumModels && '🔒'}</option>
                    <option value="o3" disabled={!canUsePremiumModels}>o3 {!canUsePremiumModels && '🔒'}</option>
                  </optgroup>
                </select>
              ) : (
                /* Council mode: multi-model selector + chairman */
                <div className={`space-y-2 p-2 rounded border ${isDark ? 'bg-gray-900 border-amber-800/50' : 'bg-amber-50/50 border-amber-200'}`}>
                  <div>
                    <label className={`block text-xs font-semibold mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Council Members (pick 2-6)</label>
                    <div className="grid grid-cols-1 gap-0.5 max-h-40 overflow-y-auto">
                      {(() => {
                        const councilModelsList = [
                          { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' },
                          { value: 'claude-opus-4', label: 'claude-opus-4' },
                          { value: 'claude-opus-4-5', label: 'claude-opus-4.5' },
                          { value: 'claude-opus-4-6', label: 'claude-opus-4.6' },
                          { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
                          { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' },
                          { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },
                          { value: 'gpt-4.1', label: 'gpt-4.1' },
                          { value: 'gpt-4o', label: 'gpt-4o' },
                          { value: 'gpt-5', label: 'gpt-5' },
                          { value: 'gpt-5-mini', label: 'gpt-5-mini' },
                          { value: 'gpt-5-nano', label: 'gpt-5-nano' },
                          { value: 'gpt-5.1', label: 'gpt-5.1' },
                          { value: 'gpt-5.2', label: 'gpt-5.2' },
                          { value: 'o3', label: 'o3', premium: true },
                        ];
                        const members: CouncilMemberConfig[] = selectedNode.data.councilModels || [];
                        const isReasoningModel = (v: string) => ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(v);
                        const supportsWebSearch = (v: string) => v.startsWith('gemini-') || v.startsWith('gpt-5') || v === 'o3' || v === 'gpt-4o';
                        const updateCouncilMember = (modelName: string, field: string, value: any) => {
                          const updated = [...members];
                          const idx = updated.findIndex(c => c.model === modelName);
                          if (idx >= 0) {
                            updated[idx] = { ...updated[idx], [field]: value };
                            handleChange('councilModels', updated);
                          }
                        };
                        // Gather available complete traces for per-member context selection
                        const availableTraces: Array<{ id: string; label: string; color: string }> = [];
                        const incTraces = selectedNode.data.traces || [];
                        const outTraces = (selectedNode.data.outgoingTraces || []) as Trace[];
                        const origTraces = outTraces.filter(t => {
                          if (t.id.startsWith('merged-')) return false;
                          return t.sourceNodeId === selectedNode.id || t.id.includes(`fork-${selectedNode.id}`) || t.id === `trace-${selectedNode.id}`;
                        });
                        const trMap = new Map<string, Trace>();
                        origTraces.forEach(t => trMap.set(t.id, t));
                        incTraces.forEach(t => trMap.set(t.id, t));
                        trMap.forEach((trace, id) => {
                          if (isTraceComplete(trace)) {
                            availableTraces.push({ id, label: `#${id.slice(-4)}`, color: trace.color });
                          }
                        });
                        (selectedNode.data.mergedTraces || []).forEach((mt: MergedTrace) => {
                          if (mt.messages && mt.messages.length > 0) {
                            availableTraces.push({ id: mt.id, label: `M#${mt.id.slice(-4)}`, color: mt.colors?.[0] || '#888' });
                          }
                        });
                        const defaultTraceId = selectedNode.data.activeTraceIds?.[0] || '';
                        const defaultTraceColor = availableTraces.find(t => t.id === defaultTraceId)?.color
                          || trMap.get(defaultTraceId)?.color || '#888';
                        return councilModelsList.map(m => {
                          const selected = members.some(c => c.model === m.value);
                          const disabled = (m as any).premium && !canUsePremiumModels;
                          const cfg = members.find(c => c.model === m.value);
                          return (
                            <div key={m.value}>
                              <label className={`flex items-center gap-1.5 text-xs py-0.5 px-1 rounded cursor-pointer ${
                                disabled ? 'opacity-40 cursor-not-allowed' : selected
                                  ? isDark ? 'bg-amber-900/40 text-amber-200' : 'bg-amber-100 text-amber-800'
                                  : isDark ? 'hover:bg-gray-800 text-gray-300' : 'hover:bg-gray-100 text-gray-700'
                              }`}>
                                <input
                                  type="checkbox"
                                  checked={selected}
                                  disabled={disabled}
                                  onChange={() => {
                                    const next = selected
                                      ? members.filter(c => c.model !== m.value)
                                      : [...members, { model: m.value }];
                                    handleChange('councilModels', next);
                                    // Auto-set chairman to first selected if current chairman was removed
                                    if (selected && selectedNode.data.chairmanModel?.model === m.value && next.length > 0) {
                                      handleChange('chairmanModel', { model: next[0].model });
                                    }
                                  }}
                                  className="w-3 h-3 accent-amber-500"
                                />
                                {m.label}
                              </label>
                              {selected && cfg && (
                                <div className={`flex items-center gap-2 ml-5 mt-0.5 mb-1 px-1 py-0.5 text-xs rounded ${isDark ? 'bg-gray-800/60' : 'bg-gray-100/80'}`}>
                                  <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                                    T:
                                    <input
                                      type="number"
                                      min={0} max={2} step={0.1}
                                      value={cfg.temperature ?? selectedNode.data.temperature}
                                      disabled={isReasoningModel(m.value)}
                                      onChange={(e) => updateCouncilMember(m.value, 'temperature', parseFloat(e.target.value) || 0)}
                                      className={`w-12 border rounded px-1 py-0 text-xs ${isReasoningModel(m.value) ? 'opacity-50 cursor-not-allowed' : ''} ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
                                    />
                                  </label>
                                  {isReasoningModel(m.value) && (
                                    <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                                      Effort:
                                      <select
                                        value={cfg.reasoningEffort ?? selectedNode.data.reasoningEffort ?? 'medium'}
                                        onChange={(e) => updateCouncilMember(m.value, 'reasoningEffort', e.target.value)}
                                        className={`border rounded px-1 py-0 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
                                      >
                                        <option value="low">Low</option>
                                        <option value="medium">Med</option>
                                        <option value="high">High</option>
                                      </select>
                                    </label>
                                  )}
                                  {supportsWebSearch(m.value) && (
                                    <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                                      <input
                                        type="checkbox"
                                        checked={cfg.enableWebSearch ?? (selectedNode.data.enableGoogleSearch !== false)}
                                        onChange={(e) => updateCouncilMember(m.value, 'enableWebSearch', e.target.checked)}
                                        className="w-3 h-3 accent-amber-500"
                                      />
                                      Web
                                    </label>
                                  )}
                                  {availableTraces.length > 1 && (() => {
                                    const selectedTrace = availableTraces.find(t => t.id === cfg.traceId);
                                    return (
                                      <div className="relative">
                                        <button
                                          type="button"
                                          onClick={(e) => {
                                            e.stopPropagation();
                                            const key = `ctx-${m.value}`;
                                            setOpenCtxDropdown(prev => prev === key ? null : key);
                                          }}
                                          className={`flex items-center gap-1 border rounded px-1 py-0 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200 hover:bg-gray-600' : 'bg-white border-gray-300 hover:bg-gray-50'}`}
                                        >
                                          <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: selectedTrace ? selectedTrace.color : defaultTraceColor }} />
                                          {selectedTrace ? selectedTrace.label : 'Ctx'}
                                          <ChevronDown size={8} />
                                        </button>
                                        {openCtxDropdown === `ctx-${m.value}` && (
                                          <div className={`absolute z-50 top-full left-0 mt-1 min-w-[100px] border rounded shadow-lg py-0.5 ${isDark ? 'bg-gray-800 border-gray-600' : 'bg-white border-gray-200'}`}>
                                            <div
                                              onClick={() => { updateCouncilMember(m.value, 'traceId', undefined); setOpenCtxDropdown(null); }}
                                              className={`flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer ${!cfg.traceId ? 'font-semibold' : ''} ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-700'}`}
                                            >
                                              <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: defaultTraceColor }} />
                                              Default
                                            </div>
                                            {availableTraces.map(t => (
                                              <div
                                                key={t.id}
                                                onClick={() => { updateCouncilMember(m.value, 'traceId', t.id); setOpenCtxDropdown(null); }}
                                                className={`flex items-center gap-1.5 px-2 py-1 text-xs cursor-pointer ${cfg.traceId === t.id ? 'font-semibold' : ''} ${isDark ? 'hover:bg-gray-700 text-gray-300' : 'hover:bg-gray-100 text-gray-700'}`}
                                              >
                                                <span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: t.color }} />
                                                {t.label}
                                              </div>
                                            ))}
                                          </div>
                                        )}
                                      </div>
                                    );
                                  })()}
                                </div>
                              )}
                            </div>
                          );
                        });
                      })()}
                    </div>
                  </div>
                  <div>
                    <label className={`block text-xs font-semibold mb-1 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Chairman Model</label>
                    <select
                      value={selectedNode.data.chairmanModel?.model || (selectedNode.data.councilModels || [])[0]?.model || ''}
                      onChange={(e) => {
                        const existing = (selectedNode.data.councilModels || []).find((c: CouncilMemberConfig) => c.model === e.target.value);
                        handleChange('chairmanModel', existing ? { ...existing } : { model: e.target.value });
                      }}
                      className={`w-full border rounded-md p-1.5 text-xs ${isDark ? 'bg-gray-800 border-gray-600 text-gray-200' : 'border-gray-300'}`}
                    >
                      {(selectedNode.data.councilModels || []).map((c: CouncilMemberConfig) => (
                        <option key={c.model} value={c.model}>{c.model}</option>
                      ))}
                    </select>
                    {(() => {
                      const chairCfg = selectedNode.data.chairmanModel;
                      if (!chairCfg) return null;
                      const isReasoningModel = (v: string) => ['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(v);
                      const supportsWebSearch = (v: string) => v.startsWith('gemini-') || v.startsWith('gpt-5') || v === 'o3' || v === 'gpt-4o';
                      const updateChairman = (field: string, value: any) => {
                        handleChange('chairmanModel', { ...chairCfg, [field]: value });
                      };
                      return (
                        <div className={`flex items-center gap-2 mt-1 px-1 py-0.5 text-xs rounded ${isDark ? 'bg-gray-800/60' : 'bg-gray-100/80'}`}>
                          <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                            T:
                            <input
                              type="number"
                              min={0} max={2} step={0.1}
                              value={chairCfg.temperature ?? selectedNode.data.temperature}
                              disabled={isReasoningModel(chairCfg.model)}
                              onChange={(e) => updateChairman('temperature', parseFloat(e.target.value) || 0)}
                              className={`w-12 border rounded px-1 py-0 text-xs ${isReasoningModel(chairCfg.model) ? 'opacity-50 cursor-not-allowed' : ''} ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
                            />
                          </label>
                          {isReasoningModel(chairCfg.model) && (
                            <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                              Effort:
                              <select
                                value={chairCfg.reasoningEffort ?? selectedNode.data.reasoningEffort ?? 'medium'}
                                onChange={(e) => updateChairman('reasoningEffort', e.target.value)}
                                className={`border rounded px-1 py-0 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'bg-white border-gray-300'}`}
                              >
                                <option value="low">Low</option>
                                <option value="medium">Med</option>
                                <option value="high">High</option>
                              </select>
                            </label>
                          )}
                          {supportsWebSearch(chairCfg.model) && (
                            <label className={`flex items-center gap-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                              <input
                                type="checkbox"
                                checked={chairCfg.enableWebSearch ?? (selectedNode.data.enableGoogleSearch !== false)}
                                onChange={(e) => updateChairman('enableWebSearch', e.target.checked)}
                                className="w-3 h-3 accent-amber-500"
                              />
                              Web
                            </label>
                          )}
                        </div>
                      );
                    })()}
                  </div>
                </div>
              )}
            </div>

            {/* Trace Selector - Single Select */}
            <div className={`p-2 rounded border ${isDark ? 'bg-gray-900 border-gray-700' : 'bg-gray-50 border-gray-200'}`}>
              <div className="flex items-center justify-between mb-2">
                <label className={`block text-xs font-bold uppercase ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                  Select Context
                </label>
                {/* Create Merged Trace Button - only show if 2+ traces */}
                {selectedNode.data.traces && selectedNode.data.traces.length >= 2 && (
                  <button
                    onClick={openMergeModal}
                    className={`text-xs px-2 py-1 rounded flex items-center gap-1 ${
                      isDark 
                        ? 'bg-purple-900 hover:bg-purple-800 text-purple-300' 
                        : 'bg-purple-100 hover:bg-purple-200 text-purple-600'
                    }`}
                  >
                    <GitMerge size={12} />
                    Merge
                  </button>
                )}
              </div>
              
              {/* New Trace option */}
              <div className={`flex items-center gap-2 text-sm p-1 rounded group mb-1 border-b pb-2 ${
                isDark ? 'hover:bg-gray-800 border-gray-700' : 'hover:bg-white border-gray-200'
              }`}>
                <div className="flex items-center gap-2 flex-1">
                  <div className="w-2 h-2 rounded-full bg-gray-400"></div>
                  <span className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Start New Trace</span>
                </div>
                <button
                  onClick={(e) => {
                    e.stopPropagation();
                    openQuickChat(null, true);
                  }}
                  className={`p-1 rounded transition-all ${
                    isDark ? 'hover:bg-blue-900 text-gray-500 hover:text-blue-400' : 'hover:bg-blue-100 text-gray-400 hover:text-blue-600'
                  }`}
                  title="Start New Trace Quick Chat"
                >
                  <MessageCircle size={14} />
                </button>
              </div>
              
              {/* All Available Traces - Incoming + Outgoing that this node originated */}
              {(() => {
                // 1. Incoming traces (context from upstream)
                const incomingTraces = selectedNode.data.traces || [];
                
                // 2. Outgoing traces that this node ORIGINATED (not pass-through, not merged)
                // This includes self-started traces, forked traces, and prepend traces
                const outgoingTraces = (selectedNode.data.outgoingTraces || []) as Trace[];
                const originatedTraces = outgoingTraces.filter(t => {
                  // Exclude merged traces - they have their own display section
                  if (t.id.startsWith('merged-')) return false;
                  
                  // Include if this node is the source (originated here)
                  // OR if the trace ID matches a forked/prepend trace pattern from this node
                  const isOriginated = t.sourceNodeId === selectedNode.id;
                  const isForkedHere = t.id.includes(`fork-${selectedNode.id}`);
                  const isSelfTrace = t.id === `trace-${selectedNode.id}`;
                  return isOriginated || isForkedHere || isSelfTrace;
                });
                
                // Combine and deduplicate by ID 
                // Priority: incoming traces (have full context) > originated outgoing traces
                const allTracesMap = new Map<string, Trace>();
                // Add originated outgoing traces first
                originatedTraces.forEach(t => allTracesMap.set(t.id, t));
                // Then incoming traces (will overwrite if same ID, as they have fuller context)
                incomingTraces.forEach(t => allTracesMap.set(t.id, t));
                const allTraces = Array.from(allTracesMap.values());
                
                if (allTraces.length === 0) return null;
                
                return (
                <div className="space-y-1 max-h-[150px] overflow-y-auto">
                  {allTraces.map((trace: Trace) => {
                    const isActive = selectedNode.data.activeTraceIds?.includes(trace.id);
                    const isComplete = canQuickChat(trace);
                    
                    return (
                      <div 
                        key={trace.id} 
                        onClick={() => handleChange('activeTraceIds', [trace.id])}
                        className={`flex items-start gap-2 text-sm p-1.5 rounded group cursor-pointer transition-all ${
                          isActive
                            ? isDark ? 'bg-blue-900/50 border border-blue-700' : 'bg-blue-50 border border-blue-200'
                            : isDark ? 'hover:bg-gray-800' : 'hover:bg-white'
                        }`}
                      >
                        <input 
                          type="radio" 
                          checked={isActive || false}
                          readOnly
                          className="mt-1"
                        />
                        
                        <div className="flex-1">
                          <div className="flex items-center gap-2">
                            {trace.isMerged ? (
                              // Merged Trace Rendering (for propagated merged traces)
                              <div className="flex -space-x-1 shrink-0">
                                {(trace.mergedColors || [trace.color]).slice(0, 3).map((color, idx) => (
                                  <div 
                                    key={idx}
                                    className="w-2 h-2 rounded-full border-2"
                                    style={{ backgroundColor: color, borderColor: isDark ? '#1f2937' : '#fff' }}
                                  />
                                ))}
                                {(trace.mergedColors?.length || 0) > 3 && (
                                  <div className={`w-2 h-2 rounded-full flex items-center justify-center text-[6px] ${
                                    isDark ? 'bg-gray-700 text-gray-400' : 'bg-gray-200 text-gray-500'
                                  }`}>
                                    +
                                  </div>
                                )}
                              </div>
                            ) : (
                              // Regular Trace Rendering
                              <div className="w-2 h-2 rounded-full" style={{ backgroundColor: trace.color }}></div>
                            )}
                            
                            <span className={`font-mono text-xs ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                              {trace.isMerged ? 'Merged ' : ''}#{trace.id.slice(-4)}
                            </span>
                            {!isComplete && (
                              <span className="text-[9px] text-orange-500">(incomplete)</span>
                            )}
                          </div>
                          <div className={`text-[10px] ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                            {trace.messages.length} msgs
                          </div>
                        </div>
                        
                        {/* Quick Chat Button */}
                        {(() => {
                          const hasDownstream = edges.some(e => 
                            e.source === selectedNode.id && 
                            e.sourceHandle?.startsWith('trace-')
                          );
                          const buttonLabel = hasDownstream ? "Duplicate & Quick Chat" : "Quick Chat";
                          
                          return (
                            <button
                              onClick={(e) => {
                                e.stopPropagation();
                                openQuickChat(trace, false);
                              }}
                              disabled={!isComplete}
                              className={`opacity-0 group-hover:opacity-100 p-1 rounded transition-all ${
                                isComplete 
                                  ? hasDownstream
                                    ? isDark ? 'hover:bg-orange-900 text-gray-500 hover:text-orange-400' : 'hover:bg-orange-100 text-gray-400 hover:text-orange-600'
                                    : isDark ? 'hover:bg-blue-900 text-gray-500 hover:text-blue-400' : 'hover:bg-blue-100 text-gray-400 hover:text-blue-600'
                                  : 'text-gray-500 cursor-not-allowed'
                              }`}
                              title={isComplete ? buttonLabel : "Trace incomplete - all nodes need Q&A"}
                            >
                              <MessageCircle size={14} />
                            </button>
                          );
                        })()}
                      </div>
                    );
                  })}
                </div>
                );
              })()}
              
              {/* Merged Traces - also single selectable */}
              {selectedNode.data.mergedTraces && selectedNode.data.mergedTraces.length > 0 && (
                <div className={`mt-2 pt-2 border-t space-y-1 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
                  <label className={`block text-[10px] font-bold uppercase mb-1 ${isDark ? 'text-purple-400' : 'text-purple-600'}`}>
                    Merged Traces
                  </label>
                  {selectedNode.data.mergedTraces.map((merged: MergedTrace) => {
                    const isActive = selectedNode.data.activeTraceIds?.includes(merged.id);
                    
                    // Check if merged trace is complete
                    const isComplete = merged.sourceTraceIds.every(sourceId => {
                      // Check trace path completeness (upstream empty nodes)
                      const pathComplete = checkTracePathComplete(selectedNode.id, sourceId);
                      if (!pathComplete) return false;
                      
                      // Check message integrity
                      const incomingTraces = selectedNode.data.traces || [];
                      const sourceTrace = incomingTraces.find(t => t.id === sourceId);
                      if (sourceTrace && !isTraceComplete(sourceTrace)) return false;
                      
                      return true;
                    });
                    
                    return (
                      <div 
                        key={merged.id}
                        onClick={() => handleChange('activeTraceIds', [merged.id])}
                        className={`flex items-center gap-2 p-1.5 rounded text-xs cursor-pointer transition-all ${
                          isActive
                            ? isDark ? 'bg-purple-900/50 border border-purple-600' : 'bg-purple-50 border border-purple-300'
                            : isDark ? 'bg-gray-800 hover:bg-gray-700' : 'bg-white border border-gray-200 hover:bg-gray-50'
                        }`}
                      >
                        <input 
                          type="radio" 
                          checked={isActive || false}
                          readOnly
                          className="shrink-0"
                        />
                        
                        {/* Alternating color indicator */}
                        <div className="flex -space-x-1 shrink-0">
                          {merged.colors.slice(0, 3).map((color, idx) => (
                            <div
                              key={idx}
                              className="w-3 h-3 rounded-full border-2"
                              style={{ backgroundColor: color, borderColor: isDark ? '#1f2937' : '#fff' }}
                            />
                          ))}
                          {merged.colors.length > 3 && (
                            <div className={`w-3 h-3 rounded-full flex items-center justify-center text-[8px] ${
                              isDark ? 'bg-gray-700 text-gray-400' : 'bg-gray-200 text-gray-500'
                            }`}>
                              +{merged.colors.length - 3}
                            </div>
                          )}
                        </div>

                        <div className="flex-1 min-w-0">
                          <div className={`flex items-center gap-1 ${isDark ? 'text-gray-300' : 'text-gray-600'}`}>
                            <span className="font-mono truncate">Merged #{merged.id.slice(-6)}</span>
                            {!isComplete && (
                              <span className="text-[9px] text-orange-500 font-sans">(incomplete)</span>
                            )}
                          </div>
                          <div className={`truncate ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                            {merged.strategy} • {merged.messages.length} msgs
                          </div>
                        </div>
                        
                        {/* Quick Chat for Merged Trace */}
                        <button
                          onClick={(e) => {
                            e.stopPropagation();
                            openMergedQuickChat(merged);
                          }}
                          disabled={!isComplete}
                          className={`p-1 rounded shrink-0 ${
                            isComplete 
                              ? isDark ? 'hover:bg-purple-900 text-gray-500 hover:text-purple-400' : 'hover:bg-purple-100 text-gray-400 hover:text-purple-600'
                              : 'text-gray-500 cursor-not-allowed opacity-50'
                          }`}
                          title={isComplete ? "Quick Chat with merged context" : "Trace incomplete"}
                        >
                          <MessageCircle size={12} />
                        </button>
                        
                        <button
                          onClick={(e) => {
                            e.stopPropagation();
                            unfoldMergedTrace(selectedNode.id, merged.id);
                          }}
                          className={`p-1 rounded shrink-0 ${
                            isDark ? 'hover:bg-blue-900 text-gray-500 hover:text-blue-400' : 'hover:bg-blue-50 text-gray-400 hover:text-blue-600'
                          }`}
                          title="Unfold to regular trace"
                        >
                          <Layers size={12} />
                        </button>

                        <button
                          onClick={(e) => {
                            e.stopPropagation();
                            deleteMergedTrace(selectedNode.id, merged.id);
                          }}
                          className={`p-1 rounded shrink-0 ${
                            isDark ? 'hover:bg-red-900 text-gray-500 hover:text-red-400' : 'hover:bg-red-50 text-gray-400 hover:text-red-600'
                          }`}
                          title="Delete merged trace"
                        >
                          <Trash2 size={12} />
                        </button>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>

            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">User Prompt</label>
              <textarea
                value={selectedNode.data.userPrompt}
                onChange={(e) => handleChange('userPrompt', e.target.value)}
                onPaste={(e) => handlePasteImage(
                  e,
                  (id) => {
                    const current = selectedNode.data.attachedFileIds || [];
                    if (!current.includes(id)) {
                      updateNodeData(selectedNode.id, { attachedFileIds: [...current, id] });
                    }
                  },
                  () => `${currentBlueprintPath || 'untitled'}/${selectedNode.id}`,
                )}
                onKeyDown={(e) => {
                  if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    if (selectedNode.data.status !== 'loading' && activeTracesCheck.complete) {
                      handleRun();
                    }
                  }
                  // Shift+Enter allows normal newline
                }}
                className="w-full border border-gray-300 rounded-md p-2 text-sm min-h-[100px]"
                placeholder="Type your message here... (Enter to run, Shift+Enter for newline)"
              />
            </div>

            {/* Warning for incomplete upstream traces */}
            {!activeTracesCheck.complete && (
              <div className={`mb-2 p-2 rounded-md text-xs flex items-center gap-2 ${
                isDark ? 'bg-yellow-900/50 text-yellow-300 border border-yellow-700' : 'bg-yellow-50 text-yellow-700 border border-yellow-200'
              }`}>
                <AlertCircle size={14} className="flex-shrink-0" />
                <span className="flex-1">Upstream node is empty. Complete the context chain before running.</span>
                <button
                  onClick={navigateToEmptyNode}
                  className={`flex-shrink-0 p-1 rounded hover:bg-yellow-600/30 transition-colors`}
                  title="Go to empty node"
                >
                  <Navigation size={14} />
                </button>
              </div>
            )}

            {selectedNode.data.councilMode ? (
              <>
                <button
                  onClick={handleRunCouncil}
                  disabled={selectedNode.data.status === 'loading' || !activeTracesCheck.complete || (selectedNode.data.councilModels || []).length < 2}
                  className={`w-full py-2 px-4 rounded-md flex items-center justify-center gap-2 transition-colors ${
                    selectedNode.data.status === 'loading' || !activeTracesCheck.complete || (selectedNode.data.councilModels || []).length < 2
                      ? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-gray-600 dark:text-gray-400'
                      : 'bg-amber-600 text-white hover:bg-amber-700'
                  }`}
                >
                  {selectedNode.data.status === 'loading' ? <Loader2 className="animate-spin" size={16} /> : <Users size={16} />}
                  {selectedNode.data.status === 'loading' && councilStage ? councilStage : `Run Council (${(selectedNode.data.councilModels || []).length})`}
                </button>
                <div className={`text-center text-[10px] mt-0.5 leading-tight ${isDark ? 'text-gray-600' : 'text-gray-400'}`}>
                  Inspired by <a href="https://github.com/karpathy/llm-council" target="_blank" rel="noopener noreferrer" className={`underline decoration-dotted ${isDark ? 'text-gray-500 hover:text-gray-400' : 'text-gray-500 hover:text-gray-600'}`}>karpathy/llm-council</a>
                </div>
              </>
            ) : (
              <button
                onClick={handleRun}
                disabled={selectedNode.data.status === 'loading' || !activeTracesCheck.complete}
                className={`w-full py-2 px-4 rounded-md flex items-center justify-center gap-2 transition-colors ${
                  selectedNode.data.status === 'loading' || !activeTracesCheck.complete
                    ? 'bg-gray-300 text-gray-500 cursor-not-allowed dark:bg-gray-600 dark:text-gray-400'
                    : 'bg-blue-600 text-white hover:bg-blue-700'
                }`}
              >
                {selectedNode.data.status === 'loading' ? <Loader2 className="animate-spin" size={16} /> : <Play size={16} />}
                Run Node
              </button>
            )}

            <div className="mt-6">
              <div className="flex items-center justify-between mb-2">
                <label className="block text-sm font-medium text-gray-700">Response</label>
                <div className="flex gap-1">
                  {selectedNode.data.response && (
                    <>
                      <button
                        onClick={() => {
                          // Copy the currently-viewed content
                          let textToCopy = selectedNode.data.response || '';
                          if (selectedNode.data.councilData && councilTab === 'responses' && selectedNode.data.councilData.stage1) {
                            const r = selectedNode.data.councilData.stage1[councilResponseTab];
                            if (r) textToCopy = r.response;
                          }
                          navigator.clipboard.writeText(textToCopy);
                          setCopiedResponse(true);
                          setTimeout(() => setCopiedResponse(false), 1500);
                        }}
                        className={`p-1 rounded ${isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200'}`}
                        title="Copy response"
                      >
                        {copiedResponse ? <ClipboardCheck size={14} className="text-green-500" /> : <Copy size={14} />}
                      </button>
                      <button
                        onClick={() => setRawTextMode(!rawTextMode)}
                        className={`p-1 rounded ${rawTextMode
                          ? (isDark ? 'bg-blue-900/50 text-blue-400' : 'bg-blue-100 text-blue-600')
                          : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200')}`}
                        title={rawTextMode ? 'Rendered view' : 'Plain text'}
                      >
                        {rawTextMode ? <Eye size={14} /> : <EyeOff size={14} />}
                      </button>
                      <button
                        onClick={() => setShowSummaryModal(true)}
                        disabled={isSummarizing}
                        className="p-1 hover:bg-gray-200 rounded text-gray-500 hover:text-gray-700 disabled:opacity-50"
                        title="Summarize"
                      >
                        {isSummarizing ? <Loader2 className="animate-spin" size={14} /> : <FileText size={14} />}
                      </button>
                      <button
                        onClick={() => setIsEditing(true)}
                        className="p-1 hover:bg-gray-200 rounded text-gray-500 hover:text-gray-700"
                        title="Edit Response"
                      >
                        <Edit3 size={14} />
                      </button>
                      <button
                        onClick={() => setIsModalOpen(true)}
                        className="p-1 hover:bg-gray-200 rounded text-gray-500 hover:text-gray-700"
                        title="Expand"
                      >
                        <Maximize2 size={14} />
                      </button>
                    </>
                  )}
              </div>
              </div>

              {/* Council tabbed response view */}
              {selectedNode.data.councilMode && selectedNode.data.councilData && (selectedNode.data.councilData.stage1 || selectedNode.data.status === 'loading') ? (
                <div>
                  {/* Council tab bar */}
                  <div className={`flex gap-0.5 mb-2 text-xs border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
                    {(['final', 'responses', 'rankings'] as const).map(tab => (
                      <button
                        key={tab}
                        onClick={() => setCouncilTab(tab)}
                        className={`px-3 py-1.5 rounded-t transition-colors capitalize ${
                          councilTab === tab
                            ? isDark ? 'bg-gray-800 text-amber-300 border-b-2 border-amber-400' : 'bg-white text-amber-700 border-b-2 border-amber-500'
                            : isDark ? 'text-gray-500 hover:text-gray-300' : 'text-gray-400 hover:text-gray-600'
                        }`}
                      >
                        {tab === 'final' ? 'Final Answer' : tab === 'responses' ? 'Individual' : 'Rankings'}
                      </button>
                    ))}
                  </div>

                  {/* Final Answer tab (Stage 3) */}
                  {councilTab === 'final' && (
                    <div>
                      {selectedNode.data.councilData.stage3 ? (
                        <div className={`text-xs mb-1 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>Chairman: {selectedNode.data.councilData.stage3.model}</div>
                      ) : selectedNode.data.status === 'loading' && councilStage.includes('Stage 3') ? (
                        <div className={`text-xs mb-1 flex items-center gap-1 ${isDark ? 'text-amber-400' : 'text-amber-600'}`}><Loader2 className="animate-spin" size={10} /> Synthesizing...</div>
                      ) : null}
                      <div className={`p-3 rounded-md border min-h-[150px] text-sm prose prose-sm max-w-none ${
                        isDark ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' : 'bg-gray-50 border-gray-200 text-gray-900'
                      }`}>
                        {rawTextMode ? (
                          <pre className="whitespace-pre-wrap break-words">{selectedNode.data.councilData.stage3?.response || councilStreamBuffer || ''}</pre>
                        ) : (
                          <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>
                            {preprocessLaTeX(selectedNode.data.councilData.stage3?.response || councilStreamBuffer || '')}
                          </ReactMarkdown>
                        )}
                      </div>
                    </div>
                  )}

                  {/* Individual Responses tab (Stage 1) */}
                  {councilTab === 'responses' && selectedNode.data.councilData.stage1 && (
                    <div>
                      <div className={`flex gap-0.5 mb-2 flex-wrap`}>
                        {selectedNode.data.councilData.stage1.map((r, i) => (
                          <button
                            key={r.model}
                            onClick={() => setCouncilResponseTab(i)}
                            className={`px-2 py-0.5 text-xs rounded transition-colors ${
                              councilResponseTab === i
                                ? isDark ? 'bg-blue-900/50 text-blue-300' : 'bg-blue-100 text-blue-700'
                                : isDark ? 'bg-gray-800 text-gray-400 hover:bg-gray-700' : 'bg-gray-100 text-gray-500 hover:bg-gray-200'
                            }`}
                          >
                            {r.model}
                          </button>
                        ))}
                      </div>
                      {(() => {
                        const r = selectedNode.data.councilData.stage1![councilResponseTab];
                        if (!r) return null;
                        return (
                          <div className={`p-3 rounded-md border min-h-[150px] text-sm prose prose-sm max-w-none ${
                            isDark ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' : 'bg-gray-50 border-gray-200 text-gray-900'
                          }`}>
                            {rawTextMode ? (
                              <pre className="whitespace-pre-wrap break-words">{r.response}</pre>
                            ) : (
                              <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(r.response)}</ReactMarkdown>
                            )}
                          </div>
                        );
                      })()}
                    </div>
                  )}

                  {/* Rankings tab (Stage 2) */}
                  {councilTab === 'rankings' && (
                    <div>
                      {selectedNode.data.councilData.stage2 ? (
                        <div>
                          {/* Aggregate Rankings */}
                          <div className={`mb-3 p-2 rounded border ${isDark ? 'bg-gray-900 border-gray-700' : 'bg-gray-50 border-gray-200'}`}>
                            <div className={`text-xs font-semibold mb-1.5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Aggregate Rankings</div>
                            {selectedNode.data.councilData.stage2.aggregate_rankings.map((r, i) => (
                              <div key={r.model} className={`flex items-center justify-between text-xs py-0.5 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
                                <span className="font-mono">#{i + 1} {r.model}</span>
                                <span className={`${isDark ? 'text-gray-500' : 'text-gray-400'}`}>avg: {r.average_rank} ({r.rankings_count} votes)</span>
                              </div>
                            ))}
                          </div>
                          {/* Individual ranker evaluations */}
                          <div className={`text-xs font-semibold mb-1.5 ${isDark ? 'text-gray-400' : 'text-gray-600'}`}>Individual Evaluations</div>
                          <div className={`flex gap-0.5 mb-2 flex-wrap`}>
                            <button
                              onClick={() => setCouncilRankingTab('aggregate')}
                              className={`px-2 py-0.5 text-xs rounded ${councilRankingTab === 'aggregate' ? (isDark ? 'bg-blue-900/50 text-blue-300' : 'bg-blue-100 text-blue-700') : (isDark ? 'bg-gray-800 text-gray-400' : 'bg-gray-100 text-gray-500')}`}
                            >Summary</button>
                            {selectedNode.data.councilData.stage2.rankings.map((r, i) => (
                              <button
                                key={r.model}
                                onClick={() => setCouncilRankingTab(i)}
                                className={`px-2 py-0.5 text-xs rounded ${councilRankingTab === i ? (isDark ? 'bg-blue-900/50 text-blue-300' : 'bg-blue-100 text-blue-700') : (isDark ? 'bg-gray-800 text-gray-400' : 'bg-gray-100 text-gray-500')}`}
                              >{r.model}</button>
                            ))}
                          </div>
                          {councilRankingTab !== 'aggregate' && (() => {
                            const r = selectedNode.data.councilData.stage2!.rankings[councilRankingTab as number];
                            if (!r) return null;
                            // De-anonymize the ranking text
                            let text = r.ranking;
                            const mapping = selectedNode.data.councilData.stage2!.label_to_model;
                            for (const [label, model] of Object.entries(mapping)) {
                              text = text.replaceAll(label, `${label} [${model}]`);
                            }
                            return (
                              <div className={`p-3 rounded-md border text-sm prose prose-sm max-w-none max-h-64 overflow-y-auto ${
                                isDark ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200' : 'bg-gray-50 border-gray-200 text-gray-900'
                              }`}>
                                <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(text)}</ReactMarkdown>
                              </div>
                            );
                          })()}
                        </div>
                      ) : selectedNode.data.status === 'loading' ? (
                        <div className={`p-3 rounded border min-h-[100px] flex items-center justify-center text-sm ${isDark ? 'bg-gray-900 border-gray-700 text-gray-500' : 'bg-gray-50 border-gray-200 text-gray-400'}`}>
                          <Loader2 className="animate-spin mr-2" size={14} /> Waiting for rankings...
                        </div>
                      ) : (
                        <div className={`p-3 rounded border min-h-[100px] flex items-center justify-center text-sm ${isDark ? 'bg-gray-900 border-gray-700 text-gray-500' : 'bg-gray-50 border-gray-200 text-gray-400'}`}>
                          No ranking data
                        </div>
                      )}
                    </div>
                  )}

                </div>
              ) : isEditing ? (
                <div className="space-y-2">
                  <textarea
                    value={editedResponse}
                    onChange={(e) => setEditedResponse(e.target.value)}
                    className={`w-full border rounded-md p-2 text-sm min-h-[200px] font-mono focus:ring-2 focus:ring-blue-500 ${
                      isDark
                        ? 'bg-gray-800 border-gray-600 text-gray-200 placeholder-gray-500'
                        : 'bg-white border-blue-300 text-gray-900'
                    }`}
                  />
                  <div className="flex gap-2 justify-end">
                    <button
                      onClick={handleCancelEdit}
                      className={`px-3 py-1 text-sm rounded flex items-center gap-1 ${
                        isDark ? 'text-gray-400 hover:bg-gray-800' : 'text-gray-600 hover:bg-gray-100'
                      }`}
                    >
                      <X size={14} /> Cancel
                    </button>
                    <button
                      onClick={handleSaveEdit}
                      className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center gap-1"
                    >
                      <Check size={14} /> Save
                    </button>
                  </div>
                </div>
              ) : rawTextMode ? (
                <pre className={`p-3 rounded-md border min-h-[150px] text-sm whitespace-pre-wrap break-words overflow-auto ${
                  isDark
                    ? 'bg-gray-900 border-gray-700 text-gray-200'
                    : 'bg-gray-50 border-gray-200 text-gray-900'
                }`}>{selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : '')}</pre>
              ) : (
                <div className={`p-3 rounded-md border min-h-[150px] text-sm prose prose-sm max-w-none ${
                  isDark
                    ? 'bg-gray-900 border-gray-700 prose-invert text-gray-200'
                    : 'bg-gray-50 border-gray-200 text-gray-900'
                }`}>
                  <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(selectedNode.data.response || (streamingNodeId === selectedNode.id ? streamBuffer : ''))}</ReactMarkdown>
                </div>
              )}
            </div>
          </div>
        )}

        {activeTab === 'settings' && (
          <div className="space-y-4">
            {/* Attachments Section */}
            {(() => {
              const isGemini = selectedNode.data.model.startsWith('gemini');
              return (
            <div className={`p-3 rounded border ${isDark ? 'bg-gray-800 border-gray-700' : 'bg-gray-50 border-gray-200'}`}>
              <label className={`block text-xs font-bold uppercase tracking-wider mb-2 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                Attached Files
              </label>
              
              {isGemini && (
                <p className={`text-xs mb-2 ${isDark ? 'text-yellow-400' : 'text-yellow-600'}`}>
                  File attachments are not supported for Gemini models.
                </p>
              )}
              
              <div className="flex gap-2 mb-3">
                <button
                  onClick={() => settingsUploadRef.current?.click()}
                  disabled={isGemini || settingsUploading}
                  className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 px-3 rounded text-xs font-medium transition-colors ${
                    isGemini || settingsUploading
                      ? 'opacity-50 cursor-not-allowed bg-gray-400 text-gray-200'
                      : isDark ? 'bg-blue-600 hover:bg-blue-700 text-white' : 'bg-blue-600 hover:bg-blue-700 text-white'
                  }`}
                  title={isGemini ? 'Not supported for Gemini models' : settingsUploading ? 'Uploading...' : 'Upload & Attach'}
                >
                  {settingsUploading ? <Loader2 size={14} className="animate-spin" /> : <Upload size={14} />}
                  {settingsUploading ? 'Uploading...' : 'Upload & Attach'}
                </button>
                <input
                  ref={settingsUploadRef}
                  type="file"
                  className="hidden"
                  onChange={handleUploadAndAttach}
                />
                
                <button
                  onClick={() => {
                    refreshFiles();
                    setShowAttachModal(true);
                  }}
                  disabled={isGemini}
                  className={`flex-1 flex items-center justify-center gap-1.5 py-1.5 px-3 rounded text-xs font-medium border transition-colors ${
                    isGemini
                      ? 'opacity-50 cursor-not-allowed border-gray-400 text-gray-400'
                      : isDark 
                        ? 'border-gray-600 hover:bg-gray-700 text-gray-200' 
                        : 'border-gray-300 hover:bg-gray-100 text-gray-700'
                  }`}
                  title={isGemini ? 'Not supported for Gemini models' : 'Attach Existing File'}
                >
                  <Link size={14} />
                  Attach Existing
                </button>
              </div>

              {(selectedNode.data.attachedFileIds || []).length === 0 ? (
                <p className={`text-xs text-center italic py-2 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                  No files attached.
                </p>
              ) : (
                <div className="space-y-1">
                  {(selectedNode.data.attachedFileIds || []).map(id => {
                    const file = files.find(f => f.id === id);
                    if (!file) return null;
                    const isImg = isImageFile(file.mime);
                    return (
                      <div
                        key={id}
                        className={`group flex items-center justify-between p-2 rounded text-xs ${
                          isDark ? 'bg-gray-700/50' : 'bg-white border border-gray-200'
                        }`}
                      >
                        <div className="flex items-center gap-2 overflow-hidden">
                          {isImg ? (
                            <img src={getImageUrl(id)} alt={file.name} className="w-8 h-8 object-cover rounded" />
                          ) : (
                            <FileText size={14} className={isDark ? 'text-blue-400' : 'text-blue-500'} />
                          )}
                          <span className={`truncate ${isDark ? 'text-gray-200' : 'text-gray-700'}`}>
                            {file.name}
                          </span>
                        </div>
                        <button
                          onClick={() => handleDetach(id)}
                          className={`opacity-0 group-hover:opacity-100 p-1 rounded hover:bg-red-500/20 text-red-500 transition-all`}
                          title="Remove attachment"
                        >
                          <X size={12} />
                        </button>
                      </div>
                    );
                  })}
                </div>
              )}
            </div>
              );
            })()}

            <div>
              <label className={`block text-sm font-medium mb-1 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>Merge Strategy</label>
              <select 
                value={selectedNode.data.mergeStrategy || 'smart'} 
                onChange={(e) => handleChange('mergeStrategy', e.target.value)}
                className={`w-full border rounded-md p-2 text-sm ${
                  isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300 bg-white text-gray-900'
                }`}
              >
                <option value="smart">Smart (Auto-merge roles)</option>
                <option value="raw">Raw (Concatenate)</option>
              </select>
              <p className={`text-xs mt-1 ${isDark ? 'text-gray-500' : 'text-gray-500'}`}>
                Smart merge combines consecutive messages from the same role to avoid API errors.
              </p>
            </div>

            <div>
              <label className={`block text-sm font-medium mb-1 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
                Temperature ({selectedNode.data.temperature})
                {[
                  'gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 
                  'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest',
                  'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'
                ].includes(selectedNode.data.model) && (
                  <span className="text-xs text-orange-500 ml-2">(Locked for Reasoning Model)</span>
                )}
              </label>
              <input 
                type="range" 
                min="0" 
                max="2" 
                step="0.1"
                value={selectedNode.data.temperature}
                onChange={(e) => handleChange('temperature', parseFloat(e.target.value))}
                disabled={[
                  'gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 
                  'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest',
                  'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'
                ].includes(selectedNode.data.model)}
                className="w-full disabled:opacity-50 disabled:cursor-not-allowed"
              />
            </div>

            {/* Reasoning Effort - Only for OpenAI reasoning models (except chat-latest) */}
            {[
              'gpt-5', 'gpt-5-mini', 'gpt-5-nano', 
              'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'
            ].includes(selectedNode.data.model) && (
              <div>
                <label className="block text-sm font-medium text-gray-700 mb-1">
                  Reasoning Effort
                </label>
                <select
                  value={selectedNode.data.reasoningEffort || 'medium'}
                  onChange={(e) => handleChange('reasoningEffort', e.target.value)}
                  className="w-full border border-gray-300 rounded-md p-2 text-sm"
                >
                  <option value="low">Low (Faster, less thorough)</option>
                  <option value="medium">Medium (Balanced)</option>
                  <option value="high">High (Slower, more thorough)</option>
                </select>
                <p className="text-xs text-gray-500 mt-1">
                  Controls how much reasoning the model performs before responding. Higher = more tokens used.
                </p>
              </div>
            )}
            
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">API Key (Optional)</label>
              <input 
                type="password"
                value={selectedNode.data.apiKey || ''}
                onChange={(e) => handleChange('apiKey', e.target.value)}
                className="w-full border border-gray-300 rounded-md p-2 text-sm"
                placeholder="Leave empty to use backend env var"
              />
            </div>
            
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">System Prompt Override</label>
              <textarea 
                value={selectedNode.data.systemPrompt}
                onChange={(e) => handleChange('systemPrompt', e.target.value)}
                className="w-full border border-gray-300 rounded-md p-2 text-sm min-h-[100px] font-mono"
                placeholder="Global system prompt will be used if empty..."
              />
            </div>

            {(selectedNode.data.model.startsWith('gemini') || 
              selectedNode.data.model.startsWith('gpt-5') || 
              ['o3', 'o4-mini', 'gpt-4o'].includes(selectedNode.data.model)) && (
              <div className="flex items-center gap-2 mt-4">
                <input 
                  type="checkbox" 
                  id="web-search"
                  checked={selectedNode.data.enableGoogleSearch !== false} // Default to true
                  onChange={(e) => handleChange('enableGoogleSearch', e.target.checked)}
                />
                <label htmlFor="web-search" className="text-sm font-medium text-gray-700 select-none cursor-pointer">
                  Enable Web Search
                </label>
              </div>
            )}
          </div>
        )}

        {activeTab === 'debug' && (
          <div className="space-y-4">
            {/* Timestamps */}
            <div className="bg-gray-50 p-3 rounded border border-gray-200">
              <label className="block text-xs font-bold text-gray-500 mb-2 uppercase">Timestamps</label>
              <div className="grid grid-cols-2 gap-2 text-xs">
            <div>
                  <span className="text-gray-500">Query Sent:</span>
                  <div className="font-mono text-gray-700">
                    {selectedNode.data.querySentAt 
                      ? new Date(selectedNode.data.querySentAt).toLocaleString() 
                      : '-'}
                  </div>
                </div>
                <div>
                  <span className="text-gray-500">Response Received:</span>
                  <div className="font-mono text-gray-700">
                    {selectedNode.data.responseReceivedAt 
                      ? new Date(selectedNode.data.responseReceivedAt).toLocaleString() 
                      : '-'}
                  </div>
                </div>
              </div>
              {selectedNode.data.querySentAt && selectedNode.data.responseReceivedAt && (
                <div className="mt-2 text-xs text-gray-500">
                  Duration: {((selectedNode.data.responseReceivedAt - selectedNode.data.querySentAt) / 1000).toFixed(2)}s
                </div>
              )}
            </div>
            
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">Active Context (Sent to LLM)</label>
              <pre className="bg-gray-900 text-gray-100 p-2 rounded text-xs overflow-x-auto max-h-[200px]">
                {JSON.stringify(getActiveContext(selectedNode.id), null, 2)}
              </pre>
            </div>
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-1">Node Traces (Incoming)</label>
              <pre className="bg-gray-900 text-gray-100 p-2 rounded text-xs overflow-x-auto max-h-[200px]">
                {JSON.stringify(selectedNode.data.traces, null, 2)}
              </pre>
            </div>
          </div>
        )}
      </div>
      
      {/* Response Modal */}
      {isModalOpen && selectedNode && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setIsModalOpen(false)}>
          <div 
            className={`rounded-lg shadow-2xl w-[80vw] max-w-4xl max-h-[80vh] flex flex-col ${isDark ? 'bg-gray-800 text-gray-100' : 'bg-white text-gray-900'}`}
            onClick={(e) => e.stopPropagation()}
          >
            {/* Modal Header */}
            <div className={`flex items-center justify-between p-4 border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
              <h3 className="font-semibold text-lg">{selectedNode.data.label} - Response</h3>
              <div className="flex gap-2 items-center">
                {!isEditing && (
                  <button
                    onClick={() => setRawTextMode(!rawTextMode)}
                    className={`px-3 py-1 text-sm rounded flex items-center gap-1 ${rawTextMode
                      ? (isDark ? 'bg-blue-900/50 text-blue-400' : 'bg-blue-100 text-blue-600')
                      : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-600 hover:bg-gray-100')}`}
                    title={rawTextMode ? 'Rendered view' : 'Plain text'}
                  >
                    {rawTextMode ? <Eye size={14} /> : <EyeOff size={14} />}
                    {rawTextMode ? 'Rendered' : 'Plain text'}
                  </button>
                )}
                {!isEditing && (
                  <button
                    onClick={() => setIsEditing(true)}
                    className={`px-3 py-1 text-sm rounded flex items-center gap-1 ${isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-600 hover:bg-gray-100'}`}
                  >
                    <Edit3 size={14} /> Edit
                  </button>
                )}
                <button
                  onClick={() => { setIsModalOpen(false); setIsEditing(false); }}
                  className={`p-1 rounded ${isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200'}`}
                >
                  <X size={18} />
                </button>
              </div>
            </div>
            
            {/* Modal Content */}
            <div className="flex-1 overflow-y-auto p-6">
              {isEditing ? (
                <textarea
                  value={editedResponse}
                  onChange={(e) => setEditedResponse(e.target.value)}
                  className="w-full h-full min-h-[400px] border border-gray-300 rounded-md p-3 text-sm font-mono focus:ring-2 focus:ring-blue-500 resize-y"
                />
              ) : rawTextMode ? (
                <pre className={`text-sm whitespace-pre-wrap break-words ${isDark ? 'text-gray-200' : 'text-gray-900'}`}>{selectedNode.data.response || ''}</pre>
              ) : (
                <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
                  <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(selectedNode.data.response || '')}</ReactMarkdown>
                </div>
              )}
            </div>
            
            {/* Modal Footer (only when editing) */}
            {isEditing && (
              <div className="flex justify-end gap-2 p-4 border-t border-gray-200">
                <button
                  onClick={handleCancelEdit}
                  className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1"
                >
                  <X size={14} /> Cancel
                </button>
                <button
                  onClick={() => { handleSaveEdit(); setIsModalOpen(false); }}
                  className="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center gap-1"
                >
                  <Check size={14} /> Save Changes
                </button>
              </div>
            )}
          </div>
        </div>
      )}
      
      {/* Summary Model Selection Modal */}
      {showSummaryModal && (
        <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setShowSummaryModal(false)}>
          <div 
            className="bg-white rounded-lg shadow-2xl w-80 p-4"
            onClick={(e) => e.stopPropagation()}
          >
            <h3 className="font-semibold text-lg mb-4">Summarize Response</h3>
            
            <div className="mb-4">
              <label className="block text-sm font-medium text-gray-700 mb-2">Select Model</label>
              <select
                value={summaryModel}
                onChange={(e) => setSummaryModel(e.target.value)}
                className="w-full border border-gray-300 rounded-md p-2 text-sm"
              >
                <optgroup label="Fast (Recommended)">
                  <option value="gpt-5-nano">gpt-5-nano</option>
                  <option value="gpt-5-mini">gpt-5-mini</option>
                  <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
                  <option value="gemini-2.5-flash">gemini-2.5-flash</option>
                </optgroup>
                <optgroup label="Standard">
                  <option value="gpt-4o">gpt-4o</option>
                  <option value="gpt-5">gpt-5</option>
                </optgroup>
              </select>
            </div>
            
            <div className="flex justify-end gap-2">
              <button
                onClick={() => setShowSummaryModal(false)}
                className="px-3 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded"
              >
                Cancel
              </button>
              <button
                onClick={handleSummarize}
                className="px-3 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 flex items-center gap-1"
              >
                <FileText size={14} /> Summarize
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Merge Traces Modal */}
      {showMergeModal && selectedNode && (
        <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={() => setShowMergeModal(false)}>
          <div 
            className={`rounded-xl shadow-2xl w-[500px] max-h-[80vh] flex flex-col ${
              isDark ? 'bg-gray-800' : 'bg-white'
            }`}
            onClick={(e) => e.stopPropagation()}
          >
            {/* Header */}
            <div className={`flex items-center justify-between p-4 border-b ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
              <div className="flex items-center gap-2">
                <GitMerge size={20} className={isDark ? 'text-purple-400' : 'text-purple-600'} />
                <h3 className={`font-semibold text-lg ${isDark ? 'text-gray-100' : 'text-gray-900'}`}>
                  Create Merged Trace
                </h3>
              </div>
              <button 
                onClick={() => setShowMergeModal(false)} 
                className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`}
              >
                <X size={20} className={isDark ? 'text-gray-400' : 'text-gray-500'} />
              </button>
            </div>
            
            {/* Trace Selection - draggable */}
            <div className={`p-4 flex-1 overflow-y-auto ${isDark ? 'bg-gray-900' : 'bg-gray-50'}`}>
              <p className={`text-xs mb-3 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                Select traces to merge. Drag to reorder for "Trace Order" strategy.
              </p>
              
              <div className="space-y-1">
                {mergeOrder
                  .map(traceId => selectedNode.data.traces?.find((t: Trace) => t.id === traceId))
                  .filter((trace): trace is Trace => trace !== undefined)
                  .map((trace) => {
                    const isSelected = mergeSelectedIds.includes(trace.id);
                    const isDragging = mergeDraggedId === trace.id;
                    const traceColors = trace.isMerged && trace.mergedColors && trace.mergedColors.length > 0
                      ? trace.mergedColors
                      : [trace.color];
                    
                    return (
                      <div 
                        key={trace.id}
                        draggable
                        onDragStart={(e) => handleMergeDragStart(e, trace.id)}
                        onDragOver={(e) => handleMergeDragOver(e, trace.id)}
                        onDragEnd={handleMergeDragEnd}
                        className={`flex items-center gap-3 p-2 rounded cursor-move transition-all ${
                          isDragging ? 'opacity-50' : ''
                        } ${isSelected 
                          ? isDark ? 'bg-purple-900/50 border border-purple-600' : 'bg-purple-50 border border-purple-300'
                          : isDark ? 'bg-gray-800 hover:bg-gray-700' : 'bg-white border border-gray-200 hover:bg-gray-50'
                        }`}
                      >
                        <GripVertical size={16} className={isDark ? 'text-gray-600' : 'text-gray-300'} />
                        
                        <input 
                          type="checkbox" 
                          checked={isSelected}
                          onChange={() => toggleMergeSelection(trace.id)}
                        />
                        
                        <div className="flex -space-x-1">
                          {traceColors.slice(0, 3).map((c, idx) => (
                            <div 
                              key={idx}
                              className="w-3 h-3 rounded-full border-2"
                              style={{ backgroundColor: c, borderColor: isDark ? '#1f2937' : '#fff' }}
                            />
                          ))}
                          {traceColors.length > 3 && (
                            <div className={`w-3 h-3 rounded-full flex items-center justify-center text-[8px] ${
                              isDark ? 'bg-gray-700 text-gray-400' : 'bg-gray-200 text-gray-500'
                            }`}>
                              +{traceColors.length - 3}
                            </div>
                          )}
                        </div>
                        
                        <div className="flex-1">
                          <span className={`font-mono text-sm ${isDark ? 'text-gray-300' : 'text-gray-600'}`}>
                            #{trace.id.slice(-6)}
                          </span>
                          <span className={`ml-2 text-xs ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                            {trace.messages.length} msgs
                          </span>
      </div>
    </div>
  );
                  })}
              </div>
              
              {mergeSelectedIds.length < 2 && (
                <p className={`text-xs mt-3 text-center ${isDark ? 'text-orange-400' : 'text-orange-500'}`}>
                  Select at least 2 traces to merge
                </p>
              )}
            </div>
            
            {/* Settings */}
            <div className={`p-4 border-t ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
              <label className={`block text-xs font-bold mb-2 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
                Merge Strategy
              </label>
              <select
                value={mergeStrategy}
                onChange={(e) => setMergeStrategy(e.target.value as MergeStrategy)}
                className={`w-full border rounded-md p-2 text-sm mb-3 ${
                  isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300'
                }`}
              >
                <option value="query_time">By Query Time</option>
                <option value="response_time">By Response Time</option>
                <option value="trace_order">By Trace Order (drag to reorder)</option>
                <option value="grouped">Grouped (each trace in full)</option>
                <option value="interleaved">Interleaved (true timeline)</option>
                <option value="summary">Summary (LLM compressed)</option>
              </select>
              
              {/* Preview */}
              {mergeSelectedIds.length >= 2 && (
                <>
                  <button
                    onClick={() => setShowMergePreview(!showMergePreview)}
                    className={`w-full text-xs py-1.5 rounded mb-2 ${
                      isDark ? 'bg-gray-700 hover:bg-gray-600 text-gray-300' : 'bg-gray-100 hover:bg-gray-200 text-gray-600'
                    }`}
                  >
                    {showMergePreview ? 'Hide Preview' : 'Show Preview'} ({getMergePreview().length} messages)
                  </button>
                  
                  {showMergePreview && (
                    <div className={`max-h-[100px] overflow-y-auto mb-3 p-2 rounded text-xs ${
                      isDark ? 'bg-gray-700' : 'bg-white border border-gray-200'
                    }`}>
                      {getMergePreview().map((msg, idx) => (
                        <div key={idx} className={`mb-1 ${msg.role === 'user' ? 'text-blue-400' : isDark ? 'text-gray-300' : 'text-gray-600'}`}>
                          <span className="font-bold">{msg.role}:</span> {msg.content.slice(0, 40)}...
                        </div>
                      ))}
                    </div>
                  )}
                </>
              )}
              
              <button
                onClick={handleCreateMergedTrace}
                disabled={mergeSelectedIds.length < 2 || isSummarizingMerge}
                className={`w-full py-2.5 rounded-md text-sm font-medium flex items-center justify-center gap-2 ${
                  mergeSelectedIds.length >= 2
                    ? isDark 
                      ? 'bg-purple-600 hover:bg-purple-500 text-white disabled:bg-purple-900' 
                      : 'bg-purple-600 hover:bg-purple-700 text-white disabled:bg-purple-300'
                    : isDark ? 'bg-gray-700 text-gray-500' : 'bg-gray-200 text-gray-400'
                }`}
              >
                {isSummarizingMerge ? (
                  <>
                    <Loader2 className="animate-spin" size={16} />
                    Summarizing...
                  </>
                ) : (
                  <>
                    <GitMerge size={16} />
                    Create Merged Trace
                  </>
                )}
              </button>
            </div>
          </div>
        </div>
      )}

      {/* Quick Chat Modal */}
      {quickChatOpen && quickChatTrace && (
        <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={() => { onInteract?.(); closeQuickChat(); }}>
          <div 
            className={`rounded-xl shadow-2xl w-[85vw] max-w-4xl h-[85vh] flex flex-col ${
              isDark ? 'bg-gray-800' : 'bg-white'
            }`}
            onClick={(e) => { e.stopPropagation(); onInteract?.(); }}
          >
            {/* Header */}
            <div className={`flex items-center justify-between p-4 border-b ${
              isDark ? 'border-gray-700' : 'border-gray-200'
            }`}>
              <div className="flex items-center gap-3">
                <div className="w-3 h-3 rounded-full" style={{ backgroundColor: quickChatTrace.color }}></div>
                <h3 className={`font-semibold text-lg ${isDark ? 'text-gray-100' : 'text-gray-900'}`}>
                  {quickChatNeedsDuplicate ? 'Duplicate & Quick Chat' : 'Quick Chat'}
                </h3>
                {quickChatNeedsDuplicate && (
                  <span className={`text-xs px-2 py-0.5 rounded-full ${
                    isDark ? 'bg-orange-900/50 text-orange-300' : 'bg-orange-100 text-orange-600'
                  }`}>
                    Will create new branch
                  </span>
                )}
                <span className={`text-xs font-mono ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>#{quickChatTrace.id.slice(-8)}</span>
              </div>
              <div className="flex items-center gap-2">
                {/* Council Toggle */}
                <button
                  onClick={() => {
                    const next = !quickChatCouncilMode;
                    setQuickChatCouncilMode(next);
                    if (next && quickChatCouncilModels.length === 0) {
                      setQuickChatCouncilConfigOpen(true);
                    }
                  }}
                  className={`p-1.5 rounded transition-colors ${quickChatCouncilMode
                    ? (isDark ? 'bg-amber-900/60 text-amber-300' : 'bg-amber-100 text-amber-700')
                    : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200')
                  }`}
                  title={quickChatCouncilMode ? 'Council mode ON' : 'Enable council mode'}
                >
                  <Users size={16} />
                </button>
                {/* Model Selector — hidden in council mode */}
                {!quickChatCouncilMode ? (
                  <select
                    value={quickChatModel}
                    onChange={(e) => setQuickChatModel(e.target.value)}
                    className={`border rounded-md px-3 py-1.5 text-sm ${
                      isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300 text-gray-900'
                    }`}
                  >
                    <optgroup label="Claude">
                      <option value="claude-sonnet-4-5">claude-sonnet-4.5</option>
                      <option value="claude-opus-4">claude-opus-4</option>
                      <option value="claude-opus-4-5">claude-opus-4.5</option>
                      <option value="claude-opus-4-6">claude-opus-4.6</option>
                    </optgroup>
                    <optgroup label="Gemini">
                      <option value="gemini-2.5-flash">gemini-2.5-flash</option>
                      <option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite</option>
                      <option value="gemini-3-pro-preview">gemini-3-pro-preview</option>
                    </optgroup>
                    <optgroup label="OpenAI (Standard)">
                      <option value="gpt-4.1">gpt-4.1</option>
                      <option value="gpt-4o">gpt-4o</option>
                    </optgroup>
                    <optgroup label="OpenAI (Reasoning)">
                      <option value="gpt-5">gpt-5</option>
                      <option value="gpt-5-chat-latest">gpt-5-chat-latest</option>
                      <option value="gpt-5-mini">gpt-5-mini</option>
                      <option value="gpt-5-nano">gpt-5-nano</option>
                      <option value="gpt-5-pro" disabled={!canUsePremiumModels}>gpt-5-pro {!canUsePremiumModels && '🔒'}</option>
                      <option value="gpt-5.1">gpt-5.1</option>
                      <option value="gpt-5.1-chat-latest">gpt-5.1-chat-latest</option>
                      <option value="gpt-5.2">gpt-5.2</option>
                      <option value="gpt-5.2-chat-latest">gpt-5.2-chat-latest</option>
                      <option value="gpt-5.2-pro" disabled={!canUsePremiumModels}>gpt-5.2-pro {!canUsePremiumModels && '🔒'}</option>
                      <option value="o3" disabled={!canUsePremiumModels}>o3 {!canUsePremiumModels && '🔒'}</option>
                    </optgroup>
                  </select>
                ) : (
                  <button
                    onClick={() => setQuickChatCouncilConfigOpen(!quickChatCouncilConfigOpen)}
                    className={`border rounded-md px-3 py-1.5 text-sm flex items-center gap-1.5 ${
                      isDark ? 'bg-gray-700 border-amber-700/50 text-amber-300' : 'border-amber-300 text-amber-700 bg-amber-50'
                    }`}
                  >
                    <Users size={14} />
                    Council ({quickChatCouncilModels.length})
                    <ChevronRight size={12} className={`transition-transform ${quickChatCouncilConfigOpen ? 'rotate-90' : ''}`} />
                  </button>
                )}
                <button
                  onClick={() => setRawTextMode(!rawTextMode)}
                  className={`p-1 rounded ${rawTextMode
                    ? (isDark ? 'bg-blue-900/50 text-blue-400' : 'bg-blue-100 text-blue-600')
                    : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-200')}`}
                  title={rawTextMode ? 'Rendered view' : 'Plain text'}
                >
                  {rawTextMode ? <Eye size={16} /> : <EyeOff size={16} />}
                </button>
                <button onClick={closeQuickChat} className={`p-1 rounded ${isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-200'}`}>
                  <X size={20} className={isDark ? 'text-gray-400' : 'text-gray-500'} />
                </button>
              </div>
            </div>

            {/* Council Config Panel (collapsible) */}
            {quickChatCouncilMode && quickChatCouncilConfigOpen && (
              <div className={`px-4 py-2 border-b ${isDark ? 'border-gray-700 bg-gray-800/50' : 'border-gray-200 bg-amber-50/30'}`}>
                <div className="grid grid-cols-2 gap-x-3 gap-y-0.5 max-h-36 overflow-y-auto text-xs">
                  {[
                    { value: 'claude-sonnet-4-5', label: 'claude-sonnet-4.5' },
                    { value: 'claude-opus-4', label: 'claude-opus-4' },
                    { value: 'claude-opus-4-5', label: 'claude-opus-4.5' },
                    { value: 'claude-opus-4-6', label: 'claude-opus-4.6' },
                    { value: 'gemini-2.5-flash', label: 'gemini-2.5-flash' },
                    { value: 'gemini-2.5-flash-lite', label: 'gemini-2.5-flash-lite' },
                    { value: 'gemini-3-pro-preview', label: 'gemini-3-pro-preview' },
                    { value: 'gpt-4.1', label: 'gpt-4.1' },
                    { value: 'gpt-4o', label: 'gpt-4o' },
                    { value: 'gpt-5', label: 'gpt-5' },
                    { value: 'gpt-5-mini', label: 'gpt-5-mini' },
                    { value: 'gpt-5-nano', label: 'gpt-5-nano' },
                    { value: 'gpt-5.1', label: 'gpt-5.1' },
                    { value: 'gpt-5.2', label: 'gpt-5.2' },
                    { value: 'o3', label: 'o3' },
                  ].map(m => {
                    const sel = quickChatCouncilModels.some(c => c.model === m.value);
                    return (
                      <label key={m.value} className={`flex items-center gap-1 py-0.5 cursor-pointer ${sel
                        ? isDark ? 'text-amber-300' : 'text-amber-700'
                        : isDark ? 'text-gray-400' : 'text-gray-600'
                      }`}>
                        <input type="checkbox" checked={sel} onChange={() => {
                          const next = sel
                            ? quickChatCouncilModels.filter(c => c.model !== m.value)
                            : [...quickChatCouncilModels, { model: m.value }];
                          setQuickChatCouncilModels(next);
                          if (sel && quickChatChairmanModel?.model === m.value && next.length > 0) {
                            setQuickChatChairmanModel({ model: next[0].model });
                          }
                        }} className="w-3 h-3 accent-amber-500" />
                        {m.label}
                      </label>
                    );
                  })}
                </div>
                {quickChatCouncilModels.length > 0 && (
                  <div className={`mt-1.5 pt-1.5 border-t flex items-center gap-2 ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
                    <span className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>Chairman:</span>
                    <select
                      value={quickChatChairmanModel?.model || quickChatCouncilModels[0]?.model || ''}
                      onChange={(e) => setQuickChatChairmanModel({ model: e.target.value })}
                      className={`border rounded px-2 py-0.5 text-xs ${isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300'}`}
                    >
                      {quickChatCouncilModels.map(c => (
                        <option key={c.model} value={c.model}>{c.model}</option>
                      ))}
                    </select>
                  </div>
                )}
              </div>
            )}

            {/* Chat Messages */}
            <div className={`flex-1 overflow-y-auto p-4 space-y-4 ${isDark ? 'bg-gray-900' : 'bg-gray-50'}`}>
              {quickChatMessages.length === 0 ? (
                <div className={`text-center py-8 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                  <MessageCircle size={48} className="mx-auto mb-2 opacity-50" />
                  <p>Start a conversation with this trace's context</p>
                </div>
              ) : (
                quickChatMessages.map((msg, idx) => {
                  // Find files sent with this message
                  const sentFilesForMsg = quickChatSentFiles.find(sf => sf.msgId === msg.id)?.fileIds || [];
                  
                  return (
                  <div
                    key={msg.id || idx}
                    className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
                  >
                    <div className="flex items-start gap-2 max-w-[80%]">
                      {/* Source trace indicator for merged traces */}
                      {msg.sourceTraceColor && msg.role !== 'user' && (
                        <div 
                          className="w-2 h-2 rounded-full mt-3 shrink-0"
                          style={{ backgroundColor: msg.sourceTraceColor }}
                          title={`From trace: ${msg.sourceTraceId?.slice(-6) || 'unknown'}`}
                        />
                      )}
                      
                      <div className={`flex flex-col gap-1 ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
                        {/* Files bubble (shown above text for user messages) */}
                        {msg.role === 'user' && sentFilesForMsg.length > 0 && (
                          <div className="bg-blue-500 text-white rounded-lg px-3 py-2 text-sm w-fit max-w-full">
                            <div className="flex flex-wrap gap-2">
                              {sentFilesForMsg.map(fileId => {
                                const file = files.find(f => f.id === fileId);
                                const isImg = file && isImageFile(file.mime);
                                return isImg ? (
                                  <a key={fileId} href={getImageUrl(fileId)} target="_blank" rel="noopener noreferrer" title={file?.name}>
                                    <img src={getImageUrl(fileId)} alt={file?.name || 'Image'} className="max-w-[200px] max-h-[150px] rounded object-cover" />
                                  </a>
                                ) : (
                                  <div key={fileId} className="flex items-center gap-1 bg-blue-600 rounded px-2 py-1 text-xs">
                                    <FileText size={12} />
                                    <span className="max-w-[120px] truncate">{file?.name || 'File'}</span>
                                  </div>
                                );
                              })}
                            </div>
                          </div>
                        )}
                        
                        <div
                          className={`rounded-lg px-4 py-2 ${
                            msg.role === 'user'
                              ? 'bg-blue-600 text-white'
                              : isDark 
                                ? 'bg-gray-800 border border-gray-700 text-gray-200 shadow-sm' 
                                : 'bg-white border border-gray-200 shadow-sm'
                          }`}
                          style={msg.sourceTraceColor ? { borderLeftColor: msg.sourceTraceColor, borderLeftWidth: '3px' } : undefined}
                        >
                          {/* Source trace label for user messages from merged trace */}
                          {msg.sourceTraceColor && msg.role === 'user' && (
                            <div 
                              className="text-[10px] opacity-70 mb-1 flex items-center gap-1"
                            >
                              <div 
                                className="w-2 h-2 rounded-full"
                                style={{ backgroundColor: msg.sourceTraceColor }}
                              />
                              <span>from trace #{msg.sourceTraceId?.slice(-4)}</span>
                            </div>
                          )}
                          
                          {msg.role === 'user' ? (
                            <p className="whitespace-pre-wrap">{msg.content}</p>
                          ) : rawTextMode ? (
                            <pre className="text-sm whitespace-pre-wrap break-words">{msg.content}</pre>
                          ) : (
                            <div className={`prose prose-sm max-w-none ${isDark ? 'prose-invert' : ''}`}>
                              <ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]} rehypePlugins={[rehypeKatex]}>{preprocessLaTeX(msg.content)}</ReactMarkdown>
                            </div>
                          )}
                        </div>
                      </div>
                      
                      {/* Source trace indicator for user messages (on the right side) */}
                      {msg.sourceTraceColor && msg.role === 'user' && (
                        <div 
                          className="w-2 h-2 rounded-full mt-3 shrink-0"
                          style={{ backgroundColor: msg.sourceTraceColor }}
                          title={`From trace: ${msg.sourceTraceId?.slice(-6) || 'unknown'}`}
                        />
                      )}
                    </div>
                  </div>
                )})
              )}
              {quickChatLoading && (
                <div className="flex justify-start">
                  <div className={`rounded-lg px-4 py-3 shadow-sm flex items-center gap-2 ${
                    isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white border border-gray-200'
                  }`}>
                    <Loader2 className={`animate-spin ${quickChatCouncilMode ? 'text-amber-500' : 'text-blue-500'}`} size={20} />
                    {quickChatCouncilStage && (
                      <span className={`text-xs ${isDark ? 'text-amber-400' : 'text-amber-600'}`}>{quickChatCouncilStage}</span>
                    )}
                  </div>
                </div>
              )}
              <div ref={quickChatEndRef} />
            </div>

            {/* Settings Row */}
            <div className={`px-4 py-2 border-t flex items-center gap-4 text-xs ${
              isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-100 bg-white'
            }`}>
              {quickChatCouncilMode ? (
                <span className={`flex items-center gap-1.5 ${isDark ? 'text-amber-400' : 'text-amber-600'}`}>
                  <Users size={12} />
                  {quickChatCouncilModels.length} models &middot; Chairman: {quickChatChairmanModel?.model || '—'}
                </span>
              ) : (
                <>
                  {/* Temperature (hide for reasoning models) */}
                  {!['gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', 'gpt-5.2', 'gpt-5.2-chat-latest', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && (
                    <div className="flex items-center gap-2">
                      <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Temp:</span>
                      <input
                        type="range"
                        min="0"
                        max="2"
                        step="0.1"
                        value={quickChatTemp}
                        onChange={(e) => setQuickChatTemp(parseFloat(e.target.value))}
                        className="w-20"
                      />
                      <span className={`w-8 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>{quickChatTemp}</span>
                    </div>
                  )}

                  {/* Reasoning Effort (only for reasoning models, except chat-latest) */}
                  {['gpt-5', 'gpt-5-mini', 'gpt-5-nano', 'gpt-5-pro', 'gpt-5.1', 'gpt-5.2', 'gpt-5.2-pro', 'o3'].includes(quickChatModel) && (
                    <div className="flex items-center gap-2">
                      <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Effort:</span>
                      <select
                        value={quickChatEffort}
                        onChange={(e) => setQuickChatEffort(e.target.value as 'low' | 'medium' | 'high')}
                        className={`border rounded px-2 py-0.5 text-xs ${
                          isDark ? 'bg-gray-700 border-gray-600 text-gray-200' : 'border-gray-300'
                        }`}
                      >
                        <option value="low">Low</option>
                        <option value="medium">Medium</option>
                        <option value="high">High</option>
                      </select>
                    </div>
                  )}

                  {/* Web Search */}
                  {(quickChatModel.startsWith('gemini') || quickChatModel.startsWith('gpt-5') || ['o3', 'gpt-4o'].includes(quickChatModel)) && (
                    <label className="flex items-center gap-1 cursor-pointer">
                      <input
                        type="checkbox"
                        checked={quickChatWebSearch}
                        onChange={(e) => setQuickChatWebSearch(e.target.checked)}
                        className="form-checkbox h-3 w-3"
                      />
                      <span className={isDark ? 'text-gray-400' : 'text-gray-500'}>Web Search</span>
                    </label>
                  )}
                </>
              )}

              {/* File Attachment Buttons - Hidden for Gemini */}
              {!quickChatModel.startsWith('gemini') && !quickChatCouncilMode && (
              <div className="flex items-center gap-1 ml-auto">
                <button
                  onClick={() => quickChatUploadRef.current?.click()}
                  disabled={quickChatUploading}
                  className={`px-2 py-0.5 rounded text-xs flex items-center gap-1 ${
                    quickChatUploading
                      ? 'opacity-50 cursor-not-allowed'
                      : ''
                  } ${
                    isDark
                      ? 'bg-gray-700 hover:bg-gray-600 text-gray-300'
                      : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
                  }`}
                  title={quickChatUploading ? "Uploading..." : "Upload & Attach"}
                >
                  {quickChatUploading ? <Loader2 size={12} className="animate-spin" /> : <Upload size={12} />}
                  {quickChatUploading ? 'Uploading...' : 'Upload'}
                </button>
                <button
                  onClick={() => {
                    refreshFiles();
                    setShowQuickChatAttachModal(true);
                  }}
                  className={`px-2 py-0.5 rounded text-xs flex items-center gap-1 ${
                    isDark
                      ? 'bg-gray-700 hover:bg-gray-600 text-gray-300'
                      : 'bg-gray-100 hover:bg-gray-200 text-gray-700'
                  }`}
                  title="Attach Existing File"
                >
                  <Link size={12} />
                  Attach
                </button>
                <input
                  ref={quickChatUploadRef}
                  type="file"
                  className="hidden"
                  onChange={handleQuickChatUpload}
                />
              </div>
              )}
            </div>

            {/* Input Area */}
            <div className={`p-4 border-t ${isDark ? 'border-gray-700 bg-gray-800' : 'border-gray-200 bg-white'}`}>
              {/* Attached Files Preview */}
              {quickChatAttachedFiles.length > 0 && (
                <div className={`mb-2 p-2 rounded-lg ${isDark ? 'bg-gray-700' : 'bg-gray-100'}`}>
                  <div className="flex flex-wrap gap-2">
                    {quickChatAttachedFiles.map(fileId => {
                      const file = files.find(f => f.id === fileId);
                      if (!file) return null;
                      const isImg = isImageFile(file.mime);
                      return (
                        <div
                          key={fileId}
                          className={`flex items-center gap-1 px-2 py-1 rounded text-xs ${
                            isDark ? 'bg-gray-600 text-gray-200' : 'bg-white text-gray-700 border border-gray-300'
                          }`}
                        >
                          {isImg ? (
                            <img src={getImageUrl(fileId)} alt={file.name} className="w-8 h-8 object-cover rounded" />
                          ) : (
                            <FileText size={12} />
                          )}
                          <span className="max-w-[120px] truncate">{file.name}</span>
                          <button
                            onClick={() => handleQuickChatDetach(fileId)}
                            className={`ml-1 p-0.5 rounded hover:bg-red-500 hover:text-white ${isDark ? 'text-gray-400' : 'text-gray-500'}`}
                          >
                            <X size={10} />
                          </button>
                        </div>
                      );
                    })}
                  </div>
                </div>
              )}
              <div className="flex gap-2">
                <textarea
                  ref={quickChatInputRef}
                  value={quickChatInput}
                  onChange={(e) => setQuickChatInput(e.target.value)}
                  onPaste={(e) => handlePasteImage(e, (id) => setQuickChatAttachedFiles(prev => [...prev, id]), getQuickChatScope)}
                  onKeyDown={(e) => {
                    if (e.key === 'Enter' && !e.shiftKey) {
                      e.preventDefault();
                      // Only send if not loading and has content (text or files)
                      if (!quickChatLoading && (quickChatInput.trim() || quickChatAttachedFiles.length > 0)) {
                        handleQuickChatSend();
                      }
                    }
                  }}
                  placeholder={quickChatLoading 
                    ? "Waiting for response... (you can type here)" 
                    : quickChatAttachedFiles.length > 0
                      ? "Add a message (optional) or just send the files..."
                      : "Type your message... (Enter to send, Shift+Enter for new line)"
                  }
                  className={`flex-1 border rounded-lg px-4 py-3 text-sm resize-y min-h-[50px] max-h-[150px] focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${
                    isDark ? 'bg-gray-700 border-gray-600 text-gray-200 placeholder-gray-400' : 'border-gray-300'
                  }`}
                  autoFocus
                />
                <button
                  onClick={handleQuickChatSend}
                  disabled={(!quickChatInput.trim() && quickChatAttachedFiles.length === 0) || quickChatLoading || (quickChatCouncilMode && quickChatCouncilModels.length < 2)}
                  className={`px-4 py-2 text-white rounded-lg disabled:cursor-not-allowed flex items-center gap-2 ${
                    quickChatCouncilMode
                      ? 'bg-amber-600 hover:bg-amber-700 disabled:bg-amber-300'
                      : 'bg-blue-600 hover:bg-blue-700 disabled:bg-blue-300'
                  }`}
                >
                  {quickChatLoading ? <Loader2 className="animate-spin" size={18} /> : <Send size={18} />}
                </button>
              </div>
              <p className={`text-[10px] mt-2 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                Each message creates a new node on the canvas, automatically connected to this trace.
              </p>
            </div>
          </div>
        </div>
      )}
      {/* Attach File Modal */}
      {showAttachModal && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
          <div 
            className={`w-full max-w-md rounded-lg shadow-xl flex flex-col max-h-[80vh] ${
              isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white'
            }`}
          >
            <div className={`p-4 border-b flex justify-between items-center ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
              <h3 className={`font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>Attach File</h3>
              <button 
                onClick={() => setShowAttachModal(false)}
                className={`p-1 rounded ${isDark ? 'hover:bg-gray-700 text-gray-400' : 'hover:bg-gray-100 text-gray-500'}`}
              >
                <X size={20} />
              </button>
            </div>
            
            <div className="p-4 border-b border-gray-200 dark:border-gray-700">
              <div className="relative">
                <Search size={16} className={`absolute left-3 top-1/2 -translate-y-1/2 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
                <input
                  type="text"
                  placeholder="Search files..."
                  value={attachSearch}
                  onChange={(e) => setAttachSearch(e.target.value)}
                  className={`w-full pl-9 pr-3 py-2 rounded-md text-sm border outline-none focus:ring-2 focus:ring-blue-500 ${
                    isDark 
                      ? 'bg-gray-900 border-gray-600 text-white placeholder-gray-500' 
                      : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
                  }`}
                  autoFocus
                />
              </div>
            </div>

            <div className="flex-1 overflow-y-auto p-2">
              {filteredFilesToAttach.length === 0 ? (
                <div className={`text-center py-8 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                  <FileText size={48} className="mx-auto mb-2 opacity-50" />
                  <p>No matching files found.</p>
                </div>
              ) : (
                <div className="space-y-1">
                  {filteredFilesToAttach.map(file => {
                    const isAttached = (selectedNode?.data.attachedFileIds || []).includes(file.id);
                    return (
                      <button
                        key={file.id}
                        onClick={() => handleAttach(file.id)}
                        disabled={isAttached}
                        className={`w-full flex items-center justify-between p-3 rounded text-left transition-colors ${
                          isAttached
                            ? isDark ? 'opacity-50 cursor-not-allowed bg-gray-700/50' : 'opacity-50 cursor-not-allowed bg-gray-100'
                            : isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-50'
                        }`}
                      >
                        <div className="flex items-center gap-3 overflow-hidden">
                          <div className={`p-2 rounded ${isDark ? 'bg-gray-700' : 'bg-gray-100'}`}>
                            <FileText size={18} className={isDark ? 'text-blue-400' : 'text-blue-600'} />
                          </div>
                          <div className="min-w-0">
                            <div className={`font-medium truncate ${isDark ? 'text-gray-200' : 'text-gray-700'}`}>
                              {file.name}
                            </div>
                            <div className={`text-xs truncate ${isDark ? 'text-gray-500' : 'text-gray-500'}`}>
                              {(file.size / 1024).toFixed(1)} KB • {new Date(file.created_at * 1000).toLocaleDateString()}
                              {file.provider && ` • ${file.provider}`}
                            </div>
                          </div>
                        </div>
                        {isAttached && (
                          <Check size={16} className="text-green-500 flex-shrink-0" />
                        )}
                      </button>
                    );
                  })}
                </div>
              )}
            </div>
            
            <div className={`p-4 border-t text-xs text-center ${isDark ? 'border-gray-700 text-gray-500' : 'border-gray-200 text-gray-500'}`}>
              Showing {filteredFilesToAttach.length} file{filteredFilesToAttach.length !== 1 ? 's' : ''}
            </div>
          </div>
        </div>
      )}
      
      {/* Quick Chat Attach File Modal */}
      {showQuickChatAttachModal && (
        <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
          <div 
            className={`w-full max-w-md rounded-lg shadow-xl flex flex-col max-h-[80vh] ${
              isDark ? 'bg-gray-800 border border-gray-700' : 'bg-white'
            }`}
          >
            <div className={`p-4 border-b flex justify-between items-center ${isDark ? 'border-gray-700' : 'border-gray-200'}`}>
              <h3 className={`font-semibold ${isDark ? 'text-white' : 'text-gray-900'}`}>Attach File to Quick Chat</h3>
              <button 
                onClick={() => setShowQuickChatAttachModal(false)}
                className={`p-1 rounded ${isDark ? 'hover:bg-gray-700 text-gray-400' : 'hover:bg-gray-100 text-gray-500'}`}
              >
                <X size={20} />
              </button>
            </div>
            
            <div className="p-4 border-b border-gray-200 dark:border-gray-700">
              <div className="relative">
                <Search size={16} className={`absolute left-3 top-1/2 -translate-y-1/2 ${isDark ? 'text-gray-500' : 'text-gray-400'}`} />
                <input
                  type="text"
                  placeholder="Search files..."
                  value={quickChatAttachSearch}
                  onChange={(e) => setQuickChatAttachSearch(e.target.value)}
                  className={`w-full pl-9 pr-3 py-2 rounded-md text-sm border outline-none focus:ring-2 focus:ring-blue-500 ${
                    isDark 
                      ? 'bg-gray-900 border-gray-600 text-white placeholder-gray-500' 
                      : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
                  }`}
                  autoFocus
                />
              </div>
            </div>

            <div className="flex-1 overflow-y-auto p-2">
              {filteredQuickChatFiles.length === 0 ? (
                <div className={`text-center py-8 ${isDark ? 'text-gray-500' : 'text-gray-400'}`}>
                  <FileText size={48} className="mx-auto mb-2 opacity-50" />
                  <p>No matching files found.</p>
                </div>
              ) : (
                <div className="space-y-1">
                  {filteredQuickChatFiles.map(file => {
                    const isAttached = quickChatAttachedFiles.includes(file.id);
                    return (
                      <button
                        key={file.id}
                        onClick={() => handleQuickChatAttach(file.id)}
                        disabled={isAttached}
                        className={`w-full flex items-center justify-between p-3 rounded text-left transition-colors ${
                          isAttached
                            ? isDark ? 'opacity-50 cursor-not-allowed bg-gray-700/50' : 'opacity-50 cursor-not-allowed bg-gray-100'
                            : isDark ? 'hover:bg-gray-700' : 'hover:bg-gray-50'
                        }`}
                      >
                        <div className="flex items-center gap-3 overflow-hidden">
                          <div className={`p-2 rounded ${isDark ? 'bg-gray-700' : 'bg-gray-100'}`}>
                            <FileText size={18} className={isDark ? 'text-blue-400' : 'text-blue-600'} />
                          </div>
                          <div className="min-w-0">
                            <div className={`font-medium truncate ${isDark ? 'text-gray-200' : 'text-gray-700'}`}>
                              {file.name}
                            </div>
                            <div className={`text-xs truncate ${isDark ? 'text-gray-500' : 'text-gray-500'}`}>
                              {(file.size / 1024).toFixed(1)} KB • {new Date(file.created_at * 1000).toLocaleDateString()}
                            </div>
                          </div>
                        </div>
                        {isAttached && (
                          <Check size={16} className="text-green-500 flex-shrink-0" />
                        )}
                      </button>
                    );
                  })}
                </div>
              )}
            </div>
            
            <div className={`p-4 border-t text-xs text-center ${isDark ? 'border-gray-700 text-gray-500' : 'border-gray-200 text-gray-500'}`}>
              Showing {filteredQuickChatFiles.length} file{filteredQuickChatFiles.length !== 1 ? 's' : ''}
            </div>
          </div>
        </div>
      )}
    </div>
  );
};

export default Sidebar;

