From 22d900161503ae180f5bd2cc7fbf6c2d083d1942 Mon Sep 17 00:00:00 2001 From: gjzhou <92616697+PotatoZhou@users.noreply.github.com> Date: Sat, 6 Dec 2025 10:45:05 +0800 Subject: [PATCH 1/4] feat: Add Delete Button for Strategy Prompts --- frontend/src/api/strategy.ts | 16 +++ .../valuecell/form/trading-strategy-form.tsx | 99 ++++++++++++++++++- .../server/api/routers/strategy_prompts.py | 28 ++++++ .../valuecell/server/api/schemas/strategy.py | 8 ++ .../db/repositories/strategy_repository.py | 28 ++++++ 5 files changed, 175 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/strategy.ts b/frontend/src/api/strategy.ts index f9c2a9fbe..6e08cde07 100644 --- a/frontend/src/api/strategy.ts +++ b/frontend/src/api/strategy.ts @@ -163,6 +163,22 @@ export const useCreateStrategyPrompt = () => { }); }; +export const useDeleteStrategyPrompt = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (promptId: string) => + apiClient.delete< + ApiResponse<{ deleted: boolean; prompt_id: string; message: string }> + >(`/strategies/prompts/${promptId}`), + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: API_QUERY_KEYS.STRATEGY.strategyPrompts, + }); + }, + }); +}; + export const useStrategyPerformance = (strategyId: number | null) => { return useQuery({ queryKey: API_QUERY_KEYS.STRATEGY.strategyPerformance( diff --git a/frontend/src/components/valuecell/form/trading-strategy-form.tsx b/frontend/src/components/valuecell/form/trading-strategy-form.tsx index 5d3149ffb..57ba3d36b 100644 --- a/frontend/src/components/valuecell/form/trading-strategy-form.tsx +++ b/frontend/src/components/valuecell/form/trading-strategy-form.tsx @@ -1,8 +1,22 @@ import { MultiSelect } from "@valuecell/multi-select"; -import { Eye, Plus } from "lucide-react"; -import { useCreateStrategyPrompt } from "@/api/strategy"; +import { Eye, Plus, Trash2 } from "lucide-react"; +import { useState } from "react"; +import { + useCreateStrategyPrompt, + useDeleteStrategyPrompt, +} from "@/api/strategy"; import NewPromptModal from "@/app/agent/components/strategy-items/modals/new-prompt-modal"; import ViewStrategyModal from "@/app/agent/components/strategy-items/modals/view-strategy-modal"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Field, @@ -37,6 +51,38 @@ export const TradingStrategyForm = withForm({ }, render({ form, prompts, tradingMode }) { const { mutateAsync: createStrategyPrompt } = useCreateStrategyPrompt(); + const { mutate: deleteStrategyPrompt } = useDeleteStrategyPrompt(); + const [deletePromptId, setDeletePromptId] = useState(null); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + + const handleDeletePrompt = (promptId: string) => { + setDeletePromptId(promptId); + setIsDeleteDialogOpen(true); + }; + + const confirmDeletePrompt = () => { + if (deletePromptId) { + deleteStrategyPrompt(deletePromptId, { + onSuccess: () => { + // If the deleted prompt was currently selected, clear the selection + if (form.state.values.template_id === deletePromptId) { + form.setFieldValue("template_id", ""); + } + setIsDeleteDialogOpen(false); + setDeletePromptId(null); + }, + onError: () => { + setIsDeleteDialogOpen(false); + setDeletePromptId(null); + }, + }); + } + }; + + const cancelDeletePrompt = () => { + setIsDeleteDialogOpen(false); + setDeletePromptId(null); + }; return ( @@ -158,8 +204,25 @@ export const TradingStrategyForm = withForm({ {prompts.length > 0 && prompts.map((prompt) => ( - - {prompt.name} + + {prompt.name} + {field.state.value !== prompt.id && ( + + )} ))} + + {/* Delete Confirmation Dialog */} + + + + Delete Strategy Prompt + + Are you sure you want to delete this strategy prompt? This + action cannot be undone and will permanently remove the prompt + from the system. + + + + + Cancel + + + Confirm Delete + + + + ); }, diff --git a/python/valuecell/server/api/routers/strategy_prompts.py b/python/valuecell/server/api/routers/strategy_prompts.py index 0b838dfcd..0771b14a2 100644 --- a/python/valuecell/server/api/routers/strategy_prompts.py +++ b/python/valuecell/server/api/routers/strategy_prompts.py @@ -11,6 +11,8 @@ from valuecell.server.api.schemas.strategy import ( PromptCreateRequest, PromptCreateResponse, + PromptDeleteResponse, + PromptDeleteSuccessResponse, PromptItem, PromptListResponse, ) @@ -66,4 +68,30 @@ async def create_prompt( except Exception as e: # noqa: BLE001 raise HTTPException(status_code=500, detail=f"Failed to create prompt: {e}") + @router.delete( + "/{prompt_id}", + response_model=PromptDeleteSuccessResponse, + summary="Delete a strategy prompt", + description="Permanently delete a strategy prompt by its ID.", + ) + async def delete_prompt(prompt_id: str, db: Session = Depends(get_db)) -> PromptDeleteSuccessResponse: + try: + repo = get_strategy_repository(db_session=db) + deleted = repo.delete_prompt(prompt_id=prompt_id) + if deleted: + return SuccessResponse.create( + data=PromptDeleteResponse( + deleted=True, + prompt_id=prompt_id, + message="Prompt successfully deleted" + ), + msg="Prompt deleted successfully" + ) + else: + raise HTTPException(status_code=404, detail="Prompt not found") + except HTTPException: + raise + except Exception as e: # noqa: BLE001 + raise HTTPException(status_code=500, detail=f"Failed to delete prompt: {e}") + return router diff --git a/python/valuecell/server/api/schemas/strategy.py b/python/valuecell/server/api/schemas/strategy.py index 874119e1f..fb04d4b35 100644 --- a/python/valuecell/server/api/schemas/strategy.py +++ b/python/valuecell/server/api/schemas/strategy.py @@ -238,6 +238,14 @@ class PromptCreateRequest(BaseModel): PromptCreateResponse = SuccessResponse[PromptItem] +class PromptDeleteResponse(BaseModel): + deleted: bool = Field(..., description="Whether the prompt was successfully deleted") + prompt_id: str = Field(..., description="ID of the deleted prompt") + message: str = Field(..., description="Delete operation result message") + +PromptDeleteSuccessResponse = SuccessResponse[PromptDeleteResponse] + + class StrategyPerformanceData(BaseModel): """Performance overview for a strategy including ROI and config.""" diff --git a/python/valuecell/server/db/repositories/strategy_repository.py b/python/valuecell/server/db/repositories/strategy_repository.py index 8ba8f3fd6..fab4bba89 100644 --- a/python/valuecell/server/db/repositories/strategy_repository.py +++ b/python/valuecell/server/db/repositories/strategy_repository.py @@ -584,6 +584,34 @@ def get_prompt_by_id(self, prompt_id: str) -> Optional[StrategyPrompt]: if not self.db_session: session.close() + def delete_prompt(self, prompt_id: str) -> bool: + """Delete a prompt by prompt_id. + + Returns True on success, False if the prompt does not exist or on error. + """ + session = self._get_session() + try: + # Ensure the prompt exists + prompt = ( + session.query(StrategyPrompt) + .filter(StrategyPrompt.id == prompt_id) + .first() + ) + if not prompt: + return False + + session.query(StrategyPrompt).filter(StrategyPrompt.id == prompt_id).delete( + synchronize_session=False + ) + session.commit() + return True + except Exception: + session.rollback() + return False + finally: + if not self.db_session: + session.close() + def delete_strategy(self, strategy_id: str, cascade: bool = True) -> bool: """Delete a strategy by strategy_id. From bca9ac5331ddc5ab72a0d78d13a5b033d8085ff9 Mon Sep 17 00:00:00 2001 From: gjzhou <92616697+PotatoZhou@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:08:39 +0800 Subject: [PATCH 2/4] style(server): format Python code --- python/valuecell/server/api/routers/strategy_prompts.py | 8 +++++--- python/valuecell/server/api/schemas/strategy.py | 5 ++++- .../server/db/repositories/strategy_repository.py | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/python/valuecell/server/api/routers/strategy_prompts.py b/python/valuecell/server/api/routers/strategy_prompts.py index 0771b14a2..2ffca1ef2 100644 --- a/python/valuecell/server/api/routers/strategy_prompts.py +++ b/python/valuecell/server/api/routers/strategy_prompts.py @@ -74,7 +74,9 @@ async def create_prompt( summary="Delete a strategy prompt", description="Permanently delete a strategy prompt by its ID.", ) - async def delete_prompt(prompt_id: str, db: Session = Depends(get_db)) -> PromptDeleteSuccessResponse: + async def delete_prompt( + prompt_id: str, db: Session = Depends(get_db) + ) -> PromptDeleteSuccessResponse: try: repo = get_strategy_repository(db_session=db) deleted = repo.delete_prompt(prompt_id=prompt_id) @@ -83,9 +85,9 @@ async def delete_prompt(prompt_id: str, db: Session = Depends(get_db)) -> Prompt data=PromptDeleteResponse( deleted=True, prompt_id=prompt_id, - message="Prompt successfully deleted" + message="Prompt successfully deleted", ), - msg="Prompt deleted successfully" + msg="Prompt deleted successfully", ) else: raise HTTPException(status_code=404, detail="Prompt not found") diff --git a/python/valuecell/server/api/schemas/strategy.py b/python/valuecell/server/api/schemas/strategy.py index fb04d4b35..e518bf678 100644 --- a/python/valuecell/server/api/schemas/strategy.py +++ b/python/valuecell/server/api/schemas/strategy.py @@ -239,10 +239,13 @@ class PromptCreateRequest(BaseModel): class PromptDeleteResponse(BaseModel): - deleted: bool = Field(..., description="Whether the prompt was successfully deleted") + deleted: bool = Field( + ..., description="Whether the prompt was successfully deleted" + ) prompt_id: str = Field(..., description="ID of the deleted prompt") message: str = Field(..., description="Delete operation result message") + PromptDeleteSuccessResponse = SuccessResponse[PromptDeleteResponse] diff --git a/python/valuecell/server/db/repositories/strategy_repository.py b/python/valuecell/server/db/repositories/strategy_repository.py index fab4bba89..861913ade 100644 --- a/python/valuecell/server/db/repositories/strategy_repository.py +++ b/python/valuecell/server/db/repositories/strategy_repository.py @@ -586,7 +586,7 @@ def get_prompt_by_id(self, prompt_id: str) -> Optional[StrategyPrompt]: def delete_prompt(self, prompt_id: str) -> bool: """Delete a prompt by prompt_id. - + Returns True on success, False if the prompt does not exist or on error. """ session = self._get_session() @@ -599,7 +599,7 @@ def delete_prompt(self, prompt_id: str) -> bool: ) if not prompt: return False - + session.query(StrategyPrompt).filter(StrategyPrompt.id == prompt_id).delete( synchronize_session=False ) From 682f65b9c91c73cf952b972d6263c202eac804ee Mon Sep 17 00:00:00 2001 From: gjzhou <92616697+PotatoZhou@users.noreply.github.com> Date: Sun, 7 Dec 2025 00:50:56 +0800 Subject: [PATCH 3/4] fix(strategy): align delete icon position with select icon in trading strategy form --- .../valuecell/form/trading-strategy-form.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/valuecell/form/trading-strategy-form.tsx b/frontend/src/components/valuecell/form/trading-strategy-form.tsx index 57ba3d36b..7370c00d2 100644 --- a/frontend/src/components/valuecell/form/trading-strategy-form.tsx +++ b/frontend/src/components/valuecell/form/trading-strategy-form.tsx @@ -212,16 +212,16 @@ export const TradingStrategyForm = withForm({ {prompt.name} {field.state.value !== prompt.id && ( + type="button" + className="absolute right-2 z-50 flex size-3.5 items-center justify-center rounded-sm p-0 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive hover:opacity-100" + onPointerUp={(e) => { + e.stopPropagation(); + e.preventDefault(); + handleDeletePrompt(prompt.id); + }} + > + + )} ))} From 48e1537f92a678f06e50f0545af4034ed051e9db Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:02:42 +0800 Subject: [PATCH 4/4] refactor(strategy): improve formatting of delete button in trading strategy form --- .../valuecell/form/trading-strategy-form.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/valuecell/form/trading-strategy-form.tsx b/frontend/src/components/valuecell/form/trading-strategy-form.tsx index 7370c00d2..9061108d5 100644 --- a/frontend/src/components/valuecell/form/trading-strategy-form.tsx +++ b/frontend/src/components/valuecell/form/trading-strategy-form.tsx @@ -212,16 +212,16 @@ export const TradingStrategyForm = withForm({ {prompt.name} {field.state.value !== prompt.id && ( + type="button" + className="absolute right-2 z-50 flex size-3.5 items-center justify-center rounded-sm p-0 opacity-0 transition-all hover:bg-destructive/10 hover:text-destructive hover:opacity-100" + onPointerUp={(e) => { + e.stopPropagation(); + e.preventDefault(); + handleDeletePrompt(prompt.id); + }} + > + + )} ))}