diff --git a/src/components/GroupedHistoryView/ProjectDialog.tsx b/src/components/GroupedHistoryView/ProjectDialog.tsx index c9c8db6f1..afd7b3059 100644 --- a/src/components/GroupedHistoryView/ProjectDialog.tsx +++ b/src/components/GroupedHistoryView/ProjectDialog.tsx @@ -22,7 +22,7 @@ import { } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { useProjectStore } from '@/store/projectStore'; -import { ProjectGroup } from '@/types/history'; +import { HistoryTask, ProjectGroup } from '@/types/history'; import { CheckCircle, Clock, @@ -46,7 +46,7 @@ interface ProjectDialogProps { historyId: string, project?: ProjectGroup ) => void; - onTaskDelete: (taskId: string) => void; + onTaskDelete: (historyId: string, task?: HistoryTask) => void; onTaskShare: (taskId: string) => void; activeTaskId?: string; } @@ -256,8 +256,12 @@ export default function ProjectDialog({ project ) } - onDelete={() => onTaskDelete(task.id.toString())} - onShare={() => onTaskShare(task.id.toString())} + onDelete={() => + onTaskDelete(task.id.toString(), task) + } + onShare={() => + onTaskShare(task.task_id || task.id.toString()) + } isLast={index === project.tasks.length - 1} showActions={false} /> diff --git a/src/components/GroupedHistoryView/ProjectGroup.tsx b/src/components/GroupedHistoryView/ProjectGroup.tsx index 3980a6671..8383d8784 100644 --- a/src/components/GroupedHistoryView/ProjectGroup.tsx +++ b/src/components/GroupedHistoryView/ProjectGroup.tsx @@ -25,7 +25,7 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { loadProjectFromHistory } from '@/lib/replay'; import { useProjectStore } from '@/store/projectStore'; import { ChatTaskStatus } from '@/types/constants'; -import { ProjectGroup as ProjectGroupType } from '@/types/history'; +import { HistoryTask, ProjectGroup as ProjectGroupType } from '@/types/history'; import { motion } from 'framer-motion'; import { Edit, @@ -51,7 +51,7 @@ interface ProjectGroupProps { historyId: string, project?: ProjectGroupType ) => void; - onTaskDelete: (taskId: string) => void; + onTaskDelete: (historyId: string, task?: HistoryTask) => void; onTaskShare: (taskId: string) => void; activeTaskId?: string; searchValue?: string; diff --git a/src/components/GroupedHistoryView/index.tsx b/src/components/GroupedHistoryView/index.tsx index 25528d829..08a37645d 100644 --- a/src/components/GroupedHistoryView/index.tsx +++ b/src/components/GroupedHistoryView/index.tsx @@ -19,7 +19,7 @@ import { fetchGroupedHistoryTasks } from '@/service/historyApi'; import { getAuthStore } from '@/store/authStore'; import { useGlobalStore } from '@/store/globalStore'; import { useProjectStore } from '@/store/projectStore'; -import { ProjectGroup as ProjectGroupType } from '@/types/history'; +import { HistoryTask, ProjectGroup as ProjectGroupType } from '@/types/history'; import { AnimatePresence, motion } from 'framer-motion'; import { FolderOpen, LayoutGrid, List, Pin, Sparkle } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -34,7 +34,11 @@ interface GroupedHistoryViewProps { historyId: string, project?: ProjectGroupType ) => void; - onTaskDelete: (historyId: string, callback: () => void) => void; + onTaskDelete: ( + historyId: string, + task: HistoryTask | undefined, + callback: () => void + ) => void; onTaskShare: (taskId: string) => void; activeTaskId?: string; refreshTrigger?: number; // For triggering refresh from parent @@ -82,9 +86,12 @@ export default function GroupedHistoryView({ } }; - const onDelete = (historyId: string) => { + const onDelete = (historyId: string, task?: HistoryTask) => { try { - onTaskDelete(historyId, () => { + const targetTask = task ?? projects.flatMap((p) => p.tasks).find( + (t) => String(t.id) === historyId + ); + onTaskDelete(historyId, targetTask, () => { setProjects((prevProjects) => { // Create new project objects instead of mutating existing ones return prevProjects diff --git a/src/components/SearchHistoryDialog.tsx b/src/components/SearchHistoryDialog.tsx index 1e33a03fc..bdd85f6ac 100644 --- a/src/components/SearchHistoryDialog.tsx +++ b/src/components/SearchHistoryDialog.tsx @@ -14,9 +14,7 @@ 'use client'; -import { ScanFace, Search } from 'lucide-react'; -import { useEffect, useState } from 'react'; - +import { proxyFetchDelete } from '@/api/http'; import GroupedHistoryView from '@/components/GroupedHistoryView'; import { CommandDialog, @@ -29,18 +27,57 @@ import { } from '@/components/ui/command'; import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { loadProjectFromHistory } from '@/lib'; +import { share } from '@/lib/share'; import { fetchHistoryTasks } from '@/service/historyApi'; +import { getAuthStore } from '@/store/authStore'; import { useGlobalStore } from '@/store/globalStore'; +import { HistoryTask } from '@/types/history'; import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; +import { Ellipsis, ScanFace, Search, Share2, Trash2 } from 'lucide-react'; +import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import AlertDialog from '@/components/ui/alertDialog'; import { Button } from './ui/button'; import { DialogTitle } from './ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './ui/dropdown-menu'; + +type IpcRendererLike = { + invoke: ( + channel: string, + email: string, + taskId: string, + projectId?: string + ) => Promise; +}; + +function getIpcRenderer(): IpcRendererLike | undefined { + const w = window as unknown as { ipcRenderer?: IpcRendererLike }; + return w.ipcRenderer; +} + +type HistoryListItem = HistoryTask & { + tasks?: { task_id: string }[]; + project_name?: string; + project_id?: string; +}; export function SearchHistoryDialog() { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const [historyTasks, setHistoryTasks] = useState([]); + const [historyTasks, setHistoryTasks] = useState([]); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [pendingDelete, setPendingDelete] = useState<{ + historyId: string; + task: HistoryTask | undefined; + callback: () => void; + } | null>(null); const { history_type } = useGlobalStore(); //Get Chatstore for the active project's task const { chatStore, projectStore } = useChatStoreAdapter(); @@ -75,14 +112,60 @@ export function SearchHistoryDialog() { } }; - const handleDelete = (taskId: string) => { - // TODO: Implement delete functionality similar to HistorySidebar - console.log('Delete task:', taskId); + const handleDelete = async ( + historyId: string, + task: HistoryTask | undefined, + callback: () => void + ) => { + try { + await proxyFetchDelete(`/api/chat/history/${historyId}`); + + const ipcRenderer = getIpcRenderer(); + if (task?.task_id && ipcRenderer) { + const { email } = getAuthStore(); + const safeEmail = email ?? ''; + try { + await ipcRenderer.invoke( + 'delete-task-files', + safeEmail, + task.task_id, + task.project_id + ); + } catch (error) { + console.warn('Local file cleanup failed:', error); + } + } + + callback(); + toast.success(t('layout.delete-success') || 'Task deleted successfully'); + } catch (error) { + console.error('Failed to delete history task:', error); + toast.error(t('layout.delete-failed') || 'Failed to delete task'); + } + }; + + const handleShare = async (taskId: string) => { + setOpen(false); + await share(taskId); + }; + + const openDeleteConfirm = ( + historyId: string, + task: HistoryTask | undefined, + callback: () => void + ) => { + setPendingDelete({ historyId, task, callback }); + setDeleteDialogOpen(true); }; - const handleShare = (taskId: string) => { - // TODO: Implement share functionality similar to HistorySidebar - console.log('Share task:', taskId); + const confirmDelete = () => { + if (!pendingDelete) return; + void handleDelete( + pendingDelete.historyId, + pendingDelete.task, + pendingDelete.callback + ); + setPendingDelete(null); }; useEffect(() => { @@ -122,26 +205,67 @@ export function SearchHistoryDialog() { ) : ( - {historyTasks.map((task) => ( + {historyTasks.map((task: HistoryListItem) => ( handleSetActive( - task.task_id, + task.project_id || task.task_id, task.question, - String(task.id) + String(task.id), + { + tasks: task.tasks || [{ task_id: task.task_id }], + project_name: task.project_name, + } ) } > - -
- {task.question} +
+ + + {task.question} +
+ + + + + + { + e.stopPropagation(); + handleShare(task.task_id || String(task.id)); + }} + > + + {t('layout.share')} + + { + e.stopPropagation(); + openDeleteConfirm(String(task.id), task, () => { + setHistoryTasks((prev: HistoryListItem[]) => + prev.filter( + (t: HistoryListItem) => t.id !== task.id + ) + ); + }); + }} + > + + {t('layout.delete')} + + + ))} @@ -149,6 +273,19 @@ export function SearchHistoryDialog() { + { + setDeleteDialogOpen(false); + setPendingDelete(null); + }} + onConfirm={confirmDelete} + title={t('layout.delete-task')} + message={t('layout.delete-task-confirmation')} + confirmText={t('layout.delete')} + cancelText={t('layout.cancel')} + confirmVariant="cuation" + /> ); } diff --git a/src/i18n/locales/ar/layout.json b/src/i18n/locales/ar/layout.json index ae10e2abd..bebd4640f 100644 --- a/src/i18n/locales/ar/layout.json +++ b/src/i18n/locales/ar/layout.json @@ -52,6 +52,8 @@ "welcome": "أهلاً وسهلاً", "delete-task": "حذف المهمة", "delete-task-confirmation": "هل أنت متأكد من أنك تريد حذف هذه المهمة؟ لا يمكن التراجع عن هذا الإجراء.", + "delete-success": "تم حذف المهمة بنجاح", + "delete-failed": "فشل حذف المهمة", "delete": "حذف", "cancel": "إلغاء", "continue": "متابعة", diff --git a/src/i18n/locales/de/layout.json b/src/i18n/locales/de/layout.json index d6f78e435..553cca29c 100644 --- a/src/i18n/locales/de/layout.json +++ b/src/i18n/locales/de/layout.json @@ -52,6 +52,8 @@ "welcome": "Willkommen", "delete-task": "Aufgabe löschen", "delete-task-confirmation": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.", + "delete-success": "Aufgabe erfolgreich gelöscht", + "delete-failed": "Aufgabe konnte nicht gelöscht werden", "delete": "Löschen", "cancel": "Abbrechen", "continue": "Fortfahren", diff --git a/src/i18n/locales/en-us/layout.json b/src/i18n/locales/en-us/layout.json index f56d4ebc4..067f7be6c 100644 --- a/src/i18n/locales/en-us/layout.json +++ b/src/i18n/locales/en-us/layout.json @@ -53,6 +53,8 @@ "welcome": "Welcome", "delete-task": "Delete Task", "delete-task-confirmation": "Are you sure you want to delete this task? This action cannot be undone.", + "delete-success": "Task deleted successfully", + "delete-failed": "Failed to delete task", "delete": "Delete", "cancel": "Cancel", "continue": "Continue", diff --git a/src/i18n/locales/es/layout.json b/src/i18n/locales/es/layout.json index 164cdb25e..046c04f5b 100644 --- a/src/i18n/locales/es/layout.json +++ b/src/i18n/locales/es/layout.json @@ -52,6 +52,8 @@ "welcome": "Bienvenido", "delete-task": "Eliminar Tarea", "delete-task-confirmation": "¿Estás seguro de que quieres eliminar esta tarea? Esta acción no se puede deshacer.", + "delete-success": "Tarea eliminada correctamente", + "delete-failed": "No se pudo eliminar la tarea", "delete": "Eliminar", "cancel": "Cancelar", "continue": "Continuar", diff --git a/src/i18n/locales/fr/layout.json b/src/i18n/locales/fr/layout.json index 562de3fc5..1785cd3a2 100644 --- a/src/i18n/locales/fr/layout.json +++ b/src/i18n/locales/fr/layout.json @@ -52,6 +52,8 @@ "welcome": "Bienvenue", "delete-task": "Supprimer la Tâche", "delete-task-confirmation": "Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action ne peut pas être annulée.", + "delete-success": "Tâche supprimée avec succès", + "delete-failed": "Échec de la suppression de la tâche", "delete": "Supprimer", "cancel": "Annuler", "continue": "Continuer", diff --git a/src/i18n/locales/it/layout.json b/src/i18n/locales/it/layout.json index 29289b3ba..7b7fe7e1d 100644 --- a/src/i18n/locales/it/layout.json +++ b/src/i18n/locales/it/layout.json @@ -52,6 +52,8 @@ "welcome": "Benvenuto", "delete-task": "Elimina Attività", "delete-task-confirmation": "Sei sicuro di voler eliminare questa attività? Questa azione non può essere annullata.", + "delete-success": "Attività eliminata con successo", + "delete-failed": "Impossibile eliminare l'attività", "delete": "Elimina", "cancel": "Annulla", "continue": "Continua", diff --git a/src/i18n/locales/ja/layout.json b/src/i18n/locales/ja/layout.json index cdbdcddb3..1e09c1e2e 100644 --- a/src/i18n/locales/ja/layout.json +++ b/src/i18n/locales/ja/layout.json @@ -52,6 +52,8 @@ "welcome": "ようこそ", "delete-task": "タスクを削除", "delete-task-confirmation": "このタスクを削除してもよろしいですか?この操作は元に戻せません。", + "delete-success": "タスクを削除しました", + "delete-failed": "タスクの削除に失敗しました", "delete": "削除", "cancel": "キャンセル", "continue": "続行", diff --git a/src/i18n/locales/ko/layout.json b/src/i18n/locales/ko/layout.json index 8d63e507b..8681d8823 100644 --- a/src/i18n/locales/ko/layout.json +++ b/src/i18n/locales/ko/layout.json @@ -52,6 +52,8 @@ "welcome": "환영", "delete-task": "작업 삭제", "delete-task-confirmation": "이 작업을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.", + "delete-success": "작업이 삭제되었습니다", + "delete-failed": "작업 삭제에 실패했습니다", "delete": "삭제", "cancel": "취소", "continue": "계속", diff --git a/src/i18n/locales/ru/layout.json b/src/i18n/locales/ru/layout.json index 07079bd04..5d08b8ebc 100644 --- a/src/i18n/locales/ru/layout.json +++ b/src/i18n/locales/ru/layout.json @@ -52,6 +52,8 @@ "welcome": "Добро пожаловать", "delete-task": "Удалить задачу", "delete-task-confirmation": "Вы уверены, что хотите удалить эту задачу? Это действие нельзя отменить.", + "delete-success": "Задача удалена", + "delete-failed": "Не удалось удалить задачу", "delete": "Удалить", "cancel": "Отмена", "continue": "Продолжить", diff --git a/src/i18n/locales/zh-Hans/layout.json b/src/i18n/locales/zh-Hans/layout.json index ce7bce0cb..8ba7d4da1 100644 --- a/src/i18n/locales/zh-Hans/layout.json +++ b/src/i18n/locales/zh-Hans/layout.json @@ -55,6 +55,8 @@ "welcome": "欢迎", "delete-task": "删除任务", "delete-task-confirmation": "您确定要删除此任务吗?此操作无法撤销。", + "delete-success": "任务删除成功", + "delete-failed": "删除任务失败", "delete": "删除", "cancel": "取消", "continue": "继续", diff --git a/src/i18n/locales/zh-Hant/layout.json b/src/i18n/locales/zh-Hant/layout.json index 184a24ab5..3aacdd2de 100644 --- a/src/i18n/locales/zh-Hant/layout.json +++ b/src/i18n/locales/zh-Hant/layout.json @@ -54,6 +54,8 @@ "welcome": "歡迎", "delete-task": "刪除任務", "delete-task-confirmation": "您確定要刪除此任務嗎?此操作無法撤銷。", + "delete-success": "任務已成功刪除", + "delete-failed": "刪除任務失敗", "delete": "刪除", "cancel": "取消", "continue": "繼續", diff --git a/src/pages/Projects/Project.tsx b/src/pages/Projects/Project.tsx index dce96a3c7..ff3e5aa58 100644 --- a/src/pages/Projects/Project.tsx +++ b/src/pages/Projects/Project.tsx @@ -19,6 +19,7 @@ import useChatStoreAdapter from '@/hooks/useChatStoreAdapter'; import { loadProjectFromHistory } from '@/lib'; import { share } from '@/lib/share'; import { fetchHistoryTasks } from '@/service/historyApi'; +import { HistoryTask } from '@/types/history'; import { ChatTaskStatus } from '@/types/constants'; import { Bird, CodeXml, FileText, Globe, Image } from 'lucide-react'; import { useEffect, useState } from 'react'; @@ -131,8 +132,12 @@ export default function Project() { navigate(`/`); }; - const handleDelete = (id: string, callback?: () => void) => { - setCurHistoryId(id); + const handleDelete = ( + historyId: string, + _task?: HistoryTask, + callback?: () => void + ) => { + setCurHistoryId(historyId); setDeleteModalOpen(true); if (callback) setDeleteCallback(callback); }; @@ -296,7 +301,9 @@ export default function Project() { onOngoingTaskResume={(taskId) => handleTakeControl('resume', taskId) } - onOngoingTaskDelete={(taskId) => handleDelete(taskId)} + onOngoingTaskDelete={(taskId) => + handleDelete(taskId, undefined, () => {}) + } onProjectDelete={handleProjectDelete} refreshTrigger={refreshTrigger} />