diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index dd4c7376..1debbdc9 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -119,7 +119,11 @@ export default function ChatBox(): JSX.Element { const navigate = useNavigate(); - const handleSend = async (messageStr?: string, taskId?: string) => { + const handleSend = async ( + messageStr?: string, + taskId?: string, + executionId?: string + ) => { const _taskId = taskId || chatStore.activeTaskId; if (message.trim() === '' && !messageStr) return; const tempMessageContent = messageStr || message; @@ -254,7 +258,8 @@ export default function ChatBox(): JSX.Element { undefined, undefined, tempMessageContent, - attachesToSend + attachesToSend, + executionId ); } catch (err: any) { console.error('Failed to start task:', err); @@ -274,6 +279,7 @@ export default function ChatBox(): JSX.Element { //Generate nextId in case new chatStore is created to sync with the backend beforehand const nextTaskId = generateUniqueId(); chatStore.setNextTaskId(nextTaskId); + chatStore.setNextExecutionId(taskId as string, executionId); // Use improve endpoint (POST /chat/{id}) - {id} is project_id // This reuses the existing SSE connection and step_solve loop @@ -329,7 +335,8 @@ export default function ChatBox(): JSX.Element { undefined, undefined, tempMessageContent, - attachesToSend + attachesToSend, + executionId ); chatStore.setHasWaitComfirm(_taskId as string, true); } catch (err: any) { @@ -784,7 +791,7 @@ export default function ChatBox(): JSX.Element { // Dequeue the task from triggerTaskStore (marks as running) taskToProcess = triggerTaskStoreState.dequeueTask(); if (!taskToProcess) return; - + // Skip if we've already started processing this trigger task if (processedTriggerTaskRef.current === taskToProcess.id) { console.log( @@ -803,8 +810,7 @@ export default function ChatBox(): JSX.Element { // Mark this trigger task as being processed to prevent duplicate sends processedTriggerTaskRef.current = taskToProcess.id; - // Register execution mapping for this task - // The executionId from the queued task will be used for tracking + // Register execution mapping for this task (for external tracking) if (taskToProcess.executionId && projectStore.activeProjectId) { const targetTaskId = chatStore.nextTaskId || taskId; @@ -817,10 +823,10 @@ export default function ChatBox(): JSX.Element { } // Process the queued message via handleSend - // Use the formattedMessage from the trigger task + // Pass executionId so startTask can set it on the new task const messageContent = taskToProcess.formattedMessage || taskToProcess.taskPrompt; - handleSend(messageContent); + handleSend(messageContent, undefined, taskToProcess.executionId); }, [ projectStore.activeProjectId, triggerTaskStoreState.taskQueue, @@ -1088,7 +1094,7 @@ export default function ChatBox(): JSX.Element { href="https://www.eigent.ai/terms-of-use" target="_blank" className="text-text-information underline" - onClick={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} rel="noreferrer" > {t('layout.terms-of-use')} {' '} @@ -1097,7 +1103,7 @@ export default function ChatBox(): JSX.Element { href="https://www.eigent.ai/privacy-policy" target="_blank" className="text-text-information underline" - onClick={(e) => e.stopPropagation()} + onClick={(e) => e.stopPropagation()} rel="noreferrer" > {t('layout.privacy-policy')} diff --git a/src/hooks/useTriggerTaskExecutor.ts b/src/hooks/useTriggerTaskExecutor.ts index ca8e8aea..5a292513 100644 --- a/src/hooks/useTriggerTaskExecutor.ts +++ b/src/hooks/useTriggerTaskExecutor.ts @@ -1,5 +1,5 @@ import { proxyFetchGet } from '@/api/http'; -import { useProjectStore } from '@/store/projectStore'; +import { ProjectType, useProjectStore } from '@/store/projectStore'; import { useTriggerStore } from '@/store/triggerStore'; import { TriggeredTask, @@ -87,7 +87,14 @@ export function useTriggerTaskExecutor() { }); // Use replayProject to load the project from history - store.replayProject(taskIdsList, question, projectId, historyId); + // store.replayProject(taskIdsList, question, projectId, historyId); + store.createProject( + `Trigger Project ${question}`, + `No tasks to replay`, + projectId, + ProjectType.NORMAL, + historyId + ); return true; } catch (error) { diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index b30c2d53..0e60825a 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -26,6 +26,7 @@ import { import { showCreditsToast } from '@/components/Toast/creditsToast'; import { showStorageToast } from '@/components/Toast/storageToast'; import { generateUniqueId, uploadLog } from '@/lib'; +import { proxyUpdateTriggerExecution } from '@/service/triggerApi'; import { ExecutionStatus } from '@/types'; import { AgentMessageStatus, @@ -77,6 +78,9 @@ interface Task { isContextExceeded?: boolean; // Streaming decompose text - stored separately to avoid frequent re-renders streamingDecomposeText: string; + // Trigger execution ID for tracking trigger task completion + executionId?: string; + nextExecutionId?: string; } export interface ChatStore { @@ -96,7 +100,8 @@ export interface ChatStore { shareToken?: string, delayTime?: number, messageContent?: string, - messageAttaches?: File[] + messageAttaches?: File[], + executionId?: string ) => Promise; handleConfirmTask: ( project_id: string, @@ -170,6 +175,11 @@ export interface ChatStore { setNextTaskId: (taskId: string | null) => void; setStreamingDecomposeText: (taskId: string, text: string) => void; clearStreamingDecomposeText: (taskId: string) => void; + setExecutionId: (taskId: string, executionId: string | undefined) => void; + setNextExecutionId: ( + taskId: string, + nextExecutionId: string | undefined + ) => void; } export type VanillaChatStore = { @@ -228,23 +238,95 @@ const ttftTracking: Record< { confirmedAt: number; firstTokenLogged: boolean } > = {}; -// Helper function to update trigger execution status using triggerTaskStore +// Track which executionIds have already been reported to prevent duplicate updates +const reportedExecutionIds = new Set(); + +// Helper function to update trigger execution status using executionId from task const updateTriggerExecutionStatus = async ( - _projectStore: any, - _project_id: string | null | undefined, + chatStoreState: ChatStore, + projectId: string | null | undefined, currentTaskId: string, status: import('@/types').ExecutionStatus, tokens: number, errorMessage?: string ) => { - // Use triggerTaskStore for reliable execution status tracking - const triggerTaskStore = useTriggerTaskStore.getState(); - await triggerTaskStore.updateExecutionStatus( + console.log('[updateTriggerExecutionStatus] Called with:', { + projectId, currentTaskId, status, tokens, - errorMessage - ); + }); + + // Get executionId directly from the task + const executionId = chatStoreState.tasks[currentTaskId]?.executionId; + + if (!executionId) { + // No executionId means this is not a trigger-initiated task, skip silently + console.log( + '[updateTriggerExecutionStatus] No executionId found for task:', + currentTaskId, + '- skipping (not a trigger-initiated task)' + ); + return; + } + + // Check if this execution has already been reported + if (reportedExecutionIds.has(executionId)) { + console.log( + '[updateTriggerExecutionStatus] Execution already reported:', + executionId + ); + return; + } + + try { + // Mark as reported to prevent duplicate updates + reportedExecutionIds.add(executionId); + + // Call the API to update execution status + await proxyUpdateTriggerExecution( + executionId, + { + status, + completed_at: new Date().toISOString(), + ...(errorMessage && { error_message: errorMessage }), + tokens_used: tokens, + }, + { projectId: projectId || undefined } + ); + + console.log( + '[updateTriggerExecutionStatus] Execution status updated:', + executionId, + '->', + status + ); + + // Complete or fail the current trigger task in triggerTaskStore + const triggerTaskStore = useTriggerTaskStore.getState(); + const currentTask = triggerTaskStore.currentTask; + + if (currentTask && currentTask.executionId === executionId) { + if (status === ExecutionStatus.Completed) { + triggerTaskStore.completeTask(currentTask.id); + } else if ( + status === ExecutionStatus.Failed || + status === ExecutionStatus.Cancelled + ) { + triggerTaskStore.failTask( + currentTask.id, + errorMessage || 'Task failed' + ); + } + } + } catch (err) { + console.warn( + `[updateTriggerExecutionStatus] Failed to update execution status to ${status}:`, + err + ); + // Remove from reported set so it can be retried + reportedExecutionIds.delete(executionId); + } }; const chatStore = (initial?: Partial) => @@ -292,6 +374,7 @@ const chatStore = (initial?: Partial) => isTakeControl: false, isTaskEdit: false, streamingDecomposeText: '', + executionId: undefined, }, }, })); @@ -426,7 +509,8 @@ const chatStore = (initial?: Partial) => shareToken?: string, delayTime?: number, messageContent?: string, - messageAttaches?: File[] + messageAttaches?: File[], + executionId?: string ) => { // ✅ Wait for backend to be ready before starting task (except for replay/share) if (!type || type === 'normal') { @@ -483,6 +567,11 @@ const chatStore = (initial?: Partial) => targetChatStore = newChatResult.chatStore; targetChatStore.getState().setIsPending(newTaskId, true); + // Set executionId if this is a trigger-initiated task + if (executionId) { + targetChatStore.getState().setExecutionId(newTaskId, executionId); + } + //From handleSend if message is given // Add the message to the new chatStore if provided if (messageContent) { @@ -850,6 +939,16 @@ const chatStore = (initial?: Partial) => updateLockedReferences(newChatStore, newTaskId); newChatStore.getState().setIsPending(newTaskId, false); + // If nextExecutionId exists, pass it to new task + if (previousChatStore.tasks[currentTaskId]?.nextExecutionId) { + newChatStore + .getState() + .setExecutionId( + newTaskId, + previousChatStore.tasks[currentTaskId]?.nextExecutionId + ); + } + if (type === 'replay') { newChatStore .getState() @@ -918,7 +1017,7 @@ const chatStore = (initial?: Partial) => const currentTaskId = getCurrentTaskId(); // Update trigger execution status to Completed for connection closed by server updateTriggerExecutionStatus( - projectStore, + getCurrentChatStore(), project_id, currentTaskId, ExecutionStatus.Running, @@ -1268,6 +1367,17 @@ const chatStore = (initial?: Partial) => step: AgentStep.WAIT_CONFIRM, isConfirm: false, }); + + // Update trigger execution status to Completed for simple question/answer flow + // This handles cases where the task ends with wait_confirm instead of the end step + updateTriggerExecutionStatus( + currentChatStore, + project_id, + currentTaskId, + ExecutionStatus.Completed, + currentChatStore.tasks[currentTaskId]?.tokens || 0 + ); + return; } // Task State @@ -1465,7 +1575,7 @@ const chatStore = (initial?: Partial) => ) { // This is a quick reply - update trigger execution status to Completed updateTriggerExecutionStatus( - projectStore, + getCurrentChatStore(), project_id, currentTaskId, ExecutionStatus.Completed, @@ -1984,7 +2094,7 @@ const chatStore = (initial?: Partial) => uploadLog(currentTaskId, type); // Update trigger execution status to Failed on error updateTriggerExecutionStatus( - projectStore, + getCurrentChatStore(), project_id, currentTaskId, ExecutionStatus.Failed, @@ -2296,7 +2406,7 @@ const chatStore = (initial?: Partial) => // Update trigger execution status to Completed updateTriggerExecutionStatus( - projectStore, + getCurrentChatStore(), project_id, currentTaskId, ExecutionStatus.Completed, @@ -2420,7 +2530,7 @@ const chatStore = (initial?: Partial) => const currentTaskId = getCurrentTaskId(); // Update trigger execution status to Completed for connection closed by server updateTriggerExecutionStatus( - projectStore, + getCurrentChatStore(), project_id, currentTaskId, ExecutionStatus.Cancelled, @@ -3235,6 +3345,36 @@ const chatStore = (initial?: Partial) => }; }); }, + setExecutionId: (taskId, executionId) => { + set((state) => { + if (!state.tasks[taskId]) return state; + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + executionId, + }, + }, + }; + }); + }, + setNextExecutionId: (taskId, nextExecutionId) => { + set((state) => { + if (!state.tasks[taskId]) return state; + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + nextExecutionId, + }, + }, + }; + }); + }, })); const filterMessage = (message: AgentMessage) => { diff --git a/src/store/triggerTaskStore.ts b/src/store/triggerTaskStore.ts index abe220c9..fce6048b 100644 --- a/src/store/triggerTaskStore.ts +++ b/src/store/triggerTaskStore.ts @@ -12,7 +12,6 @@ // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -import { proxyUpdateTriggerExecution } from '@/service/triggerApi'; import { ExecutionStatus, TriggerType } from '@/types'; import { create } from 'zustand'; @@ -224,15 +223,6 @@ interface TriggerTaskStore { getExecutionMapping: (chatTaskId: string) => ExecutionMapping | undefined; /** Remove execution mapping */ removeExecutionMapping: (chatTaskId: string) => void; - /** Update execution status on the backend */ - updateExecutionStatus: ( - chatTaskId: string, - status: ExecutionStatus, - tokens: number, - errorMessage?: string - ) => Promise; - /** Check if a task has been reported */ - isExecutionReported: (chatTaskId: string) => boolean; } export const useTriggerTaskStore = create((set, get) => ({ @@ -443,81 +433,4 @@ export const useTriggerTaskStore = create((set, get) => ({ return { executionMappings: newMappings }; }); }, - - updateExecutionStatus: async ( - chatTaskId: string, - status: ExecutionStatus, - tokens: number, - errorMessage?: string - ) => { - const { executionMappings, removeExecutionMapping, completeTask, failTask } = get(); - const mapping = executionMappings.get(chatTaskId); - - if (!mapping) { - console.warn( - '[TriggerTaskStore] No execution mapping found for chat task:', - chatTaskId - ); - return; - } - - if (mapping.reported) { - console.log( - '[TriggerTaskStore] Execution already reported for chat task:', - chatTaskId - ); - return; - } - - try { - await proxyUpdateTriggerExecution( - mapping.executionId, - { - status, - completed_at: new Date().toISOString(), - ...(errorMessage && { error_message: errorMessage }), - tokens_used: tokens, - }, - { projectId: mapping.projectId } - ); - - // Mark as reported - set((state) => { - const newMappings = new Map(state.executionMappings); - const updatedMapping = newMappings.get(chatTaskId); - if (updatedMapping) { - updatedMapping.reported = true; - } - return { executionMappings: newMappings }; - }); - - console.log( - '[TriggerTaskStore] Execution status updated:', - mapping.executionId, - '->', - status - ); - - // Complete or fail the trigger task based on status - // This moves the task from currentTask to taskHistory - if (status === ExecutionStatus.Completed) { - completeTask(mapping.triggerTaskId); - } else if (status === ExecutionStatus.Failed || status === ExecutionStatus.Cancelled) { - failTask(mapping.triggerTaskId, errorMessage || 'Task failed'); - } - - // Clean up mapping after successful update - removeExecutionMapping(chatTaskId); - } catch (err) { - console.warn( - `[TriggerTaskStore] Failed to update execution status to ${status}:`, - err - ); - } - }, - - isExecutionReported: (chatTaskId: string) => { - const mapping = get().executionMappings.get(chatTaskId); - return mapping?.reported ?? false; - }, }));