From a7dabc45bc177db6508073a7d62496b3f58ef2fe Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Fri, 23 May 2025 13:14:29 +0530 Subject: [PATCH 1/2] chore(ui): payment dialog + live indicator fix --- .../(dashboard)/agents/[threadId]/page.tsx | 7 + .../src/app/(dashboard)/dashboard/page.tsx | 127 +++--- .../billing/payment-required-dialog.tsx | 60 +++ .../home/sections/pricing-section.tsx | 24 +- .../thread/tool-call-side-panel.tsx | 184 ++++++--- .../tool-views/TerminateCommandToolView.tsx | 380 ++++++++++++++++++ .../tool-views/wrapper/ToolViewRegistry.tsx | 2 + frontend/src/components/thread/utils.ts | 4 + .../src/hooks/react-query/dashboard/keys.ts | 7 + .../dashboard/use-initiate-agent.ts | 48 +++ .../src/hooks/react-query/dashboard/utils.ts | 1 + frontend/src/hooks/use-modal-store.ts | 16 + frontend/src/lib/api.ts | 2 - frontend/src/providers/modal-providers.tsx | 9 + 14 files changed, 746 insertions(+), 125 deletions(-) create mode 100644 frontend/src/components/billing/payment-required-dialog.tsx create mode 100644 frontend/src/components/thread/tool-views/TerminateCommandToolView.tsx create mode 100644 frontend/src/hooks/react-query/dashboard/keys.ts create mode 100644 frontend/src/hooks/react-query/dashboard/use-initiate-agent.ts create mode 100644 frontend/src/hooks/react-query/dashboard/utils.ts create mode 100644 frontend/src/hooks/use-modal-store.ts create mode 100644 frontend/src/providers/modal-providers.tsx diff --git a/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx b/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx index 6f1582ff..456edd3e 100644 --- a/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx @@ -136,6 +136,8 @@ export default function ThreadPage({ const agentRunsCheckedRef = useRef(false); const previousAgentStatus = useRef('idle'); + const [externalNavIndex, setExternalNavIndex] = React.useState(undefined); + // Add debug mode state - check for debug=true in URL const [debugMode, setDebugMode] = useState(false); @@ -850,8 +852,11 @@ export default function ThreadPage({ console.log( `[PAGE] Found tool call at index ${toolIndex} for assistant message ${clickedAssistantMessageId}`, ); + setExternalNavIndex(toolIndex); setCurrentToolIndex(toolIndex); setIsSidePanelOpen(true); // Explicitly open the panel + + setTimeout(() => setExternalNavIndex(undefined), 100); } else { console.warn( `[PAGE] Could not find matching tool call in toolCalls array for assistant message ID: ${clickedAssistantMessageId}`, @@ -1129,6 +1134,7 @@ export default function ThreadPage({ }} toolCalls={toolCalls} messages={messages as ApiMessageType[]} + externalNavigateToIndex={externalNavIndex} agentStatus={agentStatus} currentIndex={currentToolIndex} onNavigate={handleSidePanelNavigate} @@ -1236,6 +1242,7 @@ export default function ThreadPage({ }} toolCalls={toolCalls} messages={messages as ApiMessageType[]} + externalNavigateToIndex={externalNavIndex} agentStatus={agentStatus} currentIndex={currentToolIndex} onNavigate={handleSidePanelNavigate} diff --git a/frontend/src/app/(dashboard)/dashboard/page.tsx b/frontend/src/app/(dashboard)/dashboard/page.tsx index f2750c16..59bbcb05 100644 --- a/frontend/src/app/(dashboard)/dashboard/page.tsx +++ b/frontend/src/app/(dashboard)/dashboard/page.tsx @@ -9,14 +9,8 @@ import { ChatInputHandles, } from '@/components/thread/chat-input/chat-input'; import { - initiateAgent, - createThread, - addUserMessage, - startAgent, - createProject, BillingError, } from '@/lib/api'; -import { generateThreadName } from '@/lib/actions/threads'; import { useIsMobile } from '@/hooks/use-mobile'; import { useSidebar } from '@/components/ui/sidebar'; import { Button } from '@/components/ui/button'; @@ -28,11 +22,11 @@ import { import { useBillingError } from '@/hooks/useBillingError'; import { BillingErrorAlert } from '@/components/billing/usage-limit-alert'; import { useAccounts } from '@/hooks/use-accounts'; -import { isLocalMode, config } from '@/lib/config'; -import { toast } from 'sonner'; +import { config } from '@/lib/config'; import { cn } from '@/lib/utils'; +import { useInitiateAgentWithInvalidation } from '@/hooks/react-query/dashboard/use-initiate-agent'; +import { ModalProviders } from '@/providers/modal-providers'; -// Constant for localStorage key to ensure consistency const PENDING_PROMPT_KEY = 'pendingAgentPrompt'; function DashboardContent() { @@ -47,6 +41,7 @@ function DashboardContent() { const { data: accounts } = useAccounts(); const personalAccount = accounts?.find((account) => account.personal_account); const chatInputRef = useRef(null); + const initiateAgentMutation = useInitiateAgentWithInvalidation(); const secondaryGradient = 'bg-gradient-to-r from-blue-500 to-blue-500 bg-clip-text text-transparent'; @@ -73,16 +68,13 @@ function DashboardContent() { const files = chatInputRef.current?.getPendingFiles() || []; localStorage.removeItem(PENDING_PROMPT_KEY); - // Always use FormData for consistency const formData = new FormData(); formData.append('prompt', message); - // Append files if present files.forEach((file, index) => { formData.append('files', file, file.name); }); - // Append options if (options?.model_name) formData.append('model_name', options.model_name); formData.append('enable_thinking', String(options?.enable_thinking ?? false)); formData.append('reasoning_effort', options?.reasoning_effort ?? 'low'); @@ -91,7 +83,7 @@ function DashboardContent() { console.log('FormData content:', Array.from(formData.entries())); - const result = await initiateAgent(formData); + const result = await initiateAgentMutation.mutateAsync(formData); console.log('Agent initiated:', result); if (result.thread_id) { @@ -115,35 +107,26 @@ function DashboardContent() { plan_name: 'Free', }, }); - setIsSubmitting(false); - return; - } - - const isConnectionError = - error instanceof TypeError && error.message.includes('Failed to fetch'); - if (!isLocalMode() || isConnectionError) { - toast.error(error.message || 'An unexpected error occurred'); } + } finally { setIsSubmitting(false); } }; - // Check for pending prompt in localStorage on mount + useEffect(() => { - // Use a small delay to ensure we're fully mounted const timer = setTimeout(() => { const pendingPrompt = localStorage.getItem(PENDING_PROMPT_KEY); if (pendingPrompt) { setInputValue(pendingPrompt); - setAutoSubmit(true); // Flag to auto-submit after mounting + setAutoSubmit(true); } }, 200); return () => clearTimeout(timer); }, []); - // Auto-submit the form if we have a pending prompt useEffect(() => { if (autoSubmit && inputValue && !isSubmitting) { const timer = setTimeout(() => { @@ -156,57 +139,59 @@ function DashboardContent() { }, [autoSubmit, inputValue, isSubmitting]); return ( -
- {isMobile && ( -
- - - - - Open menu - -
- )} + <> + +
+ {isMobile && ( +
+ + + + + Open menu + +
+ )} -
-
-

- Hey -

-

- What would you like Suna to do today? -

+
+
+

+ Hey +

+

+ What would you like Suna to do today? +

+
+ +
-
- - {/* Billing Error Alert */} - -
+ ); } diff --git a/frontend/src/components/billing/payment-required-dialog.tsx b/frontend/src/components/billing/payment-required-dialog.tsx new file mode 100644 index 00000000..5205ce24 --- /dev/null +++ b/frontend/src/components/billing/payment-required-dialog.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Zap } from 'lucide-react'; +import { useModal } from '@/hooks/use-modal-store'; +import { PricingSection } from '../home/sections/pricing-section'; + +const returnUrl = process.env.NEXT_PUBLIC_URL as string; + +export const PaymentRequiredDialog = () => { + const { isOpen, type, onClose } = useModal(); + const isModalOpen = isOpen && type === 'paymentRequiredDialog'; + + return ( + + + + + Upgrade Required + + + You've reached your plan's usage limit. Upgrade to continue enjoying our premium features. + + + +
+
+
+
+
+ +
+
+

Usage Limit Reached

+

+ Your current plan has been exhausted for this billing period. +

+
+
+
+ +
+ +
+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/home/sections/pricing-section.tsx b/frontend/src/components/home/sections/pricing-section.tsx index 133ec7a8..b939bd38 100644 --- a/frontend/src/components/home/sections/pricing-section.tsx +++ b/frontend/src/components/home/sections/pricing-section.tsx @@ -74,6 +74,7 @@ interface PricingTierProps { onSubscriptionUpdate?: () => void; isAuthenticated?: boolean; returnUrl: string; + insideDialog?: boolean; } // Components @@ -170,6 +171,7 @@ function PricingTier({ onSubscriptionUpdate, isAuthenticated = false, returnUrl, + insideDialog = false, }: PricingTierProps) { const [localSelectedPlan, setLocalSelectedPlan] = useState( selectedPlan || DEFAULT_SELECTED_PLAN, @@ -500,10 +502,10 @@ function PricingTier({
@@ -601,11 +603,15 @@ function PricingTier({ interface PricingSectionProps { returnUrl?: string; showTitleAndTabs?: boolean; + hideFree?: boolean; + insideDialog?: boolean; } export function PricingSection({ returnUrl = typeof window !== 'undefined' ? window.location.href : '/', showTitleAndTabs = true, + hideFree = false, + insideDialog = false }: PricingSectionProps) { const [deploymentType, setDeploymentType] = useState<'cloud' | 'self-hosted'>( 'cloud', @@ -679,7 +685,7 @@ export function PricingSection({ return (
{showTitleAndTabs && ( <> @@ -705,8 +711,15 @@ export function PricingSection({ )} {deploymentType === 'cloud' && ( -
- {siteConfig.cloudPricingItems.map((tier) => ( +
!hideFree || tier.price !== '$0').length > 2) + ? "min-[650px]:grid-cols-2 min-[900px]:grid-cols-3" : "min-[650px]:grid-cols-2" + )}> + {siteConfig.cloudPricingItems.filter((tier) => !hideFree || tier.price !== '$0').map((tier) => ( ))}
diff --git a/frontend/src/components/thread/tool-call-side-panel.tsx b/frontend/src/components/thread/tool-call-side-panel.tsx index f792ae3d..f8350b06 100644 --- a/frontend/src/components/thread/tool-call-side-panel.tsx +++ b/frontend/src/components/thread/tool-call-side-panel.tsx @@ -32,6 +32,7 @@ interface ToolCallSidePanelProps { toolCalls: ToolCallInput[]; currentIndex: number; onNavigate: (newIndex: number) => void; + externalNavigateToIndex?: number; messages?: ApiMessageType[]; agentStatus: string; project?: Project; @@ -46,6 +47,13 @@ interface ToolCallSidePanelProps { isLoading?: boolean; } +interface ToolCallSnapshot { + id: string; + toolCall: ToolCallInput; + index: number; + timestamp: number; +} + export function ToolCallSidePanel({ isOpen, onClose, @@ -56,30 +64,104 @@ export function ToolCallSidePanel({ agentStatus, project, isLoading = false, + externalNavigateToIndex, }: ToolCallSidePanelProps) { - // Move hooks outside of conditional const [dots, setDots] = React.useState(''); - const [showJumpToLive, setShowJumpToLive] = React.useState(false); - const currentToolCall = toolCalls[currentIndex]; - const totalCalls = toolCalls.length; + const [internalIndex, setInternalIndex] = React.useState(0); + const [navigationMode, setNavigationMode] = React.useState<'live' | 'manual'>('live'); + const [toolCallSnapshots, setToolCallSnapshots] = React.useState([]); + const [isInitialized, setIsInitialized] = React.useState(false); + + const isMobile = useIsMobile(); + + React.useEffect(() => { + const newSnapshots = toolCalls.map((toolCall, index) => ({ + id: `${index}-${toolCall.assistantCall.timestamp || Date.now()}`, + toolCall, + index, + timestamp: Date.now(), + })); + + const hadSnapshots = toolCallSnapshots.length > 0; + const hasNewSnapshots = newSnapshots.length > toolCallSnapshots.length; + setToolCallSnapshots(newSnapshots); + + if (!isInitialized && newSnapshots.length > 0) { + setInternalIndex(Math.max(0, newSnapshots.length - 1)); + setIsInitialized(true); + } else if (hasNewSnapshots && navigationMode === 'live') { + setInternalIndex(newSnapshots.length - 1); + } else if (hasNewSnapshots && navigationMode === 'manual') { + } + }, [toolCalls, navigationMode, toolCallSnapshots.length, isInitialized]); + + React.useEffect(() => { + if (isOpen && !isInitialized && toolCallSnapshots.length > 0) { + setInternalIndex(Math.min(currentIndex, toolCallSnapshots.length - 1)); + } + }, [isOpen, currentIndex, isInitialized, toolCallSnapshots.length]); + + const currentSnapshot = toolCallSnapshots[internalIndex]; + const currentToolCall = currentSnapshot?.toolCall; + const totalCalls = toolCallSnapshots.length; + const currentToolName = currentToolCall?.assistantCall?.name || 'Tool Call'; const CurrentToolIcon = getToolIcon( currentToolName === 'Tool Call' ? 'unknown' : currentToolName, ); const isStreaming = currentToolCall?.toolResult?.content === 'STREAMING'; const isSuccess = currentToolCall?.toolResult?.isSuccess ?? true; - const isMobile = useIsMobile(); - // Show jump to live button when agent is running and user is not on the last step - React.useEffect(() => { - if (agentStatus === 'running' && currentIndex + 1 < totalCalls) { - setShowJumpToLive(true); + const internalNavigate = React.useCallback((newIndex: number, source: string = 'internal') => { + if (newIndex < 0 || newIndex >= totalCalls) return; + + const isNavigatingToLatest = newIndex === totalCalls - 1; + + console.log(`[INTERNAL_NAV] ${source}: ${internalIndex} -> ${newIndex}, mode will be: ${isNavigatingToLatest ? 'live' : 'manual'}`); + + setInternalIndex(newIndex); + + if (isNavigatingToLatest) { + setNavigationMode('live'); } else { - setShowJumpToLive(false); + setNavigationMode('manual'); } - }, [agentStatus, currentIndex, totalCalls]); + + if (source === 'user_explicit') { + onNavigate(newIndex); + } + }, [internalIndex, totalCalls, onNavigate]); + + const isLiveMode = navigationMode === 'live'; + const showJumpToLive = navigationMode === 'manual' && agentStatus === 'running'; + const showJumpToLatest = navigationMode === 'manual' && agentStatus !== 'running'; + + const navigateToPrevious = React.useCallback(() => { + if (internalIndex > 0) { + internalNavigate(internalIndex - 1, 'user_explicit'); + } + }, [internalIndex, internalNavigate]); + + const navigateToNext = React.useCallback(() => { + if (internalIndex < totalCalls - 1) { + internalNavigate(internalIndex + 1, 'user_explicit'); + } + }, [internalIndex, totalCalls, internalNavigate]); + + const jumpToLive = React.useCallback(() => { + setNavigationMode('live'); + internalNavigate(totalCalls - 1, 'user_explicit'); + }, [totalCalls, internalNavigate]); + + const jumpToLatest = React.useCallback(() => { + setNavigationMode('manual'); + internalNavigate(totalCalls - 1, 'user_explicit'); + }, [totalCalls, internalNavigate]); + + const handleSliderChange = React.useCallback(([newValue]: [number]) => { + internalNavigate(newValue, 'user_explicit'); + }, [internalNavigate]); - // Add keyboard shortcut for CMD+I to close panel React.useEffect(() => { if (!isOpen) return; @@ -94,10 +176,8 @@ export function ToolCallSidePanel({ return () => window.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose]); - // Listen for sidebar toggle events React.useEffect(() => { if (!isOpen) return; - const handleSidebarToggle = (event: CustomEvent) => { if (event.detail.expanded) { onClose(); @@ -115,6 +195,12 @@ export function ToolCallSidePanel({ ); }, [isOpen, onClose]); + React.useEffect(() => { + if (externalNavigateToIndex !== undefined && externalNavigateToIndex >= 0 && externalNavigateToIndex < totalCalls) { + internalNavigate(externalNavigateToIndex, 'external_click'); + } + }, [externalNavigateToIndex, totalCalls, internalNavigate]); + React.useEffect(() => { if (!isStreaming) return; const interval = setInterval(() => { @@ -127,24 +213,6 @@ export function ToolCallSidePanel({ return () => clearInterval(interval); }, [isStreaming]); - const navigateToPrevious = React.useCallback(() => { - if (currentIndex > 0) { - onNavigate(currentIndex - 1); - } - }, [currentIndex, onNavigate]); - - const navigateToNext = React.useCallback(() => { - if (currentIndex < totalCalls - 1) { - onNavigate(currentIndex + 1); - } - }, [currentIndex, totalCalls, onNavigate]); - - const jumpToLive = React.useCallback(() => { - // Jump to the last step (totalCalls - 1) - onNavigate(totalCalls - 1); - setShowJumpToLive(false); - }, [totalCalls, onNavigate]); - if (!isOpen) return null; if (isLoading) { @@ -250,7 +318,7 @@ export function ToolCallSidePanel({ project={project} messages={messages} agentStatus={agentStatus} - currentIndex={currentIndex} + currentIndex={internalIndex} totalCalls={totalCalls} /> ); @@ -373,15 +441,20 @@ export function ToolCallSidePanel({
- {agentStatus === 'running' && ( + {isLiveMode && agentStatus === 'running' ? (
Live
+ ) : ( +
+
+ Live +
)} - Step {currentIndex + 1} of {totalCalls} + Step {internalIndex + 1} of {totalCalls}
@@ -393,7 +466,7 @@ export function ToolCallSidePanel({ variant="outline" size="sm" onClick={navigateToPrevious} - disabled={currentIndex <= 0} + disabled={internalIndex <= 0} className="h-9 px-3" > @@ -401,15 +474,20 @@ export function ToolCallSidePanel({
- {agentStatus === 'running' && ( + {isLiveMode && agentStatus === 'running' ? (
Live
+ ) : ( +
+
+ Live +
)} - {currentIndex + 1} / {totalCalls} + {internalIndex + 1} / {totalCalls}
@@ -417,7 +495,7 @@ export function ToolCallSidePanel({ variant="outline" size="sm" onClick={navigateToNext} - disabled={currentIndex >= totalCalls - 1} + disabled={internalIndex >= totalCalls - 1} className="h-9 px-3" > Next @@ -431,7 +509,7 @@ export function ToolCallSidePanel({ variant="ghost" size="icon" onClick={navigateToPrevious} - disabled={currentIndex <= 0} + disabled={internalIndex <= 0} className="h-6 w-6 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200" > @@ -440,21 +518,20 @@ export function ToolCallSidePanel({ variant="ghost" size="icon" onClick={navigateToNext} - disabled={currentIndex >= totalCalls - 1} + disabled={internalIndex >= totalCalls - 1} className="h-6 w-6 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200" > -
- +
onNavigate(newValue)} + value={[internalIndex]} + onValueChange={handleSliderChange} className="w-full [&>span:first-child]:h-1 [&>span:first-child]:bg-zinc-200 dark:[&>span:first-child]:bg-zinc-800 [&>span:first-child>span]:bg-zinc-500 dark:[&>span:first-child>span]:bg-zinc-400 [&>span:first-child>span]:h-1" /> @@ -464,16 +541,29 @@ export function ToolCallSidePanel({ -
)} + {showJumpToLatest && ( +
+
+ +
+
+
+ )}
)} diff --git a/frontend/src/components/thread/tool-views/TerminateCommandToolView.tsx b/frontend/src/components/thread/tool-views/TerminateCommandToolView.tsx new file mode 100644 index 00000000..9ce59bd6 --- /dev/null +++ b/frontend/src/components/thread/tool-views/TerminateCommandToolView.tsx @@ -0,0 +1,380 @@ +import React, { useState, useEffect } from 'react'; +import { + Terminal, + CheckCircle, + AlertTriangle, + CircleDashed, + ExternalLink, + Code, + Clock, + ChevronDown, + ChevronUp, + Loader2, + ArrowRight, + TerminalIcon, + Check, + X, + Power, + StopCircle +} from 'lucide-react'; +import { ToolViewProps } from './types'; +import { + extractExitCode, + formatTimestamp, + getToolTitle, +} from './utils'; +import { cn } from '@/lib/utils'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { ScrollArea } from "@/components/ui/scroll-area"; + +export function TerminateCommandToolView({ + name = 'terminate-command', + assistantContent, + toolContent, + assistantTimestamp, + toolTimestamp, + isSuccess = true, + isStreaming = false, +}: ToolViewProps) { + const { resolvedTheme } = useTheme(); + const isDarkTheme = resolvedTheme === 'dark'; + const [progress, setProgress] = useState(0); + const [showFullOutput, setShowFullOutput] = useState(true); + + const rawSessionName = React.useMemo(() => { + if (!assistantContent) return null; + + try { + const parsed = JSON.parse(assistantContent); + if (parsed.content) { + const sessionMatch = parsed.content.match( + /]*session_name=["']([^"']+)["'][^>]*>/, + ); + if (sessionMatch) { + return sessionMatch[1].trim(); + } + } + } catch (e) { + const sessionMatch = assistantContent.match( + /]*session_name=["']([^"']+)["'][^>]*>/, + ); + if (sessionMatch) { + return sessionMatch[1].trim(); + } + } + + return null; + }, [assistantContent]); + + const sessionName = rawSessionName?.trim(); + + const output = React.useMemo(() => { + if (!toolContent) return null; + + let extractedOutput = ''; + let success = true; + + try { + if (typeof toolContent === 'string') { + // Handle ToolResult format: ToolResult(success=False, output="message") + const toolResultMatch = toolContent.match(/ToolResult\(success=(true|false),\s*output="([^"]+)"\)/i); + if (toolResultMatch) { + success = toolResultMatch[1].toLowerCase() === 'true'; + extractedOutput = toolResultMatch[2]; + } + // Handle other formats + else if (toolContent.includes('ToolResult')) { + const successMatch = toolContent.match(/success=(true|false)/i); + success = successMatch ? successMatch[1].toLowerCase() === 'true' : true; + + //@ts-expect-error IGNORE + const outputMatch = toolContent.match(/output=['"](.*)['"]/s); + if (outputMatch && outputMatch[1]) { + extractedOutput = outputMatch[1] + .replace(/\\n/g, '\n') + .replace(/\\"/g, '"') + .replace(/\\t/g, '\t') + .replace(/\\'/g, "'"); + } else { + extractedOutput = toolContent; + } + } else { + try { + const parsed = JSON.parse(toolContent); + if (parsed.output) { + extractedOutput = parsed.output; + success = parsed.success !== false; + } else if (parsed.content) { + extractedOutput = parsed.content; + } else { + extractedOutput = JSON.stringify(parsed, null, 2); + } + } catch (e) { + extractedOutput = toolContent; + } + } + } else if (typeof toolContent === 'object' && toolContent !== null) { + // Handle case where toolContent is already an object + const typedToolContent = toolContent as Record; + if ('output' in typedToolContent) { + extractedOutput = typedToolContent.output; + success = 'success' in typedToolContent ? !!typedToolContent.success : true; + } else { + extractedOutput = JSON.stringify(toolContent, null, 2); + } + } else { + extractedOutput = String(toolContent); + } + } catch (e) { + extractedOutput = String(toolContent); + console.error('Error parsing tool content:', e); + } + + return extractedOutput; + }, [toolContent]); + + const exitCode = extractExitCode(output); + const toolTitle = getToolTitle(name) || 'Terminate Session'; + + // Determine if the termination was successful based on output content + const terminationSuccess = React.useMemo(() => { + if (!output) return false; + + // Check if the output indicates success or failure + const outputLower = output.toLowerCase(); + if (outputLower.includes('does not exist')) return false; + if (outputLower.includes('terminated') || outputLower.includes('killed')) return true; + + // Check if toolContent contains ToolResult with success=false + if (typeof toolContent === 'string') { + const toolResultMatch = toolContent.match(/ToolResult\(success=(true|false)/i); + if (toolResultMatch) { + return toolResultMatch[1].toLowerCase() === 'true'; + } + } + + // Default to checking the success flag + return isSuccess; + }, [output, isSuccess, toolContent]); + + useEffect(() => { + if (isStreaming) { + const timer = setInterval(() => { + setProgress((prevProgress) => { + if (prevProgress >= 95) { + clearInterval(timer); + return prevProgress; + } + return prevProgress + 5; + }); + }, 300); + return () => clearInterval(timer); + } else { + setProgress(100); + } + }, [isStreaming]); + + const formattedOutput = React.useMemo(() => { + if (!output) return []; + let processedOutput = output; + try { + if (typeof output === 'string' && (output.trim().startsWith('{') || output.trim().startsWith('{'))) { + const parsed = JSON.parse(output); + if (parsed && typeof parsed === 'object' && parsed.output) { + processedOutput = parsed.output; + } + } + } catch (e) { + } + + processedOutput = String(processedOutput); + processedOutput = processedOutput.replace(/\\\\/g, '\\'); + + processedOutput = processedOutput + .replace(/\\n/g, '\n') + .replace(/\\t/g, '\t') + .replace(/\\"/g, '"') + .replace(/\\'/g, "'"); + + processedOutput = processedOutput.replace(/\\u([0-9a-fA-F]{4})/g, (match, group) => { + return String.fromCharCode(parseInt(group, 16)); + }); + return processedOutput.split('\n'); + }, [output]); + + const hasMoreLines = formattedOutput.length > 10; + const previewLines = formattedOutput.slice(0, 10); + const linesToShow = showFullOutput ? formattedOutput : previewLines; + + return ( + + +
+
+
+ +
+
+ + {toolTitle} + +
+
+ + {!isStreaming && ( + + {terminationSuccess ? ( + + ) : ( + + )} + {terminationSuccess ? 'Session terminated' : 'Termination failed'} + + )} +
+
+ + + {isStreaming ? ( +
+
+
+ +
+

+ Terminating session +

+

+ {sessionName || 'Processing termination...'} +

+ +

{progress}%

+
+
+ ) : sessionName ? ( + +
+
+
+ + Session +
+
+ + {sessionName} +
+
+ + {output && ( +
+
+

+ + Result +

+ + {terminationSuccess ? 'Success' : 'Failed'} + +
+ +
+
+
+ + Termination output +
+ {!terminationSuccess && ( + + + Error + + )} +
+
+
+                        {linesToShow.map((line, index) => (
+                          
+ {line || ' '} +
+ ))} + {!showFullOutput && hasMoreLines && ( +
+ + {formattedOutput.length - 10} more lines +
+ )} +
+
+
+
+ )} + + {!output && !isStreaming && ( +
+
+ +

No output received

+
+
+ )} +
+
+ ) : ( +
+
+ +
+

+ No Session Found +

+

+ No session name was detected. Please provide a valid session to terminate. +

+
+ )} +
+ +
+
+ {!isStreaming && sessionName && ( + + + Terminate + + )} +
+ +
+ + {toolTimestamp && !isStreaming + ? formatTimestamp(toolTimestamp) + : assistantTimestamp + ? formatTimestamp(assistantTimestamp) + : ''} +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx index 30a210cb..1f03dc69 100644 --- a/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx +++ b/frontend/src/components/thread/tool-views/wrapper/ToolViewRegistry.tsx @@ -11,6 +11,7 @@ import { WebCrawlToolView } from '../WebCrawlToolView'; import { WebScrapeToolView } from '../WebScrapeToolView'; import { WebSearchToolView } from '../WebSearchToolView'; import { SeeImageToolView } from '../SeeImageToolView'; +import { TerminateCommandToolView } from '../TerminateCommandToolView'; export type ToolViewComponent = React.ComponentType; @@ -34,6 +35,7 @@ const defaultRegistry: ToolViewRegistryType = { 'browser-click-coordinates': BrowserToolView, 'execute-command': CommandToolView, + 'terminate-command': TerminateCommandToolView, 'create-file': FileOperationToolView, 'delete-file': FileOperationToolView, diff --git a/frontend/src/components/thread/utils.ts b/frontend/src/components/thread/utils.ts index badab7c2..3ba2e501 100644 --- a/frontend/src/components/thread/utils.ts +++ b/frontend/src/components/thread/utils.ts @@ -63,6 +63,9 @@ export const getToolIcon = (toolName: string): ElementType => { // Shell commands case 'execute-command': return Terminal; + case 'terminate-command': + return Terminal; + // Web operations case 'web-search': @@ -210,6 +213,7 @@ export const extractPrimaryParam = ( const TOOL_DISPLAY_NAMES = new Map([ ['execute-command', 'Executing Command'], + ['terminate-command', 'Terminating Command'], ['create-file', 'Creating File'], ['delete-file', 'Deleting File'], ['full-file-rewrite', 'Rewriting File'], diff --git a/frontend/src/hooks/react-query/dashboard/keys.ts b/frontend/src/hooks/react-query/dashboard/keys.ts new file mode 100644 index 00000000..a8ab9966 --- /dev/null +++ b/frontend/src/hooks/react-query/dashboard/keys.ts @@ -0,0 +1,7 @@ +import { createQueryKeys } from '@/hooks/use-query'; + +export const dashboardKeys = createQueryKeys({ + all: ['dashboard'] as const, + agents: ['dashboard', 'agents'] as const, + initiateAgent: () => [...dashboardKeys.agents, 'initiate'] as const, +}); diff --git a/frontend/src/hooks/react-query/dashboard/use-initiate-agent.ts b/frontend/src/hooks/react-query/dashboard/use-initiate-agent.ts new file mode 100644 index 00000000..e0a2251d --- /dev/null +++ b/frontend/src/hooks/react-query/dashboard/use-initiate-agent.ts @@ -0,0 +1,48 @@ +'use client'; + +import { initiateAgent, InitiateAgentResponse } from "@/lib/api"; +import { createMutationHook } from "@/hooks/use-query"; +import { toast } from "sonner"; +import { dashboardKeys } from "./keys"; +import { useQueryClient } from "@tanstack/react-query"; +import { useModal } from "@/hooks/use-modal-store"; + +export const useInitiateAgentMutation = createMutationHook< + InitiateAgentResponse, + FormData +>( + initiateAgent, + { + onSuccess: (data) => { + toast.success("Agent initiated successfully"); + }, + onError: (error) => { + if (error instanceof Error) { + if (error.message.includes("Cannot connect to backend server")) { + toast.error("Connection error: Please check your internet connection and ensure the backend server is running"); + } else if (error.message.includes("No access token available")) { + toast.error("Authentication error: Please sign in again"); + } else { + toast.error(`Failed to initiate agent: ${error.message}`); + } + } else { + toast.error("An unexpected error occurred while initiating the agent"); + } + } + } +); + +export const useInitiateAgentWithInvalidation = () => { + const queryClient = useQueryClient(); + const { onOpen } = useModal(); + return useInitiateAgentMutation({ + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: dashboardKeys.agents }); + }, + onError: (error) => { + if (error instanceof Error && error.message.includes("Payment Required")) { + onOpen("paymentRequiredDialog"); + } + } + }); +}; diff --git a/frontend/src/hooks/react-query/dashboard/utils.ts b/frontend/src/hooks/react-query/dashboard/utils.ts new file mode 100644 index 00000000..5bcdb033 --- /dev/null +++ b/frontend/src/hooks/react-query/dashboard/utils.ts @@ -0,0 +1 @@ +// This file is now empty as the initiateAgent function has been moved to use-initiate-agent.ts \ No newline at end of file diff --git a/frontend/src/hooks/use-modal-store.ts b/frontend/src/hooks/use-modal-store.ts new file mode 100644 index 00000000..2c8cf835 --- /dev/null +++ b/frontend/src/hooks/use-modal-store.ts @@ -0,0 +1,16 @@ +import { create } from "zustand"; +export type ModalType = "paymentRequiredDialog" + +interface ModalStore { + type: ModalType | null; + isOpen: boolean; + onOpen: (type: ModalType) => void; + onClose: () => void; +} + +export const useModal = create((set) => ({ + type: null, + isOpen: false, + onOpen: (type) => set({ type, isOpen: true }), + onClose: () => set({ type: null, isOpen: false }), +})); \ No newline at end of file diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index da072b9f..8c618c39 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1437,11 +1437,9 @@ export const initiateAgent = async ( const response = await fetch(`${API_URL}/agent/initiate`, { method: 'POST', headers: { - // Note: Don't set Content-Type for FormData Authorization: `Bearer ${session.access_token}`, }, body: formData, - // Add cache: 'no-store' to prevent caching cache: 'no-store', }); diff --git a/frontend/src/providers/modal-providers.tsx b/frontend/src/providers/modal-providers.tsx new file mode 100644 index 00000000..bd47b7c9 --- /dev/null +++ b/frontend/src/providers/modal-providers.tsx @@ -0,0 +1,9 @@ +import { PaymentRequiredDialog } from "@/components/billing/payment-required-dialog" + +export const ModalProviders = () => { + return ( + <> + + + ) +} From 7076d917d4ffec77616af81e7cbd2f806716cef2 Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Fri, 23 May 2025 13:25:35 +0530 Subject: [PATCH 2/2] chore(ui): payment dialog + live indicator fix --- .../src/components/thread/tool-views/WebScrapeToolView.tsx | 1 + .../src/components/thread/tool-views/WebSearchToolView.tsx | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/thread/tool-views/WebScrapeToolView.tsx b/frontend/src/components/thread/tool-views/WebScrapeToolView.tsx index 09f64a1c..19bb4e2e 100644 --- a/frontend/src/components/thread/tool-views/WebScrapeToolView.tsx +++ b/frontend/src/components/thread/tool-views/WebScrapeToolView.tsx @@ -34,6 +34,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp function truncateText(text: string, maxLength: number = 40) { return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; } + export function WebScrapeToolView({ name = 'scrape-webpage', assistantContent, diff --git a/frontend/src/components/thread/tool-views/WebSearchToolView.tsx b/frontend/src/components/thread/tool-views/WebSearchToolView.tsx index 73fc79a8..87bf645a 100644 --- a/frontend/src/components/thread/tool-views/WebSearchToolView.tsx +++ b/frontend/src/components/thread/tool-views/WebSearchToolView.tsx @@ -32,6 +32,10 @@ import { Progress } from '@/components/ui/progress'; import { ScrollArea } from "@/components/ui/scroll-area"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +function truncateText(text: string, maxLength: number = 70) { + return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; +} + export function WebSearchToolView({ name = 'web-search', assistantContent, @@ -278,7 +282,7 @@ export function WebSearchToolView({
- {cleanUrl(result.url)} + {truncateText(cleanUrl(result.url))}