- 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
159 lines
5.0 KiB
Vue
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>
|