From d9868550e66fe8aaa7fff55a8e24b871ee51e3b1 Mon Sep 17 00:00:00 2001 From: blackhao <13851610112@163.com> Date: Fri, 5 Dec 2025 20:40:40 -0600 Subject: init: add project files and ignore secrets --- frontend/src/App.css | 42 ++++ frontend/src/App.tsx | 138 ++++++++++ frontend/src/assets/react.svg | 1 + frontend/src/components/Sidebar.tsx | 320 +++++++++++++++++++++++ frontend/src/components/nodes/LLMNode.tsx | 139 ++++++++++ frontend/src/index.css | 11 + frontend/src/main.tsx | 10 + frontend/src/store/flowStore.ts | 405 ++++++++++++++++++++++++++++++ 8 files changed, 1066 insertions(+) create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/components/nodes/LLMNode.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/store/flowStore.ts (limited to 'frontend/src') diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..1eaafec --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,138 @@ +import { useCallback, useRef } from 'react'; +import ReactFlow, { + Background, + Controls, + MiniMap, + ReactFlowProvider, + Panel, + useReactFlow +} from 'reactflow'; +import 'reactflow/dist/style.css'; +import useFlowStore from './store/flowStore'; +import LLMNode from './components/nodes/LLMNode'; +import Sidebar from './components/Sidebar'; +import { Plus } from 'lucide-react'; + +const nodeTypes = { + llmNode: LLMNode, +}; + +function Flow() { + const { + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + addNode, + setSelectedNode + } = useFlowStore(); + + const reactFlowWrapper = useRef(null); + const { project } = useReactFlow(); + + const handleAddNode = () => { + const id = `node_${Date.now()}`; + addNode({ + id, + type: 'llmNode', + position: { x: Math.random() * 400, y: Math.random() * 400 }, + data: { + label: 'New Question', + model: 'gpt-4o', + temperature: 0.7, + systemPrompt: '', + userPrompt: '', + mergeStrategy: 'smart', + messages: [], + traces: [], + outgoingTraces: [], + forkedTraces: [], + response: '', + status: 'idle', + inputs: 1 + }, + }); + }; + + const onNodeClick = (_: any, node: any) => { + setSelectedNode(node.id); + }; + + const onPaneClick = () => { + setSelectedNode(null); + }; + + const onPaneContextMenu = (event: React.MouseEvent) => { + event.preventDefault(); + const bounds = reactFlowWrapper.current?.getBoundingClientRect(); + if (!bounds) return; + + const position = project({ + x: event.clientX - bounds.left, + y: event.clientY - bounds.top + }); + + const id = `node_${Date.now()}`; + addNode({ + id, + type: 'llmNode', + position, + data: { + label: 'New Question', + model: 'gpt-4o', + temperature: 0.7, + systemPrompt: '', + userPrompt: '', + mergeStrategy: 'smart', + messages: [], + traces: [], + outgoingTraces: [], + forkedTraces: [], + response: '', + status: 'idle', + inputs: 1 + }, + }); + }; + + return ( +
+
+ + + + + + + + +
+ +
+ ); +} + +export default function App() { + return ( + + + + ); +} diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..f62f3cb --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,320 @@ +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'; + +const Sidebar = () => { + const { nodes, selectedNodeId, updateNodeData, getActiveContext } = useFlowStore(); + const [activeTab, setActiveTab] = useState<'interact' | 'settings' | 'debug'>('interact'); + const [streamBuffer, setStreamBuffer] = useState(''); + + const selectedNode = nodes.find((n) => n.id === selectedNodeId); + + // Reset stream buffer when node changes + useEffect(() => { + setStreamBuffer(''); + }, [selectedNodeId]); + + if (!selectedNode) { + return ( +
+

Select a node to edit

+
+ ); + } + + const handleRun = async () => { + if (!selectedNode) return; + + updateNodeData(selectedNode.id, { status: 'loading', response: '' }); + setStreamBuffer(''); + + // Use getActiveContext which respects the user's selected traces + const context = getActiveContext(selectedNode.id); + + try { + const response = await fetch('http://localhost:8000/api/run_node_stream', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + node_id: selectedNode.id, + incoming_contexts: [{ messages: context }], // Simple list wrap for now + user_prompt: selectedNode.data.userPrompt, + merge_strategy: selectedNode.data.mergeStrategy || 'smart', + config: { + provider: selectedNode.data.model.includes('gpt') ? 'openai' : 'google', + model_name: selectedNode.data.model, + temperature: selectedNode.data.temperature, + system_prompt: selectedNode.data.systemPrompt, + api_key: selectedNode.data.apiKey, + } + }) + }); + + 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) break; + const chunk = decoder.decode(value); + fullResponse += chunk; + setStreamBuffer(prev => prev + chunk); + // We update the store less frequently or at the end to avoid too many re-renders + // But for "live" feel we might want to update local state `streamBuffer` and sync to store at end + } + + // Update final state + // Append the new interaction to the node's output messages + const newUserMsg = { + id: `msg_${Date.now()}_u`, + role: 'user', + content: selectedNode.data.userPrompt + }; + const newAssistantMsg = { + id: `msg_${Date.now()}_a`, + role: 'assistant', + content: fullResponse + }; + + updateNodeData(selectedNode.id, { + status: 'success', + response: fullResponse, + messages: [...context, newUserMsg, newAssistantMsg] as any + }); + + } catch (error) { + console.error(error); + updateNodeData(selectedNode.id, { status: 'error' }); + } + }; + + const handleChange = (field: keyof NodeData, value: any) => { + updateNodeData(selectedNode.id, { [field]: value }); + }; + + return ( +
+ {/* Header */} +
+ handleChange('label', e.target.value)} + className="font-bold text-lg bg-transparent border-none focus:ring-0 focus:outline-none w-full" + /> +
+
+ {selectedNode.data.status} +
+
+ ID: {selectedNode.id} +
+
+
+ + {/* Tabs */} +
+ + + +
+ + {/* Content */} +
+ {activeTab === 'interact' && ( +
+
+ + +
+ + {/* Trace Selector */} + {selectedNode.data.traces && selectedNode.data.traces.length > 0 && ( +
+ +
+ {selectedNode.data.traces.map((trace) => { + const isActive = selectedNode.data.activeTraceIds?.includes(trace.id); + return ( +
{ + const current = selectedNode.data.activeTraceIds || []; + const next = [trace.id]; // Single select mode + handleChange('activeTraceIds', next); + }} + > + +
+
+
+ #{trace.id.slice(-4)} +
+
+ From Node: {trace.sourceNodeId} +
+
+ {trace.messages.length} msgs +
+
+
+ ); + })} +
+
+ )} + +
+ +