Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions frontend/src/api/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
99 changes: 95 additions & 4 deletions frontend/src/components/valuecell/form/trading-strategy-form.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -37,6 +51,38 @@ export const TradingStrategyForm = withForm({
},
render({ form, prompts, tradingMode }) {
const { mutateAsync: createStrategyPrompt } = useCreateStrategyPrompt();
const { mutate: deleteStrategyPrompt } = useDeleteStrategyPrompt();
const [deletePromptId, setDeletePromptId] = useState<string | null>(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 (
<FieldGroup className="gap-6">
Expand Down Expand Up @@ -158,8 +204,25 @@ export const TradingStrategyForm = withForm({
<SelectContent>
{prompts.length > 0 &&
prompts.map((prompt) => (
<SelectItem key={prompt.id} value={prompt.id}>
{prompt.name}
<SelectItem
key={prompt.id}
value={prompt.id}
className="relative hover:[&_button]:opacity-100 hover:[&_button]:transition-opacity"
>
<span>{prompt.name}</span>
{field.state.value !== prompt.id && (
<button
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);
}}
>
<Trash2 className="h-3 w-3" />
</button>
)}
</SelectItem>
))}
<NewPromptModal
Expand Down Expand Up @@ -204,6 +267,34 @@ export const TradingStrategyForm = withForm({
);
}}
</form.Subscribe>

{/* Delete Confirmation Dialog */}
<AlertDialog
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Strategy Prompt</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete this strategy prompt? This
action cannot be undone and will permanently remove the prompt
from the system.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={cancelDeletePrompt}>
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDeletePrompt}
className="bg-red-600 hover:bg-red-700 focus:ring-red-600"
>
Confirm Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</FieldGroup>
);
},
Expand Down
30 changes: 30 additions & 0 deletions python/valuecell/server/api/routers/strategy_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from valuecell.server.api.schemas.strategy import (
PromptCreateRequest,
PromptCreateResponse,
PromptDeleteResponse,
PromptDeleteSuccessResponse,
PromptItem,
PromptListResponse,
)
Expand Down Expand Up @@ -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
11 changes: 11 additions & 0 deletions python/valuecell/server/api/schemas/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
28 changes: 28 additions & 0 deletions python/valuecell/server/db/repositories/strategy_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down