diff --git a/backend/app/controller/chat_controller.py b/backend/app/controller/chat_controller.py index f623653a..ae423872 100644 --- a/backend/app/controller/chat_controller.py +++ b/backend/app/controller/chat_controller.py @@ -22,6 +22,7 @@ ActionSkipTaskData, get_or_create_task_lock, get_task_lock, + set_current_task_id, ) from app.component.environment import set_user_env_path from app.utils.workforce import Workforce @@ -111,8 +112,11 @@ async def post(data: Chat, request: Request): if data.is_cloud(): os.environ["cloud_api_key"] = data.api_key + # Set the initial current_task_id in task_lock + set_current_task_id(data.project_id, data.task_id) + # Put initial action in queue to start processing - await task_lock.put_queue(ActionImproveData(data=data.question)) + await task_lock.put_queue(ActionImproveData(data=data.question, new_task_id=data.task_id)) chat_logger.info( "Chat session initialized, starting streaming response", @@ -145,7 +149,8 @@ def improve(id: str, data: SupplementChat): if hasattr(task_lock, "last_task_result"): chat_logger.info(f"[CONTEXT] Preserved task result: {len(task_lock.last_task_result)} chars") - # Update file save path if task_id is provided + # If task_id is provided, optimistically update file_save_path (will be destroyed if task is not complex) + # this is because a NEW workforce instance may be created for this task new_folder_path = None if data.task_id: try: @@ -177,7 +182,7 @@ def improve(id: str, data: SupplementChat): except Exception as e: chat_logger.error(f"Error updating file path for project_id: {id}, task_id: {data.task_id}: {e}") - asyncio.run(task_lock.put_queue(ActionImproveData(data=data.question))) + asyncio.run(task_lock.put_queue(ActionImproveData(data=data.question, new_task_id=data.task_id))) chat_logger.info("Improvement request queued with preserved context", extra={"project_id": id}) return Response(status_code=201) diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index 56c1fe70..3a02b491 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -16,6 +16,7 @@ ActionNewAgent, TaskLock, delete_task_lock, + set_current_task_id, ) from camel.toolkits import AgentCommunicationToolkit, ToolkitMessageIntegration from app.utils.toolkit.human_toolkit import HumanToolkit @@ -358,8 +359,12 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): except Exception as e: logger.error(f"Error cleaning up folder: {e}") else: - yield sse_json("confirmed", {"question": question}) + # Update the sync_step with new task_id + if hasattr(item, 'new_task_id') and item.new_task_id: + set_current_task_id(options.project_id, item.new_task_id) + yield sse_json("confirmed", {"question": question}) + context_for_coordinator = build_context_for_workforce(task_lock, options) (workforce, mcp) = await construct_workforce(options) @@ -537,6 +542,7 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): # Now trigger end of previous task using stored result yield sse_json("end", old_task_result) + # Always yield new_task_state first - this is not optional yield sse_json("new_task_state", item.data) # Trigger Queue Removal @@ -568,6 +574,9 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock): workforce.resume() continue # This continues the main while loop, waiting for next action + # Update the sync_step with new task_id before sending new task sse events + set_current_task_id(options.project_id, task_id) + yield sse_json("confirmed", {"question": new_task_content}) task_lock.status = Status.confirmed diff --git a/backend/app/service/task.py b/backend/app/service/task.py index 48fcffd1..bc8d16d7 100644 --- a/backend/app/service/task.py +++ b/backend/app/service/task.py @@ -48,6 +48,7 @@ class Action(str, Enum): class ActionImproveData(BaseModel): action: Literal[Action.improve] = Action.improve data: str + new_task_id: str | None = None class ActionStartData(BaseModel): @@ -264,6 +265,8 @@ class TaskLock: """Persistent question confirmation agent""" summary_generated: bool """Track if summary has been generated for this project""" + current_task_id: Optional[str] + """Current task ID to be used in SSE responses""" def __init__(self, id: str, queue: asyncio.Queue, human_input: dict) -> None: self.id = id @@ -278,6 +281,7 @@ def __init__(self, id: str, queue: asyncio.Queue, human_input: dict) -> None: self.last_task_result = "" self.last_task_summary = "" self.question_agent = None + self.current_task_id = None async def put_queue(self, data: ActionData): self.last_accessed = datetime.now() @@ -349,6 +353,13 @@ def get_task_lock_if_exists(id: str) -> TaskLock | None: return task_locks.get(id) +def set_current_task_id(project_id: str, task_id: str) -> None: + """Set the current task ID for a project's task lock""" + task_lock = get_task_lock(project_id) + task_lock.current_task_id = task_id + logger.info(f"Updated current_task_id to {task_id} for project {project_id}") + + def create_task_lock(id: str) -> TaskLock: if id in task_locks: raise ProgramException("Task already exists") diff --git a/backend/app/utils/server/sync_step.py b/backend/app/utils/server/sync_step.py index 1f268d3f..a3d8bc96 100644 --- a/backend/app/utils/server/sync_step.py +++ b/backend/app/utils/server/sync_step.py @@ -5,6 +5,7 @@ import json from app.service.chat_service import Chat from app.component.environment import env +from app.service.task import get_task_lock_if_exists from utils import traceroot_wrapper as traceroot logger = traceroot.get_logger("sync_step") @@ -24,15 +25,26 @@ async def wrapper(*args, **kwargs): else: value_json_str = value json_data = json.loads(value_json_str) - chat: Chat = args[0] if args else None + + # Dynamic task_id extraction - prioritize runtime data over static args + chat: Chat = args[0] if args and hasattr(args[0], 'task_id') else None + task_id = None + if chat is not None: + task_lock = get_task_lock_if_exists(chat.project_id) + if task_lock is not None: + task_id = task_lock.current_task_id \ + if hasattr(task_lock, 'current_task_id') and task_lock.current_task_id else chat.task_id + else: + logger.warning(f"Task lock not found for project_id {chat.project_id}, using chat.task_id") + task_id = chat.task_id + + if task_id: asyncio.create_task( send_to_api( sync_url, { - # TODO: revert to task_id to support multi-task project replay - # "task_id": chat.task_id, - "task_id": chat.project_id, + "task_id": task_id, "step": json_data["step"], "data": json_data["data"], }, diff --git a/backend/tests/unit/service/test_task.py b/backend/tests/unit/service/test_task.py index 8bd186d0..5ebcb3f8 100644 --- a/backend/tests/unit/service/test_task.py +++ b/backend/tests/unit/service/test_task.py @@ -55,6 +55,15 @@ def test_action_improve_data_creation(self): assert data.action == Action.improve assert data.data == "Improve this code" + assert data.new_task_id is None + + def test_action_improve_data_with_new_task_id(self): + """Test ActionImproveData model creation with new_task_id.""" + data = ActionImproveData(data="Improve this code", new_task_id="task_123") + + assert data.action == Action.improve + assert data.data == "Improve this code" + assert data.new_task_id == "task_123" def test_action_start_data_creation(self): """Test ActionStartData model creation.""" diff --git a/src/components/GroupedHistoryView/ProjectDialog.tsx b/src/components/GroupedHistoryView/ProjectDialog.tsx new file mode 100644 index 00000000..04d3748a --- /dev/null +++ b/src/components/GroupedHistoryView/ProjectDialog.tsx @@ -0,0 +1,250 @@ +import React, { useState, useEffect, useRef } from "react"; +import { ProjectGroup } from "@/types/history"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogContentSection, + DialogFooter, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Tag } from "@/components/ui/tag"; +import TaskItem from "./TaskItem"; +import { useTranslation } from "react-i18next"; +import { Hash, Zap, CheckCircle, XCircle, Clock, Pin, Edit2, Loader2, CircleSlash2, CircleSlash, ArrowLeft } from "lucide-react"; +import { useProjectStore } from "@/store/projectStore"; + +interface ProjectDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + project: ProjectGroup; + onProjectRename: (projectId: string, newName: string) => void; + onTaskSelect: (projectId: string, question: string, historyId: string) => void; + onTaskDelete: (taskId: string) => void; + onTaskShare: (taskId: string) => void; + activeTaskId?: string; +} + +export default function ProjectDialog({ + open, + onOpenChange, + project, + onProjectRename, + onTaskSelect, + onTaskDelete, + onTaskShare, + activeTaskId, +}: ProjectDialogProps) { + const { t } = useTranslation(); + const projectStore = useProjectStore(); + const [projectName, setProjectName] = useState(project.project_name || t("layout.new-project")); + const [isSaving, setIsSaving] = useState(false); + const saveTimeoutRef = useRef(null); + const lastSavedNameRef = useRef(project.project_name || t("layout.new-project")); + + // Update state when project changes + useEffect(() => { + const name = project.project_name || t("layout.new-project"); + setProjectName(name); + lastSavedNameRef.current = name; + }, [project.project_name, project.project_id, t]); + + // Auto-save with debouncing + useEffect(() => { + // Clear any existing timeout + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + + const trimmedName = projectName.trim(); + + // Only save if the name has actually changed and is not empty + if (trimmedName && trimmedName !== lastSavedNameRef.current) { + setIsSaving(true); + + // Debounce: wait 800ms after user stops typing + saveTimeoutRef.current = setTimeout(() => { + // Update via callback (for history API) + onProjectRename(project.project_id, trimmedName); + + // Also update in projectStore if the project exists there + const storeProject = projectStore.getProjectById(project.project_id); + if (storeProject) { + projectStore.updateProject(project.project_id, { name: trimmedName }); + } + + lastSavedNameRef.current = trimmedName; + setIsSaving(false); + }, 800); + } else if (!trimmedName) { + // If empty, don't show saving state + setIsSaving(false); + } + + // Cleanup timeout on unmount or when projectName changes + return () => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + }; + }, [projectName, project.project_id, onProjectRename, projectStore]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + }; + }, []); + + return ( + + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + /> + + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + {/* Project Name Section - Inline Edit with Auto-Save */} +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + +
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + setProjectName(e.target.value)} + onClick={(e) => e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + onFocus={(e) => e.stopPropagation()} + placeholder={t("layout.enter-project-name")} + /> + {isSaving ? ( + + ) : ( + <> + )} +
+
+ + {/* Project Stats */} +
+
+ + {t("layout.total-tokens")} + +
+ + + {project.total_tokens.toLocaleString()} + +
+
+ +
+ + {t("layout.total-tasks")} + +
+ + + {project.task_count} + +
+
+ +
+ + {t("layout.completed")} + +
+ + + {project.total_completed_tasks} + +
+
+ +
+ + {t("layout.failed")} + +
+ + + {project.total_failed_tasks} + +
+
+
+ + {/* Tasks List */} +
+
+ {project.tasks.length > 0 ? ( + project.tasks.map((task, index) => ( + onTaskSelect(project.project_id, task.question, task.id.toString())} + onDelete={() => onTaskDelete(task.id.toString())} + onShare={() => onTaskShare(task.id.toString())} + isLast={index === project.tasks.length - 1} + /> + )) + ) : ( +
+ + {t("layout.no-tasks-in-project")} +
+ )} +
+
+
+ + e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + + +
+
+ ); +} + diff --git a/src/components/GroupedHistoryView/ProjectGroup.tsx b/src/components/GroupedHistoryView/ProjectGroup.tsx new file mode 100644 index 00000000..cfa07d03 --- /dev/null +++ b/src/components/GroupedHistoryView/ProjectGroup.tsx @@ -0,0 +1,389 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ChevronDown, ChevronRight, Folder, FolderClosed, FolderOpen, Calendar, Target, Clock, Activity, Zap, Bot, MoreVertical, Edit, Trash2, MoreHorizontal, Pin, Hash, Sparkles, Sparkle } from "lucide-react"; +import { ProjectGroup as ProjectGroupType, HistoryTask } from "@/types/history"; +import { Button } from "@/components/ui/button"; +import { Tag } from "@/components/ui/tag"; +import { TooltipSimple } from "@/components/ui/tooltip"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { useProjectStore } from "@/store/projectStore"; +import TaskItem from "./TaskItem"; +import ProjectDialog from "./ProjectDialog"; +import { replayProject } from "@/lib/replay"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; + +interface ProjectGroupProps { + project: ProjectGroupType; + onTaskSelect: (projectId: string, question: string, historyId: string) => void; + onTaskDelete: (taskId: string) => void; + onTaskShare: (taskId: string) => void; + activeTaskId?: string; + searchValue?: string; + isOngoing?: boolean; + onOngoingTaskPause?: (taskId: string) => void; + onOngoingTaskResume?: (taskId: string) => void; + onProjectEdit?: (projectId: string) => void; + onProjectDelete?: (projectId: string) => void; + onProjectRename?: (projectId: string, newName: string) => void; + viewMode?: "grid" | "list"; +} + +export default function ProjectGroup({ + project, + onTaskSelect, + onTaskDelete, + onTaskShare, + activeTaskId, + searchValue = "", + isOngoing = false, + onOngoingTaskPause, + onOngoingTaskResume, + onProjectEdit, + onProjectDelete, + onProjectRename, + viewMode = "grid" +}: ProjectGroupProps) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const projectStore = useProjectStore(); + const [isExpanded, setIsExpanded] = useState(true); + const [isDialogOpen, setIsDialogOpen] = useState(false); + + // Handler to navigate to project + const handleProjectClick = (e: React.MouseEvent) => { + // Don't trigger if clicking on interactive elements (buttons, dropdowns) + if ((e.target as HTMLElement).closest('button, [role="menuitem"]')) { + return; + } + + // Check if project exists in store + const existingProject = projectStore.getProjectById(project.project_id); + + if (existingProject) { + // Project exists, just activate it and navigate + projectStore.setActiveProject(project.project_id); + navigate('/'); + } else { + // Project doesn't exist, trigger replay to recreate it + // Get the first task's question and use the first task's ID as history ID + const firstTask = project.tasks?.[0]; + if (firstTask) { + const question = firstTask.question || project.last_prompt || ""; + const historyId = firstTask.id?.toString() || ""; + const taskIdsList = [project.project_id]; + + replayProject(projectStore, navigate, project.project_id, question, historyId, taskIdsList); + } else { + console.warn("No tasks found in project, cannot replay"); + // Fallback: try to set as active anyway (may create a new project) + projectStore.setActiveProject(project.project_id); + navigate('/'); + } + } + }; + + // Filter tasks based on search value + const filteredTasks = project.tasks.filter(task => + task.question?.toLowerCase().includes(searchValue.toLowerCase()) + ); + + // Don't render if no tasks match the search + if (searchValue && filteredTasks.length === 0) { + return null; + } + + const formatDate = (dateString: string) => { + const date = new Date(dateString); + const now = new Date(); + const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24)); + + if (diffInDays === 0) return t("layout.today"); + if (diffInDays === 1) return t("layout.yesterday"); + if (diffInDays < 7) return `${diffInDays} ${t("layout.days-ago")}`; + + return date.toLocaleDateString(); + }; + + // Calculate if project has issues (failed tasks or tasks requiring human in the loop) + const hasFailedTasks = project.total_failed_tasks > 0; + const hasHumanInLoop = project.ongoing_tasks?.some(task => task.status === 'pending') || false; + const hasIssue = hasFailedTasks || hasHumanInLoop; + + // Calculate agent count (placeholder - count unique agents from tasks if available) + const agentCount = project.tasks?.length > 0 + ? new Set(project.tasks.map(t => t.model_type || 'default')).size + : 0; + + // Trigger count is 0 for now (disabled) + const triggerCount = 0; + + // Handle project edit - open dialog + const handleProjectEdit = (e?: React.MouseEvent) => { + if (e) { + e.stopPropagation(); + } + setIsDialogOpen(true); + // Also call the parent callback if provided + if (onProjectEdit) { + onProjectEdit(project.project_id); + } + }; + + // Handle project rename + const handleProjectRename = (projectId: string, newName: string) => { + if (onProjectRename) { + onProjectRename(projectId, newName); + } + }; + + // Grid view - Card-based design + if (viewMode === "grid") { + return ( + + {/* Project Card */} +
+ {/* Header with menu */} +
+
+
+ {isOngoing ? ( + + ) : ( + + )} + + {/* Status badges */} +
+ {isOngoing && ( + + + + {t("layout.ongoing")} + + + )} + + {!isOngoing && hasIssue && ( + + + {t("layout.issue") || "Issue"} + + + )} +
+
+ {project.project_name}

} + className="bg-surface-tertiary px-2 text-wrap break-words text-label-xs select-text pointer-events-auto shadow-perfect" + > + + {project.project_name} + +
+
+ + {/* Menu button */} + + + + + + {onProjectEdit && ( + + + {t("layout.edit") || "Edit"} + + )} + {onProjectDelete && ( + { + e.stopPropagation(); + onProjectDelete(project.project_id); + }} + className="bg-dropdown-item-bg-default hover:bg-dropdown-item-bg-hover text-text-cuation cursor-pointer" + > + + {t("layout.delete")} + + )} + + +
+ + {/* Project Dialog */} + + + {/* Footer with stats */} + +
+ {/* Token count */} + +
+ + {t("chat.token")} + + {project.total_tokens ? project.total_tokens.toLocaleString() : "0"} + +
+
+ +
+ + {/* Task count */} + +
+ + + {project.task_count} + +
+
+
+
+
+
+
+ ); + } + + // List view - Original horizontal layout + return ( +
+ {/* Project */} +
+ {/* Start: Folder icon and project name - Fixed width */} +
+ {isOngoing ? ( + + ) : ( + + )} + {project.project_name}

} + className="bg-surface-tertiary px-2 text-wrap break-words text-label-xs select-text pointer-events-auto shadow-perfect" + > + + {project.project_name} + +
+
+ + {/* Middle: Project, Trigger, Agent tags - Aligned to right */} +
+ + + {project.total_tokens ? project.total_tokens.toLocaleString() : "0"} + + + + + {project.task_count} + +
+ + {/* End: Status and menu */} +
+ {/* Status tag */} + {isOngoing && ( + + + {t("layout.ongoing")} + + )} + + {!isOngoing && hasIssue && ( + + {t("layout.issue") || "Issue"} + + )} + + {/* Menu button */} + + + + + + {onProjectEdit && ( + + + {t("layout.edit") || "Edit"} + + )} + {onProjectDelete && ( + onProjectDelete(project.project_id)} + className="bg-dropdown-item-bg-default hover:bg-dropdown-item-bg-hover text-text-cuation cursor-pointer" + > + + {t("layout.delete")} + + )} + + +
+
+ + {/* Project Dialog */} + +
+ ); +} \ No newline at end of file diff --git a/src/components/GroupedHistoryView/TaskItem.tsx b/src/components/GroupedHistoryView/TaskItem.tsx new file mode 100644 index 00000000..770ceece --- /dev/null +++ b/src/components/GroupedHistoryView/TaskItem.tsx @@ -0,0 +1,211 @@ +import React from "react"; +import { Ellipsis, Share, Trash2, Clock, CheckCircle, XCircle, CirclePause, CirclePlay, Pin, Hash } from "lucide-react"; +import { HistoryTask } from "@/types/history"; +import { Button } from "@/components/ui/button"; +import { Tag } from "@/components/ui/tag"; +import { TooltipSimple } from "@/components/ui/tooltip"; +import { + Popover, + PopoverContent, + PopoverTrigger, + PopoverClose, +} from "@/components/ui/popover"; +import { useTranslation } from "react-i18next"; +import folderIcon from "@/assets/Folder-1.svg"; + +interface TaskItemProps { + task: HistoryTask; + isActive: boolean; + onSelect: () => void; + onDelete: () => void; + onShare: () => void; + isLast: boolean; + isOngoing?: boolean; + onPause?: () => void; + onResume?: () => void; +} + +export default function TaskItem({ + task, + isActive, + onSelect, + onDelete, + onShare, + isLast, + isOngoing = false, + onPause, + onResume +}: TaskItemProps) { + const { t } = useTranslation(); + + // Check if task is paused (for ongoing tasks) + const isPaused = (task as any)._taskData?.status === "pause"; + + const getStatusTag = (status: number) => { + switch (status) { + case 1: // Running + return ( + + + {t("layout.running")} + + ); + case 2: // Completed + return ( + + + {t("layout.completed")} + + ); + case 3: // Failed + return ( + + + {t("layout.failed")} + + ); + default: // Unknown + return ( + + + {t("layout.unknown")} + + ); + } + }; + + const formatDate = (dateString?: string) => { + if (!dateString) return ""; + const date = new Date(dateString); + return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + }; + + return ( +
+
+ + +
+ +
{task.summary}
+
+ {t("layout.created")}: {formatDate(task.created_at)} +
+
+ {t("layout.tokens")}: {task.tokens ? task.tokens.toLocaleString() : "0"} +
+
+ } + > + + {task.summary || t("layout.new-project")} + + +
+
+ +
+ {!isOngoing && getStatusTag(task.status)} + + + + {task.tokens ? task.tokens.toLocaleString() : "0"} + + + {isOngoing && (onPause || onResume) && ( + { + e.stopPropagation(); + if (isPaused && onResume) { + onResume(); + } else if (!isPaused && onPause) { + onPause(); + } + }} + > + {isPaused ? ( + + ) : ( + + )} + + {isPaused ? t("layout.continue") : t("layout.pause")} + + + )} + + + + + + +
+ {!isOngoing && ( + + + + )} + + + + +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/src/components/GroupedHistoryView/index.tsx b/src/components/GroupedHistoryView/index.tsx new file mode 100644 index 00000000..a00d3750 --- /dev/null +++ b/src/components/GroupedHistoryView/index.tsx @@ -0,0 +1,571 @@ +import React, { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { ProjectGroup as ProjectGroupType, OngoingTask } from "@/types/history"; +import { fetchGroupedHistoryTasks } from "@/service/historyApi"; +import ProjectGroup from "./ProjectGroup"; +import { useTranslation } from "react-i18next"; +import { Loader2, FolderOpen, Pin, Hash, LayoutGrid, List, Sparkles, Sparkle } from "lucide-react"; +import { Tag } from "@/components/ui/tag"; +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useGlobalStore } from "@/store/globalStore"; +import { proxyFetchDelete } from "@/api/http"; +import { getAuthStore } from "@/store/authStore"; +import { useProjectStore } from "@/store/projectStore"; + +interface GroupedHistoryViewProps { + searchValue?: string; + onTaskSelect: (projectId: string, question: string, historyId: string) => void; + onTaskDelete: (historyId: string, callback: () => void) => void; + onTaskShare: (taskId: string) => void; + activeTaskId?: string; + refreshTrigger?: number; // For triggering refresh from parent + ongoingTasks?: { [taskId: string]: any }; // Add ongoing tasks from chatStore + onOngoingTaskClick?: (taskId: string) => void; // Callback for clicking ongoing tasks + onOngoingTaskPause?: (taskId: string) => void; // Callback for pausing ongoing tasks + onOngoingTaskResume?: (taskId: string) => void; // Callback for resuming ongoing tasks + onOngoingTaskDelete?: (taskId: string) => void; // Callback for deleting ongoing tasks + onProjectEdit?: (projectId: string) => void; // Callback for editing a project + onProjectDelete?: (projectId: string, callback: () => Promise) => void; // Callback for deleting a project with async callback +} + +export default function GroupedHistoryView({ + searchValue = "", + onTaskSelect, + onTaskDelete, + onTaskShare, + activeTaskId, + refreshTrigger, + ongoingTasks = {}, + onOngoingTaskClick, + onOngoingTaskPause, + onOngoingTaskResume, + onOngoingTaskDelete, + onProjectEdit, + onProjectDelete +}: GroupedHistoryViewProps) { + const { t } = useTranslation(); + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const { history_type, setHistoryType } = useGlobalStore(); + const projectStore = useProjectStore(); + + // Default to list view if not set + const viewType = history_type || "list"; + + const loadProjects = async () => { + setLoading(true); + try { + await fetchGroupedHistoryTasks(setProjects); + } catch (error) { + console.error("Failed to load grouped projects:", error); + } finally { + setLoading(false); + } + }; + + const onDelete = (historyId: string ) => { + try { + onTaskDelete(historyId, () => { + setProjects(prevProjects => { + return prevProjects.map(project => { + project.tasks = project.tasks.filter(task => String(task.id) !== historyId); + return project; + }).filter(project => project.tasks.length > 0); + }); + }); + } catch (error) { + console.error("Failed to delete task:", error); + } + } + + const handleProjectEdit = (projectId: string) => { + if (onProjectEdit) { + onProjectEdit(projectId); + } else { + console.log("Edit project:", projectId); + // TODO: Implement project edit functionality + } + } + + const handleProjectDelete = (projectId: string) => { + // Create the deletion callback that will be executed after confirmation + const deleteCallback = async () => { + try { + // Find the project in our existing data + const targetProject = projects.find(project => project.project_id === projectId); + + if (targetProject && targetProject.tasks) { + console.log(`Deleting project ${projectId} with ${targetProject.tasks.length} tasks`); + + // Delete each task one by one + for (const history of targetProject.tasks) { + try { + await proxyFetchDelete(`/api/chat/history/${history.id}`); + console.log(`Successfully deleted task ${history.task_id}`); + + // Also delete local files for this task if available (via Electron IPC) + const {email} = getAuthStore(); + if (history.task_id && (window as any).ipcRenderer) { + try { + await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id, history.project_id ?? undefined); + console.log(`Successfully cleaned up local files for task ${history.task_id}`); + } catch (error) { + console.warn(`Local file cleanup failed for task ${history.task_id}:`, error); + } + } + } catch (error) { + console.error(`Failed to delete task ${history.task_id}:`, error); + } + } + + // Remove from projectStore + projectStore.removeProject(projectId); + + // Update local state to remove the project + setProjects(prevProjects => prevProjects.filter(project => project.project_id !== projectId)); + + console.log(`Completed deletion of project ${projectId}`); + } else { + console.warn(`Project ${projectId} not found or has no tasks`); + } + } catch (error) { + console.error("Failed to delete project:", error); + throw error; // Re-throw to let parent handle errors + } + }; + + // Call parent callback with the deletion callback (for confirmation dialog) + if (onProjectDelete) { + onProjectDelete(projectId, deleteCallback); + } else { + // If no parent callback, execute deletion directly + deleteCallback(); + } + } + + const handleProjectRename = (projectId: string, newName: string) => { + // Update local state + setProjects(prevProjects => { + return prevProjects.map(project => { + if (project.project_id === projectId) { + return { + ...project, + project_name: newName + }; + } + return project; + }); + }); + + // TODO: Implement API call to update project name + console.log(`Renaming project ${projectId} to ${newName}`); + } + + useEffect(() => { + loadProjects(); + }, [refreshTrigger]); + + // Create separate project groups for ongoing tasks + const ongoingProjects = React.useMemo(() => { + const projectMap = new Map(); + + // Group ongoing tasks by their summaryTask (project name) + Object.entries(ongoingTasks).forEach(([taskId, task]) => { + // Skip finished tasks or special task types + if (task.status === "finished" || task.type) return; + + // Determine project_id - use summaryTask as project identifier + const projectId = task.summaryTask || taskId; + const projectName = task.summaryTask || t("dashboard.new-project"); + + // Get or create project + let project = projectMap.get(projectId); + if (!project) { + project = { + project_id: projectId, + project_name: projectName, + total_tokens: 0, + task_count: 0, + latest_task_date: new Date().toISOString(), + last_prompt: task.summaryTask || "", + tasks: [], + ongoing_tasks: [], + total_completed_tasks: 0, + total_failed_tasks: 0, + total_ongoing_tasks: 0, + average_tokens_per_task: 0 + }; + projectMap.set(projectId, project); + } + + // Convert ongoing task to HistoryTask format for consistent rendering + const historyTask: any = { + id: taskId, + task_id: taskId, + project_id: projectId, + question: task.summaryTask || t("dashboard.new-project"), + language: "", + model_platform: "", + model_type: "", + max_retries: 0, + project_name: projectName, + tokens: task.tokens || 0, + status: task.status === "running" ? 1 : task.status === "pause" ? 0 : 1, + created_at: new Date().toISOString(), + _isOngoing: true, // Flag to identify ongoing tasks + _taskData: task // Store original task data for controls + }; + + project.tasks.push(historyTask); + project.task_count++; + project.total_ongoing_tasks = (project.total_ongoing_tasks || 0) + 1; + project.total_tokens += historyTask.tokens; + }); + + // Convert to array and sort by latest activity + return Array.from(projectMap.values()).sort((a, b) => { + const dateA = new Date(a.latest_task_date).getTime(); + const dateB = new Date(b.latest_task_date).getTime(); + return dateB - dateA; + }); + }, [ongoingTasks, t]); + + // Filter ongoing projects based on search value + const filteredOngoingProjects = ongoingProjects.filter(project => { + if (!searchValue) return true; + + // Check if project name matches + if (project.project_name?.toLowerCase().includes(searchValue.toLowerCase())) { + return true; + } + + // Check if any task in the project matches + return project.tasks.some(task => + task.question?.toLowerCase().includes(searchValue.toLowerCase()) + ); + }); + + // Filter history projects based on search value + const filteredHistoryProjects = projects.filter(project => { + if (!searchValue) return true; + + // Check if project name matches + if (project.project_name?.toLowerCase().includes(searchValue.toLowerCase())) { + return true; + } + + // Check if any task in the project matches + return project.tasks.some(task => + task.question?.toLowerCase().includes(searchValue.toLowerCase()) + ); + }); + + // Combine for total count and checks + const allFilteredProjects = [...filteredOngoingProjects, ...filteredHistoryProjects]; + + if (loading) { + return ( +
+ + {t("layout.loading")} +
+ ); + } + + if (allFilteredProjects.length === 0) { + return ( +
+ +
+ {searchValue + ? t("dashboard.no-projects-match-search") + : t("dashboard.no-projects-found") + } +
+ {searchValue && ( +
+ {t("dashboard.try-different-search")} +
+ )} +
+ ); + } + + return ( +
+ {/* Summary */} +
+
+ + + {t("layout.projects")} + + {allFilteredProjects.length} + + + + + + {t("layout.total-tasks")} + + {allFilteredProjects.reduce((total, project) => total + project.task_count, 0)} + + +
+
+ + setHistoryType(value as "grid" | "list") + } + > + + +
+ + {t("dashboard.grid")} +
+
+ +
+ + {t("dashboard.list")} +
+
+
+
+
+
+ + + + {viewType === "grid" ? ( + // Grid layout for project cards + + + {/* Ongoing Projects */} + {filteredOngoingProjects.map((project, index) => ( + + { + if (onOngoingTaskClick) { + onOngoingTaskClick(historyId); + } + }} + onTaskDelete={(taskId) => { + if (onOngoingTaskDelete) { + onOngoingTaskDelete(taskId); + } + }} + onTaskShare={onTaskShare} + activeTaskId={activeTaskId} + searchValue={searchValue} + isOngoing={true} + onOngoingTaskPause={onOngoingTaskPause} + onOngoingTaskResume={onOngoingTaskResume} + onProjectEdit={handleProjectEdit} + onProjectDelete={handleProjectDelete} + onProjectRename={handleProjectRename} + viewMode="grid" + /> + + ))} + + {/* History Projects */} + {filteredHistoryProjects.map((project, index) => ( + + + + ))} + + + ) : ( + // List layout for projects + + + {/* Ongoing Projects */} + {filteredOngoingProjects.map((project, index) => ( + + { + if (onOngoingTaskClick) { + onOngoingTaskClick(historyId); + } + }} + onTaskDelete={(taskId) => { + if (onOngoingTaskDelete) { + onOngoingTaskDelete(taskId); + } + }} + onTaskShare={onTaskShare} + activeTaskId={activeTaskId} + searchValue={searchValue} + isOngoing={true} + onOngoingTaskPause={onOngoingTaskPause} + onOngoingTaskResume={onOngoingTaskResume} + onProjectEdit={handleProjectEdit} + onProjectDelete={handleProjectDelete} + onProjectRename={handleProjectRename} + viewMode="list" + /> + + ))} + + {/* History Projects */} + {filteredHistoryProjects.map((project, index) => ( + + + + ))} + + + )} + + +
+ ); +} \ No newline at end of file diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx index b826c869..197fa232 100644 --- a/src/components/HistorySidebar/index.tsx +++ b/src/components/HistorySidebar/index.tsx @@ -15,11 +15,16 @@ import { SquarePlay, Trash2, Bot, + Pin, + Sparkles, + Hash, + Activity, } from "lucide-react"; +import { Sparkle } from "@/components/animate-ui/icons/sparkle"; import { Button } from "@/components/ui/button"; import { useNavigate } from "react-router-dom"; import SearchInput from "./SearchInput"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, useMemo } from "react"; import { useGlobalStore } from "@/store/globalStore"; import folderIcon from "@/assets/Folder-1.svg"; import { Progress } from "@/components/ui/progress"; @@ -32,13 +37,15 @@ import { PopoverClose, } from "../ui/popover"; import AlertDialog from "../ui/alertDialog"; -import { proxyFetchGet, proxyFetchDelete, proxyFetchPost } from "@/api/http"; +import { proxyFetchGet, proxyFetchDelete } from "@/api/http"; import { Tag } from "../ui/tag"; import { share } from "@/lib/share"; import { replayProject } from "@/lib"; import { useTranslation } from "react-i18next"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; import {getAuthStore} from "@/store/authStore"; +import { fetchGroupedHistoryTasks } from "@/service/historyApi"; +import { HistoryTask, ProjectGroup } from "@/types/history"; export default function HistorySidebar() { const { t } = useTranslation(); @@ -49,16 +56,13 @@ export default function HistorySidebar() { if (!chatStore) { return
Loading...
; } - - const getTokens = chatStore.getTokens; - const { history_type, toggleHistoryType } = useGlobalStore(); const [searchValue, setSearchValue] = useState(""); const [historyOpen, setHistoryOpen] = useState(true); - const [historyTasks, setHistoryTasks] = useState([]); + const [historyTasks, setHistoryTasks] = useState([]); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [anchorStyle, setAnchorStyle] = useState<{ left: number; top: number } | null>(null); const panelRef = useRef(null); - const [curHistoryId, setCurHistoryId] = useState(""); + const [currentProjectId, setCurrentProjectId] = useState(""); const handleSearch = (e: React.ChangeEvent) => { if (e.target.value) { @@ -67,13 +71,6 @@ export default function HistorySidebar() { setSearchValue(e.target.value); }; - const toggleOpenHistory = async () => { - if (!historyOpen) { - await fetchHistoryTasks(); - } - setHistoryOpen(!historyOpen); - }; - const createChat = () => { close(); //Create a new project @@ -82,98 +79,90 @@ export default function HistorySidebar() { navigate("/"); }; - const agentMap = { - developer_agent: { - name: t("layout.developer-agent"), - textColor: "text-text-developer", - bgColor: "bg-bg-fill-coding-active", - shapeColor: "bg-bg-fill-coding-default", - borderColor: "border-bg-fill-coding-active", - bgColorLight: "bg-emerald-200", - }, - search_agent: { - name: t("layout.search-agent"), - - textColor: "text-blue-700", - bgColor: "bg-bg-fill-browser-active", - shapeColor: "bg-bg-fill-browser-default", - borderColor: "border-bg-fill-browser-active", - bgColorLight: "bg-blue-200", - }, - document_agent: { - name: t("layout.document-agent"), - - textColor: "text-yellow-700", - bgColor: "bg-bg-fill-writing-active", - shapeColor: "bg-bg-fill-writing-default", - borderColor: "border-bg-fill-writing-active", - bgColorLight: "bg-yellow-200", - }, - multi_modal_agent: { - name: t("layout.multi-modal-agent"), - - textColor: "text-fuchsia-700", - bgColor: "bg-bg-fill-multimodal-active", - shapeColor: "bg-bg-fill-multimodal-default", - borderColor: "border-bg-fill-multimodal-active", - bgColorLight: "bg-fuchsia-200", - }, - social_medium_agent: { - name: t("layout.social-media-agent"), - - textColor: "text-purple-700", - bgColor: "bg-violet-700", - shapeColor: "bg-violet-300", - borderColor: "border-violet-700", - bgColorLight: "bg-purple-50", - }, - }; - - const handleClickAgent = (taskId: string, agent_id: string) => { - chatStore.setActiveTaskId(taskId); - chatStore.setActiveWorkSpace(taskId, "workflow"); - chatStore.setActiveAgent(taskId, agent_id); - navigate(`/`); - }; - - const fetchHistoryTasks = async () => { - try { - const res = await proxyFetchGet(`/api/chat/histories`); - setHistoryTasks(res.items); - } catch (error) { - console.error("Failed to fetch history tasks:", error); - } - }; - useEffect(() => { - fetchHistoryTasks(); + fetchGroupedHistoryTasks(setHistoryTasks); }, [chatStore.updateCount]); + // Group ongoing tasks by project + const ongoingProjects = useMemo(() => { + const projectMap = new Map(); + + // Iterate through all projects + const allProjects = projectStore.getAllProjects(); + allProjects.forEach((project) => { + // Get all chat stores for this project + const chatStores = projectStore.getAllChatStores(project.id); + + let hasOngoingTasks = false; + let totalTokens = 0; + let taskCount = 0; + let lastPrompt = ""; + + // Check all chat stores for ongoing tasks + chatStores.forEach(({ chatStore: cs }) => { + const csState = cs.getState(); + Object.keys(csState.tasks || {}).forEach((taskId) => { + const task = csState.tasks[taskId]; + // Only include ongoing tasks + if (task.status !== "finished" && !task.type) { + hasOngoingTasks = true; + taskCount++; + if (task.tokens) { + totalTokens += task.tokens; + } + if (!lastPrompt && task.messages?.[0]?.content) { + lastPrompt = task.messages[0].content; + } + } + }); + }); + + // Only add project if it has ongoing tasks + if (hasOngoingTasks) { + projectMap.set(project.id, { + project_id: project.id, + project_name: project.name, + tasks: [], + task_count: taskCount, + total_tokens: totalTokens, + last_prompt: lastPrompt, + isOngoing: true + }); + } + }); + + return Array.from(projectMap.values()); + }, [chatStore.updateCount, projectStore, t]); + const handleReplay = async (projectId: string, question: string, historyId: string) => { close(); - await replayProject(projectStore, navigate, projectId, question, historyId); + // Get task IDs from the API response data in descending order (newest first) + const project = historyTasks.find(p => p.project_id === projectId); + const taskIdsList = project?.tasks.map((task: HistoryTask) => task.task_id) || [projectId]; + await replayProject(projectStore, navigate, projectId, question, historyId, taskIdsList); }; const handleDelete = (id: string) => { console.log("Delete task:", id); - setCurHistoryId(id); + setCurrentProjectId(id); setDeleteModalOpen(true); }; + // Deletes whole Project const confirmDelete = async () => { - await deleteHistoryTask(); - setHistoryTasks((list) => list.filter((item) => item.id !== curHistoryId)); - setCurHistoryId(""); + await deleteWholeProject(currentProjectId); + setHistoryTasks((list) => list.filter((item) => item.project_id !== currentProjectId)); + setCurrentProjectId(""); setDeleteModalOpen(false); }; - const deleteHistoryTask = async () => { + const deleteHistoryTask = async (project: ProjectGroup, historyId: string) => { try { - const res = await proxyFetchDelete(`/api/chat/history/${curHistoryId}`); + const res = await proxyFetchDelete(`/api/chat/history/${historyId}`); console.log(res); // also delete local files for this task if available (via Electron IPC) const {email} = getAuthStore() - const history = historyTasks.find((item) => item.id === curHistoryId); + const history = project.tasks.find((item: HistoryTask) => String(item.id) === historyId); if (history?.task_id && (window as any).ipcRenderer) { try { //TODO(file): rename endpoint to use project_id @@ -188,6 +177,47 @@ export default function HistorySidebar() { } }; + // Deletes whole project by using the tasks from historyTasks state + const deleteWholeProject = async (projectId: string) => { + try { + // Find the project in our existing data + const targetProject = historyTasks.find(project => project.project_id === projectId); + + if (targetProject && targetProject.tasks) { + console.log(`Found project ${projectId} with ${targetProject.tasks.length} tasks to delete`); + + // Delete each task one by one + for (const history of targetProject.tasks) { + console.log(`Deleting task: ${history.task_id} (history ID: ${history.id})`); + try { + const deleteRes = await proxyFetchDelete(`/api/chat/history/${history.id}`); + console.log(`Successfully deleted task ${history.task_id}:`, deleteRes); + + // Also delete local files for this task if available (via Electron IPC) + const {email} = getAuthStore(); + if (history.task_id && (window as any).ipcRenderer) { + try { + await (window as any).ipcRenderer.invoke('delete-task-files', email, history.task_id, history.project_id ?? undefined); + console.log(`Successfully cleaned up local files for task ${history.task_id}`); + } catch (error) { + console.warn(`Local file cleanup failed for task ${history.task_id}:`, error); + } + } + } catch (error) { + console.error(`Failed to delete task ${history.task_id}:`, error); + } + } + + projectStore.removeProject(projectId); + console.log(`Completed deletion of project ${projectId}`); + } else { + console.warn(`Project ${projectId} not found or has no tasks`); + } + } catch (error) { + console.error("Failed to delete whole project:", error); + } + }; + const handleShare = async (taskId: string) => { close(); share(taskId); @@ -263,29 +293,6 @@ export default function HistorySidebar() { top: anchorStyle ? anchorStyle.top : 40, }} > - {/*
- - -
*/}
{/* Search */}
-
- {/* Table view hidden - {history_type === "table" ? ( - // Table -
- {Object.keys(chatStore.tasks) - .reverse() - .map((taskId) => { - const task = chatStore.tasks[taskId]; - return task.status != "finished" && !task.type ? ( -
{ - chatStore.setActiveTaskId(taskId); - navigate(`/`); - close(); - }} - className={`${ - chatStore.activeTaskId === taskId - ? "!bg-white-100%" - : "" - } max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl backdrop-blur-xl w-[316px] h-[180px]`} - > -
-
-
- folder-icon -
-
- {task?.messages?.[0]?.content || t("layout.new-project")} -
-
- -
-
-
-
-
- {t("layout.tasks")} -
-
- {task.taskRunning?.filter( - (taskItem) => - taskItem.status === "completed" || - taskItem.status === "failed" - ).length || 0} - /{task.taskRunning?.length || 0} -
-
-
- {task.taskAssigning.map( - (taskAssigning) => - taskAssigning.status === "running" && ( -
- handleClickAgent( - taskId, - taskAssigning.agent_id as AgentNameType - ) - } - className={`transition-all duration-300 flex justify-start items-center gap-1 px-sm py-xs bg-menutabs-bg-default hover:bg-white-100% rounded-lg border border-solid border-white-100% shadow-history-item ${ - agentMap[ - taskAssigning.type as keyof typeof agentMap - ]?.borderColor - }`} - > - -
- {taskAssigning.name} -
-
- ) - )} -
-
+
+ {/* Ongoing Projects */} + {ongoingProjects + .filter((project) => + project.last_prompt?.toLowerCase().includes(searchValue.toLowerCase()) || + project.project_name?.toLowerCase().includes(searchValue.toLowerCase()) + ) + .map((project) => ( +
{ + projectStore.setActiveProject(project.project_id); + navigate(`/`); + close(); + }} + className="max-w-full relative cursor-pointer transition-all duration-300 bg-project-surface-default hover:bg-project-surface-hover rounded-xl flex justify-between items-center gap-sm w-full px-4 py-3 shadow-history-item border border-solid border-border-disabled" + > + + +
+ + {project.project_name || t("layout.new-project")}
-
- ) : ( - "" - ); - })} -
- ) : ( - // List - */} -
- {Object.keys(chatStore.tasks) - .reverse() - .map((taskId) => { - const task = chatStore.tasks[taskId]; - return task.status != "finished" && !task.type ? ( -
{ - chatStore.setActiveTaskId(taskId); - navigate(`/`); - close(); - }} - className={`${ - chatStore.activeTaskId === taskId - ? "!bg-white-100%" - : "" - } max-w-full flex w-full items-center border-radius-2xl bg-white-30% box-sizing-border-box p-3 relative h-14 gap-md transition-all duration-300 hover:bg-white-100% rounded-2xl cursor-pointer`} - > - folder-icon -
- - {task?.messages?.[0]?.content || t("layout.new-project")} -

- } - className="w-[300px] bg-surface-tertiary p-2 text-wrap break-words text-label-xs select-text pointer-events-auto shadow-perfect" + } + > + + {project.project_name || t("layout.new-project")} + +
+ +
+ +
+ + + + {project.total_tokens || 0} + + + + + + + {project.task_count} + + +
+ + + +
+ + + + +
+ + + + + + +
- ) : ( - "" - ); - })} +
+
- {/* )} */} -
-
- - {historyOpen && ( - + project.last_prompt?.toLowerCase().includes(searchValue.toLowerCase()) || + project.project_name?.toLowerCase().includes(searchValue.toLowerCase()) + ) + .map((project) => ( +
{ + handleSetActive(project.project_id, project.last_prompt, project.project_id); + }} + key={project.project_id} + className="max-w-full relative cursor-pointer transition-all duration-300 bg-project-surface-default hover:bg-project-surface-hover rounded-xl flex justify-between items-center gap-sm w-full px-4 py-3 shadow-history-item border border-solid border-border-disabled" > - {/* Table view hidden - {history_type === "table" ? ( - // Table -
- {historyTasks - .filter((task) => - task?.question - ?.toLowerCase() - .includes(searchValue.toLowerCase()) - ) - .map((task) => { - return ( -
- handleSetActive(task.task_id, task.question, task.id) - } - key={task.task_id} - className={`${ - chatStore.activeTaskId === task?.task_id - ? "!bg-white-100%" - : "" - } max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl w-[316px] h-[180px] p-6 shadow-history-item`} - > -
-
-
- folder-icon - - {t("layout.token")} {task.tokens || 0} - -
+ + +
+ + {project.last_prompt || project.project_name || t("layout.new-project")} +
+ } + > + + {project.last_prompt || project.project_name || t("layout.new-project")} + + +
-
- {task?.question.split("|")[0] || - t("dashboard.new-project")} -
-
- {task?.question.split("|")[1] || - t("dashboard.new-project")} -
-
- ); - })} +
+ + + + {project.total_tokens || 0} + + + + + + + {project.task_count} + +
- ) : ( - // List - */} -
- {historyTasks - .filter((task) => - task.question?.toLowerCase().includes(searchValue.toLowerCase()) - ) - .map((task) => { - return ( -
{ - handleSetActive(task.task_id, task.question, task.id); - }} - key={task.task_id} - className={`${ - chatStore.activeTaskId === task.task_id - ? "!bg-white-100%" - : "" - } max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-2xl flex justify-between items-center gap-md w-full p-3 h-14 shadow-history-item border border-solid border-border-disabled`} - > - folder-icon - -
- - {" "} - {task?.question.split("|")[0] || t("layout.new-project")} -
- } + + + +
- + + + +
+ + - - -
- - - + + {t("layout.share")} + + - - - -
-
- -
- ); - })} -
- {/* )} */} - - )} - + + + +
+ + +
+ ))}
diff --git a/src/components/SearchHistoryDialog.tsx b/src/components/SearchHistoryDialog.tsx index 62272268..dbcf3eae 100644 --- a/src/components/SearchHistoryDialog.tsx +++ b/src/components/SearchHistoryDialog.tsx @@ -31,11 +31,15 @@ import { generateUniqueId } from "@/lib"; import { useTranslation } from "react-i18next"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; import { replayProject } from "@/lib"; +import { fetchHistoryTasks } from "@/service/historyApi"; +import GroupedHistoryView from "@/components/GroupedHistoryView"; +import { useGlobalStore } from "@/store/globalStore"; export function SearchHistoryDialog() { const {t} = useTranslation() const [open, setOpen] = useState(false); const [historyTasks, setHistoryTasks] = useState([]); + const { history_type } = useGlobalStore(); //Get Chatstore for the active project's task const { chatStore, projectStore } = useChatStoreAdapter(); if (!chatStore) { @@ -51,7 +55,7 @@ export function SearchHistoryDialog() { projectStore.setHistoryId(projectId, historyId); projectStore.setActiveProject(projectId) navigate(`/`); - close(); + setOpen(false); } else { // if there is no record, execute replay handleReplay(projectId, question, historyId); @@ -59,21 +63,22 @@ export function SearchHistoryDialog() { }; const handleReplay = async (projectId: string, question: string, historyId: string) => { - close(); + setOpen(false); await replayProject(projectStore, navigate, projectId, question, historyId); }; - useEffect(() => { - const fetchHistoryTasks = async () => { - try { - const res = await proxyFetchGet(`/api/chat/histories`); - setHistoryTasks(res.items); - } catch (error) { - console.error("Failed to fetch history tasks:", error); - } - }; + const handleDelete = (taskId: string) => { + // TODO: Implement delete functionality similar to HistorySidebar + console.log("Delete task:", taskId); + }; - fetchHistoryTasks(); + const handleShare = (taskId: string) => { + // TODO: Implement share functionality similar to HistorySidebar + console.log("Share task:", taskId); + }; + + useEffect(() => { + fetchHistoryTasks(setHistoryTasks); }, []); return ( <> @@ -93,24 +98,35 @@ export function SearchHistoryDialog() { {t("dashboard.no-results")} - - {historyTasks.map((task) => ( - handleSetActive(task.task_id, task.question, task.id)} - > - -
- {task.question} -
-
- ))} -
+ {history_type === "grid" ? ( +
+ +
+ ) : ( + + {historyTasks.map((task) => ( + handleSetActive(task.task_id, task.question, task.id)} + > + +
+ {task.question} +
+
+ ))} +
+ )}
diff --git a/src/components/TopBar/index.tsx b/src/components/TopBar/index.tsx index f028c9bb..6efff999 100644 --- a/src/components/TopBar/index.tsx +++ b/src/components/TopBar/index.tsx @@ -12,7 +12,7 @@ import { Power, ChevronDown, ChevronLeft, - LayoutGrid, + House, Share, MoreHorizontal, } from "lucide-react"; @@ -261,7 +261,7 @@ function HeaderWin() { className="no-drag" onClick={() => navigate("/history")} > - +
*/ -} - import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva, type VariantProps } from "class-variance-authority"; @@ -12,14 +5,22 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const tagVariants = cva( - "bg-tag-file-info !rounded-full py-0.5 px-2 text-tag-text-info text-xs leading-17 font-medium", + "inline-flex justify-start items-center leading-relaxed", { variants: { variant: { - primary: "bg-tag-fill-info text-tag-text-info", + primary: "bg-tag-fill-info text-[var(--tag-foreground-info)]", + info: "bg-tag-fill-info !text-[var(--tag-foreground-info)]", + success: "bg-tag-fill-success !text-[var(--tag-foreground-success)]", + cuation: "bg-tag-fill-cuation !text-[var(--tag-foreground-cuation)]", + warning: "bg-tag-fill-warning !text-[var(--tag-foreground-warning)]", + default: "bg-tag-fill-default !text-[var(--tag-foreground-default)]", + ghost: "bg-transparent !text-[var(--tag-foreground-default)]", }, size: { - sm: "inline-flex justify-start items-center gap-1 px-2 py-1 rounded-md text-xs font-medium leading-tight [&_svg]:size-[16px]", + xs: "px-2 py-0.5 gap-1 text-body-xs font-bold leading-tight [&_svg]:size-[10px] rounded-full", + sm: "px-2 py-1.5 gap-1 text-body-xs font-bold leading-tight [&_svg]:size-[16px] rounded-full", + md: "px-3 py-1.5 gap-2 text-body-md font-semibold leading-relaxed [&_svg]:size-[20px] rounded-xl", }, }, defaultVariants: { @@ -29,25 +30,46 @@ const tagVariants = cva( } ); +interface TagProps + extends React.ComponentProps<"div">, + VariantProps { + asChild?: boolean; + text?: string; + icon?: React.ReactNode; +} function Tag({ className, variant, size, asChild = false, + text, + icon, children, ...props -}: React.ComponentProps<"div"> & - VariantProps & { - asChild?: boolean; - }) { +}: TagProps) { const Comp = asChild ? Slot : "div"; + // When asChild is true, just pass through the child without wrapping + if (asChild) { + return ( + + {children} + + ); + } + + // Normal rendering when asChild is false return ( + {icon && {icon}} + {text && {text}} {children} ); diff --git a/src/i18n/locales/ar/dashboard.json b/src/i18n/locales/ar/dashboard.json index 6d8ac427..11e1a9cc 100644 --- a/src/i18n/locales/ar/dashboard.json +++ b/src/i18n/locales/ar/dashboard.json @@ -3,9 +3,12 @@ "new-project": "مشروع جديد", "search": "بحث", "project-archives": "أرشيف المشاريع", + "ongoing-projects": "المشاريع الجارية", "ongoing-tasks": "المهام الجارية", + "completed-tasks": "المهام المكتملة", "share": "مشاركة", "delete": "حذف", + "grid": "شبكة", "table": "جدول", "list": "قائمة", "search-dialog": "مربع حوار البحث", diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index 6824fef5..ff573af4 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -46,6 +46,7 @@ "save": "حفظ", "not-found": "غير موجود", "tasks": "المهام", + "ongoing": "قيد التقدم", "token": "# الرمز", "configuring": "جاري التكوين...", "not-configured": "غير مكوّن", @@ -142,5 +143,26 @@ "opening": "جاري الفتح...", "open-browser": "فتح المتصفح", "no-cookies-saved-yet": "لم يتم حفظ ملفات تعريف الارتباط بعد", - "no-cookies-saved-yet-description": "انقر فوق \"فتح المتصفح\" أعلاه لتسجيل الدخول إلى المواقع وحفظ ملفات تعريف الارتباط الخاصة بها للمهام المستقبلية." + "no-cookies-saved-yet-description": "انقر فوق \"فتح المتصفح\" أعلاه لتسجيل الدخول إلى المواقع وحفظ ملفات تعريف الارتباط الخاصة بها للمهام المستقبلية.", + "back": "رجوع", + "total-tokens": "إجمالي الرموز", + "total-tasks": "إجمالي المهام", + "edit": "تعديل", + "project-settings": "إعدادات المشروع", + "manage-project-details": "إدارة تفاصيل ومهام مشروعك", + "project-name": "اسم المشروع", + "enter-project-name": "أدخل اسم المشروع", + "completed": "مكتمل", + "failed": "فاشل", + "average-tokens-per-task": "متوسط الرموز لكل مهمة", + "no-tasks-in-project": "لا توجد مهام في هذا المشروع", + "close": "إغلاق", + "running": "قيد التشغيل", + "unknown": "غير معروف", + "created": "تم الإنشاء", + "today": "اليوم", + "yesterday": "أمس", + "days-ago": "أيام مضت", + "delete-project": "حذف المشروع", + "delete-project-confirmation": "هل أنت متأكد من أنك تريد حذف هذا المشروع وجميع مهامه؟ لا يمكن التراجع عن هذا الإجراء." } diff --git a/src/i18n/locales/de/dashboard.json b/src/i18n/locales/de/dashboard.json index b7dc6487..dc2921bb 100644 --- a/src/i18n/locales/de/dashboard.json +++ b/src/i18n/locales/de/dashboard.json @@ -3,9 +3,12 @@ "new-project": "Neues Projekt", "search": "Suchen", "project-archives": "Projektarchive", + "ongoing-projects": "Laufende Projekte", "ongoing-tasks": "Laufende Aufgaben", + "completed-tasks": "Abgeschlossene Aufgaben", "share": "Teilen", "delete": "Löschen", + "grid": "Raster", "table": "Tabelle", "list": "Liste", "search-dialog": "Suchdialog", diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index 32766d58..5a22c9a5 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -46,6 +46,7 @@ "save": "Speichern", "not-found": "Nicht gefunden", "tasks": "AUFGABEN", + "ongoing": "Laufend", "token": "# Token", "configuring": "Konfiguration...", "not-configured": "Nicht konfiguriert", @@ -142,5 +143,26 @@ "opening": "Wird geöffnet...", "open-browser": "Browser öffnen", "no-cookies-saved-yet": "Noch keine Cookies gespeichert", - "no-cookies-saved-yet-description": "Klicken Sie oben auf \"Browser öffnen\", um sich bei Websites anzumelden und deren Cookies für zukünftige Aufgaben zu speichern." + "no-cookies-saved-yet-description": "Klicken Sie oben auf \"Browser öffnen\", um sich bei Websites anzumelden und deren Cookies für zukünftige Aufgaben zu speichern.", + "back": "Zurück", + "total-tokens": "Gesamt-Tokens", + "total-tasks": "Gesamt-Aufgaben", + "edit": "Bearbeiten", + "project-settings": "Projekteinstellungen", + "manage-project-details": "Verwalten Sie Ihre Projektdetails und Aufgaben", + "project-name": "Projektname", + "enter-project-name": "Projektname eingeben", + "completed": "Abgeschlossen", + "failed": "Fehlgeschlagen", + "average-tokens-per-task": "Durchschnittliche Tokens pro Aufgabe", + "no-tasks-in-project": "Keine Aufgaben in diesem Projekt", + "close": "Schließen", + "running": "Läuft", + "unknown": "Unbekannt", + "created": "Erstellt", + "today": "Heute", + "yesterday": "Gestern", + "days-ago": "Tage zuvor", + "delete-project": "Projekt löschen", + "delete-project-confirmation": "Sind Sie sicher, dass Sie dieses Projekt und alle seine Aufgaben löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." } diff --git a/src/i18n/locales/en-us/dashboard.json b/src/i18n/locales/en-us/dashboard.json index 9ebb25c9..cc9fd130 100644 --- a/src/i18n/locales/en-us/dashboard.json +++ b/src/i18n/locales/en-us/dashboard.json @@ -3,9 +3,12 @@ "new-project": "Untitled Project", "search": "Search", "project-archives": "Project Archives", + "ongoing-projects": "Ongoing Projects", "ongoing-tasks": "Ongoing Tasks", + "completed-tasks": "Completed Tasks", "share": "Share", "delete": "Delete", + "grid": "Grid", "table": "Table", "list": "List", "search-dialog": "Search Dialog", diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index fb7ed4eb..f90f7c6f 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -46,6 +46,7 @@ "save": "Save", "not-found": "Not Found", "tasks": "TASKS", + "ongoing": "Ongoing", "token": "# Token", "configuring": "Configuring...", "not-configured": "Not Configured", @@ -143,5 +144,25 @@ "opening": "Opening...", "open-browser": "Open Browser", "no-cookies-saved-yet": "No cookies saved yet", - "no-cookies-saved-yet-description": "Click \"Open Browser\" above to sign in to websites and save their cookies for future tasks." + "no-cookies-saved-yet-description": "Click \"Open Browser\" above to sign in to websites and save their cookies for future tasks.", + "total-tokens": "Total Tokens", + "total-tasks": "Total Tasks", + "edit": "Edit", + "project-settings": "Project Settings", + "manage-project-details": "Manage your project details and tasks", + "project-name": "Project Name", + "enter-project-name": "Enter project name", + "completed": "Completed", + "failed": "Failed", + "average-tokens-per-task": "Average Tokens per Task", + "no-tasks-in-project": "No tasks in this project", + "close": "Close", + "running": "Running", + "unknown": "Unknown", + "created": "Created", + "today": "Today", + "yesterday": "Yesterday", + "days-ago": "days ago", + "delete-project": "Delete Project", + "delete-project-confirmation": "Are you sure you want to delete this project and all its tasks? This action cannot be undone." } diff --git a/src/i18n/locales/es/dashboard.json b/src/i18n/locales/es/dashboard.json index 1f94bb67..fee7a535 100644 --- a/src/i18n/locales/es/dashboard.json +++ b/src/i18n/locales/es/dashboard.json @@ -3,9 +3,12 @@ "new-project": "Nuevo Proyecto", "search": "Buscar", "project-archives": "Archivos de proyecto", + "ongoing-projects": "Proyectos en progreso", "ongoing-tasks": "Tareas en progreso", + "completed-tasks": "Tareas completadas", "share": "Compartir", "delete": "Eliminar", + "grid": "Cuadrícula", "table": "Tabla", "list": "Lista", "search-dialog": "Buscar Dialog", diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index a18a5d55..2c993fe4 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -46,6 +46,7 @@ "save": "Guardar", "not-found": "No encontrado", "tasks": "TAREAS", + "ongoing": "En progreso", "token": "# Token", "configuring": "Configurando...", "not-configured": "No configurado", @@ -142,5 +143,26 @@ "opening": "Abriendo...", "open-browser": "Abrir Navegador", "no-cookies-saved-yet": "Aún no se han guardado cookies", - "no-cookies-saved-yet-description": "Haga clic en \"Abrir Navegador\" arriba para iniciar sesión en sitios web y guardar sus cookies para tareas futuras." + "no-cookies-saved-yet-description": "Haga clic en \"Abrir Navegador\" arriba para iniciar sesión en sitios web y guardar sus cookies para tareas futuras.", + "back": "Atrás", + "total-tokens": "Total de Tokens", + "total-tasks": "Total de Tareas", + "edit": "Editar", + "project-settings": "Configuración del Proyecto", + "manage-project-details": "Gestionar los detalles y tareas de su proyecto", + "project-name": "Nombre del Proyecto", + "enter-project-name": "Ingrese el nombre del proyecto", + "completed": "Completado", + "failed": "Falló", + "average-tokens-per-task": "Promedio de Tokens por Tarea", + "no-tasks-in-project": "No hay tareas en este proyecto", + "close": "Cerrar", + "running": "En ejecución", + "unknown": "Desconocido", + "created": "Creado", + "today": "Hoy", + "yesterday": "Ayer", + "days-ago": "días atrás", + "delete-project": "Eliminar Proyecto", + "delete-project-confirmation": "¿Estás seguro de que quieres eliminar este proyecto y todas sus tareas? Esta acción no se puede deshacer." } diff --git a/src/i18n/locales/fr/dashboard.json b/src/i18n/locales/fr/dashboard.json index 74c13c46..5b3d290f 100644 --- a/src/i18n/locales/fr/dashboard.json +++ b/src/i18n/locales/fr/dashboard.json @@ -3,9 +3,12 @@ "new-project": "Nouveau projet", "search": "Rechercher", "project-archives": "Archives de projets", + "ongoing-projects": "Projets en cours", "ongoing-tasks": "Tâches en cours", + "completed-tasks": "Tâches terminées", "share": "Partager", "delete": "Supprimer", + "grid": "Grille", "table": "Tableau", "list": "Liste", "search-dialog": "Boîte de dialogue de recherche", @@ -14,5 +17,10 @@ "search-for-a-task-or-document": "Rechercher une tâche ou un document", "calendar": "Calendrier", "search-emoji": "Rechercher un emoji", - "calculator": "Calculatrice" + "calculator": "Calculatrice", + "developer-agent": "Agent Développeur", + "search-agent": "Agent de Recherche", + "document-agent": "Agent de Documents", + "multi-modal-agent": "Agent Multi Modal", + "social-media-agent": "Agent de Médias Sociaux" } diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index 19859c7a..66c8acb3 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -46,6 +46,7 @@ "save": "Enregistrer", "not-found": "Non trouvé", "tasks": "TÂCHES", + "ongoing": "En cours", "token": "# Token", "configuring": "Configuration...", "not-configured": "Non configuré", @@ -142,5 +143,26 @@ "opening": "Ouverture...", "open-browser": "Ouvrir le Navigateur", "no-cookies-saved-yet": "Aucun cookie enregistré pour le moment", - "no-cookies-saved-yet-description": "Cliquez sur \"Ouvrir le Navigateur\" ci-dessus pour vous connecter aux sites web et enregistrer leurs cookies pour les tâches futures." + "no-cookies-saved-yet-description": "Cliquez sur \"Ouvrir le Navigateur\" ci-dessus pour vous connecter aux sites web et enregistrer leurs cookies pour les tâches futures.", + "back": "Retour", + "total-tokens": "Total des Tokens", + "total-tasks": "Total des Tâches", + "edit": "Modifier", + "project-settings": "Paramètres du Projet", + "manage-project-details": "Gérer les détails et les tâches de votre projet", + "project-name": "Nom du Projet", + "enter-project-name": "Entrez le nom du projet", + "completed": "Terminé", + "failed": "Échoué", + "average-tokens-per-task": "Moyenne de Tokens par Tâche", + "no-tasks-in-project": "Aucune tâche dans ce projet", + "close": "Fermer", + "running": "En cours d'exécution", + "unknown": "Inconnu", + "created": "Créé", + "today": "Aujourd'hui", + "yesterday": "Hier", + "days-ago": "jours auparavant", + "delete-project": "Supprimer le Projet", + "delete-project-confirmation": "Êtes-vous sûr de vouloir supprimer ce projet et toutes ses tâches ? Cette action ne peut pas être annulée." } diff --git a/src/i18n/locales/it/dashboard.json b/src/i18n/locales/it/dashboard.json index a9f3fcf6..f2392c27 100644 --- a/src/i18n/locales/it/dashboard.json +++ b/src/i18n/locales/it/dashboard.json @@ -3,9 +3,12 @@ "new-project": "Nuovo Progetto", "search": "Cerca", "project-archives": "Archivi Progetti", + "ongoing-projects": "Progetti in Corso", "ongoing-tasks": "Attività in Corso", + "completed-tasks": "Attività Completate", "share": "Condividi", "delete": "Elimina", + "grid": "Griglia", "table": "Tabella", "list": "Lista", "search-dialog": "Dialogo di Ricerca", diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 987ed2e7..2a097c03 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -46,6 +46,7 @@ "save": "Salva", "not-found": "Non trovato", "tasks": "ATTIVITÀ", + "ongoing": "In corso", "token": "# Token", "configuring": "Configurazione...", "not-configured": "Non configurato", @@ -142,5 +143,26 @@ "opening": "Apertura...", "open-browser": "Apri Browser", "no-cookies-saved-yet": "Nessun cookie salvato ancora", - "no-cookies-saved-yet-description": "Fai clic su \"Apri Browser\" sopra per accedere ai siti web e salvare i loro cookie per le attività future." + "no-cookies-saved-yet-description": "Fai clic su \"Apri Browser\" sopra per accedere ai siti web e salvare i loro cookie per le attività future.", + "back": "Indietro", + "total-tokens": "Totale Token", + "total-tasks": "Totale Attività", + "edit": "Modifica", + "project-settings": "Impostazioni Progetto", + "manage-project-details": "Gestisci i dettagli e le attività del tuo progetto", + "project-name": "Nome Progetto", + "enter-project-name": "Inserisci il nome del progetto", + "completed": "Completato", + "failed": "Fallito", + "average-tokens-per-task": "Media Token per Attività", + "no-tasks-in-project": "Nessuna attività in questo progetto", + "close": "Chiudi", + "running": "In esecuzione", + "unknown": "Sconosciuto", + "created": "Creato", + "today": "Oggi", + "yesterday": "Ieri", + "days-ago": "giorni fa", + "delete-project": "Elimina Progetto", + "delete-project-confirmation": "Sei sicuro di voler eliminare questo progetto e tutte le sue attività? Questa azione non può essere annullata." } diff --git a/src/i18n/locales/ja/dashboard.json b/src/i18n/locales/ja/dashboard.json index bdaeb97f..587ab3a3 100644 --- a/src/i18n/locales/ja/dashboard.json +++ b/src/i18n/locales/ja/dashboard.json @@ -3,9 +3,12 @@ "new-project": "新規プロジェクト", "search": "検索", "project-archives": "プロジェクトアーカイブ", + "ongoing-projects": "進行中のプロジェクト", "ongoing-tasks": "進行中のタスク", + "completed-tasks": "完了したタスク", "share": "共有", "delete": "削除", + "grid": "グリッド", "table": "テーブル", "list": "リスト", "search-dialog": "検索ダイアログ", diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index 59acec67..98881eb1 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -46,6 +46,7 @@ "save": "保存", "not-found": "見つかりません", "tasks": "タスク", + "ongoing": "進行中", "token": "# トークン", "configuring": "設定中...", "not-configured": "未設定", @@ -142,5 +143,26 @@ "opening": "開いています...", "open-browser": "ブラウザを開く", "no-cookies-saved-yet": "まだCookieが保存されていません", - "no-cookies-saved-yet-description": "上記の「ブラウザを開く」をクリックして、ウェブサイトにログインし、将来のタスクのためにCookieを保存します。" + "no-cookies-saved-yet-description": "上記の「ブラウザを開く」をクリックして、ウェブサイトにログインし、将来のタスクのためにCookieを保存します。", + "back": "戻る", + "total-tokens": "合計トークン", + "total-tasks": "合計タスク", + "edit": "編集", + "project-settings": "プロジェクト設定", + "manage-project-details": "プロジェクトの詳細とタスクを管理", + "project-name": "プロジェクト名", + "enter-project-name": "プロジェクト名を入力", + "completed": "完了", + "failed": "失敗", + "average-tokens-per-task": "タスクあたりの平均トークン", + "no-tasks-in-project": "このプロジェクトにタスクがありません", + "close": "閉じる", + "running": "実行中", + "unknown": "不明", + "created": "作成日", + "today": "今日", + "yesterday": "昨日", + "days-ago": "日前", + "delete-project": "プロジェクトを削除", + "delete-project-confirmation": "このプロジェクトとそのすべてのタスクを削除してもよろしいですか?この操作は元に戻せません。" } diff --git a/src/i18n/locales/ko/dashboard.json b/src/i18n/locales/ko/dashboard.json index 0894e231..1645b6cc 100644 --- a/src/i18n/locales/ko/dashboard.json +++ b/src/i18n/locales/ko/dashboard.json @@ -3,9 +3,12 @@ "new-project": "새 프로젝트", "search": "검색", "project-archives": "프로젝트 보관소", + "ongoing-projects": "진행 중인 프로젝트", "ongoing-tasks": "진행 중인 작업", + "completed-tasks": "완료된 작업", "share": "공유", "delete": "삭제", + "grid": "그리드", "table": "테이블", "list": "목록", "search-dialog": "검색 대화 상자", diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index f35f8ea2..bb359035 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -46,6 +46,7 @@ "save": "저장", "not-found": "찾을 수 없음", "tasks": "작업", + "ongoing": "진행 중", "token": "# 토큰", "configuring": "구성 중...", "not-configured": "구성되지 않음", @@ -142,5 +143,26 @@ "opening": "열는 중...", "open-browser": "브라우저 열기", "no-cookies-saved-yet": "아직 쿠키가 저장되지 않았습니다", - "no-cookies-saved-yet-description": "위의 \"브라우저 열기\"를 클릭하여 웹사이트에 로그인하고 향후 작업을 위해 쿠키를 저장하세요." + "no-cookies-saved-yet-description": "위의 \"브라우저 열기\"를 클릭하여 웹사이트에 로그인하고 향후 작업을 위해 쿠키를 저장하세요.", + "back": "뒤로", + "total-tokens": "총 토큰", + "total-tasks": "총 작업", + "edit": "편집", + "project-settings": "프로젝트 설정", + "manage-project-details": "프로젝트 세부 정보 및 작업 관리", + "project-name": "프로젝트 이름", + "enter-project-name": "프로젝트 이름 입력", + "completed": "완료됨", + "failed": "실패", + "average-tokens-per-task": "작업당 평균 토큰", + "no-tasks-in-project": "이 프로젝트에 작업이 없습니다", + "close": "닫기", + "running": "실행 중", + "unknown": "알 수 없음", + "created": "생성됨", + "today": "오늘", + "yesterday": "어제", + "days-ago": "일 전", + "delete-project": "프로젝트 삭제", + "delete-project-confirmation": "이 프로젝트와 모든 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다." } diff --git a/src/i18n/locales/ru/dashboard.json b/src/i18n/locales/ru/dashboard.json index 46494a24..1ea357ca 100644 --- a/src/i18n/locales/ru/dashboard.json +++ b/src/i18n/locales/ru/dashboard.json @@ -3,9 +3,12 @@ "new-project": "Новый проект", "search": "Поиск", "project-archives": "Архивы проектов", + "ongoing-projects": "Текущие проекты", "ongoing-tasks": "Текущие задачи", + "completed-tasks": "Завершенные задачи", "share": "Поделиться", "delete": "Удалить", + "grid": "Сетка", "table": "Таблица", "list": "Список", "search-dialog": "Диалог поиска", diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index 50db959c..ad956fa6 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -46,6 +46,7 @@ "save": "Сохранить", "not-found": "Не найдено", "tasks": "ЗАДАЧИ", + "ongoing": "В процессе", "token": "# Токен", "configuring": "Настройка...", "not-configured": "Не настроено", @@ -142,5 +143,26 @@ "opening": "Открытие...", "open-browser": "Открыть браузер", "no-cookies-saved-yet": "Файлы cookie ещё не сохранены", - "no-cookies-saved-yet-description": "Нажмите \"Открыть браузер\" выше, чтобы войти на веб-сайты и сохранить их файлы cookie для будущих задач." + "no-cookies-saved-yet-description": "Нажмите \"Открыть браузер\" выше, чтобы войти на веб-сайты и сохранить их файлы cookie для будущих задач.", + "back": "Назад", + "total-tokens": "Всего токенов", + "total-tasks": "Всего задач", + "edit": "Редактировать", + "project-settings": "Настройки проекта", + "manage-project-details": "Управляйте деталями и задачами вашего проекта", + "project-name": "Название проекта", + "enter-project-name": "Введите название проекта", + "completed": "Завершено", + "failed": "Неудачно", + "average-tokens-per-task": "Среднее количество токенов на задачу", + "no-tasks-in-project": "Нет задач в этом проекте", + "close": "Закрыть", + "running": "Выполняется", + "unknown": "Неизвестно", + "created": "Создано", + "today": "Сегодня", + "yesterday": "Вчера", + "days-ago": "дней назад", + "delete-project": "Удалить проект", + "delete-project-confirmation": "Вы уверены, что хотите удалить этот проект и все его задачи? Это действие нельзя отменить." } diff --git a/src/i18n/locales/zh-Hans/dashboard.json b/src/i18n/locales/zh-Hans/dashboard.json index e638c9aa..7b99594a 100644 --- a/src/i18n/locales/zh-Hans/dashboard.json +++ b/src/i18n/locales/zh-Hans/dashboard.json @@ -3,9 +3,12 @@ "new-project": "新项目", "search": "搜索", "project-archives": "项目归档", + "ongoing-projects": "进行中的项目", "ongoing-tasks": "进行中的任务", + "completed-tasks": "已完成的任务", "share": "分享", "delete": "删除", + "grid": "网格", "table": "表格", "list": "列表", "search-dialog": "搜索对话", diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index 9f82e4fc..854accb6 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -145,5 +145,25 @@ "opening": "打开中...", "open-browser": "打开浏览器", "no-cookies-saved-yet": "尚未保存 Cookie", - "no-cookies-saved-yet-description": "点击上方的「打开浏览器」登录网站并保存其 Cookie,以便用于未来的任务。" + "no-cookies-saved-yet-description": "点击上方的「打开浏览器」登录网站并保存其 Cookie,以便用于未来的任务。", + "total-tokens": "总令牌数", + "total-tasks": "总任务数", + "edit": "编辑", + "project-settings": "项目设置", + "manage-project-details": "管理您的项目详情和任务", + "project-name": "项目名称", + "enter-project-name": "输入项目名称", + "completed": "已完成", + "failed": "失败", + "average-tokens-per-task": "每个任务的平均令牌数", + "no-tasks-in-project": "此项目中没有任务", + "close": "关闭", + "running": "运行中", + "unknown": "未知", + "created": "创建于", + "today": "今天", + "yesterday": "昨天", + "days-ago": "天前", + "delete-project": "删除项目", + "delete-project-confirmation": "您确定要删除此项目及其所有任务吗?此操作无法撤销。" } diff --git a/src/i18n/locales/zh-Hant/dashboard.json b/src/i18n/locales/zh-Hant/dashboard.json index fb607ed3..3e63d8d0 100644 --- a/src/i18n/locales/zh-Hant/dashboard.json +++ b/src/i18n/locales/zh-Hant/dashboard.json @@ -3,9 +3,12 @@ "new-project": "新專案", "search": "搜尋", "project-archives": "專案封存", + "ongoing-projects": "進行中的專案", "ongoing-tasks": "進行中的任務", + "completed-tasks": "已完成的任務", "share": "分享", "delete": "刪除", + "grid": "網格", "table": "表格", "list": "列表", "search-dialog": "搜尋對話框", diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 0c83e563..1dd80898 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -48,6 +48,7 @@ "save": "儲存", "not-found": "未找到", "tasks": "任務", + "ongoing": "進行中", "token": "# 令牌", "configuring": "設定中...", "not-configured": "未設定", @@ -145,5 +146,25 @@ "opening": "開啟中...", "open-browser": "開啟瀏覽器", "no-cookies-saved-yet": "尚未儲存 Cookie", - "no-cookies-saved-yet-description": "點擊上方的「開啟瀏覽器」登入網站並儲存其 Cookie,以便用於未來的任務。" + "no-cookies-saved-yet-description": "點擊上方的「開啟瀏覽器」登入網站並儲存其 Cookie,以便用於未來的任務。", + "total-tokens": "總令牌數", + "total-tasks": "總任務數", + "edit": "編輯", + "project-settings": "專案設定", + "manage-project-details": "管理您的專案詳情和任務", + "project-name": "專案名稱", + "enter-project-name": "輸入專案名稱", + "completed": "已完成", + "failed": "失敗", + "average-tokens-per-task": "每個任務的平均令牌數", + "no-tasks-in-project": "此專案中沒有任務", + "close": "關閉", + "running": "執行中", + "unknown": "未知", + "created": "建立於", + "today": "今天", + "yesterday": "昨天", + "days-ago": "天前", + "delete-project": "刪除專案", + "delete-project-confirmation": "您確定要刪除此專案及其所有任務嗎?此操作無法撤銷。" } diff --git a/src/lib/replay.ts b/src/lib/replay.ts index fcb4ed26..819ee9db 100644 --- a/src/lib/replay.ts +++ b/src/lib/replay.ts @@ -1,5 +1,7 @@ +import { fetchGroupedHistoryTasks } from "@/service/historyApi"; import { ChatStore } from "@/store/chatStore"; import { ProjectStore } from "@/store/projectStore"; +import { ProjectGroup } from "@/types/history"; import { NavigateFunction } from "react-router-dom"; /** @@ -17,14 +19,10 @@ export const replayProject = async ( navigate: NavigateFunction, projectId: string, question: string, - historyId: string + historyId: string, + taskIdsList?: 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]; + if(!taskIdsList) taskIdsList = [projectId]; projectStore.replayProject(taskIdsList, question, projectId, historyId); navigate({ pathname: "/" }); }; diff --git a/src/pages/Dashboard/Project.tsx b/src/pages/Dashboard/Project.tsx index 55c39bb2..584fe486 100644 --- a/src/pages/Dashboard/Project.tsx +++ b/src/pages/Dashboard/Project.tsx @@ -37,23 +37,32 @@ import { proxyFetchDelete, proxyFetchGet, } from "@/api/http"; +import { getAuthStore } from "@/store/authStore"; +import { ProjectGroup as ProjectGroupType } from "@/types/history"; import { Tag } from "@/components/ui/tag"; import { share } from "@/lib/share"; import { useTranslation } from "react-i18next"; import AlertDialog from "@/components/ui/alertDialog"; +import { fetchHistoryTasks } from "@/service/historyApi"; +import GroupedHistoryView from "@/components/GroupedHistoryView"; export default function Project() { const {t} = useTranslation() const navigate = useNavigate(); - const { chatStore } = useChatStoreAdapter(); - if (!chatStore) { + const [deleteCallback, setDeleteCallback] = useState<() => void>(() => {}); + const { chatStore, projectStore } = useChatStoreAdapter(); + if (!chatStore || !projectStore) { return
Loading...
; } const { history_type, setHistoryType } = useGlobalStore(); const [historyTasks, setHistoryTasks] = useState([]); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [curHistoryId, setCurHistoryId] = useState(""); + const [deleteProjectModalOpen, setDeleteProjectModalOpen] = useState(false); + const [curProjectId, setCurProjectId] = useState(""); + const [refreshTrigger, setRefreshTrigger] = useState(0); + const [projectDeleteCallback, setProjectDeleteCallback] = useState<(() => Promise) | null>(null); const agentMap = { developer_agent: { name: t("dashboard.developer-agent"), @@ -136,9 +145,10 @@ export default function Project() { navigate(`/`); }; - const handleDelete = (id: string) => { + const handleDelete = (id: string, callback?: () => void) => { setCurHistoryId(id); setDeleteModalOpen(true); + if(callback) setDeleteCallback(callback); }; const confirmDelete = async () => { @@ -155,6 +165,29 @@ export default function Project() { } finally { setCurHistoryId(""); setDeleteModalOpen(false); + deleteCallback(); + } + }; + + const handleProjectDelete = (projectId: string, callback: () => Promise) => { + setCurProjectId(projectId); + setProjectDeleteCallback(() => callback); + setDeleteProjectModalOpen(true); + }; + + const confirmProjectDelete = async () => { + const projectId = curProjectId; + if (!projectId || !projectDeleteCallback) return; + + try { + // Execute the deletion callback provided by GroupedHistoryView + await projectDeleteCallback(); + } catch (error) { + console.error("Failed to delete project:", error); + } finally { + setCurProjectId(""); + setProjectDeleteCallback(null); + setDeleteProjectModalOpen(false); } }; @@ -202,16 +235,7 @@ export default function Project() { useEffect(() => { - const fetchHistoryTasks = async () => { - try { - const res = await proxyFetchGet(`/api/chat/histories`); - setHistoryTasks(res.items); - } catch (error) { - console.error("Failed to fetch history tasks:", error); - } - }; - - fetchHistoryTasks(); + fetchHistoryTasks(setHistoryTasks); }, []); // Feature flag to hide table view without deleting code @@ -219,7 +243,7 @@ export default function Project() { return (
- {/* alert dialog */} + {/* alert dialog for task deletion */} setDeleteModalOpen(false)} @@ -230,7 +254,18 @@ export default function Project() { cancelText={t("layout.cancel")} /> - {/* Header Section */} + {/* alert dialog for project deletion */} + setDeleteProjectModalOpen(false)} + onConfirm={confirmProjectDelete} + title={t("layout.delete-project") || "Delete Project"} + message={t("layout.delete-project-confirmation") || "Are you sure you want to delete this project and all its tasks? This action cannot be undone."} + confirmText={t("layout.delete")} + cancelText={t("layout.cancel")} + /> + + {/* Header Section */}
@@ -243,431 +278,22 @@ export default function Project() {
-
-
{t("dashboard.ongoing-tasks")}
-
- {TABLE_VIEW_ENABLED && ( - - setHistoryType(value as "table" | "list") - } - > - - - -
{t("dashboard.table")}
- - - -
{t("dashboard.list")}
-
- - - )} - - - {TABLE_VIEW_ENABLED && history_type === "table" ? ( - // Table -
- {Object.keys(chatStore.tasks).map((taskId) => { - const task = chatStore.tasks[taskId]; - return task.status != "finished" && !task.type ? ( -
{ - chatStore.setActiveTaskId(taskId); - navigate(`/`); - }} - className={`${ - chatStore.activeTaskId === taskId ? "!bg-white-100%" : "" - } relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] px-6 shadow-history-item`} - > -
-
- folder-icon -
-
- {task.summaryTask || t("dashboard.new-project")} -
-
- -
-
-
-
-
- {t("layout.tasks")} -
-
- {task.taskRunning?.filter( - (taskItem) => - taskItem.status === "completed" || - taskItem.status === "failed" - ).length || 0} - /{task.taskRunning?.length || 0} -
-
-
- {task.taskAssigning.map( - (taskAssigning) => - taskAssigning.status === "running" && ( -
- handleClickAgent( - taskId, - taskAssigning.agent_id as AgentNameType - ) - } - className={`transition-all duration-300 flex justify-start items-center gap-1 px-sm py-xs bg-menutabs-bg-default rounded-lg border border-solid border-white-100% ${ - agentMap[ - taskAssigning.type as keyof typeof agentMap - ]?.borderColor - }`} - > - -
- {taskAssigning.name} -
-
- ) - )} - {/* bottom spacer to avoid content touching the viewport edge on scroll */} -
-
-
-
- ) : ( - "" - ); - })} -
- ) : ( - // List -
- {Object.keys(chatStore.tasks).map((taskId) => { - const task = chatStore.tasks[taskId]; - return task.status != "finished" && !task.type ? ( -
{ - chatStore.setActiveTaskId(taskId); - navigate(`/`); - }} - className={`${ - chatStore.activeTaskId === taskId ? "!bg-white-100%" : "" - } max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-2xl flex justify-between items-center gap-md w-full p-3 h-14 shadow-history-item`} - > -
-
- folder-icon -
- - - {task.summaryTask || t("dashboard.new-project")} - - -

{task.summaryTask || t("dashboard.new-project")}

-
-
-
-
0 && - "border-x border-white-100%" - }`} - > - {task.taskAssigning && task.taskAssigning.map((taskAssigning) => ( -
- handleClickAgent( - taskId, - taskAssigning.agent_id as AgentNameType - ) - } - > - -
- { - agentIconMap[ - taskAssigning.type as keyof typeof agentIconMap - ] - } -
-
- ))} -
-
- {task.taskRunning?.filter( - (taskItem) => - taskItem.status === "completed" || - taskItem.status === "failed" - ).length || 0} - /{task.taskRunning?.length || 0} -
- {(chatStore.tasks[taskId].status === "running" || - chatStore.tasks[taskId].status === "pause") && ( - - )} - {(chatStore.tasks[taskId].status === "pause" || - chatStore.tasks[taskId].status === "pending") && ( - - - - - -
- - - -
-
-
- )} -
- ) : ( - "" - ); - })} -
- )} -
-
{t("dashboard.project-archives")}
-
- {historyTasks.length === 0 && TABLE_VIEW_ENABLED && ( - - setHistoryType(value as "table" | "list") - } - defaultValue="list" - className="" - > - - -
-
{t("dashboard.table")}
- - - -
{t("dashboard.list")}
-
- - - )} - - - {TABLE_VIEW_ENABLED && history_type === "table" ? ( - // Table -
- {historyTasks.map((task) => { - return ( -
handleSetActive(task.task_id, task.question)} - key={task.task_id} - className={`${ - chatStore.activeTaskId === task.task_id - ? "!bg-white-100%" - : "" - } relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-3xl flex justify-between items-center flex-wrap gap-md flex-initial w-[calc(33%-48px)] min-w-[300px] max-w-[500px] h-[180px] p-6 shadow-history-item border border-solid border-border-disabled`} - > -
- folder-icon -
- - {t("layout.token")} {task.tokens || 0} - -
-
-
-
- {task?.question || t("dashboard.new-project")} -
-
-
- ); - })} -
- ) : ( - // List -
- {historyTasks.map((task) => { - return ( -
{ - handleSetActive(task.task_id, task.question); - }} - key={task.task_id} - className={`${ - chatStore.activeTaskId === task.task_id - ? "!bg-white-100%" - : "" - } max-w-full relative cursor-pointer transition-all duration-300 bg-white-30% hover:bg-white-100% rounded-2xl flex justify-between items-center gap-md w-full p-3 h-14 shadow-history-item border border-solid border-border-disabled`} - > -
-
- folder-icon - -
- - - - {" "} - {task?.question.split("|")[0] || t("dashboard.new-project")} - - - -
- {" "} - {task?.question.split("|")[0] || t("dashboard.new-project")} -
-
-
-
- - {t("layout.token")} {task.tokens || 0} - - - - - - - -
- - - - - - - -
-
-
-
- ); - })} -
- )} + { + chatStore.setActiveTaskId(taskId); + navigate(`/`); + }} + onOngoingTaskPause={(taskId) => handleTakeControl("pause", taskId)} + onOngoingTaskResume={(taskId) => handleTakeControl("resume", taskId)} + onOngoingTaskDelete={(taskId) => handleDelete(taskId)} + onProjectDelete={handleProjectDelete} + refreshTrigger={refreshTrigger} + /> diff --git a/src/pages/History.tsx b/src/pages/History.tsx index 3f4f0ea4..5882285a 100644 --- a/src/pages/History.tsx +++ b/src/pages/History.tsx @@ -17,9 +17,8 @@ import { cn } from "@/lib/utils"; import { Hammer } from "@/components/animate-ui/icons/hammer"; import MCP from "./Setting/MCP"; import Browser from "./Dashboard/Browser"; -import folderIcon from "@/assets/Folder.svg"; -import SplitText from "@/components/ui/SplitText/SplitText"; import WordCarousel from "@/components/ui/WordCarousel"; +import { Sparkle } from "@/components/animate-ui/icons/sparkle"; @@ -121,7 +120,7 @@ export default function Home() {
v && setActiveTab(v as typeof activeTab)}> - }>{t("layout.projects")} + }>{t("layout.projects")} }>{t("layout.mcp-tools")} }>{t("layout.browser")} }>{t("layout.settings")} diff --git a/src/service/historyApi.ts b/src/service/historyApi.ts new file mode 100644 index 00000000..741339ff --- /dev/null +++ b/src/service/historyApi.ts @@ -0,0 +1,117 @@ +import { proxyFetchGet } from "@/api/http"; +import { HistoryTask, ProjectGroup, GroupedHistoryResponse } from "@/types/history"; + +// Group tasks by project_id and add project-level metadata +const groupTasksByProject = (tasks: HistoryTask[]): ProjectGroup[] => { + const projectMap = new Map(); + + tasks.forEach(task => { + const projectId = task.project_id; + + if (!projectMap.has(projectId)) { + projectMap.set(projectId, { + project_id: projectId, + project_name: task.project_name || `Project ${projectId}`, + total_tokens: 0, + task_count: 0, + latest_task_date: task.created_at || new Date().toISOString(), + tasks: [], + total_completed_tasks: 0, + total_failed_tasks: 0, + average_tokens_per_task: 0, + last_prompt: task.question || "", + }); + } + + const project = projectMap.get(projectId)!; + project.tasks.push(task); + project.task_count++; + project.total_tokens += task.tokens || 0; + + // Count status-based metrics + if (task.status === 2) { // Assuming 2 is completed + project.total_completed_tasks++; + } else if (task.status === 3) { // Assuming 3 is failed + project.total_failed_tasks++; + } + + // Update latest task date + if (task.created_at && task.created_at > project.latest_task_date) { + project.latest_task_date = task.created_at; + } + }); + + // Calculate averages and sort tasks within each project + projectMap.forEach(project => { + project.average_tokens_per_task = project.task_count > 0 + ? Math.round(project.total_tokens / project.task_count) + : 0; + + // Sort tasks by creation date (newest first) + project.tasks.sort((a, b) => { + const dateA = new Date(a.created_at || 0).getTime(); + const dateB = new Date(b.created_at || 0).getTime(); + return dateB - dateA; + }); + }); + + // Convert to array and sort by latest task date (newest first) + return Array.from(projectMap.values()).sort((a, b) => { + const dateA = new Date(a.latest_task_date).getTime(); + const dateB = new Date(b.latest_task_date).getTime(); + return dateB - dateA; + }); +}; + +export const fetchHistoryTasks = async (setTasks: React.Dispatch>) => { + try { + const res = await proxyFetchGet(`/api/chat/histories`); + setTasks(res.items) + } catch (error) { + console.error("Failed to fetch history tasks:", error); + setTasks([]) + } +}; + +// New function to fetch grouped history tasks from the backend endpoint +export const fetchGroupedHistoryTasks = async (setProjects: React.Dispatch>) => { + try { + const res = await proxyFetchGet(`/api/chat/histories/grouped?include_tasks=true`); + if(res.status !== 200) { + fetchGroupedHistoryTasksLegacy(setProjects); + } else setProjects(res.projects); + } catch (error) { + console.error("Failed to fetch grouped history summaries:", error); + setProjects([]); + } +}; + +// Function to fetch grouped history summaries only (without individual tasks for better performance) +export const fetchGroupedHistorySummaries = async (setProjects: React.Dispatch>) => { + try { + const res = await proxyFetchGet(`/api/chat/histories/grouped?include_tasks=false`); + if(res.status !== 200) { + fetchGroupedHistoryTasksLegacy(setProjects); + } else setProjects(res.projects); + } catch (error) { + console.error("Failed to fetch grouped history summaries:", error); + setProjects([]); + } +}; + +// Legacy function for backward compatibility - groups on frontend +export const fetchGroupedHistoryTasksLegacy = async (setProjects: React.Dispatch>) => { + try { + const res = await proxyFetchGet(`/api/chat/histories`); + const groupedProjects = groupTasksByProject(res.items); + setProjects(groupedProjects); + } catch (error) { + console.error("Failed to fetch grouped history tasks:", error); + setProjects([]); + } +}; + +// Utility function to get all tasks from grouped data (for backward compatibility) +export const flattenProjectTasks = (projects: ProjectGroup[]): HistoryTask[] => { + return projects.flatMap(project => project.tasks); +}; \ No newline at end of file diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 95a95c22..f50f9957 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -230,7 +230,7 @@ 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/${project_id}?delay_time=${delayTime}` + `${base_Url}/api/chat/steps/playback/${newTaskId}?delay_time=${delayTime}` : `${baseURL}/chat` const { tasks } = get() @@ -350,19 +350,14 @@ const chatStore = (initial?: Partial) => createStore()( } catch (error) { console.log('get-env-path error', error) } - - + // create history - if (!type && !historyId) { + if (!type) { const authStore = getAuthStore(); const obj = { - /** - * 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, + "project_id": project_id, + "task_id": newTaskId, "user_id": authStore.user_id, "question": messageContent || (targetChatStore.getState().tasks[newTaskId]?.messages[0]?.content ?? ''), "language": systemLanguage, @@ -496,9 +491,35 @@ const chatStore = (initial?: Partial) => createStore()( }); console.log("[NEW CHATSTORE] Created for ", project_id); - //Handle Original cases - with new chatStore - newChatStore.getState().setHasWaitComfirm(currentTaskId, false); - newChatStore.getState().setStatus(currentTaskId, 'pending'); + //Create a new history point + if (!type) { + const authStore = getAuthStore(); + + const obj = { + "project_id": project_id, + "task_id": newTaskId, + "user_id": authStore.user_id, + "question": question || messageContent || (targetChatStore.getState().tasks[newTaskId]?.messages[0]?.content ?? ''), + "language": systemLanguage, + "model_platform": apiModel.model_platform, + "model_type": apiModel.model_type, + "api_url": modelType === 'cloud' ? "cloud" : apiModel.api_url, + "max_retries": 3, + "file_save_path": "string", + "installed_mcp": "string", + "status": 1, + "tokens": 0 + } + 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); + }) + } } } else { //NOTE: Triggered only with first "confirmed" in the project diff --git a/src/store/globalStore.ts b/src/store/globalStore.ts index 2b5f56d9..2b425cd0 100644 --- a/src/store/globalStore.ts +++ b/src/store/globalStore.ts @@ -3,8 +3,8 @@ import { persist } from 'zustand/middleware'; // Define state types interface GlobalStore { - history_type: "table" | "list"; - setHistoryType: (history_type: "table" | "list") => void; + history_type: "grid" | "list" | "table"; + setHistoryType: (history_type: "grid" | "list" | "table") => void; toggleHistoryType: () => void; } @@ -12,13 +12,16 @@ interface GlobalStore { const globalStore = create()( persist( (set) => ({ - history_type: "list", - setHistoryType: (history_type: "table" | "list") => + history_type: "grid", + setHistoryType: (history_type: "grid" | "list" | "table") => set({ history_type }), toggleHistoryType: () => - set((state) => ({ - history_type: state.history_type === "table" ? "list" : "table", - })), + set((state) => { + // Cycle through: grid -> list -> table -> grid + if (state.history_type === "grid") return { history_type: "list" }; + if (state.history_type === "list") return { history_type: "table" }; + return { history_type: "grid" }; + }), }), { name: 'global-storage', diff --git a/src/store/projectStore.ts b/src/store/projectStore.ts index 78fa237e..7e726b3e 100644 --- a/src/store/projectStore.ts +++ b/src/store/projectStore.ts @@ -289,9 +289,6 @@ const projectStore = create()((set, get) => ({ return null; } - // Calculate total tokens across all chat stores in the project - const totalProjectTokens = getProjectTotalTokens(projectId); - // Create new chat store & append in the current project const newChatId = createChatStore(projectId, chatName); @@ -311,9 +308,6 @@ const projectStore = create()((set, get) => ({ // Create a new task in the new chat store with the queued content const newTaskId = newChatStore.getState().create(customTaskId); - // Accumulate project tokens - newChatStore.getState().addTokens(newTaskId, totalProjectTokens); - //Set the initTask as the active taskId newChatStore.getState().setActiveTaskId(newTaskId); diff --git a/src/style/token.css b/src/style/token.css index 65af5ff3..137ce863 100644 --- a/src/style/token.css +++ b/src/style/token.css @@ -356,10 +356,23 @@ --tag-fill-multimodal: var(--fill-multimodal); --tag-fill-socialmedia: var(--fill-socialmedia); --tag-fill-info: var(--surface-information); - --tag-text-info: var(--text-information); + --tag-foreground-info: var(--text-information); --tag-surface-hover: var(--button-tertiery-fill-hover); --tag-fill-success: var(--surface-success); - --tag-text-success: var(--text-success); + --tag-foreground-success: var(--text-success); + --tag-fill-warning: var(--surface-warning); + --tag-foreground-warning: var(--text-warning); + --tag-fill-cuation: var(--surface-cuation); + --tag-foreground-cuation: var(--text-cuation); + --tag-foreground-default: var(--text-body); + --tag-fill-default: var(--surface-tertiary); + --tag-fill-default-foreground: var(--surface-secondary); + + /* Project Component */ + --project-surface: var(--surface-tertiary); + --project-surface-hover: var(--surface-tertiary-hover); + --project-border-default: var(--border-tertiary); + --project-border-hover: var(--border-primary); /* Message Component */ --message-fill-default: var(--surface-tertiary); @@ -443,6 +456,7 @@ --surface-action-hover: var(--colors-primary-1); --surface-disabled: var(--colors-off-white-30); --surface-tertiary: var(--colors-white-100); + --surface-tertiary-hover: var(--colors-white-50); --surface-card: var(--colors-off-white-30); --surface-card-hover: var(--colors-off-white-80); --surface-card-focus: var(--colors-white-100); @@ -560,6 +574,7 @@ --surface-action-hover: var(--colors-primary-1); --surface-disabled: var(--colors-off-white-30); --surface-tertiary: var(--colors-white-100); + --surface-tertiary-hover: var(--colors-white-50); --surface-card: var(--colors-off-white-30); --surface-card-hover: var(--colors-off-white-80); --surface-card-focus: var(--colors-white-100); diff --git a/src/types/history.d.ts b/src/types/history.d.ts new file mode 100644 index 00000000..3abcbed0 --- /dev/null +++ b/src/types/history.d.ts @@ -0,0 +1,71 @@ +// History API types for project-grouped structure + +export interface HistoryTask { + id: number; + task_id: string; + project_id: string; + question: string; + language: string; + model_platform: string; + model_type: string; + api_key?: string; + api_url?: string; + max_retries: number; + file_save_path?: string; + installed_mcp?: string; + project_name?: string; + summary?: string; + tokens: number; + status: number; + created_at?: string; + updated_at?: string; +} + +export interface OngoingTask { + task_id: string; + project_id?: string; + project_name?: string; + question: string; + status: 'running' | 'pending' | 'pause'; + tokens: number; + created_at?: string; + taskAssigning?: any[]; + taskRunning?: any[]; + progressValue?: number; +} + +export interface ProjectGroup { + project_id: string; + project_name?: string; + total_tokens: number; + task_count: number; + latest_task_date: string; + last_prompt: string; + tasks: HistoryTask[]; + ongoing_tasks?: OngoingTask[]; // Add ongoing tasks to the project group + // Additional project-level metadata + total_completed_tasks: number; + total_failed_tasks: number; + total_ongoing_tasks?: number; + average_tokens_per_task: number; +} + +export interface GroupedHistoryResponse { + projects: ProjectGroup[]; + total_projects: number; + total_tasks: number; + total_tokens: number; +} + +// Legacy flat response for backward compatibility +export interface FlatHistoryResponse { + items: HistoryTask[]; + total: number; + page: number; + size: number; +} + +export interface HistoryApiOptions { + grouped?: boolean; // New parameter to control response format + include_tasks?: boolean; // Whether to include individual tasks in groups +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 1293711a..cb0e2014 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -391,10 +391,25 @@ module.exports = { "fill-multimodal": "var(--tag-fill-multimodal)", "fill-socialmedia": "var(--tag-fill-socialmedia)", "fill-info": "var(--tag-fill-info)", + "foreground-info": "var(--tag-foreground-info)", "text-info": "var(--tag-text-info)", "surface-hover": "var(--tag-surface-hover)", "fill-success": "var(--tag-fill-success)", + "foreground-success": "var(--tag-foreground-success)", "text-success": "var(--tag-text-success)", + "fill-warning": "var(--tag-fill-warning)", + "foreground-warning": "var(--tag-foreground-warning)", + "fill-cuation": "var(--tag-fill-cuation)", + "foreground-cuation": "var(--tag-foreground-cuation)", + "fill-default": "var(--tag-fill-default)", + "foreground-default": "var(--tag-foreground-default)", + "fill-default-foreground": "var(--tag-fill-default-foreground)", + }, + project: { + "surface-default": "var(--project-surface)", + "surface-hover": "var(--project-surface-hover)", + "border-default": "var(--project-border-default)", + "border-hover": "var(--project-border-hover)", }, message: { "fill-default": "var(--message-fill-default)", diff --git a/utils/__pycache__/__init__.cpython-310.pyc b/utils/__pycache__/__init__.cpython-310.pyc index 10d1d4a6..57ecb1ad 100644 Binary files a/utils/__pycache__/__init__.cpython-310.pyc and b/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc index d695ee21..b8d9747f 100644 Binary files a/utils/__pycache__/traceroot_wrapper.cpython-310.pyc and b/utils/__pycache__/traceroot_wrapper.cpython-310.pyc differ