From 3e030d4c18b75e58f9e5f1dfb0f1e754b62bd32a Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Thu, 15 May 2025 12:04:53 +0530 Subject: [PATCH] chore(dev): frontend refactor - started with thread page --- .../(dashboard)/agents/[threadId]/page.tsx | 313 +++++++++--------- .../src/hooks/react-query/threads/keys.ts | 10 + .../react-query/threads/use-agent-run.ts | 39 +++ .../react-query/threads/use-billing-status.ts | 14 + .../hooks/react-query/threads/use-messages.ts | 24 ++ .../hooks/react-query/threads/use-project.ts | 27 ++ .../hooks/react-query/threads/use-threads.ts | 13 + 7 files changed, 285 insertions(+), 155 deletions(-) create mode 100644 frontend/src/hooks/react-query/threads/keys.ts create mode 100644 frontend/src/hooks/react-query/threads/use-agent-run.ts create mode 100644 frontend/src/hooks/react-query/threads/use-billing-status.ts create mode 100644 frontend/src/hooks/react-query/threads/use-messages.ts create mode 100644 frontend/src/hooks/react-query/threads/use-project.ts create mode 100644 frontend/src/hooks/react-query/threads/use-threads.ts diff --git a/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx b/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx index 0ed147e8..336af2a9 100644 --- a/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/agents/[threadId]/page.tsx @@ -3,37 +3,19 @@ import React, { useCallback, useEffect, - useMemo, useRef, useState, } from 'react'; -import Image from 'next/image'; import { useRouter, useSearchParams } from 'next/navigation'; import { - ArrowDown, - CheckCircle, - CircleDashed, AlertTriangle, - Info, - File, - ChevronRight, } from 'lucide-react'; import { - addUserMessage, - startAgent, - stopAgent, - getAgentRuns, - getMessages, - getProject, - getThread, - updateProject, + BillingError, Project, Message as BaseApiMessageType, - BillingError, - checkBillingStatus, } from '@/lib/api'; import { toast } from 'sonner'; -import { Skeleton } from '@/components/ui/skeleton'; import { ChatInput } from '@/components/thread/chat-input/chat-input'; import { FileViewerModal } from '@/components/thread/file-viewer-modal'; import { SiteHeader } from '@/components/thread/thread-site-header'; @@ -58,7 +40,11 @@ import { import { safeJsonParse, } from '@/components/thread/utils'; - +import { useThreadQuery } from '@/hooks/react-query/threads/use-threads'; +import { useAddUserMessageMutation, useMessagesQuery } from '@/hooks/react-query/threads/use-messages'; +import { useProjectQuery } from '@/hooks/react-query/threads/use-project'; +import { useAgentRunsQuery, useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run'; +import { useBillingStatusQuery } from '@/hooks/react-query/threads/use-billing-status'; // Extend the base Message type with the expected database fields interface ApiMessageType extends BaseApiMessageType { @@ -136,6 +122,18 @@ export default function ThreadPage({ // Add debug mode state - check for debug=true in URL const [debugMode, setDebugMode] = useState(false); + const threadQuery = useThreadQuery(threadId); + const messagesQuery = useMessagesQuery(threadId); + const projectId = threadQuery.data?.project_id || ''; + const projectQuery = useProjectQuery(projectId); + const agentRunsQuery = useAgentRunsQuery(threadId); + const billingStatusQuery = useBillingStatusQuery(); + + const addUserMessageMutation = useAddUserMessageMutation(); + const startAgentMutation = useStartAgentMutation(); + const stopAgentMutation = useStopAgentMutation(); + + const handleProjectRenamed = useCallback((newName: string) => { setProjectName(newName); }, []); @@ -356,89 +354,86 @@ export default function ThreadPage({ messagesEndRef.current?.scrollIntoView({ behavior }); }; + // Effect to load initial data using React Query useEffect(() => { let isMounted = true; - async function loadData() { + async function initializeData() { if (!initialLoadCompleted.current) setIsLoading(true); setError(null); try { if (!threadId) throw new Error('Thread ID is required'); - const threadData = await getThread(threadId).catch((err) => { - throw new Error('Failed to load thread data: ' + err.message); - }); + // Check if we have thread data + if (threadQuery.isError) { + throw new Error('Failed to load thread data: ' + threadQuery.error); + } if (!isMounted) return; - if (threadData?.project_id) { - const projectData = await getProject(threadData.project_id); - if (isMounted && projectData) { - // Set project data - setProject(projectData); + // Process project data when available + if (projectQuery.data) { + // Set project data + setProject(projectQuery.data); - // Make sure sandbox ID is set correctly - if (typeof projectData.sandbox === 'string') { - setSandboxId(projectData.sandbox); - } else if (projectData.sandbox?.id) { - setSandboxId(projectData.sandbox.id); - } + // Make sure sandbox ID is set correctly + if (typeof projectQuery.data.sandbox === 'string') { + setSandboxId(projectQuery.data.sandbox); + } else if (projectQuery.data.sandbox?.id) { + setSandboxId(projectQuery.data.sandbox.id); + } - setProjectName(projectData.name || ''); + setProjectName(projectQuery.data.name || ''); + } + + // Process messages data when available + if (messagesQuery.data && !messagesLoadedRef.current) { + // Map API message type to UnifiedMessage type + const unifiedMessages = (messagesQuery.data || []) + .filter((msg) => msg.type !== 'status') + .map((msg: ApiMessageType) => ({ + message_id: msg.message_id || null, + thread_id: msg.thread_id || threadId, + type: (msg.type || 'system') as UnifiedMessage['type'], + is_llm_message: Boolean(msg.is_llm_message), + content: msg.content || '', + metadata: msg.metadata || '{}', + created_at: msg.created_at || new Date().toISOString(), + updated_at: msg.updated_at || new Date().toISOString(), + })); + + setMessages(unifiedMessages); + console.log('[PAGE] Loaded Messages (excluding status, keeping browser_state):', unifiedMessages.length); + messagesLoadedRef.current = true; + + if (!hasInitiallyScrolled.current) { + scrollToBottom('auto'); + hasInitiallyScrolled.current = true; } } - if (!messagesLoadedRef.current) { - const messagesData = await getMessages(threadId); - if (isMounted) { - // Map API message type to UnifiedMessage type - const unifiedMessages = (messagesData || []) - .filter((msg) => msg.type !== 'status') - .map((msg: ApiMessageType) => ({ - message_id: msg.message_id || null, - thread_id: msg.thread_id || threadId, - type: (msg.type || 'system') as UnifiedMessage['type'], - is_llm_message: Boolean(msg.is_llm_message), - content: msg.content || '', - metadata: msg.metadata || '{}', - created_at: msg.created_at || new Date().toISOString(), - updated_at: msg.updated_at || new Date().toISOString(), - })); + // Check for active agent runs + if (agentRunsQuery.data && !agentRunsCheckedRef.current && isMounted) { + console.log('[PAGE] Checking for active agent runs...'); + agentRunsCheckedRef.current = true; - setMessages(unifiedMessages); - console.log('[PAGE] Loaded Messages (excluding status, keeping browser_state):', unifiedMessages.length); - messagesLoadedRef.current = true; - - if (!hasInitiallyScrolled.current) { - scrollToBottom('auto'); - hasInitiallyScrolled.current = true; - } - } - } - - if (!agentRunsCheckedRef.current && isMounted) { - try { - console.log('[PAGE] Checking for active agent runs...'); - const agentRuns = await getAgentRuns(threadId); - agentRunsCheckedRef.current = true; - - const activeRun = agentRuns.find((run) => run.status === 'running'); - if (activeRun && isMounted) { - console.log('[PAGE] Found active run on load:', activeRun.id); - setAgentRunId(activeRun.id); - } else { - console.log('[PAGE] No active agent runs found'); - if (isMounted) setAgentStatus('idle'); - } - } catch (err) { - console.error('[PAGE] Error checking for active runs:', err); - agentRunsCheckedRef.current = true; + const activeRun = agentRunsQuery.data.find((run) => run.status === 'running'); + if (activeRun && isMounted) { + console.log('[PAGE] Found active run on load:', activeRun.id); + setAgentRunId(activeRun.id); + } else { + console.log('[PAGE] No active agent runs found'); if (isMounted) setAgentStatus('idle'); } } - initialLoadCompleted.current = true; + // Mark initialization as complete when we have the core data + if (threadQuery.data && messagesQuery.data && agentRunsQuery.data) { + initialLoadCompleted.current = true; + setIsLoading(false); + } + } catch (err) { console.error('Error loading thread data:', err); if (isMounted) { @@ -446,18 +441,27 @@ export default function ThreadPage({ err instanceof Error ? err.message : 'Failed to load thread'; setError(errorMessage); toast.error(errorMessage); + setIsLoading(false); } - } finally { - if (isMounted) setIsLoading(false); } } - loadData(); + if (threadId) { + initializeData(); + } return () => { isMounted = false; }; - }, [threadId]); + }, [ + threadId, + threadQuery.data, + threadQuery.isError, + threadQuery.error, + projectQuery.data, + messagesQuery.data, + agentRunsQuery.data + ]); const handleSubmitMessage = useCallback( async ( @@ -483,10 +487,18 @@ export default function ThreadPage({ scrollToBottom('smooth'); try { - const results = await Promise.allSettled([ - addUserMessage(threadId, message), - startAgent(threadId, options), - ]); + // Use React Query mutations instead of direct API calls + const messagePromise = addUserMessageMutation.mutateAsync({ + threadId, + message + }); + + const agentPromise = startAgentMutation.mutateAsync({ + threadId, + options + }); + + const results = await Promise.allSettled([messagePromise, agentPromise]); // Handle failure to add the user message if (results[0].status === 'rejected') { @@ -525,6 +537,11 @@ export default function ThreadPage({ // If agent started successfully const agentResult = results[1].value; setAgentRunId(agentResult.agent_run_id); + + // Refresh queries after successful operations + messagesQuery.refetch(); + agentRunsQuery.refetch(); + } catch (err) { // Catch errors from addUserMessage or non-BillingError agent start errors console.error('Error sending message or starting agent:', err); @@ -540,8 +557,8 @@ export default function ThreadPage({ setIsSending(false); } }, - [threadId, project?.account_id], - ); // Ensure project.account_id is a dependency + [threadId, project?.account_id, addUserMessageMutation, startAgentMutation, messagesQuery, agentRunsQuery], + ); const handleStopAgent = useCallback(async () => { console.log(`[PAGE] Requesting agent stop via hook.`); @@ -549,11 +566,18 @@ export default function ThreadPage({ // First stop the streaming and let the hook handle refetching await stopStreaming(); - - // We don't need to refetch messages here since the hook will do that - // The centralizing of refetching in the hook simplifies this logic - }, [stopStreaming]); - + + // Use React Query's stopAgentMutation if we have an agent run ID + if (agentRunId) { + try { + await stopAgentMutation.mutateAsync(agentRunId); + // Refresh agent runs after stopping + agentRunsQuery.refetch(); + } catch (error) { + console.error('Error stopping agent:', error); + } + } + }, [stopStreaming, agentRunId, stopAgentMutation, agentRunsQuery]); useEffect(() => { const lastMsg = messages[messages.length - 1]; @@ -561,7 +585,7 @@ export default function ThreadPage({ if ((isNewUserMessage || agentStatus === 'running') && !userHasScrolled) { scrollToBottom('smooth'); } - }, [messages, agentStatus, userHasScrolled, scrollToBottom]); + }, [messages, agentStatus, userHasScrolled]); useEffect(() => { if (!latestMessageRef.current || messages.length === 0) return; @@ -571,7 +595,7 @@ export default function ThreadPage({ ); observer.observe(latestMessageRef.current); return () => observer.disconnect(); - }, [messages, streamingTextContent, streamingToolCall, setShowScrollButton]); + }, [messages, streamingTextContent, streamingToolCall]); useEffect(() => { console.log(`[PAGE] 🔄 Page AgentStatus: ${agentStatus}, Hook Status: ${streamHookStatus}, Target RunID: ${agentRunId || 'none'}, Hook RunID: ${currentHookRunId || 'none'}`); @@ -930,57 +954,34 @@ export default function ThreadPage({ } }, [projectName]); - // Add another useEffect to ensure messages are refreshed when agent status changes to idle + // Update messages when they change in the query useEffect(() => { - if ( - agentStatus === 'idle' && - streamHookStatus !== 'streaming' && - streamHookStatus !== 'connecting' - ) { - console.log( - '[PAGE] Agent status changed to idle, ensuring messages are up to date', - ); - // Only do this if we're not in the initial loading state - if (!isLoading && initialLoadCompleted.current) { - // Double-check messages after a short delay to ensure we have latest content - const timer = setTimeout(() => { - getMessages(threadId) - .then((messagesData) => { - if (messagesData) { - console.log( - `[PAGE] Backup refetch completed with ${messagesData.length} messages`, - ); - // Map API message type to UnifiedMessage type - const unifiedMessages = (messagesData || []) - .filter((msg) => msg.type !== 'status') - .map((msg: ApiMessageType) => ({ - message_id: msg.message_id || null, - thread_id: msg.thread_id || threadId, - type: (msg.type || 'system') as UnifiedMessage['type'], - is_llm_message: Boolean(msg.is_llm_message), - content: msg.content || '', - metadata: msg.metadata || '{}', - created_at: msg.created_at || new Date().toISOString(), - updated_at: msg.updated_at || new Date().toISOString(), - })); + if (messagesQuery.data && messagesQuery.status === 'success') { + // Only update if we're not in initial loading and the agent isn't running + if (!isLoading && agentStatus !== 'running' && agentStatus !== 'connecting') { + // Map API message type to UnifiedMessage type + const unifiedMessages = (messagesQuery.data || []) + .filter((msg) => msg.type !== 'status') + .map((msg: ApiMessageType) => ({ + message_id: msg.message_id || null, + thread_id: msg.thread_id || threadId, + type: (msg.type || 'system') as UnifiedMessage['type'], + is_llm_message: Boolean(msg.is_llm_message), + content: msg.content || '', + metadata: msg.metadata || '{}', + created_at: msg.created_at || new Date().toISOString(), + updated_at: msg.updated_at || new Date().toISOString(), + })); - setMessages(unifiedMessages); - // Reset auto-opened panel to allow tool detection with fresh messages - setAutoOpenedPanel(false); - scrollToBottom('smooth'); - } - }) - .catch((err) => { - console.error('Error in backup message refetch:', err); - }); - }, 1000); - - return () => clearTimeout(timer); + setMessages(unifiedMessages); + // Reset auto-opened panel to allow tool detection with fresh messages + setAutoOpenedPanel(false); + scrollToBottom('smooth'); } } - }, [agentStatus, threadId, isLoading, streamHookStatus]); + }, [messagesQuery.data, messagesQuery.status, isLoading, agentStatus, threadId]); - // Update the checkBillingStatus function + // Check billing status and handle billing limit const checkBillingLimits = useCallback(async () => { // Skip billing checks in local development mode if (isLocalMode()) { @@ -991,9 +992,11 @@ export default function ThreadPage({ } try { - const result = await checkBillingStatus(); + // Use React Query to get billing status + await billingStatusQuery.refetch(); + const result = billingStatusQuery.data; - if (!result.can_run) { + if (result && !result.can_run) { setBillingData({ currentUsage: result.subscription?.minutes_limit || 0, limit: result.subscription?.minutes_limit || 0, @@ -1008,9 +1011,9 @@ export default function ThreadPage({ console.error('Error checking billing status:', err); return false; } - }, [project?.account_id]); + }, [project?.account_id, billingStatusQuery]); - // Update useEffect to use the renamed function + // Check billing when agent status changes useEffect(() => { const previousStatus = previousAgentStatus.current; @@ -1023,7 +1026,7 @@ export default function ThreadPage({ previousAgentStatus.current = agentStatus; }, [agentStatus, checkBillingLimits]); - // Update other useEffect to use the renamed function + // Check billing on initial load useEffect(() => { if (project?.account_id && initialLoadCompleted.current) { console.log('Checking billing status on page load'); @@ -1031,7 +1034,7 @@ export default function ThreadPage({ } }, [project?.account_id, checkBillingLimits, initialLoadCompleted]); - // Update the last useEffect to use the renamed function + // Check billing after messages loaded useEffect(() => { if (messagesLoadedRef.current && project?.account_id && !isLoading) { console.log('Checking billing status after messages loaded'); @@ -1085,10 +1088,10 @@ export default function ThreadPage({ ? 'This thread either does not exist or you do not have access to it.' : error } -

- - - +

+ + + { @@ -1128,7 +1131,7 @@ export default function ThreadPage({ onDismiss={() => setShowBillingAlert(false)} isOpen={showBillingAlert} /> - + ); } else { return ( diff --git a/frontend/src/hooks/react-query/threads/keys.ts b/frontend/src/hooks/react-query/threads/keys.ts new file mode 100644 index 00000000..ae0564b3 --- /dev/null +++ b/frontend/src/hooks/react-query/threads/keys.ts @@ -0,0 +1,10 @@ +import { createQueryKeys } from "@/hooks/use-query"; + +export const threadKeys = createQueryKeys({ + all: ['threads'] as const, + details: (threadId: string) => ['thread', threadId] as const, + messages: (threadId: string) => ['thread', threadId, 'messages'] as const, + project: (projectId: string) => ['project', projectId] as const, + agentRuns: (threadId: string) => ['thread', threadId, 'agent-runs'] as const, + billingStatus: ['billing', 'status'] as const, +}); \ No newline at end of file diff --git a/frontend/src/hooks/react-query/threads/use-agent-run.ts b/frontend/src/hooks/react-query/threads/use-agent-run.ts new file mode 100644 index 00000000..cb310343 --- /dev/null +++ b/frontend/src/hooks/react-query/threads/use-agent-run.ts @@ -0,0 +1,39 @@ +import { createMutationHook, createQueryHook } from "@/hooks/use-query"; +import { threadKeys } from "./keys"; +import { BillingError, getAgentRuns, startAgent, stopAgent } from "@/lib/api"; + +export const useAgentRunsQuery = (threadId: string) => + createQueryHook( + threadKeys.agentRuns(threadId), + () => getAgentRuns(threadId), + { + enabled: !!threadId, + retry: 1, + } + )(); + +export const useStartAgentMutation = () => + createMutationHook( + ({ + threadId, + options, + }: { + threadId: string; + options?: { + model_name?: string; + enable_thinking?: boolean; + reasoning_effort?: string; + stream?: boolean; + }; + }) => startAgent(threadId, options), + { + onError: (error) => { + if (!(error instanceof BillingError)) { + throw error; + } + }, + } + )(); + +export const useStopAgentMutation = () => + createMutationHook((agentRunId: string) => stopAgent(agentRunId))(); diff --git a/frontend/src/hooks/react-query/threads/use-billing-status.ts b/frontend/src/hooks/react-query/threads/use-billing-status.ts new file mode 100644 index 00000000..a310784b --- /dev/null +++ b/frontend/src/hooks/react-query/threads/use-billing-status.ts @@ -0,0 +1,14 @@ +import { createQueryHook } from "@/hooks/use-query"; +import { threadKeys } from "./keys"; +import { checkBillingStatus } from "@/lib/api"; + +export const useBillingStatusQuery = (enabled = true) => + createQueryHook( + threadKeys.billingStatus, + () => checkBillingStatus(), + { + enabled, + retry: 1, + staleTime: 1000 * 60 * 5, + } + )(); diff --git a/frontend/src/hooks/react-query/threads/use-messages.ts b/frontend/src/hooks/react-query/threads/use-messages.ts new file mode 100644 index 00000000..c75029c1 --- /dev/null +++ b/frontend/src/hooks/react-query/threads/use-messages.ts @@ -0,0 +1,24 @@ +import { createMutationHook, createQueryHook } from "@/hooks/use-query"; +import { threadKeys } from "./keys"; +import { addUserMessage, getMessages } from "@/lib/api"; + +export const useMessagesQuery = (threadId: string) => + createQueryHook( + threadKeys.messages(threadId), + () => getMessages(threadId), + { + enabled: !!threadId, + retry: 1, + } + )(); + +export const useAddUserMessageMutation = () => + createMutationHook( + ({ + threadId, + message, + }: { + threadId: string; + message: string; + }) => addUserMessage(threadId, message) + )(); diff --git a/frontend/src/hooks/react-query/threads/use-project.ts b/frontend/src/hooks/react-query/threads/use-project.ts new file mode 100644 index 00000000..4cdafffd --- /dev/null +++ b/frontend/src/hooks/react-query/threads/use-project.ts @@ -0,0 +1,27 @@ +import { createMutationHook, createQueryHook } from "@/hooks/use-query"; +import { threadKeys } from "./keys"; +import { getProject, Project, updateProject } from "@/lib/api"; + +export const useProjectQuery = (projectId: string | undefined) => + createQueryHook( + threadKeys.project(projectId || ""), + () => + projectId + ? getProject(projectId) + : Promise.reject("No project ID"), + { + enabled: !!projectId, + retry: 1, + } + )(); + +export const useUpdateProjectMutation = () => + createMutationHook( + ({ + projectId, + data, + }: { + projectId: string; + data: Partial; + }) => updateProject(projectId, data) + )(); diff --git a/frontend/src/hooks/react-query/threads/use-threads.ts b/frontend/src/hooks/react-query/threads/use-threads.ts new file mode 100644 index 00000000..ffe6c339 --- /dev/null +++ b/frontend/src/hooks/react-query/threads/use-threads.ts @@ -0,0 +1,13 @@ +import { createQueryHook } from "@/hooks/use-query"; +import { threadKeys } from "./keys"; +import { getThread } from "@/lib/api"; + +export const useThreadQuery = (threadId: string) => + createQueryHook( + threadKeys.details(threadId), + () => getThread(threadId), + { + enabled: !!threadId, + retry: 1, + } + )();