feat(chat): integrate podcast support into chat components

- Updated ChatMessage and ChatWindow components to handle podcast content alongside films and songs.
- Enhanced useContentPanel to extract and manage podcasts, allowing for richer media interactions.
- Added PodcastCard and PodcastGrid components for displaying podcast information.
- Improved UI elements to accommodate podcast selection and detail viewing.
- Updated styles for empty state icons and added new CSS for podcast-related elements.

Made-with: Cursor
This commit is contained in:
Dorian
2026-03-02 19:57:44 +00:00
parent 7a0e42bf63
commit 49ec6c09b6
18 changed files with 890 additions and 39 deletions
+106 -9
View File
@@ -14,12 +14,26 @@
<!-- Content surface (main area) -->
<main
class="flex-1 min-w-0 path-glass-card overflow-hidden flex flex-col"
class="flex-1 min-w-0 path-glass-card overflow-hidden flex flex-col relative"
:class="[
panelSide === 'left' ? 'order-last' : 'order-first',
(selectedFilm || selectedSong) && 'detail-active'
(selectedFilm || selectedSong || selectedPodcast) && 'detail-active'
]"
>
<button
v-if="(selectedFilm || selectedSong || selectedPodcast) && (panelOpen || chatStore.isStreaming)"
class="absolute top-3 right-3 z-10 p-2 rounded-lg transition-colors"
:class="isDark
? 'text-white/70 hover:bg-white/10'
: 'text-gray-500 hover:bg-black/5 hover:text-gray-800'"
title="Clear content"
aria-label="Clear content"
@click="closePanel"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<Transition name="content-fade" mode="out-in">
<div v-if="panelOpen" key="content" class="flex-1 min-h-0 flex flex-col">
<FilmDetail
@@ -32,30 +46,105 @@
:song="selectedSong"
@back="closeSongDetail"
/>
<PodcastDetail
v-else-if="selectedPodcast"
:podcast="selectedPodcast"
@back="closePodcastDetail"
/>
<FilmGrid
v-else-if="contentType === 'film'"
:films="panelFilms"
:title="panelTitle"
@select-film="openFilmDetail"
/>
>
<template #header-actions>
<button
class="p-2 rounded-lg transition-colors -mr-1"
:class="isDark
? 'text-white/70 hover:bg-white/10'
: 'text-gray-500 hover:bg-black/5 hover:text-gray-800'"
title="Clear content"
aria-label="Clear content"
@click="closePanel"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</template>
</FilmGrid>
<SongGrid
v-else
v-else-if="contentType === 'song'"
:songs="panelSongs"
:title="panelTitle"
@select-song="openSongDetail"
/>
>
<template #header-actions>
<button
class="p-2 rounded-lg transition-colors -mr-1"
:class="isDark
? 'text-white/70 hover:bg-white/10'
: 'text-gray-500 hover:bg-black/5 hover:text-gray-800'"
title="Clear content"
aria-label="Clear content"
@click="closePanel"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</template>
</SongGrid>
<PodcastGrid
v-else
:podcasts="panelPodcasts"
:title="panelTitle"
@select-podcast="openPodcastDetail"
>
<template #header-actions>
<button
class="p-2 rounded-lg transition-colors -mr-1"
:class="isDark
? 'text-white/70 hover:bg-white/10'
: 'text-gray-500 hover:bg-black/5 hover:text-gray-800'"
title="Clear content"
aria-label="Clear content"
@click="closePanel"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</template>
</PodcastGrid>
</div>
<ContextLoader
v-else-if="chatStore.isStreaming"
key="loader"
:context-type="loaderContextType"
/>
>
<template #header-actions>
<button
class="p-2 rounded-lg transition-colors -mr-1"
:class="isDark
? 'text-white/70 hover:bg-white/10'
: 'text-gray-500 hover:bg-black/5 hover:text-gray-800'"
title="Clear content"
aria-label="Clear content"
@click="closePanel"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</template>
</ContextLoader>
<div v-else key="empty" class="flex-1 flex items-center justify-center">
<div class="text-center space-y-4 animate-fade-up max-w-sm px-6">
<div class="w-20 h-20 rounded-2xl path-glass-card flex items-center justify-center mx-auto overflow-hidden">
<img src="/icon.svg" alt="AIUI" class="w-12 h-12 object-contain" />
<div class="empty-state-icon w-20 h-20 rounded-2xl path-glass-icon flex items-center justify-center mx-auto overflow-hidden">
<span class="text-3xl" :class="isDark ? 'text-[#fafafa]' : 'text-gray-800'"></span>
</div>
<div>
<h2 class="text-lg font-bold mb-1"
@@ -64,7 +153,7 @@
</h2>
<p class="text-sm leading-relaxed"
:class="isDark ? 'text-white/30' : 'text-gray-400'">
Ask about films or songs in the chat to see rich content here.
Ask about films, songs, or podcasts in the chat to see rich content here.
</p>
</div>
</div>
@@ -100,6 +189,8 @@ import FilmGrid from '@/components/content/FilmGrid.vue'
import FilmDetail from '@/components/content/FilmDetail.vue'
import SongGrid from '@/components/content/SongGrid.vue'
import SongDetail from '@/components/content/SongDetail.vue'
import PodcastGrid from '@/components/content/PodcastGrid.vue'
import PodcastDetail from '@/components/content/PodcastDetail.vue'
import ContextLoader from '@/components/content/ContextLoader.vue'
import PlayerBar from '@/components/player/PlayerBar.vue'
import { usePlayer } from '@/composables/usePlayer'
@@ -114,14 +205,19 @@ const {
panelOpen,
panelFilms,
panelSongs,
panelPodcasts,
panelTitle,
contentType,
selectedFilm,
selectedSong,
selectedPodcast,
openFilmDetail,
closeFilmDetail,
openSongDetail,
closeSongDetail,
openPodcastDetail,
closePodcastDetail,
closePanel,
} = useContentPanel()
const panelSide = computed(() => chatStore.panelSide)
@@ -132,6 +228,7 @@ const loaderContextType = computed(() => {
const lastUser = [...chatStore.messages].reverse().find((m) => m.role === 'user')
const q = (lastUser?.content ?? '').toLowerCase()
if (/\b(song|music|track|album|band|artist|listen)\b/.test(q)) return 'song'
if (/\b(podcast|episode|show|listen to)\b/.test(q)) return 'podcast'
return contentType.value
})
</script>