From 06ca86c5c52d5137fc778f59442a392aafd9c116 Mon Sep 17 00:00:00 2001 From: Adam Cohen Hillel Date: Sat, 12 Apr 2025 17:37:45 +0100 Subject: [PATCH] OK --- frontend/src/app/page.tsx | 217 +---- frontendbasejump/package-lock.json | 61 ++ frontendbasejump/package.json | 3 + .../agents/[threadId]/page.tsx | 747 ++++++++++++++---- .../dashboard/(personalAccount)/layout.tsx | 64 +- frontendbasejump/src/app/layout.tsx | 40 +- frontendbasejump/src/app/providers.tsx | 27 + .../src/components/layout/DashboardLayout.tsx | 52 +- .../src/components/tools/tool-components.tsx | 369 +++++++++ .../src/lib/hooks/use-tools-panel.tsx | 203 +++++ frontendbasejump/src/lib/types/tool-calls.ts | 48 ++ frontendbasejump/yarn.lock | 35 +- 12 files changed, 1427 insertions(+), 439 deletions(-) create mode 100644 frontendbasejump/src/app/providers.tsx create mode 100644 frontendbasejump/src/components/tools/tool-components.tsx create mode 100644 frontendbasejump/src/lib/hooks/use-tools-panel.tsx create mode 100644 frontendbasejump/src/lib/types/tool-calls.ts diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 10646aa2..66abd0f2 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,222 +1,13 @@ 'use client'; -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { useAuth } from '@/context/auth-context'; -import Link from 'next/link'; -import { Button } from '@/components/ui/button'; -import { Search, Image, Calculator } from 'lucide-react'; +import ComputerViewer from '@/components/ComputerViewer'; + export default function HomePage() { - const { user, isLoading } = useAuth(); - const router = useRouter(); - - useEffect(() => { - // Redirect authenticated users to dashboard - if (!isLoading && user) { - router.push('/dashboard'); - } - }, [user, isLoading, router]); - - // If still loading or user is authenticated (redirecting), show loading - if (isLoading || user) { - return null; - } - + return (
- {/* Hero Section */} -
-
-

AgentPress

-

- A meticulously AI-powered search engine with RAG and search grounding capabilities. Clean interface and built for everyone. -

-
- - -
-
-
- - {/* RAG & Search Grounding */} -
-
-

RAG & Search Grounding

-
-
-
-
- 1 -
-
-

Enable modern RAG capabilities in your real-world applications

-

Semantic search provides better context for AI agents

-
-
- -
-
- 2 -
-
-

Prioritized instant search

-

Get results as you type for a responsive experience

-
-
- -
-
- 3 -
-
-

Integration is just a few lines of code for "information access"

-

Simple API to connect to your comprehensive search

-
-
-
-
-
-
- - {/* Powered By */} -
-
-

Powered By

-
-
-
- - - -
-

Deployed on Vercel

-
-
-
- - - - - -
-

Design by Tekky

-
-
-
-
- - {/* Stats Section */} -
-
-
-

350K+

-

Requests Processed

-
-
-

100K+

-

Active Users

-
-
-

7K+

-

Connections Built

-
-
-
- - {/* Featured on Vercel's Blog */} -
-
-

Featured on Vercel's Blog

-
-
-

- Recognized for our innovative use of AI technology and its integration for a seamless experience. Our approach to developer community growth stands out. -

- - Read the feature → - -
-
-
-
-
- - {/* Advanced Features */} -
-
-

Advanced Search Features

-
-
-
- -
-

Smart Understanding

-

Easily analyze content and context for better search results

-
- -
-
- -
-

Image Understanding

-

Get contextual responses based on visual inputs

-
- -
-
- -
-

Smart Calculations

-

Perform complex math and calculations in real-time

-
-
-
-
- - {/* Built For Everyone */} -
-
-

Built For Everyone

-
-
-

Students

-
    -
  • • Research paper analysis
  • -
  • • Complex calculations
  • -
  • • Study guidance
  • -
-
- -
-

Researchers

-
    -
  • • Access to paper archives
  • -
  • • Data visualization
  • -
  • • Citation formatting
  • -
-
- -
-

Professionals

-
    -
  • • Market research
  • -
  • • Technical troubleshooting
  • -
  • • Code reviews
  • -
-
-
-
-
- - {/* Footer */} -
-

© 2023 AgentPress. All rights reserved.

-
+
); } diff --git a/frontendbasejump/package-lock.json b/frontendbasejump/package-lock.json index d8d7436c..26ce267d 100644 --- a/frontendbasejump/package-lock.json +++ b/frontendbasejump/package-lock.json @@ -22,6 +22,8 @@ "clsx": "^2.1.0", "cmdk": "^0.2.0", "date-fns": "^3.6.0", + "diff": "^7.0.0", + "framer-motion": "^12.6.5", "geist": "^1.2.1", "lucide-react": "^0.368.0", "next": "latest", @@ -39,6 +41,7 @@ "zod": "^3.24.2" }, "devDependencies": { + "@types/diff": "^7.0.2", "@types/node": "20.11.5", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", @@ -1291,6 +1294,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.11.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.5.tgz", @@ -1992,6 +2002,15 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "license": "Apache-2.0" }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -2113,6 +2132,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.6.5", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.6.5.tgz", + "integrity": "sha512-MKvnWov0paNjvRJuIy6x418w23tFqRfS6CXHhZrCiSEpXVlo/F+usr8v4/3G6O0u7CpsaO1qop+v4Ip7PRCBqQ==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.6.5", + "motion-utils": "^12.6.5", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2421,6 +2467,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.6.5", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.6.5.tgz", + "integrity": "sha512-jpM9TQLXzYMWMJ7Ec7sAj0iis8oIuu6WvjI3yNKJLdrZyrsI/b2cRInDVL8dCl683zQQq19DpL9cSMP+k8T1NA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.6.5" + } + }, + "node_modules/motion-utils": { + "version": "12.6.5", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.6.5.tgz", + "integrity": "sha512-IsOeKsOF+FWBhxQEDFBO6ZYC8/jlidmVbbLpe9/lXSA9j9kzGIMUuIBx2SZY+0reAS0DjZZ1i7dJp4NHrjocPw==", + "license": "MIT" + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", diff --git a/frontendbasejump/package.json b/frontendbasejump/package.json index 6b27ff78..6cdfb1e1 100644 --- a/frontendbasejump/package.json +++ b/frontendbasejump/package.json @@ -23,6 +23,8 @@ "clsx": "^2.1.0", "cmdk": "^0.2.0", "date-fns": "^3.6.0", + "diff": "^7.0.0", + "framer-motion": "^12.6.5", "geist": "^1.2.1", "lucide-react": "^0.368.0", "next": "latest", @@ -40,6 +42,7 @@ "zod": "^3.24.2" }, "devDependencies": { + "@types/diff": "^7.0.2", "@types/node": "20.11.5", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", diff --git a/frontendbasejump/src/app/dashboard/(personalAccount)/agents/[threadId]/page.tsx b/frontendbasejump/src/app/dashboard/(personalAccount)/agents/[threadId]/page.tsx index cc805b9f..62d5fd9a 100644 --- a/frontendbasejump/src/app/dashboard/(personalAccount)/agents/[threadId]/page.tsx +++ b/frontendbasejump/src/app/dashboard/(personalAccount)/agents/[threadId]/page.tsx @@ -1,13 +1,16 @@ 'use client'; -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { getProject, getMessages, getThread, addUserMessage, startAgent, stopAgent, getAgentRuns, createThread, type Message, type Project, type Thread, type AgentRun } from "@/lib/api"; +import React, { useState, useEffect, useCallback, useRef, useContext } from "react"; +import { getProject, getMessages, getThread, addUserMessage, startAgent, stopAgent, getAgentRuns, streamAgent, type Message, type Project, type Thread, type AgentRun } from "@/lib/api"; import { useRouter, useSearchParams } from "next/navigation"; -import { AlertCircle, ArrowDown, Play, Square } from "lucide-react"; +import { AlertCircle, Play, Square, Send, User, Plus } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Skeleton } from "@/components/ui/skeleton"; +import { SUPPORTED_XML_TAGS, ParsedTag } from "@/lib/types/tool-calls"; +import { ToolCallsContext } from "@/app/providers"; +import { getComponentForTag } from "@/components/tools/tool-components"; interface AgentPageProps { params: { @@ -15,16 +18,203 @@ interface AgentPageProps { }; } -// Simple component to handle message formatting +// Parse XML tags in content +function parseXMLTags(content: string): { parts: (string | ParsedTag)[], openTags: Record } { + const parts: (string | ParsedTag)[] = []; + const openTags: Record = {}; + const tagStack: Array<{tagName: string, position: number}> = []; + + // Find all opening and closing tags + let currentPosition = 0; + + // Match opening tags with attributes like + const openingTagRegex = new RegExp(`<(${SUPPORTED_XML_TAGS.join('|')})\\s*([^>]*)>`, 'g'); + // Match closing tags like + const closingTagRegex = new RegExp(``, 'g'); + + let match: RegExpExecArray | null; + let matches: { regex: RegExp, match: RegExpExecArray, isOpening: boolean, position: number }[] = []; + + // Find all opening tags + while ((match = openingTagRegex.exec(content)) !== null) { + matches.push({ + regex: openingTagRegex, + match, + isOpening: true, + position: match.index + }); + } + + // Find all closing tags + while ((match = closingTagRegex.exec(content)) !== null) { + matches.push({ + regex: closingTagRegex, + match, + isOpening: false, + position: match.index + }); + } + + // Sort matches by their position in the content + matches.sort((a, b) => a.position - b.position); + + // Process matches in order + for (const { match, isOpening, position } of matches) { + const tagName = match[1]; + const matchEnd = position + match[0].length; + + // Add text before this tag if needed + if (position > currentPosition) { + parts.push(content.substring(currentPosition, position)); + } + + if (isOpening) { + // Parse attributes for opening tags + const attributesStr = match[2]?.trim(); + const attributes: Record = {}; + + if (attributesStr) { + // Match attributes in format: name="value" or name='value' + const attrRegex = /(\w+)=["']([^"']*)["']/g; + let attrMatch; + while ((attrMatch = attrRegex.exec(attributesStr)) !== null) { + attributes[attrMatch[1]] = attrMatch[2]; + } + } + + // Create tag object with unique ID + const parsedTag: ParsedTag = { + tagName, + attributes, + content: '', + isClosing: false, + id: `${tagName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + rawMatch: match[0] + }; + + // Add timestamp if not present + if (!parsedTag.timestamp) { + parsedTag.timestamp = Date.now(); + } + + // Push to parts and track in stack + parts.push(parsedTag); + tagStack.push({ tagName, position: parts.length - 1 }); + openTags[tagName] = parsedTag; + + } else { + // Handle closing tag + // Find the corresponding opening tag in the stack (last in, first out) + let foundOpeningTag = false; + + for (let i = tagStack.length - 1; i >= 0; i--) { + if (tagStack[i].tagName === tagName) { + const openTagIndex = tagStack[i].position; + const openTag = parts[openTagIndex] as ParsedTag; + + // Get content between this opening and closing tag pair + const contentStart = position; + let tagContentStart = openTagIndex + 1; + let tagContentEnd = parts.length; + + // Mark that we need to capture content between these positions + let contentToCapture = ''; + + // Collect all content parts between the opening and closing tags + for (let j = tagContentStart; j < tagContentEnd; j++) { + if (typeof parts[j] === 'string') { + contentToCapture += parts[j]; + } + } + + // Try getting content directly from original text (most reliable approach) + const openTagMatch = openTag.rawMatch || ''; + const openTagPosition = content.indexOf(openTagMatch, Math.max(0, openTagIndex > 0 ? currentPosition - 200 : 0)); + if (openTagPosition >= 0) { + const openTagEndPosition = openTagPosition + openTagMatch.length; + // Only use if the positions make sense + if (openTagEndPosition > 0 && position > openTagEndPosition) { + // Get content and clean up excessive whitespace but preserve formatting + let extractedContent = content.substring(openTagEndPosition, position); + + // Trim leading newline if present + if (extractedContent.startsWith('\n')) { + extractedContent = extractedContent.substring(1); + } + + // Trim trailing newline if present + if (extractedContent.endsWith('\n')) { + extractedContent = extractedContent.substring(0, extractedContent.length - 1); + } + + contentToCapture = extractedContent; + + // Debug info in development + console.log(`[XML Parse] Extracted content for ${tagName}:`, contentToCapture); + } + } + + // Update opening tag with collected content + openTag.content = contentToCapture; + openTag.isClosing = true; + + // Remove all parts between the opening tag and this position + // because they're now captured in the tag's content + if (tagContentStart < tagContentEnd) { + parts.splice(tagContentStart, tagContentEnd - tagContentStart); + } + + // Remove this tag from the stack + tagStack.splice(i, 1); + // Remove from openTags + delete openTags[tagName]; + + foundOpeningTag = true; + break; + } + } + + // If no corresponding opening tag found, add closing tag as text + if (!foundOpeningTag) { + parts.push(match[0]); + } + } + + currentPosition = matchEnd; + } + + // Add any remaining text + if (currentPosition < content.length) { + parts.push(content.substring(currentPosition)); + } + + return { parts, openTags }; +} + +// Simple component to handle message formatting with XML tag support function MessageContent({ content }: { content: string }) { + const { parts, openTags } = parseXMLTags(content); + return (
- {content.split('\n').map((line, i) => ( - - {line} - {i < content.split('\n').length - 1 &&
} -
- ))} + {parts.map((part, index) => { + if (typeof part === 'string') { + return ( + + {part.split('\n').map((line, i) => ( + + {line} + {i < part.split('\n').length - 1 &&
} +
+ ))} +
+ ); + } else { + // Render specialized tool component based on tag type + const ToolComponent = getComponentForTag(part); + return ; + } + })}
); } @@ -35,6 +225,7 @@ export default function AgentPage({ params }: AgentPageProps) { const searchParams = useSearchParams(); const initialMessage = searchParams.get('message'); const messagesEndRef = useRef(null); + const streamCleanupRef = useRef<(() => void) | null>(null); const [agent, setAgent] = useState(null); const [conversation, setConversation] = useState(null); @@ -44,6 +235,150 @@ export default function AgentPage({ params }: AgentPageProps) { const [error, setError] = useState(null); const [userMessage, setUserMessage] = useState(""); const [isSending, setIsSending] = useState(false); + const [isStreaming, setIsStreaming] = useState(false); + const [streamContent, setStreamContent] = useState(""); + const [currentAgentRunId, setCurrentAgentRunId] = useState(null); + + // Get tool calls context + const { toolCalls, setToolCalls } = useContext(ToolCallsContext); + + // Process messages and stream for tool calls + useEffect(() => { + // Extract tool calls from all messages + const allContent = [...messages.map(msg => msg.content), streamContent].filter(Boolean); + + console.log(`[TOOLS] Processing ${allContent.length} content items for tool calls`); + + // Create a new array of tags with a better deduplication strategy + const extractedTags: ParsedTag[] = []; + const seenTagIds = new Set(); + + allContent.forEach((content, idx) => { + console.log(`[TOOLS] Extracting from content #${idx}, length: ${content.length}`); + const { parts, openTags } = parseXMLTags(content); + + let tagsFound = 0; + + // Mark tool calls vs results based on position and sender + const isUserMessage = idx % 2 === 0; // Assuming alternating user/assistant messages + + // Process all parts to mark as tool calls or results + parts.forEach(part => { + if (typeof part !== 'string') { + // Create a unique ID for this tag if not present + if (!part.id) { + part.id = `${part.tagName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + } + + // Add timestamp if not present + if (!part.timestamp) { + part.timestamp = Date.now(); + } + + // Mark as tool call or result + part.isToolCall = !isUserMessage; + part.status = part.isClosing ? 'completed' : 'running'; + + // Use ID for deduplication + if (!seenTagIds.has(part.id)) { + seenTagIds.add(part.id); + extractedTags.push(part); + tagsFound++; + } + } + }); + + // Also add any open tags + Object.values(openTags).forEach(tag => { + // Create a unique ID for this tag if not present + if (!tag.id) { + tag.id = `${tag.tagName}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; + } + + // Add timestamp if not present + if (!tag.timestamp) { + tag.timestamp = Date.now(); + } + + // Mark as tool call or result + tag.isToolCall = !isUserMessage; + tag.status = tag.isClosing ? 'completed' : 'running'; + + // Use ID for deduplication + if (!seenTagIds.has(tag.id)) { + seenTagIds.add(tag.id); + extractedTags.push(tag); + tagsFound++; + } + }); + + console.log(`[TOOLS] Found ${tagsFound} tags in content #${idx}`); + }); + + console.log(`[TOOLS] Extracted ${extractedTags.length} total tools`); + + // Sort the tools by timestamp (oldest first) + extractedTags.sort((a, b) => { + if (a.timestamp && b.timestamp) { + return a.timestamp - b.timestamp; + } + return 0; + }); + + // Try to pair tool calls with their results + const pairedTags: ParsedTag[] = []; + const callsByTagName: Record = {}; + + // Group by tag name first + extractedTags.forEach(tag => { + if (!callsByTagName[tag.tagName]) { + callsByTagName[tag.tagName] = []; + } + callsByTagName[tag.tagName].push(tag); + }); + + // For each tag type, try to pair calls with results + Object.values(callsByTagName).forEach(tagGroup => { + const toolCalls = tagGroup.filter(tag => tag.isToolCall); + const toolResults = tagGroup.filter(tag => !tag.isToolCall); + + // Try to match each tool call with a result + toolCalls.forEach(toolCall => { + // Find the nearest matching result (by timestamp) + const matchingResult = toolResults.find(result => + !result.isPaired && result.attributes && + Object.keys(toolCall.attributes).every(key => + toolCall.attributes[key] === result.attributes[key] + ) + ); + + if (matchingResult) { + // Pair them + toolCall.resultTag = matchingResult; + toolCall.isPaired = true; + toolCall.status = 'completed'; + matchingResult.isPaired = true; + + // Add to paired list + pairedTags.push(toolCall); + } else { + // No result yet, tool call is still running + toolCall.status = 'running'; + pairedTags.push(toolCall); + } + }); + + // Add any unpaired results + toolResults.filter(result => !result.isPaired).forEach(result => { + pairedTags.push(result); + }); + }); + + console.log(`[TOOLS] Paired ${pairedTags.length} tools, ${pairedTags.filter(t => t.isPaired).length} were paired`); + + // Update tool calls in the shared context + setToolCalls(pairedTags); + }, [messages, streamContent, setToolCalls]); // Scroll to bottom of messages const scrollToBottom = useCallback(() => { @@ -84,6 +419,13 @@ export default function AgentPage({ params }: AgentPageProps) { const agentRunsData = await getAgentRuns(threadId); setAgentRuns(agentRunsData); + + // Check if there's a running agent run + const runningAgent = agentRunsData.find(run => run.status === "running"); + if (runningAgent) { + setCurrentAgentRunId(runningAgent.id); + handleStreamAgent(runningAgent.id); + } } } } catch (err: any) { @@ -101,41 +443,156 @@ export default function AgentPage({ params }: AgentPageProps) { } loadData(); + + // Clean up streaming on component unmount + return () => { + if (streamCleanupRef.current) { + console.log("[PAGE] Cleaning up stream on unmount"); + streamCleanupRef.current(); + streamCleanupRef.current = null; + } + }; }, [threadId, initialMessage, router]); // Scroll to bottom when messages change useEffect(() => { scrollToBottom(); - }, [messages, scrollToBottom]); + }, [messages, streamContent, scrollToBottom]); - // Check for agent status periodically if an agent is running - useEffect(() => { - if (!conversation) return; + // Handle streaming agent responses + const handleStreamAgent = useCallback((agentRunId: string) => { + // Clean up any existing stream first + if (streamCleanupRef.current) { + console.log("[PAGE] Cleaning up existing stream before starting new one"); + streamCleanupRef.current(); + streamCleanupRef.current = null; + } - const isAgentRunning = agentRuns.some(run => run.status === "running"); - if (!isAgentRunning) return; + setIsStreaming(true); + setStreamContent(""); - // Poll for updates every 3 seconds if agent is running - const interval = setInterval(async () => { - try { - const updatedAgentRuns = await getAgentRuns(conversation.thread_id); - setAgentRuns(updatedAgentRuns); - - // Also refresh messages - const updatedMessages = await getMessages(conversation.thread_id); - setMessages(updatedMessages); - - // If no agent is running anymore, stop polling - if (!updatedAgentRuns.some(run => run.status === "running")) { - clearInterval(interval); + console.log(`[PAGE] Setting up stream for agent run ${agentRunId}`); + + const cleanup = streamAgent(agentRunId, { + onMessage: (rawData: string) => { + try { + console.log(`[PAGE] Raw message data:`, rawData); + + // Handle data: prefix format (SSE standard) + let processedData = rawData; + let jsonData: { + type?: string; + status?: string; + content?: string; + } | null = null; + + if (rawData.startsWith('data: ')) { + processedData = rawData.substring(6).trim(); + } + + // Try to parse as JSON + try { + jsonData = JSON.parse(processedData); + + // Handle status messages + if (jsonData?.type === 'status') { + if (jsonData?.status === 'completed') { + console.log("[PAGE] Detected completion status event"); + + // Reset streaming on completion + setIsStreaming(false); + + // Fetch updated messages + if (threadId) { + getMessages(threadId) + .then(updatedMsgs => { + console.log("[PAGE] Updated messages:"); + console.log(updatedMsgs); + setMessages(updatedMsgs); + setStreamContent(""); + + // Also update agent runs + return getAgentRuns(threadId); + }) + .then(updatedRuns => { + setAgentRuns(updatedRuns); + setCurrentAgentRunId(null); + }) + .catch(err => console.error("[PAGE] Failed to update after completion:", err)); + } + + return; + } + return; // Don't process other status messages further + } + + // Handle content messages + if (jsonData?.type === 'content' && jsonData?.content) { + setStreamContent(prev => prev + jsonData?.content); + return; + } + } catch (e) { + // If not valid JSON, just append the raw data + console.warn("[PAGE] Failed to parse as JSON:", e); + } + + // If we couldn't parse as special format, just append the raw data + if (!jsonData) { + setStreamContent(prev => prev + processedData); + } + } catch (error) { + console.warn("[PAGE] Failed to process message:", error); } - } catch (err) { - console.error("Error polling agent status:", err); + }, + onError: (error: Error | string) => { + console.error("[PAGE] Streaming error:", error); + setIsStreaming(false); + }, + onClose: async () => { + console.log("[PAGE] Stream connection closed"); + + // Set UI state to not streaming + setIsStreaming(false); + + try { + // Update messages and agent runs + if (threadId) { + const updatedMessages = await getMessages(threadId); + setMessages(updatedMessages); + + const updatedAgentRuns = await getAgentRuns(threadId); + setAgentRuns(updatedAgentRuns); + + // Reset current agent run + setCurrentAgentRunId(null); + + // Clear streaming content after a short delay + setTimeout(() => { + setStreamContent(""); + }, 50); + } + } catch (err) { + console.error("[PAGE] Error checking final status:", err); + + // If there was streaming content, add it as a message + if (streamContent) { + const assistantMessage: Message = { + role: 'assistant', + content: streamContent + "\n\n[Connection to agent lost]", + }; + setMessages(prev => [...prev, assistantMessage]); + setStreamContent(""); + } + } + + // Clear cleanup reference + streamCleanupRef.current = null; } - }, 3000); + }); - return () => clearInterval(interval); - }, [agentRuns, conversation]); + // Store cleanup function + streamCleanupRef.current = cleanup; + }, [threadId]); // Handle sending a message const handleSendMessage = async () => { @@ -144,21 +601,25 @@ export default function AgentPage({ params }: AgentPageProps) { setIsSending(true); try { - // Add user message - await addUserMessage(conversation.thread_id, userMessage); - - // Start the agent - await startAgent(conversation.thread_id); + // Add user message optimistically to UI + const userMsg: Message = { + role: 'user', + content: userMessage, + }; + setMessages(prev => [...prev, userMsg]); // Clear the input setUserMessage(""); - // Refresh data - const updatedMessages = await getMessages(conversation.thread_id); - setMessages(updatedMessages); + // Add user message to API and start agent + await addUserMessage(conversation.thread_id, userMessage); + const agentResponse = await startAgent(conversation.thread_id); - const updatedAgentRuns = await getAgentRuns(conversation.thread_id); - setAgentRuns(updatedAgentRuns); + // Set current agent run ID and start streaming + if (agentResponse.agent_run_id) { + setCurrentAgentRunId(agentResponse.agent_run_id); + handleStreamAgent(agentResponse.agent_run_id); + } } catch (err) { console.error("Error sending message:", err); setError(err instanceof Error ? err.message : "Failed to send message"); @@ -167,37 +628,36 @@ export default function AgentPage({ params }: AgentPageProps) { } }; - // Handle starting the agent - const handleRunAgent = async () => { - if (isSending || !conversation) return; - - setIsSending(true); - try { - await startAgent(conversation.thread_id); - - // Refresh agent runs - const updatedAgentRuns = await getAgentRuns(conversation.thread_id); - setAgentRuns(updatedAgentRuns); - } catch (err) { - console.error("Error starting agent:", err); - setError(err instanceof Error ? err.message : "Failed to start agent"); - } finally { - setIsSending(false); - } - }; - // Handle stopping the agent const handleStopAgent = async () => { try { // Find the running agent run - const runningAgent = agentRuns.find(run => run.status === "running"); - if (runningAgent) { - await stopAgent(runningAgent.id); + const runningAgentId = currentAgentRunId || agentRuns.find(run => run.status === "running")?.id; + + if (runningAgentId) { + // Clean up stream first + if (streamCleanupRef.current) { + console.log("[PAGE] Cleaning up stream before stopping agent"); + streamCleanupRef.current(); + streamCleanupRef.current = null; + } + + // Then stop the agent + await stopAgent(runningAgentId); + setIsStreaming(false); + setCurrentAgentRunId(null); // Refresh agent runs if (conversation) { const updatedAgentRuns = await getAgentRuns(conversation.thread_id); setAgentRuns(updatedAgentRuns); + + // Also refresh messages to get any partial responses + const updatedMessages = await getMessages(conversation.thread_id); + setMessages(updatedMessages); + + // Clear streaming content + setStreamContent(""); } } } catch (err) { @@ -206,8 +666,8 @@ export default function AgentPage({ params }: AgentPageProps) { } }; - // Check if agent is running - const isAgentRunning = agentRuns.some(run => run.status === "running"); + // Check if agent is running either from agent runs list or streaming state + const isAgentRunning = isStreaming || currentAgentRunId !== null || agentRuns.some(run => run.status === "running"); if (error) { return ( @@ -247,9 +707,9 @@ export default function AgentPage({ params }: AgentPageProps) { } return ( -
-
- {messages.length === 0 ? ( +
+
+ {messages.length === 0 && !streamContent ? (

Start a conversation

@@ -260,29 +720,65 @@ export default function AgentPage({ params }: AgentPageProps) {
) : ( <> - {messages.map((message, index) => ( -
+ {messages.map((message, index) => { + // Skip messages containing "ToolResult(" + if (message.content.includes("ToolResult(")) { + return null; + } + + return (
- +
+ {message.role === "user" && ( + + )} + +
+
+ ); + })} + + {/* Show streaming content if available */} + {streamContent && ( +
+
+
+ + {isStreaming && ( + + )} + +
- ))} + )} - {/* Show a loading indicator if the agent is running */} - {isAgentRunning && ( + {/* Show a loading indicator if the agent is running but no stream yet */} + {isAgentRunning && !streamContent && (
-
+
@@ -297,53 +793,40 @@ export default function AgentPage({ params }: AgentPageProps) {
-
-
-
-