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:
OpenSquared
2026-06-22 21:13:34 +02:00
parent d794ad68aa
commit a2315c3b78
3 changed files with 156 additions and 31 deletions

View File

@@ -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."""

View File

@@ -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({

View File

@@ -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">