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
This commit is contained in:
Dorian
2026-03-02 19:37:00 +00:00
parent 80aeefa4bf
commit 7a0e42bf63
25 changed files with 1165 additions and 111 deletions
+61 -21
View File
@@ -1,6 +1,7 @@
<template>
<div
class="glass-strong rounded-t-2xl flex items-center justify-between px-4 py-3 relative z-20 border-t-0 border-x-0"
ref="headerRef"
class="path-glass-card rounded-t-2xl rounded-b-none flex items-center justify-between px-4 py-3 relative z-[60] shrink-0 border-t-0 border-x-0"
:class="isDark ? '!border-b-white/10' : '!border-b-black/8'"
>
<div class="flex items-center gap-3 min-w-0 flex-1">
@@ -10,6 +11,7 @@
</div>
<div class="min-w-0 flex-1 relative">
<button
ref="chatListTriggerRef"
class="w-full text-left group/btn"
:class="conversationList.length > 0 ? 'cursor-pointer' : ''"
@click="conversationList.length > 0 && (showChatList = !showChatList)"
@@ -21,6 +23,7 @@
:class="isDark ? 'text-white/40' : 'text-gray-400'">{{ conversationId }}</p>
<span class="text-[10px]" :class="isDark ? 'text-white/20' : 'text-gray-300'">·</span>
<button
ref="modelPickerTriggerRef"
class="text-[10px] text-accent/70 hover:text-accent transition-colors truncate"
@click.stop="showModelPicker = !showModelPicker; showChatList = false"
>
@@ -28,19 +31,14 @@
</button>
</div>
</button>
<Transition name="picker">
<div
v-if="showChatList && conversationList.length > 0"
class="contents"
>
<Teleport to="body">
<div v-if="showChatList && conversationList.length > 0" class="fixed inset-0 z-[9998]" aria-hidden="true" @click="showChatList = false" />
<Transition name="picker">
<div
class="fixed inset-0 z-40"
aria-hidden="true"
@click="showChatList = false"
/>
<div
class="absolute left-0 right-0 top-full z-50 mt-1 p-2 max-h-48 overflow-y-auto animate-fade-up-fast shadow-2xl rounded-xl"
v-if="showChatList && conversationList.length > 0"
class="fixed z-[9999] mt-1 p-2 max-h-48 overflow-y-auto animate-fade-up-fast shadow-2xl rounded-xl min-w-[200px]"
:class="isDark ? 'bg-[#161618] border border-white/10' : 'bg-white border border-black/10'"
:style="chatListDropdownStyle"
@click.stop
>
<p class="text-[10px] font-semibold uppercase tracking-wider mb-2 px-1"
@@ -59,8 +57,8 @@
{{ c.title || 'Untitled' }}
</button>
</div>
</div>
</Transition>
</Transition>
</Teleport>
</div>
</div>
@@ -125,11 +123,15 @@
</button>
</div>
<Transition name="picker">
<div
v-if="showModelPicker"
class="absolute left-0 right-0 top-full z-50 mx-3 mt-1 glass-card p-3 space-y-3 animate-fade-up-fast shadow-2xl"
>
<Teleport to="body">
<div v-if="showModelPicker" class="fixed inset-0 z-[9998]" aria-hidden="true" @click="showModelPicker = false" />
<Transition name="picker">
<div
v-if="showModelPicker"
class="fixed z-[9999] path-glass-card p-3 space-y-3 animate-fade-up-fast shadow-2xl min-w-[220px]"
:style="modelPickerDropdownStyle"
@click.stop
>
<div v-for="provider in availableProviders" :key="provider.id">
<p class="text-[10px] font-semibold uppercase tracking-wider mb-1.5 px-1"
:class="isDark ? 'text-white/40' : 'text-gray-400'">
@@ -152,12 +154,13 @@
</div>
</div>
</div>
</Transition>
</Transition>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import { useChatStore } from '@/stores/chat'
import { useAI } from '@/composables/useAI'
import { useTheme } from '@/composables/useTheme'
@@ -180,6 +183,43 @@ const { isDark, toggleTheme } = useTheme()
const chatStore = useChatStore()
const showModelPicker = ref(false)
const showChatList = ref(false)
const headerRef = ref<HTMLElement | null>(null)
const chatListTriggerRef = ref<HTMLElement | null>(null)
const modelPickerTriggerRef = ref<HTMLElement | null>(null)
const chatListDropdownStyle = ref<Record<string, string>>({})
const modelPickerDropdownStyle = ref<Record<string, string>>({})
function updateChatListPosition() {
nextTick(() => {
const el = chatListTriggerRef.value
if (el) {
const r = el.getBoundingClientRect()
chatListDropdownStyle.value = {
top: `${r.bottom + 4}px`,
left: `${r.left}px`,
width: `${Math.max(r.width, 200)}px`,
}
}
})
}
function updateModelPickerPosition() {
nextTick(() => {
const el = modelPickerTriggerRef.value
if (el) {
const r = el.getBoundingClientRect()
modelPickerDropdownStyle.value = {
top: `${r.bottom + 4}px`,
right: 'auto',
left: `${r.left}px`,
width: `${Math.max(r.width + 24, 220)}px`,
}
}
})
}
watch(showChatList, (v) => { if (v) updateChatListPosition() })
watch(showModelPicker, (v) => { if (v) updateModelPickerPosition() })
const conversationList = computed(() => chatStore.conversationList)
const activeConversationId = computed(() => chatStore.activeConversationId)
@@ -1,7 +1,7 @@
<template>
<div class="p-3 md:p-4">
<div
class="glass rounded-2xl px-4 py-3 flex items-end gap-3 transition-all duration-300"
class="path-glass-bubble rounded-2xl px-4 py-3 flex items-end gap-3 transition-all duration-300"
:class="focused
? isDark
? 'border-white/30'
@@ -24,7 +24,7 @@
/>
<button
:disabled="!canSend"
class="shrink-0 glass-button glass-button-sm rounded-xl px-3 transition-all duration-200"
class="shrink-0 path-glass-button path-glass-button-sm rounded-xl px-3 transition-all duration-200"
:class="canSend
? 'hover:opacity-80 active:scale-95'
: 'opacity-30 cursor-not-allowed'"
@@ -73,8 +73,8 @@ const isUser = computed(() => props.message.role === 'user')
const bubbleClasses = computed(() =>
isUser.value
? 'glass-strong rounded-br-md'
: 'glass-card rounded-bl-md'
? 'path-glass-bubble-user rounded-br-md rounded-2xl'
: 'path-glass-bubble rounded-bl-md rounded-2xl'
)
const inlineFilms = computed(() => {
@@ -12,12 +12,12 @@
<div
ref="messageListRef"
class="flex-1 overflow-y-auto scrollbar-hide p-4 space-y-3"
class="relative z-0 flex-1 min-h-0 overflow-y-auto scrollbar-hide p-4 space-y-3"
>
<div v-if="messages.length === 0" class="flex items-center justify-center h-full">
<div class="text-center space-y-3 animate-fade-up">
<div class="w-16 h-16 rounded-2xl glass flex items-center justify-center mx-auto">
<span class="text-2xl" :class="isDark ? 'text-[#fafafa]' : 'text-gray-800'"></span>
<div class="w-16 h-16 rounded-2xl path-glass-card flex items-center justify-center mx-auto overflow-hidden">
<img src="/icon.svg" alt="AIUI" class="w-10 h-10 object-contain" />
</div>
<p class="text-sm" :class="isDark ? 'text-white/30' : 'text-gray-400'">
Start a conversation
@@ -1,6 +1,6 @@
<template>
<div class="flex justify-start animate-fade-up-fast">
<div class="glass-card rounded-2xl rounded-bl-md px-4 py-3">
<div class="path-glass-card rounded-2xl rounded-bl-md px-4 py-3">
<div class="flex items-center gap-1.5">
<span
v-for="i in 3"