diff options
Diffstat (limited to 'frontend/src/components')
| -rw-r--r-- | frontend/src/components/LeftSidebar.tsx | 134 | ||||
| -rw-r--r-- | frontend/src/components/Sidebar.tsx | 419 | ||||
| -rw-r--r-- | frontend/src/components/nodes/LLMNode.tsx | 22 |
3 files changed, 547 insertions, 28 deletions
diff --git a/frontend/src/components/LeftSidebar.tsx b/frontend/src/components/LeftSidebar.tsx new file mode 100644 index 0000000..fa8b471 --- /dev/null +++ b/frontend/src/components/LeftSidebar.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { Folder, FileText, Archive, ChevronLeft, ChevronRight, Trash2, MessageSquare } from 'lucide-react'; +import useFlowStore from '../store/flowStore'; + +interface LeftSidebarProps { + isOpen: boolean; + onToggle: () => void; +} + +const LeftSidebar: React.FC<LeftSidebarProps> = ({ isOpen, onToggle }) => { + const [activeTab, setActiveTab] = useState<'project' | 'files' | 'archive'>('project'); + const { archivedNodes, removeFromArchive, createNodeFromArchive } = useFlowStore(); + + const handleDragStart = (e: React.DragEvent, archiveId: string) => { + e.dataTransfer.setData('archiveId', archiveId); + e.dataTransfer.effectAllowed = 'copy'; + }; + + if (!isOpen) { + return ( + <div className="border-r border-gray-200 h-screen bg-white flex flex-col items-center py-4 w-12 z-10 transition-all duration-300"> + <button + onClick={onToggle} + className="p-2 hover:bg-gray-100 rounded mb-4" + title="Expand" + > + <ChevronRight size={20} className="text-gray-500" /> + </button> + {/* Icons when collapsed */} + <div className="flex flex-col gap-4"> + <Folder size={20} className={activeTab === 'project' ? "text-blue-500" : "text-gray-400"} /> + <FileText size={20} className={activeTab === 'files' ? "text-blue-500" : "text-gray-400"} /> + <Archive size={20} className={activeTab === 'archive' ? "text-blue-500" : "text-gray-400"} /> + </div> + </div> + ); + } + + return ( + <div className="w-64 border-r border-gray-200 h-screen flex flex-col bg-white shadow-xl z-10 transition-all duration-300"> + {/* Header */} + <div className="p-3 border-b border-gray-200 flex justify-between items-center bg-gray-50"> + <h2 className="font-bold text-sm text-gray-700 uppercase">Workspace</h2> + <button + onClick={onToggle} + className="p-1 hover:bg-gray-200 rounded" + > + <ChevronLeft size={16} className="text-gray-500" /> + </button> + </div> + + {/* Tabs */} + <div className="flex border-b border-gray-200"> + <button + onClick={() => setActiveTab('project')} + className={`flex-1 p-3 text-xs flex justify-center items-center gap-2 ${activeTab === 'project' ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-600 hover:bg-gray-50'}`} + > + <Folder size={14} /> Project + </button> + <button + onClick={() => setActiveTab('files')} + className={`flex-1 p-3 text-xs flex justify-center items-center gap-2 ${activeTab === 'files' ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-600 hover:bg-gray-50'}`} + > + <FileText size={14} /> Files + </button> + <button + onClick={() => setActiveTab('archive')} + className={`flex-1 p-3 text-xs flex justify-center items-center gap-2 ${activeTab === 'archive' ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-600 hover:bg-gray-50'}`} + > + <Archive size={14} /> Archive + </button> + </div> + + {/* Content Area */} + <div className="flex-1 overflow-y-auto p-4 text-sm text-gray-500"> + {activeTab === 'project' && ( + <div className="flex flex-col items-center justify-center h-full opacity-50"> + <Folder size={48} className="mb-2" /> + <p>Project settings coming soon</p> + </div> + )} + {activeTab === 'files' && ( + <div className="flex flex-col items-center justify-center h-full opacity-50"> + <FileText size={48} className="mb-2" /> + <p>File manager coming soon</p> + </div> + )} + {activeTab === 'archive' && ( + <div className="space-y-2"> + {archivedNodes.length === 0 ? ( + <div className="flex flex-col items-center justify-center h-40 opacity-50"> + <Archive size={32} className="mb-2" /> + <p className="text-xs text-center"> + No archived nodes.<br/> + Right-click a node → "Add to Archive" + </p> + </div> + ) : ( + <> + <p className="text-xs text-gray-400 mb-2">Drag to canvas to create a copy</p> + {archivedNodes.map((archived) => ( + <div + key={archived.id} + draggable + onDragStart={(e) => handleDragStart(e, archived.id)} + className="p-2 bg-gray-50 border border-gray-200 rounded-md cursor-grab hover:bg-gray-100 hover:border-gray-300 transition-colors group" + > + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <MessageSquare size={14} className="text-gray-500" /> + <span className="text-sm font-medium truncate max-w-[140px]">{archived.label}</span> + </div> + <button + onClick={() => removeFromArchive(archived.id)} + className="opacity-0 group-hover:opacity-100 p-1 hover:bg-red-100 rounded text-gray-400 hover:text-red-500 transition-all" + title="Remove from archive" + > + <Trash2 size={12} /> + </button> + </div> + <div className="text-[10px] text-gray-400 mt-1">{archived.model}</div> + </div> + ))} + </> + )} + </div> + )} + </div> + </div> + ); +}; + +export default LeftSidebar; + diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index f62f3cb..165028c 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -2,24 +2,75 @@ import React, { useState, useEffect } from 'react'; import useFlowStore from '../store/flowStore'; import type { NodeData } from '../store/flowStore'; import ReactMarkdown from 'react-markdown'; -import { Play, Settings, Info, Save } from 'lucide-react'; +import { Play, Settings, Info, Save, ChevronLeft, ChevronRight, Maximize2, Edit3, X, Check, FileText } from 'lucide-react'; -const Sidebar = () => { +interface SidebarProps { + isOpen: boolean; + onToggle: () => void; +} + +const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle }) => { const { nodes, selectedNodeId, updateNodeData, getActiveContext } = useFlowStore(); const [activeTab, setActiveTab] = useState<'interact' | 'settings' | 'debug'>('interact'); const [streamBuffer, setStreamBuffer] = useState(''); + + // Response Modal & Edit states + const [isModalOpen, setIsModalOpen] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editedResponse, setEditedResponse] = useState(''); + + // Summary states + const [showSummaryModal, setShowSummaryModal] = useState(false); + const [summaryModel, setSummaryModel] = useState('gpt-5-nano'); + const [isSummarizing, setIsSummarizing] = useState(false); const selectedNode = nodes.find((n) => n.id === selectedNodeId); - // Reset stream buffer when node changes + // Reset stream buffer and modal states when node changes useEffect(() => { setStreamBuffer(''); + setIsModalOpen(false); + setIsEditing(false); }, [selectedNodeId]); + + // Sync editedResponse when entering edit mode + useEffect(() => { + if (isEditing && selectedNode) { + setEditedResponse(selectedNode.data.response || ''); + } + }, [isEditing, selectedNode?.data.response]); + + if (!isOpen) { + return ( + <div className="border-l border-gray-200 h-screen bg-white flex flex-col items-center py-4 w-12 z-10 transition-all duration-300"> + <button + onClick={onToggle} + className="p-2 hover:bg-gray-100 rounded mb-4" + title="Expand" + > + <ChevronLeft size={20} className="text-gray-500" /> + </button> + {selectedNode && ( + <div className="writing-vertical text-xs font-bold text-gray-500 uppercase tracking-widest mt-4" style={{ writingMode: 'vertical-rl' }}> + {selectedNode.data.label} + </div> + )} + </div> + ); + } if (!selectedNode) { return ( - <div className="w-96 border-l border-gray-200 h-screen p-4 bg-gray-50 text-gray-500 text-center flex flex-col justify-center"> - <p>Select a node to edit</p> + <div className="w-96 border-l border-gray-200 h-screen flex flex-col bg-white shadow-xl z-10 transition-all duration-300"> + <div className="p-3 border-b border-gray-200 flex justify-between items-center bg-gray-50"> + <span className="text-sm font-medium text-gray-500">Details</span> + <button onClick={onToggle} className="p-1 hover:bg-gray-200 rounded"> + <ChevronRight size={16} className="text-gray-500" /> + </button> + </div> + <div className="flex-1 p-4 bg-gray-50 text-gray-500 text-center flex flex-col justify-center"> + <p>Select a node to edit</p> + </div> </div> ); } @@ -43,11 +94,13 @@ const Sidebar = () => { user_prompt: selectedNode.data.userPrompt, merge_strategy: selectedNode.data.mergeStrategy || 'smart', config: { - provider: selectedNode.data.model.includes('gpt') ? 'openai' : 'google', + provider: 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, // Default true + reasoning_effort: selectedNode.data.reasoningEffort || 'medium', // For reasoning models } }) }); @@ -85,6 +138,10 @@ const Sidebar = () => { response: fullResponse, messages: [...context, newUserMsg, newAssistantMsg] as any }); + + // Auto-generate title using gpt-5-nano (async, non-blocking) + // Always regenerate title after each query + generateTitle(selectedNode.id, selectedNode.data.userPrompt, fullResponse); } catch (error) { console.error(error); @@ -95,17 +152,85 @@ const Sidebar = () => { 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('http://localhost:8000/api/summarize', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + 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('http://localhost:8000/api/generate_title', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + 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 + } + }; return ( - <div className="w-96 border-l border-gray-200 h-screen flex flex-col bg-white shadow-xl z-10"> + <div className="w-96 border-l border-gray-200 h-screen flex flex-col bg-white shadow-xl z-10 transition-all duration-300"> {/* Header */} - <div className="p-4 border-b border-gray-200 bg-gray-50"> - <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" - /> + <div className="p-4 border-b border-gray-200 bg-gray-50 flex flex-col gap-2"> + <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" + /> + <button onClick={onToggle} className="p-1 hover:bg-gray-200 rounded shrink-0"> + <ChevronRight size={16} className="text-gray-500" /> + </button> + </div> <div className="flex items-center justify-between mt-1"> <div className="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded uppercase"> {selectedNode.data.status} @@ -146,13 +271,41 @@ const Sidebar = () => { <label className="block text-sm font-medium text-gray-700 mb-1">Model</label> <select value={selectedNode.data.model} - onChange={(e) => handleChange('model', e.target.value)} + onChange={(e) => { + const newModel = e.target.value; + // Auto-set temperature to 1 for reasoning models + 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', 'o3' + ]; + const isReasoning = reasoningModels.includes(newModel); + + if (isReasoning) { + handleChange('temperature', 1); + } + handleChange('model', newModel); + }} className="w-full border border-gray-300 rounded-md p-2 text-sm" > - <option value="gpt-4o">GPT-4o</option> - <option value="gpt-4o-mini">GPT-4o Mini</option> - <option value="gemini-1.5-pro">Gemini 1.5 Pro</option> - <option value="gemini-1.5-flash">Gemini 1.5 Flash</option> + <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">gpt-5-pro</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="o3">o3</option> + </optgroup> </select> </div> @@ -216,10 +369,65 @@ const Sidebar = () => { </button> <div className="mt-6"> - <label className="block text-sm font-medium text-gray-700 mb-2">Response</label> - <div className="bg-gray-50 p-3 rounded-md border border-gray-200 min-h-[150px] text-sm prose prose-sm max-w-none"> - <ReactMarkdown>{selectedNode.data.response || streamBuffer}</ReactMarkdown> + <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={() => 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> + + {isEditing ? ( + <div className="space-y-2"> + <textarea + value={editedResponse} + onChange={(e) => setEditedResponse(e.target.value)} + className="w-full border border-blue-300 rounded-md p-2 text-sm min-h-[200px] font-mono focus:ring-2 focus:ring-blue-500" + /> + <div className="flex gap-2 justify-end"> + <button + onClick={handleCancelEdit} + className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1" + > + <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> + ) : ( + <div className="bg-gray-50 p-3 rounded-md border border-gray-200 min-h-[150px] text-sm prose prose-sm max-w-none"> + <ReactMarkdown>{selectedNode.data.response || streamBuffer}</ReactMarkdown> + </div> + )} </div> </div> )} @@ -242,7 +450,15 @@ const Sidebar = () => { </div> <div> - <label className="block text-sm font-medium text-gray-700 mb-1">Temperature ({selectedNode.data.temperature})</label> + <label className="block text-sm font-medium text-gray-700 mb-1"> + 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', '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" @@ -250,9 +466,37 @@ const Sidebar = () => { step="0.1" value={selectedNode.data.temperature} onChange={(e) => handleChange('temperature', parseFloat(e.target.value))} - className="w-full" + disabled={[ + 'gpt-5', 'gpt-5-chat-latest', 'gpt-5-mini', 'gpt-5-nano', + 'gpt-5-pro', 'gpt-5.1', 'gpt-5.1-chat-latest', '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', '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> @@ -274,6 +518,22 @@ const Sidebar = () => { 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> )} @@ -294,6 +554,117 @@ const Sidebar = () => { </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="bg-white rounded-lg shadow-2xl w-[80vw] max-w-4xl max-h-[80vh] flex flex-col" + onClick={(e) => e.stopPropagation()} + > + {/* Modal Header */} + <div className="flex items-center justify-between p-4 border-b border-gray-200"> + <h3 className="font-semibold text-lg">{selectedNode.data.label} - Response</h3> + <div className="flex gap-2"> + {!isEditing && ( + <button + onClick={() => setIsEditing(true)} + className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded flex items-center gap-1" + > + <Edit3 size={14} /> Edit + </button> + )} + <button + onClick={() => { setIsModalOpen(false); setIsEditing(false); }} + className="p-1 hover:bg-gray-200 rounded text-gray-500" + > + <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" + /> + ) : ( + <div className="prose prose-sm max-w-none"> + <ReactMarkdown>{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> + )} </div> ); }; diff --git a/frontend/src/components/nodes/LLMNode.tsx b/frontend/src/components/nodes/LLMNode.tsx index cdd402c..592ab5b 100644 --- a/frontend/src/components/nodes/LLMNode.tsx +++ b/frontend/src/components/nodes/LLMNode.tsx @@ -49,18 +49,32 @@ const LLMNode = ({ id, data, selected }: NodeProps<NodeData>) => { const inputsToShow = Math.max(maxConnectedIndex + 2, 1); + const isDisabled = data.disabled; + return ( - <div className={`px-4 py-2 shadow-md rounded-md bg-white border-2 min-w-[200px] ${selected ? 'border-blue-500' : 'border-gray-200'}`}> + <div + className={`px-4 py-2 shadow-md rounded-md border-2 min-w-[200px] transition-all ${ + isDisabled + ? 'bg-gray-100 border-gray-300 opacity-50 cursor-not-allowed' + : selected + ? 'bg-white border-blue-500' + : 'bg-white border-gray-200' + }`} + style={{ pointerEvents: isDisabled ? 'none' : 'auto' }} + > <div className="flex items-center mb-2"> - <div className="rounded-full w-8 h-8 flex justify-center items-center bg-gray-100"> + <div className={`rounded-full w-8 h-8 flex justify-center items-center ${isDisabled ? 'bg-gray-200' : 'bg-gray-100'}`}> {data.status === 'loading' ? ( <Loader2 className="w-4 h-4 animate-spin text-blue-500" /> ) : ( - <MessageSquare className="w-4 h-4 text-gray-600" /> + <MessageSquare className={`w-4 h-4 ${isDisabled ? 'text-gray-400' : 'text-gray-600'}`} /> )} </div> <div className="ml-2"> - <div className="text-sm font-bold truncate max-w-[150px]">{data.label}</div> + <div className={`text-sm font-bold truncate max-w-[150px] ${isDisabled ? 'text-gray-400' : ''}`}> + {data.label} + {isDisabled && <span className="text-xs ml-1">(disabled)</span>} + </div> <div className="text-xs text-gray-500">{data.model}</div> </div> </div> |
