feat: inline edit for patterns in library (name, description, category, direction, regime)
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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."""
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 (
|
||||
<div className="card hover:border-slate-600/50 transition-colors flex flex-col gap-2">
|
||||
<div className={clsx(
|
||||
'card flex flex-col gap-2 transition-colors',
|
||||
editing ? 'border-blue-600/40' : 'hover:border-slate-600/50',
|
||||
)}>
|
||||
{/* Header row */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<SignalBadge direction={pattern.signal_direction} />
|
||||
<SignalBadge direction={editing ? editDir : pattern.signal_direction} />
|
||||
<span className="text-[10px] font-mono text-slate-500">{pattern.id}</span>
|
||||
<span className="text-[10px] text-amber-400 tracking-wider">{probStars(pattern.probability)}</span>
|
||||
{pattern.category && <CategoryBadge category={pattern.category} />}
|
||||
{pattern.regime_tag && (
|
||||
{!editing && pattern.category && <CategoryBadge category={pattern.category} />}
|
||||
{!editing && pattern.regime_tag && (
|
||||
<span className="text-[10px] text-violet-400 font-mono">#{pattern.regime_tag}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{pattern.ai_quality_score != null && (
|
||||
<div className="flex items-center gap-1.5 shrink-0">
|
||||
{pattern.ai_quality_score != null && !editing && (
|
||||
<span className="text-[10px] font-mono text-slate-500">
|
||||
AI <span className="text-slate-300 font-semibold">{pattern.ai_quality_score}</span>/100
|
||||
</span>
|
||||
)}
|
||||
{isDeletable && (
|
||||
{isEditable && !editing && !confirmDel && (
|
||||
<button onClick={startEdit} className="text-slate-700 hover:text-blue-400 transition-colors p-0.5 rounded" title="Edit">
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
{isEditable && editing && (
|
||||
<>
|
||||
<button onClick={saveEdit} disabled={saving}
|
||||
className="text-slate-700 hover:text-emerald-400 transition-colors p-0.5 rounded" title="Save">
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setEditing(false)}
|
||||
className="text-slate-700 hover:text-slate-300 transition-colors p-0.5 rounded" title="Cancel">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{isEditable && !editing && (
|
||||
confirmDel ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => deletePattern(pattern.id)}
|
||||
disabled={deleting}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded bg-red-900/50 border border-red-700/60 text-red-400 hover:bg-red-900/70 transition-colors"
|
||||
>
|
||||
<button onClick={() => deletePattern(pattern.id)} disabled={deleting}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded bg-red-900/50 border border-red-700/60 text-red-400 hover:bg-red-900/70 transition-colors">
|
||||
{deleting ? '…' : 'Confirm'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDel(false)}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded border border-slate-700 text-slate-500 hover:text-slate-300 transition-colors"
|
||||
>
|
||||
<button onClick={() => setConfirmDel(false)}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded border border-slate-700 text-slate-500 hover:text-slate-300 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setConfirmDel(true)}
|
||||
className="text-slate-700 hover:text-red-400 transition-colors p-0.5 rounded"
|
||||
title="Delete pattern"
|
||||
>
|
||||
<button onClick={() => setConfirmDel(true)}
|
||||
className="text-slate-700 hover:text-red-400 transition-colors p-0.5 rounded" title="Delete">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)
|
||||
@@ -200,11 +241,52 @@ function PatternCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name + description */}
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white leading-snug">{pattern.name}</div>
|
||||
<div className="text-xs text-slate-400 mt-0.5 line-clamp-2">{pattern.description}</div>
|
||||
</div>
|
||||
{/* Edit mode fields */}
|
||||
{editing ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
value={editName}
|
||||
onChange={e => setEditName(e.target.value)}
|
||||
className="input text-sm font-semibold py-1.5 w-full"
|
||||
placeholder="Pattern name"
|
||||
/>
|
||||
<textarea
|
||||
value={editDesc}
|
||||
onChange={e => setEditDesc(e.target.value)}
|
||||
rows={3}
|
||||
className="input text-xs py-1.5 w-full resize-none"
|
||||
placeholder="Description"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<select value={editDir} onChange={e => setEditDir(e.target.value)} className="input text-xs py-1.5 flex-1">
|
||||
<option value="">Direction…</option>
|
||||
<option value="bullish">↑ Bullish</option>
|
||||
<option value="bearish">↓ Bearish</option>
|
||||
<option value="neutral">⚡ Vol / Neutral</option>
|
||||
</select>
|
||||
<input
|
||||
value={editCat}
|
||||
onChange={e => setEditCat(e.target.value)}
|
||||
className="input text-xs py-1.5 flex-1"
|
||||
placeholder="Category (geopolitical…)"
|
||||
/>
|
||||
<input
|
||||
value={editRegime}
|
||||
onChange={e => setEditRegime(e.target.value)}
|
||||
className="input text-xs py-1.5 flex-1 font-mono"
|
||||
placeholder="#regime"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Name + description */}
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white leading-snug">{pattern.name}</div>
|
||||
<div className="text-xs text-slate-400 mt-0.5 line-clamp-2">{pattern.description}</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Stats row */}
|
||||
<div className="border-t border-slate-700/40 pt-2 flex flex-wrap gap-x-3 gap-y-1 text-xs text-slate-400">
|
||||
|
||||
Reference in New Issue
Block a user