From c1db3a8aae3d2585fbe9f578d0b300c6ad797cf1 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Fri, 27 Mar 2026 16:25:49 -0500 Subject: [PATCH] Add Spotify player drawer to the web UI - Add a persisted Spotify player store with preset playlists and custom links - Render the player in ChatView and expose it from the sidebar --- apps/web/src/components/ChatView.tsx | 3 + apps/web/src/components/Sidebar.tsx | 4 + apps/web/src/components/SpotifyPlayer.tsx | 219 ++++++++++++++++++++++ apps/web/src/spotifyPlayerStore.ts | 168 +++++++++++++++++ 4 files changed, 394 insertions(+) create mode 100644 apps/web/src/components/SpotifyPlayer.tsx create mode 100644 apps/web/src/spotifyPlayerStore.ts diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index f9fd68e37..924441a8b 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -90,6 +90,7 @@ import BranchToolbar from "./BranchToolbar"; import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings"; import PlanSidebar from "./PlanSidebar"; import ThreadTerminalDrawer from "./ThreadTerminalDrawer"; +import { SpotifyPlayerDrawer } from "./SpotifyPlayer"; import { BotIcon, ChevronDownIcon, @@ -4429,6 +4430,8 @@ export default function ChatView({ threadId }: ChatViewProps) { ); })()} + + {expandedImage && expandedImageItem && (
+ + + {isOnSettings ? ( p.uri === selectedPlaylistUri); + + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Categories for the playlist picker +// --------------------------------------------------------------------------- +const CATEGORIES = [...new Set(DEFAULT_PLAYLISTS.map((p) => p.category))]; + +// --------------------------------------------------------------------------- +// Main Spotify Player Drawer — rendered at the bottom of ChatView +// --------------------------------------------------------------------------- +export function SpotifyPlayerDrawer() { + const { isOpen, selectedPlaylistUri, customUri, setOpen, selectPlaylist, setCustomUri } = + useSpotifyPlayerStore(); + const [expanded, setExpanded] = useState(false); + const [customInput, setCustomInput] = useState(""); + const [activeCategory, setActiveCategory] = useState(CATEGORIES[0] ?? "Focus"); + const inputRef = useRef(null); + + const embedUrl = useMemo(() => { + // Custom URI takes priority + if (customUri) { + const parsed = parseSpotifyUri(customUri); + if (parsed) return buildEmbedUrl(parsed.type, parsed.id); + } + // Selected preset playlist + if (selectedPlaylistUri) { + return buildEmbedUrl("playlist", selectedPlaylistUri); + } + return null; + }, [customUri, selectedPlaylistUri]); + + const handleCustomSubmit = useCallback(() => { + const trimmed = customInput.trim(); + if (!trimmed) return; + const parsed = parseSpotifyUri(trimmed); + if (parsed) { + setCustomUri(trimmed); + setCustomInput(""); + setExpanded(false); + } + }, [customInput, setCustomUri]); + + if (!isOpen) return null; + + const filteredPlaylists = DEFAULT_PLAYLISTS.filter((p) => p.category === activeCategory); + + return ( +
+ {/* Header bar */} +
+ + Spotify + + + + +
+ + {/* Expanded playlist picker */} + {expanded && ( +
+ {/* Category tabs */} +
+ {CATEGORIES.map((cat) => ( + + ))} +
+ + {/* Playlist grid */} +
+ {filteredPlaylists.map((playlist) => ( + + ))} +
+ + {/* Custom URL input */} +
+ setCustomInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + handleCustomSubmit(); + } + }} + placeholder="Paste Spotify link..." + className="flex-1 rounded-md border border-border/60 bg-background px-2 py-1 text-[11px] text-foreground placeholder:text-muted-foreground/40 focus:border-emerald-500/50 focus:outline-none" + /> + +
+
+ )} + + {/* Spotify embed iframe */} + {embedUrl ? ( +
+