From 93dbe11014cf967690727c25e89d9d1075008c24 Mon Sep 17 00:00:00 2001
From: blackhao <13851610112@163.com>
Date: Sat, 6 Dec 2025 01:30:57 -0600
Subject: UX

---
 frontend/src/components/Sidebar.tsx | 419 +++++++++++++++++++++++++++++++++---
 1 file changed, 395 insertions(+), 24 deletions(-)

(limited to 'frontend/src/components/Sidebar.tsx')

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>
   );
 };
-- 
cgit v1.2.3

