Files
archy/packages/app/src/pages/ChatPage.vue
T
Dorian 7a0e42bf63 feat(app): integrate Jamendo API and enhance UI components
- Added Jamendo API client ID to the environment configuration for music search.
- Updated pnpm lock file to include new dependencies for enhanced functionality.
- Integrated Plyr library for improved media playback experience.
- Refactored ChatHeader, ChatInput, and ChatMessage components to utilize new styles and improve user interaction.
- Enhanced CSS styles for path-glass elements to align with the new design system.

Made-with: Cursor
2026-03-02 19:37:00 +00:00

159 lines
5.0 KiB
Vue

<template>
<div
class="h-full flex flex-col relative overflow-hidden transition-colors duration-300 transition-[padding]"
:class="[
hasTrack && 'pb-[72px]'
]"
:style="isDark
? { background: '#000 url(/assets/img/bg-intro-3.jpg) center center / cover no-repeat fixed' }
: { backgroundColor: '#f5f4f1' }"
>
<div v-if="isDark" class="absolute inset-0 pointer-events-none bg-black/20" />
<div class="flex-1 flex h-full p-3 md:p-4 gap-3 md:gap-4">
<!-- Content surface (main area) -->
<main
class="flex-1 min-w-0 path-glass-card overflow-hidden flex flex-col"
:class="[
panelSide === 'left' ? 'order-last' : 'order-first',
(selectedFilm || selectedSong) && 'detail-active'
]"
>
<Transition name="content-fade" mode="out-in">
<div v-if="panelOpen" key="content" class="flex-1 min-h-0 flex flex-col">
<FilmDetail
v-if="selectedFilm"
:film="selectedFilm"
@back="closeFilmDetail"
/>
<SongDetail
v-else-if="selectedSong"
:song="selectedSong"
@back="closeSongDetail"
/>
<FilmGrid
v-else-if="contentType === 'film'"
:films="panelFilms"
:title="panelTitle"
@select-film="openFilmDetail"
/>
<SongGrid
v-else
:songs="panelSongs"
:title="panelTitle"
@select-song="openSongDetail"
/>
</div>
<ContextLoader
v-else-if="chatStore.isStreaming"
key="loader"
:context-type="loaderContextType"
/>
<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>
<div>
<h2 class="text-lg font-bold mb-1"
:class="isDark ? 'text-white/80' : 'text-gray-800'">
Content Surface
</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.
</p>
</div>
</div>
</div>
</Transition>
</main>
<!-- Chat panel: high z-index so it + dropdowns stay above content -->
<aside
class="relative z-[100] w-80 xl:w-96 shrink-0 flex flex-col path-glass-card overflow-visible"
:class="panelSide === 'left' ? 'order-first' : 'order-last'"
>
<ChatWindow
:side="panelSide"
@switch-side="chatStore.switchSide()"
/>
</aside>
</div>
<PlayerBar />
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useChatStore } from '@/stores/chat'
import { useTheme } from '@/composables/useTheme'
import { useAI } from '@/composables/useAI'
import { useContentPanel } from '@/composables/useContentPanel'
import ChatWindow from '@/components/chat/ChatWindow.vue'
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 ContextLoader from '@/components/content/ContextLoader.vue'
import PlayerBar from '@/components/player/PlayerBar.vue'
import { usePlayer } from '@/composables/usePlayer'
const chatStore = useChatStore()
const { hasTrack } = usePlayer()
const { isDark } = useTheme()
useAI()
const {
panelOpen,
panelFilms,
panelSongs,
panelTitle,
contentType,
selectedFilm,
selectedSong,
openFilmDetail,
closeFilmDetail,
openSongDetail,
closeSongDetail,
} = useContentPanel()
const panelSide = computed(() => chatStore.panelSide)
// Infer loader type from last user message when streaming (before tags are parsed)
const loaderContextType = computed(() => {
if (panelOpen.value) return contentType.value
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'
return contentType.value
})
</script>
<style scoped>
.content-fade-enter-active,
.content-fade-leave-active {
transition: opacity 0.2s ease;
}
.content-fade-enter-from,
.content-fade-leave-to {
opacity: 0;
}
/*
* When film detail is active, hide the border so the hero image
* fills edge-to-edge with no visible seam lines.
* The card keeps its border-radius and overflow:hidden,
* so the image is still clipped to rounded corners.
*/
.detail-active {
border-color: transparent !important;
}
</style>