feat: use proj id for history & replay

This commit is contained in:
a7m-1st 2025-10-13 01:29:03 +03:00
parent 407e6822ee
commit 6aeff827aa
7 changed files with 162 additions and 39 deletions

View file

@ -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(

View file

@ -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"],
},

View file

@ -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 (
<div className="w-full h-full flex flex-col items-center justify-center">
{(chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId].messages.length > 0) ||
chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages ? (
{hasAnyMessages ? (
<div className="w-full h-[calc(100vh-54px)] flex flex-col rounded-xl border border-border-disabled border-solid relative shadow-blur-effect overflow-hidden">
<div className="absolute inset-0 blur-bg bg-bg-surface-secondary pointer-events-none"></div>

View file

@ -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 (
<div
onClick={() =>
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 (
<div
onClick={() => {
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={`${

View file

@ -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() {
<CommandItem
key={task.id}
className="cursor-pointer"
onSelect={() => 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)}
>
<ScanFace />
<div className="overflow-hidden text-ellipsis whitespace-nowrap">

View file

@ -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 (
<div
onClick={() => 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 (
<div
onClick={() => {
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={`${

View file

@ -193,6 +193,9 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
//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<ChatStore>) => createStore<ChatStore>()(
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<ChatStore>) => createStore<ChatStore>()(
// 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<ChatStore>) => createStore<ChatStore>()(
}
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<ChatStore>) => createStore<ChatStore>()(
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 = () => {