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..9061108d5 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..2ffca1ef2 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,32 @@ 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..e518bf678 100644 --- a/python/valuecell/server/api/schemas/strategy.py +++ b/python/valuecell/server/api/schemas/strategy.py @@ -238,6 +238,17 @@ 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..861913ade 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.