From a2315c3b78dc8b0356541c30042dc38a15459188 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 22 Jun 2026 21:13:34 +0200 Subject: [PATCH] feat: inline edit for patterns in library (name, description, category, direction, regime) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: PATCH /api/patterns/custom/{id} — partial update, only provided fields changed - useApi.ts: usePatchPattern mutation hook - PatternCard: pencil icon (non-builtin only) → edit mode with inline inputs for name, description, direction (select), category, and #regime; ✓/✗ buttons to save or cancel; card border highlights blue while editing Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/patterns.py | 28 +++++ frontend/src/hooks/useApi.ts | 15 +++ frontend/src/pages/PatternExplorer.tsx | 144 +++++++++++++++++++------ 3 files changed, 156 insertions(+), 31 deletions(-) diff --git a/backend/routers/patterns.py b/backend/routers/patterns.py index 7915cf8..0c32a86 100644 --- a/backend/routers/patterns.py +++ b/backend/routers/patterns.py @@ -61,6 +61,34 @@ def update_pattern(pat_id: str, req: PatternRequest): return {"id": pat_id, "status": "updated"} +class PatternPatchRequest(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + category: Optional[str] = None + signal_direction: Optional[str] = None + regime_tag: Optional[str] = None + + +@router.patch("/custom/{pat_id}") +def patch_pattern(pat_id: str, req: PatternPatchRequest): + """Partial update — only provided fields are changed.""" + from services.database import get_conn + conn = get_conn() + if not conn.execute("SELECT id FROM custom_patterns WHERE id=?", (pat_id,)).fetchone(): + conn.close() + raise HTTPException(404, "Pattern not found") + updates = {k: v for k, v in req.model_dump().items() if v is not None} + if updates: + set_clause = ", ".join(f"{k}=?" for k in updates) + conn.execute( + f"UPDATE custom_patterns SET {set_clause}, updated_at=datetime('now') WHERE id=?", + list(updates.values()) + [pat_id], + ) + conn.commit() + conn.close() + return {"id": pat_id, "status": "updated", "fields": list(updates.keys())} + + @router.delete("/purge-all") def purge_all_patterns(): """Delete ALL custom/backtested patterns from the pattern library.""" diff --git a/frontend/src/hooks/useApi.ts b/frontend/src/hooks/useApi.ts index cdd2360..29bc71b 100644 --- a/frontend/src/hooks/useApi.ts +++ b/frontend/src/hooks/useApi.ts @@ -311,6 +311,21 @@ export const useDeletePattern = () => { }) } +export const usePatchPattern = () => { + const qc = useQueryClient() + return useMutation({ + mutationFn: (body: { + id: string + name?: string + description?: string + category?: string + signal_direction?: string + regime_tag?: string + }) => api.patch(`/patterns/custom/${body.id}`, body).then(r => r.data), + onSuccess: () => qc.invalidateQueries({ queryKey: ['all-patterns'] }), + }) +} + export const useTogglePattern = () => { const qc = useQueryClient() return useMutation({ diff --git a/frontend/src/pages/PatternExplorer.tsx b/frontend/src/pages/PatternExplorer.tsx index 5d4ab35..b1eb5c4 100644 --- a/frontend/src/pages/PatternExplorer.tsx +++ b/frontend/src/pages/PatternExplorer.tsx @@ -4,13 +4,14 @@ import clsx from 'clsx' import { TreePine, Search, ChevronRight, ChevronDown, TrendingUp, TrendingDown, Zap, BookOpen, ExternalLink, RefreshCw, - Layers, GitMerge, Tag, Trash2, + Layers, GitMerge, Tag, Trash2, Pencil, Check, X, } from 'lucide-react' import { useAllPatterns, usePatternsByInstrument, useLastScores, useDeletePattern, + usePatchPattern, validateTicker, } from '../hooks/useApi' import { INSTRUMENTS, INSTRUMENT_CATEGORIES } from '../constants/instruments' @@ -140,59 +141,99 @@ function PatternCard({ pattern: Pattern highlightTrades?: string[] }) { - const [expanded, setExpanded] = useState(false) - const [confirmDel, setConfirmDel] = useState(false) - const { mutate: deletePattern, isPending: deleting } = useDeletePattern() + const [expanded, setExpanded] = useState(false) + const [confirmDel, setConfirmDel] = useState(false) + const [editing, setEditing] = useState(false) + const [editName, setEditName] = useState(pattern.name) + const [editDesc, setEditDesc] = useState(pattern.description) + const [editCat, setEditCat] = useState(pattern.category ?? '') + const [editDir, setEditDir] = useState(pattern.signal_direction ?? '') + const [editRegime, setEditRegime] = useState(pattern.regime_tag ?? '') + + const { mutate: deletePattern, isPending: deleting } = useDeletePattern() + const { mutate: patchPattern, isPending: saving } = usePatchPattern() const trades = pattern.suggested_trades ?? [] const instances = pattern.historical_instances ?? [] const assetLabel = ASSET_CLASS_LABELS[pattern.asset_class] ?? pattern.asset_class - const isDeletable = pattern.source !== 'builtin' + const isEditable = pattern.source !== 'builtin' const moveSign = pattern.expected_move_pct >= 0 ? '+' : '' + const startEdit = () => { + setEditName(pattern.name) + setEditDesc(pattern.description) + setEditCat(pattern.category ?? '') + setEditDir(pattern.signal_direction ?? '') + setEditRegime(pattern.regime_tag ?? '') + setEditing(true) + } + + const saveEdit = () => { + patchPattern({ + id: pattern.id, + name: editName.trim() || pattern.name, + description: editDesc.trim() || pattern.description, + category: editCat || undefined, + signal_direction: editDir || undefined, + regime_tag: editRegime.trim() || undefined, + }, { onSuccess: () => setEditing(false) }) + } + return ( -
+
{/* Header row */}
- + {pattern.id} {probStars(pattern.probability)} - {pattern.category && } - {pattern.regime_tag && ( + {!editing && pattern.category && } + {!editing && pattern.regime_tag && ( #{pattern.regime_tag} )}
-
- {pattern.ai_quality_score != null && ( +
+ {pattern.ai_quality_score != null && !editing && ( AI {pattern.ai_quality_score}/100 )} - {isDeletable && ( + {isEditable && !editing && !confirmDel && ( + + )} + {isEditable && editing && ( + <> + + + + )} + {isEditable && !editing && ( confirmDel ? (
- -
) : ( - ) @@ -200,11 +241,52 @@ function PatternCard({
- {/* Name + description */} -
-
{pattern.name}
-
{pattern.description}
-
+ {/* Edit mode fields */} + {editing ? ( +
+ setEditName(e.target.value)} + className="input text-sm font-semibold py-1.5 w-full" + placeholder="Pattern name" + /> +