diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 29a7da361..e0c64df18 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -96,7 +96,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): if confirm is not True: yield confirm else: - yield sse_json("confirmed", "") + yield sse_json("confirmed", {"question": question}) (workforce, mcp) = await construct_workforce(options) for new_agent in options.new_agents: workforce.add_single_agent_worker( diff --git a/backend/app/utils/server/sync_step.py b/backend/app/utils/server/sync_step.py index c45714e2f..febc6b6c6 100644 --- a/backend/app/utils/server/sync_step.py +++ b/backend/app/utils/server/sync_step.py @@ -28,7 +28,9 @@ def sync_step(func): send_to_api( sync_url, { - "task_id": chat.task_id, + # TODO: revert to task_id to support multi-task project replay + # "task_id": chat.task_id, + "task_id": chat.project_id, "step": json_data["step"], "data": json_data["data"], }, diff --git a/src/components/ChatBox/index.tsx b/src/components/ChatBox/index.tsx index b9033b766..d477ecab8 100644 --- a/src/components/ChatBox/index.tsx +++ b/src/components/ChatBox/index.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useCallback } from "react"; +import { useState, useRef, useEffect, useCallback, useMemo } from "react"; import { fetchPost, proxyFetchPut, fetchPut, fetchDelete, proxyFetchDelete } from "@/api/http"; import BottomBox from "./BottomBox"; import { ProjectChatContainer } from "./ProjectChatContainer"; @@ -556,12 +556,35 @@ export default function ChatBox(): JSX.Element { console.error(`Can't remove ${task_id} due to ${error}`) } } + const getAllChatStoresMemoized = useMemo(() => { + const project_id = projectStore.activeProjectId; + if(!project_id) return []; + + return projectStore.getAllChatStores(project_id); + }, [projectStore, projectStore.activeProjectId, chatStore]) + + // Check if any chat store in the project has messages + const hasAnyMessages = useMemo(() => { + // First check current active chat store + if (chatStore.activeTaskId && + (chatStore.tasks[chatStore.activeTaskId].messages.length > 0 || + chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages)) { + return true; + } + + // Then check all other chat stores in the project + return getAllChatStoresMemoized.some(({chatStore: store}) => { + const state = store.getState(); + return state.activeTaskId && + state.tasks[state.activeTaskId] && + (state.tasks[state.activeTaskId].messages.length > 0 || + state.tasks[state.activeTaskId].hasMessages); + }); + }, [chatStore, getAllChatStoresMemoized]); return (
- {(chatStore.activeTaskId && - chatStore.tasks[chatStore.activeTaskId].messages.length > 0) || - chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages ? ( + {hasAnyMessages ? (
diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx index 7ff536750..679153d29 100644 --- a/src/components/HistorySidebar/index.tsx +++ b/src/components/HistorySidebar/index.tsx @@ -148,9 +148,15 @@ export default function HistorySidebar() { fetchHistoryTasks(); }, [chatStore.updateCount]); - const handleReplay = async (taskId: string, question: string) => { + const handleReplay = async (projectId: string, question: string, historyId: string) => { close(); - projectStore.replayProject([taskId], question, projectStore.activeProjectId ?? undefined); + /** + * TODO(history): For now all replaying is appending to the same instance + * of task_id (to be renamed projectId). Later we need to filter task_id from + * /api/chat/histories by project_id then feed it here. + */ + const taskIdsList = [projectId]; + projectStore.replayProject(taskIdsList, question, projectId, historyId); navigate({ pathname: "/" }); }; @@ -181,16 +187,18 @@ export default function HistorySidebar() { share(taskId); }; - const handleSetActive = (taskId: string, question: string) => { - const task = chatStore.tasks[taskId]; - if (task) { + const handleSetActive = (projectId: string, question: string, historyId: string) => { + const project = projectStore.getProjectById(projectId); + //If project exists + if (project) { // if there is record, show result - chatStore.setActiveTaskId(taskId); + projectStore.setHistoryId(projectId, historyId); + projectStore.setActiveProject(projectId) navigate(`/`); close(); } else { // if there is no record, execute replay - handleReplay(taskId, question); + handleReplay(projectId, question, historyId); } }; @@ -468,7 +476,11 @@ export default function HistorySidebar() { return (
- handleSetActive(task.task_id, task.question) + /** + * TODO(history): Update to use project_id field + * after update instead. + */ + handleSetActive(task.task_id, task.question, task.id) } key={task.task_id} className={`${ @@ -509,7 +521,11 @@ export default function HistorySidebar() { return (
{ - handleSetActive(task.task_id, task.question); + /** + * TODO(history): Update to use project_id field + * after update instead. + */ + handleSetActive(task.task_id, task.question, task.id); }} key={task.task_id} className={`${ diff --git a/src/components/SearchHistoryDialog.tsx b/src/components/SearchHistoryDialog.tsx index c6b9f2da8..adf76e0ee 100644 --- a/src/components/SearchHistoryDialog.tsx +++ b/src/components/SearchHistoryDialog.tsx @@ -43,21 +43,33 @@ export function SearchHistoryDialog() { } const navigate = useNavigate(); - const handleSetActive = (taskId: string, question: string) => { - const task = chatStore.tasks[taskId]; - if (task) { - // if there is a record, show the result - chatStore.setActiveTaskId(taskId); + const handleSetActive = (projectId: string, question: string, historyId: string) => { + const project = projectStore.getProjectById(projectId); + //If project exists + if (project) { + // if there is record, show result + projectStore.setHistoryId(projectId, historyId); + projectStore.setActiveProject(projectId) navigate(`/`); + close(); } else { // if there is no record, execute replay - handleReplay(taskId, question); + handleReplay(projectId, question, historyId); } }; - const handleReplay = async (taskId: string, question: string) => { - projectStore.replayProject([taskId], question, projectStore.activeProjectId ?? undefined); + + const handleReplay = async (projectId: string, question: string, historyId: string) => { + close(); + /** + * TODO(history): For now all replaying is appending to the same instance + * of task_id (to be renamed projectId). Later we need to filter task_id from + * /api/chat/histories by project_id then feed it here. + */ + const taskIdsList = [projectId]; + projectStore.replayProject(taskIdsList, question, projectId, historyId); navigate({ pathname: "/" }); }; + useEffect(() => { const fetchHistoryTasks = async () => { try { @@ -93,7 +105,11 @@ export function SearchHistoryDialog() { handleSetActive(task.task_id, task.question)} + /** + * TODO(history): Update to use project_id field + * after update instead. + */ + onSelect={() => handleSetActive(task.task_id, task.question, task.id)} >
diff --git a/src/pages/History.tsx b/src/pages/History.tsx index bf149846b..3117be43f 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -175,20 +175,29 @@ export default function Home() { share(taskId); }; - const handleReplay = async (taskId: string, question: string) => { - projectStore.replayProject([taskId], question, projectStore.activeProjectId ?? undefined); + const handleReplay = async (projectId: string, question: string, historyId: string) => { + /** + * TODO(history): For now all replaying is appending to the same instance + * of task_id (to be renamed projectId). Later we need to filter task_id from + * /api/chat/histories by project_id then feed it here. + */ + const taskIdsList = [projectId]; + projectStore.replayProject(taskIdsList, question, projectId, historyId); navigate({ pathname: "/" }); }; - const handleSetActive = (taskId: string, question: string) => { - const task = chatStore.tasks[taskId]; - if (task) { - // if there is a record, display the result - chatStore.setActiveTaskId(taskId); + const handleSetActive = (projectId: string, question: string, historyId: string) => { + const project = projectStore.getProjectById(projectId); + //If project exists + if (project) { + // if there is record, show result + projectStore.setHistoryId(projectId, historyId); + projectStore.setActiveProject(projectId) navigate(`/`); + close(); } else { // if there is no record, execute replay - handleReplay(taskId, question); + handleReplay(projectId, question, historyId); } }; @@ -547,7 +556,11 @@ export default function Home() { {historyTasks.map((task) => { return (
handleSetActive(task.task_id, task.question)} + /** + * TODO(history): Update to use project_id field + * after update instead. + */ + onClick={() => handleSetActive(task.task_id, task.question, task.id)} key={task.task_id} className={`${ chatStore.activeTaskId === task.task_id @@ -589,7 +602,11 @@ export default function Home() { return (
{ - handleSetActive(task.task_id, task.question); + /** + * TODO(history): Update to use project_id field + * after update instead. + */ + handleSetActive(task.task_id, task.question, task.id); }} key={task.task_id} className={`${ diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 61dc9ef23..355728332 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -193,6 +193,9 @@ const chatStore = (initial?: Partial) => createStore()( //Create a new chatStore on Start let newTaskId = taskId; let targetChatStore = { getState: () => get() }; // Default to current store + /** + * Replay creates its own chatStore for each task with replayProject + */ if(project_id && type !== "replay") { console.log("Creating a new Chat Instance for current project on end") const newChatResult = projectStore.appendInitChatStore(project_id); @@ -219,12 +222,13 @@ const chatStore = (initial?: Partial) => createStore()( const api = type == 'share' ? `${base_Url}/api/chat/share/playback/${shareToken}?delay_time=${delayTime}` : type == 'replay' ? - `${base_Url}/api/chat/steps/playback/${taskId}?delay_time=${delayTime}` + `${base_Url}/api/chat/steps/playback/${project_id}?delay_time=${delayTime}` : `${baseURL}/chat` const { tasks } = get() - let historyId: string | null = null; + let historyId: string | null = projectStore.getHistoryId(project_id); let snapshots: any = []; + let skipFirstConfirmOnReplay = true; // replay or share request if (type) { @@ -304,12 +308,16 @@ const chatStore = (initial?: Partial) => createStore()( // create history - if (!type) { + if (!type && !historyId) { const authStore = getAuthStore(); const obj = { - "project_id": project_id, - "task_id": newTaskId, + /** + * TODO(history): Currently reusing project_id as the source + * of truth per project. Need to update field + * name after backend update. + */ + "task_id": project_id, "user_id": authStore.user_id, "question": messageContent || (targetChatStore.getState().tasks[newTaskId]?.messages[0]?.content ?? ''), "language": systemLanguage, @@ -324,6 +332,12 @@ const chatStore = (initial?: Partial) => createStore()( } await proxyFetchPost(`/api/chat/history`, obj).then(res => { historyId = res.id; + + /**Save history id for replay reuse purposes. + * TODO(history): Remove historyId handling to support per projectId + * instead in history api + */ + if(project_id && historyId) projectStore.setHistoryId(project_id, historyId); }) } const browser_port = await window.ipcRenderer.invoke('get-browser-port'); @@ -363,6 +377,41 @@ const chatStore = (initial?: Partial) => createStore()( multi_modal_agent: "Multi Modal Agent", social_medium_agent: "Social Media Agent", }; + + //Create new chatStore for replay + //TODO(history): Remove when per task replay is implemented + // waiting for implementing project_id to backend + if(type === "replay" && agentMessages.step === "confirmed" + && !skipFirstConfirmOnReplay) { + const { question } = agentMessages.data; + /** + * For Tasks where appended to existing project by + * reusing same projectId. Need to create new chatStore + * as it has been skipped earlier in startTask. + */ + if(type && project_id) { + const newChatResult = projectStore.appendInitChatStore(project_id); + + if (newChatResult) { + newTaskId = newChatResult.taskId; + targetChatStore = newChatResult.chatStore; + targetChatStore.getState().setIsPending(newTaskId, false); + + //From handleSend if message is given + // Add the message to the new chatStore if provided + if (question) { + targetChatStore.getState().addMessages(newTaskId, { + id: generateUniqueId(), + role: "user", + content: question, + attaches: messageAttaches || [], + }); + } + } + } + } + //Enable it for the rest of current SSE session + skipFirstConfirmOnReplay = false; // Dynamic getter function that always returns the current active chat store const getCurrentChatStore = () => {