From 1cc192a22136f3c5b063bf246afdc2aef0bebd1e Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Mon, 22 Jun 2026 17:08:24 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20PatternExplorer=20=E2=80=94=20sort=20+?= =?UTF-8?q?=20category=20filter=20+=20category=20badge=20on=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sort dropdown: AI Score / Probability / Expected Move / Date Added - Category filter pills auto-populated from patterns in current node - CategoryBadge component with colored pills (matches Trade Ideas palette) - AI score shown top-right on each pattern card - Changing taxonomy node resets category filter Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/PatternExplorer.tsx | 121 +++++++++++++++++++++++-- 1 file changed, 114 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/PatternExplorer.tsx b/frontend/src/pages/PatternExplorer.tsx index 6e438f8..7bcdd09 100644 --- a/frontend/src/pages/PatternExplorer.tsx +++ b/frontend/src/pages/PatternExplorer.tsx @@ -118,6 +118,28 @@ function SignalBadge({ direction }: { direction?: string }) { ) } +// ── Category Badge ──────────────────────────────────────────────────────────── + +const CATEGORY_COLORS: Record = { + geopolitical: 'bg-red-900/40 border-red-700/40 text-red-300', + macro: 'bg-blue-900/40 border-blue-700/40 text-blue-300', + volatility: 'bg-amber-900/40 border-amber-700/40 text-amber-300', + 'risk-off': 'bg-orange-900/40 border-orange-700/40 text-orange-300', + supply: 'bg-teal-900/40 border-teal-700/40 text-teal-300', + credit: 'bg-purple-900/40 border-purple-700/40 text-purple-300', + 'geo-eco': 'bg-emerald-900/40 border-emerald-700/40 text-emerald-300', + sentiment: 'bg-yellow-900/40 border-yellow-700/40 text-yellow-300', +} + +function CategoryBadge({ category }: { category: string }) { + const cls = CATEGORY_COLORS[category] ?? 'bg-violet-900/40 border-violet-700/40 text-violet-300' + return ( + + {category} + + ) +} + // ── Pattern Card ────────────────────────────────────────────────────────────── function PatternCard({ @@ -143,7 +165,13 @@ function PatternCard({ {pattern.id} {probStars(pattern.probability)} + {pattern.category && } + {pattern.ai_quality_score != null && ( + + AI {pattern.ai_quality_score}/100 + + )} {/* Name + description */} @@ -318,12 +346,14 @@ function TreeView({ patterns }: { patterns: Pattern[] }) { const [selectedPath, setSelectedPath] = useState([]) const [nameFilter, setNameFilter] = useState('') + const [categoryFilter, setCategoryFilter] = useState('') + const [sortBy, setSortBy] = useState<'ai_score' | 'probability' | 'expected_move' | 'date'>('ai_score') - const filteredPatterns = useMemo(() => { + // Patterns matching the selected taxonomy node + name filter + const nodePatterns = useMemo(() => { let list = patterns - if (selectedPath.length > 0) { + if (selectedPath.length > 0) list = list.filter(p => pathStartsWith(p.taxonomy_path, selectedPath)) - } if (nameFilter.trim()) { const q = nameFilter.trim().toLowerCase() list = list.filter(p => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q)) @@ -331,8 +361,46 @@ function TreeView({ patterns }: { patterns: Pattern[] }) { return list }, [patterns, selectedPath, nameFilter]) + // Available categories within current node (for filter pills) + const availableCategories = useMemo(() => { + const cats = new Set(nodePatterns.map(p => p.category).filter(Boolean) as string[]) + return Array.from(cats).sort() + }, [nodePatterns]) + + // Apply category filter + sort + const filteredPatterns = useMemo(() => { + let list = nodePatterns + if (categoryFilter) list = list.filter(p => p.category === categoryFilter) + return [...list].sort((a, b) => { + if (sortBy === 'ai_score') { + const sa = a.ai_quality_score ?? -1 + const sb = b.ai_quality_score ?? -1 + return sb - sa + } + if (sortBy === 'probability') return b.probability - a.probability + if (sortBy === 'expected_move') return Math.abs(b.expected_move_pct) - Math.abs(a.expected_move_pct) + // date + const da = (a as any).created_at ?? '' + const db = (b as any).created_at ?? '' + return db.localeCompare(da) + }) + }, [nodePatterns, categoryFilter, sortBy]) + const breadcrumb = selectedPath.length > 0 ? selectedPath.join(' › ') : 'All Patterns' + const CATEGORY_COLORS: Record = { + geopolitical: 'bg-red-900/40 border-red-700/40 text-red-300', + macro: 'bg-blue-900/40 border-blue-700/40 text-blue-300', + volatility: 'bg-amber-900/40 border-amber-700/40 text-amber-300', + 'risk-off': 'bg-orange-900/40 border-orange-700/40 text-orange-300', + supply: 'bg-teal-900/40 border-teal-700/40 text-teal-300', + credit: 'bg-purple-900/40 border-purple-700/40 text-purple-300', + 'geo-eco': 'bg-emerald-900/40 border-emerald-700/40 text-emerald-300', + sentiment: 'bg-yellow-900/40 border-yellow-700/40 text-yellow-300', + } + const catColor = (cat: string) => + CATEGORY_COLORS[cat] ?? 'bg-slate-700/40 border-slate-600/40 text-slate-300' + return (
{/* Left sidebar */} @@ -349,7 +417,7 @@ function TreeView({ patterns }: { patterns: Pattern[] }) { ? 'bg-blue-600/30 text-blue-300 border border-blue-500/30' : 'text-slate-400 hover:bg-slate-700/30 hover:text-slate-200' )} - onClick={() => setSelectedPath([])} + onClick={() => { setSelectedPath([]); setCategoryFilter('') }} > All Patterns @@ -366,7 +434,7 @@ function TreeView({ patterns }: { patterns: Pattern[] }) { node={node} level={0} selectedPath={selectedPath} - onSelect={setSelectedPath} + onSelect={path => { setSelectedPath(path); setCategoryFilter('') }} defaultExpandedDepth={1} /> )) @@ -377,7 +445,7 @@ function TreeView({ patterns }: { patterns: Pattern[] }) { {/* Right panel */}
- {/* Breadcrumb + filter */} + {/* Row 1: breadcrumb + search + count */}
Path: @@ -398,13 +466,52 @@ function TreeView({ patterns }: { patterns: Pattern[] }) {
+ {/* Row 2: category pills + sort */} +
+ {availableCategories.length > 0 && ( + <> + Category: + + {availableCategories.map(cat => ( + + ))} + + )} +
+ Sort: + +
+
+ {/* Pattern grid */} {filteredPatterns.length === 0 ? (
No patterns match the selected filter
) : ( -
+
{filteredPatterns.map(p => ( ))}