+
+
+ {searchValue
+ ? t("dashboard.no-projects-match-search")
+ : t("dashboard.no-projects-found")
+ }
+
+ {searchValue && (
+
+ {t("dashboard.try-different-search")}
+
+ )}
+
+ );
+ }
+
+ return (
+
+
+ {filteredProjects.map((project) => (
+
+
+
+ ))}
+
+
+ {/* Summary footer */}
+
+
+ {filteredProjects.length} {t("layout.projects")} • {" "}
+ {filteredProjects.reduce((total, project) => total + project.task_count, 0)} {t("layout.total-tasks")} • {" "}
+ {filteredProjects.reduce((total, project) => total + project.total_tokens, 0).toLocaleString()} {t("layout.total-tokens")}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/HistorySidebar/index.tsx b/src/components/HistorySidebar/index.tsx
index e6a2b8713..7de59f81a 100644
--- a/src/components/HistorySidebar/index.tsx
+++ b/src/components/HistorySidebar/index.tsx
@@ -40,6 +40,7 @@ import { useTranslation } from "react-i18next";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
import {getAuthStore} from "@/store/authStore";
import { fetchHistoryTasks } from "@/service/historyApi";
+import GroupedHistoryView from "@/components/GroupedHistoryView";
export default function HistorySidebar() {
const { t } = useTranslation();
@@ -497,6 +498,16 @@ export default function HistorySidebar() {
);
})}
+ ) : history_type === "grouped" ? (
+ // Grouped view
+ ([]);
+ const { history_type } = useGlobalStore();
//Get Chatstore for the active project's task
const { chatStore, projectStore } = useChatStoreAdapter();
if (!chatStore) {
@@ -52,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);
@@ -60,10 +63,20 @@ export function SearchHistoryDialog() {
};
const handleReplay = async (projectId: string, question: string, historyId: string) => {
- close();
+ setOpen(false);
await replayProject(projectStore, navigate, projectId, question, historyId);
};
+ const handleDelete = (taskId: string) => {
+ // TODO: Implement delete functionality similar to HistorySidebar
+ console.log("Delete task:", taskId);
+ };
+
+ const handleShare = (taskId: string) => {
+ // TODO: Implement share functionality similar to HistorySidebar
+ console.log("Share task:", taskId);
+ };
+
useEffect(() => {
fetchHistoryTasks(setHistoryTasks);
}, []);
@@ -85,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 === "grouped" ? (
+
+
+
+ ) : (
+
+ {historyTasks.map((task) => (
+ handleSetActive(task.task_id, task.question, task.id)}
+ >
+
+
+ {task.question}
+
+
+ ))}
+
+ )}
diff --git a/src/pages/Dashboard/Project.tsx b/src/pages/Dashboard/Project.tsx
index 2acd57b72..9a8b1f04a 100644
--- a/src/pages/Dashboard/Project.tsx
+++ b/src/pages/Dashboard/Project.tsx
@@ -42,6 +42,7 @@ 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() {
@@ -548,6 +549,16 @@ export default function Project() {
);
})}
+ ) : history_type === "grouped" ? (
+ // Grouped view
+
+
+
) : (
// List
diff --git a/src/service/historyApi.ts b/src/service/historyApi.ts
index a3a881e97..d12fc0ced 100644
--- a/src/service/historyApi.ts
+++ b/src/service/historyApi.ts
@@ -1,4 +1,66 @@
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
+ });
+ }
+
+ 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 {
@@ -8,4 +70,21 @@ export const fetchHistoryTasks = async (setTasks: 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/globalStore.ts b/src/store/globalStore.ts
index 2b5f56d9a..7cb816212 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: "table" | "list" | "grouped";
+ setHistoryType: (history_type: "table" | "list" | "grouped") => void;
toggleHistoryType: () => void;
}
@@ -12,13 +12,16 @@ interface GlobalStore {
const globalStore = create()(
persist(
(set) => ({
- history_type: "list",
- setHistoryType: (history_type: "table" | "list") =>
+ history_type: "grouped",
+ setHistoryType: (history_type: "table" | "list" | "grouped") =>
set({ history_type }),
toggleHistoryType: () =>
- set((state) => ({
- history_type: state.history_type === "table" ? "list" : "table",
- })),
+ set((state) => {
+ // Cycle through: grouped -> list -> table -> grouped
+ if (state.history_type === "grouped") return { history_type: "list" };
+ if (state.history_type === "list") return { history_type: "table" };
+ return { history_type: "grouped" };
+ }),
}),
{
name: 'global-storage',
diff --git a/src/types/history.d.ts b/src/types/history.d.ts
new file mode 100644
index 000000000..1ccc511ad
--- /dev/null
+++ b/src/types/history.d.ts
@@ -0,0 +1,55 @@
+// 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 ProjectGroup {
+ project_id: string;
+ project_name?: string;
+ total_tokens: number;
+ task_count: number;
+ latest_task_date: string;
+ tasks: HistoryTask[];
+ // Additional project-level metadata
+ total_completed_tasks: number;
+ total_failed_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