summaryrefslogtreecommitdiff
path: root/frontend/src/components/Sidebar.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/Sidebar.tsx')
-rw-r--r--frontend/src/components/Sidebar.tsx419
1 files changed, 395 insertions, 24 deletions
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>
);
};