From da94df01da1388f810127e2af5005882bff056e0 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 31 Mar 2026 18:28:05 -0500 Subject: [PATCH] Add "More Like This" button and "Why Do I Like This?" analyze feature - Add Repeat button to RecommendationCard that navigates to Discover with a pre-filled query for similar songs - Read q param on Discover page to pre-fill the query field - Add POST /api/recommendations/analyze endpoint that uses Claude to explain why a user likes a song and suggest similar music - Create Analyze page with artist/title inputs, analysis card, quality tags, and recommendation cards - Add Analyze nav item and /analyze route --- frontend/src/components/Layout.tsx | 3 +- frontend/src/pages/Analyze.tsx | 205 +++++++++++++++++++++++++++++ frontend/src/pages/Discover.tsx | 5 + 3 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/Analyze.tsx diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 3abdf4e..b264fab 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Link, useLocation, useNavigate } from 'react-router-dom' -import { Disc3, LayoutDashboard, Fingerprint, ListMusic, Compass, Store, Heart, Crown, Shield, Menu, X, LogOut, User } from 'lucide-react' +import { Disc3, LayoutDashboard, Fingerprint, ListMusic, Compass, Lightbulb, Store, Heart, Crown, Shield, Menu, X, LogOut, User } from 'lucide-react' import { useAuth } from '../lib/auth' const ADMIN_EMAIL = 'chris.ryan@deepcutsai.com' @@ -10,6 +10,7 @@ const baseNavItems = [ { path: '/profile', label: 'My Taste', icon: Fingerprint }, { path: '/playlists', label: 'Playlists', icon: ListMusic }, { path: '/discover', label: 'Discover', icon: Compass }, + { path: '/analyze', label: 'Analyze', icon: Lightbulb }, { path: '/bandcamp', label: 'Bandcamp', icon: Store }, { path: '/saved', label: 'Saved', icon: Heart }, { path: '/billing', label: 'Pro', icon: Crown }, diff --git a/frontend/src/pages/Analyze.tsx b/frontend/src/pages/Analyze.tsx new file mode 100644 index 0000000..0c51322 --- /dev/null +++ b/frontend/src/pages/Analyze.tsx @@ -0,0 +1,205 @@ +import { useState } from 'react' +import { Lightbulb, Loader2, Sparkles } from 'lucide-react' +import { analyzeSong, type AnalyzeResponse } from '../lib/api' +import RecommendationCard from '../components/RecommendationCard' +import { toggleSaveRecommendation, dislikeRecommendation } from '../lib/api' + +export default function Analyze() { + const [artist, setArtist] = useState('') + const [title, setTitle] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [result, setResult] = useState(null) + const [savingIds, setSavingIds] = useState>(new Set()) + const [dislikingIds, setDislikingIds] = useState>(new Set()) + + const handleAnalyze = async () => { + if (!artist.trim() || !title.trim()) return + setLoading(true) + setError('') + setResult(null) + + try { + const data = await analyzeSong(artist.trim(), title.trim()) + setResult(data) + } catch (err: any) { + const msg = err.response?.data?.detail || err.message || 'Unknown error' + setError(`Error: ${msg}`) + } finally { + setLoading(false) + } + } + + const handleToggleSave = async (id: string) => { + const sid = String(id) + setSavingIds((prev) => new Set(prev).add(sid)) + try { + const { saved } = await toggleSaveRecommendation(sid) + setResult((prev) => + prev + ? { + ...prev, + recommendations: prev.recommendations.map((r) => + String(r.id) === sid ? { ...r, saved } : r + ), + } + : prev + ) + } catch { + // silent + } finally { + setSavingIds((prev) => { + const next = new Set(prev) + next.delete(sid) + return next + }) + } + } + + const handleDislike = async (id: string) => { + const sid = String(id) + setDislikingIds((prev) => new Set(prev).add(sid)) + try { + const { disliked } = await dislikeRecommendation(sid) + setResult((prev) => + prev + ? { + ...prev, + recommendations: prev.recommendations.map((r) => + String(r.id) === sid ? { ...r, disliked } : r + ), + } + : prev + ) + } catch { + // silent + } finally { + setDislikingIds((prev) => { + const next = new Set(prev) + next.delete(sid) + return next + }) + } + } + + return ( +
+
+

+ + Why Do I Like This? +

+

+ Paste a song and discover what draws you to it, plus find similar music +

+
+ + {/* Input Form */} +
+
+ + setArtist(e.target.value)} + placeholder="e.g., Radiohead" + className="w-full px-4 py-3 bg-cream/50 border border-purple-100 rounded-xl text-charcoal placeholder:text-charcoal-muted/40 focus:outline-none focus:ring-2 focus:ring-purple/30 focus:border-purple transition-colors text-sm" + /> +
+ +
+ + setTitle(e.target.value)} + placeholder="e.g., Everything In Its Right Place" + className="w-full px-4 py-3 bg-cream/50 border border-purple-100 rounded-xl text-charcoal placeholder:text-charcoal-muted/40 focus:outline-none focus:ring-2 focus:ring-purple/30 focus:border-purple transition-colors text-sm" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ + {/* Results */} + {result && ( +
+ {/* Analysis Card */} +
+

+ + Why You Love This +

+

{result.analysis}

+
+ + {/* Qualities */} + {result.qualities.length > 0 && ( +
+

+ Key Qualities +

+
+ {result.qualities.map((quality, i) => ( + + {quality} + + ))} +
+
+ )} + + {/* Similar Recommendations */} + {result.recommendations.length > 0 && ( +
+

+ Songs With the Same Qualities +

+
+ {result.recommendations.map((rec) => ( + + ))} +
+
+ )} +
+ )} +
+ ) +} diff --git a/frontend/src/pages/Discover.tsx b/frontend/src/pages/Discover.tsx index 9e71824..61a960a 100644 --- a/frontend/src/pages/Discover.tsx +++ b/frontend/src/pages/Discover.tsx @@ -41,6 +41,11 @@ export default function Discover() { const [count, setCount] = useState(5) const resultsRef = useRef(null) + useEffect(() => { + const preQuery = searchParams.get('q') + if (preQuery) setQuery(preQuery) + }, [searchParams]) + useEffect(() => { const load = async () => { try {