feat(chat): enhance chat functionality with song integration and UI improvements
- Updated ChatHeader and ChatMessage components to support song selection and display. - Enhanced useContentPanel to handle both film and song content, allowing for richer interactions. - Improved FilmCard and added SongCard components for better media representation. - Refactored environment variables for better configuration management. - Updated .gitignore to include development chat history. Made-with: Cursor
This commit is contained in:
@@ -1,33 +1,87 @@
|
||||
<template>
|
||||
<div
|
||||
class="glass-strong rounded-t-2xl flex items-center justify-between px-4 py-3 relative"
|
||||
:style="isDark
|
||||
? 'border-bottom: 1px solid rgba(255, 255, 255, 0.08); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22)'
|
||||
: 'border-bottom: 1px solid rgba(0, 0, 0, 0.06); box-shadow: inset 0 1px 0 rgba(254, 253, 249, 1)'"
|
||||
class="glass-strong rounded-t-2xl flex items-center justify-between px-4 py-3 relative z-20 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">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center shrink-0"
|
||||
:class="isDark ? 'bg-accent/20' : 'bg-accent/10'">
|
||||
<span class="text-accent text-sm font-bold">AI</span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<h2 class="text-sm font-semibold truncate"
|
||||
:class="isDark ? 'text-white/96' : 'text-gray-900'">{{ title }}</h2>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-[10px] truncate font-mono"
|
||||
:class="isDark ? 'text-white/40' : 'text-gray-400'">{{ conversationId }}</p>
|
||||
<div class="min-w-0 flex-1 relative">
|
||||
<button
|
||||
class="w-full text-left group/btn"
|
||||
:class="conversationList.length > 0 ? 'cursor-pointer' : ''"
|
||||
@click="conversationList.length > 0 && (showChatList = !showChatList)"
|
||||
>
|
||||
<h2 class="text-sm font-semibold truncate"
|
||||
:class="isDark ? 'text-white/96' : 'text-gray-900'">{{ title }}</h2>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-[10px] truncate font-mono"
|
||||
:class="isDark ? 'text-white/40' : 'text-gray-400'">{{ conversationId }}</p>
|
||||
<span class="text-[10px]" :class="isDark ? 'text-white/20' : 'text-gray-300'">·</span>
|
||||
<button
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors truncate"
|
||||
@click="showModelPicker = !showModelPicker"
|
||||
<button
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors truncate"
|
||||
@click.stop="showModelPicker = !showModelPicker; showChatList = false"
|
||||
>
|
||||
{{ modelDisplayName }}
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
<Transition name="picker">
|
||||
<div
|
||||
v-if="showChatList && conversationList.length > 0"
|
||||
class="contents"
|
||||
>
|
||||
{{ modelDisplayName }}
|
||||
</button>
|
||||
</div>
|
||||
<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"
|
||||
:class="isDark ? 'bg-[#161618] border border-white/10' : 'bg-white border border-black/10'"
|
||||
@click.stop
|
||||
>
|
||||
<p class="text-[10px] font-semibold uppercase tracking-wider mb-2 px-1"
|
||||
:class="isDark ? 'text-white/40' : 'text-gray-400'">Saved chats</p>
|
||||
<button
|
||||
v-for="c in conversationList"
|
||||
:key="c.id"
|
||||
class="w-full text-left px-3 py-2 rounded-lg text-xs transition-all"
|
||||
:class="c.id === activeConversationId
|
||||
? 'nav-tab-active'
|
||||
: isDark
|
||||
? 'text-white/60 hover:text-white hover:bg-white/10'
|
||||
: 'text-gray-500 hover:text-gray-900 hover:bg-black/5'"
|
||||
@click="selectChat(c.id)"
|
||||
>
|
||||
{{ c.title || 'Untitled' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
class="p-2 rounded-lg transition-colors"
|
||||
:class="isDark
|
||||
? 'text-white/70 hover:text-white hover:bg-white/10'
|
||||
: 'text-gray-500 hover:text-gray-900 hover:bg-black/5'"
|
||||
:title="isDark ? 'Light mode' : 'Dark mode'"
|
||||
aria-label="Toggle theme"
|
||||
@click="toggleTheme"
|
||||
>
|
||||
<svg v-if="isDark" 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="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg v-else 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="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="p-2 rounded-lg transition-colors"
|
||||
:class="isDark
|
||||
@@ -74,7 +128,7 @@
|
||||
<Transition name="picker">
|
||||
<div
|
||||
v-if="showModelPicker"
|
||||
class="absolute left-0 right-0 top-full z-20 mx-3 mt-1 glass-card p-3 space-y-3 animate-fade-up-fast"
|
||||
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"
|
||||
>
|
||||
<div v-for="provider in availableProviders" :key="provider.id">
|
||||
<p class="text-[10px] font-semibold uppercase tracking-wider mb-1.5 px-1"
|
||||
@@ -103,7 +157,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import { useAI } from '@/composables/useAI'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
|
||||
@@ -121,8 +176,22 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const { activeProvider, activeModel, availableProviders, setProvider, setModel } = useAI()
|
||||
const { isDark } = useTheme()
|
||||
const { isDark, toggleTheme } = useTheme()
|
||||
const chatStore = useChatStore()
|
||||
const showModelPicker = ref(false)
|
||||
const showChatList = ref(false)
|
||||
|
||||
const conversationList = computed(() => chatStore.conversationList)
|
||||
const activeConversationId = computed(() => chatStore.activeConversationId)
|
||||
|
||||
function selectChat(id: string) {
|
||||
chatStore.setActiveConversation(id)
|
||||
showChatList.value = false
|
||||
}
|
||||
|
||||
watch(() => chatStore.conversationList.length, () => {
|
||||
showChatList.value = false
|
||||
})
|
||||
|
||||
const modelDisplayName = computed(() => {
|
||||
for (const p of availableProviders.value) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p class="text-sm leading-relaxed whitespace-pre-wrap break-words"
|
||||
:class="isDark ? 'text-white/90' : 'text-gray-800'">{{ displayText }}</p>
|
||||
|
||||
<div v-if="inlineFilms.length > 0" class="mt-3 space-y-1">
|
||||
<div v-if="inlineFilms.length > 0" class="mt-3 space-y-1" @click.stop>
|
||||
<FilmCard
|
||||
v-for="film in inlineFilms"
|
||||
:key="film.id"
|
||||
@@ -21,16 +21,32 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="inlineSongs.length > 0" class="mt-3 space-y-1" @click.stop>
|
||||
<SongCard
|
||||
v-for="song in inlineSongs"
|
||||
:key="song.id"
|
||||
:song="song"
|
||||
@select="handleSongSelect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<span class="text-[10px] select-none"
|
||||
:class="isDark ? 'text-white/30' : 'text-gray-400'">{{ formattedTime }}</span>
|
||||
<button
|
||||
v-if="inlineFilms.length > 1"
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
|
||||
@click="openPanel"
|
||||
@click.stop="openPanel"
|
||||
>
|
||||
View all {{ inlineFilms.length }} films →
|
||||
</button>
|
||||
<button
|
||||
v-else-if="inlineSongs.length > 1"
|
||||
class="text-[10px] text-accent/70 hover:text-accent transition-colors"
|
||||
@click.stop="openPanel"
|
||||
>
|
||||
View all {{ inlineSongs.length }} songs →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,10 +55,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { Message } from '@aiui/core/types/message'
|
||||
import type { Film } from '@aiui/core/types/content'
|
||||
import type { Film, Song } from '@aiui/core/types/content'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { useContentPanel } from '@/composables/useContentPanel'
|
||||
import FilmCard from '@/components/content/FilmCard.vue'
|
||||
import SongCard from '@/components/content/SongCard.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
message: Message
|
||||
@@ -50,7 +67,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const { extractFilmIds, resolveFilms, stripFilmTags, updatePanelFromText, openFilmDetail } = useContentPanel()
|
||||
const { extractAllFilms, extractAllSongs, stripContentTags, updatePanelFromText, openFilmDetail, openSongDetail, closeFilmDetail, closeSongDetail } = useContentPanel()
|
||||
|
||||
const isUser = computed(() => props.message.role === 'user')
|
||||
|
||||
@@ -62,15 +79,19 @@ const bubbleClasses = computed(() =>
|
||||
|
||||
const inlineFilms = computed(() => {
|
||||
if (isUser.value) return []
|
||||
const ids = extractFilmIds(props.message.content)
|
||||
return resolveFilms(ids)
|
||||
return extractAllFilms(props.message.content)
|
||||
})
|
||||
|
||||
const hasContext = computed(() => !isUser.value && inlineFilms.value.length > 0)
|
||||
const inlineSongs = computed(() => {
|
||||
if (isUser.value) return []
|
||||
return extractAllSongs(props.message.content)
|
||||
})
|
||||
|
||||
const hasContext = computed(() => !isUser.value && (inlineFilms.value.length > 0 || inlineSongs.value.length > 0))
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (isUser.value) return props.message.content
|
||||
return stripFilmTags(props.message.content)
|
||||
return stripContentTags(props.message.content)
|
||||
})
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
@@ -80,10 +101,19 @@ const formattedTime = computed(() => {
|
||||
|
||||
function handleFilmSelect(film: Film) {
|
||||
updatePanelFromText(props.message.content)
|
||||
closeSongDetail()
|
||||
openFilmDetail(film)
|
||||
}
|
||||
|
||||
function handleSongSelect(song: Song) {
|
||||
updatePanelFromText(props.message.content)
|
||||
closeFilmDetail()
|
||||
openSongDetail(song)
|
||||
}
|
||||
|
||||
function openPanel() {
|
||||
closeFilmDetail()
|
||||
closeSongDetail()
|
||||
updatePanelFromText(props.message.content)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full rounded-2xl overflow-hidden transition-all duration-300">
|
||||
<div class="flex flex-col h-full rounded-2xl overflow-visible transition-all duration-300">
|
||||
<ChatHeader
|
||||
:title="title"
|
||||
:conversation-id="displayId"
|
||||
@@ -17,7 +17,7 @@
|
||||
<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">✦</span>
|
||||
<span class="text-2xl" :class="isDark ? 'text-[#fafafa]' : 'text-gray-800'">✦</span>
|
||||
</div>
|
||||
<p class="text-sm" :class="isDark ? 'text-white/30' : 'text-gray-400'">
|
||||
Start a conversation
|
||||
@@ -120,7 +120,7 @@ watch(
|
||||
const el = messageListRef.value
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
})
|
||||
if (content && !isStreaming.value) {
|
||||
if (content) {
|
||||
updatePanelFromText(content)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user